mirror of
https://github.com/grafana/grafana.git
synced 2025-01-17 04:02:50 -06:00
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>
This commit is contained in:
parent
80a2dce994
commit
5203376bc5
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -1,25 +1,27 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
renderIssueTab() {
|
||||
return <div>TODO: show issue form</div>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { panel } = this.props;
|
||||
const { last, tab } = this.state;
|
||||
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 },
|
||||
];
|
||||
if (this.state.metaDS) {
|
||||
tabs.push({ label: 'Meta Data', value: InspectTab.Meta });
|
||||
}
|
||||
|
||||
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)} />;
|
||||
})}
|
||||
</TabsBar>
|
||||
<TabContent>
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
<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 style={{ width, height }}>
|
||||
<Table width={width} height={height} data={processed[selected]} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderIssueTab() {
|
||||
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 = [];
|
||||
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, index) => {
|
||||
return (
|
||||
<Tab
|
||||
key={`${t.value}-${index}`}
|
||||
label={t.label}
|
||||
active={t.value === tab}
|
||||
onChangeTab={() => this.onSelectTab(t)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TabsBar>
|
||||
<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(
|
||||
|
Loading…
Reference in New Issue
Block a user