Scenes: Show transformations when editing scene dashboard (#80372)

* Make dashboard data source query actually use DashboardDataSource

* remove commented out bit

* Always wrap SceneQueryRunner with SceneDataTransformer

* Update Dashboard model compat wrapper tests

* DashboardQueryEditor test

* VizPanelManager tests update

* transform save model to scene tests update

* Betterer

* PanelMenuBehavior test update

* Few more bits

* Prettier

* Show transformations when editing scene dashboard

* remove and edit transformations works

* add add and remove buttons

* Change styles to object to fix betterer issue

* Revert "Change styles to object to fix betterer issue"

This reverts commit 8627b9162c.

* Fix the correct file...

* Some refactoring

* remove unneessary if statement

* panel data not present on first render

* move transformation tabs out of folder

* fix tests

* add lint exception

* refactor tab component

* Fix merge issue

* reorder components

---------

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
pull/80667/head
Oscar Kilhed 1 year ago committed by GitHub
parent dbae7ccd3f
commit 14c82c2725
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 37
      public/app/features/dashboard-scene/panel-edit/PanelDataPane/EmptyTransformationsMessage.tsx
  2. 8
      public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx
  3. 54
      public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx
  4. 115
      public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx
  5. 36
      public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.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 (
<Box alignItems="center" padding={4}>
<Stack direction="column" alignItems="center" gap={2}>
<Text element="h3" textAlignment="center">
<Trans key="transformations.empty.add-transformation-header">Start transforming data</Trans>
</Text>
<Text element="p" textAlignment="center" data-testid={selectors.components.Transforms.noTransformationsMessage}>
<Trans key="transformations.empty.add-transformation-body">
Transformations allow data to be changed in various ways before your visualization is shown.
<br />
This includes joining data together, renaming fields, making calculations, formatting data for display, and
more.
</Trans>
</Text>
<Button
icon="plus"
variant="primary"
size="md"
onClick={props.onShowPicker}
data-testid={selectors.components.Transforms.addTransformationButton}
>
Add transformation
</Button>
</Stack>
</Box>
);
}

@ -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<PanelDataPane>) {
);
})}
</TabsBar>
<TabContent className={styles.tabContent}>{currentTab && <currentTab.Component model={currentTab} />}</TabContent>
<TabContent className={styles.tabContent}>
<CustomScrollbar autoHeightMin="100%">
<Container>{currentTab && <currentTab.Component model={currentTab} />}</Container>
</CustomScrollbar>
</TabContent>
</div>
);
}

@ -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(<PanelDataTransformationsTabRendered model={modelMock}></PanelDataTransformationsTabRendered>);
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(<PanelDataTransformationsTabRendered model={modelMock}></PanelDataTransformationsTabRendered>);
await screen.findByText('1 - Add field from calculation');
});
});

@ -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();
}
}
export function PanelDataTransformationsTabRendered({ model }: SceneComponentProps<PanelDataTransformationsTab>) {
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 <EmptyTransformationsMessage onShowPicker={() => {}}></EmptyTransformationsMessage>;
}
function PanelDataTransformationsTabRendered({ model }: SceneComponentProps<PanelDataTransformationsTab>) {
// const { dataRef } = model.useState();
// const dataObj = dataRef.resolve();
// // const { transformations } = dataObj.useState();
if (!data) {
return;
}
return (
<>
<TransformationsEditor data={data} transformations={transformations} model={model} />
<ButtonGroup>
<Button
icon="plus"
variant="secondary"
onClick={() => {}}
data-testid={selectors.components.Transforms.addTransformationButton}
>
Add another transformation
</Button>
<Button className={styles.removeAll} icon="times" variant="secondary" onClick={() => {}}>
Delete all transformations
</Button>
</ButtonGroup>
<ConfirmModal
isOpen={false}
title="Delete all transformations?"
body="By deleting all transformations, you will go back to the main selection screen."
confirmText="Delete all"
onConfirm={() => {}}
onDismiss={() => {}}
/>
</>
);
}
return <div>TODO Transformations</div>;
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 (
<DragDropContext onDragEnd={() => {}}>
<Droppable droppableId="transformations-list" direction="vertical">
{(provided) => {
return (
<div ref={provided.innerRef} {...provided.droppableProps}>
<TransformationOperationRows
onChange={(index, transformation) => {
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}
></TransformationOperationRows>
{provided.placeholder}
</div>
);
}}
</Droppable>
</DragDropContext>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
removeAll: css({
marginLeft: theme.spacing(2),
}),
});

@ -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<TransformationsE
renderEmptyMessage = () => {
return (
<Box alignItems="center" padding={4}>
<Stack direction="column" alignItems="center" gap={2}>
<Text element="h3" textAlignment="center">
<Trans key="transformations.empty.add-transformation-header">Start transforming data</Trans>
</Text>
<Text
element="p"
textAlignment="center"
data-testid={selectors.components.Transforms.noTransformationsMessage}
>
<Trans key="transformations.empty.add-transformation-body">
Transformations allow data to be changed in various ways before your visualization is shown.
<br />
This includes joining data together, renaming fields, making calculations, formatting data for display,
and more.
</Trans>
</Text>
<Button
icon="plus"
variant="primary"
size="md"
onClick={() => {
<EmptyTransformationsMessage
onShowPicker={() => {
this.setState({ showPicker: true });
}}
data-testid={selectors.components.Transforms.addTransformationButton}
>
Add transformation
</Button>
</Stack>
</Box>
></EmptyTransformationsMessage>
);
};

Loading…
Cancel
Save