mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Support full inspect drawer (#32005)
* Move InspectSubtitle to grafana/ui * Move inspect elements to features/inspector * Move InspectJSON and use it also in Explore * Move and use QueryInspector * Move all to features/inspector * WIP Data tab implementation * Process dataframes for explores inspector * Clean up * Update test * Clean up * Fix Explore Inspector button name
This commit is contained in:
parent
a6cb9fb295
commit
664b13d83e
@ -27,6 +27,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
tabContent: css`
|
tabContent: css`
|
||||||
padding: ${theme.spacing.md};
|
padding: ${theme.spacing.md};
|
||||||
background-color: ${theme.colors.bodyBg};
|
background-color: ${theme.colors.bodyBg};
|
||||||
|
height: 100%;
|
||||||
`,
|
`,
|
||||||
close: css`
|
close: css`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -24,6 +24,7 @@ export type IconName =
|
|||||||
| 'bolt'
|
| 'bolt'
|
||||||
| 'book-open'
|
| 'book-open'
|
||||||
| 'book'
|
| 'book'
|
||||||
|
| 'brackets-curly'
|
||||||
| 'bug'
|
| 'bug'
|
||||||
| 'calculator-alt'
|
| 'calculator-alt'
|
||||||
| 'calendar-alt'
|
| 'calendar-alt'
|
||||||
@ -157,6 +158,7 @@ export const getAvailableIcons = (): IconName[] => [
|
|||||||
'bolt',
|
'bolt',
|
||||||
'book-open',
|
'book-open',
|
||||||
'book',
|
'book',
|
||||||
|
'brackets-curly',
|
||||||
'bug',
|
'bug',
|
||||||
'calculator-alt',
|
'calculator-alt',
|
||||||
'calendar-alt',
|
'calendar-alt',
|
||||||
|
@ -2,15 +2,15 @@ import React, { useState } from 'react';
|
|||||||
import { DataSourceApi, PanelData, PanelPlugin } from '@grafana/data';
|
import { DataSourceApi, PanelData, PanelPlugin } from '@grafana/data';
|
||||||
import { getTemplateSrv } from '@grafana/runtime';
|
import { getTemplateSrv } from '@grafana/runtime';
|
||||||
import { CustomScrollbar, Drawer, TabContent } from '@grafana/ui';
|
import { CustomScrollbar, Drawer, TabContent } from '@grafana/ui';
|
||||||
import { getPanelInspectorStyles } from './styles';
|
import { getPanelInspectorStyles } from 'app/features/inspector/styles';
|
||||||
import { InspectSubtitle } from './InspectSubtitle';
|
import { InspectMetadataTab } from 'app/features/inspector/InspectMetadataTab';
|
||||||
import { InspectDataTab } from './InspectDataTab';
|
import { InspectSubtitle } from 'app/features/inspector/InspectSubtitle';
|
||||||
import { InspectMetadataTab } from './InspectMetadataTab';
|
import { InspectJSONTab } from 'app/features/inspector/InspectJSONTab';
|
||||||
import { InspectJSONTab } from './InspectJSONTab';
|
import { QueryInspector } from 'app/features/inspector/QueryInspector';
|
||||||
import { InspectErrorTab } from './InspectErrorTab';
|
import { InspectStatsTab } from 'app/features/inspector/InspectStatsTab';
|
||||||
import { InspectStatsTab } from './InspectStatsTab';
|
import { InspectErrorTab } from 'app/features/inspector/InspectErrorTab';
|
||||||
import { QueryInspector } from './QueryInspector';
|
import { InspectDataTab } from 'app/features/inspector/InspectDataTab';
|
||||||
import { InspectTab } from './types';
|
import { InspectTab } from 'app/features/inspector/types';
|
||||||
import { DashboardModel, PanelModel } from '../../state';
|
import { DashboardModel, PanelModel } from '../../state';
|
||||||
import { GetDataOptions } from '../../../query/state/PanelQueryRunner';
|
import { GetDataOptions } from '../../../query/state/PanelQueryRunner';
|
||||||
|
|
||||||
@ -94,7 +94,9 @@ export const InspectContent: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
{activeTab === InspectTab.Error && <InspectErrorTab error={error} />}
|
{activeTab === InspectTab.Error && <InspectErrorTab error={error} />}
|
||||||
{data && activeTab === InspectTab.Stats && <InspectStatsTab data={data} timeZone={dashboard.getTimezone()} />}
|
{data && activeTab === InspectTab.Stats && <InspectStatsTab data={data} timeZone={dashboard.getTimezone()} />}
|
||||||
{data && activeTab === InspectTab.Query && <QueryInspector panel={panel} data={data.series} />}
|
{data && activeTab === InspectTab.Query && (
|
||||||
|
<QueryInspector panel={panel} data={data.series} onRefreshQuery={() => panel.refresh()} />
|
||||||
|
)}
|
||||||
</TabContent>
|
</TabContent>
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
@ -10,7 +10,7 @@ import { usePanelLatestData } from '../PanelEditor/usePanelLatestData';
|
|||||||
import { InspectContent } from './InspectContent';
|
import { InspectContent } from './InspectContent';
|
||||||
import { useDatasourceMetadata, useInspectTabs } from './hooks';
|
import { useDatasourceMetadata, useInspectTabs } from './hooks';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { InspectTab } from './types';
|
import { InspectTab } from 'app/features/inspector/types';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
|
@ -4,7 +4,7 @@ import { getDataSourceSrv } from '@grafana/runtime';
|
|||||||
import { DashboardModel } from 'app/features/dashboard/state';
|
import { DashboardModel } from 'app/features/dashboard/state';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { supportsDataQuery } from '../PanelEditor/utils';
|
import { supportsDataQuery } from '../PanelEditor/utils';
|
||||||
import { InspectTab } from './types';
|
import { InspectTab } from 'app/features/inspector/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given PanelData return first data source supporting metadata inspector
|
* Given PanelData return first data source supporting metadata inspector
|
||||||
|
@ -6,7 +6,7 @@ import { getLocationSrv, getTemplateSrv } from '@grafana/runtime';
|
|||||||
|
|
||||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { InspectTab } from '../../components/Inspector/types';
|
import { InspectTab } from 'app/features/inspector/types';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
|
||||||
enum InfoMode {
|
enum InfoMode {
|
||||||
|
@ -7,18 +7,18 @@ import { ExploreQueryInspector } from './ExploreQueryInspector';
|
|||||||
|
|
||||||
type ExploreQueryInspectorProps = ComponentProps<typeof ExploreQueryInspector>;
|
type ExploreQueryInspectorProps = ComponentProps<typeof ExploreQueryInspector>;
|
||||||
|
|
||||||
jest.mock('../dashboard/components/Inspector/styles', () => ({
|
jest.mock('../inspector/styles', () => ({
|
||||||
getPanelInspectorStyles: () => ({}),
|
getPanelInspectorStyles: () => ({}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('app/core/services/backend_srv', () => ({
|
jest.mock('app/core/services/backend_srv', () => ({
|
||||||
getBackendSrv: () => ({
|
backendSrv: {
|
||||||
getInspectorStream: () =>
|
getInspectorStream: () =>
|
||||||
new Observable((subscriber) => {
|
new Observable((subscriber) => {
|
||||||
subscriber.next(response());
|
subscriber.next(response());
|
||||||
subscriber.next(response(true));
|
subscriber.next(response(true));
|
||||||
}) as any,
|
}) as any,
|
||||||
}),
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('app/core/services/context_srv', () => ({
|
jest.mock('app/core/services/context_srv', () => ({
|
||||||
@ -38,6 +38,7 @@ const setup = () => {
|
|||||||
series: [],
|
series: [],
|
||||||
timeRange: {} as TimeRange,
|
timeRange: {} as TimeRange,
|
||||||
},
|
},
|
||||||
|
runQueries: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return render(<ExploreQueryInspector {...props} />);
|
return render(<ExploreQueryInspector {...props} />);
|
||||||
|
@ -1,65 +1,20 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { Button, JSONFormatter, LoadingPlaceholder, TabbedContainer, TabConfig } from '@grafana/ui';
|
import { TabbedContainer, TabConfig } from '@grafana/ui';
|
||||||
import { PanelData, TimeZone } from '@grafana/data';
|
import { PanelData, TimeZone } from '@grafana/data';
|
||||||
|
import { runQueries } from './state/query';
|
||||||
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
|
|
||||||
import { StoreState, ExploreItemState, ExploreId } from 'app/types';
|
import { StoreState, ExploreItemState, ExploreId } from 'app/types';
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { ExploreDrawer } from 'app/features/explore/ExploreDrawer';
|
import { ExploreDrawer } from 'app/features/explore/ExploreDrawer';
|
||||||
import { useEffectOnce } from 'react-use';
|
import { InspectJSONTab } from 'app/features/inspector/InspectJSONTab';
|
||||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
import { QueryInspector } from 'app/features/inspector/QueryInspector';
|
||||||
import { InspectStatsTab } from '../dashboard/components/Inspector/InspectStatsTab';
|
import { InspectStatsTab } from 'app/features/inspector/InspectStatsTab';
|
||||||
import { getPanelInspectorStyles } from '../dashboard/components/Inspector/styles';
|
import { InspectDataTab } from 'app/features/inspector/InspectDataTab';
|
||||||
import { dispatch } from 'app/store/store';
|
|
||||||
import { notifyApp } from 'app/core/actions';
|
|
||||||
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
|
||||||
|
|
||||||
function stripPropsFromResponse(response: any) {
|
interface DispatchProps {
|
||||||
// ignore silent requests and return early
|
runQueries: typeof runQueries;
|
||||||
if (response.config?.hideFromInspector) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const clonedResponse = { ...response }; // clone - dont modify the response
|
|
||||||
|
|
||||||
if (clonedResponse.headers) {
|
|
||||||
delete clonedResponse.headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clonedResponse.config) {
|
|
||||||
clonedResponse.request = clonedResponse.config;
|
|
||||||
|
|
||||||
delete clonedResponse.config;
|
|
||||||
delete clonedResponse.request.transformRequest;
|
|
||||||
delete clonedResponse.request.transformResponse;
|
|
||||||
delete clonedResponse.request.paramSerializer;
|
|
||||||
delete clonedResponse.request.jsonpCallbackParam;
|
|
||||||
delete clonedResponse.request.headers;
|
|
||||||
delete clonedResponse.request.requestId;
|
|
||||||
delete clonedResponse.request.inspect;
|
|
||||||
delete clonedResponse.request.retry;
|
|
||||||
delete clonedResponse.request.timeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clonedResponse.data) {
|
|
||||||
clonedResponse.response = clonedResponse.data;
|
|
||||||
|
|
||||||
delete clonedResponse.config;
|
|
||||||
delete clonedResponse.data;
|
|
||||||
delete clonedResponse.status;
|
|
||||||
delete clonedResponse.statusText;
|
|
||||||
delete clonedResponse.ok;
|
|
||||||
delete clonedResponse.url;
|
|
||||||
delete clonedResponse.redirected;
|
|
||||||
delete clonedResponse.type;
|
|
||||||
delete clonedResponse.$$config;
|
|
||||||
}
|
|
||||||
|
|
||||||
return clonedResponse;
|
|
||||||
}
|
}
|
||||||
|
interface Props extends DispatchProps {
|
||||||
interface Props {
|
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
@ -68,50 +23,9 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ExploreQueryInspector(props: Props) {
|
export function ExploreQueryInspector(props: Props) {
|
||||||
const [formattedJSON, setFormattedJSON] = useState({});
|
|
||||||
|
|
||||||
const getTextForClipboard = () => {
|
|
||||||
return JSON.stringify(formattedJSON, null, 2);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClipboardSuccess = () => {
|
|
||||||
dispatch(notifyApp(createSuccessNotification('Content copied to clipboard')));
|
|
||||||
};
|
|
||||||
|
|
||||||
const [allNodesExpanded, setAllNodesExpanded] = useState(false);
|
|
||||||
const getOpenNodeCount = () => {
|
|
||||||
if (allNodesExpanded === null) {
|
|
||||||
return 3; // 3 is default, ie when state is null
|
|
||||||
} else if (allNodesExpanded) {
|
|
||||||
return 20;
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onToggleExpand = () => {
|
|
||||||
setAllNodesExpanded(!allNodesExpanded);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { loading, width, onClose, queryResponse } = props;
|
const { loading, width, onClose, queryResponse } = props;
|
||||||
|
const dataFrames = queryResponse?.series || [];
|
||||||
|
|
||||||
const [response, setResponse] = useState<PanelData>({} as PanelData);
|
|
||||||
useEffectOnce(() => {
|
|
||||||
const inspectorStreamSub = getBackendSrv()
|
|
||||||
.getInspectorStream()
|
|
||||||
.subscribe((resp) => {
|
|
||||||
const strippedResponse = stripPropsFromResponse(resp);
|
|
||||||
if (strippedResponse) {
|
|
||||||
setResponse(strippedResponse);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
inspectorStreamSub?.unsubscribe();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const haveData = response && Object.keys(response).length > 0;
|
|
||||||
const styles = getPanelInspectorStyles();
|
|
||||||
const statsTab: TabConfig = {
|
const statsTab: TabConfig = {
|
||||||
label: 'Stats',
|
label: 'Stats',
|
||||||
value: 'stats',
|
value: 'stats',
|
||||||
@ -119,52 +33,34 @@ export function ExploreQueryInspector(props: Props) {
|
|||||||
content: <InspectStatsTab data={queryResponse!} timeZone={queryResponse?.request?.timezone as TimeZone} />,
|
content: <InspectStatsTab data={queryResponse!} timeZone={queryResponse?.request?.timezone as TimeZone} />,
|
||||||
};
|
};
|
||||||
|
|
||||||
const inspectorTab: TabConfig = {
|
const jsonTab: TabConfig = {
|
||||||
label: 'Query Inspector',
|
label: 'JSON',
|
||||||
value: 'query_inspector',
|
value: 'json',
|
||||||
icon: 'info-circle',
|
icon: 'brackets-curly',
|
||||||
content: (
|
content: <InspectJSONTab data={queryResponse} onClose={onClose} />,
|
||||||
<>
|
};
|
||||||
<div className={styles.toolbar}>
|
|
||||||
{haveData && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
icon={allNodesExpanded ? 'minus' : 'plus'}
|
|
||||||
variant="secondary"
|
|
||||||
className={styles.toolbarItem}
|
|
||||||
onClick={onToggleExpand}
|
|
||||||
>
|
|
||||||
{allNodesExpanded ? 'Collapse' : 'Expand'} all
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<CopyToClipboard
|
const dataTab: TabConfig = {
|
||||||
text={getTextForClipboard}
|
label: 'Data',
|
||||||
onSuccess={onClipboardSuccess}
|
value: 'data',
|
||||||
elType="div"
|
icon: 'database',
|
||||||
className={styles.toolbarItem}
|
content: (
|
||||||
>
|
<InspectDataTab
|
||||||
<Button icon="copy" variant="secondary">
|
data={dataFrames}
|
||||||
Copy to clipboard
|
isLoading={loading}
|
||||||
</Button>
|
options={{ withTransforms: false, withFieldConfig: false }}
|
||||||
</CopyToClipboard>
|
/>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className="flex-grow-1" />
|
|
||||||
</div>
|
|
||||||
<div className={styles.contentQueryInspector}>
|
|
||||||
{loading && <LoadingPlaceholder text="Loading query inspector..." />}
|
|
||||||
{!loading && haveData && (
|
|
||||||
<JSONFormatter json={response!} open={getOpenNodeCount()} onDidRender={setFormattedJSON} />
|
|
||||||
)}
|
|
||||||
{!loading && !haveData && (
|
|
||||||
<p className="muted">No request & response collected yet. Run query to collect request & response.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs = [statsTab, inspectorTab];
|
const queryInspectorTab: TabConfig = {
|
||||||
|
label: 'Query Inspector',
|
||||||
|
value: 'query_inspector',
|
||||||
|
icon: 'info-circle',
|
||||||
|
content: <QueryInspector data={dataFrames} onRefreshQuery={() => props.runQueries(props.exploreId)} />,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabs = [statsTab, queryInspectorTab, jsonTab, dataTab];
|
||||||
return (
|
return (
|
||||||
<ExploreDrawer width={width} onResize={() => {}}>
|
<ExploreDrawer width={width} onResize={() => {}}>
|
||||||
<TabbedContainer tabs={tabs} onClose={onClose} closeIconTooltip="Close query inspector" />
|
<TabbedContainer tabs={tabs} onClose={onClose} closeIconTooltip="Close query inspector" />
|
||||||
@ -183,4 +79,8 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreI
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps)(ExploreQueryInspector));
|
const mapDispatchToProps: DispatchProps = {
|
||||||
|
runQueries,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(ExploreQueryInspector));
|
||||||
|
@ -54,7 +54,7 @@ export function SecondaryActions(props: Props) {
|
|||||||
onClick={props.onClickQueryInspectorButton}
|
onClick={props.onClickQueryInspectorButton}
|
||||||
icon="info-circle"
|
icon="info-circle"
|
||||||
>
|
>
|
||||||
Query inspector
|
Inspector
|
||||||
</Button>
|
</Button>
|
||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,17 +19,17 @@ import { getPanelInspectorStyles } from './styles';
|
|||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { GetDataOptions } from '../../../query/state/PanelQueryRunner';
|
import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner';
|
||||||
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
|
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
|
||||||
import { PanelModel } from 'app/features/dashboard/state';
|
import { PanelModel } from 'app/features/dashboard/state';
|
||||||
import { DetailText } from './DetailText';
|
import { DetailText } from 'app/features/inspector/DetailText';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
panel: PanelModel;
|
|
||||||
data?: DataFrame[];
|
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
options: GetDataOptions;
|
options: GetDataOptions;
|
||||||
onOptionsChange: (options: GetDataOptions) => void;
|
data?: DataFrame[];
|
||||||
|
panel?: PanelModel;
|
||||||
|
onOptionsChange?: (options: GetDataOptions) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@ -93,8 +93,9 @@ export class InspectDataTab extends PureComponent<Props, State> {
|
|||||||
const blob = new Blob([String.fromCharCode(0xfeff), dataFrameCsv], {
|
const blob = new Blob([String.fromCharCode(0xfeff), dataFrameCsv], {
|
||||||
type: 'text/csv;charset=utf-8',
|
type: 'text/csv;charset=utf-8',
|
||||||
});
|
});
|
||||||
|
const displayTitle = panel ? panel.getDisplayTitle() : 'Explore';
|
||||||
const transformation = transformId !== DataTransformerID.noop ? '-as-' + transformId.toLocaleLowerCase() : '';
|
const transformation = transformId !== DataTransformerID.noop ? '-as-' + transformId.toLocaleLowerCase() : '';
|
||||||
const fileName = `${panel.getDisplayTitle()}-data${transformation}-${dateTimeFormat(new Date())}.csv`;
|
const fileName = `${displayTitle}-data${transformation}-${dateTimeFormat(new Date())}.csv`;
|
||||||
saveAs(blob, fileName);
|
saveAs(blob, fileName);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -108,10 +109,10 @@ export class InspectDataTab extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getProcessedData(): DataFrame[] {
|
getProcessedData(): DataFrame[] {
|
||||||
const { options } = this.props;
|
const { options, panel } = this.props;
|
||||||
const data = this.state.transformedData;
|
const data = this.state.transformedData;
|
||||||
|
|
||||||
if (!options.withFieldConfig) {
|
if (!options.withFieldConfig || !panel) {
|
||||||
return applyRawFieldOverrides(data);
|
return applyRawFieldOverrides(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +121,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
|
|||||||
return applyFieldOverrides({
|
return applyFieldOverrides({
|
||||||
data,
|
data,
|
||||||
theme: config.theme,
|
theme: config.theme,
|
||||||
fieldConfig: this.props.panel.fieldConfig,
|
fieldConfig: panel.fieldConfig,
|
||||||
replaceVariables: (value: string) => {
|
replaceVariables: (value: string) => {
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
@ -166,6 +167,9 @@ export class InspectDataTab extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
renderDataOptions(dataFrames: DataFrame[]) {
|
renderDataOptions(dataFrames: DataFrame[]) {
|
||||||
const { options, onOptionsChange, panel, data } = this.props;
|
const { options, onOptionsChange, panel, data } = this.props;
|
||||||
|
if (!panel || !onOptionsChange) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const { transformId, transformationOptions, selectedDataFrame } = this.state;
|
const { transformId, transformationOptions, selectedDataFrame } = this.state;
|
||||||
|
|
||||||
const styles = getPanelInspectorStyles();
|
const styles = getPanelInspectorStyles();
|
@ -5,12 +5,12 @@ import { Button, CodeEditor, Field, Select } from '@grafana/ui';
|
|||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { appEvents } from 'app/core/core';
|
import { appEvents } from 'app/core/core';
|
||||||
import { DashboardModel, PanelModel } from '../../state';
|
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||||
import { getPanelInspectorStyles } from './styles';
|
import { getPanelInspectorStyles } from '../inspector/styles';
|
||||||
|
|
||||||
enum ShowContent {
|
enum ShowContent {
|
||||||
PanelJSON = 'panel',
|
PanelJSON = 'panel',
|
||||||
PanelData = 'data',
|
DataJSON = 'data',
|
||||||
DataStructure = 'structure',
|
DataStructure = 'structure',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,9 +21,9 @@ const options: Array<SelectableValue<ShowContent>> = [
|
|||||||
value: ShowContent.PanelJSON,
|
value: ShowContent.PanelJSON,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Panel data',
|
label: 'Data',
|
||||||
description: 'The raw model passed to the panel visualization',
|
description: 'The raw model passed to the panel visualization',
|
||||||
value: ShowContent.PanelData,
|
value: ShowContent.DataJSON,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'DataFrame structure',
|
label: 'DataFrame structure',
|
||||||
@ -33,10 +33,10 @@ const options: Array<SelectableValue<ShowContent>> = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dashboard: DashboardModel;
|
|
||||||
panel: PanelModel;
|
|
||||||
data?: PanelData;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
dashboard?: DashboardModel;
|
||||||
|
panel?: PanelModel;
|
||||||
|
data?: PanelData;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@ -45,11 +45,15 @@ interface State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class InspectJSONTab extends PureComponent<Props, State> {
|
export class InspectJSONTab extends PureComponent<Props, State> {
|
||||||
|
hasPanelJSON: boolean;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.hasPanelJSON = !!(props.panel && props.dashboard);
|
||||||
|
// If we are in panel, we want to show PanelJSON, otherwise show DataJSON
|
||||||
this.state = {
|
this.state = {
|
||||||
show: ShowContent.PanelJSON,
|
show: this.hasPanelJSON ? ShowContent.PanelJSON : ShowContent.DataJSON,
|
||||||
text: getPrettyJSON(props.panel.getSaveModel()),
|
text: this.hasPanelJSON ? getPrettyJSON(props.panel!.getSaveModel()) : getPrettyJSON(props.data),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,16 +69,17 @@ export class InspectJSONTab extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getJSONObject(show: ShowContent) {
|
getJSONObject(show: ShowContent) {
|
||||||
if (show === ShowContent.PanelData) {
|
const { data, panel } = this.props;
|
||||||
return this.props.data;
|
if (show === ShowContent.DataJSON) {
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (show === ShowContent.DataStructure) {
|
if (show === ShowContent.DataStructure) {
|
||||||
const series = this.props.data?.series;
|
const series = data?.series;
|
||||||
if (!series) {
|
if (!series) {
|
||||||
return { note: 'Missing Response Data' };
|
return { note: 'Missing Response Data' };
|
||||||
}
|
}
|
||||||
return this.props.data!.series.map((frame) => {
|
return data!.series.map((frame) => {
|
||||||
const { table, fields, ...rest } = frame as any; // remove 'table' from arrow response
|
const { table, fields, ...rest } = frame as any; // remove 'table' from arrow response
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
@ -85,8 +90,8 @@ export class InspectJSONTab extends PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (show === ShowContent.PanelJSON) {
|
if (this.hasPanelJSON && show === ShowContent.PanelJSON) {
|
||||||
return this.props.panel.getSaveModel();
|
return panel!.getSaveModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
return { note: `Unknown Object: ${show}` };
|
return { note: `Unknown Object: ${show}` };
|
||||||
@ -94,39 +99,41 @@ export class InspectJSONTab extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
onApplyPanelModel = () => {
|
onApplyPanelModel = () => {
|
||||||
const { panel, dashboard, onClose } = this.props;
|
const { panel, dashboard, onClose } = this.props;
|
||||||
|
if (this.hasPanelJSON) {
|
||||||
try {
|
try {
|
||||||
if (!dashboard.meta.canEdit) {
|
if (!dashboard!.meta.canEdit) {
|
||||||
appEvents.emit(AppEvents.alertError, ['Unable to apply']);
|
appEvents.emit(AppEvents.alertError, ['Unable to apply']);
|
||||||
} else {
|
} else {
|
||||||
const updates = JSON.parse(this.state.text);
|
const updates = JSON.parse(this.state.text);
|
||||||
panel.restoreModel(updates);
|
panel!.restoreModel(updates);
|
||||||
panel.refresh();
|
panel!.refresh();
|
||||||
appEvents.emit(AppEvents.alertSuccess, ['Panel model updated']);
|
appEvents.emit(AppEvents.alertSuccess, ['Panel model updated']);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error applying updates', err);
|
||||||
|
appEvents.emit(AppEvents.alertError, ['Invalid JSON text']);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error('Error applying updates', err);
|
|
||||||
appEvents.emit(AppEvents.alertError, ['Invalid JSON text']);
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { dashboard } = this.props;
|
const { dashboard } = this.props;
|
||||||
const { show, text } = this.state;
|
const { show, text } = this.state;
|
||||||
|
const jsonOptions = this.hasPanelJSON ? options : options.slice(1, options.length);
|
||||||
const selected = options.find((v) => v.value === show);
|
const selected = options.find((v) => v.value === show);
|
||||||
const isPanelJSON = show === ShowContent.PanelJSON;
|
const isPanelJSON = show === ShowContent.PanelJSON;
|
||||||
const canEdit = dashboard.meta.canEdit;
|
const canEdit = dashboard && dashboard.meta.canEdit;
|
||||||
const styles = getPanelInspectorStyles();
|
const styles = getPanelInspectorStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.toolbar} aria-label={selectors.components.PanelInspector.Json.content}>
|
<div className={styles.toolbar} aria-label={selectors.components.PanelInspector.Json.content}>
|
||||||
<Field label="Select source" className="flex-grow-1">
|
<Field label="Select source" className="flex-grow-1">
|
||||||
<Select options={options} value={selected} onChange={this.onSelectChanged} />
|
<Select options={jsonOptions} value={selected} onChange={this.onSelectChanged} />
|
||||||
</Field>
|
</Field>
|
||||||
{isPanelJSON && canEdit && (
|
{this.hasPanelJSON && isPanelJSON && canEdit && (
|
||||||
<Button className={styles.toolbarItem} onClick={this.onApplyPanelModel}>
|
<Button className={styles.toolbarItem} onClick={this.onApplyPanelModel}>
|
||||||
Apply
|
Apply
|
||||||
</Button>
|
</Button>
|
@ -1,8 +1,8 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { stylesFactory, Tab, TabsBar, useTheme } from '@grafana/ui';
|
import { stylesFactory, useTheme, Tab, TabsBar } from '@grafana/ui';
|
||||||
import { GrafanaTheme, SelectableValue, PanelData, getValueFormat, formattedValueToString } from '@grafana/data';
|
import { GrafanaTheme, SelectableValue, PanelData, getValueFormat, formattedValueToString } from '@grafana/data';
|
||||||
import { InspectTab } from './types';
|
import { InspectTab } from '../inspector/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tab: InspectTab;
|
tab: InspectTab;
|
@ -7,7 +7,7 @@ import appEvents from 'app/core/app_events';
|
|||||||
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
|
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
|
||||||
import { PanelModel } from 'app/features/dashboard/state';
|
import { PanelModel } from 'app/features/dashboard/state';
|
||||||
import { getPanelInspectorStyles } from './styles';
|
import { getPanelInspectorStyles } from './styles';
|
||||||
import { supportsDataQuery } from '../PanelEditor/utils';
|
import { supportsDataQuery } from 'app/features/dashboard/components/PanelEditor/utils';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
@ -27,8 +27,9 @@ interface ExecutedQueryInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
panel: PanelModel;
|
|
||||||
data: DataFrame[];
|
data: DataFrame[];
|
||||||
|
onRefreshQuery: () => void;
|
||||||
|
panel?: PanelModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@ -66,8 +67,10 @@ export class QueryInspector extends PureComponent<Props, State> {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.subs.add(panel.events.subscribe(RefreshEvent, this.onPanelRefresh));
|
if (panel) {
|
||||||
this.updateQueryList();
|
this.subs.add(panel.events.subscribe(RefreshEvent, this.onPanelRefresh));
|
||||||
|
this.updateQueryList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(oldProps: Props) {
|
componentDidUpdate(oldProps: Props) {
|
||||||
@ -111,10 +114,6 @@ export class QueryInspector extends PureComponent<Props, State> {
|
|||||||
this.setState({ executedQueries });
|
this.setState({ executedQueries });
|
||||||
}
|
}
|
||||||
|
|
||||||
onIssueNewQuery = () => {
|
|
||||||
this.props.panel.refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.subs.unsubscribe();
|
this.subs.unsubscribe();
|
||||||
}
|
}
|
||||||
@ -255,12 +254,13 @@ export class QueryInspector extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { allNodesExpanded, executedQueries } = this.state;
|
const { allNodesExpanded, executedQueries } = this.state;
|
||||||
|
const { panel, onRefreshQuery } = this.props;
|
||||||
const { response, isLoading } = this.state.dsQuery;
|
const { response, isLoading } = this.state.dsQuery;
|
||||||
const openNodes = this.getNrOfOpenNodes();
|
const openNodes = this.getNrOfOpenNodes();
|
||||||
const styles = getPanelInspectorStyles();
|
const styles = getPanelInspectorStyles();
|
||||||
const haveData = Object.keys(response).length > 0;
|
const haveData = Object.keys(response).length > 0;
|
||||||
|
|
||||||
if (!supportsDataQuery(this.props.panel.plugin)) {
|
if (panel && !supportsDataQuery(panel.plugin)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,7 +277,7 @@ export class QueryInspector extends PureComponent<Props, State> {
|
|||||||
<div className={styles.toolbar}>
|
<div className={styles.toolbar}>
|
||||||
<Button
|
<Button
|
||||||
icon="sync"
|
icon="sync"
|
||||||
onClick={this.onIssueNewQuery}
|
onClick={onRefreshQuery}
|
||||||
aria-label={selectors.components.PanelInspector.Query.refreshButton}
|
aria-label={selectors.components.PanelInspector.Query.refreshButton}
|
||||||
>
|
>
|
||||||
Refresh
|
Refresh
|
@ -24,6 +24,7 @@ export const getPanelInspectorStyles = stylesFactory(() => {
|
|||||||
`,
|
`,
|
||||||
content: css`
|
content: css`
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
height: 100%;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
`,
|
`,
|
||||||
contentQueryInspector: css`
|
contentQueryInspector: css`
|
Loading…
Reference in New Issue
Block a user