diff --git a/.betterer.results b/.betterer.results
index 1faa18d7dd4..b017487e851 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -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 ", "0"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "1"]
- ],
- "public/app/features/storage/CreateNewFolderModal.tsx:5381": [
- [0, 0, 0, "No untranslated strings. Wrap text with ", "0"]
- ],
- "public/app/features/storage/FileView.tsx:5381": [
- [0, 0, 0, "No untranslated strings. Wrap text with ", "0"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "1"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "2"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "3"]
- ],
- "public/app/features/storage/FolderView.tsx:5381": [
- [0, 0, 0, "No untranslated strings. Wrap text with ", "0"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "1"]
- ],
- "public/app/features/storage/RootView.tsx:5381": [
- [0, 0, 0, "No untranslated strings. Wrap text with ", "0"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "1"]
- ],
- "public/app/features/storage/StorageFolderPage.tsx:5381": [
- [0, 0, 0, "No untranslated strings. Wrap text with ", "0"]
- ],
- "public/app/features/storage/StoragePage.tsx:5381": [
- [0, 0, 0, "No untranslated strings. Wrap text with ", "0"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "1"]
- ],
- "public/app/features/storage/UploadButton.tsx:5381": [
- [0, 0, 0, "No untranslated strings. Wrap text with ", "0"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "1"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "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 ", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with ", "1"],
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 2ed38aabee7..e4542d59375 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -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
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 10b62c61edf..01d94f82df4 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -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)
diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx
index a1ae9d5f019..fffdf296dd0 100644
--- a/public/app/features/dashboard/containers/DashboardPage.tsx
+++ b/public/app/features/dashboard/containers/DashboardPage.tsx
@@ -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') {
diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts
index cf58c391fd3..4ee7261cf02 100644
--- a/public/app/features/dashboard/state/initDashboard.ts
+++ b/public/app/features/dashboard/state/initDashboard.ts
@@ -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 };
}
diff --git a/public/app/features/storage/AddRootView.tsx b/public/app/features/storage/AddRootView.tsx
deleted file mode 100644
index 7850fa85c4e..00000000000
--- a/public/app/features/storage/AddRootView.tsx
+++ /dev/null
@@ -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 (
-
-
TODO... Add ROOT
-
-
- );
-}
diff --git a/public/app/features/storage/Breadcrumb.tsx b/public/app/features/storage/Breadcrumb.tsx
deleted file mode 100644
index 2242751782a..00000000000
--- a/public/app/features/storage/Breadcrumb.tsx
+++ /dev/null
@@ -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 (
-
- {rootIcon && (
- -
- onPathChange('')} />
-
- )}
- {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
- -
- {path}
-
- );
- })}
-
- );
-}
-
-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,
- },
- },
- }),
- };
-}
diff --git a/public/app/features/storage/CreateNewFolderModal.tsx b/public/app/features/storage/CreateNewFolderModal.tsx
deleted file mode 100644
index 8f52939f8a7..00000000000
--- a/public/app/features/storage/CreateNewFolderModal.tsx
+++ /dev/null
@@ -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;
- onDismiss: () => void;
- validate: Validate;
-}
-
-const initialFormModel = { folderName: '' };
-
-export function CreateNewFolderModal({ validate, onDismiss, onSubmit }: Props) {
- return (
-
-
-
- );
-}
diff --git a/public/app/features/storage/FileView.tsx b/public/app/features/storage/FileView.tsx
deleted file mode 100644
index 80059c6c76d..00000000000
--- a/public/app/features/storage/FileView.tsx
+++ /dev/null
@@ -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 CONFIGURE?
;
- case StorageView.Perms:
- return Permissions
;
- case StorageView.History:
- return TODO... history
;
- }
-
- let src = `api/storage/read/${path}`;
- if (src.endsWith('/')) {
- src = src.substring(0, src.length - 1);
- }
-
- switch (info.category) {
- case 'svg':
- return (
-
-
-
- );
- case 'image':
- return (
-
- );
- case 'text':
- return (
-
-
- {({ width, height }) => (
- {
- console.log('CHANGED!', text);
- }}
- />
- )}
-
-
- );
- }
-
- return (
-
- );
-}
-
-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({}),
-});
diff --git a/public/app/features/storage/FolderView.tsx b/public/app/features/storage/FolderView.tsx
deleted file mode 100644
index 2ada5b0ae33..00000000000
--- a/public/app/features/storage/FolderView.tsx
+++ /dev/null
@@ -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 CONFIGURE?
;
- case StorageView.Perms:
- return Permissions
;
- }
-
- return (
-
-
- {({ width, height }) => (
-
- )}
-
-
- );
-}
-
-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),
- }),
-});
diff --git a/public/app/features/storage/RootView.tsx b/public/app/features/storage/RootView.tsx
deleted file mode 100644
index bfa87d16458..00000000000
--- a/public/app/features/storage/RootView.tsx
+++ /dev/null
@@ -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('');
- 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 (
-
- {roots.map((s) => {
- const ok = s.ready;
- return (
-
- {s.config.name}
-
- {s.config.description}
- {s.config.git?.remote && {s.config.git?.remote}}
-
- {s.notice?.map((notice) => )}
-
-
-
-
-
-
-
-
-
-
- );
- })}
-
- );
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
{renderRoots('', roots.base)}
-
-
-
Content
- {renderRoots('content/', roots.content)}
-
-
- );
-}
-
-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';
- }
-}
diff --git a/public/app/features/storage/StorageFolderPage.tsx b/public/app/features/storage/StorageFolderPage.tsx
deleted file mode 100644
index 1808f6c51c2..00000000000
--- a/public/app/features/storage/StorageFolderPage.tsx
+++ /dev/null
@@ -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 => {
- 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 (
-
- {name}
-
-
-
-
- );
- });
- }
- if (listing.loading) {
- return ;
- }
- return ?
;
- };
-
- const navModel = getRootContentNavModel();
-
- return (
-
- {renderListing()}
-
- );
-}
-
-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;
diff --git a/public/app/features/storage/StoragePage.tsx b/public/app/features/storage/StoragePage.tsx
deleted file mode 100644
index 94b597e77f4..00000000000
--- a/public/app/features/storage/StoragePage.tsx
+++ /dev/null
@@ -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 {}
-
-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([]);
-
- const listing = useAsync((): Promise => {
- 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 ;
- }
- return ;
- }
-
- const frame = listing.value;
- if (!isDataFrame(frame)) {
- return <>>;
- }
-
- if (isRoot) {
- return ;
- }
-
- 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 (
-
-
- {errorMessages.map((error) => {
- return {error}
;
- })}
-
-
- );
- };
-
- const clearAlert = () => {
- setErrorMessages([]);
- };
-
- return (
-
-
-
-
- {canAddFolder && (
- <>
-
-
- >
- )}
- {canDelete && (
-
- )}
-
-
-
- {errorMessages.length > 0 && getErrorMessages()}
-
-
- {opts.map((opt) => (
- setPath(path, opt.what)}
- />
- ))}
-
- {isFolder ? (
-
- ) : (
-
- )}
-
- {isAddingNewFolder && (
- {
- 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;
- }}
- />
- )}
-
- );
- };
-
- return (
-
- {renderView()}
-
- );
-}
-
-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),
- }),
-});
diff --git a/public/app/features/storage/UploadButton.tsx b/public/app/features/storage/UploadButton.tsx
deleted file mode 100644
index 989e3d49690..00000000000
--- a/public/app/features/storage/UploadButton.tsx
+++ /dev/null
@@ -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(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) => {
- 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 (
- <>
-
- Upload
-
-
- {file && filenameExists && (
-
- {file?.name}
- A file with this name already exists.
- What would you like to do?
-
- }
- title={'This file already exists'}
- confirmText={'Replace'}
- onConfirm={onOverwriteConfirm}
- onDismiss={onOverwriteDismiss}
- />
- )}
- >
- );
-}
-
-const getStyles = (theme: GrafanaTheme2) => ({
- uploadButton: css({
- marginRight: theme.spacing(2),
- }),
-});
diff --git a/public/app/features/storage/storage.ts b/public/app/features/storage/storage.ts
deleted file mode 100644
index 38c05e9ec38..00000000000
--- a/public/app/features/storage/storage.ts
+++ /dev/null
@@ -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: (path: string) => Promise;
- list: (path: string) => Promise;
- upload: (folder: string, file: File, overwriteExistingFile: boolean) => Promise;
- createFolder: (path: string) => Promise<{ error?: string }>;
- delete: (path: { isFolder: boolean; path: string }) => Promise<{ error?: string }>;
-
- /** Admin only */
- getConfig: () => Promise;
-
- /** Called before save */
- getOptions: (path: string) => Promise;
-
- /** Saves dashboards */
- write: (path: string, options: WriteValueRequest) => Promise;
-}
-
-class SimpleStorage implements GrafanaStorage {
- constructor() {}
-
- async get(path: string): Promise {
- const storagePath = `api/storage/read/${path}`.replace('//', '/');
- return getBackendSrv().get(storagePath);
- }
-
- async list(path: string): Promise {
- let url = 'api/storage/list/';
- if (path) {
- url += path + '/';
- }
- const rsp = await getBackendSrv().get(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 {
- 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 {
- return backendSrv.post(`/api/storage/write/${path}`, options);
- }
-
- async getConfig() {
- return getBackendSrv().get('/api/storage/config');
- }
-
- async getOptions(path: string) {
- return getBackendSrv().get(`/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;
-}
diff --git a/public/app/features/storage/types.ts b/public/app/features/storage/types.ts
deleted file mode 100644
index 21f24bb126f..00000000000
--- a/public/app/features/storage/types.ts
+++ /dev/null
@@ -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>;
-}
diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx
index 3543f334cdf..6bf2b59d254 100644
--- a/public/app/routes/routes.tsx
+++ b/public/app/routes/routes.tsx
@@ -369,13 +369,6 @@ export function getAppRoutes(): RouteDescriptor[] {
)
: () => ,
},
- {
- path: '/admin/storage/:path/*',
- roles: () => ['Admin'],
- component: SafeDynamicImport(
- () => import(/* webpackChunkName: "StoragePage" */ 'app/features/storage/StoragePage')
- ),
- },
{
path: '/admin/stats',
component: SafeDynamicImport(
diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts
index 396642594a9..63e913d5b75 100644
--- a/public/app/types/dashboard.ts
+++ b/public/app/types/dashboard.ts
@@ -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',