diff --git a/packages/grafana-runtime/src/services/LocationSrv.ts b/packages/grafana-runtime/src/services/LocationSrv.ts index 148ff186266..0f041feef0a 100644 --- a/packages/grafana-runtime/src/services/LocationSrv.ts +++ b/packages/grafana-runtime/src/services/LocationSrv.ts @@ -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; export interface LocationSrv { diff --git a/packages/grafana-ui/src/components/Drawer/Drawer.story.tsx b/packages/grafana-ui/src/components/Drawer/Drawer.story.tsx index e812c545594..1dbb7765561 100644 --- a/packages/grafana-ui/src/components/Drawer/Drawer.story.tsx +++ b/packages/grafana-ui/src/components/Drawer/Drawer.story.tsx @@ -69,6 +69,7 @@ export const longContent = () => { {state.isOpen && ( { updateValue({ isOpen: !state.isOpen }); diff --git a/packages/grafana-ui/src/components/Drawer/Drawer.tsx b/packages/grafana-ui/src/components/Drawer/Drawer.tsx index aeed8c30a95..7f867c948e0 100644 --- a/packages/grafana-ui/src/components/Drawer/Drawer.tsx +++ b/packages/grafana-ui/src/components/Drawer/Drawer.tsx @@ -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 = ({ inline = false, onClose, closeOnMaskClick = false, + scrollableContent = false, title, width = '40%', }) => { const theme = useTheme(); - const drawerStyles = getStyles(theme); + const drawerStyles = getStyles(theme, scrollableContent); return ( = ({ -
{children}
+
+ {!scrollableContent ? children : {children}} +
); }; diff --git a/packages/grafana-ui/src/components/Tabs/TabContent.tsx b/packages/grafana-ui/src/components/Tabs/TabContent.tsx index 35aeb351f7f..e45a673a5d1 100644 --- a/packages/grafana-ui/src/components/Tabs/TabContent.tsx +++ b/packages/grafana-ui/src/components/Tabs/TabContent.tsx @@ -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 { 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 = ({ children }) => { +export const TabContent: FC = ({ children, className, ...restProps }) => { const theme = useTheme(); const styles = getTabContentStyle(theme); - return
{children}
; + return ( +
+ {children} +
+ ); }; diff --git a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx index 3eba34db81f..9534e4f7321 100644 --- a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx +++ b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx @@ -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 { this.state = { data: [], selected: 0, - tab: InspectTab.Data, + tab: props.selectedTab || InspectTab.Data, }; } @@ -72,8 +102,7 @@ export class PanelInspector extends PureComponent { 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 { // 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 { } // 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 { if (!metaDS || !metaDS.components?.MetadataInspector) { return
No Metadata Inspector
; } - return ; + return ( + + + + ); } - renderDataTab(width: number, height: number) { + renderDataTab() { const { data, selected } = this.state; const styles = getStyles(); + if (!data || !data.length) { return
No Data
; } @@ -160,7 +197,7 @@ export class PanelInspector extends PureComponent { }); return ( -
+
{choices.length > 1 && (
@@ -177,63 +214,110 @@ export class PanelInspector extends PureComponent {
- +
+ + {({ width, height }) => { + if (width === 0) { + return null; + } + return ( +
+
+ + ); + }} + + ); } renderIssueTab() { - return
TODO: show issue form
; + return TODO: show issue form; + } + + renderErrorTab(error?: DataQueryError) { + if (!error) { + return null; + } + if (error.data) { + return ( + +

{error.data.message}

+
+            {error.data.error}
+          
+
+ ); + } + return
{error.message}
; + } + + renderRawJsonTab(last: PanelData) { + return ( + + + + ); } 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 ( - {tabs.map(t => { - return this.onSelectTab(t)} />; + {tabs.map((t, index) => { + return ( + this.onSelectTab(t)} + /> + ); })} - - - {({ width, height }) => { - if (width === 0) { - return null; - } - - return ( -
- {tab === InspectTab.Data && this.renderDataTab(width, height)} - - {tab === InspectTab.Meta && this.renderMetadataInspector()} - - {tab === InspectTab.Issue && this.renderIssueTab()} - - {tab === InspectTab.Raw && ( -
- -
- )} -
- ); - }} -
+ + {tab === InspectTab.Data ? ( + this.renderDataTab() + ) : ( + + {({ width, height }) => { + if (width === 0) { + return null; + } + return ( +
+ {tab === InspectTab.Meta && this.renderMetadataInspector()} + {tab === InspectTab.Issue && this.renderIssueTab()} + {tab === InspectTab.Raw && this.renderRawJsonTab(last)} + {tab === InspectTab.Error && this.renderErrorTab(error)} +
+ ); + }} +
+ )}
); diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 40c92dd8b27..9e592f13018 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -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 { } 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 { - {inspectPanel && } + {inspectPanel && } {editPanel && } ); @@ -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 = { diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx index f0e6b6d8b27..5d6192a9d5f 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx @@ -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 { * 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) { diff --git a/public/app/features/dashboard/state/PanelQueryRunner.ts b/public/app/features/dashboard/state/PanelQueryRunner.ts index efb47c20039..48d2bfe63a6 100644 --- a/public/app/features/dashboard/state/PanelQueryRunner.ts +++ b/public/app/features/dashboard/state/PanelQueryRunner.ts @@ -186,6 +186,10 @@ export class PanelQueryRunner { this.subscription.unsubscribe(); } } + + getLastResult(): PanelData { + return this.lastResult; + } } async function getDataSource(