Datagrid: Switch to panel context update (#67221)

* WIP

* Switch Datagrid to use PanelContext onUpdateData

* PR modifications

* refactor

* block panel if not enabled
pull/67283/head
Victor Marin 2 years ago committed by GitHub
parent 47aa8c7654
commit 9599e8003b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      devenv/dev-dashboards/panel-datagrid/datagrid_metric_values.json
  2. 8
      e2e/datagrid-suite/datagrid-data-change.spec.ts
  3. 13
      e2e/datagrid-suite/datagrid-editing-features.spec.ts
  4. 2
      public/app/plugins/datasource/grafana/utils.ts
  5. 38
      public/app/plugins/panel/datagrid/DataGridPanel.test.tsx
  6. 95
      public/app/plugins/panel/datagrid/DataGridPanel.tsx
  7. 2
      public/app/plugins/panel/datagrid/featureFlagUtils.tsx
  8. 4
      public/app/plugins/panel/datagrid/state.ts
  9. 2
      public/app/plugins/panel/datagrid/utils.test.ts
  10. 92
      public/app/plugins/panel/datagrid/utils.ts

@ -75,5 +75,6 @@
"timezone": "",
"title": "Datagrid example",
"version": 0,
"uid": "c01bf42b-b783-4447-a304-8554cee1843b",
"weekStart": ""
}

@ -1,6 +1,6 @@
import { e2e } from '@grafana/e2e';
const DASHBOARD_ID = 'a70ecb44-6c31-412d-ae74-d6306303ce37';
const DASHBOARD_ID = 'c01bf42b-b783-4447-a304-8554cee1843b';
const DATAGRID_SELECT_SERIES = 'Datagrid Select series';
e2e.scenario({
@ -27,8 +27,10 @@ e2e.scenario({
// Edit datagrid which triggers a snapshot query
cy.get('.dvn-scroller').click(200, 100);
cy.get('[data-testid="glide-cell-2-1"]').should('have.attr', 'aria-selected', 'true');
cy.get('body').type('123455{enter}', { delay: 1000 });
cy.get('body').type('12{enter}', { delay: 500 });
cy.get('[data-testid="query-editor-row"]').contains('Spreadsheet or snapshot');
cy.get('[aria-label="Confirm Modal Danger Button"]').click();
cy.get('[data-testid="query-editor-row"]').contains('Snapshot');
},
});

@ -1,6 +1,6 @@
import { e2e } from '@grafana/e2e';
const DASHBOARD_ID = 'a70ecb44-6c31-412d-ae74-d6306303ce37';
const DASHBOARD_ID = 'c01bf42b-b783-4447-a304-8554cee1843b';
const DATAGRID_CANVAS = 'data-grid-canvas';
e2e.scenario({
@ -15,7 +15,9 @@ e2e.scenario({
// Edit datagrid which triggers a snapshot query
cy.get('.dvn-scroller').click(200, 100);
cy.get('[data-testid="glide-cell-2-1"]').should('have.attr', 'aria-selected', 'true');
cy.get('body').type('1{enter}');
cy.get('body').type('123{enter}', { delay: 500 });
cy.get('[aria-label="Confirm Modal Danger Button"]').click();
// Delete a cell
cy.get('.dvn-scroller').click(200, 200);
@ -55,6 +57,7 @@ e2e.scenario({
cy.get('.dvn-scroller').click(20, 190, { waitForAnimations: true });
cy.get('.dvn-scroller').click(20, 90, { shiftKey: true, waitForAnimations: true }); // with shift to select all rows between clicks
cy.get('body').type('{del}');
cy.get('[aria-label="Confirm Modal Danger Button"]').click();
cy.get('[data-testid="glide-cell-1-4"]').should('have.text', '');
cy.get('[data-testid="glide-cell-1-3"]').should('have.text', '');
cy.get('[data-testid="glide-cell-1-2"]').should('have.text', '');
@ -68,6 +71,9 @@ e2e.scenario({
cy.get('.dvn-scroller').click(20, 190, { waitForAnimations: true });
cy.get('.dvn-scroller').click(20, 90, { commandKey: true, waitForAnimations: true }); // with cmd to select only clicked rows
cy.get('body').type('{del}');
cy.get('[aria-label="Confirm Modal Danger Button"]').click();
cy.get('[data-testid="glide-cell-1-1"]').should('have.text', '');
cy.get('[data-testid="glide-cell-2-1"]').should('have.text', 0);
cy.get('[data-testid="glide-cell-2-4"]').should('have.text', 0);
@ -83,6 +89,7 @@ e2e.scenario({
// Delete column through header dropdown menu
cy.get('.dvn-scroller').click(250, 15); // click header dropdown
cy.get('body').click(450, 420); // click delete column
cy.get('[aria-label="Confirm Modal Danger Button"]').click();
cy.get(`[data-testid="${DATAGRID_CANVAS}"] th`).should('have.length', 1);
// Delete row through context menu
@ -101,6 +108,7 @@ e2e.scenario({
cy.get('.dvn-scroller').click(20, 90, { commandKey: true, waitForAnimations: true }); // with shift to select all rows between clicks
cy.get('.dvn-scroller').rightclick(40, 90);
cy.get('[aria-label="Context menu"]').click(10, 10);
cy.get('[aria-label="Confirm Modal Danger Button"]').click();
cy.get(`[data-testid="${DATAGRID_CANVAS}"] tbody tr`).should('have.length', 5); // there are 5 data rows + 1 for the add new row btns
// Delete column through context menu
@ -113,6 +121,7 @@ e2e.scenario({
// Add a new column
cy.get('body').click(350, 200).type('New Column{enter}');
cy.get('[aria-label="Confirm Modal Danger Button"]').click();
cy.get('body')
.click(350, 230)
.type('Value 1{enter}')

@ -22,7 +22,7 @@ export function onUpdatePanelSnapshotData(panel: PanelModel, frames: DataFrame[]
appEvents.publish(
new ShowConfirmModalEvent({
title: 'Change to panel embedded data',
text: 'If you want to change the data shown in this panel Grafana will need to remove the panels current query and replace it with a snapshot of the current data. This enabled you to edit the data',
text: 'If you want to change the data shown in this panel Grafana will need to remove the panels current query and replace it with a snapshot of the current data. This enables you to edit the data.',
yesText: 'Continue',
icon: 'pen',
onConfirm: () => {

@ -1,14 +1,14 @@
import { render, screen, fireEvent, act, waitFor } from '@testing-library/react';
import * as React from 'react';
import { ArrayVector, DataFrame, dateTime, EventBus, FieldType, LoadingState, MutableDataFrame } from '@grafana/data';
import { ArrayVector, DataFrame, dateTime, EventBus, Field, FieldType, LoadingState } from '@grafana/data';
import { DataGridPanel, DataGridProps } from './DataGridPanel';
import * as utils from './utils';
jest.mock('./featureFlagUtils', () => {
return {
isDatagridEditEnabled: jest.fn().mockReturnValue(true),
isDatagridEnabled: jest.fn().mockReturnValue(true),
};
});
@ -18,7 +18,7 @@ jest.mock('./utils', () => {
...originalModule,
deleteRows: jest.fn(),
clearCellsFromRangeSelection: jest.fn(),
publishSnapshot: jest.fn(),
updateSnapshot: jest.fn(),
};
});
@ -192,7 +192,7 @@ describe('DataGrid', () => {
});
it('editing a cell triggers publishing the snapshot', async () => {
const spy = jest.spyOn(utils, 'publishSnapshot');
const spy = jest.spyOn(utils, 'updateSnapshot');
jest.useFakeTimers();
render(<DataGridPanel {...props} />, {
wrapper: Context,
@ -215,7 +215,7 @@ describe('DataGrid', () => {
const expectedField = {
...props.data.series[0].fields[0],
};
expectedField.values = new ArrayVector([1, 9, 3, 4]);
expectedField.values = [1, 9, 3, 4];
await waitFor(() => {
const overlay = screen.getByDisplayValue('9');
@ -234,7 +234,7 @@ describe('DataGrid', () => {
expect.objectContaining({
fields: expect.arrayContaining([expectedField]),
}),
1
undefined
);
});
});
@ -272,7 +272,7 @@ describe('DataGrid', () => {
});
});
it('should add a new column', async () => {
const spy = jest.spyOn(utils, 'publishSnapshot');
const spy = jest.spyOn(utils, 'updateSnapshot');
jest.useFakeTimers();
render(<DataGridPanel {...props} />, {
wrapper: Context,
@ -301,11 +301,12 @@ describe('DataGrid', () => {
}),
]),
}),
1
undefined
);
});
it('should not add a new column if input is empty', async () => {
const spy = jest.spyOn(utils, 'publishSnapshot');
const spy = jest.spyOn(utils, 'updateSnapshot');
jest.useFakeTimers();
render(<DataGridPanel {...props} />, {
wrapper: Context,
@ -322,8 +323,9 @@ describe('DataGrid', () => {
expect(spy).not.toBeCalled();
});
it('should add a new row', async () => {
const spy = jest.spyOn(utils, 'publishSnapshot');
const spy = jest.spyOn(utils, 'updateSnapshot');
jest.useFakeTimers();
render(<DataGridPanel {...props} />, {
wrapper: Context,
@ -368,9 +370,9 @@ describe('DataGrid', () => {
});
it('should clear cell when cell is selected and delete button clicked', async () => {
const spy = jest.spyOn(utils, 'updateSnapshot');
const spyClearingCells = jest.spyOn(utils, 'clearCellsFromRangeSelection');
const spyDeleteRows = jest.spyOn(utils, 'deleteRows');
const spy = jest.spyOn(utils, 'publishSnapshot');
jest.useFakeTimers();
render(<DataGridPanel {...props} />, {
@ -398,9 +400,9 @@ describe('DataGrid', () => {
});
it('should clear row when row is selected delete button clicked', async () => {
const spy = jest.spyOn(utils, 'updateSnapshot');
const spyClearingCells = jest.spyOn(utils, 'clearCellsFromRangeSelection');
const spyDeleteRows = jest.spyOn(utils, 'deleteRows');
const spy = jest.spyOn(utils, 'publishSnapshot');
jest.useFakeTimers();
render(<DataGridPanel {...props} />, {
@ -424,8 +426,7 @@ describe('DataGrid', () => {
});
it('should move column when column dragged and dropped', async () => {
const spy = jest.spyOn(utils, 'publishSnapshot');
const spy = jest.spyOn(utils, 'updateSnapshot');
jest.useFakeTimers();
render(<DataGridPanel {...props} />, {
wrapper: Context,
@ -446,17 +447,18 @@ describe('DataGrid', () => {
fireEvent.mouseUp(canvas);
const df = new MutableDataFrame(props.data.series[0]);
const df = {
...props.data.series[0],
};
df.fields = [df.fields[1], df.fields[0], df.fields[2]];
const received = spy.mock.calls[spy.mock.calls.length - 1][0].fields.map((f) => f.name);
const received = spy.mock.calls[spy.mock.calls.length - 1][0].fields.map((f: Field) => f.name);
expect(received).toEqual(df.fields.map((f) => f.name));
});
it('should move row when row dragged and dropped', async () => {
const spy = jest.spyOn(utils, 'publishSnapshot');
const spy = jest.spyOn(utils, 'updateSnapshot');
jest.useFakeTimers();
render(<DataGridPanel {...props} />, {
wrapper: Context,

@ -10,16 +10,16 @@ import DataEditor, {
} from '@glideapps/glide-data-grid';
import React, { useEffect, useReducer } from 'react';
import { Field, PanelProps, FieldType } from '@grafana/data';
import { Field, PanelProps, FieldType, DataFrame } from '@grafana/data';
import { PanelDataErrorView } from '@grafana/runtime';
import { useTheme2 } from '@grafana/ui';
import { usePanelContext, useTheme2 } from '@grafana/ui';
import '@glideapps/glide-data-grid/dist/index.css';
import { AddColumn } from './components/AddColumn';
import { DatagridContextMenu } from './components/DatagridContextMenu';
import { RenameColumnCell } from './components/RenameColumnCell';
import { isDatagridEditEnabled } from './featureFlagUtils';
import { isDatagridEnabled } from './featureFlagUtils';
import { PanelOptions } from './panelcfg.gen';
import { DatagridActionType, datagridReducer, initialState } from './state';
import {
@ -28,19 +28,21 @@ import {
EMPTY_CELL,
getGridCellKind,
getGridTheme,
publishSnapshot,
RIGHT_ELEMENT_PROPS,
TRAILING_ROW_OPTIONS,
getStyles,
ROW_MARKER_BOTH,
ROW_MARKER_NUMBER,
hasGridSelection,
updateSnapshot,
} from './utils';
export interface DataGridProps extends PanelProps<PanelOptions> {}
export function DataGridPanel({ options, data, id, fieldConfig, width, height }: DataGridProps) {
const [state, dispatch] = useReducer(datagridReducer, initialState);
const { onUpdateData } = usePanelContext();
const {
columns,
contextMenuData,
@ -74,9 +76,23 @@ export function DataGridPanel({ options, data, id, fieldConfig, width, height }:
return getGridCellKind(field, row, hasGridSelection(gridSelection));
};
const onCellEdited = (cell: Item, newValue: EditableGridCell) => {
const onCellEdited = async (cell: Item, newValue: EditableGridCell) => {
// if there are rows selected, return early, we don't want to edit any cell
if (hasGridSelection(gridSelection)) {
return;
}
const [col, row] = cell;
const field: Field = frame.fields[col];
const frameCopy = {
...frame,
fields: frame.fields.map((f) => {
return {
...f,
values: [...f.values],
};
}),
};
const field: Field = frameCopy.fields[col];
if (!field) {
return;
@ -85,14 +101,14 @@ export function DataGridPanel({ options, data, id, fieldConfig, width, height }:
const values = field.values.toArray();
values[row] = newValue.data;
field.values = values;
field.values = [...values];
publishSnapshot(frame, id);
updateSnapshot(frameCopy, onUpdateData);
};
const onColumnInputBlur = (columnName: string) => {
const len = frame.length ?? 0;
publishSnapshot(
updateSnapshot(
{
...frame,
fields: [
@ -105,7 +121,7 @@ export function DataGridPanel({ options, data, id, fieldConfig, width, height }:
},
],
},
id
onUpdateData
);
};
@ -115,7 +131,8 @@ export function DataGridPanel({ options, data, id, fieldConfig, width, height }:
values.push(null);
return { ...f, values };
});
publishSnapshot({ ...frame, fields, length: frame.length + 1 }, id);
updateSnapshot({ ...frame, fields, length: frame.length + 1 }, onUpdateData);
};
const onColumnResize = (column: GridColumn, width: number, columnIndex: number, newSizeWithGrow: number) => {
@ -133,12 +150,12 @@ export function DataGridPanel({ options, data, id, fieldConfig, width, height }:
const onDeletePressed = (selection: GridSelection) => {
if (selection.current && selection.current.range) {
publishSnapshot(clearCellsFromRangeSelection(frame, selection.current.range), id);
updateSnapshot(clearCellsFromRangeSelection(frame, selection.current.range), onUpdateData);
return true;
}
if (selection.rows) {
publishSnapshot(deleteRows(frame, selection.rows.toArray()), id);
updateSnapshot(deleteRows(frame, selection.rows.toArray()), onUpdateData);
return true;
}
@ -162,14 +179,17 @@ export function DataGridPanel({ options, data, id, fieldConfig, width, height }:
});
};
const onColumnMove = (from: number, to: number) => {
const onColumnMove = async (from: number, to: number) => {
const fields = frame.fields.map((f) => f);
const field = fields[from];
fields.splice(from, 1);
fields.splice(to, 0, field);
dispatch({ type: DatagridActionType.columnMove, payload: { from, to } });
publishSnapshot({ ...frame, fields }, id);
const hasUpdated = await updateSnapshot({ ...frame, fields }, onUpdateData);
if (hasUpdated) {
dispatch({ type: DatagridActionType.columnMove, payload: { from, to } });
}
};
const onRowMove = (from: number, to: number) => {
@ -181,7 +201,7 @@ export function DataGridPanel({ options, data, id, fieldConfig, width, height }:
field.values.splice(to, 0, value);
}
publishSnapshot({ ...frame, fields }, id);
updateSnapshot({ ...frame, fields }, onUpdateData);
};
const onColumnRename = () => {
@ -193,7 +213,8 @@ export function DataGridPanel({ options, data, id, fieldConfig, width, height }:
fields[columnIdx].name = columnName;
dispatch({ type: DatagridActionType.hideColumnRenameInput });
publishSnapshot({ ...frame, fields }, id);
updateSnapshot({ ...frame, fields }, onUpdateData);
};
const onSearchClose = () => {
@ -204,10 +225,18 @@ export function DataGridPanel({ options, data, id, fieldConfig, width, height }:
dispatch({ type: DatagridActionType.multipleCellsSelected, payload: { selection } });
};
const onContextMenuSave = (data: DataFrame) => {
updateSnapshot(data, onUpdateData);
};
if (!frame) {
return <PanelDataErrorView panelId={id} fieldConfig={fieldConfig} data={data} />;
}
if (!isDatagridEnabled()) {
return <PanelDataErrorView panelId={id} message="Datagrid is not enabled" fieldConfig={fieldConfig} data={data} />;
}
if (!document.getElementById('portal')) {
const portal = document.createElement('div');
portal.id = 'portal';
@ -230,37 +259,37 @@ export function DataGridPanel({ options, data, id, fieldConfig, width, height }:
smoothScrollX
smoothScrollY
overscrollY={50}
onCellEdited={isDatagridEditEnabled() ? onCellEdited : undefined}
getCellsForSelection={isDatagridEditEnabled() ? true : undefined}
showSearch={isDatagridEditEnabled() ? toggleSearch : false}
onCellEdited={isDatagridEnabled() ? onCellEdited : undefined}
getCellsForSelection={isDatagridEnabled() ? true : undefined}
showSearch={isDatagridEnabled() ? toggleSearch : false}
onSearchClose={onSearchClose}
onPaste={isDatagridEditEnabled() ? true : undefined}
onPaste={isDatagridEnabled() ? true : undefined}
gridSelection={gridSelection}
onGridSelectionChange={isDatagridEditEnabled() ? onGridSelectionChange : undefined}
onRowAppended={isDatagridEditEnabled() ? addNewRow : undefined}
onDelete={isDatagridEditEnabled() ? onDeletePressed : undefined}
rowMarkers={isDatagridEditEnabled() ? ROW_MARKER_BOTH : ROW_MARKER_NUMBER}
onGridSelectionChange={isDatagridEnabled() ? onGridSelectionChange : undefined}
onRowAppended={isDatagridEnabled() ? addNewRow : undefined}
onDelete={isDatagridEnabled() ? onDeletePressed : undefined}
rowMarkers={isDatagridEnabled() ? ROW_MARKER_BOTH : ROW_MARKER_NUMBER}
onColumnResize={onColumnResize}
onColumnResizeEnd={onColumnResizeEnd}
onCellContextMenu={isDatagridEditEnabled() ? onCellContextMenu : undefined}
onHeaderContextMenu={isDatagridEditEnabled() ? onHeaderContextMenu : undefined}
onHeaderMenuClick={isDatagridEditEnabled() ? onHeaderMenuClick : undefined}
onCellContextMenu={isDatagridEnabled() ? onCellContextMenu : undefined}
onHeaderContextMenu={isDatagridEnabled() ? onHeaderContextMenu : undefined}
onHeaderMenuClick={isDatagridEnabled() ? onHeaderMenuClick : undefined}
trailingRowOptions={TRAILING_ROW_OPTIONS}
rightElement={
isDatagridEditEnabled() ? (
isDatagridEnabled() ? (
<AddColumn onColumnInputBlur={onColumnInputBlur} divStyle={styles.addColumnDiv} />
) : null
}
rightElementProps={RIGHT_ELEMENT_PROPS}
freezeColumns={columnFreezeIndex}
onRowMoved={isDatagridEditEnabled() ? onRowMove : undefined}
onColumnMoved={isDatagridEditEnabled() ? onColumnMove : undefined}
onRowMoved={isDatagridEnabled() ? onRowMove : undefined}
onColumnMoved={isDatagridEnabled() ? onColumnMove : undefined}
/>
{contextMenuData.isContextMenuOpen && (
<DatagridContextMenu
menuData={contextMenuData}
data={frame}
saveData={(data) => publishSnapshot(data, id)}
saveData={onContextMenuSave}
closeContextMenu={closeContextMenu}
dispatch={dispatch}
gridSelection={gridSelection}

@ -1,5 +1,5 @@
import { config } from '@grafana/runtime';
export const isDatagridEditEnabled = () => {
export const isDatagridEnabled = () => {
return config.featureToggles.enableDatagridEditing;
};

@ -10,7 +10,7 @@ import {
import { DataFrame, Field, FieldType, getFieldDisplayName } from '@grafana/data';
import { isDatagridEditEnabled } from './featureFlagUtils';
import { isDatagridEnabled } from './featureFlagUtils';
import {
DatagridContextMenuData,
DEFAULT_CONTEXT_MENU,
@ -224,7 +224,7 @@ export const datagridReducer = (state: DatagridState, action: DatagridAction): D
title: displayName,
width: state.columns[index]?.width ?? getCellWidth(field),
icon: typeToIconMap.get(field.type),
hasMenu: isDatagridEditEnabled(),
hasMenu: isDatagridEnabled(),
trailingRowOptions: { targetColumn: --index },
};
}),

@ -54,7 +54,7 @@ describe('when deleting rows', () => {
expect(newDf.fields[2].values.toArray()).toEqual(['a', 'c', 'e']);
expect(newDf.length).toEqual(3);
newDf = deleteRows(df, [2], true);
newDf = deleteRows(newDf, [2], true);
expect(newDf.fields[0].values.toArray()).toEqual(['a', 'c']);
expect(newDf.fields[1].values.toArray()).toEqual([1, 3]);

@ -1,11 +1,9 @@
import { css } from '@emotion/css';
import { CompactSelection, GridCell, GridCellKind, GridSelection, Theme } from '@glideapps/glide-data-grid';
import { ArrayVector, DataFrame, DataFrameJSON, dataFrameToJSON, Field, GrafanaTheme2, FieldType } from '@grafana/data';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { GrafanaQuery, GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
import { DataFrame, Field, GrafanaTheme2, FieldType } from '@grafana/data';
import { isDatagridEditEnabled } from './featureFlagUtils';
import { isDatagridEnabled } from './featureFlagUtils';
const HEADER_FONT_FAMILY = '600 13px Inter';
const CELL_FONT_FAMILY = '400 13px Inter';
@ -25,11 +23,6 @@ export const EMPTY_DF = {
length: 0,
};
export const GRAFANA_DS = {
type: 'grafana',
uid: 'grafana',
};
export const EMPTY_CELL: GridCell = {
kind: GridCellKind.Text,
data: '',
@ -79,6 +72,17 @@ interface CellRange {
height: number;
}
export async function updateSnapshot(
frame: DataFrame,
updateData?: (frames: DataFrame[]) => Promise<boolean>
): Promise<boolean> {
if (updateData && isDatagridEnabled()) {
return await updateData([frame]);
}
return false;
}
export const getTextWidth = (text: string, isHeader = false): number => {
const context = TEXT_CANVAS.getContext('2d');
context!.font = isHeader ? HEADER_FONT_FAMILY : CELL_FONT_FAMILY;
@ -107,7 +111,12 @@ export const getCellWidth = (field: Field): number => {
};
export const deleteRows = (gridData: DataFrame, rows: number[], hardDelete = false): DataFrame => {
for (const field of gridData.fields) {
const copy = {
...gridData,
fields: gridData.fields.map((field) => ({ ...field, values: field.values.slice() })),
};
for (const field of copy.fields) {
const valuesArray = field.values.toArray();
//delete from the end of the array to avoid index shifting
@ -119,13 +128,12 @@ export const deleteRows = (gridData: DataFrame, rows: number[], hardDelete = fal
}
}
field.values = new ArrayVector(valuesArray);
field.values = [...valuesArray];
}
return {
...gridData,
fields: [...gridData.fields],
length: gridData.fields[0]?.values.length ?? 0,
...copy,
length: copy.fields[0]?.values.length ?? 0,
};
};
@ -133,50 +141,29 @@ export const clearCellsFromRangeSelection = (gridData: DataFrame, range: CellRan
const colFrom: number = range.x;
const rowFrom: number = range.y;
const colTo: number = range.x + range.width - 1;
const copy = {
...gridData,
fields: gridData.fields.map((field) => ({ ...field, values: field.values.slice() })),
};
for (let i = colFrom; i <= colTo; i++) {
const field = gridData.fields[i];
const field = copy.fields[i];
const valuesArray = field.values.toArray();
valuesArray.splice(rowFrom, range.height, ...new Array(range.height).fill(null));
field.values = new ArrayVector(valuesArray);
field.values = [...valuesArray];
}
return {
...gridData,
fields: [...gridData.fields],
length: gridData.fields[0]?.values.length ?? 0,
};
};
export const publishSnapshot = (data: DataFrame, panelID: number): void => {
if (!isDatagridEditEnabled()) {
return;
}
const snapshot: DataFrameJSON[] = [dataFrameToJSON(data)];
const dashboard = getDashboardSrv().getCurrent();
const panelModel = dashboard?.getPanelById(panelID);
const query: GrafanaQuery = {
refId: 'A',
queryType: GrafanaQueryType.Snapshot,
snapshot,
datasource: GRAFANA_DS,
...copy,
length: copy.fields[0]?.values.length ?? 0,
};
panelModel!.updateQueries({
dataSource: GRAFANA_DS,
queries: [query],
});
panelModel!.refresh();
};
//Converting an array of nulls or undefineds returns them as strings and prints them in the cells instead of empty cells. Thus the cleanup func
export const cleanStringFieldAfterConversion = (field: Field): void => {
const valuesArray = field.values.toArray();
field.values = new ArrayVector(valuesArray.map((val) => (val === 'undefined' || val === 'null' ? null : val)));
field.values = valuesArray.map((val) => (val === 'undefined' || val === 'null' ? null : val));
return;
};
@ -217,7 +204,7 @@ export const getGridCellKind = (field: Field, row: number, hasGridSelection = fa
return {
kind: GridCellKind.Number,
data: value ? value : 0,
allowOverlay: isDatagridEditEnabled()! && !hasGridSelection,
allowOverlay: isDatagridEnabled()! && !hasGridSelection,
readonly: false,
displayData: value !== null && value !== undefined ? value.toString() : '',
};
@ -225,7 +212,7 @@ export const getGridCellKind = (field: Field, row: number, hasGridSelection = fa
return {
kind: GridCellKind.Text,
data: value ? value : '',
allowOverlay: isDatagridEditEnabled()! && !hasGridSelection,
allowOverlay: isDatagridEnabled()! && !hasGridSelection,
readonly: false,
displayData: value !== null && value !== undefined ? value.toString() : '',
};
@ -233,7 +220,7 @@ export const getGridCellKind = (field: Field, row: number, hasGridSelection = fa
return {
kind: GridCellKind.Text,
data: value ? value : '',
allowOverlay: isDatagridEditEnabled()! && !hasGridSelection,
allowOverlay: isDatagridEnabled()! && !hasGridSelection,
readonly: false,
displayData: value !== null && value !== undefined ? value.toString() : '',
};
@ -301,9 +288,16 @@ export const getStyles = (theme: GrafanaTheme2, isResizeInProgress: boolean) =>
};
export const hasGridSelection = (gridSelection: GridSelection): boolean => {
if (!gridSelection.current) {
if (gridSelection.rows.length || gridSelection.columns.length) {
return true;
}
if (gridSelection.current === undefined) {
return false;
}
return gridSelection.current.range && gridSelection.current.range.height > 1 && gridSelection.current.range.width > 1;
return (
gridSelection.current.range &&
!(gridSelection.current.range.height === 1 && gridSelection.current.range.width === 1)
);
};

Loading…
Cancel
Save