From 68c61514b0d39c95c1a59d20df2f2f677e910e04 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 22 Nov 2024 13:38:02 +0300 Subject: [PATCH] Chore: Remove experimental Storage UI (#96887) --- .betterer.results | 37 --- .github/CODEOWNERS | 1 - pkg/api/api.go | 7 +- .../dashboard/containers/DashboardPage.tsx | 17 +- .../features/dashboard/state/initDashboard.ts | 4 - public/app/features/storage/AddRootView.tsx | 18 -- public/app/features/storage/Breadcrumb.tsx | 61 ---- .../features/storage/CreateNewFolderModal.tsx | 44 --- public/app/features/storage/FileView.tsx | 155 --------- public/app/features/storage/FolderView.tsx | 69 ---- public/app/features/storage/RootView.tsx | 135 -------- .../features/storage/StorageFolderPage.tsx | 72 ----- public/app/features/storage/StoragePage.tsx | 296 ------------------ public/app/features/storage/UploadButton.tsx | 118 ------- public/app/features/storage/storage.ts | 145 --------- public/app/features/storage/types.ts | 74 ----- public/app/routes/routes.tsx | 7 - public/app/types/dashboard.ts | 1 - 18 files changed, 4 insertions(+), 1257 deletions(-) delete mode 100644 public/app/features/storage/AddRootView.tsx delete mode 100644 public/app/features/storage/Breadcrumb.tsx delete mode 100644 public/app/features/storage/CreateNewFolderModal.tsx delete mode 100644 public/app/features/storage/FileView.tsx delete mode 100644 public/app/features/storage/FolderView.tsx delete mode 100644 public/app/features/storage/RootView.tsx delete mode 100644 public/app/features/storage/StorageFolderPage.tsx delete mode 100644 public/app/features/storage/StoragePage.tsx delete mode 100644 public/app/features/storage/UploadButton.tsx delete mode 100644 public/app/features/storage/storage.ts delete mode 100644 public/app/features/storage/types.ts 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 ( - -
- {({ register, errors }) => ( - <> - - - - - - - - - )} -
-
- ); -} 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 ( -
- - File preview - -
- ); - case 'text': - return ( -
- - {({ width, height }) => ( - { - console.log('CHANGED!', text); - }} - /> - )} - -
- ); - } - - return ( -
- FILE: {path} -
- ); -} - -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',