=> {
- 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/plugins/panel/geomap/editor/FrameSelectionEditor.tsx b/public/app/plugins/panel/geomap/editor/FrameSelectionEditor.tsx
index 88eabf2ed1b..00b245ba035 100644
--- a/public/app/plugins/panel/geomap/editor/FrameSelectionEditor.tsx
+++ b/public/app/plugins/panel/geomap/editor/FrameSelectionEditor.tsx
@@ -1,7 +1,11 @@
import { useCallback } from 'react';
import { FrameMatcherID, MatcherConfig, StandardEditorProps } from '@grafana/data';
-import { RefIDPicker } from '@grafana/ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher';
+import {
+ RefIDMultiPicker,
+ RefIDPicker,
+ stringsToRegexp,
+} from '@grafana/ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher';
type Props = StandardEditorProps;
@@ -24,3 +28,28 @@ export const FrameSelectionEditor = ({ value, context, onChange }: Props) => {
);
};
+
+export const FrameMultiSelectionEditor = ({ value, context, onChange }: Props) => {
+ const onFilterChange = useCallback(
+ (v: string[]) => {
+ onChange(
+ v?.length
+ ? {
+ id: FrameMatcherID.byRefId,
+ options: stringsToRegexp(v),
+ }
+ : undefined
+ );
+ },
+ [onChange]
+ );
+
+ return (
+
+ );
+};
diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx
index 7403dec1ae5..fa7cb27ae8d 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',