mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Datagrid: Switch to panel context update (#67221)
* WIP * Switch Datagrid to use PanelContext onUpdateData * PR modifications * refactor * block panel if not enabled
This commit is contained in:
@@ -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,
|
||||
...copy,
|
||||
length: copy.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,
|
||||
};
|
||||
|
||||
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)
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user