Show transformations when editing scene dashboard

This commit is contained in:
Oscar Kilhed
2024-01-15 16:24:44 +01:00
parent 46b0bf596c
commit 5b2ba605d5
6 changed files with 214 additions and 81 deletions
@@ -16,7 +16,7 @@ import { VizPanelManager } from '../VizPanelManager';
import { PanelDataAlertingTab } from './PanelDataAlertingTab';
import { PanelDataQueriesTab } from './PanelDataQueriesTab';
import { PanelDataTransformationsTab } from './PanelDataTransformationsTab';
import { PanelDataTransformationsTab } from './PanelDataTransformations/PanelDataTransformationsTab';
import { PanelDataPaneTab } from './types';
export interface PanelDataPaneState extends SceneObjectState {
@@ -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>
);
}
@@ -0,0 +1,64 @@
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 {
panelManager: {
useState: () => {
return {
panel: {
useState: () => {
return { $data: 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');
});
});
@@ -0,0 +1,106 @@
import React from 'react';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { IconName } from '@grafana/data';
import { SceneObjectBase, SceneComponentProps, SceneDataTransformer } from '@grafana/scenes';
import { Container, CustomScrollbar } from '@grafana/ui';
import { TransformationOperationRows } from 'app/features/dashboard/components/TransformationsEditor/TransformationOperationRows';
import { TransformationsEditorTransformation } from 'app/features/dashboard/components/TransformationsEditor/types';
import { VizPanelManager } from '../../VizPanelManager';
import { PanelDataPaneTabState, PanelDataPaneTab } from '../types';
import { EmptyTransformationsMessage } from './EmptyTransformationsMessage';
interface PanelDataTransformationsTabState extends PanelDataPaneTabState {}
export class PanelDataTransformationsTab
extends SceneObjectBase<PanelDataTransformationsTabState>
implements PanelDataPaneTab
{
static Component = PanelDataTransformationsTabRendered;
tabId = 'transformations';
icon: IconName = 'process';
private _panelManager: VizPanelManager;
getTabLabel() {
return 'Transformations';
}
getItemsCount() {
return null;
}
constructor(panelManager: VizPanelManager) {
super({});
this._panelManager = panelManager;
}
get panelManager() {
return this._panelManager;
}
}
interface TransformationEditorProps {
sceneDataTransformer: SceneDataTransformer;
}
function TransformationsEditor(props: TransformationEditorProps) {
const dataState = props.sceneDataTransformer.useState();
const transformationEditorRows: TransformationsEditorTransformation[] = [];
let i = 0; // Ids need to be unique for drag to change order to work
for (const t of dataState.transformations) {
if ('id' in t) {
transformationEditorRows.push({ id: `${i} - ${t.id}`, transformation: t });
i++;
}
}
return (
<DragDropContext onDragEnd={() => {}}>
<Droppable droppableId="transformations-list" direction="vertical">
{(provided) => {
return (
<div ref={provided.innerRef} {...provided.droppableProps}>
<TransformationOperationRows
onChange={() => {}}
onRemove={() => {}}
configs={transformationEditorRows}
data={{
series: dataState.data?.series || [],
annotations: dataState.data?.annotations || [],
}}
></TransformationOperationRows>
{provided.placeholder}
</div>
);
}}
</Droppable>
</DragDropContext>
);
}
export function PanelDataTransformationsTabRendered({ model }: SceneComponentProps<PanelDataTransformationsTab>) {
const panelManagerState = model.panelManager.useState();
const panelState = panelManagerState.panel.useState();
if (!(panelState.$data instanceof SceneDataTransformer)) {
return;
}
const dataState = panelState.$data.useState();
return (
<CustomScrollbar autoHeightMin="100%">
<Container>
{dataState.transformations.length < 1 ? (
<EmptyTransformationsMessage onShowPicker={() => {}}></EmptyTransformationsMessage>
) : (
<TransformationsEditor sceneDataTransformer={panelState.$data}></TransformationsEditor>
)}
</Container>
</CustomScrollbar>
);
}
@@ -1,46 +0,0 @@
import React from 'react';
import { IconName } from '@grafana/data';
import { SceneObjectBase, SceneComponentProps } from '@grafana/scenes';
import { VizPanelManager } from '../VizPanelManager';
import { PanelDataPaneTabState, PanelDataPaneTab } from './types';
interface PanelDataTransformationsTabState extends PanelDataPaneTabState {}
export class PanelDataTransformationsTab
extends SceneObjectBase<PanelDataTransformationsTabState>
implements PanelDataPaneTab
{
static Component = PanelDataTransformationsTabRendered;
tabId = 'transformations';
icon: IconName = 'process';
private _panelManager: VizPanelManager;
getTabLabel() {
return 'Transformations';
}
getItemsCount() {
return 0;
}
constructor(panelManager: VizPanelManager) {
super({});
this._panelManager = panelManager;
}
get panelManager() {
return this._panelManager;
}
}
function PanelDataTransformationsTabRendered({ model }: SceneComponentProps<PanelDataTransformationsTab>) {
// const { dataRef } = model.useState();
// const dataObj = dataRef.resolve();
// // const { transformations } = dataObj.useState();
return <div>TODO Transformations</div>;
}
@@ -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/PanelDataTransformations/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={() => {
this.setState({ showPicker: true });
}}
data-testid={selectors.components.Transforms.addTransformationButton}
>
Add transformation
</Button>
</Stack>
</Box>
<EmptyTransformationsMessage
onShowPicker={() => {
this.setState({ showPicker: true });
}}
></EmptyTransformationsMessage>
);
};