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,
|
SceneObjectUrlValues,
|
||||||
VizPanel,
|
VizPanel,
|
||||||
} from '@grafana/scenes';
|
} 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 { shouldShowAlertingTab } from 'app/features/dashboard/components/PanelEditor/state/selectors';
|
||||||
|
|
||||||
import { VizPanelManager } from '../VizPanelManager';
|
import { VizPanelManager } from '../VizPanelManager';
|
||||||
@ -158,7 +158,11 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</TabsBar>
|
</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>
|
</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 React from 'react';
|
||||||
|
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
import { IconName } from '@grafana/data';
|
import { DataTransformerConfig, GrafanaTheme2, IconName, PanelData } from '@grafana/data';
|
||||||
import { SceneObjectBase, SceneComponentProps } from '@grafana/scenes';
|
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 { VizPanelManager } from '../VizPanelManager';
|
||||||
|
|
||||||
|
import { EmptyTransformationsMessage } from './EmptyTransformationsMessage';
|
||||||
import { PanelDataPaneTabState, PanelDataPaneTab } from './types';
|
import { PanelDataPaneTabState, PanelDataPaneTab } from './types';
|
||||||
|
|
||||||
interface PanelDataTransformationsTabState extends PanelDataPaneTabState {}
|
interface PanelDataTransformationsTabState extends PanelDataPaneTabState {}
|
||||||
@ -23,7 +29,7 @@ export class PanelDataTransformationsTab
|
|||||||
}
|
}
|
||||||
|
|
||||||
getItemsCount() {
|
getItemsCount() {
|
||||||
return 0;
|
return this.getDataTransformer().state.transformations.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(panelManager: VizPanelManager) {
|
constructor(panelManager: VizPanelManager) {
|
||||||
@ -32,15 +38,104 @@ export class PanelDataTransformationsTab
|
|||||||
this._panelManager = panelManager;
|
this._panelManager = panelManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
get panelManager() {
|
public getDataTransformer(): SceneDataTransformer {
|
||||||
return this._panelManager;
|
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>) {
|
export function PanelDataTransformationsTabRendered({ model }: SceneComponentProps<PanelDataTransformationsTab>) {
|
||||||
// const { dataRef } = model.useState();
|
const styles = useStyles2(getStyles);
|
||||||
// const dataObj = dataRef.resolve();
|
const { data, transformations: transformsWrongType } = model.getDataTransformer().useState();
|
||||||
// // const { transformations } = dataObj.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,
|
withTheme,
|
||||||
IconButton,
|
IconButton,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Box,
|
|
||||||
Text,
|
|
||||||
Stack,
|
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import config from 'app/core/config';
|
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 { PanelModel } from '../../state';
|
||||||
import { PanelNotSupported } from '../PanelEditor/PanelNotSupported';
|
import { PanelNotSupported } from '../PanelEditor/PanelNotSupported';
|
||||||
@ -258,36 +255,11 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
|
|||||||
|
|
||||||
renderEmptyMessage = () => {
|
renderEmptyMessage = () => {
|
||||||
return (
|
return (
|
||||||
<Box alignItems="center" padding={4}>
|
<EmptyTransformationsMessage
|
||||||
<Stack direction="column" alignItems="center" gap={2}>
|
onShowPicker={() => {
|
||||||
<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 });
|
this.setState({ showPicker: true });
|
||||||
}}
|
}}
|
||||||
data-testid={selectors.components.Transforms.addTransformationButton}
|
></EmptyTransformationsMessage>
|
||||||
>
|
|
||||||
Add transformation
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user