diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/EmptyTransformationsMessage.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/EmptyTransformationsMessage.tsx new file mode 100644 index 00000000000..c452a9cfd11 --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/EmptyTransformationsMessage.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import { selectors } from '@grafana/e2e-selectors'; +import { Box, Button, Stack, Text } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; + +interface EmptyTransformationsProps { + onShowPicker: () => void; +} +export function EmptyTransformationsMessage(props: EmptyTransformationsProps) { + return ( + + + + Start transforming data + + + + Transformations allow data to be changed in various ways before your visualization is shown. +
+ This includes joining data together, renaming fields, making calculations, formatting data for display, and + more. +
+
+ +
+
+ ); +} diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx index 64d7f7f97e0..379b314c627 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx @@ -11,7 +11,7 @@ import { SceneObjectUrlValues, VizPanel, } from '@grafana/scenes'; -import { Tab, TabContent, TabsBar, useStyles2 } from '@grafana/ui'; +import { Container, CustomScrollbar, Tab, TabContent, TabsBar, useStyles2 } from '@grafana/ui'; import { shouldShowAlertingTab } from 'app/features/dashboard/components/PanelEditor/state/selectors'; import { VizPanelManager } from '../VizPanelManager'; @@ -158,7 +158,11 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { ); })} - {currentTab && } + + + {currentTab && } + + ); } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx new file mode 100644 index 00000000000..1baa44f4c5c --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx @@ -0,0 +1,54 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { FieldType, LoadingState, TimeRange, standardTransformersRegistry, toDataFrame } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { SceneDataTransformer } from '@grafana/scenes'; +import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; + +import { PanelDataTransformationsTab, PanelDataTransformationsTabRendered } from './PanelDataTransformationsTab'; + +function createPanelManagerMock(sceneDataTransformer: SceneDataTransformer) { + return { + getDataTransformer: () => sceneDataTransformer, + } as unknown as PanelDataTransformationsTab; +} + +describe('PanelDataTransformationsTab', () => { + it('renders empty message when there are no transformations', async () => { + const modelMock = createPanelManagerMock(new SceneDataTransformer({ transformations: [] })); + render(); + + await screen.findByTestId(selectors.components.Transforms.noTransformationsMessage); + }); + + it('renders transformations when there are transformations', async () => { + standardTransformersRegistry.setInit(getStandardTransformers); + const modelMock = createPanelManagerMock( + new SceneDataTransformer({ + data: { + timeRange: {} as unknown as TimeRange, + state: {} as unknown as LoadingState, + series: [ + toDataFrame({ + name: 'A', + fields: [ + { name: 'time', type: FieldType.time, values: [100, 200, 300] }, + { name: 'values', type: FieldType.number, values: [1, 2, 3] }, + ], + }), + ], + }, + transformations: [ + { + id: 'calculateField', + options: {}, + }, + ], + }) + ); + render(); + + await screen.findByText('1 - Add field from calculation'); + }); +}); diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx index c0025078ed7..3b1ed5a141e 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx @@ -1,10 +1,16 @@ +import { css } from '@emotion/css'; import React from 'react'; +import { DragDropContext, Droppable } from 'react-beautiful-dnd'; -import { IconName } from '@grafana/data'; -import { SceneObjectBase, SceneComponentProps } from '@grafana/scenes'; +import { DataTransformerConfig, GrafanaTheme2, IconName, PanelData } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { SceneObjectBase, SceneComponentProps, SceneDataTransformer } from '@grafana/scenes'; +import { Button, ButtonGroup, ConfirmModal, useStyles2 } from '@grafana/ui'; +import { TransformationOperationRows } from 'app/features/dashboard/components/TransformationsEditor/TransformationOperationRows'; import { VizPanelManager } from '../VizPanelManager'; +import { EmptyTransformationsMessage } from './EmptyTransformationsMessage'; import { PanelDataPaneTabState, PanelDataPaneTab } from './types'; interface PanelDataTransformationsTabState extends PanelDataPaneTabState {} @@ -23,7 +29,7 @@ export class PanelDataTransformationsTab } getItemsCount() { - return 0; + return this.getDataTransformer().state.transformations.length; } constructor(panelManager: VizPanelManager) { @@ -32,15 +38,104 @@ export class PanelDataTransformationsTab this._panelManager = panelManager; } - get panelManager() { - return this._panelManager; + public getDataTransformer(): SceneDataTransformer { + const provider = this._panelManager.state.panel.state.$data; + if (!provider || !(provider instanceof SceneDataTransformer)) { + throw new Error('Could not find SceneDataTransformer for panel'); + } + + return provider; + } + + public changeTransformations(transformations: DataTransformerConfig[]) { + const dataProvider = this.getDataTransformer(); + dataProvider.setState({ transformations }); + dataProvider.reprocessTransformations(); } } -function PanelDataTransformationsTabRendered({ model }: SceneComponentProps) { - // const { dataRef } = model.useState(); - // const dataObj = dataRef.resolve(); - // // const { transformations } = dataObj.useState(); +export function PanelDataTransformationsTabRendered({ model }: SceneComponentProps) { + const styles = useStyles2(getStyles); + const { data, transformations: transformsWrongType } = model.getDataTransformer().useState(); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const transformations: DataTransformerConfig[] = transformsWrongType as unknown as DataTransformerConfig[]; + + if (transformations.length < 1) { + return {}}>; + } + + if (!data) { + return; + } - return
TODO Transformations
; + return ( + <> + + + + + + {}} + onDismiss={() => {}} + /> + + ); } + +interface TransformationEditorProps { + transformations: DataTransformerConfig[]; + model: PanelDataTransformationsTab; + data: PanelData; +} + +function TransformationsEditor({ transformations, model, data }: TransformationEditorProps) { + const transformationEditorRows = transformations.map((t, i) => ({ id: `${i} - ${t.id}`, transformation: t })); + + return ( + {}}> + + {(provided) => { + return ( +
+ { + const newTransformations = transformations.slice(); + newTransformations[index] = transformation; + model.changeTransformations(newTransformations); + }} + onRemove={(index) => { + const newTransformations = transformations.slice(); + newTransformations.splice(index); + model.changeTransformations(newTransformations); + }} + configs={transformationEditorRows} + data={data} + > + {provided.placeholder} +
+ ); + }} +
+
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + removeAll: css({ + marginLeft: theme.spacing(2), + }), +}); diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx index 62f881f8f4a..6a2666e5351 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx @@ -21,12 +21,9 @@ import { withTheme, IconButton, ButtonGroup, - Box, - Text, - Stack, } from '@grafana/ui'; import config from 'app/core/config'; -import { Trans } from 'app/core/internationalization'; +import { EmptyTransformationsMessage } from 'app/features/dashboard-scene/panel-edit/PanelDataPane/EmptyTransformationsMessage'; import { PanelModel } from '../../state'; import { PanelNotSupported } from '../PanelEditor/PanelNotSupported'; @@ -258,36 +255,11 @@ class UnThemedTransformationsEditor extends React.PureComponent { return ( - - - - Start transforming data - - - - Transformations allow data to be changed in various ways before your visualization is shown. -
- This includes joining data together, renaming fields, making calculations, formatting data for display, - and more. -
-
- -
-
+ { + this.setState({ showPicker: true }); + }} + > ); };