mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboards: load from storage (#51949)
This commit is contained in:
parent
eab03aa207
commit
da1701ce57
@ -5843,12 +5843,15 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/features/storage/helper.ts:5381": [
|
||||
"public/app/features/storage/storage.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "7"]
|
||||
],
|
||||
"public/app/features/teams/CreateTeam.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
|
@ -41,6 +41,7 @@ export interface FeatureToggles {
|
||||
annotationComments?: boolean;
|
||||
migrationLocking?: boolean;
|
||||
storage?: boolean;
|
||||
dashboardsFromStorage?: boolean;
|
||||
export?: boolean;
|
||||
storageLocalUpload?: boolean;
|
||||
azureMonitorResourcePickerForMetrics?: boolean;
|
||||
|
@ -108,6 +108,10 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/dashboards/*", reqSignedIn, hs.Index)
|
||||
r.Get("/goto/:uid", reqSignedIn, hs.redirectFromShortURL, hs.Index)
|
||||
|
||||
if hs.Features.IsEnabled(featuremgmt.FlagDashboardsFromStorage) {
|
||||
r.Get("/g/*", reqSignedIn, hs.Index)
|
||||
}
|
||||
|
||||
if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) {
|
||||
r.Get("/public-dashboards/:accessToken", publicdashboardsapi.SetPublicDashboardFlag(), hs.Index)
|
||||
}
|
||||
|
@ -141,6 +141,11 @@ var (
|
||||
Description: "Configurable storage for dashboards, datasources, and resources",
|
||||
State: FeatureStateAlpha,
|
||||
},
|
||||
{
|
||||
Name: "dashboardsFromStorage",
|
||||
Description: "Load dashboards from the generic storage interface",
|
||||
State: FeatureStateAlpha,
|
||||
},
|
||||
{
|
||||
Name: "export",
|
||||
Description: "Export grafana instance (to git, etc)",
|
||||
|
@ -107,6 +107,10 @@ const (
|
||||
// Configurable storage for dashboards, datasources, and resources
|
||||
FlagStorage = "storage"
|
||||
|
||||
// FlagDashboardsFromStorage
|
||||
// Load dashboards from the generic storage interface
|
||||
FlagDashboardsFromStorage = "dashboardsFromStorage"
|
||||
|
||||
// FlagExport
|
||||
// Export grafana instance (to git, etc)
|
||||
FlagExport = "export"
|
||||
|
@ -14,10 +14,11 @@ import { catchError, filter, map, mergeMap, retryWhen, share, takeUntil, tap, th
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { AppEvents, DataQueryErrorType } from '@grafana/data';
|
||||
import { BackendSrv as BackendService, BackendSrvRequest, FetchError, FetchResponse } from '@grafana/runtime';
|
||||
import { BackendSrv as BackendService, BackendSrvRequest, config, FetchError, FetchResponse } from '@grafana/runtime';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { getConfig } from 'app/core/config';
|
||||
import { DashboardSearchHit } from 'app/features/search/types';
|
||||
import { getGrafanaStorage } from 'app/features/storage/storage';
|
||||
import { TokenRevokedModal } from 'app/features/users/TokenRevokedModal';
|
||||
import { DashboardDTO, FolderDTO } from 'app/types';
|
||||
|
||||
@ -430,7 +431,10 @@ export class BackendSrv implements BackendService {
|
||||
return this.get('/api/search', query);
|
||||
}
|
||||
|
||||
getDashboardByUid(uid: string) {
|
||||
getDashboardByUid(uid: string): Promise<DashboardDTO> {
|
||||
if (uid.indexOf('/') > 0 && config.featureToggles.dashboardsFromStorage) {
|
||||
return getGrafanaStorage().getDashboard(uid);
|
||||
}
|
||||
return this.get<DashboardDTO>(`/api/dashboards/uid/${uid}`);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { Drawer, Tab, TabsBar } from '@grafana/ui';
|
||||
import { Drawer, Spinner, Tab, TabsBar } from '@grafana/ui';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
import { jsonDiff } from '../VersionHistory/utils';
|
||||
@ -70,6 +70,14 @@ export const SaveDashboardDrawer = ({ dashboard, onDismiss, onSaveSuccess, isCop
|
||||
return <SaveDashboardDiff diff={data.diff} oldValue={previous.value} newValue={data.clone} />;
|
||||
}
|
||||
|
||||
if (state.loading) {
|
||||
return (
|
||||
<div>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNew || isCopy) {
|
||||
return (
|
||||
<SaveDashboardAsForm
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { CloneOptions, DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
import { DashboardDataDTO } from 'app/types';
|
||||
|
||||
import { Diffs } from '../VersionHistory/utils';
|
||||
|
||||
@ -16,6 +17,13 @@ export interface SaveDashboardOptions extends CloneOptions {
|
||||
makeEditable?: boolean;
|
||||
}
|
||||
|
||||
export interface SaveDashboardCommand {
|
||||
dashboard: DashboardDataDTO;
|
||||
message?: string;
|
||||
folderId?: number;
|
||||
overwrite?: boolean;
|
||||
}
|
||||
|
||||
export interface SaveDashboardFormProps {
|
||||
dashboard: DashboardModel;
|
||||
onCancel: () => void;
|
||||
|
@ -36,6 +36,9 @@ export const useDashboardSave = (dashboard: DashboardModel) => {
|
||||
|
||||
const notifyApp = useAppNotification();
|
||||
useEffect(() => {
|
||||
if (state.error) {
|
||||
notifyApp.error(state.error.message ?? 'Error saving dashboard');
|
||||
}
|
||||
if (state.value) {
|
||||
dashboard.version = state.value.version;
|
||||
dashboard.clearUnsavedChanges();
|
||||
|
@ -8,6 +8,8 @@ import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import impressionSrv from 'app/core/services/impression_srv';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { getGrafanaStorage } from 'app/features/storage/storage';
|
||||
import { DashboardRoutes } from 'app/types';
|
||||
|
||||
import { appEvents } from '../../../core/core';
|
||||
|
||||
@ -39,6 +41,8 @@ export class DashboardLoaderSrv {
|
||||
promise = backendSrv.get('/api/snapshots/' + slug).catch(() => {
|
||||
return this._dashboardLoadFailed('Snapshot not found', true);
|
||||
});
|
||||
} else if (type === DashboardRoutes.Path) {
|
||||
promise = getGrafanaStorage().getDashboard(slug!);
|
||||
} else if (type === 'ds') {
|
||||
promise = this._loadFromDatasource(slug); // explore dashboards as code
|
||||
} else if (type === 'public') {
|
||||
|
@ -87,6 +87,10 @@ async function fetchDashboard(
|
||||
case DashboardRoutes.New: {
|
||||
return getNewDashboardModelData(args.urlFolderId, args.panelType);
|
||||
}
|
||||
case DashboardRoutes.Path: {
|
||||
const path = args.urlSlug ?? '';
|
||||
return await dashboardLoaderSrv.loadDashboard(DashboardRoutes.Path, path, path);
|
||||
}
|
||||
default:
|
||||
throw { message: 'Unknown route ' + args.routeName };
|
||||
}
|
||||
|
@ -2,8 +2,10 @@ import { DataSourceInstanceSettings, locationUtil } from '@grafana/data';
|
||||
import { getDataSourceSrv, locationService, getBackendSrv, isFetchError } from '@grafana/runtime';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types';
|
||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||
import { DashboardDataDTO, DashboardDTO, FolderInfo, PermissionLevelString, ThunkResult } from 'app/types';
|
||||
import { getGrafanaStorage } from 'app/features/storage/storage';
|
||||
import { DashboardDTO, FolderInfo, PermissionLevelString, ThunkResult } from 'app/types';
|
||||
|
||||
import { LibraryElementExport } from '../../dashboard/components/DashExportModal/DashboardExporter';
|
||||
import { getLibraryPanel } from '../../library-panels/state/api';
|
||||
@ -262,16 +264,13 @@ export function deleteFoldersAndDashboards(folderUids: string[], dashboardUids:
|
||||
return executeInOrder(tasks);
|
||||
}
|
||||
|
||||
export interface SaveDashboardOptions {
|
||||
dashboard: DashboardDataDTO;
|
||||
message?: string;
|
||||
folderId?: number;
|
||||
overwrite?: boolean;
|
||||
}
|
||||
|
||||
export function saveDashboard(options: SaveDashboardOptions) {
|
||||
export function saveDashboard(options: SaveDashboardCommand) {
|
||||
dashboardWatcher.ignoreNextSave();
|
||||
|
||||
if (options.dashboard.uid.indexOf('/') > 0) {
|
||||
return getGrafanaStorage().saveDashboard(options);
|
||||
}
|
||||
|
||||
return getBackendSrv().post('/api/dashboards/db/', {
|
||||
dashboard: options.dashboard,
|
||||
message: options.message ?? '',
|
||||
|
@ -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);
|
||||
}
|
@ -443,6 +443,7 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
() => import(/* webpackChunkName: "NotificationsPage"*/ 'app/features/notifications/NotificationsPage')
|
||||
),
|
||||
},
|
||||
...getBrowseStorageRoutes(),
|
||||
...getDynamicDashboardRoutes(),
|
||||
...getPluginCatalogRoutes(),
|
||||
...getLiveRoutes(),
|
||||
@ -460,6 +461,28 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
];
|
||||
}
|
||||
|
||||
export function getBrowseStorageRoutes(cfg = config): RouteDescriptor[] {
|
||||
if (!cfg.featureToggles.dashboardsFromStorage) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
path: '/g/:slug*.json', // suffix will eventually include dashboard
|
||||
pageClass: 'page-dashboard',
|
||||
routeName: DashboardRoutes.Path,
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "DashboardPage" */ '../features/dashboard/containers/DashboardPage')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/g/:slug*',
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "StorageFolderPage" */ '../features/storage/StorageFolderPage')
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function getDynamicDashboardRoutes(cfg = config): RouteDescriptor[] {
|
||||
if (!cfg.featureToggles.scenes) {
|
||||
return [];
|
||||
|
@ -11,6 +11,8 @@ export interface DashboardDTO {
|
||||
}
|
||||
|
||||
export interface DashboardMeta {
|
||||
slug?: string;
|
||||
uid?: string;
|
||||
canSave?: boolean;
|
||||
canEdit?: boolean;
|
||||
canDelete?: boolean;
|
||||
@ -27,6 +29,7 @@ export interface DashboardMeta {
|
||||
isStarred?: boolean;
|
||||
showSettings?: boolean;
|
||||
expires?: string;
|
||||
isFolder?: boolean;
|
||||
isSnapshot?: boolean;
|
||||
folderTitle?: string;
|
||||
folderUrl?: string;
|
||||
@ -59,12 +62,16 @@ export interface DashboardDataDTO {
|
||||
list: VariableModel[];
|
||||
};
|
||||
panels?: any[];
|
||||
|
||||
/** @deprecated -- components should key on uid rather than id */
|
||||
id?: number;
|
||||
}
|
||||
|
||||
export enum DashboardRoutes {
|
||||
Home = 'home-dashboard',
|
||||
New = 'new-dashboard',
|
||||
Normal = 'normal-dashboard',
|
||||
Path = 'path-dashboard',
|
||||
Scripted = 'scripted-dashboard',
|
||||
Public = 'public-dashboard',
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user