mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboards: show changes in save dialog (#46557)
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
parent
a338c78ca8
commit
15ca294be0
@ -197,7 +197,7 @@ exports[`no enzyme tests`] = {
|
||||
"public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardAsForm.test.tsx:2536713486": [
|
||||
[1, 17, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.test.tsx:4134073823": [
|
||||
"public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.test.tsx:1262111696": [
|
||||
[1, 17, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx:1044891955": [
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Story } from '@storybook/react';
|
||||
import { Button, Drawer } from '@grafana/ui';
|
||||
import { Button, Drawer, Tab, TabsBar } from '@grafana/ui';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import mdx from './Drawer.mdx';
|
||||
@ -72,6 +72,7 @@ export const Global: Story<Props> = (args) => {
|
||||
</UseState>
|
||||
);
|
||||
};
|
||||
|
||||
Global.args = {
|
||||
title: 'Drawer title',
|
||||
};
|
||||
@ -223,7 +224,31 @@ export const InLine: Story<Props> = (args) => {
|
||||
</UseState>
|
||||
);
|
||||
};
|
||||
|
||||
InLine.args = {
|
||||
title: 'Storybook',
|
||||
inline: true,
|
||||
};
|
||||
|
||||
export function WithTabs() {
|
||||
const [activeTab, setActiveTab] = useState('options');
|
||||
|
||||
const tabs = (
|
||||
<TabsBar>
|
||||
<Tab label={'Options'} active={activeTab === 'options'} onChangeTab={() => setActiveTab('options')} />
|
||||
<Tab
|
||||
label={'Changes'}
|
||||
active={activeTab === 'changes'}
|
||||
onChangeTab={() => setActiveTab('changes')}
|
||||
counter={10}
|
||||
/>
|
||||
</TabsBar>
|
||||
);
|
||||
|
||||
return (
|
||||
<Drawer title={'Main title'} subtitle={'Sub title'} width={700} onClose={() => {}} tabs={tabs}>
|
||||
{activeTab === 'options' && <div>Here are some options</div>}
|
||||
{activeTab === 'changes' && <div>Here are some changes</div>}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { CSSProperties, FC, ReactNode, useState, useEffect } from 'react';
|
||||
import React, { CSSProperties, ReactNode, useState, useEffect } from 'react';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import RcDrawer from 'rc-drawer';
|
||||
import { css } from '@emotion/css';
|
||||
@ -6,7 +6,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
import { stylesFactory, useTheme2 } from '../../themes';
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { FocusScope } from '@react-aria/focus';
|
||||
import { useDialog } from '@react-aria/dialog';
|
||||
import { useOverlay } from '@react-aria/overlays';
|
||||
@ -25,71 +25,15 @@ export interface Props {
|
||||
width?: number | string;
|
||||
/** Should the Drawer be expandable to full width */
|
||||
expandable?: boolean;
|
||||
|
||||
/** Tabs */
|
||||
tabs?: React.ReactNode;
|
||||
/** Set to true if the component rendered within in drawer content has its own scroll */
|
||||
scrollableContent?: boolean;
|
||||
|
||||
/** Callback for closing the drawer */
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme2, scrollableContent: boolean) => {
|
||||
return {
|
||||
container: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`,
|
||||
drawer: css`
|
||||
.drawer-content {
|
||||
background-color: ${theme.colors.background.primary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
&.drawer-open .drawer-mask {
|
||||
background-color: ${theme.components.overlay.background};
|
||||
backdrop-filter: blur(1px);
|
||||
opacity: 1;
|
||||
}
|
||||
.drawer-mask {
|
||||
background-color: ${theme.components.overlay.background};
|
||||
backdrop-filter: blur(1px);
|
||||
}
|
||||
.drawer-open .drawer-content-wrapper {
|
||||
box-shadow: ${theme.shadows.z3};
|
||||
}
|
||||
z-index: ${theme.zIndex.dropdown};
|
||||
`,
|
||||
header: css`
|
||||
background-color: ${theme.colors.background.canvas};
|
||||
z-index: 1;
|
||||
flex-grow: 0;
|
||||
padding-top: ${theme.spacing(0.5)};
|
||||
`,
|
||||
actions: css`
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: flex-end;
|
||||
`,
|
||||
titleWrapper: css`
|
||||
margin-bottom: ${theme.spacing(3)};
|
||||
padding: ${theme.spacing(0, 1, 0, 3)};
|
||||
overflow-wrap: break-word;
|
||||
`,
|
||||
titleSpacing: css`
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
`,
|
||||
content: css`
|
||||
padding: ${theme.spacing(2)};
|
||||
flex: 1;
|
||||
overflow: ${!scrollableContent ? 'hidden' : 'auto'};
|
||||
z-index: 0;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
export const Drawer: FC<Props> = ({
|
||||
export function Drawer({
|
||||
children,
|
||||
inline = false,
|
||||
onClose,
|
||||
@ -99,9 +43,9 @@ export const Drawer: FC<Props> = ({
|
||||
subtitle,
|
||||
width = '40%',
|
||||
expandable = false,
|
||||
}) => {
|
||||
const theme = useTheme2();
|
||||
const drawerStyles = getStyles(theme, scrollableContent);
|
||||
tabs,
|
||||
}: Props) {
|
||||
const drawerStyles = useStyles2(getStyles);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const currentWidth = isExpanded ? '100%' : width;
|
||||
@ -119,6 +63,8 @@ export const Drawer: FC<Props> = ({
|
||||
setIsOpen(true);
|
||||
}, []);
|
||||
|
||||
const content = <div className={drawerStyles.content}>{children}</div>;
|
||||
|
||||
return (
|
||||
<RcDrawer
|
||||
level={null}
|
||||
@ -172,15 +118,76 @@ export const Drawer: FC<Props> = ({
|
||||
<h3 {...titleProps}>{title}</h3>
|
||||
{typeof subtitle === 'string' && <div className="muted">{subtitle}</div>}
|
||||
{typeof subtitle !== 'string' && subtitle}
|
||||
{tabs && <div className={drawerStyles.tabsWrapper}>{tabs}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{typeof title !== 'string' && title}
|
||||
<div className={drawerStyles.content}>
|
||||
{!scrollableContent ? children : <CustomScrollbar>{children}</CustomScrollbar>}
|
||||
<div className={drawerStyles.contentScroll}>
|
||||
{!scrollableContent ? content : <CustomScrollbar autoHeightMin="100%">{content}</CustomScrollbar>}
|
||||
</div>
|
||||
</div>
|
||||
</FocusScope>
|
||||
</RcDrawer>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex: 1 1 0;
|
||||
`,
|
||||
drawer: css`
|
||||
.drawer-content {
|
||||
background-color: ${theme.colors.background.primary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
&.drawer-open .drawer-mask {
|
||||
background-color: ${theme.components.overlay.background};
|
||||
backdrop-filter: blur(1px);
|
||||
opacity: 1;
|
||||
}
|
||||
.drawer-mask {
|
||||
background-color: ${theme.components.overlay.background};
|
||||
backdrop-filter: blur(1px);
|
||||
}
|
||||
.drawer-open .drawer-content-wrapper {
|
||||
box-shadow: ${theme.shadows.z3};
|
||||
}
|
||||
z-index: ${theme.zIndex.dropdown};
|
||||
`,
|
||||
header: css`
|
||||
background-color: ${theme.colors.background.canvas};
|
||||
flex-grow: 0;
|
||||
padding-top: ${theme.spacing(0.5)};
|
||||
`,
|
||||
actions: css`
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: flex-end;
|
||||
`,
|
||||
titleWrapper: css`
|
||||
margin-bottom: ${theme.spacing(3)};
|
||||
padding: ${theme.spacing(0, 1, 0, 3)};
|
||||
overflow-wrap: break-word;
|
||||
`,
|
||||
content: css({
|
||||
padding: theme.spacing(2),
|
||||
height: '100%',
|
||||
flexGrow: 1,
|
||||
}),
|
||||
contentScroll: css({
|
||||
minHeight: 0,
|
||||
flex: 1,
|
||||
}),
|
||||
tabsWrapper: css({
|
||||
paddingLeft: theme.spacing(2),
|
||||
margin: theme.spacing(3, -1, -3, -3),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ import appEvents from 'app/core/app_events';
|
||||
import { getExploreUrl } from 'app/core/utils/explore';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { ShareModal } from 'app/features/dashboard/components/ShareModal';
|
||||
import { SaveDashboardModalProxy } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardModalProxy';
|
||||
import { SaveDashboardDrawer } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { exitKioskMode, toggleKioskMode } from '../navigation/kiosk';
|
||||
import {
|
||||
@ -200,7 +200,7 @@ export class KeybindingSrv {
|
||||
if (dashboard.meta.canSave) {
|
||||
appEvents.publish(
|
||||
new ShowModalReactEvent({
|
||||
component: SaveDashboardModalProxy,
|
||||
component: SaveDashboardDrawer,
|
||||
props: {
|
||||
dashboard,
|
||||
},
|
||||
|
@ -14,7 +14,7 @@ import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
|
||||
import { DashboardModel } from '../../state';
|
||||
import { KioskMode } from 'app/types';
|
||||
import { ShareModal } from 'app/features/dashboard/components/ShareModal';
|
||||
import { SaveDashboardModalProxy } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardModalProxy';
|
||||
import { SaveDashboardDrawer } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer';
|
||||
import { DashboardCommentsModal } from 'app/features/dashboard/components/DashboardComments/DashboardCommentsModal';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { toggleKioskMode } from 'app/core/navigation/kiosk';
|
||||
@ -228,7 +228,7 @@ class DashNav extends PureComponent<Props> {
|
||||
tooltip="Save dashboard"
|
||||
icon="save"
|
||||
onClick={() => {
|
||||
showModal(SaveDashboardModalProxy, {
|
||||
showModal(SaveDashboardDrawer, {
|
||||
dashboard,
|
||||
onDismiss: hideModal,
|
||||
});
|
||||
|
@ -1,10 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { DataSourceApi, PanelData, PanelPlugin } from '@grafana/data';
|
||||
import { DataSourceApi, formattedValueToString, getValueFormat, PanelData, PanelPlugin } from '@grafana/data';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { CustomScrollbar, Drawer, TabContent } from '@grafana/ui';
|
||||
import { getPanelInspectorStyles } from 'app/features/inspector/styles';
|
||||
import { Drawer, Tab, TabsBar } from '@grafana/ui';
|
||||
import { InspectMetadataTab } from 'app/features/inspector/InspectMetadataTab';
|
||||
import { InspectSubtitle } from 'app/features/inspector/InspectSubtitle';
|
||||
import { InspectJSONTab } from 'app/features/inspector/InspectJSONTab';
|
||||
import { QueryInspector } from 'app/features/inspector/QueryInspector';
|
||||
import { InspectStatsTab } from 'app/features/inspector/InspectStatsTab';
|
||||
@ -13,7 +11,6 @@ import { InspectDataTab } from 'app/features/inspector/InspectDataTab';
|
||||
import { InspectTab } from 'app/features/inspector/types';
|
||||
import { DashboardModel, PanelModel } from '../../state';
|
||||
import { GetDataOptions } from '../../../query/state/PanelQueryRunner';
|
||||
import { InspectActionsTab } from './PanelInspectActions';
|
||||
|
||||
interface Props {
|
||||
dashboard: DashboardModel;
|
||||
@ -50,7 +47,6 @@ export const InspectContent: React.FC<Props> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const styles = getPanelInspectorStyles();
|
||||
const error = data?.error;
|
||||
|
||||
// Validate that the active tab is actually valid and allowed
|
||||
@ -58,22 +54,31 @@ export const InspectContent: React.FC<Props> = ({
|
||||
if (!tabs.find((item) => item.value === currentTab)) {
|
||||
activeTab = InspectTab.JSON;
|
||||
}
|
||||
|
||||
const title = getTemplateSrv().replace(panel.title, panel.scopedVars, 'text');
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={`Inspect: ${title || 'Panel'}`}
|
||||
subtitle={
|
||||
<InspectSubtitle
|
||||
tabs={tabs}
|
||||
tab={activeTab}
|
||||
data={data}
|
||||
onSelectTab={(item) => setCurrentTab(item.value || InspectTab.Data)}
|
||||
/>
|
||||
}
|
||||
subtitle={data && formatStats(data)}
|
||||
width="50%"
|
||||
onClose={onClose}
|
||||
expandable
|
||||
scrollableContent
|
||||
tabs={
|
||||
<TabsBar>
|
||||
{tabs.map((t, index) => {
|
||||
return (
|
||||
<Tab
|
||||
key={`${t.value}-${index}`}
|
||||
label={t.label}
|
||||
active={t.value === activeTab}
|
||||
onChangeTab={() => setCurrentTab(t.value || InspectTab.Data)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TabsBar>
|
||||
}
|
||||
>
|
||||
{activeTab === InspectTab.Data && (
|
||||
<InspectDataTab
|
||||
@ -85,23 +90,31 @@ export const InspectContent: React.FC<Props> = ({
|
||||
timeZone={dashboard.timezone}
|
||||
/>
|
||||
)}
|
||||
<CustomScrollbar autoHeightMin="100%">
|
||||
<TabContent className={styles.tabContent}>
|
||||
{data && activeTab === InspectTab.Meta && (
|
||||
<InspectMetadataTab data={data} metadataDatasource={metadataDatasource} />
|
||||
)}
|
||||
{data && activeTab === InspectTab.Meta && (
|
||||
<InspectMetadataTab data={data} metadataDatasource={metadataDatasource} />
|
||||
)}
|
||||
|
||||
{activeTab === InspectTab.JSON && (
|
||||
<InspectJSONTab panel={panel} dashboard={dashboard} data={data} onClose={onClose} />
|
||||
)}
|
||||
{activeTab === InspectTab.Error && <InspectErrorTab error={error} />}
|
||||
{data && activeTab === InspectTab.Stats && <InspectStatsTab data={data} timeZone={dashboard.getTimezone()} />}
|
||||
{data && activeTab === InspectTab.Query && (
|
||||
<QueryInspector panel={panel} data={data.series} onRefreshQuery={() => panel.refresh()} />
|
||||
)}
|
||||
{activeTab === InspectTab.Actions && <InspectActionsTab panel={panel} data={data} />}
|
||||
</TabContent>
|
||||
</CustomScrollbar>
|
||||
{activeTab === InspectTab.JSON && (
|
||||
<InspectJSONTab panel={panel} dashboard={dashboard} data={data} onClose={onClose} />
|
||||
)}
|
||||
{activeTab === InspectTab.Error && <InspectErrorTab error={error} />}
|
||||
{data && activeTab === InspectTab.Stats && <InspectStatsTab data={data} timeZone={dashboard.getTimezone()} />}
|
||||
{data && activeTab === InspectTab.Query && (
|
||||
<QueryInspector panel={panel} data={data.series} onRefreshQuery={() => panel.refresh()} />
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
function formatStats(data: PanelData) {
|
||||
const { request } = data;
|
||||
if (!request) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const queryCount = request.targets.length;
|
||||
const requestTime = request.endTime ? request.endTime - request.startTime : 0;
|
||||
const formatted = formattedValueToString(getValueFormat('ms')(requestTime));
|
||||
|
||||
return `${queryCount} queries with total query time of ${formatted}`;
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ import { DashNavTimeControls } from '../DashNav/DashNavTimeControls';
|
||||
import { OptionsPane } from './OptionsPane';
|
||||
import { SubMenuItems } from 'app/features/dashboard/components/SubMenu/SubMenuItems';
|
||||
import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper';
|
||||
import { SaveDashboardModalProxy } from '../SaveDashboard/SaveDashboardModalProxy';
|
||||
import { SaveDashboardDrawer } from '../SaveDashboard/SaveDashboardDrawer';
|
||||
import { DashboardPanel } from '../../dashgrid/DashboardPanel';
|
||||
|
||||
import { discardPanelChanges, initPanelEditor, updatePanelEditorUIState } from './state/actions';
|
||||
@ -145,7 +145,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||
onSaveDashboard = () => {
|
||||
appEvents.publish(
|
||||
new ShowModalReactEvent({
|
||||
component: SaveDashboardModalProxy,
|
||||
component: SaveDashboardDrawer,
|
||||
props: { dashboard: this.props.dashboard },
|
||||
})
|
||||
);
|
||||
|
@ -1,50 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { Modal } from '@grafana/ui';
|
||||
import { SaveDashboardAsForm } from './forms/SaveDashboardAsForm';
|
||||
import { SaveDashboardErrorProxy } from './SaveDashboardErrorProxy';
|
||||
import { useDashboardSave } from './useDashboardSave';
|
||||
import { SaveDashboardModalProps } from './types';
|
||||
|
||||
export const SaveDashboardAsModal: React.FC<
|
||||
SaveDashboardModalProps & {
|
||||
isNew?: boolean;
|
||||
}
|
||||
> = ({ dashboard, onDismiss, isNew }) => {
|
||||
const { state, onDashboardSave } = useDashboardSave(dashboard);
|
||||
const [dashboardSaveModelClone, setDashboardSaveModelClone] = useState();
|
||||
return (
|
||||
<>
|
||||
{state.error && (
|
||||
<SaveDashboardErrorProxy
|
||||
error={state.error}
|
||||
dashboard={dashboard}
|
||||
dashboardSaveModel={dashboardSaveModelClone}
|
||||
onDismiss={onDismiss}
|
||||
/>
|
||||
)}
|
||||
{!state.error && (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
title="Save dashboard as..."
|
||||
icon="copy"
|
||||
onDismiss={onDismiss}
|
||||
className={css`
|
||||
width: 500px;
|
||||
`}
|
||||
>
|
||||
<SaveDashboardAsForm
|
||||
dashboard={dashboard}
|
||||
onCancel={onDismiss}
|
||||
onSuccess={onDismiss}
|
||||
onSubmit={(clone, options, dashboard) => {
|
||||
setDashboardSaveModelClone(clone);
|
||||
return onDashboardSave(clone, options, dashboard);
|
||||
}}
|
||||
isNew={isNew}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Button, ButtonVariant, ModalsController, FullWidthButtonContainer } from '@grafana/ui';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { SaveDashboardAsModal } from './SaveDashboardAsModal';
|
||||
import { SaveDashboardModalProxy } from './SaveDashboardModalProxy';
|
||||
import { SaveDashboardDrawer } from './SaveDashboardDrawer';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
interface SaveDashboardButtonProps {
|
||||
@ -17,7 +16,7 @@ export const SaveDashboardButton: React.FC<SaveDashboardButtonProps> = ({ dashbo
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
showModal(SaveDashboardModalProxy, {
|
||||
showModal(SaveDashboardDrawer, {
|
||||
dashboard,
|
||||
onSaveSuccess,
|
||||
onDismiss: hideModal,
|
||||
@ -45,10 +44,11 @@ export const SaveDashboardAsButton: React.FC<SaveDashboardButtonProps & { varian
|
||||
<FullWidthButtonContainer>
|
||||
<Button
|
||||
onClick={() => {
|
||||
showModal(SaveDashboardAsModal, {
|
||||
showModal(SaveDashboardDrawer, {
|
||||
dashboard,
|
||||
onSaveSuccess,
|
||||
onDismiss: hideModal,
|
||||
isCopy: true,
|
||||
});
|
||||
}}
|
||||
variant={variant}
|
||||
|
@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { Spinner, useStyles2 } from '@grafana/ui';
|
||||
import { Diffs } from '../VersionHistory/utils';
|
||||
import { DiffGroup } from '../VersionHistory/DiffGroup';
|
||||
import { DiffViewer } from '../VersionHistory/DiffViewer';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
interface SaveDashboardDiffProps {
|
||||
oldValue?: any;
|
||||
newValue?: any;
|
||||
|
||||
// calculated by parent so we can see summary in tabs
|
||||
diff?: Diffs;
|
||||
}
|
||||
|
||||
export const SaveDashboardDiff = ({ diff, oldValue, newValue }: SaveDashboardDiffProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const loader = useAsync(async () => {
|
||||
const oldJSON = JSON.stringify(oldValue ?? {}, null, 2);
|
||||
const newJSON = JSON.stringify(newValue ?? {}, null, 2);
|
||||
return {
|
||||
oldJSON,
|
||||
newJSON,
|
||||
diffs: Object.entries(diff ?? []).map(([key, diffs]) => (
|
||||
<DiffGroup diffs={diffs} key={key} title={key} /> // this takes a long time for large diffs
|
||||
)),
|
||||
};
|
||||
}, [diff, oldValue, newValue]);
|
||||
|
||||
const { value } = loader;
|
||||
if (!value || !oldValue) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (!value.diffs.length) {
|
||||
return <div>No changes in this dashboard</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.spacer}>{value.diffs}</div>
|
||||
|
||||
<h4>JSON Diff</h4>
|
||||
<DiffViewer oldValue={value.oldJSON} newValue={value.newJSON} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
spacer: css`
|
||||
margin-bottom: ${theme.v1.spacing.xl};
|
||||
`,
|
||||
});
|
@ -0,0 +1,128 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Drawer, Tab, TabsBar } from '@grafana/ui';
|
||||
import { SaveDashboardData, SaveDashboardModalProps, SaveDashboardOptions } from './types';
|
||||
import { jsonDiff } from '../VersionHistory/utils';
|
||||
import { useAsync } from 'react-use';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { useDashboardSave } from './useDashboardSave';
|
||||
import { SaveProvisionedDashboardForm } from './forms/SaveProvisionedDashboardForm';
|
||||
import { SaveDashboardErrorProxy } from './SaveDashboardErrorProxy';
|
||||
import { SaveDashboardAsForm } from './forms/SaveDashboardAsForm';
|
||||
import { SaveDashboardForm } from './forms/SaveDashboardForm';
|
||||
import { SaveDashboardDiff } from './SaveDashboardDiff';
|
||||
|
||||
export const SaveDashboardDrawer = ({ dashboard, onDismiss, onSaveSuccess, isCopy }: SaveDashboardModalProps) => {
|
||||
const [options, setOptions] = useState<SaveDashboardOptions>({});
|
||||
|
||||
const isProvisioned = dashboard.meta.provisioned;
|
||||
const isNew = dashboard.version === 0;
|
||||
|
||||
const previous = useAsync(async () => {
|
||||
if (isNew) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result = await backendSrv.getDashboardByUid(dashboard.uid);
|
||||
return result.dashboard;
|
||||
}, [dashboard, isNew]);
|
||||
|
||||
const data = useMemo<SaveDashboardData>(() => {
|
||||
const clone = dashboard.getSaveModelClone({
|
||||
saveTimerange: Boolean(options.saveTimerange),
|
||||
saveVariables: Boolean(options.saveVariables),
|
||||
});
|
||||
|
||||
if (!previous.value) {
|
||||
return { clone, diff: {}, diffCount: 0, hasChanges: false };
|
||||
}
|
||||
|
||||
const cloneJSON = JSON.stringify(clone, null, 2);
|
||||
const cloneSafe = JSON.parse(cloneJSON); // avoids undefined issues
|
||||
|
||||
const diff = jsonDiff(previous.value, cloneSafe);
|
||||
let diffCount = 0;
|
||||
for (const d of Object.values(diff)) {
|
||||
diffCount += d.length;
|
||||
}
|
||||
|
||||
return {
|
||||
clone,
|
||||
diff,
|
||||
diffCount,
|
||||
hasChanges: diffCount > 0 && !isNew,
|
||||
};
|
||||
}, [dashboard, previous.value, options, isNew]);
|
||||
|
||||
const [showDiff, setShowDiff] = useState(false);
|
||||
const { state, onDashboardSave } = useDashboardSave(dashboard);
|
||||
const onSuccess = onSaveSuccess
|
||||
? () => {
|
||||
onDismiss();
|
||||
onSaveSuccess();
|
||||
}
|
||||
: onDismiss;
|
||||
|
||||
const renderBody = () => {
|
||||
if (showDiff) {
|
||||
return <SaveDashboardDiff diff={data.diff} oldValue={previous.value} newValue={data.clone} />;
|
||||
}
|
||||
|
||||
if (isNew || isCopy) {
|
||||
return (
|
||||
<SaveDashboardAsForm
|
||||
dashboard={dashboard}
|
||||
onCancel={onDismiss}
|
||||
onSuccess={onSuccess}
|
||||
onSubmit={onDashboardSave}
|
||||
isNew={isNew}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isProvisioned) {
|
||||
return <SaveProvisionedDashboardForm dashboard={dashboard} onCancel={onDismiss} onSuccess={onSuccess} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SaveDashboardForm
|
||||
dashboard={dashboard}
|
||||
saveModel={data}
|
||||
onCancel={onDismiss}
|
||||
onSuccess={onSuccess}
|
||||
onSubmit={onDashboardSave}
|
||||
options={options}
|
||||
onOptionsChange={setOptions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
if (state.error) {
|
||||
return (
|
||||
<SaveDashboardErrorProxy
|
||||
error={state.error}
|
||||
dashboard={dashboard}
|
||||
dashboardSaveModel={data.clone}
|
||||
onDismiss={onDismiss}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={isCopy ? 'Save dashboard copy' : 'Save dashboard'}
|
||||
onClose={onDismiss}
|
||||
width={'40%'}
|
||||
subtitle={dashboard.title}
|
||||
tabs={
|
||||
<TabsBar>
|
||||
<Tab label={'Details'} active={!showDiff} onChangeTab={() => setShowDiff(false)} />
|
||||
<Tab label={'Changes'} active={showDiff} onChangeTab={() => setShowDiff(true)} counter={data.diffCount} />
|
||||
</TabsBar>
|
||||
}
|
||||
expandable
|
||||
scrollableContent
|
||||
>
|
||||
{renderBody()}
|
||||
</Drawer>
|
||||
);
|
||||
};
|
@ -1,51 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
import { SaveDashboardForm } from './forms/SaveDashboardForm';
|
||||
import { SaveDashboardErrorProxy } from './SaveDashboardErrorProxy';
|
||||
import { useDashboardSave } from './useDashboardSave';
|
||||
import { SaveDashboardModalProps } from './types';
|
||||
|
||||
export const SaveDashboardModal: React.FC<SaveDashboardModalProps> = ({ dashboard, onDismiss, onSaveSuccess }) => {
|
||||
const { state, onDashboardSave } = useDashboardSave(dashboard);
|
||||
const [dashboardSaveModelClone, setDashboardSaveModelClone] = useState();
|
||||
|
||||
return (
|
||||
<>
|
||||
{state.error && (
|
||||
<SaveDashboardErrorProxy
|
||||
error={state.error}
|
||||
dashboard={dashboard}
|
||||
dashboardSaveModel={dashboardSaveModelClone}
|
||||
onDismiss={onDismiss}
|
||||
/>
|
||||
)}
|
||||
{!state.error && (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
title="Save dashboard"
|
||||
icon="copy"
|
||||
onDismiss={onDismiss}
|
||||
className={css`
|
||||
width: 500px;
|
||||
`}
|
||||
>
|
||||
<SaveDashboardForm
|
||||
dashboard={dashboard}
|
||||
onCancel={onDismiss}
|
||||
onSuccess={() => {
|
||||
onDismiss();
|
||||
if (onSaveSuccess) {
|
||||
onSaveSuccess();
|
||||
}
|
||||
}}
|
||||
onSubmit={(clone, options, dashboard) => {
|
||||
setDashboardSaveModelClone(clone);
|
||||
return onDashboardSave(clone, options, dashboard);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
import { SaveProvisionedDashboard } from './SaveProvisionedDashboard';
|
||||
import { SaveDashboardAsModal } from './SaveDashboardAsModal';
|
||||
import { SaveDashboardModalProps } from './types';
|
||||
import { SaveDashboardModal } from './SaveDashboardModal';
|
||||
|
||||
export const SaveDashboardModalProxy: React.FC<SaveDashboardModalProps> = ({ dashboard, onDismiss, onSaveSuccess }) => {
|
||||
const isProvisioned = dashboard.meta.provisioned;
|
||||
const isNew = dashboard.version === 0;
|
||||
const isChanged = dashboard.version > 0;
|
||||
|
||||
const modalProps = {
|
||||
dashboard,
|
||||
onDismiss,
|
||||
onSaveSuccess,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isChanged && !isProvisioned && <SaveDashboardModal {...modalProps} />}
|
||||
{isProvisioned && <SaveProvisionedDashboard {...modalProps} />}
|
||||
{isNew && <SaveDashboardAsModal {...modalProps} isNew />}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Modal } from '@grafana/ui';
|
||||
import { SaveProvisionedDashboardForm } from './forms/SaveProvisionedDashboardForm';
|
||||
import { SaveDashboardModalProps } from './types';
|
||||
|
||||
export const SaveProvisionedDashboard: React.FC<SaveDashboardModalProps> = ({ dashboard, onDismiss }) => {
|
||||
return (
|
||||
<Modal isOpen={true} title="Cannot save provisioned dashboard" icon="copy" onDismiss={onDismiss}>
|
||||
<SaveProvisionedDashboardForm dashboard={dashboard} onCancel={onDismiss} onSuccess={onDismiss} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Button, Input, Switch, Form, Field, InputControl, Modal } from '@grafana/ui';
|
||||
import { Button, Input, Switch, Form, Field, InputControl, HorizontalGroup } from '@grafana/ui';
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||
import { SaveDashboardFormProps } from '../types';
|
||||
@ -119,17 +119,19 @@ export const SaveDashboardAsForm: React.FC<SaveDashboardAsFormProps> = ({
|
||||
name="$folder"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Copy tags">
|
||||
<Switch {...register('copyTags')} />
|
||||
</Field>
|
||||
<Modal.ButtonRow>
|
||||
{!isNew && (
|
||||
<Field label="Copy tags">
|
||||
<Switch {...register('copyTags')} />
|
||||
</Field>
|
||||
)}
|
||||
<HorizontalGroup>
|
||||
<Button type="button" variant="secondary" onClick={onCancel} fill="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" aria-label="Save dashboard button">
|
||||
Save
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
</HorizontalGroup>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
|
@ -3,6 +3,7 @@ import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { SaveDashboardForm } from './SaveDashboardForm';
|
||||
import { SaveDashboardOptions } from '../types';
|
||||
|
||||
const prepareDashboardMock = (
|
||||
timeChanged: boolean,
|
||||
@ -36,6 +37,16 @@ const renderAndSubmitForm = async (dashboard: any, submitSpy: any) => {
|
||||
submitSpy(jsonModel);
|
||||
return { status: 'success' };
|
||||
}}
|
||||
saveModel={{
|
||||
clone: dashboard,
|
||||
diff: {},
|
||||
diffCount: 0,
|
||||
hasChanges: true,
|
||||
}}
|
||||
options={{}}
|
||||
onOptionsChange={(opts: SaveDashboardOptions) => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -56,6 +67,16 @@ describe('SaveDashboardAsForm', () => {
|
||||
onSubmit={async () => {
|
||||
return {};
|
||||
}}
|
||||
saveModel={{
|
||||
clone: prepareDashboardMock(true, true, jest.fn(), jest.fn()) as any,
|
||||
diff: {},
|
||||
diffCount: 0,
|
||||
hasChanges: true,
|
||||
}}
|
||||
options={{}}
|
||||
onOptionsChange={(opts: SaveDashboardOptions) => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -1,70 +1,106 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { Button, Checkbox, Form, Modal, TextArea } from '@grafana/ui';
|
||||
import { Button, Checkbox, Form, TextArea } from '@grafana/ui';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { SaveDashboardFormProps } from '../types';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { SaveDashboardData, SaveDashboardOptions } from '../types';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
|
||||
interface SaveDashboardFormDTO {
|
||||
interface FormDTO {
|
||||
message: string;
|
||||
saveVariables: boolean;
|
||||
saveTimerange: boolean;
|
||||
}
|
||||
|
||||
export const SaveDashboardForm: React.FC<SaveDashboardFormProps> = ({ dashboard, onCancel, onSuccess, onSubmit }) => {
|
||||
type Props = {
|
||||
dashboard: DashboardModel; // original
|
||||
saveModel: SaveDashboardData; // already cloned
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
onSubmit?: (clone: any, options: SaveDashboardOptions, dashboard: DashboardModel) => Promise<any>;
|
||||
options: SaveDashboardOptions;
|
||||
onOptionsChange: (opts: SaveDashboardOptions) => void;
|
||||
};
|
||||
|
||||
export const SaveDashboardForm = ({
|
||||
dashboard,
|
||||
saveModel,
|
||||
options,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
onOptionsChange,
|
||||
}: Props) => {
|
||||
const hasTimeChanged = useMemo(() => dashboard.hasTimeChanged(), [dashboard]);
|
||||
const hasVariableChanged = useMemo(() => dashboard.hasVariableValuesChanged(), [dashboard]);
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
return (
|
||||
<Form
|
||||
onSubmit={async (data: SaveDashboardFormDTO) => {
|
||||
onSubmit={async (data: FormDTO) => {
|
||||
if (!onSubmit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await onSubmit(dashboard.getSaveModelClone(data), data, dashboard);
|
||||
setSaving(true);
|
||||
const result = await onSubmit(saveModel.clone, options, dashboard);
|
||||
if (result.status === 'success') {
|
||||
if (data.saveVariables) {
|
||||
if (options.saveVariables) {
|
||||
dashboard.resetOriginalVariables();
|
||||
}
|
||||
if (data.saveTimerange) {
|
||||
if (options.saveTimerange) {
|
||||
dashboard.resetOriginalTime();
|
||||
}
|
||||
onSuccess();
|
||||
}
|
||||
setSaving(false);
|
||||
}}
|
||||
>
|
||||
{({ register, errors }) => (
|
||||
<>
|
||||
<div>
|
||||
{hasTimeChanged && (
|
||||
<Checkbox
|
||||
{...register('saveTimerange')}
|
||||
label="Save current time range as dashboard default"
|
||||
aria-label={selectors.pages.SaveDashboardModal.saveTimerange}
|
||||
/>
|
||||
)}
|
||||
{hasVariableChanged && (
|
||||
<Checkbox
|
||||
{...register('saveVariables')}
|
||||
label="Save current variable values as dashboard default"
|
||||
aria-label={selectors.pages.SaveDashboardModal.saveVariables}
|
||||
/>
|
||||
)}
|
||||
{(hasVariableChanged || hasTimeChanged) && <div className="gf-form-group" />}
|
||||
<Stack direction="column" gap={2}>
|
||||
{hasTimeChanged && (
|
||||
<Checkbox
|
||||
checked={options.saveTimerange}
|
||||
onChange={() =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
saveTimerange: !options.saveTimerange,
|
||||
})
|
||||
}
|
||||
label="Save current time range as dashboard default"
|
||||
aria-label={selectors.pages.SaveDashboardModal.saveTimerange}
|
||||
/>
|
||||
)}
|
||||
{hasVariableChanged && (
|
||||
<Checkbox
|
||||
checked={options.saveVariables}
|
||||
onChange={() =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
saveVariables: !options.saveVariables,
|
||||
})
|
||||
}
|
||||
label="Save current variable values as dashboard default"
|
||||
aria-label={selectors.pages.SaveDashboardModal.saveVariables}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextArea {...register('message')} placeholder="Add a note to describe your changes." autoFocus />
|
||||
</div>
|
||||
<TextArea {...register('message')} placeholder="Add a note to describe your changes." autoFocus rows={5} />
|
||||
|
||||
<Modal.ButtonRow>
|
||||
<Stack alignItems="center">
|
||||
<Button variant="secondary" onClick={onCancel} fill="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" aria-label={selectors.pages.SaveDashboardModal.save}>
|
||||
Save
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!saveModel.hasChanges}
|
||||
icon={saving ? 'fa fa-spinner' : undefined}
|
||||
aria-label={selectors.pages.SaveDashboardModal.save}
|
||||
>
|
||||
{saving ? '' : 'Save'}
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
</>
|
||||
{!saveModel.hasChanges && <div>No changes to save</div>}
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
|
@ -1,10 +1,11 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { Button, ClipboardButton, Modal, stylesFactory, TextArea, useTheme } from '@grafana/ui';
|
||||
import { Button, ClipboardButton, HorizontalGroup, stylesFactory, TextArea, useTheme } from '@grafana/ui';
|
||||
import { SaveDashboardFormProps } from '../types';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
|
||||
export const SaveProvisionedDashboardForm: React.FC<SaveDashboardFormProps> = ({ dashboard, onCancel }) => {
|
||||
const theme = useTheme();
|
||||
@ -29,7 +30,7 @@ export const SaveProvisionedDashboardForm: React.FC<SaveDashboardFormProps> = ({
|
||||
const styles = getStyles(theme);
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Stack direction="column" gap={2}>
|
||||
<div>
|
||||
This dashboard cannot be saved from the Grafana UI because it has been provisioned from another source. Copy
|
||||
the JSON or save it to a file below, then you can update your dashboard in the provisioning source.
|
||||
@ -57,7 +58,7 @@ export const SaveProvisionedDashboardForm: React.FC<SaveDashboardFormProps> = ({
|
||||
}}
|
||||
className={styles.json}
|
||||
/>
|
||||
<Modal.ButtonRow>
|
||||
<HorizontalGroup>
|
||||
<Button variant="secondary" onClick={onCancel} fill="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
@ -65,8 +66,8 @@ export const SaveProvisionedDashboardForm: React.FC<SaveDashboardFormProps> = ({
|
||||
Copy JSON to clipboard
|
||||
</ClipboardButton>
|
||||
<Button onClick={saveToFile}>Save JSON to file</Button>
|
||||
</Modal.ButtonRow>
|
||||
</div>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,12 @@
|
||||
import { CloneOptions, DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
import { Diffs } from '../VersionHistory/utils';
|
||||
|
||||
export interface SaveDashboardData {
|
||||
clone: DashboardModel; // cloned copy
|
||||
diff: Diffs;
|
||||
diffCount: number; // cumulative count
|
||||
hasChanges: boolean; // not new and has changes
|
||||
}
|
||||
|
||||
export interface SaveDashboardOptions extends CloneOptions {
|
||||
folderId?: number;
|
||||
@ -18,4 +26,5 @@ export interface SaveDashboardModalProps {
|
||||
dashboard: DashboardModel;
|
||||
onDismiss: () => void;
|
||||
onSaveSuccess?: () => void;
|
||||
isCopy?: boolean;
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
AnnotationQuery,
|
||||
AppEvent,
|
||||
DashboardCursorSync,
|
||||
dateTime,
|
||||
dateTimeFormat,
|
||||
dateTimeFormatTimeAgo,
|
||||
DateTimeInput,
|
||||
@ -1077,7 +1078,16 @@ export class DashboardModel implements TimeModel {
|
||||
}
|
||||
|
||||
hasTimeChanged() {
|
||||
return !isEqual(this.time, this.originalTime);
|
||||
const { time, originalTime } = this;
|
||||
if (isEqual(time, originalTime)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare momemt values vs strings values
|
||||
return !(
|
||||
isEqual(dateTime(time?.from), dateTime(originalTime?.from)) &&
|
||||
isEqual(dateTime(time?.to), dateTime(originalTime?.to))
|
||||
);
|
||||
}
|
||||
|
||||
resetOriginalVariables(initial = false) {
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
transformDataFrame,
|
||||
TimeZone,
|
||||
} from '@grafana/data';
|
||||
import { Button, Container, Spinner, Table } from '@grafana/ui';
|
||||
import { Button, Spinner, Table } from '@grafana/ui';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { InspectDataOptions } from './InspectDataOptions';
|
||||
import { getPanelInspectorStyles } from './styles';
|
||||
@ -232,8 +232,8 @@ export class InspectDataTab extends PureComponent<Props, State> {
|
||||
const hasTraces = dataFrames.some((df) => df?.meta?.preferredVisualisationType === 'trace');
|
||||
|
||||
return (
|
||||
<div className={styles.dataTabContent} aria-label={selectors.components.PanelInspector.Data.content}>
|
||||
<div className={styles.actionsWrapper}>
|
||||
<div className={styles.wrap} aria-label={selectors.components.PanelInspector.Data.content}>
|
||||
<div className={styles.toolbar}>
|
||||
<InspectDataOptions
|
||||
data={data}
|
||||
panel={panel}
|
||||
@ -281,7 +281,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Container grow={1}>
|
||||
<div className={styles.content}>
|
||||
<AutoSizer>
|
||||
{({ width, height }) => {
|
||||
if (width === 0) {
|
||||
@ -295,7 +295,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
</Container>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ export const InspectErrorTab: React.FC<InspectErrorTabProps> = ({ error }) => {
|
||||
if (error.data) {
|
||||
return (
|
||||
<>
|
||||
<h3>{error.data.message}</h3>
|
||||
<h4>{error.data.message}</h4>
|
||||
<JSONFormatter json={error} open={2} />
|
||||
</>
|
||||
);
|
||||
|
@ -129,7 +129,7 @@ export class InspectJSONTab extends PureComponent<Props, State> {
|
||||
const styles = getPanelInspectorStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.wrap}>
|
||||
<div className={styles.toolbar} aria-label={selectors.components.PanelInspector.Json.content}>
|
||||
<Field label="Select source" className="flex-grow-1">
|
||||
<Select
|
||||
@ -162,7 +162,7 @@ export class InspectJSONTab extends PureComponent<Props, State> {
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,57 +0,0 @@
|
||||
import React, { FC } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { stylesFactory, useTheme, Tab, TabsBar } from '@grafana/ui';
|
||||
import { GrafanaTheme, SelectableValue, PanelData, getValueFormat, formattedValueToString } from '@grafana/data';
|
||||
import { InspectTab } from '../inspector/types';
|
||||
|
||||
interface Props {
|
||||
tab: InspectTab;
|
||||
tabs: Array<{ label: string; value: InspectTab }>;
|
||||
data?: PanelData;
|
||||
onSelectTab: (tab: SelectableValue<InspectTab>) => void;
|
||||
}
|
||||
|
||||
export const InspectSubtitle: FC<Props> = ({ tab, tabs, onSelectTab, data }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
{data && <div className="muted">{formatStats(data)}</div>}
|
||||
<TabsBar className={styles.tabsBar}>
|
||||
{tabs.map((t, index) => {
|
||||
return (
|
||||
<Tab
|
||||
key={`${t.value}-${index}`}
|
||||
label={t.label}
|
||||
active={t.value === tab}
|
||||
onChangeTab={() => onSelectTab(t)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TabsBar>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
tabsBar: css`
|
||||
padding-left: ${theme.spacing.md};
|
||||
margin: ${theme.spacing.lg} -${theme.spacing.sm} -${theme.spacing.lg} -${theme.spacing.lg};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
function formatStats(data: PanelData) {
|
||||
const { request } = data;
|
||||
if (!request) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const queryCount = request.targets.length;
|
||||
const requestTime = request.endTime ? request.endTime - request.startTime : 0;
|
||||
const formatted = formattedValueToString(getValueFormat('ms')(requestTime));
|
||||
|
||||
return `${queryCount} queries with total query time of ${formatted}`;
|
||||
}
|
@ -264,7 +264,7 @@ export class QueryInspector extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.wrap}>
|
||||
<div aria-label={selectors.components.PanelInspector.Query.content}>
|
||||
<h3 className="section-heading">Query inspector</h3>
|
||||
<p className="small muted">
|
||||
@ -306,7 +306,7 @@ export class QueryInspector extends PureComponent<Props, State> {
|
||||
)}
|
||||
<div className="flex-grow-1" />
|
||||
</div>
|
||||
<div className={styles.contentQueryInspector}>
|
||||
<div className={styles.content}>
|
||||
{isLoading && <LoadingPlaceholder text="Loading query inspector..." />}
|
||||
{!isLoading && haveData && (
|
||||
<JSONFormatter json={response} open={openNodes} onDidRender={this.setFormattedJson} />
|
||||
@ -315,7 +315,7 @@ export class QueryInspector extends PureComponent<Props, State> {
|
||||
<p className="muted">No request and response collected yet. Hit refresh button</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -25,11 +25,6 @@ export const getPanelInspectorStyles = stylesFactory(() => {
|
||||
content: css`
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
padding-bottom: 16px;
|
||||
`,
|
||||
contentQueryInspector: css`
|
||||
flex-grow: 1;
|
||||
padding: ${config.theme.spacing.md} 0;
|
||||
`,
|
||||
editor: css`
|
||||
font-family: monospace;
|
||||
@ -42,20 +37,6 @@ export const getPanelInspectorStyles = stylesFactory(() => {
|
||||
dataFrameSelect: css`
|
||||
flex-grow: 2;
|
||||
`,
|
||||
tabContent: css`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`,
|
||||
dataTabContent: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`,
|
||||
actionsWrapper: css`
|
||||
display: flex;
|
||||
`,
|
||||
leftActions: css`
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
Loading…
Reference in New Issue
Block a user