mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Export: move export page to a full page (not view on storage) (#60263)
Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
This commit is contained in:
parent
821614fb43
commit
4064fa51c6
@ -161,6 +161,16 @@ func (s *ServiceImpl) getServerAdminNode(c *models.ReqContext) *navtree.NavLink
|
|||||||
}
|
}
|
||||||
adminNavLinks = append(adminNavLinks, storage)
|
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) {
|
if s.features.IsEnabled(featuremgmt.FlagK8s) {
|
||||||
storage.Children = append(storage.Children, &navtree.NavLink{
|
storage.Children = append(storage.Children, &navtree.NavLink{
|
||||||
Text: "Kubernetes",
|
Text: "Kubernetes",
|
||||||
|
278
public/app/features/storage/ExportPage.tsx
Normal file
278
public/app/features/storage/ExportPage.tsx
Normal file
@ -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<string, boolean>;
|
||||||
|
|
||||||
|
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<SelectableValue<string>> = [
|
||||||
|
{ 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<ExportStatusMessage>();
|
||||||
|
const [body, setBody] = useLocalStorage<ExportJob>(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<string, boolean> = {};
|
||||||
|
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<ExportStatusMessage>({
|
||||||
|
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 && (
|
||||||
|
<div>
|
||||||
|
<h3>Status</h3>
|
||||||
|
<pre>{JSON.stringify(status, null, 2)}</pre>
|
||||||
|
{status.running && (
|
||||||
|
<div>
|
||||||
|
<Button variant="secondary" onClick={doStop}>
|
||||||
|
Stop
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const formFragment = !Boolean(status?.running) && (
|
||||||
|
<div>
|
||||||
|
<Field label="Format">
|
||||||
|
<Select
|
||||||
|
options={formats}
|
||||||
|
width={40}
|
||||||
|
value={formats.find((v) => v.value === body?.format)}
|
||||||
|
onChange={(v) => setBody({ ...body!, format: v.value! })}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
{!isEntityStoreEnabled && body?.format !== StorageFormat.Git && (
|
||||||
|
<div>
|
||||||
|
<Alert title="Missing feature flag">Enable the `entityStore` feature flag</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{body?.format === StorageFormat.Git && (
|
||||||
|
<>
|
||||||
|
<Field label="Keep history">
|
||||||
|
<Switch value={body?.history} onChange={(v) => setBody({ ...body!, history: v.currentTarget.checked })} />
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Include">
|
||||||
|
<>
|
||||||
|
<InlineFieldRow>
|
||||||
|
<InlineField label="Toggle all" labelWidth={labelWith}>
|
||||||
|
<InlineSwitch
|
||||||
|
value={Object.keys(body?.exclude ?? {}).length === 0}
|
||||||
|
onChange={(v) => setInclude('*', v.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
|
{serverOptions.value && (
|
||||||
|
<div>
|
||||||
|
{serverOptions.value.exporters.map((ex) => (
|
||||||
|
<InlineFieldRow key={ex.key}>
|
||||||
|
<InlineField label={ex.name} labelWidth={labelWith} tooltip={ex.description}>
|
||||||
|
<InlineSwitch
|
||||||
|
value={body?.exclude?.[ex.key] !== true}
|
||||||
|
onChange={(v) => setInclude(ex.key, v.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</Field>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{shouldDisplayContent && (
|
||||||
|
<>
|
||||||
|
<Field label="General folder" description="Set the folder name for items without a real folder">
|
||||||
|
<Input
|
||||||
|
width={40}
|
||||||
|
value={body?.generalFolderPath ?? ''}
|
||||||
|
onChange={(v) => setBody({ ...body!, generalFolderPath: v.currentTarget.value })}
|
||||||
|
placeholder="root folder path"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<HorizontalGroup>
|
||||||
|
<Button onClick={doStart} variant="primary">
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
<LinkButton href="admin/storage/" variant="secondary">
|
||||||
|
Cancel
|
||||||
|
</LinkButton>
|
||||||
|
</HorizontalGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestDetailsFragment = (isEntityStoreEnabled || body?.format === StorageFormat.Git) && (
|
||||||
|
<Collapse label="Request details" isOpen={details} onToggle={setDetails} collapsible={true}>
|
||||||
|
<CodeEditor
|
||||||
|
height={275}
|
||||||
|
value={JSON.stringify(body, null, 2) ?? ''}
|
||||||
|
showLineNumbers={false}
|
||||||
|
readOnly={false}
|
||||||
|
language="json"
|
||||||
|
showMiniMap={false}
|
||||||
|
onBlur={(text: string) => {
|
||||||
|
setBody(JSON.parse(text)); // force JSON?
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Collapse>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{statusFragment}
|
||||||
|
{formFragment}
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
{requestDetailsFragment}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page navModel={navModel}>
|
||||||
|
<Page.Contents>{renderView()}</Page.Contents>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
@ -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<string, boolean>;
|
|
||||||
|
|
||||||
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<SelectableValue<string>> = [
|
|
||||||
{ 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<ExportStatusMessage>();
|
|
||||||
const [body, setBody] = useLocalStorage<ExportJob>(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<string, boolean> = {};
|
|
||||||
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<ExportStatusMessage>({
|
|
||||||
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 (
|
|
||||||
<div>
|
|
||||||
{status && (
|
|
||||||
<div>
|
|
||||||
<h3>Status</h3>
|
|
||||||
<pre>{JSON.stringify(status, null, 2)}</pre>
|
|
||||||
{status.running && (
|
|
||||||
<div>
|
|
||||||
<Button variant="secondary" onClick={doStop}>
|
|
||||||
Stop
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!Boolean(status?.running) && (
|
|
||||||
<div>
|
|
||||||
<h3>Export grafana instance</h3>
|
|
||||||
<Field label="Format">
|
|
||||||
<Select
|
|
||||||
options={formats}
|
|
||||||
width={40}
|
|
||||||
value={formats.find((v) => v.value === body?.format)}
|
|
||||||
onChange={(v) => setBody({ ...body!, format: v.value! })}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
{body?.format === 'entityStore' && !config.featureToggles.entityStore && (
|
|
||||||
<div>
|
|
||||||
<Alert title="Missing feature flag">Enable the `entityStore` feature flag</Alert>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{body?.format === 'git' && (
|
|
||||||
<>
|
|
||||||
<Field label="Keep history">
|
|
||||||
<Switch
|
|
||||||
value={body?.history}
|
|
||||||
onChange={(v) => setBody({ ...body!, history: v.currentTarget.checked })}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Include">
|
|
||||||
<>
|
|
||||||
<InlineFieldRow>
|
|
||||||
<InlineField label="Toggle all" labelWidth={labelWith}>
|
|
||||||
<InlineSwitch
|
|
||||||
value={Object.keys(body?.exclude ?? {}).length === 0}
|
|
||||||
onChange={(v) => setInclude('*', v.currentTarget.checked)}
|
|
||||||
/>
|
|
||||||
</InlineField>
|
|
||||||
</InlineFieldRow>
|
|
||||||
{serverOptions.value && (
|
|
||||||
<div>
|
|
||||||
{serverOptions.value.exporters.map((ex) => (
|
|
||||||
<InlineFieldRow key={ex.key}>
|
|
||||||
<InlineField label={ex.name} labelWidth={labelWith} tooltip={ex.description}>
|
|
||||||
<InlineSwitch
|
|
||||||
value={body?.exclude?.[ex.key] !== true}
|
|
||||||
onChange={(v) => setInclude(ex.key, v.currentTarget.checked)}
|
|
||||||
/>
|
|
||||||
</InlineField>
|
|
||||||
</InlineFieldRow>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
</Field>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Field label="General folder" description="Set the folder name for items without a real folder">
|
|
||||||
<Input
|
|
||||||
width={40}
|
|
||||||
value={body?.generalFolderPath ?? ''}
|
|
||||||
onChange={(v) => setBody({ ...body!, generalFolderPath: v.currentTarget.value })}
|
|
||||||
placeholder="root folder path"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<HorizontalGroup>
|
|
||||||
<Button onClick={doStart} variant="primary">
|
|
||||||
Export
|
|
||||||
</Button>
|
|
||||||
<LinkButton href="admin/storage/" variant="secondary">
|
|
||||||
Cancel
|
|
||||||
</LinkButton>
|
|
||||||
</HorizontalGroup>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<Collapse label="Request details" isOpen={details} onToggle={setDetails} collapsible={true}>
|
|
||||||
<CodeEditor
|
|
||||||
height={275}
|
|
||||||
value={JSON.stringify(body, null, 2) ?? ''}
|
|
||||||
showLineNumbers={false}
|
|
||||||
readOnly={false}
|
|
||||||
language="json"
|
|
||||||
showMiniMap={false}
|
|
||||||
onBlur={(text: string) => {
|
|
||||||
setBody(JSON.parse(text)); // force JSON?
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Collapse>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -3,7 +3,6 @@ import React, { useMemo, useState } from 'react';
|
|||||||
import { useAsync } from 'react-use';
|
import { useAsync } from 'react-use';
|
||||||
|
|
||||||
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
@ -100,11 +99,6 @@ export function RootView({ root, onPathChange }: Props) {
|
|||||||
<Button className="pull-right" onClick={() => onPathChange('', StorageView.AddRoot)}>
|
<Button className="pull-right" onClick={() => onPathChange('', StorageView.AddRoot)}>
|
||||||
Add Root
|
Add Root
|
||||||
</Button>
|
</Button>
|
||||||
{config.featureToggles.export && (
|
|
||||||
<Button className="pull-right" onClick={() => onPathChange('', StorageView.Export)}>
|
|
||||||
Export
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>{renderRoots('', roots.base)}</div>
|
<div>{renderRoots('', roots.base)}</div>
|
||||||
|
@ -14,7 +14,6 @@ import { ShowConfirmModalEvent } from 'app/types/events';
|
|||||||
import { AddRootView } from './AddRootView';
|
import { AddRootView } from './AddRootView';
|
||||||
import { Breadcrumb } from './Breadcrumb';
|
import { Breadcrumb } from './Breadcrumb';
|
||||||
import { CreateNewFolderModal } from './CreateNewFolderModal';
|
import { CreateNewFolderModal } from './CreateNewFolderModal';
|
||||||
import { ExportView } from './ExportView';
|
|
||||||
import { FileView } from './FileView';
|
import { FileView } from './FileView';
|
||||||
import { FolderView } from './FolderView';
|
import { FolderView } from './FolderView';
|
||||||
import { RootView } from './RootView';
|
import { RootView } from './RootView';
|
||||||
@ -116,13 +115,6 @@ export default function StoragePage(props: Props) {
|
|||||||
const renderView = () => {
|
const renderView = () => {
|
||||||
const isRoot = !path?.length || path === '/';
|
const isRoot = !path?.length || path === '/';
|
||||||
switch (view) {
|
switch (view) {
|
||||||
case StorageView.Export:
|
|
||||||
if (!isRoot) {
|
|
||||||
setPath('');
|
|
||||||
return <Spinner />;
|
|
||||||
}
|
|
||||||
return <ExportView onPathChange={setPath} />;
|
|
||||||
|
|
||||||
case StorageView.AddRoot:
|
case StorageView.AddRoot:
|
||||||
if (!isRoot) {
|
if (!isRoot) {
|
||||||
setPath('');
|
setPath('');
|
||||||
|
@ -4,7 +4,6 @@ export enum StorageView {
|
|||||||
Data = 'data',
|
Data = 'data',
|
||||||
Config = 'config',
|
Config = 'config',
|
||||||
Perms = 'perms',
|
Perms = 'perms',
|
||||||
Export = 'export',
|
|
||||||
History = 'history',
|
History = 'history',
|
||||||
AddRoot = 'add',
|
AddRoot = 'add',
|
||||||
}
|
}
|
||||||
|
@ -366,6 +366,13 @@ export function getAppRoutes(): RouteDescriptor[] {
|
|||||||
() => import(/* webpackChunkName: "K8SStoragePage" */ 'app/features/storage/k8s/K8SPage')
|
() => 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*',
|
path: '/admin/storage/:path*',
|
||||||
roles: () => ['Admin'],
|
roles: () => ['Admin'],
|
||||||
|
Loading…
Reference in New Issue
Block a user