From 4064fa51c6128cd293f35f2616ab2660ec69095c Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 13 Dec 2022 19:40:20 -0800 Subject: [PATCH] Export: move export page to a full page (not view on storage) (#60263) Co-authored-by: nmarrs --- pkg/services/navtree/navtreeimpl/admin.go | 10 + public/app/features/storage/ExportPage.tsx | 278 ++++++++++++++++++++ public/app/features/storage/ExportView.tsx | 256 ------------------ public/app/features/storage/RootView.tsx | 6 - public/app/features/storage/StoragePage.tsx | 8 - public/app/features/storage/types.ts | 1 - public/app/routes/routes.tsx | 7 + 7 files changed, 295 insertions(+), 271 deletions(-) create mode 100644 public/app/features/storage/ExportPage.tsx delete mode 100644 public/app/features/storage/ExportView.tsx diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go index 310d15d5603..00c6cf09fde 100644 --- a/pkg/services/navtree/navtreeimpl/admin.go +++ b/pkg/services/navtree/navtreeimpl/admin.go @@ -161,6 +161,16 @@ func (s *ServiceImpl) getServerAdminNode(c *models.ReqContext) *navtree.NavLink } adminNavLinks = append(adminNavLinks, storage) + if s.features.IsEnabled(featuremgmt.FlagExport) { + storage.Children = append(storage.Children, &navtree.NavLink{ + Text: "Export", + Id: "export", + SubTitle: "Export grafana settings", + Icon: "cube", + Url: s.cfg.AppSubURL + "/admin/storage/export", + }) + } + if s.features.IsEnabled(featuremgmt.FlagK8s) { storage.Children = append(storage.Children, &navtree.NavLink{ Text: "Kubernetes", diff --git a/public/app/features/storage/ExportPage.tsx b/public/app/features/storage/ExportPage.tsx new file mode 100644 index 00000000000..96e5e39caeb --- /dev/null +++ b/public/app/features/storage/ExportPage.tsx @@ -0,0 +1,278 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { useAsync, useLocalStorage } from 'react-use'; + +import { isLiveChannelMessageEvent, isLiveChannelStatusEvent, LiveChannelScope, SelectableValue } from '@grafana/data'; +import { getBackendSrv, getGrafanaLiveSrv, config } from '@grafana/runtime'; +import { + Button, + CodeEditor, + Collapse, + Field, + HorizontalGroup, + InlineField, + InlineFieldRow, + InlineSwitch, + Input, + LinkButton, + Select, + Switch, + Alert, +} from '@grafana/ui'; +import { Page } from 'app/core/components/Page/Page'; +import { useNavModel } from 'app/core/hooks/useNavModel'; +import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; + +export const EXPORT_LOCAL_STORAGE_KEY = 'grafana.export.config'; + +interface ExportStatusMessage { + running: boolean; + target: string; + started: number; + finished: number; + update: number; + count: number; + current: number; + last: string; + status: string; +} + +interface ExportJob { + format: string; // 'git'; + generalFolderPath: string; + history: boolean; + exclude: Record; + + git?: {}; +} + +const defaultJob: ExportJob = { + format: 'git', + generalFolderPath: 'general', + history: true, + exclude: {}, + git: {}, +}; + +interface ExporterInfo { + key: string; + name: string; + description: string; + children?: ExporterInfo[]; +} + +enum StorageFormat { + Git = 'git', + EntityStore = 'entityStore', +} + +const formats: Array> = [ + { label: 'GIT', value: StorageFormat.Git, description: 'Exports a fresh git repository' }, + { label: 'Entity store', value: StorageFormat.EntityStore, description: 'Export to the SQL based entity store' }, +]; + +interface Props extends GrafanaRouteComponentProps {} + +const labelWith = 18; + +export default function ExportPage(props: Props) { + const navModel = useNavModel('export'); + const [status, setStatus] = useState(); + const [body, setBody] = useLocalStorage(EXPORT_LOCAL_STORAGE_KEY, defaultJob); + const [details, setDetails] = useState(false); + + const serverOptions = useAsync(() => { + return getBackendSrv().get<{ exporters: ExporterInfo[] }>('/api/admin/export/options'); + }, []); + + const doStart = () => { + getBackendSrv() + .post('/api/admin/export', body) + .then((v) => { + if (v.cfg && v.status.running) { + setBody(v.cfg); // saves the valid parsed body + } + }); + }; + + const doStop = () => { + getBackendSrv().post('/api/admin/export/stop'); + }; + + const setInclude = useCallback( + (k: string, v: boolean) => { + if (!serverOptions.value || !body) { + return; + } + const exclude: Record = {}; + if (k === '*') { + if (!v) { + for (let exp of serverOptions.value.exporters) { + exclude[exp.key] = true; + } + } + setBody({ ...body, exclude }); + return; + } + + for (let exp of serverOptions.value.exporters) { + let val = body.exclude?.[exp.key]; + if (k === exp.key) { + val = !v; + } + if (val) { + exclude[exp.key] = val; + } + } + setBody({ ...body, exclude }); + }, + [body, setBody, serverOptions] + ); + + useEffect(() => { + const subscription = getGrafanaLiveSrv() + .getStream({ + scope: LiveChannelScope.Grafana, + namespace: 'broadcast', + path: 'export', + }) + .subscribe({ + next: (evt) => { + if (isLiveChannelMessageEvent(evt)) { + setStatus(evt.message); + } else if (isLiveChannelStatusEvent(evt)) { + setStatus(evt.message); + } + }, + }); + + return () => { + subscription.unsubscribe(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const renderView = () => { + const isEntityStoreEnabled = body?.format === StorageFormat.EntityStore && config.featureToggles.entityStore; + const shouldDisplayContent = isEntityStoreEnabled || body?.format === StorageFormat.Git; + + const statusFragment = status && ( +
+

Status

+
{JSON.stringify(status, null, 2)}
+ {status.running && ( +
+ +
+ )} +
+ ); + + const formFragment = !Boolean(status?.running) && ( +
+ + setBody({ ...body!, generalFolderPath: v.currentTarget.value })} + placeholder="root folder path" + /> + + + + + + Cancel + + + + )} +
+ ); + + const requestDetailsFragment = (isEntityStoreEnabled || body?.format === StorageFormat.Git) && ( + + { + setBody(JSON.parse(text)); // force JSON? + }} + /> + + ); + + return ( +
+ {statusFragment} + {formFragment} +
+
+ {requestDetailsFragment} +
+ ); + }; + + return ( + + {renderView()} + + ); +} diff --git a/public/app/features/storage/ExportView.tsx b/public/app/features/storage/ExportView.tsx deleted file mode 100644 index 73827a3e7bc..00000000000 --- a/public/app/features/storage/ExportView.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import React, { useEffect, useState, useCallback } from 'react'; -import { useAsync, useLocalStorage } from 'react-use'; - -import { isLiveChannelMessageEvent, isLiveChannelStatusEvent, LiveChannelScope, SelectableValue } from '@grafana/data'; -import { getBackendSrv, getGrafanaLiveSrv, config } from '@grafana/runtime'; -import { - Button, - CodeEditor, - Collapse, - Field, - HorizontalGroup, - InlineField, - InlineFieldRow, - InlineSwitch, - Input, - LinkButton, - Select, - Switch, - Alert, -} from '@grafana/ui'; - -import { StorageView } from './types'; - -export const EXPORT_LOCAL_STORAGE_KEY = 'grafana.export.config'; - -interface ExportStatusMessage { - running: boolean; - target: string; - started: number; - finished: number; - update: number; - count: number; - current: number; - last: string; - status: string; -} - -interface ExportJob { - format: string; // 'git'; - generalFolderPath: string; - history: boolean; - exclude: Record; - - git?: {}; -} - -const defaultJob: ExportJob = { - format: 'git', - generalFolderPath: 'general', - history: true, - exclude: {}, - git: {}, -}; - -interface ExporterInfo { - key: string; - name: string; - description: string; - children?: ExporterInfo[]; -} - -const formats: Array> = [ - { label: 'GIT', value: 'git', description: 'Exports a fresh git repository' }, - { label: 'Entity store', value: 'entityStore', description: 'Export to the SQL based entity store' }, -]; - -interface Props { - onPathChange: (p: string, v?: StorageView) => void; -} - -const labelWith = 18; - -export const ExportView = ({ onPathChange }: Props) => { - const [status, setStatus] = useState(); - const [body, setBody] = useLocalStorage(EXPORT_LOCAL_STORAGE_KEY, defaultJob); - const [details, setDetails] = useState(false); - - const serverOptions = useAsync(() => { - return getBackendSrv().get<{ exporters: ExporterInfo[] }>('/api/admin/export/options'); - }, []); - - const doStart = () => { - getBackendSrv() - .post('/api/admin/export', body) - .then((v) => { - if (v.cfg && v.status.running) { - setBody(v.cfg); // saves the valid parsed body - } - }); - }; - - const doStop = () => { - getBackendSrv().post('/api/admin/export/stop'); - }; - - const setInclude = useCallback( - (k: string, v: boolean) => { - if (!serverOptions.value || !body) { - return; - } - const exclude: Record = {}; - if (k === '*') { - if (!v) { - for (let exp of serverOptions.value.exporters) { - exclude[exp.key] = true; - } - } - setBody({ ...body, exclude }); - return; - } - - for (let exp of serverOptions.value.exporters) { - let val = body.exclude?.[exp.key]; - if (k === exp.key) { - val = !v; - } - if (val) { - exclude[exp.key] = val; - } - } - setBody({ ...body, exclude }); - }, - [body, setBody, serverOptions] - ); - - useEffect(() => { - const subscription = getGrafanaLiveSrv() - .getStream({ - scope: LiveChannelScope.Grafana, - namespace: 'broadcast', - path: 'export', - }) - .subscribe({ - next: (evt) => { - if (isLiveChannelMessageEvent(evt)) { - setStatus(evt.message); - } else if (isLiveChannelStatusEvent(evt)) { - setStatus(evt.message); - } - }, - }); - - return () => { - subscription.unsubscribe(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( -
- {status && ( -
-

Status

-
{JSON.stringify(status, null, 2)}
- {status.running && ( -
- -
- )} -
- )} - - {!Boolean(status?.running) && ( -
-

Export grafana instance

- - setBody({ ...body!, generalFolderPath: v.currentTarget.value })} - placeholder="root folder path" - /> - - - - - - Cancel - - -
- )} -
-
- - - { - setBody(JSON.parse(text)); // force JSON? - }} - /> - -
- ); -}; diff --git a/public/app/features/storage/RootView.tsx b/public/app/features/storage/RootView.tsx index e1c56fe1ee4..019648c25a2 100644 --- a/public/app/features/storage/RootView.tsx +++ b/public/app/features/storage/RootView.tsx @@ -3,7 +3,6 @@ import React, { useMemo, useState } from 'react'; import { useAsync } from 'react-use'; import { DataFrame, GrafanaTheme2 } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { Alert, Button, @@ -100,11 +99,6 @@ export function RootView({ root, onPathChange }: Props) { - {config.featureToggles.export && ( - - )}
{renderRoots('', roots.base)}
diff --git a/public/app/features/storage/StoragePage.tsx b/public/app/features/storage/StoragePage.tsx index dfd86c879cb..0a3645a8bb9 100644 --- a/public/app/features/storage/StoragePage.tsx +++ b/public/app/features/storage/StoragePage.tsx @@ -14,7 +14,6 @@ import { ShowConfirmModalEvent } from 'app/types/events'; import { AddRootView } from './AddRootView'; import { Breadcrumb } from './Breadcrumb'; import { CreateNewFolderModal } from './CreateNewFolderModal'; -import { ExportView } from './ExportView'; import { FileView } from './FileView'; import { FolderView } from './FolderView'; import { RootView } from './RootView'; @@ -116,13 +115,6 @@ export default function StoragePage(props: Props) { const renderView = () => { const isRoot = !path?.length || path === '/'; switch (view) { - case StorageView.Export: - if (!isRoot) { - setPath(''); - return ; - } - return ; - case StorageView.AddRoot: if (!isRoot) { setPath(''); diff --git a/public/app/features/storage/types.ts b/public/app/features/storage/types.ts index 7d6c173b0e7..4b42f5679a9 100644 --- a/public/app/features/storage/types.ts +++ b/public/app/features/storage/types.ts @@ -4,7 +4,6 @@ export enum StorageView { Data = 'data', Config = 'config', Perms = 'perms', - Export = 'export', History = 'history', AddRoot = 'add', } diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index facee048ca4..8cb51f92928 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -366,6 +366,13 @@ export function getAppRoutes(): RouteDescriptor[] { () => import(/* webpackChunkName: "K8SStoragePage" */ 'app/features/storage/k8s/K8SPage') ), }, + { + path: '/admin/storage/export', + roles: () => ['Admin'], + component: SafeDynamicImport( + () => import(/* webpackChunkName: "ExportPage" */ 'app/features/storage/ExportPage') + ), + }, { path: '/admin/storage/:path*', roles: () => ['Admin'],