Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync
This commit is contained in:
24 files changed
+530
-658
No files matched your search
+1
-2
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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 }));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+7
-2
@@ -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([]);
|
||||
});
|
||||
|
||||
|
||||
+3
-2
@@ -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);
|
||||
|
||||
+91
-11
@@ -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 {
|
||||
|
||||
+161
-44
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user