diff --git a/package.json b/package.json index 5d19e8cf0d2..555fb46d6a8 100644 --- a/package.json +++ b/package.json @@ -416,8 +416,7 @@ "uplot": "1.6.31", "uuid": "11.0.5", "visjs-network": "4.25.0", - "whatwg-fetch": "3.6.20", - "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz" + "whatwg-fetch": "3.6.20" }, "resolutions": { "underscore": "1.13.7", diff --git a/packages/grafana-data/src/themes/themeDefinitions/gloom.ts b/packages/grafana-data/src/themes/themeDefinitions/gloom.ts new file mode 100644 index 00000000000..849d4ece51a --- /dev/null +++ b/packages/grafana-data/src/themes/themeDefinitions/gloom.ts @@ -0,0 +1,82 @@ +import { NewThemeOptions } from '../createTheme'; + +/** + * Torkel's GrafanaCon theme + * very WIP state + */ + +const whiteBase = `210, 210, 220`; +const secondaryBase = `210, 210, 220`; +//const brandMain = '#3d71d9'; +//const brandText = '#6e9fff'; +const brandMain = '#ff934d'; +const brandText = '#f99a5c'; + +const disabledText = `rgba(${whiteBase}, 0.6)`; + +const gloomTheme: NewThemeOptions = { + name: 'Gloom', + colors: { + mode: 'dark', + border: { + weak: `rgba(${whiteBase}, 0.08)`, + medium: `rgba(${whiteBase}, 0.15)`, + strong: `rgba(${whiteBase}, 0.30)`, + }, + + text: { + primary: `rgb(${whiteBase})`, + secondary: `rgba(${whiteBase}, 0.65)`, + disabled: disabledText, + link: brandText, + maxContrast: '#FFF', + }, + + primary: { + main: brandMain, + text: brandText, + border: brandMain, + name: 'primary', + }, + + secondary: { + main: `rgba(${secondaryBase}, 0.10)`, + shade: `rgba(${secondaryBase}, 0.14)`, + transparent: `rgba(${secondaryBase}, 0.08)`, + text: `rgba(${secondaryBase})`, + contrastText: `rgb(${secondaryBase})`, + border: `rgba(${secondaryBase}, 0.08)`, + }, + + background: { + canvas: '#000', + primary: '#0d0b14', + secondary: '#19171f', + }, + + action: { + hover: `rgba(${secondaryBase}, 0.10)`, + selected: `rgba(${secondaryBase}, 0.12)`, + selectedBorder: brandMain, + focus: `rgba(${secondaryBase}, 0.10)`, + hoverOpacity: 0.05, + disabledText: disabledText, + disabledBackground: `rgba(${whiteBase}, 0.04)`, + disabledOpacity: 0.38, + }, + + // gradients: { + // brandHorizontal: 'linear-gradient(270deg, #ff934d 0%, #FEAC34 100%)', + // brandVertical: 'linear-gradient(0.01deg, #ff934d 0.01%, #FEAC34 99.99%)', + // }, + + contrastThreshold: 3, + hoverFactor: 0.03, + tonalOffset: 0.15, + }, + shape: { + borderRadius: 5, + }, +}; + +export default gloomTheme; diff --git a/packages/grafana-data/src/themes/themeDefinitions/index.ts b/packages/grafana-data/src/themes/themeDefinitions/index.ts index f0277312fe4..151ae00593e 100644 --- a/packages/grafana-data/src/themes/themeDefinitions/index.ts +++ b/packages/grafana-data/src/themes/themeDefinitions/index.ts @@ -9,3 +9,4 @@ export { default as synthwave } from './synthwave'; export { default as tron } from './tron'; export { default as victorian } from './victorian'; export { default as zen } from './zen'; +export { default as gloom } from './gloom'; diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue index e49511ddddd..9286c13c003 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -43,7 +43,7 @@ DashboardV2Spec: { annotations: [...AnnotationQueryKind] - layout: GridLayoutKind + layout: GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind // Plugins only. The version of the dashboard installed together with the plugin. @@ -488,6 +488,11 @@ RowRepeatOptions: { value: string } +ResponsiveGridRepeatOptions: { + mode: RepeatMode + value: string +} + GridLayoutItemSpec: { x: int y: int @@ -524,6 +529,47 @@ GridLayoutKind: { spec: GridLayoutSpec } +RowsLayoutKind: { + kind: "RowsLayout" + spec: RowsLayoutSpec +} + +RowsLayoutSpec: { + rows: [...RowsLayoutRowKind] +} + +RowsLayoutRowKind: { + kind: "RowsLayoutRow" + spec: RowsLayoutRowSpec +} + +RowsLayoutRowSpec: { + title?: string + collapsed: bool + repeat?: RowRepeatOptions + layout: GridLayoutKind | ResponsiveGridLayoutKind +} + +ResponsiveGridLayoutKind: { + kind: "ResponsiveGridLayout" + spec: ResponsiveGridLayoutSpec +} + +ResponsiveGridLayoutSpec: { + row: string, + col: string, + items: [...ResponsiveGridLayoutItemKind] +} + +ResponsiveGridLayoutItemKind: { + kind: "ResponsiveGridLayoutItem" + spec: ResponsiveGridLayoutItemSpec +} + +ResponsiveGridLayoutItemSpec: { + element: ElementReference +} + PanelSpec: { id: number title: string diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts index 641cb9173cb..70262bbfdd5 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts @@ -30,7 +30,7 @@ export interface DashboardV2Spec { variables: VariableKind[]; elements: Record; annotations: AnnotationQueryKind[]; - layout: GridLayoutKind; + layout: GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind; // Plugins only. The version of the dashboard installed together with the plugin. // This is used to determine if the dashboard should be updated when the plugin is updated. revision?: number; @@ -710,6 +710,16 @@ export const defaultRowRepeatOptions = (): RowRepeatOptions => ({ value: "", }); +export interface ResponsiveGridRepeatOptions { + mode: "variable"; + value: string; +} + +export const defaultResponsiveGridRepeatOptions = (): ResponsiveGridRepeatOptions => ({ + mode: RepeatMode, + value: "", +}); + export interface GridLayoutItemSpec { x: number; y: number; @@ -752,6 +762,7 @@ export interface GridLayoutRowSpec { y: number; collapsed: boolean; title: string; + // Grid items in the row will have their Y value be relative to the rows Y value. This means a panel positioned at Y: 0 in a row with Y: 10 will be positioned at Y: 11 (row header has a heigh of 1) in the dashboard. elements: GridLayoutItemKind[]; repeat?: RowRepeatOptions; } @@ -781,6 +792,86 @@ export const defaultGridLayoutKind = (): GridLayoutKind => ({ spec: defaultGridLayoutSpec(), }); +export interface RowsLayoutKind { + kind: "RowsLayout"; + spec: RowsLayoutSpec; +} + +export const defaultRowsLayoutKind = (): RowsLayoutKind => ({ + kind: "RowsLayout", + spec: defaultRowsLayoutSpec(), +}); + +export interface RowsLayoutSpec { + rows: RowsLayoutRowKind[]; +} + +export const defaultRowsLayoutSpec = (): RowsLayoutSpec => ({ + rows: [], +}); + +export interface RowsLayoutRowKind { + kind: "RowsLayoutRow"; + spec: RowsLayoutRowSpec; +} + +export const defaultRowsLayoutRowKind = (): RowsLayoutRowKind => ({ + kind: "RowsLayoutRow", + spec: defaultRowsLayoutRowSpec(), +}); + +export interface RowsLayoutRowSpec { + title?: string; + collapsed: boolean; + repeat?: RowRepeatOptions; + layout: GridLayoutKind | ResponsiveGridLayoutKind; +} + +export const defaultRowsLayoutRowSpec = (): RowsLayoutRowSpec => ({ + collapsed: false, + layout: defaultGridLayoutKind(), +}); + +export interface ResponsiveGridLayoutKind { + kind: "ResponsiveGridLayout"; + spec: ResponsiveGridLayoutSpec; +} + +export const defaultResponsiveGridLayoutKind = (): ResponsiveGridLayoutKind => ({ + kind: "ResponsiveGridLayout", + spec: defaultResponsiveGridLayoutSpec(), +}); + +export interface ResponsiveGridLayoutSpec { + row: string; + col: string; + items: ResponsiveGridLayoutItemKind[]; +} + +export const defaultResponsiveGridLayoutSpec = (): ResponsiveGridLayoutSpec => ({ + row: "", + col: "", + items: [], +}); + +export interface ResponsiveGridLayoutItemKind { + kind: "ResponsiveGridLayoutItem"; + spec: ResponsiveGridLayoutItemSpec; +} + +export const defaultResponsiveGridLayoutItemKind = (): ResponsiveGridLayoutItemKind => ({ + kind: "ResponsiveGridLayoutItem", + spec: defaultResponsiveGridLayoutItemSpec(), +}); + +export interface ResponsiveGridLayoutItemSpec { + element: ElementReference; +} + +export const defaultResponsiveGridLayoutItemSpec = (): ResponsiveGridLayoutItemSpec => ({ + element: defaultElementReference(), +}); + export interface PanelSpec { id: number; title: string; diff --git a/pkg/apiserver/rest/dualwriter_mode2.go b/pkg/apiserver/rest/dualwriter_mode2.go index 105f450da4d..cc064ff4d5c 100644 --- a/pkg/apiserver/rest/dualwriter_mode2.go +++ b/pkg/apiserver/rest/dualwriter_mode2.go @@ -156,51 +156,6 @@ func (d *DualWriterMode2) List(ctx context.Context, options *metainternalversion return ll, err } d.recordLegacyDuration(false, mode2Str, d.resource, method, startLegacy) - - legacyList, err := meta.ExtractList(ll) - if err != nil { - log.Error(err, "unable to extract list from legacy storage") - return nil, err - } - - // Record the index of each LegacyStorage object so it can later be replaced by - // an equivalent Storage object if it exists. - legacyNames, err := parseList(legacyList) - if err != nil { - return nil, err - } - - startStorage := time.Now() - sl, err := d.Storage.List(ctx, options) - if err != nil { - log.Error(err, "unable to list objects from storage") - d.recordStorageDuration(true, mode2Str, d.resource, method, startStorage) - return sl, err - } - d.recordStorageDuration(false, mode2Str, d.resource, method, startStorage) - - storageList, err := meta.ExtractList(sl) - if err != nil { - log.Error(err, "unable to extract list from storage") - return nil, err - } - - for _, obj := range storageList { - name := getName(obj) - if i, ok := legacyNames[name]; ok { - areEqual := Compare(obj, legacyList[i]) - d.recordOutcome(mode2Str, name, areEqual, method) - if !areEqual { - log.WithValues("name", name).Info("object from legacy and storage are not equal") - } - } - } - - if err = meta.SetList(ll, legacyList); err != nil { - return nil, err - } - - // always return the list from legacy storage return ll, nil } diff --git a/pkg/apiserver/rest/dualwriter_mode3.go b/pkg/apiserver/rest/dualwriter_mode3.go index 1e7358c62e2..8b68c748769 100644 --- a/pkg/apiserver/rest/dualwriter_mode3.go +++ b/pkg/apiserver/rest/dualwriter_mode3.go @@ -162,37 +162,9 @@ func (d *DualWriterMode3) List(ctx context.Context, options *metainternalversion if err != nil { log.Error(err, "unable to list object in storage") } - - //nolint:errcheck - go d.listFromLegacyStorage(ctx, options, objFromStorage) - return objFromStorage, err } -func (d *DualWriterMode3) listFromLegacyStorage(ctx context.Context, options *metainternalversion.ListOptions, objFromStorage runtime.Object) error { - var method = "list" - log := d.Log.WithValues("resourceVersion", options.ResourceVersion, "method", method) - startLegacy := time.Now() - - ctx, cancel := context.WithTimeoutCause(context.WithoutCancel(ctx), time.Second*10, errors.New("legacy list timeout")) - defer cancel() - - objFromLegacy, err := d.Legacy.List(ctx, options) - d.recordLegacyDuration(err != nil, mode3Str, d.resource, method, startLegacy) - if err != nil { - log.Error(err, "unable to list object in legacy storage") - cancel() - } - - areEqual := Compare(objFromStorage, objFromLegacy) - d.recordOutcome(mode3Str, getName(objFromStorage), areEqual, method) - if !areEqual { - log.WithValues("name", getName(objFromStorage)).Info("object from legacy and storage are not equal") - } - - return err -} - func (d *DualWriterMode3) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { var method = "delete" log := d.Log.WithValues("name", name, "method", method) diff --git a/pkg/apiserver/rest/dualwriter_mode3_test.go b/pkg/apiserver/rest/dualwriter_mode3_test.go index ee1bcb888dd..d5ddbe04a88 100644 --- a/pkg/apiserver/rest/dualwriter_mode3_test.go +++ b/pkg/apiserver/rest/dualwriter_mode3_test.go @@ -250,58 +250,6 @@ func TestMode3_List(t *testing.T) { } } -func TestMode1_ListFromLegacyStorage(t *testing.T) { - ctxCanceled, cancel := context.WithCancel(context.TODO()) - cancel() - - type testCase struct { - ctx *context.Context - name string - setupLegacyFn func(m *mock.Mock) - } - tests := - []testCase{ - { - name: "list from legacy storage", - setupLegacyFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(anotherList, nil) - }, - }, - { - name: "list from legacy storage works even if parent context is canceled", - ctx: &ctxCanceled, - setupLegacyFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(anotherList, nil) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) - s := (Storage)(nil) - m := &mock.Mock{} - - ls := legacyStoreMock{m, l} - us := storageMock{m, s} - - if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m) - } - - ctx := context.TODO() - if tt.ctx != nil { - ctx = *tt.ctx - } - - dw := NewDualWriter(Mode3, ls, us, p, kind) - - err := dw.(*DualWriterMode3).listFromLegacyStorage(ctx, &metainternalversion.ListOptions{}, anotherList) - assert.NoError(t, err) - }) - } -} - func TestMode3_Delete(t *testing.T) { type testCase struct { setupLegacyFn func(m *mock.Mock, input string) diff --git a/pkg/services/preference/themes.go b/pkg/services/preference/themes.go index 4e88235d5d2..73366b64d6d 100644 --- a/pkg/services/preference/themes.go +++ b/pkg/services/preference/themes.go @@ -21,6 +21,7 @@ var themes = []ThemeDTO{ {ID: "tron", Type: "dark", IsExtra: true}, {ID: "victorian", Type: "dark", IsExtra: true}, {ID: "zen", Type: "light", IsExtra: true}, + {ID: "gloom", Type: "dark", IsExtra: true}, } func GetThemeByID(id string) *ThemeDTO { diff --git a/pkg/tests/apis/openapi_test.go b/pkg/tests/apis/openapi_test.go index 43ca0b0a273..58ca74d8f26 100644 --- a/pkg/tests/apis/openapi_test.go +++ b/pkg/tests/apis/openapi_test.go @@ -73,6 +73,9 @@ func TestIntegrationOpenAPIs(t *testing.T) { }, { Group: "iam.grafana.app", Version: "v0alpha1", + }, { + Group: "provisioning.grafana.app", + Version: "v0alpha1", }} for _, gv := range groups { VerifyOpenAPISnapshots(t, dir, gv, h) diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index 16d91551060..fa23e066c7a 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -92,6 +92,7 @@ export class SharedPreferences extends PureComponent { allowedExtraThemes.push('gildedgrove'); allowedExtraThemes.push('sapphiredusk'); allowedExtraThemes.push('tron'); + allowedExtraThemes.push('gloom'); } this.themeOptions = getBuiltInThemes(allowedExtraThemes).map((theme) => ({ diff --git a/public/app/core/utils/sheet.test.ts b/public/app/core/utils/sheet.test.ts deleted file mode 100644 index 139dc02dc1a..00000000000 --- a/public/app/core/utils/sheet.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { utils } from 'xlsx'; - -import { DataFrame } from '@grafana/data'; - -import { workSheetToFrame } from './sheet'; - -describe('sheets', () => { - it('should handle an empty sheet', () => { - const emptySheet = utils.aoa_to_sheet([]); - const frame = workSheetToFrame(emptySheet); - - expect(frame.name).toBeUndefined(); - expect(frame.fields).toHaveLength(0); - expect(frame.length).toBe(0); - }); - - it('will use first row as names', () => { - const sheet = utils.aoa_to_sheet([ - ['Number', 'String', 'Bool', 'Date', 'Object'], - [1, 'A', true, Date.UTC(2020, 1, 1), { hello: 'world' }], - [2, 'B', false, Date.UTC(2020, 1, 2), { hello: 'world' }], - ]); - const frame = workSheetToFrame(sheet); - - expect(toSnapshotFrame(frame)).toMatchInlineSnapshot(` - [ - { - "name": "Number", - "type": "number", - "values": [ - 1, - 2, - ], - }, - { - "name": "String", - "type": "string", - "values": [ - "A", - "B", - ], - }, - { - "name": "Bool", - "type": "boolean", - "values": [ - true, - false, - ], - }, - { - "name": "Date", - "type": "number", - "values": [ - 1580515200000, - 1580601600000, - ], - }, - { - "name": "Object", - "type": "string", - "values": [ - undefined, - undefined, - ], - }, - ] - `); - }); - - it('will use calculated data when cells are typed', () => { - const sheet = utils.aoa_to_sheet([ - [1, 'A', true, Date.UTC(2020, 1, 1), { hello: 'world' }], - [2, 'B', false, Date.UTC(2020, 1, 2), { hello: 'world' }], - [3, 'C', true, Date.UTC(2020, 1, 3), { hello: 'world' }], - ]); - const frame = workSheetToFrame(sheet); - - expect(toSnapshotFrame(frame)).toMatchInlineSnapshot(` - [ - { - "name": "A", - "type": "number", - "values": [ - 1, - 2, - 3, - ], - }, - { - "name": "B", - "type": "string", - "values": [ - "A", - "B", - "C", - ], - }, - { - "name": "C", - "type": "boolean", - "values": [ - true, - false, - true, - ], - }, - { - "name": "D", - "type": "number", - "values": [ - 1580515200000, - 1580601600000, - 1580688000000, - ], - }, - { - "name": "E", - "type": "string", - "values": [ - undefined, - undefined, - undefined, - ], - }, - ] - `); - }); - - it('is OK with nulls and undefineds, and misalignment', () => { - const sheet = utils.aoa_to_sheet([ - [null, 'A', true], - [2, 'B', null, Date.UTC(2020, 1, 2), { hello: 'world' }], - [3, 'C', true, undefined, { hello: 'world' }], - ]); - const frame = workSheetToFrame(sheet); - - expect(toSnapshotFrame(frame)).toMatchInlineSnapshot(` - [ - { - "name": "A", - "type": "number", - "values": [ - undefined, - 2, - 3, - ], - }, - { - "name": "B", - "type": "string", - "values": [ - "A", - "B", - "C", - ], - }, - { - "name": "C", - "type": "boolean", - "values": [ - true, - undefined, - true, - ], - }, - { - "name": "D", - "type": "number", - "values": [ - undefined, - 1580601600000, - undefined, - ], - }, - { - "name": "E", - "type": "string", - "values": [ - undefined, - undefined, - undefined, - ], - }, - ] - `); - }); -}); - -function toSnapshotFrame(frame: DataFrame) { - return frame.fields.map((f) => ({ name: f.name, values: f.values, type: f.type })); -} diff --git a/public/app/core/utils/sheet.ts b/public/app/core/utils/sheet.ts deleted file mode 100644 index a97d31c2b33..00000000000 --- a/public/app/core/utils/sheet.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { read, utils, WorkSheet, WorkBook, Range, ColInfo, CellObject, ExcelDataType } from 'xlsx'; - -import { DataFrame, FieldType } from '@grafana/data'; - -export function readSpreadsheet(file: ArrayBuffer): DataFrame[] { - return workBookToFrames(read(file, { type: 'buffer' })); -} - -export function workBookToFrames(wb: WorkBook): DataFrame[] { - return wb.SheetNames.map((name) => workSheetToFrame(wb.Sheets[name], name)); -} - -export function workSheetToFrame(sheet: WorkSheet, name?: string): DataFrame { - const columns = sheetAsColumns(sheet); - if (!columns?.length) { - return { - fields: [], - name: name, - length: 0, - }; - } - - return { - fields: columns.map((c, idx) => { - let type = FieldType.string; - let values: unknown[] = []; - switch (c.type ?? 's') { - case 'b': - type = FieldType.boolean; - values = c.data.map((v) => (v?.v == null ? v?.v : Boolean(v.v))); - break; - - case 'n': - type = FieldType.number; - values = c.data.map((v) => (v?.v == null ? v?.v : +v.v)); - break; - - case 'd': - type = FieldType.time; - values = c.data.map((v) => (v?.v == null ? v?.v : +v.v)); // ??? - break; - - default: - type = FieldType.string; - values = c.data.map((v) => (v?.v == null ? v?.v : utils.format_cell(v))); - break; - } - - return { - name: c.name, - config: {}, // TODO? we could apply decimal formatting from worksheet - type, - values, - }; - }), - name: name, - length: columns[0].data.length, - }; -} - -interface ColumnData { - index: number; - name: string; - info?: ColInfo; - data: CellObject[]; - type?: ExcelDataType; -} - -function sheetAsColumns(sheet: WorkSheet): ColumnData[] | null { - const r = sheet['!ref']; - if (!r) { - return null; - } - const columnInfo = sheet['!cols']; - const cols: ColumnData[] = []; - const range = safe_decode_range(r); - const types = new Set(); - let firstRowIsHeader = true; - - for (let c = range.s.c; c <= range.e.c; ++c) { - types.clear(); - const info = columnInfo?.[c] ?? {}; - if (info.hidden) { - continue; // skip the column - } - const field: ColumnData = { - index: c, - name: utils.encode_col(c), - data: [], - info, - }; - const pfix = utils.encode_col(c); - for (let r = range.s.r; r <= range.e.r; ++r) { - const cell = sheet[pfix + utils.encode_row(r)]; - if (cell) { - if (field.data.length) { - types.add(cell.t); - } else if (cell.t !== 's') { - firstRowIsHeader = false; - } - } - field.data.push(cell); - } - cols.push(field); - if (types.size === 1) { - field.type = Array.from(types)[0]; - } - } - - if (firstRowIsHeader) { - return cols.map((c) => { - const first = c.data[0]; - if (first?.v) { - c.name = utils.format_cell(first); - } - c.data = c.data.slice(1); - return c; - }); - } - return cols; -} - -/** - * Copied from Apache 2 licensed sheetjs: - * https://git.sheetjs.com/sheetjs/sheetjs/src/branch/master/xlsx.flow.js#L4338 - */ -function safe_decode_range(range: string): Range { - let o = { s: { c: 0, r: 0 }, e: { c: 0, r: 0 } }; - let idx = 0, - i = 0, - cc = 0; - let len = range.length; - for (idx = 0; i < len; ++i) { - if ((cc = range.charCodeAt(i) - 64) < 1 || cc > 26) { - break; - } - idx = 26 * idx + cc; - } - o.s.c = --idx; - - for (idx = 0; i < len; ++i) { - if ((cc = range.charCodeAt(i) - 48) < 0 || cc > 9) { - break; - } - idx = 10 * idx + cc; - } - o.s.r = --idx; - - if (i === len || cc !== 10) { - o.e.c = o.s.c; - o.e.r = o.s.r; - return o; - } - ++i; - - for (idx = 0; i !== len; ++i) { - if ((cc = range.charCodeAt(i) - 64) < 1 || cc > 26) { - break; - } - idx = 26 * idx + cc; - } - o.e.c = --idx; - - for (idx = 0; i !== len; ++i) { - if ((cc = range.charCodeAt(i) - 48) < 0 || cc > 9) { - break; - } - idx = 10 * idx + cc; - } - o.e.r = --idx; - return o; -} diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx index 383f4a59290..04061ea503d 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx @@ -35,6 +35,11 @@ export class ResponsiveGridLayoutManager public readonly descriptor = ResponsiveGridLayoutManager.descriptor; + public static defaultCSS = { + templateColumns: 'repeat(auto-fit, minmax(400px, auto))', + autoRows: 'minmax(300px, auto)', + }; + public constructor(state: ResponsiveGridLayoutManagerState) { super(state); @@ -118,8 +123,8 @@ export class ResponsiveGridLayoutManager return new ResponsiveGridLayoutManager({ layout: new SceneCSSGridLayout({ children: [], - templateColumns: 'repeat(auto-fit, minmax(400px, auto))', - autoRows: 'minmax(300px, auto)', + templateColumns: ResponsiveGridLayoutManager.defaultCSS.templateColumns, + autoRows: ResponsiveGridLayoutManager.defaultCSS.autoRows, }), }); } diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts index 0844b4c454e..27956a2bbd7 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts @@ -12,6 +12,7 @@ import { defaultDashboardV2Spec, defaultPanelSpec, defaultTimeSettingsSpec, + GridLayoutKind, PanelSpec, } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; import { AnnoKeyDashboardSnapshotOriginalUrl } from 'app/features/apiserver/types'; @@ -736,7 +737,8 @@ describe('DashboardSceneSerializer', () => { const saveAsModel = serializer.getSaveAsModel(emptyDashboard, baseOptions); expect(saveAsModel.elements).toEqual({}); - expect(saveAsModel.layout.spec.items).toEqual([]); + expect(saveAsModel.layout.kind).toBe('GridLayout'); + expect((saveAsModel.layout as GridLayoutKind).spec.items).toEqual([]); expect(saveAsModel.variables).toEqual([]); }); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts index 7be999edebc..f57944b68d7 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts @@ -22,6 +22,7 @@ import { DashboardV2Spec, DatasourceVariableKind, GridLayoutItemSpec, + GridLayoutSpec, GroupByVariableKind, IntervalVariableKind, QueryVariableKind, @@ -231,7 +232,7 @@ describe('transformSaveModelSchemaV2ToScene', () => { const panel = getPanelElement(dash, 'panel-1')!; expect(layout.state.grid.state.children.length).toBe(3); expect(layout.state.grid.state.children[0].state.key).toBe(`grid-item-${panel.spec.id}`); - const gridLayoutItemSpec = dash.layout.spec.items[0].spec as GridLayoutItemSpec; + const gridLayoutItemSpec = (dash.layout.spec as GridLayoutSpec).items[0].spec as GridLayoutItemSpec; expect(layout.state.grid.state.children[0].state.width).toBe(gridLayoutItemSpec.width); expect(layout.state.grid.state.children[0].state.height).toBe(gridLayoutItemSpec.height); expect(layout.state.grid.state.children[0].state.x).toBe(gridLayoutItemSpec.x); @@ -242,7 +243,7 @@ describe('transformSaveModelSchemaV2ToScene', () => { // Library Panel const libraryPanel = getLibraryPanelElement(dash, 'panel-2')!; expect(layout.state.grid.state.children[1].state.key).toBe(`grid-item-${libraryPanel.spec.id}`); - const libraryGridLayoutItemSpec = dash.layout.spec.items[1].spec as GridLayoutItemSpec; + const libraryGridLayoutItemSpec = (dash.layout.spec as GridLayoutSpec).items[1].spec as GridLayoutItemSpec; expect(layout.state.grid.state.children[1].state.width).toBe(libraryGridLayoutItemSpec.width); expect(layout.state.grid.state.children[1].state.height).toBe(libraryGridLayoutItemSpec.height); expect(layout.state.grid.state.children[1].state.x).toBe(libraryGridLayoutItemSpec.x); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 61ed7495a8c..6ff95bef909 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -10,6 +10,7 @@ import { GroupByVariable, IntervalVariable, QueryVariable, + SceneCSSGridLayout, SceneDataLayerControls, SceneDataProvider, SceneDataQuery, @@ -46,6 +47,8 @@ import { defaultQueryVariableKind, defaultTextVariableKind, GridLayoutItemSpec, + GridLayoutKind, + Element, GroupByVariableKind, IntervalVariableKind, LibraryPanelKind, @@ -53,6 +56,7 @@ import { PanelQueryKind, QueryVariableKind, TextVariableKind, + ResponsiveGridLayoutItemKind, } from '@grafana/schema/src/schema/dashboard/v2alpha0'; import { contextSrv } from 'app/core/core'; import { @@ -86,9 +90,14 @@ import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; import { RowActions } from '../scene/layout-default/row-actions/RowActions'; +import { ResponsiveGridItem } from '../scene/layout-responsive-grid/ResponsiveGridItem'; +import { ResponsiveGridLayoutManager } from '../scene/layout-responsive-grid/ResponsiveGridLayoutManager'; +import { RowItem } from '../scene/layout-rows/RowItem'; +import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager'; import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; +import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager'; import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState'; -import { getIntervalsFromQueryString, getVizPanelKeyForPanelId } from '../utils/utils'; +import { getGridItemKeyForPanelId, getIntervalsFromQueryString, getVizPanelKeyForPanelId } from '../utils/utils'; import { GRID_ROW_HEIGHT } from './const'; import { SnapshotVariable } from './custom-variables/SnapshotVariable'; @@ -166,6 +175,8 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo { + let layout: DashboardLayoutManager | undefined = undefined; + + if (row.spec.layout.kind === 'GridLayout') { + layout = new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: createSceneGridLayoutForItems(row.spec.layout, dashboard.elements), + }), + }); + } + + if (row.spec.layout.kind === 'ResponsiveGridLayout') { + layout = new ResponsiveGridLayoutManager({ + layout: new SceneCSSGridLayout({ + templateColumns: row.spec.layout.spec.col, + autoRows: row.spec.layout.spec.row, + children: createResponsiveGridItems(row.spec.layout.spec.items, dashboard.elements), + }), + }); + } + + if (!layout) { + throw new Error(`Unsupported layout kind: ${row.spec.layout.kind} in row`); + } + return new RowItem({ + title: row.spec.title, + isCollapsed: row.spec.collapsed, + layout: layout, + }); + }), + }); + } else if (dashboard.layout.kind === 'ResponsiveGridLayout') { + return new ResponsiveGridLayoutManager({ + layout: new SceneCSSGridLayout({ + templateColumns: dashboard.layout.spec.col, + autoRows: dashboard.layout.spec.row, + children: createResponsiveGridItems(dashboard.layout.spec.items, dashboard.elements), + }), + }); + } + + // @ts-ignore - this complains because we should never reach this point. If the model does not match the schema we will though. + throw new Error(`Unsupported layout type: ${dashboard.layout.kind}`); +} + +function createResponsiveGridItems( + items: ResponsiveGridLayoutItemKind[], + elements: Record +): ResponsiveGridItem[] { + return items.map((item) => { + const panel = elements[item.spec.element.name]; + if (!panel) { + throw new Error(`Panel with uid ${item.spec.element.name} not found in the dashboard elements`); + } + if (panel.kind !== 'Panel') { + throw new Error(`Unsupported element kind: ${panel.kind}`); + } + return new ResponsiveGridItem({ + key: getGridItemKeyForPanelId(panel.spec.id), + body: buildVizPanel(panel), + }); + }); +} + +function createSceneGridLayoutForItems(layout: GridLayoutKind, elements: Record): SceneGridItemLike[] { + const gridElements = layout.spec.items; return gridElements.map((element) => { if (element.kind === 'GridLayoutItem') { - const panel = dashboard.elements[element.spec.element.name]; + const panel = elements[element.spec.element.name]; if (!panel) { throw new Error(`Panel with uid ${element.spec.element.name} not found in the dashboard elements`); @@ -279,7 +359,7 @@ function createSceneGridLayoutForItems(dashboard: DashboardV2Spec): SceneGridIte } } else if (element.kind === 'GridLayoutRow') { const children = element.spec.elements.map((gridElement) => { - const panel = dashboard.elements[gridElement.spec.element.name]; + const panel = elements[gridElement.spec.element.name]; if (panel.kind === 'Panel') { return buildGridItem(gridElement.spec, panel, element.spec.y + GRID_ROW_HEIGHT + gridElement.spec.y); } else { diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index 928ce92733f..7a318e38b64 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -43,6 +43,10 @@ import { DashboardCursorSync, FieldConfig, FieldColor, + GridLayoutKind, + RowsLayoutKind, + ResponsiveGridLayoutKind, + ResponsiveGridLayoutItemKind, } from '../../../../../packages/grafana-schema/src/schema/dashboard/v2alpha0'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; @@ -50,6 +54,10 @@ import { PanelTimeRange } from '../scene/PanelTimeRange'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; +import { ResponsiveGridItem } from '../scene/layout-responsive-grid/ResponsiveGridItem'; +import { ResponsiveGridLayoutManager } from '../scene/layout-responsive-grid/ResponsiveGridLayoutManager'; +import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager'; +import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager'; import { isClonedKey } from '../utils/clone'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { @@ -73,22 +81,22 @@ type DeepPartial = T extends object : T; export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnapshot = false): DashboardV2Spec { - const oldDash = scene.state; - const timeRange = oldDash.$timeRange!.state; + const sceneDash = scene.state; + const timeRange = sceneDash.$timeRange!.state; - const controlsState = oldDash.controls?.state; + const controlsState = sceneDash.controls?.state; const refreshPicker = controlsState?.refreshPicker; const dashboardSchemaV2: DeepPartial = { //dashboard settings - title: oldDash.title, - description: oldDash.description ?? '', - cursorSync: getCursorSync(oldDash), - liveNow: getLiveNow(oldDash), - preload: oldDash.preload, - editable: oldDash.editable, - links: oldDash.links, - tags: oldDash.tags, + title: sceneDash.title, + description: sceneDash.description ?? '', + cursorSync: getCursorSync(sceneDash), + liveNow: getLiveNow(sceneDash), + preload: sceneDash.preload, + editable: sceneDash.editable, + links: sceneDash.links, + tags: sceneDash.tags, // EOF dashboard settings // time settings @@ -107,24 +115,19 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps // EOF time settings // variables - variables: getVariables(oldDash), + variables: getVariables(sceneDash), // EOF variables // elements - elements: getElements(oldDash), + elements: getElements(sceneDash), // EOF elements // annotations - annotations: getAnnotations(oldDash), + annotations: getAnnotations(sceneDash), // EOF annotations // layout - layout: { - kind: 'GridLayout', - spec: { - items: getGridLayoutItems(oldDash, isSnapshot), - }, - }, + layout: getLayout(sceneDash.body, isSnapshot), // EOF layout }; @@ -141,6 +144,75 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps } } +function getLayout( + layoutManager: DashboardLayoutManager, + isSnapshot?: boolean +): GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind { + if (layoutManager instanceof DefaultGridLayoutManager) { + return getGridLayout(layoutManager, isSnapshot); + } else if (layoutManager instanceof RowsLayoutManager) { + return { + kind: 'RowsLayout', + spec: { + rows: layoutManager.state.rows.map((row) => { + if (row.state.layout instanceof RowsLayoutManager) { + throw new Error('Nesting row layouts is not supported'); + } + let layout: GridLayoutKind | ResponsiveGridLayoutKind | undefined = undefined; + if (row.state.layout instanceof DefaultGridLayoutManager) { + layout = getGridLayout(row.state.layout, isSnapshot); + } else if (row.state.layout instanceof ResponsiveGridLayoutManager) { + layout = { + kind: 'ResponsiveGridLayout', + spec: { + items: getResponsiveGridLayoutItems(row.state.layout), + col: + row.state.layout.state.layout.state.templateColumns?.toString() ?? + ResponsiveGridLayoutManager.defaultCSS.templateColumns, + row: + row.state.layout.state.layout.state.autoRows?.toString() ?? + ResponsiveGridLayoutManager.defaultCSS.autoRows, + }, + }; + } + if (!layout) { + throw new Error('Unsupported layout type'); + } + return { + kind: 'RowsLayoutRow', + spec: { + title: row.state.title, + collapsed: row.state.isCollapsed ?? false, + layout: layout, + }, + }; + }), + }, + }; + } else if (layoutManager instanceof ResponsiveGridLayoutManager) { + return { + kind: 'ResponsiveGridLayout', + spec: { + items: getResponsiveGridLayoutItems(layoutManager), + col: + layoutManager.state.layout.state.templateColumns?.toString() ?? + ResponsiveGridLayoutManager.defaultCSS.templateColumns, + row: layoutManager.state.layout.state.autoRows?.toString() ?? ResponsiveGridLayoutManager.defaultCSS.autoRows, + }, + }; + } + throw new Error('Unsupported layout type'); +} + +function getGridLayout(layoutManager: DefaultGridLayoutManager, isSnapshot?: boolean): GridLayoutKind { + return { + kind: 'GridLayout', + spec: { + items: getGridLayoutItems(layoutManager, isSnapshot), + }, + }; +} + function getCursorSync(state: DashboardSceneState) { const cursorSync = state.$behaviors?.find((b): b is behaviors.CursorSync => b instanceof behaviors.CursorSync)?.state .sync; @@ -160,33 +232,49 @@ function getLiveNow(state: DashboardSceneState) { } function getGridLayoutItems( - state: DashboardSceneState, + body: DefaultGridLayoutManager, isSnapshot?: boolean ): Array { - const body = state.body; let elements: Array = []; - if (body instanceof DefaultGridLayoutManager) { - for (const child of body.state.grid.state.children) { - if (child instanceof DashboardGridItem) { - // TODO: handle panel repeater scenario - if (child.state.variableName) { - elements = elements.concat(repeaterToLayoutItems(child, isSnapshot)); - } else { - elements.push(gridItemToGridLayoutItemKind(child, isSnapshot)); - } - } else if (child instanceof SceneGridRow) { - if (isClonedKey(child.state.key!) && !isSnapshot) { - // Skip repeat rows - continue; - } - elements.push(gridRowToLayoutRowKind(child, isSnapshot)); + for (const child of body.state.grid.state.children) { + if (child instanceof DashboardGridItem) { + // TODO: handle panel repeater scenario + if (child.state.variableName) { + elements = elements.concat(repeaterToLayoutItems(child, isSnapshot)); + } else { + elements.push(gridItemToGridLayoutItemKind(child, isSnapshot)); } + } else if (child instanceof SceneGridRow) { + if (isClonedKey(child.state.key!) && !isSnapshot) { + // Skip repeat rows + continue; + } + elements.push(gridRowToLayoutRowKind(child, isSnapshot)); } } return elements; } +function getResponsiveGridLayoutItems(body: ResponsiveGridLayoutManager): ResponsiveGridLayoutItemKind[] { + const items: ResponsiveGridLayoutItemKind[] = []; + + for (const child of body.state.layout.state.children) { + if (child instanceof ResponsiveGridItem) { + items.push({ + kind: 'ResponsiveGridLayoutItem', + spec: { + element: { + kind: 'ElementReference', + name: child.state?.body?.state.key ?? 'DefaultName', + }, + }, + }); + } + } + return items; +} + export function gridItemToGridLayoutItemKind( gridItem: DashboardGridItem, isSnapshot = false, @@ -729,15 +817,44 @@ function validateDashboardSchemaV2(dash: unknown): dash is DashboardV2Spec { if (!('layout' in dash) || typeof dash.layout !== 'object' || dash.layout === null) { throw new Error('Layout is not an object or is null'); } - if (!('kind' in dash.layout) || dash.layout.kind !== 'GridLayout') { - throw new Error('Layout kind is not GridLayout'); + + if (!('kind' in dash.layout) || dash.layout.kind === 'GridLayout') { + validateGridLayout(dash.layout); } - if (!('spec' in dash.layout) || typeof dash.layout.spec !== 'object' || dash.layout.spec === null) { - throw new Error('Layout spec is not an object or is null'); - } - if (!('items' in dash.layout.spec) || !Array.isArray(dash.layout.spec.items)) { - throw new Error('Layout spec items is not an array'); + + if (!('kind' in dash.layout) || dash.layout.kind === 'RowsLayout') { + validateRowsLayout(dash.layout); } return true; } + +function validateGridLayout(layout: unknown) { + if (typeof layout !== 'object' || layout === null) { + throw new Error('Layout is not an object or is null'); + } + if (!('kind' in layout) || layout.kind !== 'GridLayout') { + throw new Error('Layout kind is not GridLayout'); + } + if (!('spec' in layout) || typeof layout.spec !== 'object' || layout.spec === null) { + throw new Error('Layout spec is not an object or is null'); + } + if (!('items' in layout.spec) || !Array.isArray(layout.spec.items)) { + throw new Error('Layout spec items is not an array'); + } +} + +function validateRowsLayout(layout: unknown) { + if (typeof layout !== 'object' || layout === null) { + throw new Error('Layout is not an object or is null'); + } + if (!('kind' in layout) || layout.kind !== 'RowsLayout') { + throw new Error('Layout kind is not RowsLayout'); + } + if (!('spec' in layout) || typeof layout.spec !== 'object' || layout.spec === null) { + throw new Error('Layout spec is not an object or is null'); + } + if (!('rows' in layout.spec) || !Array.isArray(layout.spec.rows)) { + throw new Error('Layout spec items is not an array'); + } +} diff --git a/public/app/features/dashboard/api/ResponseTransformers.test.ts b/public/app/features/dashboard/api/ResponseTransformers.test.ts index 1dfbd14eb04..7cf86532d4a 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.test.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.test.ts @@ -3,6 +3,7 @@ import { DashboardV2Spec, GridLayoutItemKind, GridLayoutItemSpec, + GridLayoutKind, GridLayoutRowSpec, PanelKind, VariableKind, @@ -469,8 +470,10 @@ describe('ResponseTransformers', () => { expect(spec.annotations).toEqual([]); // Panel - expect(spec.layout.spec.items).toHaveLength(4); - expect(spec.layout.spec.items[0].spec).toEqual({ + expect(spec.layout.kind).toBe('GridLayout'); + const layout = spec.layout as GridLayoutKind; + expect(layout.spec.items).toHaveLength(4); + expect(layout.spec.items[0].spec).toEqual({ element: { kind: 'ElementReference', name: '1', @@ -533,7 +536,7 @@ describe('ResponseTransformers', () => { }, }); // Library Panel - expect(spec.layout.spec.items[1].spec).toEqual({ + expect(layout.spec.items[1].spec).toEqual({ element: { kind: 'ElementReference', name: '2', @@ -555,7 +558,7 @@ describe('ResponseTransformers', () => { }, }); - const rowSpec = spec.layout.spec.items[2].spec as GridLayoutRowSpec; + const rowSpec = layout.spec.items[2].spec as GridLayoutRowSpec; expect(rowSpec.collapsed).toBe(false); expect(rowSpec.title).toBe('Row test title'); @@ -574,7 +577,7 @@ describe('ResponseTransformers', () => { height: 8, }); - const collapsedRowSpec = spec.layout.spec.items[3].spec as GridLayoutRowSpec; + const collapsedRowSpec = layout.spec.items[3].spec as GridLayoutRowSpec; expect(collapsedRowSpec.collapsed).toBe(true); expect(collapsedRowSpec.title).toBe('Collapsed row title'); expect(collapsedRowSpec.repeat).toBeUndefined(); @@ -748,9 +751,11 @@ describe('ResponseTransformers', () => { validateAnnotation(dashboard.annotations!.list![3], dashboardV2.spec.annotations[3]); // panel const panelKey = 'panel-1'; + expect(dashboardV2.spec.elements[panelKey].kind).toBe('Panel'); const panelV2 = dashboardV2.spec.elements[panelKey] as PanelKind; expect(panelV2.kind).toBe('Panel'); - validatePanel(dashboard.panels![0], panelV2, dashboardV2.spec.layout, panelKey); + expect(dashboardV2.spec.layout.kind).toBe('GridLayout'); + validatePanel(dashboard.panels![0], panelV2, dashboardV2.spec.layout as GridLayoutKind, panelKey); // library panel expect(dashboard.panels![1].libraryPanel).toEqual({ uid: 'uid-for-library-panel', @@ -845,7 +850,7 @@ describe('ResponseTransformers', () => { expect(v1.filter).toEqual(v2Spec.filter); } - function validatePanel(v1: Panel, v2: PanelKind, layoutV2: DashboardV2Spec['layout'], panelKey: string) { + function validatePanel(v1: Panel, v2: PanelKind, layoutV2: GridLayoutKind, panelKey: string) { const { spec: v2Spec } = v2; expect(v1.id).toBe(v2Spec.id); diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index 1b72d6b3c08..38f882abbc1 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -860,6 +860,10 @@ function getPanelsV1( let maxPanelId = 0; + if (layout.kind !== 'GridLayout') { + throw new Error('Cannot convert non-GridLayout layout to v1'); + } + for (const item of layout.spec.items) { if (item.kind === 'GridLayoutItem') { const panel = panels[item.spec.element.name]; diff --git a/public/app/features/dataframe-import/constants.ts b/public/app/features/dataframe-import/constants.ts index e32027a5e46..9753ce71f08 100644 --- a/public/app/features/dataframe-import/constants.ts +++ b/public/app/features/dataframe-import/constants.ts @@ -2,10 +2,6 @@ import { Accept } from 'react-dropzone'; export const acceptedFiles: Accept = { 'text/plain': ['.csv', '.txt'], - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], - 'application/vnd.ms-excel': ['.xls'], - 'application/vnd.apple.numbers': ['.numbers'], - 'application/vnd.oasis.opendocument.spreadsheet': ['.ods'], 'application/json': ['.json'], }; diff --git a/public/app/features/dataframe-import/utils.test.ts b/public/app/features/dataframe-import/utils.test.ts deleted file mode 100644 index 20631933136..00000000000 --- a/public/app/features/dataframe-import/utils.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { formatFileTypes } from './utils'; - -describe('Dataframe import / Utils', () => { - describe('formatFileTypes', () => { - it('should nicely format file extensions', () => { - expect( - formatFileTypes({ - 'text/plain': ['.csv', '.txt'], - 'application/json': ['.json'], - }) - ).toBe('.csv, .txt or .json'); - }); - - it('should remove duplicates', () => { - expect( - formatFileTypes({ - 'text/plain': ['.csv', '.txt'], - 'application/json': ['.json', '.txt'], - }) - ).toBe('.csv, .txt or .json'); - }); - - it('should nicely format a single file type extension', () => { - expect( - formatFileTypes({ - 'text/plain': ['.txt'], - }) - ).toBe('.txt'); - }); - - it('should nicely format two file type extension', () => { - expect( - formatFileTypes({ - 'text/plain': ['.txt', '.csv'], - }) - ).toBe('.txt or .csv'); - }); - }); -}); diff --git a/public/app/features/dataframe-import/utils.ts b/public/app/features/dataframe-import/utils.ts index 83bb58383a5..c190cd1d538 100644 --- a/public/app/features/dataframe-import/utils.ts +++ b/public/app/features/dataframe-import/utils.ts @@ -1,55 +1,31 @@ -import { Accept } from 'react-dropzone'; import { Observable } from 'rxjs'; -import { toDataFrame } from '@grafana/data'; +import { readCSV, toDataFrame } from '@grafana/data'; import { FileImportResult } from './types'; -function getFileExtensions(acceptedFiles: Accept) { - const fileExtentions = new Set(); - Object.keys(acceptedFiles).forEach((v) => { - acceptedFiles[v].forEach((extension) => { - fileExtentions.add(extension); - }); - }); - return fileExtentions; -} - -export function formatFileTypes(acceptedFiles: Accept) { - const fileExtentions = Array.from(getFileExtensions(acceptedFiles)); - if (fileExtentions.length === 1) { - return fileExtentions[0]; - } - return `${fileExtentions.slice(0, -1).join(', ')} or ${fileExtentions.slice(-1)}`; -} - export function filesToDataframes(files: File[]): Observable { return new Observable((subscriber) => { let completedFiles = 0; - import('app/core/utils/sheet') - .then((sheet) => { - files.forEach((file) => { - const reader = new FileReader(); - reader.readAsArrayBuffer(file); - reader.onload = () => { - const result = reader.result; - if (result && result instanceof ArrayBuffer) { - if (file.type === 'application/json') { - const decoder = new TextDecoder('utf-8'); - const json = JSON.parse(decoder.decode(result)); - subscriber.next({ dataFrames: [toDataFrame(json)], file: file }); - } else { - subscriber.next({ dataFrames: sheet.readSpreadsheet(result), file: file }); - } - if (++completedFiles >= files.length) { - subscriber.complete(); - } - } - }; - }); - }) - .catch(() => { - throw 'Failed to load sheets module'; - }); + files.forEach((file) => { + const reader = new FileReader(); + reader.readAsArrayBuffer(file); + reader.onload = () => { + const result = reader.result; + if (result && result instanceof ArrayBuffer) { + const decoder = new TextDecoder('utf-8'); + const fileString = decoder.decode(result); + if (file.type === 'application/json') { + const json = JSON.parse(fileString); + subscriber.next({ dataFrames: [toDataFrame(json)], file: file }); + } else { + subscriber.next({ dataFrames: readCSV(fileString), file: file }); + } + if (++completedFiles >= files.length) { + subscriber.complete(); + } + } + }; + }); }); } diff --git a/yarn.lock b/yarn.lock index 1c4fee5ed1d..2ebb6dc6940 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18096,7 +18096,6 @@ __metadata: webpack-merge: "npm:6.0.1" webpackbar: "npm:^7.0.0" whatwg-fetch: "npm:3.6.20" - xlsx: "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz" yaml: "npm:^2.0.0" yargs: "npm:^17.5.1" dependenciesMeta: @@ -31772,15 +31771,6 @@ __metadata: languageName: node linkType: hard -"xlsx@https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz": - version: 0.20.2 - resolution: "xlsx@https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz" - bin: - xlsx: ./bin/xlsx.njs - checksum: 10/2d8e0644888f90fa9145ea74ed90b844154ce89c4f0e4f92fcce3f224fa71654da99aa48d99d65ba86eb0632a4858ba2dea7eef8b54fd8bd23954a09d1884aa1 - languageName: node - linkType: hard - "xml-but-prettier@npm:^1.0.1": version: 1.0.1 resolution: "xml-but-prettier@npm:1.0.1"