mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
c8a0ebe0e8
commit
73a675af02
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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>;
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
@ -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(
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user