Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync

This commit is contained in:
Mariell Hoversholm committed 2025-02-07 15:39:00 +01:00
commit bd261794b2
24 files changed
+530 -658

No files matched your search

+1 -2
View File
@@ -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",
@@ -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;
@@ -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';
@@ -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
@@ -30,7 +30,7 @@ export interface DashboardV2Spec {
variables: VariableKind[];
elements: Record<string, Element>;
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;
-45
View File
@@ -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
}
-28
View File
@@ -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)
@@ -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)
+1
View File
@@ -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 {
+3
View File
@@ -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)
@@ -92,6 +92,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
allowedExtraThemes.push('gildedgrove');
allowedExtraThemes.push('sapphiredusk');
allowedExtraThemes.push('tron');
allowedExtraThemes.push('gloom');
}
this.themeOptions = getBuiltInThemes(allowedExtraThemes).map((theme) => ({
-192
View File
@@ -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 }));
}
-172
View File
@@ -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<ExcelDataType>();
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;
}
@@ -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,
}),
});
}
@@ -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([]);
});
@@ -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);
@@ -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<D
meta.canSave = false;
}
const layoutManager: DashboardLayoutManager = createLayoutManager(dashboard);
const dashboardScene = new DashboardScene({
description: dashboard.description,
editable: dashboard.editable,
@@ -178,12 +189,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
title: dashboard.title,
uid: metadata.name,
version: parseInt(metadata.resourceVersion, 10),
body: new DefaultGridLayoutManager({
grid: new SceneGridLayout({
isLazy: !(dashboard.preload || contextSrv.user.authenticatedBy === 'render'),
children: createSceneGridLayoutForItems(dashboard),
}),
}),
body: layoutManager,
$timeRange: new SceneTimeRange({
from: dashboard.timeSettings.from,
to: dashboard.timeSettings.to,
@@ -249,12 +255,86 @@ function buildGridItem(gridItem: GridLayoutItemSpec, panel: PanelKind, yOverride
});
}
function createSceneGridLayoutForItems(dashboard: DashboardV2Spec): SceneGridItemLike[] {
const gridElements = dashboard.layout.spec.items;
function createLayoutManager(dashboard: DashboardV2Spec): DashboardLayoutManager {
if (dashboard.layout.kind === 'GridLayout') {
return new DefaultGridLayoutManager({
grid: new SceneGridLayout({
isLazy: !(dashboard.preload || contextSrv.user.authenticatedBy === 'render'),
children: createSceneGridLayoutForItems(dashboard.layout, dashboard.elements),
}),
});
} else if (dashboard.layout.kind === 'RowsLayout') {
return new RowsLayoutManager({
rows: dashboard.layout.spec.rows.map((row) => {
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<string, Element>
): 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<string, Element>): 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 {
@@ -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> = 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<DashboardV2Spec> = {
//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<GridLayoutItemKind | GridLayoutRowKind> {
const body = state.body;
let elements: Array<GridLayoutItemKind | GridLayoutRowKind> = [];
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');
}
}
@@ -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);
@@ -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];
@@ -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'],
};
@@ -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');
});
});
});
+21 -45
View File
@@ -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<string>();
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<FileImportResult> {
return new Observable<FileImportResult>((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();
}
}
};
});
});
}
-10
View File
@@ -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"