DashboardScene: Inspect panel data tab (#74646)

* Refactor data tab to be usable from scenes

* DashboardScene: Inspect data tab

* Everything seem to work now

* don't change drawer size in this PR

* Remove uncommented code

* Fix layout issues for data actions

* Added comment explaining retry
This commit is contained in:
Torkel Ödegaard 2023-09-11 16:11:22 +02:00 committed by GitHub
parent c8a0ebe0e8
commit 73a675af02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 183 additions and 136 deletions

View File

@ -3,6 +3,7 @@ import React, { MouseEventHandler } from 'react';
import { DraggableProvided } from 'react-beautiful-dnd'; import { DraggableProvided } from 'react-beautiful-dnd';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { Icon, IconButton, useStyles2 } from '@grafana/ui'; import { Icon, IconButton, useStyles2 } from '@grafana/ui';
export interface QueryOperationRowHeaderProps { export interface QueryOperationRowHeaderProps {
@ -58,7 +59,7 @@ export const QueryOperationRowHeader = ({
{headerElement} {headerElement}
</div> </div>
<div className={styles.column}> <Stack gap={1} alignItems="center" wrap={false}>
{actionsElement} {actionsElement}
{draggable && ( {draggable && (
<Icon <Icon
@ -70,7 +71,7 @@ export const QueryOperationRowHeader = ({
{...dragHandleProps} {...dragHandleProps}
/> />
)} )}
</div> </Stack>
</div> </div>
); );
}; };

View File

@ -1,20 +1,77 @@
import React from 'react'; import React from 'react';
import { SceneComponentProps, SceneObjectBase, VizPanel } from '@grafana/scenes'; import { LoadingState } from '@grafana/data';
import { t } from 'app/core/internationalization'; import {
SceneComponentProps,
SceneDataProvider,
SceneDataTransformer,
sceneGraph,
SceneObjectBase,
} from '@grafana/scenes';
import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner';
import { InspectTab } from '../../inspector/types'; import { InspectDataTab as InspectDataTabOld } from '../../inspector/InspectDataTab';
import { InspectTabState } from './types'; import { InspectTabState } from './types';
export class InspectDataTab extends SceneObjectBase<InspectTabState> { export interface InspectDataTabState extends InspectTabState {
constructor(public panel: VizPanel) { options: GetDataOptions;
super({ label: t('dashboard.inspect.data-tab', 'Data'), value: InspectTab.Data }); }
export class InspectDataTab extends SceneObjectBase<InspectDataTabState> {
public constructor(state: Omit<InspectDataTabState, 'options'>) {
super({
...state,
options: {
withTransforms: true,
withFieldConfig: true,
},
});
} }
static Component = ({ model }: SceneComponentProps<InspectDataTab>) => { public onOptionsChange = (options: GetDataOptions) => {
//const data = sceneGraph.getData(model.panel).useState(); this.setState({ options });
};
return <div>Data tab</div>; static Component = ({ model }: SceneComponentProps<InspectDataTab>) => {
const { options } = model.useState();
const panel = model.state.panelRef.resolve();
const dataProvider = sceneGraph.getData(panel);
const { data } = getDataProviderToSubscribeTo(dataProvider, options.withTransforms).useState();
const timeRange = sceneGraph.getTimeRange(panel);
if (!data) {
<div>No data found</div>;
}
return (
<InspectDataTabOld
isLoading={data?.state === LoadingState.Loading}
data={data?.series}
options={options}
hasTransformations={hasTransformations(dataProvider)}
timeZone={timeRange.getTimeZone()}
panelPluginId={panel.state.pluginId}
dataName={panel.state.title}
fieldConfig={panel.state.fieldConfig}
onOptionsChange={model.onOptionsChange}
/>
);
}; };
} }
function hasTransformations(dataProvider: SceneDataProvider) {
if (dataProvider instanceof SceneDataTransformer) {
return dataProvider.state.transformations.length > 0;
}
return false;
}
function getDataProviderToSubscribeTo(dataProvider: SceneDataProvider, withTransforms: boolean) {
if (withTransforms && dataProvider instanceof SceneDataTransformer) {
return dataProvider.state.$data!;
}
return dataProvider;
}

View File

@ -1,17 +1,10 @@
import React from 'react'; import React from 'react';
import { SceneComponentProps, SceneObjectBase, VizPanel } from '@grafana/scenes'; import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
import { t } from 'app/core/internationalization';
import { InspectTab } from '../../inspector/types';
import { InspectTabState } from './types'; import { InspectTabState } from './types';
export class InspectJsonTab extends SceneObjectBase<InspectTabState> { export class InspectJsonTab extends SceneObjectBase<InspectTabState> {
constructor(public panel: VizPanel) {
super({ label: t('dashboard.inspect.json-tab', 'JSON'), value: InspectTab.JSON });
}
static Component = ({ model }: SceneComponentProps<InspectJsonTab>) => { static Component = ({ model }: SceneComponentProps<InspectJsonTab>) => {
return <div>JSON</div>; return <div>JSON</div>;
}; };

View File

@ -1,21 +1,15 @@
import React from 'react'; import React from 'react';
import { SceneComponentProps, sceneGraph, SceneObjectBase, VizPanel } from '@grafana/scenes'; import { SceneComponentProps, sceneGraph, SceneObjectBase } from '@grafana/scenes';
import { t } from 'app/core/internationalization';
import { InspectStatsTab as OldInspectStatsTab } from '../../inspector/InspectStatsTab'; import { InspectStatsTab as OldInspectStatsTab } from '../../inspector/InspectStatsTab';
import { InspectTab } from '../../inspector/types';
import { InspectTabState } from './types'; import { InspectTabState } from './types';
export class InspectStatsTab extends SceneObjectBase<InspectTabState> { export class InspectStatsTab extends SceneObjectBase<InspectTabState> {
constructor(public panel: VizPanel) {
super({ label: t('dashboard.inspect.stats-tab', 'Stats'), value: InspectTab.Stats });
}
static Component = ({ model }: SceneComponentProps<InspectStatsTab>) => { static Component = ({ model }: SceneComponentProps<InspectStatsTab>) => {
const data = sceneGraph.getData(model.panel).useState(); const data = sceneGraph.getData(model.state.panelRef.resolve()).useState();
const timeRange = sceneGraph.getTimeRange(model.panel); const timeRange = sceneGraph.getTimeRange(model.state.panelRef.resolve());
if (!data.data) { if (!data.data) {
return null; return null;

View File

@ -12,8 +12,10 @@ import {
VizPanel, VizPanel,
SceneObjectRef, SceneObjectRef,
} from '@grafana/scenes'; } from '@grafana/scenes';
import { Drawer, Tab, TabsBar } from '@grafana/ui'; import { Alert, Drawer, Tab, TabsBar } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { supportsDataQuery } from 'app/features/dashboard/components/PanelEditor/utils'; import { supportsDataQuery } from 'app/features/dashboard/components/PanelEditor/utils';
import { InspectTab } from 'app/features/inspector/types';
import { InspectDataTab } from './InspectDataTab'; import { InspectDataTab } from './InspectDataTab';
import { InspectJsonTab } from './InspectJsonTab'; import { InspectJsonTab } from './InspectJsonTab';
@ -23,6 +25,7 @@ import { InspectTabState } from './types';
interface PanelInspectDrawerState extends SceneObjectState { interface PanelInspectDrawerState extends SceneObjectState {
tabs?: Array<SceneObject<InspectTabState>>; tabs?: Array<SceneObject<InspectTabState>>;
panelRef: SceneObjectRef<VizPanel>; panelRef: SceneObjectRef<VizPanel>;
pluginNotLoaded?: boolean;
} }
export class PanelInspectDrawer extends SceneObjectBase<PanelInspectDrawerState> { export class PanelInspectDrawer extends SceneObjectBase<PanelInspectDrawerState> {
@ -31,22 +34,35 @@ export class PanelInspectDrawer extends SceneObjectBase<PanelInspectDrawerState>
constructor(state: PanelInspectDrawerState) { constructor(state: PanelInspectDrawerState) {
super(state); super(state);
this.buildTabs(); this.buildTabs(0);
} }
buildTabs() { /**
const panel = this.state.panelRef.resolve(); * We currently have no async await to get the panel plugin from the VizPanel.
* That is why there is a retry argument here and a setTimeout, to try again a bit later.
*/
buildTabs(retry: number) {
const panelRef = this.state.panelRef;
const panel = panelRef.resolve();
const plugin = panel.getPlugin(); const plugin = panel.getPlugin();
const tabs: Array<SceneObject<InspectTabState>> = []; const tabs: Array<SceneObject<InspectTabState>> = [];
if (plugin) { if (plugin) {
if (supportsDataQuery(plugin)) { if (supportsDataQuery(plugin)) {
tabs.push(new InspectDataTab(panel)); tabs.push(
tabs.push(new InspectStatsTab(panel)); new InspectDataTab({ panelRef, label: t('dashboard.inspect.data-tab', 'Data'), value: InspectTab.Data })
);
tabs.push(
new InspectStatsTab({ panelRef, label: t('dashboard.inspect.stats-tab', 'Stats'), value: InspectTab.Stats })
);
} }
} else if (retry < 2000) {
setTimeout(() => this.buildTabs(retry + 100), 100);
} else {
this.setState({ pluginNotLoaded: true });
} }
tabs.push(new InspectJsonTab(panel)); tabs.push(new InspectJsonTab({ panelRef, label: t('dashboard.inspect.json-tab', 'JSON'), value: InspectTab.JSON }));
this.setState({ tabs }); this.setState({ tabs });
} }
@ -62,7 +78,7 @@ export class PanelInspectDrawer extends SceneObjectBase<PanelInspectDrawerState>
} }
function PanelInspectRenderer({ model }: SceneComponentProps<PanelInspectDrawer>) { function PanelInspectRenderer({ model }: SceneComponentProps<PanelInspectDrawer>) {
const { tabs } = model.useState(); const { tabs, pluginNotLoaded } = model.useState();
const location = useLocation(); const location = useLocation();
const queryParams = new URLSearchParams(location.search); const queryParams = new URLSearchParams(location.search);
@ -78,7 +94,7 @@ function PanelInspectRenderer({ model }: SceneComponentProps<PanelInspectDrawer>
title={model.getDrawerTitle()} title={model.getDrawerTitle()}
scrollableContent scrollableContent
onClose={model.onClose} onClose={model.onClose}
size="md" size="lg"
tabs={ tabs={
<TabsBar> <TabsBar>
{tabs.map((tab) => { {tabs.map((tab) => {
@ -94,6 +110,11 @@ function PanelInspectRenderer({ model }: SceneComponentProps<PanelInspectDrawer>
</TabsBar> </TabsBar>
} }
> >
{pluginNotLoaded && (
<Alert title="Panel plugin not loaded">
Make sure the panel you want to inspect is visible and has been displayed before opening inspect.
</Alert>
)}
{currentTab.Component && <currentTab.Component model={currentTab} />} {currentTab.Component && <currentTab.Component model={currentTab} />}
</Drawer> </Drawer>
); );

View File

@ -1,7 +1,8 @@
import { SceneObjectState } from '@grafana/scenes'; import { SceneObjectRef, SceneObjectState, VizPanel } from '@grafana/scenes';
import { InspectTab } from 'app/features/inspector/types'; import { InspectTab } from 'app/features/inspector/types';
export interface InspectTabState extends SceneObjectState { export interface InspectTabState extends SceneObjectState {
label: string; label: string;
value: InspectTab; value: InspectTab;
panelRef: SceneObjectRef<VizPanel>;
} }

View File

@ -88,7 +88,10 @@ export const InspectContent = ({
> >
{activeTab === InspectTab.Data && ( {activeTab === InspectTab.Data && (
<InspectDataTab <InspectDataTab
panel={panel} dataName={panel.getDisplayTitle()}
panelPluginId={panel.type}
fieldConfig={panel.fieldConfig}
hasTransformations={Boolean(panel.transformations?.length)}
data={data && data.series} data={data && data.series}
isLoading={isDataLoading} isLoading={isDataLoading}
options={dataOptions} options={dataOptions}

View File

@ -3,7 +3,7 @@ import { useToggle } from 'react-use';
import { DataFrame, DataTransformerConfig, TransformerRegistryItem, FrameMatcherID } from '@grafana/data'; import { DataFrame, DataTransformerConfig, TransformerRegistryItem, FrameMatcherID } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime'; import { reportInteraction } from '@grafana/runtime';
import { ConfirmModal, HorizontalGroup } from '@grafana/ui'; import { ConfirmModal } from '@grafana/ui';
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp'; import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
import { import {
QueryOperationAction, QueryOperationAction,
@ -103,7 +103,7 @@ export const TransformationOperationRow = ({
const renderActions = ({ isOpen }: QueryOperationRowRenderProps) => { const renderActions = ({ isOpen }: QueryOperationRowRenderProps) => {
return ( return (
<HorizontalGroup align="center" width="auto"> <>
{uiConfig.state && <PluginStateInfo state={uiConfig.state} />} {uiConfig.state && <PluginStateInfo state={uiConfig.state} />}
<QueryOperationToggleAction <QueryOperationToggleAction
title="Show transform help" title="Show transform help"
@ -152,7 +152,7 @@ export const TransformationOperationRow = ({
onDismiss={() => setShowDeleteModal(false)} onDismiss={() => setShowDeleteModal(false)}
/> />
)} )}
</HorizontalGroup> </>
); );
}; };

View File

@ -56,6 +56,7 @@ export function ExploreQueryInspector(props: Props) {
content: ( content: (
<InspectDataTab <InspectDataTab
data={dataFrames} data={dataFrames}
dataName={'Explore'}
isLoading={loading} isLoading={loading}
options={{ withTransforms: false, withFieldConfig: false }} options={{ withTransforms: false, withFieldConfig: false }}
timeZone={timeZone} timeZone={timeZone}

View File

@ -4,7 +4,6 @@ import { DataFrame, DataTransformerID, getFrameDisplayName, SelectableValue } fr
import { Field, HorizontalGroup, Select, Switch, VerticalGroup, useStyles2 } from '@grafana/ui'; import { Field, HorizontalGroup, Select, Switch, VerticalGroup, useStyles2 } from '@grafana/ui';
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow'; import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
import { t } from 'app/core/internationalization'; import { t } from 'app/core/internationalization';
import { PanelModel } from 'app/features/dashboard/state';
import { DetailText } from 'app/features/inspector/DetailText'; import { DetailText } from 'app/features/inspector/DetailText';
import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner'; import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner';
@ -13,24 +12,24 @@ import { getPanelInspectorStyles2 } from './styles';
interface Props { interface Props {
options: GetDataOptions; options: GetDataOptions;
dataFrames: DataFrame[]; dataFrames: DataFrame[];
transformId: DataTransformerID;
transformationOptions: Array<SelectableValue<DataTransformerID>>; transformationOptions: Array<SelectableValue<DataTransformerID>>;
selectedDataFrame: number | DataTransformerID; selectedDataFrame: number | DataTransformerID;
downloadForExcel: boolean; downloadForExcel: boolean;
onDataFrameChange: (item: SelectableValue<DataTransformerID | number>) => void; onDataFrameChange: (item: SelectableValue<DataTransformerID | number>) => void;
toggleDownloadForExcel: () => void; toggleDownloadForExcel: () => void;
data?: DataFrame[]; data?: DataFrame[];
panel?: PanelModel; hasTransformations?: boolean;
onOptionsChange?: (options: GetDataOptions) => void; onOptionsChange?: (options: GetDataOptions) => void;
actions?: React.ReactNode;
} }
export const InspectDataOptions = ({ export const InspectDataOptions = ({
options, options,
actions,
onOptionsChange, onOptionsChange,
panel, hasTransformations,
data, data,
dataFrames, dataFrames,
transformId,
transformationOptions, transformationOptions,
selectedDataFrame, selectedDataFrame,
onDataFrameChange, onDataFrameChange,
@ -39,10 +38,6 @@ export const InspectDataOptions = ({
}: Props) => { }: Props) => {
const styles = useStyles2(getPanelInspectorStyles2); const styles = useStyles2(getPanelInspectorStyles2);
const panelTransformations = panel?.getTransformations();
const showPanelTransformationsOption = Boolean(panelTransformations?.length);
const showFieldConfigsOption = panel && !panel.plugin?.fieldConfigRegistry.isEmpty();
let dataSelect = dataFrames; let dataSelect = dataFrames;
if (selectedDataFrame === DataTransformerID.joinByField) { if (selectedDataFrame === DataTransformerID.joinByField) {
dataSelect = data!; dataSelect = data!;
@ -100,6 +95,7 @@ export const InspectDataOptions = ({
title={t('dashboard.inspect-data.data-options', 'Data options')} title={t('dashboard.inspect-data.data-options', 'Data options')}
headerElement={<DetailText>{getActiveString()}</DetailText>} headerElement={<DetailText>{getActiveString()}</DetailText>}
isOpen={false} isOpen={false}
actions={actions}
> >
<div className={styles.options} data-testid="dataOptions"> <div className={styles.options} data-testid="dataOptions">
<VerticalGroup spacing="none"> <VerticalGroup spacing="none">
@ -116,7 +112,7 @@ export const InspectDataOptions = ({
)} )}
<HorizontalGroup> <HorizontalGroup>
{showPanelTransformationsOption && onOptionsChange && ( {hasTransformations && onOptionsChange && (
<Field <Field
label={t('dashboard.inspect-data.transformations-label', 'Apply panel transformations')} label={t('dashboard.inspect-data.transformations-label', 'Apply panel transformations')}
description={t( description={t(
@ -130,7 +126,7 @@ export const InspectDataOptions = ({
/> />
</Field> </Field>
)} )}
{showFieldConfigsOption && onOptionsChange && ( {onOptionsChange && (
<Field <Field
label={t('dashboard.inspect-data.formatted-data-label', 'Formatted data')} label={t('dashboard.inspect-data.formatted-data-label', 'Formatted data')}
description={t( description={t(

View File

@ -1,4 +1,3 @@
import { css } from '@emotion/css';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
@ -7,7 +6,6 @@ import {
applyFieldOverrides, applyFieldOverrides,
applyRawFieldOverrides, applyRawFieldOverrides,
CoreApp, CoreApp,
CSVConfig,
DataFrame, DataFrame,
DataTransformerID, DataTransformerID,
FieldConfigSource, FieldConfigSource,
@ -20,7 +18,6 @@ import { reportInteraction } from '@grafana/runtime';
import { Button, Spinner, Table } from '@grafana/ui'; import { Button, Spinner, Table } from '@grafana/ui';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { t, Trans } from 'app/core/internationalization'; import { t, Trans } from 'app/core/internationalization';
import { PanelModel } from 'app/features/dashboard/state';
import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner'; import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner';
import { dataFrameToLogsModel } from '../logs/logsModel'; import { dataFrameToLogsModel } from '../logs/logsModel';
@ -35,7 +32,11 @@ interface Props {
timeZone: TimeZone; timeZone: TimeZone;
app?: CoreApp; app?: CoreApp;
data?: DataFrame[]; data?: DataFrame[];
panel?: PanelModel; /** The title of the panel or other context name */
dataName: string;
panelPluginId?: string;
fieldConfig?: FieldConfigSource;
hasTransformations?: boolean;
onOptionsChange?: (options: GetDataOptions) => void; onOptionsChange?: (options: GetDataOptions) => void;
} }
@ -91,15 +92,20 @@ export class InspectDataTab extends PureComponent<Props, State> {
} }
} }
exportCsv = (dataFrame: DataFrame, csvConfig: CSVConfig = {}) => { exportCsv(dataFrames: DataFrame[], hasLogs: boolean) {
const { panel } = this.props; const { dataName } = this.props;
const { transformId } = this.state; const { transformId } = this.state;
const dataFrame = dataFrames[this.state.dataFrameIndex];
downloadDataFrameAsCsv(dataFrame, panel ? panel.getDisplayTitle() : 'Explore', csvConfig, transformId); if (hasLogs) {
}; reportInteraction('grafana_logs_download_clicked', { app: this.props.app, format: 'csv' });
}
exportLogsAsTxt = () => { downloadDataFrameAsCsv(dataFrame, dataName, { useExcelHeader: this.state.downloadForExcel }, transformId);
const { data, panel, app } = this.props; }
onExportLogsAsTxt = () => {
const { data, dataName, app } = this.props;
reportInteraction('grafana_logs_download_logs_clicked', { reportInteraction('grafana_logs_download_logs_clicked', {
app, app,
@ -108,11 +114,11 @@ export class InspectDataTab extends PureComponent<Props, State> {
}); });
const logsModel = dataFrameToLogsModel(data || []); const logsModel = dataFrameToLogsModel(data || []);
downloadLogsModelAsTxt(logsModel, panel ? panel.getDisplayTitle() : 'Explore'); downloadLogsModelAsTxt(logsModel, dataName);
}; };
exportTracesAsJson = () => { onExportTracesAsJson = () => {
const { data, panel, app } = this.props; const { data, dataName, app } = this.props;
if (!data) { if (!data) {
return; return;
@ -123,7 +129,8 @@ export class InspectDataTab extends PureComponent<Props, State> {
if (df.meta?.preferredVisualisationType !== 'trace') { if (df.meta?.preferredVisualisationType !== 'trace') {
continue; continue;
} }
const traceFormat = downloadTraceAsJson(df, (panel ? panel.getDisplayTitle() : 'Explore') + '-traces');
const traceFormat = downloadTraceAsJson(df, dataName + '-traces');
reportInteraction('grafana_traces_download_traces_clicked', { reportInteraction('grafana_traces_download_traces_clicked', {
app, app,
@ -134,8 +141,9 @@ export class InspectDataTab extends PureComponent<Props, State> {
} }
}; };
exportServiceGraph = () => { onExportServiceGraph = () => {
const { data, panel, app } = this.props; const { data, dataName, app } = this.props;
reportInteraction('grafana_traces_download_service_graph_clicked', { reportInteraction('grafana_traces_download_service_graph_clicked', {
app, app,
grafana_version: config.buildInfo.version, grafana_version: config.buildInfo.version,
@ -146,7 +154,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
return; return;
} }
downloadAsJson(data, panel ? panel.getDisplayTitle() : 'Explore'); downloadAsJson(data, dataName);
}; };
onDataFrameChange = (item: SelectableValue<DataTransformerID | number>) => { onDataFrameChange = (item: SelectableValue<DataTransformerID | number>) => {
@ -158,28 +166,28 @@ export class InspectDataTab extends PureComponent<Props, State> {
}); });
}; };
toggleDownloadForExcel = () => { onToggleDownloadForExcel = () => {
this.setState((prevState) => ({ this.setState((prevState) => ({
downloadForExcel: !prevState.downloadForExcel, downloadForExcel: !prevState.downloadForExcel,
})); }));
}; };
getProcessedData(): DataFrame[] { getProcessedData(): DataFrame[] {
const { options, panel, timeZone } = this.props; const { options, panelPluginId, fieldConfig, timeZone } = this.props;
const data = this.state.transformedData; const data = this.state.transformedData;
if (!options.withFieldConfig || !panel) { if (!options.withFieldConfig || !panelPluginId || !fieldConfig) {
return applyRawFieldOverrides(data); return applyRawFieldOverrides(data);
} }
const fieldConfig = this.cleanTableConfigFromFieldConfig(panel.type, panel.fieldConfig); const fieldConfigCleaned = this.cleanTableConfigFromFieldConfig(panelPluginId, fieldConfig);
// We need to apply field config as it's not done by PanelQueryRunner (even when withFieldConfig is true). // We need to apply field config as it's not done by PanelQueryRunner (even when withFieldConfig is true).
// It's because transformers create new fields and data frames, and we need to clean field config of any table settings. // It's because transformers create new fields and data frames, and we need to clean field config of any table settings.
return applyFieldOverrides({ return applyFieldOverrides({
data, data,
theme: config.theme2, theme: config.theme2,
fieldConfig, fieldConfig: fieldConfigCleaned,
timeZone, timeZone,
replaceVariables: (value: string) => { replaceVariables: (value: string) => {
return value; return value;
@ -210,9 +218,34 @@ export class InspectDataTab extends PureComponent<Props, State> {
return fieldConfig; return fieldConfig;
} }
renderActions(dataFrames: DataFrame[], hasLogs: boolean, hasTraces: boolean, hasServiceGraph: boolean) {
return (
<>
<Button variant="primary" onClick={() => this.exportCsv(dataFrames, hasLogs)} size="sm">
<Trans i18nKey="dashboard.inspect-data.download-csv">Download CSV</Trans>
</Button>
{hasLogs && (
<Button variant="primary" onClick={this.onExportLogsAsTxt} size="sm">
<Trans i18nKey="dashboard.inspect-data.download-logs">Download logs</Trans>
</Button>
)}
{hasTraces && (
<Button variant="primary" onClick={this.onExportTracesAsJson} size="sm">
<Trans i18nKey="dashboard.inspect-data.download-traces">Download traces</Trans>
</Button>
)}
{hasServiceGraph && (
<Button variant="primary" onClick={this.onExportServiceGraph} size="sm">
<Trans i18nKey="dashboard.inspect-data.download-service">Download service graph</Trans>
</Button>
)}
</>
);
}
render() { render() {
const { isLoading, options, data, panel, onOptionsChange, app } = this.props; const { isLoading, options, data, onOptionsChange, hasTransformations } = this.props;
const { dataFrameIndex, transformId, transformationOptions, selectedDataFrame, downloadForExcel } = this.state; const { dataFrameIndex, transformationOptions, selectedDataFrame, downloadForExcel } = this.state;
const styles = getPanelInspectorStyles(); const styles = getPanelInspectorStyles();
if (isLoading) { if (isLoading) {
@ -241,70 +274,17 @@ export class InspectDataTab extends PureComponent<Props, State> {
<div className={styles.toolbar}> <div className={styles.toolbar}>
<InspectDataOptions <InspectDataOptions
data={data} data={data}
panel={panel} hasTransformations={hasTransformations}
options={options} options={options}
dataFrames={dataFrames} dataFrames={dataFrames}
transformId={transformId}
transformationOptions={transformationOptions} transformationOptions={transformationOptions}
selectedDataFrame={selectedDataFrame} selectedDataFrame={selectedDataFrame}
downloadForExcel={downloadForExcel} downloadForExcel={downloadForExcel}
onOptionsChange={onOptionsChange} onOptionsChange={onOptionsChange}
onDataFrameChange={this.onDataFrameChange} onDataFrameChange={this.onDataFrameChange}
toggleDownloadForExcel={this.toggleDownloadForExcel} toggleDownloadForExcel={this.onToggleDownloadForExcel}
actions={this.renderActions(dataFrames, hasLogs, hasTraces, hasServiceGraph)}
/> />
<Button
variant="primary"
onClick={() => {
if (hasLogs) {
reportInteraction('grafana_logs_download_clicked', {
app,
format: 'csv',
});
}
this.exportCsv(dataFrames[dataFrameIndex], { useExcelHeader: this.state.downloadForExcel });
}}
className={css`
margin-bottom: 10px;
`}
>
<Trans i18nKey="dashboard.inspect-data.download-csv">Download CSV</Trans>
</Button>
{hasLogs && (
<Button
variant="primary"
onClick={this.exportLogsAsTxt}
className={css`
margin-bottom: 10px;
margin-left: 10px;
`}
>
<Trans i18nKey="dashboard.inspect-data.download-logs">Download logs</Trans>
</Button>
)}
{hasTraces && (
<Button
variant="primary"
onClick={this.exportTracesAsJson}
className={css`
margin-bottom: 10px;
margin-left: 10px;
`}
>
<Trans i18nKey="dashboard.inspect-data.download-traces">Download traces</Trans>
</Button>
)}
{hasServiceGraph && (
<Button
variant="primary"
onClick={this.exportServiceGraph}
className={css`
margin-bottom: 10px;
margin-left: 10px;
`}
>
<Trans i18nKey="dashboard.inspect-data.download-service">Download service graph</Trans>
</Button>
)}
</div> </div>
<div className={styles.content}> <div className={styles.content}>
<AutoSizer> <AutoSizer>

View File

@ -23,7 +23,7 @@ import {
} from '@grafana/data'; } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { AngularComponent, getAngularLoader, getDataSourceSrv } from '@grafana/runtime'; import { AngularComponent, getAngularLoader, getDataSourceSrv } from '@grafana/runtime';
import { Badge, ErrorBoundaryAlert, HorizontalGroup } from '@grafana/ui'; import { Badge, ErrorBoundaryAlert } from '@grafana/ui';
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp'; import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
import { import {
QueryOperationAction, QueryOperationAction,
@ -439,7 +439,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
const hasEditorHelp = datasource?.components?.QueryEditorHelp; const hasEditorHelp = datasource?.components?.QueryEditorHelp;
return ( return (
<HorizontalGroup width="auto"> <>
{hasEditorHelp && ( {hasEditorHelp && (
<QueryOperationToggleAction <QueryOperationToggleAction
title="Show data source help" title="Show data source help"
@ -468,7 +468,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
/> />
) : null} ) : null}
<QueryOperationAction title="Remove query" icon="trash-alt" onClick={this.onRemoveQuery} /> <QueryOperationAction title="Remove query" icon="trash-alt" onClick={this.onRemoveQuery} />
</HorizontalGroup> </>
); );
}; };