diff --git a/devenv/dev-dashboards/panel-datagrid/datagrid_metric_values.json b/devenv/dev-dashboards/panel-datagrid/datagrid_metric_values.json index 9ee8da0fd5d..6fb2f433102 100644 --- a/devenv/dev-dashboards/panel-datagrid/datagrid_metric_values.json +++ b/devenv/dev-dashboards/panel-datagrid/datagrid_metric_values.json @@ -75,5 +75,6 @@ "timezone": "", "title": "Datagrid example", "version": 0, + "uid": "c01bf42b-b783-4447-a304-8554cee1843b", "weekStart": "" } \ No newline at end of file diff --git a/e2e/datagrid-suite/datagrid-data-change.spec.ts b/e2e/datagrid-suite/datagrid-data-change.spec.ts index df61fca4751..f4409e32220 100644 --- a/e2e/datagrid-suite/datagrid-data-change.spec.ts +++ b/e2e/datagrid-suite/datagrid-data-change.spec.ts @@ -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'); }, }); diff --git a/e2e/datagrid-suite/datagrid-editing-features.spec.ts b/e2e/datagrid-suite/datagrid-editing-features.spec.ts index f1972fd6457..e71d1f31967 100644 --- a/e2e/datagrid-suite/datagrid-editing-features.spec.ts +++ b/e2e/datagrid-suite/datagrid-editing-features.spec.ts @@ -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}') diff --git a/public/app/plugins/datasource/grafana/utils.ts b/public/app/plugins/datasource/grafana/utils.ts index ec9b6ff321c..1e7972f5888 100644 --- a/public/app/plugins/datasource/grafana/utils.ts +++ b/public/app/plugins/datasource/grafana/utils.ts @@ -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: () => { diff --git a/public/app/plugins/panel/datagrid/DataGridPanel.test.tsx b/public/app/plugins/panel/datagrid/DataGridPanel.test.tsx index 49bc1ffae58..d2e13269bf7 100644 --- a/public/app/plugins/panel/datagrid/DataGridPanel.test.tsx +++ b/public/app/plugins/panel/datagrid/DataGridPanel.test.tsx @@ -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(, { 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(, { 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(, { 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(, { 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(, { @@ -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(, { @@ -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(, { 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(, { wrapper: Context, diff --git a/public/app/plugins/panel/datagrid/DataGridPanel.tsx b/public/app/plugins/panel/datagrid/DataGridPanel.tsx index f99370c49ad..dc72717ce5c 100644 --- a/public/app/plugins/panel/datagrid/DataGridPanel.tsx +++ b/public/app/plugins/panel/datagrid/DataGridPanel.tsx @@ -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 {} 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 ; } + if (!isDatagridEnabled()) { + return ; + } + 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() ? ( ) : 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 && ( publishSnapshot(data, id)} + saveData={onContextMenuSave} closeContextMenu={closeContextMenu} dispatch={dispatch} gridSelection={gridSelection} diff --git a/public/app/plugins/panel/datagrid/featureFlagUtils.tsx b/public/app/plugins/panel/datagrid/featureFlagUtils.tsx index e2fed546cf0..0b41f53c114 100644 --- a/public/app/plugins/panel/datagrid/featureFlagUtils.tsx +++ b/public/app/plugins/panel/datagrid/featureFlagUtils.tsx @@ -1,5 +1,5 @@ import { config } from '@grafana/runtime'; -export const isDatagridEditEnabled = () => { +export const isDatagridEnabled = () => { return config.featureToggles.enableDatagridEditing; }; diff --git a/public/app/plugins/panel/datagrid/state.ts b/public/app/plugins/panel/datagrid/state.ts index 28d7db07807..65840df7afe 100644 --- a/public/app/plugins/panel/datagrid/state.ts +++ b/public/app/plugins/panel/datagrid/state.ts @@ -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 }, }; }), diff --git a/public/app/plugins/panel/datagrid/utils.test.ts b/public/app/plugins/panel/datagrid/utils.test.ts index 3d493c2106a..79144a21426 100644 --- a/public/app/plugins/panel/datagrid/utils.test.ts +++ b/public/app/plugins/panel/datagrid/utils.test.ts @@ -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]); diff --git a/public/app/plugins/panel/datagrid/utils.ts b/public/app/plugins/panel/datagrid/utils.ts index 5cb6cf08724..3545dc3af86 100644 --- a/public/app/plugins/panel/datagrid/utils.ts +++ b/public/app/plugins/panel/datagrid/utils.ts @@ -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 +): Promise { + 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) + ); };