View File Name : StoragePage.9b93a99e75f7f2842c11.js.map
\n }\n title={'This file already exists'}\n confirmText={'Replace'}\n onConfirm={onOverwriteConfirm}\n onDismiss={onOverwriteDismiss}\n />\n )}\n >\n );\n}\n\nconst getStyles = (theme: GrafanaTheme2) => ({\n uploadButton: css`\n margin-right: ${theme.spacing(2)};\n `,\n});\n","import { css } from '@emotion/css';\nimport React, { useMemo, useState } from 'react';\nimport { useAsync } from 'react-use';\n\nimport { DataFrame, GrafanaTheme2, isDataFrame, ValueLinkConfig } from '@grafana/data';\nimport { locationService } from '@grafana/runtime';\nimport { useStyles2, Spinner, TabsBar, Tab, Button, HorizontalGroup, Alert, toIconName } from '@grafana/ui';\nimport appEvents from 'app/core/app_events';\nimport { Page } from 'app/core/components/Page/Page';\nimport { useNavModel } from 'app/core/hooks/useNavModel';\nimport { GrafanaRouteComponentProps } from 'app/core/navigation/types';\nimport { ShowConfirmModalEvent } from 'app/types/events';\n\nimport { AddRootView } from './AddRootView';\nimport { Breadcrumb } from './Breadcrumb';\nimport { CreateNewFolderModal } from './CreateNewFolderModal';\nimport { FileView } from './FileView';\nimport { FolderView } from './FolderView';\nimport { RootView } from './RootView';\nimport { UploadButton } from './UploadButton';\nimport { getGrafanaStorage, filenameAlreadyExists } from './storage';\nimport { StorageView } from './types';\n\ninterface RouteParams {\n path: string;\n}\n\ninterface QueryParams {\n view: StorageView;\n}\n\nconst folderNameRegex = /^[a-z\\d!\\-_.*'() ]+$/;\nconst folderNameMaxLength = 256;\n\ninterface Props extends GrafanaRouteComponentProps
{}\n\nconst getParentPath = (path: string) => {\n const lastSlashIdx = path.lastIndexOf('/');\n if (lastSlashIdx < 1) {\n return '';\n }\n\n return path.substring(0, lastSlashIdx);\n};\n\nexport default function StoragePage(props: Props) {\n const styles = useStyles2(getStyles);\n const navModel = useNavModel('storage');\n const path = props.match.params.path ?? '';\n const view = props.queryParams.view ?? StorageView.Data;\n const setPath = (p: string, view?: StorageView) => {\n let url = ('/admin/storage/' + p).replace('//', '/');\n if (view && view !== StorageView.Data) {\n url += '?view=' + view;\n }\n locationService.push(url);\n };\n\n const [isAddingNewFolder, setIsAddingNewFolder] = useState(false);\n const [errorMessages, setErrorMessages] = useState([]);\n\n const listing = useAsync((): Promise => {\n return getGrafanaStorage()\n .list(path)\n .then((frame) => {\n if (frame) {\n const name = frame.fields[0];\n frame.fields[0] = {\n ...name,\n getLinks: (cfg: ValueLinkConfig) => {\n const n = name.values[cfg.valueRowIndex ?? 0];\n const p = path + '/' + n;\n return [\n {\n title: `Open ${n}`,\n href: `/admin/storage/${p}`,\n target: '_self',\n origin: name,\n onClick: () => {\n setPath(p);\n },\n },\n ];\n },\n };\n }\n return frame;\n });\n }, [path]);\n\n const isFolder = useMemo(() => {\n let isFolder = path?.indexOf('/') < 0;\n if (listing.value) {\n const length = listing.value.length;\n if (length === 1) {\n const first: string = listing.value.fields[0].values[0];\n isFolder = !path.endsWith(first);\n } else {\n // TODO: handle files/folders which do not exist\n isFolder = true;\n }\n }\n return isFolder;\n }, [path, listing]);\n\n const fileNames = useMemo(() => {\n return listing.value?.fields?.find((f) => f.name === 'name')?.values.filter((v) => typeof v === 'string') ?? [];\n }, [listing]);\n\n const renderView = () => {\n const isRoot = !path?.length || path === '/';\n switch (view) {\n case StorageView.AddRoot:\n if (!isRoot) {\n setPath('');\n return ;\n }\n return ;\n }\n\n const frame = listing.value;\n if (!isDataFrame(frame)) {\n return <>>;\n }\n\n if (isRoot) {\n return ;\n }\n\n const opts = [{ what: StorageView.Data, text: 'Data' }];\n\n // Root folders have a config page\n if (path.indexOf('/') < 0) {\n opts.push({ what: StorageView.Config, text: 'Configure' });\n }\n\n // Lets only apply permissions to folders (for now)\n if (isFolder) {\n // opts.push({ what: StorageView.Perms, text: 'Permissions' });\n } else {\n // TODO: only if the file exists in a storage engine with\n opts.push({ what: StorageView.History, text: 'History' });\n }\n\n const canAddFolder = isFolder && (path.startsWith('resources') || path.startsWith('content'));\n const canDelete = path.startsWith('resources/') || path.startsWith('content/');\n\n const getErrorMessages = () => {\n return (\n \n
\n {errorMessages.map((error) => {\n return {error}
;\n })}\n \n
\n );\n };\n\n const clearAlert = () => {\n setErrorMessages([]);\n };\n\n return (\n \n \n \n \n {canAddFolder && (\n <>\n \n \n >\n )}\n {canDelete && (\n \n )}\n \n \n\n {errorMessages.length > 0 && getErrorMessages()}\n\n \n {opts.map((opt) => (\n setPath(path, opt.what)}\n />\n ))}\n \n {isFolder ? (\n \n ) : (\n \n )}\n\n {isAddingNewFolder && (\n {\n const folderPath = `${path}/${folderName}`;\n const res = await getGrafanaStorage().createFolder(folderPath);\n if (typeof res?.error !== 'string') {\n setPath(folderPath);\n setIsAddingNewFolder(false);\n }\n }}\n onDismiss={() => {\n setIsAddingNewFolder(false);\n }}\n validate={(folderName) => {\n const lowerCase = folderName.toLowerCase();\n\n if (filenameAlreadyExists(folderName, fileNames)) {\n return 'A file or a folder with the same name already exists';\n }\n\n if (!folderNameRegex.test(lowerCase)) {\n return 'Name contains illegal characters';\n }\n\n if (folderName.length > folderNameMaxLength) {\n return `Name is too long, maximum length: ${folderNameMaxLength} characters`;\n }\n\n return true;\n }}\n />\n )}\n
\n );\n };\n\n return (\n \n {renderView()}\n \n );\n}\n\nconst getStyles = (theme: GrafanaTheme2) => ({\n // TODO: remove `height: 90%`\n wrapper: css`\n display: flex;\n flex-direction: column;\n height: 100%;\n `,\n tableControlRowWrapper: css`\n display: flex;\n flex-direction: row;\n align-items: center;\n margin-bottom: ${theme.spacing(2)};\n `,\n // TODO: remove `height: 100%`\n tableWrapper: css`\n border: 1px solid ${theme.colors.border.medium};\n height: 100%;\n `,\n border: css`\n border: 1px solid ${theme.colors.border.medium};\n padding: ${theme.spacing(2)};\n `,\n errorAlert: css`\n padding-top: 20px;\n `,\n uploadButton: css`\n margin-right: ${theme.spacing(2)};\n `,\n});\n","import { DataFrame, dataFrameFromJSON, DataFrameJSON, getDisplayProcessor } from '@grafana/data';\nimport { config, getBackendSrv } from '@grafana/runtime';\nimport { backendSrv } from 'app/core/services/backend_srv';\n\nimport { UploadResponse, StorageInfo, ItemOptions, WriteValueRequest, WriteValueResponse } from './types';\n\n// Likely should be built into the search interface!\nexport interface GrafanaStorage {\n get: (path: string) => Promise;\n list: (path: string) => Promise;\n upload: (folder: string, file: File, overwriteExistingFile: boolean) => Promise;\n createFolder: (path: string) => Promise<{ error?: string }>;\n delete: (path: { isFolder: boolean; path: string }) => Promise<{ error?: string }>;\n\n /** Admin only */\n getConfig: () => Promise;\n\n /** Called before save */\n getOptions: (path: string) => Promise;\n\n /** Saves dashboards */\n write: (path: string, options: WriteValueRequest) => Promise;\n}\n\nclass SimpleStorage implements GrafanaStorage {\n constructor() {}\n\n async get(path: string): Promise {\n const storagePath = `api/storage/read/${path}`.replace('//', '/');\n return getBackendSrv().get(storagePath);\n }\n\n async list(path: string): Promise {\n let url = 'api/storage/list/';\n if (path) {\n url += path + '/';\n }\n const rsp = await getBackendSrv().get(url);\n if (rsp?.data) {\n const f = dataFrameFromJSON(rsp);\n for (const field of f.fields) {\n field.display = getDisplayProcessor({ field, theme: config.theme2 });\n }\n return f;\n }\n return undefined;\n }\n\n async createFolder(path: string): Promise<{ error?: string }> {\n const res = await getBackendSrv().post<{ success: boolean; message: string }>(\n '/api/storage/createFolder',\n JSON.stringify({ path })\n );\n\n if (!res.success) {\n return {\n error: res.message ?? 'unknown error',\n };\n }\n\n return {};\n }\n\n async deleteFolder(req: { path: string; force: boolean }): Promise<{ error?: string }> {\n const res = await getBackendSrv().post<{ success: boolean; message: string }>(\n `/api/storage/deleteFolder`,\n JSON.stringify(req)\n );\n\n if (!res.success) {\n return {\n error: res.message ?? 'unknown error',\n };\n }\n\n return {};\n }\n\n async deleteFile(req: { path: string }): Promise<{ error?: string }> {\n const res = await getBackendSrv().post<{ success: boolean; message: string }>(`/api/storage/delete/${req.path}`);\n\n if (!res.success) {\n return {\n error: res.message ?? 'unknown error',\n };\n }\n\n return {};\n }\n\n async delete(req: { isFolder: boolean; path: string }): Promise<{ error?: string }> {\n return req.isFolder ? this.deleteFolder({ path: req.path, force: true }) : this.deleteFile({ path: req.path });\n }\n\n async upload(folder: string, file: File, overwriteExistingFile: boolean): Promise {\n const formData = new FormData();\n formData.append('folder', folder);\n formData.append('file', file);\n formData.append('overwriteExistingFile', String(overwriteExistingFile));\n const res = await fetch('/api/storage/upload', {\n method: 'POST',\n body: formData,\n });\n\n let body = await res.json();\n if (!body) {\n body = {};\n }\n body.status = res.status;\n body.statusText = res.statusText;\n if (res.status !== 200 && !body.err) {\n body.err = true;\n }\n return body;\n }\n\n async write(path: string, options: WriteValueRequest): Promise {\n return backendSrv.post(`/api/storage/write/${path}`, options);\n }\n\n async getConfig() {\n return getBackendSrv().get('/api/storage/config');\n }\n\n async getOptions(path: string) {\n return getBackendSrv().get(`/api/storage/options/${path}`);\n }\n}\n\nexport function filenameAlreadyExists(folderName: string, fileNames: string[]) {\n const lowerCase = folderName.toLowerCase();\n const trimmedLowerCase = lowerCase.trim();\n const existingTrimmedLowerCaseNames = fileNames.map((f) => f.trim().toLowerCase());\n\n return existingTrimmedLowerCaseNames.includes(trimmedLowerCase);\n}\n\nlet storage: GrafanaStorage | undefined;\n\nexport function getGrafanaStorage() {\n if (!storage) {\n storage = new SimpleStorage();\n }\n return storage;\n}\n"],"names":["AddRootView","onPathChange","Button","Breadcrumb","pathName","rootIcon","styles","getStyles","paths","Icon","path","index","url","onClickBreadcrumb","isLastBreadcrumb","theme","initialFormModel","CreateNewFolderModal","validate","onDismiss","onSubmit","Modal","Form","register","errors","Field","Input","StorageView","WorkflowID","FileView","listing","view","info","getFileDisplayInfo","body","useAsync","rsp","src","SanitizedSVG","width","height","CodeEditor","text","idx","FolderView","Table","RootView","root","storage","searchQuery","setSearchQuery","base","roots","show","lower","r","v","content","renderRoots","pfix","s","ok","Card","notice","Alert","TagList","getTags","getIconName","InlineField","FilterInput","tags","type","fileFormats","UploadButton","setErrorMessages","setPath","fileNames","file","setFile","filenameExists","setFilenameExists","fileUploadKey","setFileUploadKey","isConfirmOpen","setIsConfirmOpen","prev","onUpload","doUpload","fileToUpload","overwriteExistingFile","onFileUpload","event","onOverwriteConfirm","onOverwriteDismiss","FileUpload","ConfirmModal","folderNameRegex","folderNameMaxLength","getParentPath","lastSlashIdx","StoragePage","props","navModel","useNavModel","p","isAddingNewFolder","setIsAddingNewFolder","errorMessages","frame","name","cfg","n","isFolder","first","f","renderView","isRoot","Spinner","opts","canAddFolder","canDelete","getErrorMessages","clearAlert","error","parentPath","TabsBar","opt","Tab","folderName","folderPath","lowerCase","Page","SimpleStorage","storagePath","field","res","req","folder","formData","options","filenameAlreadyExists","trimmedLowerCase","getGrafanaStorage"],"sourceRoot":""}