Commit 5203376b by Peter Holmberg Committed by GitHub

Inspect: Add error tab (#21565)

* add error tab

* conditional tabs

* feedback from review

* expose lastResult via function

* remove todo and weird char

* fixing overflow states and height of tabcontent

* style fixes

* more changes to scroll handling

* fixing null checks

* Change drawer content padding

* Add scroll in the story

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
parent 80a2dce9
......@@ -18,7 +18,7 @@ export interface LocationUpdate {
replace?: boolean;
}
export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[] | undefined;
export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[] | undefined | null;
export type UrlQueryMap = Record<string, UrlQueryValue>;
export interface LocationSrv {
......
......@@ -69,6 +69,7 @@ export const longContent = () => {
</div>
{state.isOpen && (
<Drawer
scrollableContent
title="Drawer with long content"
onClose={() => {
updateValue({ isOpen: !state.isOpen });
......
......@@ -2,6 +2,7 @@ import React, { CSSProperties, FC, ReactNode } from 'react';
import { GrafanaTheme } from '@grafana/data';
import RcDrawer from 'rc-drawer';
import { css } from 'emotion';
import CustomScrollbar from '../CustomScrollbar/CustomScrollbar';
import { stylesFactory, useTheme, selectThemeVariant } from '../../themes';
export interface Props {
......@@ -15,10 +16,13 @@ export interface Props {
/** Either a number in px or a string with unit postfix */
width?: number | string;
/** Set to true if the component rendered within in drawer content has its own scroll */
scrollableContent?: boolean;
onClose: () => void;
}
const getStyles = stylesFactory((theme: GrafanaTheme) => {
const getStyles = stylesFactory((theme: GrafanaTheme, scollableContent: boolean) => {
const closeButtonWidth = '50px';
const borderColor = selectThemeVariant(
{
......@@ -31,6 +35,9 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
drawer: css`
.drawer-content {
background-color: ${theme.colors.bodyBg};
display: flex;
flex-direction: column;
overflow: hidden;
}
`,
titleWrapper: css`
......@@ -41,8 +48,9 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
border-bottom: 1px solid ${borderColor};
padding: ${theme.spacing.sm} 0 ${theme.spacing.sm} ${theme.spacing.md};
background-color: ${theme.colors.bodyBg};
position: sticky;
top: 0;
z-index: 1;
flex-grow: 0;
`,
close: css`
cursor: pointer;
......@@ -54,7 +62,9 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
`,
content: css`
padding: ${theme.spacing.md};
height: 100%;
flex-grow: 1;
overflow: ${!scollableContent ? 'hidden' : 'auto'};
z-index: 0;
`,
};
});
......@@ -64,11 +74,12 @@ export const Drawer: FC<Props> = ({
inline = false,
onClose,
closeOnMaskClick = false,
scrollableContent = false,
title,
width = '40%',
}) => {
const theme = useTheme();
const drawerStyles = getStyles(theme);
const drawerStyles = getStyles(theme, scrollableContent);
return (
<RcDrawer
......@@ -89,7 +100,9 @@ export const Drawer: FC<Props> = ({
<i className="fa fa-close" />
</div>
</div>
<div className={drawerStyles.content}>{children}</div>
<div className={drawerStyles.content}>
{!scrollableContent ? children : <CustomScrollbar>{children}</CustomScrollbar>}
</div>
</RcDrawer>
);
};
import React, { FC, ReactNode } from 'react';
import React, { FC, HTMLAttributes, ReactNode } from 'react';
import { stylesFactory, useTheme } from '../../themes';
import { css } from 'emotion';
import { css, cx } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
interface Props {
interface Props extends HTMLAttributes<HTMLDivElement> {
children: ReactNode;
}
const getTabContentStyle = stylesFactory((theme: GrafanaTheme) => {
return {
tabContent: css`
padding: ${theme.spacing.xs};
height: 90%;
overflow: hidden;
padding: ${theme.spacing.sm};
`,
};
});
export const TabContent: FC<Props> = ({ children }) => {
export const TabContent: FC<Props> = ({ children, className, ...restProps }) => {
const theme = useTheme();
const styles = getTabContentStyle(theme);
return <div className={styles.tabContent}>{children}</div>;
return (
<div {...restProps} className={cx(styles.tabContent, className)}>
{children}
</div>
);
};
......@@ -4,26 +4,47 @@ import { saveAs } from 'file-saver';
import { css } from 'emotion';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { JSONFormatter, Drawer, Select, Table, TabsBar, Tab, TabContent, Forms, stylesFactory } from '@grafana/ui';
import {
JSONFormatter,
Drawer,
Select,
Table,
TabsBar,
Tab,
TabContent,
Forms,
stylesFactory,
CustomScrollbar,
} from '@grafana/ui';
import { getLocationSrv, getDataSourceSrv } from '@grafana/runtime';
import { DataFrame, DataSourceApi, SelectableValue, applyFieldOverrides, toCSV } from '@grafana/data';
import {
DataFrame,
DataSourceApi,
SelectableValue,
applyFieldOverrides,
toCSV,
DataQueryError,
PanelData,
} from '@grafana/data';
import { config } from 'app/core/config';
interface Props {
dashboard: DashboardModel;
panel: PanelModel;
selectedTab: InspectTab;
}
enum InspectTab {
export enum InspectTab {
Data = 'data',
Raw = 'raw',
Issue = 'issue',
Meta = 'meta', // When result metadata exists
Error = 'error',
}
interface State {
// The last raw response
last?: any;
last?: PanelData;
// Data frem the last response
data: DataFrame[];
......@@ -52,6 +73,15 @@ const getStyles = stylesFactory(() => {
downloadCsv: css`
margin-left: 16px;
`,
tabContent: css`
height: calc(100% - 32px);
`,
dataTabContent: css`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
`,
};
});
......@@ -61,7 +91,7 @@ export class PanelInspector extends PureComponent<Props, State> {
this.state = {
data: [],
selected: 0,
tab: InspectTab.Data,
tab: props.selectedTab || InspectTab.Data,
};
}
......@@ -72,8 +102,7 @@ export class PanelInspector extends PureComponent<Props, State> {
return;
}
// TODO? should we get the result with an observable once?
const lastResult = (panel.getQueryRunner() as any).lastResult;
const lastResult = panel.getQueryRunner().getLastResult();
if (!lastResult) {
this.onDismiss(); // Usually opened from refresh?
return;
......@@ -81,14 +110,16 @@ export class PanelInspector extends PureComponent<Props, State> {
// Find the first DataSource wanting to show custom metadata
let metaDS: DataSourceApi;
const data = lastResult?.series as DataFrame[];
const data = lastResult?.series;
const error = lastResult?.error;
if (data) {
for (const frame of data) {
const key = frame.meta?.datasource;
if (key) {
const ds = await getDataSourceSrv().get(key);
if (ds && ds.components.MetadataInspector) {
metaDS = ds;
const dataSource = await getDataSourceSrv().get(key);
if (dataSource && dataSource.components?.MetadataInspector) {
metaDS = dataSource;
break;
}
}
......@@ -96,16 +127,17 @@ export class PanelInspector extends PureComponent<Props, State> {
}
// Set last result, but no metadata inspector
this.setState({
this.setState(prevState => ({
last: lastResult,
data,
metaDS,
});
tab: error ? InspectTab.Error : prevState.tab,
}));
}
onDismiss = () => {
getLocationSrv().update({
query: { inspect: null },
query: { inspect: null, tab: null },
partial: true,
});
};
......@@ -133,12 +165,17 @@ export class PanelInspector extends PureComponent<Props, State> {
if (!metaDS || !metaDS.components?.MetadataInspector) {
return <div>No Metadata Inspector</div>;
}
return <metaDS.components.MetadataInspector datasource={metaDS} data={data} />;
return (
<CustomScrollbar>
<metaDS.components.MetadataInspector datasource={metaDS} data={data} />
</CustomScrollbar>
);
}
renderDataTab(width: number, height: number) {
renderDataTab() {
const { data, selected } = this.state;
const styles = getStyles();
if (!data || !data.length) {
return <div>No Data</div>;
}
......@@ -160,7 +197,7 @@ export class PanelInspector extends PureComponent<Props, State> {
});
return (
<div>
<div className={styles.dataTabContent}>
<div className={styles.toolbar}>
{choices.length > 1 && (
<div className={styles.dataFrameSelect}>
......@@ -177,63 +214,110 @@ export class PanelInspector extends PureComponent<Props, State> {
</Forms.Button>
</div>
</div>
<Table width={width} height={height} data={processed[selected]} />
<div style={{ flexGrow: 1 }}>
<AutoSizer>
{({ width, height }) => {
if (width === 0) {
return null;
}
return (
<div style={{ width, height }}>
<Table width={width} height={height} data={processed[selected]} />
</div>
);
}}
</AutoSizer>
</div>
</div>
);
}
renderIssueTab() {
return <div>TODO: show issue form</div>;
return <CustomScrollbar>TODO: show issue form</CustomScrollbar>;
}
renderErrorTab(error?: DataQueryError) {
if (!error) {
return null;
}
if (error.data) {
return (
<CustomScrollbar>
<h3>{error.data.message}</h3>
<pre>
<code>{error.data.error}</code>
</pre>
</CustomScrollbar>
);
}
return <div>{error.message}</div>;
}
renderRawJsonTab(last: PanelData) {
return (
<CustomScrollbar>
<JSONFormatter json={last} open={2} />
</CustomScrollbar>
);
}
render() {
const { panel } = this.props;
const { last, tab } = this.state;
const styles = getStyles();
const error = last?.error;
if (!panel) {
this.onDismiss(); // Try to close the component
return null;
}
const tabs = [
{ label: 'Data', value: InspectTab.Data },
{ label: 'Issue', value: InspectTab.Issue },
{ label: 'Raw JSON', value: InspectTab.Raw },
];
const tabs = [];
if (last && last?.series?.length > 0) {
tabs.push({ label: 'Data', value: InspectTab.Data });
}
if (this.state.metaDS) {
tabs.push({ label: 'Meta Data', value: InspectTab.Meta });
}
if (error && error.message) {
tabs.push({ label: 'Error', value: InspectTab.Error });
}
tabs.push({ label: 'Raw JSON', value: InspectTab.Raw });
return (
<Drawer title={panel.title} onClose={this.onDismiss}>
<TabsBar>
{tabs.map(t => {
return <Tab label={t.label} active={t.value === tab} onChangeTab={() => this.onSelectTab(t)} />;
{tabs.map((t, index) => {
return (
<Tab
key={`${t.value}-${index}`}
label={t.label}
active={t.value === tab}
onChangeTab={() => this.onSelectTab(t)}
/>
);
})}
</TabsBar>
<TabContent>
<AutoSizer>
{({ width, height }) => {
if (width === 0) {
return null;
}
return (
<div style={{ width }}>
{tab === InspectTab.Data && this.renderDataTab(width, height)}
{tab === InspectTab.Meta && this.renderMetadataInspector()}
{tab === InspectTab.Issue && this.renderIssueTab()}
{tab === InspectTab.Raw && (
<div>
<JSONFormatter json={last} open={2} />
</div>
)}
</div>
);
}}
</AutoSizer>
<TabContent className={styles.tabContent}>
{tab === InspectTab.Data ? (
this.renderDataTab()
) : (
<AutoSizer>
{({ width, height }) => {
if (width === 0) {
return null;
}
return (
<div style={{ width, height }}>
{tab === InspectTab.Meta && this.renderMetadataInspector()}
{tab === InspectTab.Issue && this.renderIssueTab()}
{tab === InspectTab.Raw && this.renderRawJsonTab(last)}
{tab === InspectTab.Error && this.renderErrorTab(error)}
</div>
);
}}
</AutoSizer>
)}
</TabContent>
</Drawer>
);
......
......@@ -29,7 +29,7 @@ import {
} from 'app/types';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { PanelInspector } from '../components/Inspector/PanelInspector';
import { InspectTab, PanelInspector } from '../components/Inspector/PanelInspector';
export interface Props {
urlUid?: string;
......@@ -53,6 +53,7 @@ export interface Props {
cleanUpDashboard: typeof cleanUpDashboard;
notifyApp: typeof notifyApp;
updateLocation: typeof updateLocation;
inspectTab?: InspectTab;
}
export interface State {
......@@ -252,7 +253,16 @@ export class DashboardPage extends PureComponent<Props, State> {
}
render() {
const { dashboard, editview, $injector, isInitSlow, initError, inspectPanelId, urlEditPanel } = this.props;
const {
dashboard,
editview,
$injector,
isInitSlow,
initError,
inspectPanelId,
urlEditPanel,
inspectTab,
} = this.props;
const { isSettingsOpening, isEditing, isFullscreen, scrollTop, updateScrollTop } = this.state;
if (!dashboard) {
......@@ -314,7 +324,7 @@ export class DashboardPage extends PureComponent<Props, State> {
</CustomScrollbar>
</div>
{inspectPanel && <PanelInspector dashboard={dashboard} panel={inspectPanel} />}
{inspectPanel && <PanelInspector dashboard={dashboard} panel={inspectPanel} selectedTab={inspectTab} />}
{editPanel && <PanelEditor dashboard={dashboard} panel={editPanel} />}
</div>
);
......@@ -336,6 +346,7 @@ export const mapStateToProps = (state: StoreState) => ({
isInitSlow: state.dashboard.isInitSlow,
initError: state.dashboard.initError,
dashboard: state.dashboard.model as DashboardModel,
inspectTab: state.location.query.tab,
});
const mapDispatchToProps = {
......
......@@ -7,6 +7,7 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import templateSrv from 'app/features/templating/template_srv';
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { getLocationSrv } from '@grafana/runtime';
import { InspectTab } from '../../components/Inspector/PanelInspector';
enum InfoMode {
Error = 'Error',
......@@ -73,7 +74,7 @@ export class PanelHeaderCorner extends Component<Props> {
* Open the Panel Inspector when we click on an error
*/
onClickError = () => {
getLocationSrv().update({ partial: true, query: { inspect: this.props.panel.id } });
getLocationSrv().update({ partial: true, query: { inspect: this.props.panel.id, tab: InspectTab.Error } });
};
renderCornerType(infoMode: InfoMode, content: PopoverContent, onClick?: () => void) {
......
......@@ -186,6 +186,10 @@ export class PanelQueryRunner {
this.subscription.unsubscribe();
}
}
getLastResult(): PanelData {
return this.lastResult;
}
}
async function getDataSource(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment