From 1114d33936b89936c8acba1b1eff87773f5ac9c9 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Thu, 1 May 2025 17:06:07 +0200 Subject: [PATCH] Dashboard export: Allow exports by resource and in YAML format (#104149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Dashboard export: Allow YAML export * use js-yaml to fix ci errors * add yaml transformation in the "Copy to cilpboard" * simplify * new ui for resource exports * simplify * Don't show export mode for v2 dashboard * Add metadata, apiVersion, add logic for export type resources * i18n; switch title to as code * update export as file button * Remove managedFields from metadata export * Remove metadata fields that are not needed for sharing externally * Copy * fix legacy mode * address bugs * Update public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.tsx Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com> * i18n * improve classic mode; rename * Update public/app/features/dashboard-scene/sharing/ShareExportTab.tsx Co-authored-by: Dominik Prokop * change order --------- Co-authored-by: Haris Rozajac Co-authored-by: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com> --- .../dashboard-scene/scene/DashboardScene.tsx | 12 +- .../actions/ExportDashboardButton.tsx | 46 +-- .../serialization/DashboardSceneSerializer.ts | 1 + .../transformSaveModelSchemaV2ToScene.ts | 4 +- .../transformSaveModelToScene.ts | 8 +- .../{ExportAsJson.tsx => ExportAsCode.tsx} | 86 +++--- .../sharing/ExportButton/ExportMenu.tsx | 8 +- .../sharing/ExportButton/ResourceExport.tsx | 113 ++++++++ .../sharing/ShareDrawer/ShareDrawer.tsx | 4 +- .../sharing/ShareExportTab.tsx | 264 ++++++++++++++---- public/app/features/dashboard/api/v1.ts | 12 +- public/app/features/dashboard/api/v2.ts | 12 +- public/locales/en-US/grafana.json | 15 +- 13 files changed, 441 insertions(+), 144 deletions(-) rename public/app/features/dashboard-scene/sharing/ExportButton/{ExportAsJson.tsx => ExportAsCode.tsx} (63%) create mode 100644 public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 29433b0bb56..d4ac35e827b 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -725,17 +725,23 @@ export class DashboardScene extends SceneObjectBase impleme }; /** Hacky temp function until we refactor transformSaveModelToScene a bit */ - setInitialSaveModel(model?: Dashboard, meta?: DashboardMeta): void; - setInitialSaveModel(model?: DashboardV2Spec, meta?: DashboardWithAccessInfo['metadata']): void; + setInitialSaveModel(model?: Dashboard, meta?: DashboardMeta, apiVersion?: string): void; + setInitialSaveModel( + model?: DashboardV2Spec, + meta?: DashboardWithAccessInfo['metadata'], + apiVersion?: string + ): void; public setInitialSaveModel( saveModel?: Dashboard | DashboardV2Spec, - meta?: DashboardMeta | DashboardWithAccessInfo['metadata'] + meta?: DashboardMeta | DashboardWithAccessInfo['metadata'], + apiVersion?: string ): void { this.serializer.initializeElementMapping(saveModel); this.serializer.initializeDSReferencesMapping(saveModel); const sortedModel = sortedDeepCloneWithoutNulls(saveModel); this.serializer.initialSaveModel = sortedModel; this.serializer.metadata = meta; + this.serializer.apiVersion = apiVersion; } public getTrackingInformation() { diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ExportDashboardButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ExportDashboardButton.tsx index 4d361dfcdfb..f95b79ecf2b 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ExportDashboardButton.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ExportDashboardButton.tsx @@ -1,5 +1,5 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; -import { locationService } from '@grafana/runtime'; +import { config, locationService } from '@grafana/runtime'; import { t } from 'app/core/internationalization'; import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; @@ -11,23 +11,29 @@ import { ShareExportDashboardButton } from './ShareExportDashboardButton'; const newExportButtonSelector = e2eSelectors.pages.Dashboard.DashNav.NewExportButton; -export const ExportDashboardButton = ({ dashboard }: ToolbarActionProps) => ( - } - groupTestId={newExportButtonSelector.container} - buttonLabel={t('dashboard.toolbar.new.export.title', 'Export')} - buttonTooltip={t('dashboard.toolbar.new.export.tooltip', 'Export as JSON')} - buttonTestId={newExportButtonSelector.container} - onButtonClick={() => { - locationService.partial({ shareView: shareDashboardType.export }); +export const ExportDashboardButton = ({ dashboard }: ToolbarActionProps) => { + const buttonTooltip = config.featureToggles.kubernetesDashboards + ? t('dashboard.toolbar.new.export.tooltip.as-code', 'Export as code') + : t('dashboard.toolbar.new.export.tooltip.json', 'Export as JSON'); - DashboardInteractions.sharingCategoryClicked({ - item: shareDashboardType.export, - shareResource: getTrackingSource(), - }); - }} - arrowLabel={t('dashboard.toolbar.new.export.arrow', 'Export')} - arrowTestId={newExportButtonSelector.arrowMenu} - dashboard={dashboard} - /> -); + return ( + } + groupTestId={newExportButtonSelector.container} + buttonLabel={t('dashboard.toolbar.new.export.title', 'Export')} + buttonTooltip={buttonTooltip} + buttonTestId={newExportButtonSelector.container} + onButtonClick={() => { + locationService.partial({ shareView: shareDashboardType.export }); + + DashboardInteractions.sharingCategoryClicked({ + item: shareDashboardType.export, + shareResource: getTrackingSource(), + }); + }} + arrowLabel={t('dashboard.toolbar.new.export.arrow', 'Export')} + arrowTestId={newExportButtonSelector.arrowMenu} + dashboard={dashboard} + /> + ); +}; diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts index 373b72358c8..fc1083b9a0d 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts @@ -35,6 +35,7 @@ export interface DashboardSceneSerializerLike T; diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index d89c53dbd47..0fa343a5c4e 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -87,7 +87,7 @@ export type TypedVariableModelV2 = | AdhocVariableKind; export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo): DashboardScene { - const { spec: dashboard, metadata } = dto; + const { spec: dashboard, metadata, apiVersion } = dto; // annotations might not come with the builtIn Grafana annotation, we need to add it @@ -221,7 +221,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo) { +function ExportAsCodeRenderer({ model }: SceneComponentProps) { const styles = useStyles2(getStyles); - const { isSharingExternally } = model.useState(); + const { isSharingExternally, isViewingYAML, exportMode } = model.useState(); const dashboardJson = useAsync(async () => { const json = await model.getExportableDashboardJson(); + return json; - }, [isSharingExternally]); + }, [isSharingExternally, exportMode]); const stringifiedDashboardJson = JSON.stringify(dashboardJson.value?.json, null, 2); - const hasLibraryPanels = dashboardJson.value?.hasLibraryPanels; - const isV2Dashboard = dashboardJson.value?.json && 'elements' in dashboardJson.value.json; - const showV2LibPanelAlert = isV2Dashboard && isSharingExternally && hasLibraryPanels; + const stringifiedDashboardYAML = yaml.dump(dashboardJson.value?.json, { + skipInvalid: true, + }); + const stringifiedDashboard = isViewingYAML ? stringifiedDashboardYAML : stringifiedDashboardJson; const onClickDownload = async () => { await model.onSaveAsFile(); @@ -61,50 +56,41 @@ function ExportAsJsonRenderer({ model }: SceneComponentProps) {

- Copy or download a JSON file containing the JSON of your dashboard + Copy or download a file containing the definition of your dashboard

- - + + {config.featureToggles.kubernetesDashboards ? ( + + ) : ( + + )} - {showV2LibPanelAlert && ( - - - The dynamic dashboard functionality is experimental, and has not full feature parity with current - dashboards behaviour. It is based on a new schema format, that does not support library panels. This means - that when exporting the dashboard to use it in another instance, we will not include library panels. We - intend to support them as we progress in the feature{' '} - - life cycle - - . - - - )} -
{({ width, height }) => { - if (stringifiedDashboardJson) { + if (stringifiedDashboard) { return ( ) { variant="secondary" icon="copy" disabled={dashboardJson.loading} - getText={() => stringifiedDashboardJson ?? ''} + getText={() => stringifiedDashboard ?? ''} onClipboardCopy={() => { DashboardInteractions.exportCopyJsonClicked(); }} diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.tsx index 02e99fe5ff9..e96704adbba 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; -import { locationService } from '@grafana/runtime'; +import { config, locationService } from '@grafana/runtime'; import { IconName, Menu } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; @@ -37,11 +37,15 @@ export default function ExportMenu({ dashboard }: { dashboard: DashboardScene }) customShareDrawerItem.forEach((d) => menuItems.push(d)); + const label = config.featureToggles.kubernetesDashboards + ? t('dashboard.toolbar.new.export.tooltip.as-code', 'Export as code') + : t('share-dashboard.menu.export-json-title', 'Export as JSON'); + menuItems.push({ shareId: shareDashboardType.export, testId: newExportButtonSelector.exportAsJson, icon: 'arrow', - label: t('share-dashboard.menu.export-json-title', 'Export as JSON'), + label, renderCondition: true, onClick: () => onMenuItemClick(shareDashboardType.export), }); diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx new file mode 100644 index 00000000000..9c91aaaa577 --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx @@ -0,0 +1,113 @@ +import { AsyncState } from 'react-use/lib/useAsync'; + +import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Alert, Label, RadioButtonGroup, Stack, Switch, TextLink } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; +import { DashboardJson } from 'app/features/manage-dashboards/types'; + +import { ExportableResource } from '../ShareExportTab'; + +export enum ExportMode { + Classic = 'classic', + V1Resource = 'v1-resource', + V2Resource = 'v2-resource', +} + +interface Props { + dashboardJson: AsyncState<{ + json: Dashboard | DashboardJson | DashboardV2Spec | ExportableResource | { error: unknown }; + hasLibraryPanels?: boolean; + initialSaveModelVersion: 'v1' | 'v2'; + }>; + isSharingExternally: boolean; + exportMode: ExportMode; + isViewingYAML: boolean; + onExportModeChange: (mode: ExportMode) => void; + onShareExternallyChange: () => void; + onViewYAML: () => void; +} + +export function ResourceExport({ + dashboardJson, + isSharingExternally, + exportMode, + isViewingYAML, + onExportModeChange, + onShareExternallyChange, + onViewYAML, +}: Props) { + const hasLibraryPanels = dashboardJson.value?.hasLibraryPanels; + const initialSaveModelVersion = dashboardJson.value?.initialSaveModelVersion; + const isV2Dashboard = + dashboardJson.value?.json && 'spec' in dashboardJson.value.json && 'elements' in dashboardJson.value.json.spec; + const showV2LibPanelAlert = isV2Dashboard && isSharingExternally && hasLibraryPanels; + + const switchExportLabel = + exportMode === ExportMode.V2Resource + ? t('export.json.export-remove-ds-refs', 'Remove deployment details') + : t('share-modal.export.share-externally-label', `Export for sharing externally`); + const switchExportModeLabel = t('export.json.export-mode', 'Model'); + const switchExportFormatLabel = t('export.json.export-format', 'Format'); + + return ( + + + {initialSaveModelVersion === 'v1' && ( + + + onExportModeChange(value)} + /> + + )} + {exportMode !== ExportMode.Classic && ( + + + + + )} + {(isV2Dashboard || exportMode === ExportMode.Classic) && ( + + + + + )} + + + {showV2LibPanelAlert && ( + + + The dynamic dashboard functionality is experimental, and has not full feature parity with current dashboards + behaviour. It is based on a new schema format, that does not support library panels. This means that when + exporting the dashboard to use it in another instance, we will not include library panels. We intend to + support them as we progress in the feature{' '} + + life cycle + + . + + + )} + + ); +} diff --git a/public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawer.tsx b/public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawer.tsx index 317ec324e49..bbb48cc6524 100644 --- a/public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawer.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawer.tsx @@ -5,7 +5,7 @@ import { Drawer } from '@grafana/ui'; import { shareDashboardType } from '../../../dashboard/components/ShareModal/utils'; import { DashboardScene } from '../../scene/DashboardScene'; import { getDashboardSceneFor } from '../../utils/utils'; -import { ExportAsJson } from '../ExportButton/ExportAsJson'; +import { ExportAsCode } from '../ExportButton/ExportAsCode'; import { ShareExternally } from '../ShareButton/share-externally/ShareExternally'; import { ShareInternally } from '../ShareButton/share-internally/ShareInternally'; import { ShareSnapshot } from '../ShareButton/share-snapshot/ShareSnapshot'; @@ -90,7 +90,7 @@ function getShareView( case shareDashboardType.snapshot: return new ShareSnapshot({ dashboardRef, panelRef, onDismiss }); case shareDashboardType.export: - return new ExportAsJson({ onDismiss }); + return new ExportAsCode({ onDismiss }); default: return new ShareInternally({ onDismiss }); } diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx index ab81fb4229c..177940061eb 100644 --- a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx @@ -1,24 +1,48 @@ import saveAs from 'file-saver'; +import yaml from 'js-yaml'; +import { cloneDeep } from 'lodash'; import { useAsync } from 'react-use'; import AutoSizer from 'react-virtualized-auto-sizer'; +import { config } from '@grafana/runtime'; import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes'; import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; -import { Alert, Button, ClipboardButton, CodeEditor, Field, Modal, Stack, Switch, TextLink } from '@grafana/ui'; +import { Button, ClipboardButton, CodeEditor, Field, Modal, Stack, Switch } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; +import { ObjectMeta } from 'app/features/apiserver/types'; +import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { isDashboardV2Spec } from 'app/features/dashboard/api/utils'; +import { K8S_V2_DASHBOARD_API_CONFIG } from 'app/features/dashboard/api/v2'; import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; +import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardJson } from 'app/features/manage-dashboards/types'; +import { DashboardScene } from '../scene/DashboardScene'; +import { makeExportableV1, makeExportableV2 } from '../scene/export/exporters'; +import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; +import { transformSceneToSaveModelSchemaV2 } from '../serialization/transformSceneToSaveModelSchemaV2'; +import { getVariablesCompatibility } from '../utils/getVariablesCompatibility'; import { DashboardInteractions } from '../utils/interactions'; import { getDashboardSceneFor } from '../utils/utils'; +import { ExportMode, ResourceExport } from './ExportButton/ResourceExport'; import { SceneShareTabState, ShareView } from './types'; +export interface ExportableResource { + apiVersion: string; + kind: 'Dashboard'; + metadata: DashboardWithAccessInfo['metadata'] | Partial; + spec: Dashboard | DashboardModel | DashboardV2Spec | { error: unknown }; + // A placeholder for now because as code tooling expects it + status: {}; +} + export interface ShareExportTabState extends SceneShareTabState { isSharingExternally?: boolean; isViewingJSON?: boolean; + isViewingYAML?: boolean; + exportMode?: ExportMode; } export class ShareExportTab extends SceneObjectBase implements ShareView { @@ -27,9 +51,10 @@ export class ShareExportTab extends SceneObjectBase impleme constructor(state: Omit) { super({ + ...state, isSharingExternally: false, isViewingJSON: false, - ...state, + exportMode: config.featureToggles.kubernetesDashboards ? ExportMode.Classic : undefined, }); } @@ -43,46 +68,140 @@ export class ShareExportTab extends SceneObjectBase impleme }); }; + public onExportModeChange = (exportMode: ExportMode) => { + this.setState({ + exportMode, + }); + + if (exportMode === ExportMode.Classic) { + this.setState({ + isViewingYAML: false, + }); + } + }; + public onViewJSON = () => { this.setState({ isViewingJSON: !this.state.isViewingJSON, }); }; + public onViewYAML = () => { + this.setState({ + isViewingYAML: !this.state.isViewingYAML, + }); + }; + public getClipboardText() { return; } public getExportableDashboardJson = async (): Promise<{ - json: Dashboard | DashboardJson | DashboardV2Spec | { error: unknown }; + json: Dashboard | DashboardJson | DashboardV2Spec | ExportableResource | { error: unknown }; hasLibraryPanels?: boolean; + initialSaveModelVersion: 'v1' | 'v2'; }> => { - const { isSharingExternally } = this.state; + const { isSharingExternally, exportMode } = this.state; const scene = getDashboardSceneFor(this); const exportableDashboard = await scene.serializer.makeExportableExternally(scene); + const initialSaveModel = scene.getInitialSaveModel(); + const initialSaveModelVersion = initialSaveModel && isDashboardV2Spec(initialSaveModel) ? 'v2' : 'v1'; const origDashboard = scene.serializer.getSaveModel(scene); const exportable = isSharingExternally ? exportableDashboard : origDashboard; + const metadata = getMetadata(scene, Boolean(isSharingExternally)); + + if (isDashboardV2Spec(origDashboard) && 'elements' in exportable && initialSaveModelVersion === 'v2') { + this.setState({ + exportMode: ExportMode.V2Resource, + }); - if (isDashboardV2Spec(origDashboard)) { return { - json: exportable, + json: { + apiVersion: scene.serializer.apiVersion ?? '', + kind: 'Dashboard', + metadata, + spec: exportable, + status: {}, + }, + initialSaveModelVersion, hasLibraryPanels: Object.values(origDashboard.elements).some((element) => element.kind === 'LibraryPanel'), }; } + if (exportMode === ExportMode.V1Resource) { + const spec = transformSceneToSaveModel(scene); + + return { + json: { + apiVersion: scene.serializer.apiVersion ?? '', + kind: 'Dashboard', + metadata, + spec, + status: {}, + }, + initialSaveModelVersion, + hasLibraryPanels: undefined, + }; + } + + if (exportMode === ExportMode.V2Resource) { + const spec = transformSceneToSaveModelSchemaV2(scene); + const specCopy = JSON.parse(JSON.stringify(spec)); + const statelessSpec = await makeExportableV2(specCopy); + const exportableV2 = isSharingExternally ? statelessSpec : spec; + + return { + json: { + // Forcing V2 version here because in this case we have v1 serializer + apiVersion: `${K8S_V2_DASHBOARD_API_CONFIG.group}/${K8S_V2_DASHBOARD_API_CONFIG.version}`, + kind: 'Dashboard', + metadata, + spec: exportableV2, + status: {}, + }, + initialSaveModelVersion, + }; + } + + // Classic mode + // This handles a case when: + // 1. dashboardNewLayouts feature toggle is enabled + // 2. v1 dashboard is loaded + // 3. dashboard hasn't been edited yet - if it was edited, user would be forced to save it in v2 version + if ( + initialSaveModelVersion === 'v1' && + isDashboardV2Spec(origDashboard) && + initialSaveModel && + 'panels' in initialSaveModel + ) { + const oldModel = new DashboardModel(initialSaveModel, undefined, { + getVariablesFromState: () => { + return getVariablesCompatibility(window.__grafanaSceneContext); + }, + }); + const exportableV1 = isSharingExternally ? await makeExportableV1(oldModel) : initialSaveModel; + return { + json: exportableV1, + hasLibraryPanels: undefined, + initialSaveModelVersion, + }; + } + + // legacy mode or classic mode when dashboardNewLayouts is disabled return { json: exportable, hasLibraryPanels: undefined, + initialSaveModelVersion, }; }; public onSaveAsFile = async () => { const dashboard = await this.getExportableDashboardJson(); const dashboardJsonPretty = JSON.stringify(dashboard.json, null, 2); - const { isSharingExternally } = this.state; + const { isSharingExternally, isViewingYAML } = this.state; - const blob = new Blob([dashboardJsonPretty], { + const blob = new Blob([isViewingYAML ? yaml.dump(dashboard.json) : dashboardJsonPretty], { type: 'application/json;charset=utf-8', }); @@ -91,26 +210,73 @@ export class ShareExportTab extends SceneObjectBase impleme if ('title' in dashboard.json && dashboard.json.title) { title = dashboard.json.title; } - saveAs(blob, `${title}-${time}.json`); + const extension = isViewingYAML ? 'yaml' : 'json'; + saveAs(blob, `${title}-${time}.${extension}`); DashboardInteractions.exportDownloadJsonClicked({ externally: isSharingExternally, }); }; } +function getMetadata( + scene: DashboardScene, + isSharingExternally: boolean +): DashboardWithAccessInfo['metadata'] | Partial { + let result: Partial = {}; + + if (scene.serializer.metadata) { + if ('k8s' in scene.serializer.metadata) { + result = scene.serializer.metadata.k8s ? cloneDeep(scene.serializer.metadata.k8s) : {}; + } else if ('annotations' in scene.serializer.metadata) { + result = cloneDeep(scene.serializer.metadata); + } + } + + if ('managedFields' in result) { + delete result['managedFields']; + } + + if (isSharingExternally) { + // Remove fields that are not needed for sharing externally + if ('uid' in result) { + delete result['uid']; + } + delete result['resourceVersion']; + delete result['namespace']; + + // iterate over labels and delete all keys that start with grafana.app/ + for (const key in result['labels']) { + if (key.startsWith('grafana.app/')) { + // @ts-expect-error + delete result['labels'][key]; + } + } + + // iterate over annotations and delete all keys that start with grafana.app/ + for (const key in result['annotations']) { + if (key.startsWith('grafana.app/')) { + // @ts-expect-error + delete result['annotations'][key]; + } + } + } + + return result; +} + function ShareExportTabRenderer({ model }: SceneComponentProps) { - const { isSharingExternally, isViewingJSON, modalRef } = model.useState(); + const { isSharingExternally, isViewingJSON, modalRef, exportMode, isViewingYAML } = model.useState(); const dashboardJson = useAsync(async () => { const json = await model.getExportableDashboardJson(); return json; - }, [isViewingJSON, isSharingExternally]); + }, [isViewingJSON, isSharingExternally, exportMode]); const stringifiedDashboardJson = JSON.stringify(dashboardJson.value?.json, null, 2); - const hasLibraryPanels = dashboardJson.value?.hasLibraryPanels; - - const isV2Dashboard = dashboardJson.value?.json && 'elements' in dashboardJson.value.json; - const showV2LibPanelAlert = isV2Dashboard && isSharingExternally && hasLibraryPanels; + const stringifiedDashboardYAML = yaml.dump(dashboardJson.value?.json, { + skipInvalid: true, + }); + const stringifiedDashboard = isViewingYAML ? stringifiedDashboardYAML : stringifiedDashboardJson; const exportExternallyTranslation = t('share-modal.export.share-externally-label', `Export for sharing externally`); @@ -121,35 +287,27 @@ function ShareExportTabRenderer({ model }: SceneComponentProps)

Export this dashboard.

- - - - - {showV2LibPanelAlert && ( - - - The dynamic dashboard functionality is experimental, and has not full feature parity with current - dashboards behaviour. It is based on a new schema format, that does not support library panels. This - means that when exporting the dashboard to use it in another instance, we will not include library - panels. We intend to support them as we progress in the feature{' '} - - life cycle - - . - - - )} - + {config.featureToggles.kubernetesDashboards ? ( + + ) : ( + + + + + + )} - + {isViewingYAML ? ( + + ) : ( + + )} @@ -177,9 +341,9 @@ function ShareExportTabRenderer({ model }: SceneComponentProps) if (dashboardJson.value) { return ( ) variant="secondary" icon="copy" disabled={dashboardJson.loading} - getText={() => stringifiedDashboardJson ?? ''} + getText={() => stringifiedDashboard ?? ''} > Copy to Clipboard diff --git a/public/app/features/dashboard/api/v1.ts b/public/app/features/dashboard/api/v1.ts index d02a1b06502..00b3a56a7a9 100644 --- a/public/app/features/dashboard/api/v1.ts +++ b/public/app/features/dashboard/api/v1.ts @@ -21,15 +21,17 @@ import { SaveDashboardCommand } from '../components/SaveDashboard/types'; import { DashboardAPI, DashboardVersionError, DashboardWithAccessInfo } from './types'; +export const K8S_V1_DASHBOARD_API_CONFIG = { + group: 'dashboard.grafana.app', + version: 'v1beta1', + resource: 'dashboards', +}; + export class K8sDashboardAPI implements DashboardAPI { private client: ResourceClient; constructor() { - this.client = new ScopedResourceClient({ - group: 'dashboard.grafana.app', - version: 'v1beta1', - resource: 'dashboards', - }); + this.client = new ScopedResourceClient(K8S_V1_DASHBOARD_API_CONFIG); } saveDashboard(options: SaveDashboardCommand): Promise { diff --git a/public/app/features/dashboard/api/v2.ts b/public/app/features/dashboard/api/v2.ts index 0f6d1486051..efaa4c5af5e 100644 --- a/public/app/features/dashboard/api/v2.ts +++ b/public/app/features/dashboard/api/v2.ts @@ -24,17 +24,19 @@ import { SaveDashboardCommand } from '../components/SaveDashboard/types'; import { DashboardAPI, DashboardVersionError, DashboardWithAccessInfo } from './types'; +export const K8S_V2_DASHBOARD_API_CONFIG = { + group: 'dashboard.grafana.app', + version: 'v2alpha1', + resource: 'dashboards', +}; + export class K8sDashboardV2API implements DashboardAPI | DashboardDTO, DashboardV2Spec> { private client: ResourceClient; constructor() { - this.client = new ScopedResourceClient({ - group: 'dashboard.grafana.app', - version: 'v2alpha1', - resource: 'dashboards', - }); + this.client = new ScopedResourceClient(K8S_V2_DASHBOARD_API_CONFIG); } async getDashboardDTO(uid: string) { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 57ff9764163..ae3a7156489 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -3671,7 +3671,10 @@ "export": { "arrow": "Export", "title": "Export", - "tooltip": "Export as JSON" + "tooltip": { + "as-code": "Export as code", + "json": "Export as JSON" + } }, "mark-favorite": "Mark as favorite", "more-save-options": "More save options", @@ -5109,8 +5112,11 @@ "download-button": "Download file", "download-successful_toast_message": "Your JSON has been downloaded", "export-externally-label": "Export the dashboard to use in another instance", - "info-text": "Copy or download a JSON file containing the JSON of your dashboard", - "title": "Export dashboard JSON" + "export-format": "Format", + "export-mode": "Model", + "export-remove-ds-refs": "Remove deployment details", + "info-text": "Copy or download a file containing the definition of your dashboard", + "title": "Export dashboard" }, "menu": { "export-as-json-label": "Export", @@ -8219,7 +8225,8 @@ "loading": "Loading...", "save-button": "Save to file", "share-externally-label": "Export for sharing externally", - "view-button": "View JSON" + "view-button": "View JSON", + "view-button-yaml": "View YAML" }, "library": { "info": "Create library panel."