From 4167214e3535f41c85004c6aca86fcb9ae8956dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Bedi?= Date: Tue, 24 Jan 2023 10:43:44 +0100 Subject: [PATCH] Panel edit: Add feature to drag & drop spreadsheet files to the grafana datasource (#60586) Co-authored-by: Oscar Kilhed Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Co-authored-by: Adela Almasan --- .../feature-toggles/index.md | 1 + package.json | 3 +- .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 6 + pkg/services/featuremgmt/toggles_gen.go | 4 + public/app/core/utils/sheet.ts | 12 ++ .../grafana/components/QueryEditor.tsx | 117 ++++++++++++++++-- .../app/plugins/datasource/grafana/types.ts | 6 + yarn.lock | 10 ++ 9 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 public/app/core/utils/sheet.ts diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index c1fdca1d1c1..8fd69ec9ccb 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -95,6 +95,7 @@ Alpha features might be changed or removed without prior notice. | `authnService` | Use new auth service to perform authentication | | `sessionRemoteCache` | Enable using remote cache for user sessions | | `alertingBacktesting` | Rule backtesting API for alerting | +| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | | `azureMultipleResourcePicker` | Azure multiple resource picker | ## Development feature toggles diff --git a/package.json b/package.json index 23a3f9bcd2b..99c2f4ef579 100644 --- a/package.json +++ b/package.json @@ -404,7 +404,8 @@ "uuid": "9.0.0", "vendor": "link:./public/vendor", "visjs-network": "4.25.0", - "whatwg-fetch": "3.6.2" + "whatwg-fetch": "3.6.2", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz" }, "resolutions": { "underscore": "1.13.6", diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index e19fb2fe59f..f46074443ac 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -87,6 +87,7 @@ export interface FeatureToggles { sessionRemoteCache?: boolean; disablePrometheusExemplarSampling?: boolean; alertingBacktesting?: boolean; + editPanelCSVDragAndDrop?: boolean; alertingNoNormalState?: boolean; azureMultipleResourcePicker?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 06927320c33..1dc085f4b6d 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -397,6 +397,12 @@ var ( Description: "Rule backtesting API for alerting", State: FeatureStateAlpha, }, + { + Name: "editPanelCSVDragAndDrop", + Description: "Enables drag and drop for CSV and Excel files", + FrontendOnly: true, + State: FeatureStateAlpha, + }, { Name: "alertingNoNormalState", Description: "Stop maintaining state of alerts that are not firing", diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index af44efff9a3..b2915a9f32a 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -291,6 +291,10 @@ const ( // Rule backtesting API for alerting FlagAlertingBacktesting = "alertingBacktesting" + // FlagEditPanelCSVDragAndDrop + // Enables drag and drop for CSV and Excel files + FlagEditPanelCSVDragAndDrop = "editPanelCSVDragAndDrop" + // FlagAlertingNoNormalState // Stop maintaining state of alerts that are not firing FlagAlertingNoNormalState = "alertingNoNormalState" diff --git a/public/app/core/utils/sheet.ts b/public/app/core/utils/sheet.ts new file mode 100644 index 00000000000..5771b772283 --- /dev/null +++ b/public/app/core/utils/sheet.ts @@ -0,0 +1,12 @@ +import { read, utils } from 'xlsx'; + +import { ArrayDataFrame, DataFrame } from '@grafana/data'; + +export function readSpreadsheet(file: ArrayBuffer): DataFrame[] { + const wb = read(file, { type: 'buffer' }); + return wb.SheetNames.map((name) => { + const frame = new ArrayDataFrame(utils.sheet_to_json(wb.Sheets[name])); + frame.name = name; + return frame; + }); +} diff --git a/public/app/plugins/datasource/grafana/components/QueryEditor.tsx b/public/app/plugins/datasource/grafana/components/QueryEditor.tsx index aae44eac03b..3956c836a19 100644 --- a/public/app/plugins/datasource/grafana/components/QueryEditor.tsx +++ b/public/app/plugins/datasource/grafana/components/QueryEditor.tsx @@ -1,3 +1,4 @@ +import { css } from '@emotion/css'; import pluralize from 'pluralize'; import React, { PureComponent } from 'react'; @@ -8,10 +9,27 @@ import { rangeUtil, DataQueryRequest, DataFrame, + DataFrameJSON, + dataFrameToJSON, + GrafanaTheme2, + getValueFormat, + formattedValueToString, } from '@grafana/data'; import { config, getBackendSrv, getDataSourceSrv } from '@grafana/runtime'; -import { InlineField, Select, Alert, Input, InlineFieldRow, InlineLabel } from '@grafana/ui'; +import { + InlineField, + Select, + Alert, + Input, + InlineFieldRow, + InlineLabel, + FileDropzone, + DropzoneFile, + Themeable2, + withTheme2, +} from '@grafana/ui'; import { hasAlphaPanels } from 'app/core/config'; +import { readSpreadsheet } from 'app/core/utils/sheet'; import { SearchQuery } from 'app/features/search/service'; import { GrafanaDatasource } from '../datasource'; @@ -19,7 +37,7 @@ import { defaultQuery, GrafanaQuery, GrafanaQueryType } from '../types'; import SearchEditor from './SearchEditor'; -type Props = QueryEditorProps; +interface Props extends QueryEditorProps, Themeable2 {} const labelWidth = 12; @@ -29,7 +47,7 @@ interface State { folders?: Array>; } -export class QueryEditor extends PureComponent { +export class UnthemedQueryEditor extends PureComponent { state: State = { channels: [], channelFields: {} }; queryTypes: Array> = [ @@ -60,6 +78,13 @@ export class QueryEditor extends PureComponent { description: 'Search for grafana resources', }); } + if (config.featureToggles.editPanelCSVDragAndDrop) { + this.queryTypes.push({ + label: 'Spreadsheet or snapshot', + value: GrafanaQueryType.Snapshot, + description: 'Query an uploaded spreadsheet or a snapshot', + }); + } } loadChannelInfo() { @@ -345,15 +370,47 @@ export class QueryEditor extends PureComponent { ); } + // Skip rendering the file list as we're handling that in this component instead. + fileListRenderer = (file: DropzoneFile, removeFile: (file: DropzoneFile) => void) => { + return null; + }; + + onDropAccepted = (files: File[]) => { + this.props.onChange({ ...this.props.query, file: { name: files[0].name, size: files[0].size } }); + }; + renderSnapshotQuery() { - const { query } = this.props; + const { query, theme } = this.props; + const file = query.file; + const styles = getStyles(theme); + const fileSize = getValueFormat('decbytes')(file ? file.size : 0); return ( - - - {pluralize('frame', query.snapshot?.length ?? 0, true)} - - + <> + + + {pluralize('frame', query.snapshot?.length ?? 0, true)} + + + {config.featureToggles.editPanelCSVDragAndDrop && ( + <> + + {file && ( +
+ {file?.name} + + {formattedValueToString(fileSize)} + +
+ )} + + )} + ); } @@ -367,6 +424,28 @@ export class QueryEditor extends PureComponent { onRunQuery(); }; + onFileDrop = (result: ArrayBuffer | String | null) => { + const snapshot: DataFrameJSON[] = []; + + if (result) { + if (!result || result instanceof String) { + return; + } + const dataFrames = readSpreadsheet(result); + dataFrames.forEach((df) => { + const dataframeJson = dataFrameToJSON(df); + snapshot.push(dataframeJson); + }); + } + + this.props.onChange({ + ...this.props.query, + queryType: GrafanaQueryType.Snapshot, + snapshot, + }); + this.props.onRunQuery(); + }; + render() { const query = { ...defaultQuery, @@ -377,7 +456,7 @@ export class QueryEditor extends PureComponent { // Only show "snapshot" when it already exists let queryTypes = this.queryTypes; - if (queryType === GrafanaQueryType.Snapshot) { + if (queryType === GrafanaQueryType.Snapshot && !config.featureToggles.editPanelCSVDragAndDrop) { queryTypes = [ ...this.queryTypes, { @@ -414,3 +493,21 @@ export class QueryEditor extends PureComponent { ); } } + +export const QueryEditor = withTheme2(UnthemedQueryEditor); + +function getStyles(theme: GrafanaTheme2) { + return { + file: css` + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: ${theme.spacing(2)}; + border: 1px dashed ${theme.colors.border.medium}; + background-color: ${theme.colors.background.secondary}; + margin-top: ${theme.spacing(1)}; + `, + }; +} diff --git a/public/app/plugins/datasource/grafana/types.ts b/public/app/plugins/datasource/grafana/types.ts index 55988f8fd81..435b03a9c22 100644 --- a/public/app/plugins/datasource/grafana/types.ts +++ b/public/app/plugins/datasource/grafana/types.ts @@ -26,6 +26,12 @@ export interface GrafanaQuery extends DataQuery { path?: string; // for list and read search?: SearchQuery; snapshot?: DataFrameJSON[]; + file?: GrafanaQueryFile; +} + +export interface GrafanaQueryFile { + name: string; + size: number; } export const defaultQuery: GrafanaQuery = { diff --git a/yarn.lock b/yarn.lock index 95d070c1c31..7750c3aa7f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21903,6 +21903,7 @@ __metadata: webpack-manifest-plugin: 5.0.0 webpack-merge: 5.8.0 whatwg-fetch: 3.6.2 + xlsx: "https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz" languageName: unknown linkType: soft @@ -39179,6 +39180,15 @@ __metadata: languageName: node linkType: hard +"xlsx@https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz": + version: 0.19.1 + resolution: "xlsx@https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz" + bin: + xlsx: ./bin/xlsx.njs + checksum: a7fa1b95dfc9a6923458e19f0dcd0c64d70ce49a5959c8f38c219fd232a4fdfd48d70115dbe01b0e58e80f336ce872495dcc3d12943c4d19b041c87f8eeb69c8 + languageName: node + linkType: hard + "xml-name-validator@npm:^3.0.0": version: 3.0.0 resolution: "xml-name-validator@npm:3.0.0"