mirror of
https://github.com/grafana/grafana.git
synced 2025-08-06 20:59:35 +08:00
Dashboards: load from storage (#51949)
This commit is contained in:
@ -8,7 +8,7 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
||||
import { CodeEditor, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { getGrafanaStorage } from './helper';
|
||||
import { getGrafanaStorage } from './storage';
|
||||
import { StorageView } from './types';
|
||||
|
||||
interface FileDisplayInfo {
|
||||
|
79
public/app/features/storage/StorageFolderPage.tsx
Normal file
79
public/app/features/storage/StorageFolderPage.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { FC } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Card, Icon, Spinner, useStyles2 } from '@grafana/ui';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
|
||||
import { getGrafanaStorage } from './storage';
|
||||
|
||||
export interface Props extends GrafanaRouteComponentProps<{ slug: string }> {}
|
||||
|
||||
export const StorageFolderPage: FC<Props> = (props) => {
|
||||
const slug = props.match.params.slug;
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
const listing = useAsync((): Promise<DataFrame | undefined> => {
|
||||
return getGrafanaStorage().list(slug);
|
||||
}, [slug]);
|
||||
|
||||
let base = document.location.pathname;
|
||||
if (!base.endsWith('/')) {
|
||||
base += '/';
|
||||
}
|
||||
let parent = '';
|
||||
const idx = base.lastIndexOf('/', base.length - 2);
|
||||
if (idx > 0) {
|
||||
parent = base.substring(0, idx);
|
||||
}
|
||||
|
||||
const renderListing = () => {
|
||||
if (listing.value) {
|
||||
const names = listing.value.fields[0].values.toArray();
|
||||
return names.map((item: string) => {
|
||||
let name = item;
|
||||
const isFolder = name.indexOf('.') < 0;
|
||||
const isDash = !isFolder && name.endsWith('.json');
|
||||
return (
|
||||
<Card key={name} href={isFolder || isDash ? base + name : undefined}>
|
||||
<Card.Heading>{name}</Card.Heading>
|
||||
<Card.Figure>
|
||||
<Icon name={isFolder ? 'folder' : isDash ? 'gf-grid' : 'file-alt'} size="sm" />
|
||||
</Card.Figure>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
}
|
||||
if (listing.loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
return <div>?</div>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
{slug?.length > 0 && (
|
||||
<>
|
||||
<h1>{slug}</h1>
|
||||
<Card href={parent}>
|
||||
<Card.Heading>{parent}</Card.Heading>
|
||||
<Card.Figure>
|
||||
<Icon name="arrow-left" size="sm" />
|
||||
</Card.Figure>
|
||||
</Card>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
{renderListing()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
wrapper: css`
|
||||
margin: 50px;
|
||||
`,
|
||||
});
|
||||
|
||||
export default StorageFolderPage;
|
@ -3,8 +3,8 @@ import React, { useMemo, useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { DataFrame, GrafanaTheme2, isDataFrame, ValueLinkConfig } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { useStyles2, IconName, Spinner, TabsBar, Tab, Button, HorizontalGroup } from '@grafana/ui';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { useStyles2, IconName, Spinner, TabsBar, Tab, Button, HorizontalGroup, LinkButton } from '@grafana/ui';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { useNavModel } from 'app/core/hooks/useNavModel';
|
||||
@ -18,7 +18,7 @@ import { ExportView } from './ExportView';
|
||||
import { FileView } from './FileView';
|
||||
import { FolderView } from './FolderView';
|
||||
import { RootView } from './RootView';
|
||||
import { getGrafanaStorage, filenameAlreadyExists } from './helper';
|
||||
import { getGrafanaStorage, filenameAlreadyExists } from './storage';
|
||||
import { StorageView } from './types';
|
||||
|
||||
interface RouteParams {
|
||||
@ -162,12 +162,19 @@ export default function StoragePage(props: Props) {
|
||||
}
|
||||
const canAddFolder = isFolder && path.startsWith('resources');
|
||||
const canDelete = path.startsWith('resources/');
|
||||
const canViewDashboard =
|
||||
path.startsWith('devenv/') && config.featureToggles.dashboardsFromStorage && (isFolder || path.endsWith('.json'));
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<HorizontalGroup width="100%" justify="space-between" spacing={'md'} height={25}>
|
||||
<Breadcrumb pathName={path} onPathChange={setPath} rootIcon={navModel.node.icon as IconName} />
|
||||
<HorizontalGroup>
|
||||
{canViewDashboard && (
|
||||
<LinkButton icon="dashboard" href={`g/${path}`}>
|
||||
Dashboard
|
||||
</LinkButton>
|
||||
)}
|
||||
{canAddFolder && <Button onClick={() => setIsAddingNewFolder(true)}>New Folder</Button>}
|
||||
{canDelete && (
|
||||
<Button
|
||||
|
@ -5,7 +5,7 @@ import SVG from 'react-inlinesvg';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, Button, ButtonGroup, Checkbox, Field, FileDropzone, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { filenameAlreadyExists, getGrafanaStorage } from './helper';
|
||||
import { filenameAlreadyExists, getGrafanaStorage } from './storage';
|
||||
import { UploadReponse } from './types';
|
||||
|
||||
interface Props {
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { DataFrame, dataFrameFromJSON, DataFrameJSON, getDisplayProcessor } from '@grafana/data';
|
||||
import { config, getBackendSrv } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types';
|
||||
import { DashboardDTO } from 'app/types';
|
||||
|
||||
import { UploadReponse } from './types';
|
||||
|
||||
@ -10,6 +13,13 @@ export interface GrafanaStorage {
|
||||
upload: (folder: string, file: File, overwriteExistingFile: boolean) => Promise<UploadReponse>;
|
||||
createFolder: (path: string) => Promise<{ error?: string }>;
|
||||
delete: (path: { isFolder: boolean; path: string }) => Promise<{ error?: string }>;
|
||||
|
||||
/**
|
||||
* Temporary shim that will return a DashboardDTO shape for files in storage
|
||||
* Longer term, this will call an "Entity API" that is eventually backed by storage
|
||||
*/
|
||||
getDashboard: (path: string) => Promise<DashboardDTO>;
|
||||
saveDashboard: (options: SaveDashboardCommand) => Promise<any>;
|
||||
}
|
||||
|
||||
class SimpleStorage implements GrafanaStorage {
|
||||
@ -103,6 +113,75 @@ class SimpleStorage implements GrafanaStorage {
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
// Temporary shim that can be loaded into the existing dashboard page structure
|
||||
async getDashboard(path: string): Promise<DashboardDTO> {
|
||||
if (!config.featureToggles.dashboardsFromStorage) {
|
||||
return Promise.reject('Dashboards from storage is not enabled');
|
||||
}
|
||||
|
||||
if (!path.endsWith('.json')) {
|
||||
path += '.json';
|
||||
}
|
||||
const result = await backendSrv.get(`/api/storage/read/${path}`);
|
||||
result.uid = path;
|
||||
delete result.id; // Saved with the dev dashboards!
|
||||
|
||||
return {
|
||||
meta: {
|
||||
uid: path,
|
||||
slug: path,
|
||||
canEdit: true,
|
||||
canSave: true,
|
||||
canStar: false, // needs id
|
||||
},
|
||||
dashboard: result,
|
||||
};
|
||||
}
|
||||
|
||||
async saveDashboard(options: SaveDashboardCommand): Promise<any> {
|
||||
if (!config.featureToggles.dashboardsFromStorage) {
|
||||
return Promise.reject('Dashboards from storage is not enabled');
|
||||
}
|
||||
|
||||
const blob = new Blob([JSON.stringify(options.dashboard)], {
|
||||
type: 'application/json',
|
||||
});
|
||||
|
||||
const uid = options.dashboard.uid;
|
||||
const formData = new FormData();
|
||||
if (options.message) {
|
||||
formData.append('message', options.message);
|
||||
}
|
||||
formData.append('overwriteExistingFile', options.overwrite === false ? 'false' : 'true');
|
||||
formData.append('file.path', uid);
|
||||
formData.append('file', blob);
|
||||
const res = await fetch('/api/storage/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
let body = (await res.json()) as UploadReponse;
|
||||
if (res.status !== 200 && !body?.err) {
|
||||
console.log('SAVE', options, body);
|
||||
return Promise.reject({ message: body?.message ?? res.statusText });
|
||||
}
|
||||
|
||||
return {
|
||||
uid,
|
||||
url: `/g/${uid}`,
|
||||
slug: uid,
|
||||
status: 'success',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function filenameAlreadyExists(folderName: string, fileNames: string[]) {
|
||||
const lowerCase = folderName.toLowerCase();
|
||||
const trimmedLowerCase = lowerCase.trim();
|
||||
const existingTrimmedLowerCaseNames = fileNames.map((f) => f.trim().toLowerCase());
|
||||
|
||||
return existingTrimmedLowerCaseNames.includes(trimmedLowerCase);
|
||||
}
|
||||
|
||||
let storage: GrafanaStorage | undefined;
|
||||
@ -113,11 +192,3 @@ export function getGrafanaStorage() {
|
||||
}
|
||||
return storage;
|
||||
}
|
||||
|
||||
export function filenameAlreadyExists(folderName: string, fileNames: string[]) {
|
||||
const lowerCase = folderName.toLowerCase();
|
||||
const trimmedLowerCase = lowerCase.trim();
|
||||
const existingTrimmedLowerCaseNames = fileNames.map((f) => f.trim().toLowerCase());
|
||||
|
||||
return existingTrimmedLowerCaseNames.includes(trimmedLowerCase);
|
||||
}
|
Reference in New Issue
Block a user