mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Chore: Remove experimental Storage UI (#96887)
This commit is contained in:
parent
6a0fd05a9e
commit
68c61514b0
@ -3883,43 +3883,6 @@ exports[`better eslint`] = {
|
||||
"public/app/features/serviceaccounts/state/reducers.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/storage/AddRootView.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
],
|
||||
"public/app/features/storage/CreateNewFolderModal.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
],
|
||||
"public/app/features/storage/FileView.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
|
||||
],
|
||||
"public/app/features/storage/FolderView.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
],
|
||||
"public/app/features/storage/RootView.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
],
|
||||
"public/app/features/storage/StorageFolderPage.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
],
|
||||
"public/app/features/storage/StoragePage.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
],
|
||||
"public/app/features/storage/UploadButton.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
||||
],
|
||||
"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"]
|
||||
],
|
||||
"public/app/features/support-bundles/SupportBundles.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -465,7 +465,6 @@ playwright.config.ts @grafana/plugins-platform-frontend
|
||||
/public/app/features/browse-dashboards/ @grafana/grafana-frontend-platform
|
||||
/public/app/features/search/ @grafana/grafana-frontend-platform
|
||||
/public/app/features/serviceaccounts/ @grafana/identity-squad
|
||||
/public/app/features/storage/ @grafana/grafana-app-platform-squad
|
||||
/public/app/features/teams/ @grafana/access-squad
|
||||
/public/app/features/templating/ @grafana/dashboards-squad
|
||||
/public/app/features/trails/ @grafana/observability-metrics
|
||||
|
@ -33,6 +33,8 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/middleware/requestmeta"
|
||||
@ -51,7 +53,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/api")
|
||||
@ -113,10 +114,6 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/admin/orgs", authorizeInOrg(ac.UseGlobalOrg, ac.OrgsAccessEvaluator), hs.Index)
|
||||
r.Get("/admin/orgs/edit/:id", authorizeInOrg(ac.UseGlobalOrg, ac.OrgsAccessEvaluator), hs.Index)
|
||||
r.Get("/admin/stats", authorize(ac.EvalPermission(ac.ActionServerStatsRead)), hs.Index)
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagStorage) {
|
||||
r.Get("/admin/storage", reqSignedIn, hs.Index)
|
||||
r.Get("/admin/storage/*", reqSignedIn, hs.Index)
|
||||
}
|
||||
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrations) {
|
||||
r.Get("/admin/migrate-to-cloud", reqOrgAdmin, hs.Index)
|
||||
|
@ -20,8 +20,7 @@ import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||
import { AngularDeprecationNotice } from 'app/features/plugins/angularDeprecation/AngularDeprecationNotice';
|
||||
import { AngularMigrationNotice } from 'app/features/plugins/angularDeprecation/AngularMigrationNotice';
|
||||
import { getPageNavFromSlug, getRootContentNavModel } from 'app/features/storage/StorageFolderPage';
|
||||
import { DashboardRoutes, KioskMode, StoreState } from 'app/types';
|
||||
import { KioskMode, StoreState } from 'app/types';
|
||||
import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events';
|
||||
|
||||
import { cancelVariables, templateVarsChangedInUrl } from '../../variables/state/actions';
|
||||
@ -519,19 +518,7 @@ function updateStatePageNavFromProps(props: Props, state: State): State {
|
||||
};
|
||||
}
|
||||
|
||||
if (props.route.routeName === DashboardRoutes.Path) {
|
||||
sectionNav = getRootContentNavModel();
|
||||
const pageNav = getPageNavFromSlug(props.params.slug!);
|
||||
if (pageNav?.parentItem) {
|
||||
pageNav.parentItem = pageNav.parentItem;
|
||||
}
|
||||
} else {
|
||||
sectionNav = getNavModel(
|
||||
props.navIndex,
|
||||
ID_PREFIX + dashboard.uid,
|
||||
getNavModel(props.navIndex, 'dashboards/browse')
|
||||
);
|
||||
}
|
||||
sectionNav = getNavModel(props.navIndex, ID_PREFIX + dashboard.uid, getNavModel(props.navIndex, 'dashboards/browse'));
|
||||
|
||||
const { folderUid } = dashboard.meta;
|
||||
if (folderUid && pageNav && sectionNav.main.id !== 'starred') {
|
||||
|
@ -126,10 +126,6 @@ async function fetchDashboard(
|
||||
}
|
||||
return await buildNewDashboardSaveModel(args.urlFolderUid);
|
||||
}
|
||||
case DashboardRoutes.Path: {
|
||||
const path = args.urlSlug ?? '';
|
||||
return await dashboardLoaderSrv.loadDashboard(DashboardRoutes.Path, path, path);
|
||||
}
|
||||
default:
|
||||
throw { message: 'Unknown route ' + args.routeName };
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
import { Button } from '@grafana/ui';
|
||||
|
||||
import { StorageView } from './types';
|
||||
|
||||
interface Props {
|
||||
onPathChange: (p: string, v?: StorageView) => void;
|
||||
}
|
||||
|
||||
export function AddRootView({ onPathChange }: Props) {
|
||||
return (
|
||||
<div>
|
||||
<div>TODO... Add ROOT</div>
|
||||
<Button variant="secondary" onClick={() => onPathChange('/')}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { uniqueId } from 'lodash';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, IconName, useStyles2 } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
rootIcon?: IconName;
|
||||
pathName: string;
|
||||
onPathChange: (path: string) => void;
|
||||
}
|
||||
|
||||
export function Breadcrumb({ pathName, onPathChange, rootIcon }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const paths = pathName.split('/').filter(Boolean);
|
||||
|
||||
return (
|
||||
<ul className={styles.breadCrumb}>
|
||||
{rootIcon && (
|
||||
<li>
|
||||
<Icon name={rootIcon} onClick={() => onPathChange('')} />
|
||||
</li>
|
||||
)}
|
||||
{paths.map((path, index) => {
|
||||
let url = '/' + paths.slice(0, index + 1).join('/');
|
||||
const onClickBreadcrumb = () => onPathChange(url);
|
||||
const isLastBreadcrumb = index === paths.length - 1;
|
||||
return (
|
||||
// TODO: fix keyboard a11y
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
||||
<li key={uniqueId(path)} onClick={isLastBreadcrumb ? undefined : onClickBreadcrumb}>
|
||||
{path}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
breadCrumb: css({
|
||||
listStyle: 'none',
|
||||
padding: theme.spacing(2, 1),
|
||||
|
||||
li: {
|
||||
display: 'inline',
|
||||
|
||||
':not(:last-child)': {
|
||||
color: theme.colors.text.link,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
'+ li:before': {
|
||||
content: "'>'",
|
||||
padding: theme.spacing(1),
|
||||
color: theme.colors.text.secondary,
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import { SubmitHandler, Validate } from 'react-hook-form';
|
||||
|
||||
import { Button, Field, Input, Modal } from '@grafana/ui';
|
||||
import { Form } from 'app/core/components/Form/Form';
|
||||
|
||||
type FormModel = { folderName: string };
|
||||
|
||||
interface Props {
|
||||
onSubmit: SubmitHandler<FormModel>;
|
||||
onDismiss: () => void;
|
||||
validate: Validate<string, FormModel>;
|
||||
}
|
||||
|
||||
const initialFormModel = { folderName: '' };
|
||||
|
||||
export function CreateNewFolderModal({ validate, onDismiss, onSubmit }: Props) {
|
||||
return (
|
||||
<Modal onDismiss={onDismiss} isOpen={true} title="New Folder">
|
||||
<Form defaultValues={initialFormModel} onSubmit={onSubmit} maxWidth={'none'}>
|
||||
{({ register, errors }) => (
|
||||
<>
|
||||
<Field
|
||||
label="Folder name"
|
||||
invalid={!!errors.folderName}
|
||||
error={errors.folderName && errors.folderName.message}
|
||||
>
|
||||
<Input
|
||||
id="folder-name-input"
|
||||
{...register('folderName', {
|
||||
required: 'Folder name is required.',
|
||||
validate: { validate },
|
||||
})}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Modal.ButtonRow>
|
||||
<Button type="submit">Create</Button>
|
||||
</Modal.ButtonRow>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -1,155 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { isString } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
||||
import { CodeEditor, useStyles2 } from '@grafana/ui';
|
||||
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
|
||||
|
||||
import { getGrafanaStorage } from './storage';
|
||||
import { StorageView } from './types';
|
||||
|
||||
interface FileDisplayInfo {
|
||||
category?: 'svg' | 'image' | 'text';
|
||||
language?: string; // match code editor
|
||||
}
|
||||
|
||||
interface Props {
|
||||
listing: DataFrame;
|
||||
path: string;
|
||||
onPathChange: (p: string, view?: StorageView) => void;
|
||||
view: StorageView;
|
||||
}
|
||||
|
||||
export function FileView({ listing, path, onPathChange, view }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const info = useMemo(() => getFileDisplayInfo(path), [path]);
|
||||
const body = useAsync(async () => {
|
||||
if (info.category === 'text') {
|
||||
const rsp = await getGrafanaStorage().get(path);
|
||||
if (isString(rsp)) {
|
||||
return rsp;
|
||||
}
|
||||
return JSON.stringify(rsp, null, 2);
|
||||
}
|
||||
return null;
|
||||
}, [info, path]);
|
||||
|
||||
switch (view) {
|
||||
case StorageView.Config:
|
||||
return <div>CONFIGURE?</div>;
|
||||
case StorageView.Perms:
|
||||
return <div>Permissions</div>;
|
||||
case StorageView.History:
|
||||
return <div>TODO... history</div>;
|
||||
}
|
||||
|
||||
let src = `api/storage/read/${path}`;
|
||||
if (src.endsWith('/')) {
|
||||
src = src.substring(0, src.length - 1);
|
||||
}
|
||||
|
||||
switch (info.category) {
|
||||
case 'svg':
|
||||
return (
|
||||
<div>
|
||||
<SanitizedSVG src={src} className={styles.icon} />
|
||||
</div>
|
||||
);
|
||||
case 'image':
|
||||
return (
|
||||
<div>
|
||||
<a target={'_self'} href={src}>
|
||||
<img src={src} alt="File preview" className={styles.img} />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
case 'text':
|
||||
return (
|
||||
<div className={styles.tableWrapper}>
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<CodeEditor
|
||||
width={width}
|
||||
height={height}
|
||||
value={body.value ?? ''}
|
||||
showLineNumbers={false}
|
||||
readOnly={true}
|
||||
language={info.language ?? 'text'}
|
||||
showMiniMap={false}
|
||||
onBlur={(text: string) => {
|
||||
console.log('CHANGED!', text);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
FILE: <a href={src}>{path}</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getFileDisplayInfo(path: string): FileDisplayInfo {
|
||||
const idx = path.lastIndexOf('.');
|
||||
if (idx < 0) {
|
||||
return {};
|
||||
}
|
||||
const suffix = path.substring(idx + 1).toLowerCase();
|
||||
switch (suffix) {
|
||||
case 'svg':
|
||||
return { category: 'svg' };
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
case 'png':
|
||||
case 'webp':
|
||||
case 'gif':
|
||||
return { category: 'image' };
|
||||
|
||||
case 'geojson':
|
||||
case 'json':
|
||||
return { category: 'text', language: 'json' };
|
||||
case 'text':
|
||||
case 'go':
|
||||
case 'md':
|
||||
return { category: 'text' };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
// TODO: remove `height: 90%`
|
||||
wrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
}),
|
||||
tableControlRowWrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
// TODO: remove `height: 100%`
|
||||
tableWrapper: css({
|
||||
border: `1px solid ${theme.colors.border.medium}`,
|
||||
height: '100%',
|
||||
}),
|
||||
uploadSpot: css({
|
||||
marginLeft: theme.spacing(2),
|
||||
}),
|
||||
border: css({
|
||||
border: `1px solid ${theme.colors.border.medium}`,
|
||||
padding: theme.spacing(2),
|
||||
}),
|
||||
img: css({
|
||||
maxWidth: '100%',
|
||||
}),
|
||||
icon: css({}),
|
||||
});
|
@ -1,69 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Table, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { StorageView } from './types';
|
||||
|
||||
interface Props {
|
||||
listing: DataFrame;
|
||||
view: StorageView;
|
||||
}
|
||||
|
||||
export function FolderView({ listing, view }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
switch (view) {
|
||||
case StorageView.Config:
|
||||
return <div>CONFIGURE?</div>;
|
||||
case StorageView.Perms:
|
||||
return <div>Permissions</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.tableWrapper}>
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<div style={{ width: `${width}px`, height: `${height}px` }}>
|
||||
<Table
|
||||
height={height}
|
||||
width={width}
|
||||
data={listing}
|
||||
noHeader={false}
|
||||
showTypeIcons={false}
|
||||
resizable={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
// TODO: remove `height: 90%`
|
||||
wrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
}),
|
||||
tableControlRowWrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
// TODO: remove `height: 100%`
|
||||
tableWrapper: css({
|
||||
border: `1px solid ${theme.colors.border.medium}`,
|
||||
height: '100%',
|
||||
}),
|
||||
uploadSpot: css({
|
||||
marginLeft: theme.spacing(2),
|
||||
}),
|
||||
border: css({
|
||||
border: `1px solid ${theme.colors.border.medium}`,
|
||||
padding: theme.spacing(2),
|
||||
}),
|
||||
});
|
@ -1,135 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, Button, Card, FilterInput, Icon, IconName, TagList, useStyles2, Stack, InlineField } from '@grafana/ui';
|
||||
|
||||
import { getGrafanaStorage } from './storage';
|
||||
import { StorageInfo, StorageView } from './types';
|
||||
|
||||
interface Props {
|
||||
root: DataFrame;
|
||||
onPathChange: (p: string, v?: StorageView) => void;
|
||||
}
|
||||
|
||||
export function RootView({ root, onPathChange }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const storage = useAsync(getGrafanaStorage().getConfig);
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
let base = location.pathname;
|
||||
if (!base.endsWith('/')) {
|
||||
base += '/';
|
||||
}
|
||||
|
||||
const roots = useMemo(() => {
|
||||
let show = storage.value ?? [];
|
||||
if (searchQuery?.length) {
|
||||
const lower = searchQuery.toLowerCase();
|
||||
show = show.filter((r) => {
|
||||
const v = r.config;
|
||||
const isMatch = v.name.toLowerCase().indexOf(lower) >= 0 || v.description.toLowerCase().indexOf(lower) >= 0;
|
||||
if (isMatch) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
const base: StorageInfo[] = [];
|
||||
const content: StorageInfo[] = [];
|
||||
for (const r of show ?? []) {
|
||||
if (r.config.underContentRoot) {
|
||||
content.push(r);
|
||||
} else if (r.config.prefix !== 'content') {
|
||||
base.push(r);
|
||||
}
|
||||
}
|
||||
return { base, content };
|
||||
}, [searchQuery, storage]);
|
||||
|
||||
const renderRoots = (pfix: string, roots: StorageInfo[]) => {
|
||||
return (
|
||||
<Stack direction="column">
|
||||
{roots.map((s) => {
|
||||
const ok = s.ready;
|
||||
return (
|
||||
<Card key={s.config.prefix} href={ok ? `admin/storage/${pfix}${s.config.prefix}/` : undefined}>
|
||||
<Card.Heading>{s.config.name}</Card.Heading>
|
||||
<Card.Meta className={styles.clickable}>
|
||||
{s.config.description}
|
||||
{s.config.git?.remote && <a href={s.config.git?.remote}>{s.config.git?.remote}</a>}
|
||||
</Card.Meta>
|
||||
{s.notice?.map((notice) => <Alert key={notice.text} severity={notice.severity} title={notice.text} />)}
|
||||
|
||||
<Card.Tags className={styles.clickable}>
|
||||
<Stack>
|
||||
<TagList tags={getTags(s)} />
|
||||
</Stack>
|
||||
</Card.Tags>
|
||||
<Card.Figure className={styles.clickable}>
|
||||
<Icon name={getIconName(s.config.type)} size="xxxl" className={styles.secondaryTextColor} />
|
||||
</Card.Figure>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="page-action-bar">
|
||||
<InlineField grow>
|
||||
<FilterInput placeholder="Search Storage" value={searchQuery} onChange={setSearchQuery} />
|
||||
</InlineField>
|
||||
<div className="page-action-bar__spacer" />
|
||||
<Button onClick={() => onPathChange('', StorageView.AddRoot)}>Add Root</Button>
|
||||
</div>
|
||||
|
||||
<div>{renderRoots('', roots.base)}</div>
|
||||
|
||||
<div>
|
||||
<h3>Content</h3>
|
||||
{renderRoots('content/', roots.content)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
secondaryTextColor: css({
|
||||
color: theme.colors.text.secondary,
|
||||
}),
|
||||
clickable: css({
|
||||
pointerEvents: 'none',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function getTags(v: StorageInfo) {
|
||||
const tags: string[] = [];
|
||||
if (v.builtin) {
|
||||
tags.push('Builtin');
|
||||
}
|
||||
|
||||
// Error
|
||||
if (!v.ready) {
|
||||
tags.push('Not ready');
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
export function getIconName(type: string): IconName {
|
||||
switch (type) {
|
||||
case 'git':
|
||||
return 'code-branch';
|
||||
case 'disk':
|
||||
return 'folder-open';
|
||||
case 'sql':
|
||||
return 'database';
|
||||
default:
|
||||
return 'folder-open';
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
import { useParams } from 'react-router-dom-v5-compat';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { DataFrame, NavModel, NavModelItem } from '@grafana/data';
|
||||
import { Card, Icon, Spinner } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
import { getGrafanaStorage } from './storage';
|
||||
|
||||
export function StorageFolderPage() {
|
||||
const { slug = '' } = useParams();
|
||||
const listing = useAsync((): Promise<DataFrame | undefined> => {
|
||||
return getGrafanaStorage().list('content/' + slug);
|
||||
}, [slug]);
|
||||
|
||||
const childRoot = slug.length > 0 ? `g/${slug}/` : 'g/';
|
||||
const pageNav = getPageNavFromSlug(slug);
|
||||
|
||||
const renderListing = () => {
|
||||
if (listing.value) {
|
||||
const names = listing.value.fields[0].values;
|
||||
return names.map((item: string) => {
|
||||
let name = item;
|
||||
const isFolder = name.indexOf('.') < 0;
|
||||
const isDash = !isFolder && name.endsWith('.json');
|
||||
const url = `${childRoot}${name}`;
|
||||
|
||||
return (
|
||||
<Card key={name} href={isFolder || isDash ? url : 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>;
|
||||
};
|
||||
|
||||
const navModel = getRootContentNavModel();
|
||||
|
||||
return (
|
||||
<Page navModel={navModel} pageNav={pageNav}>
|
||||
{renderListing()}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export function getPageNavFromSlug(slug: string) {
|
||||
const parts = slug.split('/');
|
||||
let pageNavs: NavModelItem[] = [];
|
||||
let url = 'g';
|
||||
let lastPageNav: NavModelItem | undefined;
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
url += `/${parts[i]}`;
|
||||
pageNavs.push({ text: parts[i], url, parentItem: lastPageNav });
|
||||
lastPageNav = pageNavs[pageNavs.length - 1];
|
||||
}
|
||||
|
||||
return lastPageNav;
|
||||
}
|
||||
|
||||
export function getRootContentNavModel(): NavModel {
|
||||
return { main: { text: 'C:' }, node: { text: 'Content', url: '/g' } };
|
||||
}
|
||||
|
||||
export default StorageFolderPage;
|
@ -1,296 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom-v5-compat';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { DataFrame, GrafanaTheme2, isDataFrame, ValueLinkConfig } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { useStyles2, Spinner, TabsBar, Tab, Button, Stack, Box, Alert, toIconName } 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';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
import { ShowConfirmModalEvent } from 'app/types/events';
|
||||
|
||||
import { AddRootView } from './AddRootView';
|
||||
import { Breadcrumb } from './Breadcrumb';
|
||||
import { CreateNewFolderModal } from './CreateNewFolderModal';
|
||||
import { FileView } from './FileView';
|
||||
import { FolderView } from './FolderView';
|
||||
import { RootView } from './RootView';
|
||||
import { UploadButton } from './UploadButton';
|
||||
import { getGrafanaStorage, filenameAlreadyExists } from './storage';
|
||||
import { StorageView } from './types';
|
||||
|
||||
interface RouteParams {
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface QueryParams {
|
||||
view: StorageView;
|
||||
}
|
||||
|
||||
const folderNameRegex = /^[a-z\d!\-_.*'() ]+$/;
|
||||
const folderNameMaxLength = 256;
|
||||
|
||||
interface Props extends GrafanaRouteComponentProps<RouteParams, QueryParams> {}
|
||||
|
||||
const getParentPath = (path: string) => {
|
||||
const lastSlashIdx = path.lastIndexOf('/');
|
||||
if (lastSlashIdx < 1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return path.substring(0, lastSlashIdx);
|
||||
};
|
||||
|
||||
export default function StoragePage(props: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const navModel = useNavModel('storage');
|
||||
const { path = '' } = useParams();
|
||||
const view = props.queryParams.view ?? StorageView.Data;
|
||||
const setPath = (p: string, view?: StorageView) => {
|
||||
let url = ('/admin/storage/' + p).replace('//', '/');
|
||||
if (view && view !== StorageView.Data) {
|
||||
url += '?view=' + view;
|
||||
}
|
||||
locationService.push(url);
|
||||
};
|
||||
|
||||
const [isAddingNewFolder, setIsAddingNewFolder] = useState(false);
|
||||
const [errorMessages, setErrorMessages] = useState<string[]>([]);
|
||||
|
||||
const listing = useAsync((): Promise<DataFrame | undefined> => {
|
||||
return getGrafanaStorage()
|
||||
.list(path)
|
||||
.then((frame) => {
|
||||
if (frame) {
|
||||
const name = frame.fields[0];
|
||||
frame.fields[0] = {
|
||||
...name,
|
||||
getLinks: (cfg: ValueLinkConfig) => {
|
||||
const n = name.values[cfg.valueRowIndex ?? 0];
|
||||
const p = path + '/' + n;
|
||||
return [
|
||||
{
|
||||
title: `Open ${n}`,
|
||||
href: `/admin/storage/${p}`,
|
||||
target: '_self',
|
||||
origin: name,
|
||||
onClick: () => {
|
||||
setPath(p);
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
}
|
||||
return frame;
|
||||
});
|
||||
}, [path]);
|
||||
|
||||
const isFolder = useMemo(() => {
|
||||
let isFolder = path?.indexOf('/') < 0;
|
||||
if (listing.value) {
|
||||
const length = listing.value.length;
|
||||
if (length === 1) {
|
||||
const first: string = listing.value.fields[0].values[0];
|
||||
isFolder = !path.endsWith(first);
|
||||
} else {
|
||||
// TODO: handle files/folders which do not exist
|
||||
isFolder = true;
|
||||
}
|
||||
}
|
||||
return isFolder;
|
||||
}, [path, listing]);
|
||||
|
||||
const fileNames = useMemo(() => {
|
||||
return listing.value?.fields?.find((f) => f.name === 'name')?.values.filter((v) => typeof v === 'string') ?? [];
|
||||
}, [listing]);
|
||||
|
||||
const renderView = () => {
|
||||
const isRoot = !path?.length || path === '/';
|
||||
switch (view) {
|
||||
case StorageView.AddRoot:
|
||||
if (!isRoot) {
|
||||
setPath('');
|
||||
return <Spinner />;
|
||||
}
|
||||
return <AddRootView onPathChange={setPath} />;
|
||||
}
|
||||
|
||||
const frame = listing.value;
|
||||
if (!isDataFrame(frame)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (isRoot) {
|
||||
return <RootView root={frame} onPathChange={setPath} />;
|
||||
}
|
||||
|
||||
const opts = [{ what: StorageView.Data, text: 'Data' }];
|
||||
|
||||
// Root folders have a config page
|
||||
if (path.indexOf('/') < 0) {
|
||||
opts.push({ what: StorageView.Config, text: 'Configure' });
|
||||
}
|
||||
|
||||
// Lets only apply permissions to folders (for now)
|
||||
if (isFolder) {
|
||||
// opts.push({ what: StorageView.Perms, text: 'Permissions' });
|
||||
} else {
|
||||
// TODO: only if the file exists in a storage engine with
|
||||
opts.push({ what: StorageView.History, text: 'History' });
|
||||
}
|
||||
|
||||
const canAddFolder = isFolder && (path.startsWith('resources') || path.startsWith('content'));
|
||||
const canDelete = path.startsWith('resources/') || path.startsWith('content/');
|
||||
|
||||
const getErrorMessages = () => {
|
||||
return (
|
||||
<div className={styles.errorAlert}>
|
||||
<Alert title="Upload failed" severity="error" onRemove={clearAlert}>
|
||||
{errorMessages.map((error) => {
|
||||
return <div key={error}>{error}</div>;
|
||||
})}
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const clearAlert = () => {
|
||||
setErrorMessages([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Box display="flex" justifyContent="space-between" width="100%" height={3}>
|
||||
<Breadcrumb pathName={path} onPathChange={setPath} rootIcon={toIconName(navModel.node.icon ?? '')} />
|
||||
<Stack>
|
||||
{canAddFolder && (
|
||||
<>
|
||||
<UploadButton path={path} setErrorMessages={setErrorMessages} fileNames={fileNames} setPath={setPath} />
|
||||
<Button onClick={() => setIsAddingNewFolder(true)}>New Folder</Button>
|
||||
</>
|
||||
)}
|
||||
{canDelete && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
const text = isFolder
|
||||
? 'Are you sure you want to delete this folder and all its contents?'
|
||||
: 'Are you sure you want to delete this file?';
|
||||
|
||||
const parentPath = getParentPath(path);
|
||||
appEvents.publish(
|
||||
new ShowConfirmModalEvent({
|
||||
title: `Delete ${isFolder ? 'folder' : 'file'}`,
|
||||
text,
|
||||
icon: 'trash-alt',
|
||||
yesText: 'Delete',
|
||||
onConfirm: () =>
|
||||
getGrafanaStorage()
|
||||
.delete({ path, isFolder })
|
||||
.then(() => {
|
||||
setPath(parentPath);
|
||||
}),
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{errorMessages.length > 0 && getErrorMessages()}
|
||||
|
||||
<TabsBar>
|
||||
{opts.map((opt) => (
|
||||
<Tab
|
||||
key={opt.what}
|
||||
label={opt.text}
|
||||
active={opt.what === view}
|
||||
onChangeTab={() => setPath(path, opt.what)}
|
||||
/>
|
||||
))}
|
||||
</TabsBar>
|
||||
{isFolder ? (
|
||||
<FolderView listing={frame} view={view} />
|
||||
) : (
|
||||
<FileView path={path} listing={frame} onPathChange={setPath} view={view} />
|
||||
)}
|
||||
|
||||
{isAddingNewFolder && (
|
||||
<CreateNewFolderModal
|
||||
onSubmit={async ({ folderName }) => {
|
||||
const folderPath = `${path}/${folderName}`;
|
||||
const res = await getGrafanaStorage().createFolder(folderPath);
|
||||
if (typeof res?.error !== 'string') {
|
||||
setPath(folderPath);
|
||||
setIsAddingNewFolder(false);
|
||||
}
|
||||
}}
|
||||
onDismiss={() => {
|
||||
setIsAddingNewFolder(false);
|
||||
}}
|
||||
validate={(folderName) => {
|
||||
const lowerCase = folderName.toLowerCase();
|
||||
|
||||
if (filenameAlreadyExists(folderName, fileNames)) {
|
||||
return 'A file or a folder with the same name already exists';
|
||||
}
|
||||
|
||||
if (!folderNameRegex.test(lowerCase)) {
|
||||
return 'Name contains illegal characters';
|
||||
}
|
||||
|
||||
if (folderName.length > folderNameMaxLength) {
|
||||
return `Name is too long, maximum length: ${folderNameMaxLength} characters`;
|
||||
}
|
||||
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents isLoading={listing.loading}>{renderView()}</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
// TODO: remove `height: 90%`
|
||||
wrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
}),
|
||||
tableControlRowWrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
// TODO: remove `height: 100%`
|
||||
tableWrapper: css({
|
||||
border: `1px solid ${theme.colors.border.medium}`,
|
||||
height: '100%',
|
||||
}),
|
||||
border: css({
|
||||
border: `1px solid ${theme.colors.border.medium}`,
|
||||
padding: theme.spacing(2),
|
||||
}),
|
||||
errorAlert: css({
|
||||
paddingTop: '20px',
|
||||
}),
|
||||
uploadButton: css({
|
||||
marginRight: theme.spacing(2),
|
||||
}),
|
||||
});
|
@ -1,118 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { FormEvent, useEffect, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { ConfirmModal, FileUpload, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { filenameAlreadyExists, getGrafanaStorage } from './storage';
|
||||
import { StorageView, UploadResponse } from './types';
|
||||
|
||||
interface Props {
|
||||
setErrorMessages: (errors: string[]) => void;
|
||||
setPath: (p: string, view?: StorageView) => void;
|
||||
path: string;
|
||||
fileNames: string[];
|
||||
}
|
||||
|
||||
const fileFormats = 'image/jpg, image/jpeg, image/png, image/gif, image/webp';
|
||||
|
||||
export function UploadButton({ setErrorMessages, setPath, path, fileNames }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const [file, setFile] = useState<File | undefined>(undefined);
|
||||
const [filenameExists, setFilenameExists] = useState(false);
|
||||
const [fileUploadKey, setFileUploadKey] = useState(1);
|
||||
const [isConfirmOpen, setIsConfirmOpen] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setFileUploadKey((prev) => prev + 1);
|
||||
}, [file]);
|
||||
|
||||
const onUpload = (rsp: UploadResponse) => {
|
||||
console.log('Uploaded: ' + path);
|
||||
if (rsp.path) {
|
||||
setPath(rsp.path);
|
||||
} else {
|
||||
setPath(path); // back to data
|
||||
}
|
||||
};
|
||||
|
||||
const doUpload = async (fileToUpload: File, overwriteExistingFile: boolean) => {
|
||||
if (!fileToUpload) {
|
||||
setErrorMessages(['Please select a file.']);
|
||||
return;
|
||||
}
|
||||
|
||||
const rsp = await getGrafanaStorage().upload(path, fileToUpload, overwriteExistingFile);
|
||||
if (rsp.status !== 200) {
|
||||
setErrorMessages([rsp.message]);
|
||||
} else {
|
||||
onUpload(rsp);
|
||||
}
|
||||
};
|
||||
|
||||
const onFileUpload = (event: FormEvent<HTMLInputElement>) => {
|
||||
setErrorMessages([]);
|
||||
|
||||
const fileToUpload =
|
||||
event.currentTarget.files && event.currentTarget.files.length > 0 && event.currentTarget.files[0]
|
||||
? event.currentTarget.files[0]
|
||||
: undefined;
|
||||
if (fileToUpload) {
|
||||
setFile(fileToUpload);
|
||||
|
||||
const fileExists = filenameAlreadyExists(fileToUpload.name, fileNames);
|
||||
if (!fileExists) {
|
||||
setFilenameExists(false);
|
||||
doUpload(fileToUpload, false).then((r) => {});
|
||||
} else {
|
||||
setFilenameExists(true);
|
||||
setIsConfirmOpen(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onOverwriteConfirm = () => {
|
||||
if (file) {
|
||||
doUpload(file, true).then((r) => {});
|
||||
setIsConfirmOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onOverwriteDismiss = () => {
|
||||
setFile(undefined);
|
||||
setFilenameExists(false);
|
||||
setIsConfirmOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FileUpload accept={fileFormats} onFileUpload={onFileUpload} key={fileUploadKey} className={styles.uploadButton}>
|
||||
Upload
|
||||
</FileUpload>
|
||||
|
||||
{file && filenameExists && (
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmOpen}
|
||||
body={
|
||||
<div>
|
||||
<p>{file?.name}</p>
|
||||
<p>A file with this name already exists.</p>
|
||||
<p>What would you like to do?</p>
|
||||
</div>
|
||||
}
|
||||
title={'This file already exists'}
|
||||
confirmText={'Replace'}
|
||||
onConfirm={onOverwriteConfirm}
|
||||
onDismiss={onOverwriteDismiss}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
uploadButton: css({
|
||||
marginRight: theme.spacing(2),
|
||||
}),
|
||||
});
|
@ -1,145 +0,0 @@
|
||||
import { DataFrame, dataFrameFromJSON, DataFrameJSON, getDisplayProcessor } from '@grafana/data';
|
||||
import { config, getBackendSrv } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
import { UploadResponse, StorageInfo, ItemOptions, WriteValueRequest, WriteValueResponse } from './types';
|
||||
|
||||
// Likely should be built into the search interface!
|
||||
export interface GrafanaStorage {
|
||||
get: <T = any>(path: string) => Promise<T>;
|
||||
list: (path: string) => Promise<DataFrame | undefined>;
|
||||
upload: (folder: string, file: File, overwriteExistingFile: boolean) => Promise<UploadResponse>;
|
||||
createFolder: (path: string) => Promise<{ error?: string }>;
|
||||
delete: (path: { isFolder: boolean; path: string }) => Promise<{ error?: string }>;
|
||||
|
||||
/** Admin only */
|
||||
getConfig: () => Promise<StorageInfo[]>;
|
||||
|
||||
/** Called before save */
|
||||
getOptions: (path: string) => Promise<ItemOptions>;
|
||||
|
||||
/** Saves dashboards */
|
||||
write: (path: string, options: WriteValueRequest) => Promise<WriteValueResponse>;
|
||||
}
|
||||
|
||||
class SimpleStorage implements GrafanaStorage {
|
||||
constructor() {}
|
||||
|
||||
async get<T = any>(path: string): Promise<T> {
|
||||
const storagePath = `api/storage/read/${path}`.replace('//', '/');
|
||||
return getBackendSrv().get<T>(storagePath);
|
||||
}
|
||||
|
||||
async list(path: string): Promise<DataFrame | undefined> {
|
||||
let url = 'api/storage/list/';
|
||||
if (path) {
|
||||
url += path + '/';
|
||||
}
|
||||
const rsp = await getBackendSrv().get<DataFrameJSON>(url);
|
||||
if (rsp?.data) {
|
||||
const f = dataFrameFromJSON(rsp);
|
||||
for (const field of f.fields) {
|
||||
field.display = getDisplayProcessor({ field, theme: config.theme2 });
|
||||
}
|
||||
return f;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async createFolder(path: string): Promise<{ error?: string }> {
|
||||
const res = await getBackendSrv().post<{ success: boolean; message: string }>(
|
||||
'/api/storage/createFolder',
|
||||
JSON.stringify({ path })
|
||||
);
|
||||
|
||||
if (!res.success) {
|
||||
return {
|
||||
error: res.message ?? 'unknown error',
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
async deleteFolder(req: { path: string; force: boolean }): Promise<{ error?: string }> {
|
||||
const res = await getBackendSrv().post<{ success: boolean; message: string }>(
|
||||
`/api/storage/deleteFolder`,
|
||||
JSON.stringify(req)
|
||||
);
|
||||
|
||||
if (!res.success) {
|
||||
return {
|
||||
error: res.message ?? 'unknown error',
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
async deleteFile(req: { path: string }): Promise<{ error?: string }> {
|
||||
const res = await getBackendSrv().post<{ success: boolean; message: string }>(`/api/storage/delete/${req.path}`);
|
||||
|
||||
if (!res.success) {
|
||||
return {
|
||||
error: res.message ?? 'unknown error',
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
async delete(req: { isFolder: boolean; path: string }): Promise<{ error?: string }> {
|
||||
return req.isFolder ? this.deleteFolder({ path: req.path, force: true }) : this.deleteFile({ path: req.path });
|
||||
}
|
||||
|
||||
async upload(folder: string, file: File, overwriteExistingFile: boolean): Promise<UploadResponse> {
|
||||
const formData = new FormData();
|
||||
formData.append('folder', folder);
|
||||
formData.append('file', file);
|
||||
formData.append('overwriteExistingFile', String(overwriteExistingFile));
|
||||
const res = await fetch('/api/storage/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
let body = await res.json();
|
||||
if (!body) {
|
||||
body = {};
|
||||
}
|
||||
body.status = res.status;
|
||||
body.statusText = res.statusText;
|
||||
if (res.status !== 200 && !body.err) {
|
||||
body.err = true;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
async write(path: string, options: WriteValueRequest): Promise<WriteValueResponse> {
|
||||
return backendSrv.post<WriteValueResponse>(`/api/storage/write/${path}`, options);
|
||||
}
|
||||
|
||||
async getConfig() {
|
||||
return getBackendSrv().get<StorageInfo[]>('/api/storage/config');
|
||||
}
|
||||
|
||||
async getOptions(path: string) {
|
||||
return getBackendSrv().get<ItemOptions>(`/api/storage/options/${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
export function getGrafanaStorage() {
|
||||
if (!storage) {
|
||||
storage = new SimpleStorage();
|
||||
}
|
||||
return storage;
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
import { QueryResultMetaNotice, SelectableValue } from '@grafana/data';
|
||||
|
||||
export enum StorageView {
|
||||
Data = 'data',
|
||||
Config = 'config',
|
||||
Perms = 'perms',
|
||||
History = 'history',
|
||||
AddRoot = 'add',
|
||||
}
|
||||
|
||||
export interface UploadResponse {
|
||||
status: number;
|
||||
statusText: string;
|
||||
|
||||
err?: boolean;
|
||||
message: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface StorageInfo {
|
||||
editable?: boolean;
|
||||
builtin?: boolean;
|
||||
ready?: boolean;
|
||||
notice?: QueryResultMetaNotice[];
|
||||
config: StorageConfig;
|
||||
}
|
||||
|
||||
export interface StorageConfig {
|
||||
type: string;
|
||||
prefix: string;
|
||||
name: string;
|
||||
description: string;
|
||||
underContentRoot: string;
|
||||
disk?: {
|
||||
path: string;
|
||||
};
|
||||
git?: {
|
||||
remote: string;
|
||||
branch: string;
|
||||
root: string;
|
||||
requirePullRequest: boolean;
|
||||
accessToken: string;
|
||||
};
|
||||
sql?: {};
|
||||
}
|
||||
|
||||
export enum WorkflowID {
|
||||
Save = 'save',
|
||||
PR = 'pr',
|
||||
Push = 'push',
|
||||
}
|
||||
|
||||
export interface WriteValueRequest {
|
||||
kind: string;
|
||||
body: {}; // json body
|
||||
message?: string;
|
||||
title?: string;
|
||||
workflow: WorkflowID;
|
||||
}
|
||||
|
||||
export interface WriteValueResponse {
|
||||
code: number;
|
||||
message?: string;
|
||||
url?: string;
|
||||
hash?: string;
|
||||
branch?: string;
|
||||
pending?: boolean;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export interface ItemOptions {
|
||||
path: string;
|
||||
workflows: Array<SelectableValue<WorkflowID>>;
|
||||
}
|
@ -369,13 +369,6 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
)
|
||||
: () => <Navigate replace to="/admin" />,
|
||||
},
|
||||
{
|
||||
path: '/admin/storage/:path/*',
|
||||
roles: () => ['Admin'],
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "StoragePage" */ 'app/features/storage/StoragePage')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/admin/stats',
|
||||
component: SafeDynamicImport(
|
||||
|
@ -101,7 +101,6 @@ export enum DashboardRoutes {
|
||||
Home = 'home-dashboard',
|
||||
New = 'new-dashboard',
|
||||
Normal = 'normal-dashboard',
|
||||
Path = 'path-dashboard',
|
||||
Scripted = 'scripted-dashboard',
|
||||
Public = 'public-dashboard',
|
||||
Embedded = 'embedded-dashboard',
|
||||
|
Loading…
Reference in New Issue
Block a user