mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
parent
dbae7ccd3f
commit
14c82c2725
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
function PanelDataTransformationsTabRendered({ model }: SceneComponentProps<PanelDataTransformationsTab>) {
|
||||
// const { dataRef } = model.useState();
|
||||
// const dataObj = dataRef.resolve();
|
||||
// // const { transformations } = dataObj.useState();
|
||||
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[];
|
||||
|
||||
return <div>TODO Transformations</div>;
|
||||
if (transformations.length < 1) {
|
||||
return <EmptyTransformationsMessage onShowPicker={() => {}}></EmptyTransformationsMessage>;
|
||||
}
|
||||
|
||||
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={() => {}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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={() => {
|
||||
this.setState({ showPicker: true });
|
||||
}}
|
||||
data-testid={selectors.components.Transforms.addTransformationButton}
|
||||
>
|
||||
Add transformation
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
<EmptyTransformationsMessage
|
||||
onShowPicker={() => {
|
||||
this.setState({ showPicker: true });
|
||||
}}
|
||||
></EmptyTransformationsMessage>
|
||||
);
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user