mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Scenes: Add transformation flow for panel edit (#80738)
* Adding transformations works * Use source data as data input for transformation settings, add search box suffix * remove useCallback that are probably not needed, fix tests * remove unused import * add tests for adding and removing transformations * use view all constant * Add reordering functionality * Fix removing one transformation removes all consecutive transformations * use closeDrawer function * Add tests for changing transformations * Remove any --------- Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
parent
a06197188f
commit
57ba8dc75d
@ -334,6 +334,7 @@ export const Components = {
|
||||
searchInput: 'data-testid search transformations',
|
||||
noTransformationsMessage: 'data-testid no transformations message',
|
||||
addTransformationButton: 'data-testid add transformation button',
|
||||
removeAllTransformationsButton: 'data-testid remove all transformations button',
|
||||
},
|
||||
NavBar: {
|
||||
Configuration: {
|
||||
|
@ -1,54 +1,197 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { FieldType, LoadingState, TimeRange, standardTransformersRegistry, toDataFrame } from '@grafana/data';
|
||||
import {
|
||||
DataTransformerConfig,
|
||||
FieldType,
|
||||
LoadingState,
|
||||
PanelData,
|
||||
TimeRange,
|
||||
standardTransformersRegistry,
|
||||
toDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { SceneDataTransformer } from '@grafana/scenes';
|
||||
import { SceneDataTransformer, SceneQueryRunner } from '@grafana/scenes';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { getStandardTransformers } from 'app/features/transformers/standardTransformers';
|
||||
import { DashboardDataDTO } from 'app/types';
|
||||
|
||||
import { transformSaveModelToScene } from '../../serialization/transformSaveModelToScene';
|
||||
import { DashboardModelCompatibilityWrapper } from '../../utils/DashboardModelCompatibilityWrapper';
|
||||
import { findVizPanelByKey } from '../../utils/utils';
|
||||
import { VizPanelManager } from '../VizPanelManager';
|
||||
import { testDashboard } from '../testfiles/testDashboard';
|
||||
|
||||
import { PanelDataTransformationsTab, PanelDataTransformationsTabRendered } from './PanelDataTransformationsTab';
|
||||
|
||||
function createPanelManagerMock(sceneDataTransformer: SceneDataTransformer) {
|
||||
function createModelMock(
|
||||
panelData: PanelData,
|
||||
transformations?: DataTransformerConfig[],
|
||||
onChangeTransformationsMock?: Function
|
||||
) {
|
||||
return {
|
||||
getDataTransformer: () => sceneDataTransformer,
|
||||
getDataTransformer: () => new SceneDataTransformer({ data: panelData, transformations: transformations || [] }),
|
||||
getQueryRunner: () => new SceneQueryRunner({ queries: [], data: panelData }),
|
||||
onChangeTransformations: onChangeTransformationsMock,
|
||||
} as unknown as PanelDataTransformationsTab;
|
||||
}
|
||||
|
||||
const mockData = {
|
||||
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] },
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
describe('PanelDataTransformationsModel', () => {
|
||||
it('can change transformations', () => {
|
||||
const vizPanelManager = setupVizPanelManger('panel-1');
|
||||
const model = new PanelDataTransformationsTab(vizPanelManager);
|
||||
model.onChangeTransformations([{ id: 'calculateField', options: {} }]);
|
||||
expect(model.getDataTransformer().state.transformations).toEqual([{ id: 'calculateField', options: {} }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PanelDataTransformationsTab', () => {
|
||||
standardTransformersRegistry.setInit(getStandardTransformers);
|
||||
|
||||
it('renders empty message when there are no transformations', async () => {
|
||||
const modelMock = createPanelManagerMock(new SceneDataTransformer({ transformations: [] }));
|
||||
const modelMock = createModelMock({} as PanelData);
|
||||
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: {},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
const modelMock = createModelMock(mockData, [
|
||||
{
|
||||
id: 'calculateField',
|
||||
options: {},
|
||||
},
|
||||
]);
|
||||
render(<PanelDataTransformationsTabRendered model={modelMock}></PanelDataTransformationsTabRendered>);
|
||||
|
||||
await screen.findByText('1 - Add field from calculation');
|
||||
});
|
||||
|
||||
it('shows show the transformation selection drawer', async () => {
|
||||
const modelMock = createModelMock(mockData);
|
||||
render(<PanelDataTransformationsTabRendered model={modelMock}></PanelDataTransformationsTabRendered>);
|
||||
const addButton = await screen.findByTestId(selectors.components.Transforms.addTransformationButton);
|
||||
userEvent.click(addButton);
|
||||
await screen.findByTestId(selectors.components.Transforms.searchInput);
|
||||
});
|
||||
|
||||
it('adds a transformation when a transformation is clicked in the drawer and there are no previous transformations', async () => {
|
||||
const onChangeTransformation = jest.fn();
|
||||
const modelMock = createModelMock(mockData, [], onChangeTransformation);
|
||||
render(<PanelDataTransformationsTabRendered model={modelMock}></PanelDataTransformationsTabRendered>);
|
||||
const addButton = await screen.findByTestId(selectors.components.Transforms.addTransformationButton);
|
||||
await act(async () => {
|
||||
userEvent.click(addButton);
|
||||
});
|
||||
const transformationCard = await screen.findByTestId(
|
||||
selectors.components.TransformTab.newTransform('Add field from calculation')
|
||||
);
|
||||
const button = transformationCard.getElementsByTagName('button').item(0);
|
||||
|
||||
await userEvent.click(button!);
|
||||
|
||||
expect(onChangeTransformation).toHaveBeenCalledWith([{ id: 'calculateField', options: {} }]);
|
||||
});
|
||||
|
||||
it('adds a transformation when a transformation is clicked in the drawer and there are transformations', async () => {
|
||||
const onChangeTransformation = jest.fn();
|
||||
const modelMock = createModelMock(
|
||||
mockData,
|
||||
[
|
||||
{
|
||||
id: 'calculateField',
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
onChangeTransformation
|
||||
);
|
||||
render(<PanelDataTransformationsTabRendered model={modelMock}></PanelDataTransformationsTabRendered>);
|
||||
const addButton = await screen.findByTestId(selectors.components.Transforms.addTransformationButton);
|
||||
await act(async () => {
|
||||
userEvent.click(addButton);
|
||||
});
|
||||
const transformationCard = await screen.findByTestId(
|
||||
selectors.components.TransformTab.newTransform('Add field from calculation')
|
||||
);
|
||||
const button = transformationCard.getElementsByTagName('button').item(0);
|
||||
|
||||
await userEvent.click(button!);
|
||||
|
||||
expect(onChangeTransformation).toHaveBeenCalledWith([
|
||||
{ id: 'calculateField', options: {} },
|
||||
{ id: 'calculateField', options: {} },
|
||||
]);
|
||||
});
|
||||
|
||||
it('deletes all transformations', async () => {
|
||||
const onChangeTransformation = jest.fn();
|
||||
const modelMock = createModelMock(
|
||||
mockData,
|
||||
[
|
||||
{
|
||||
id: 'calculateField',
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
onChangeTransformation
|
||||
);
|
||||
render(<PanelDataTransformationsTabRendered model={modelMock}></PanelDataTransformationsTabRendered>);
|
||||
const removeButton = await screen.findByTestId(selectors.components.Transforms.removeAllTransformationsButton);
|
||||
await act(async () => {
|
||||
userEvent.click(removeButton);
|
||||
});
|
||||
const confirmButton = await screen.findByTestId(selectors.pages.ConfirmModal.delete);
|
||||
await act(async () => {
|
||||
await userEvent.click(confirmButton);
|
||||
});
|
||||
|
||||
expect(onChangeTransformation).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('can filter transformations in the drawer', async () => {
|
||||
const modelMock = createModelMock(mockData);
|
||||
render(<PanelDataTransformationsTabRendered model={modelMock}></PanelDataTransformationsTabRendered>);
|
||||
const addButton = await screen.findByTestId(selectors.components.Transforms.addTransformationButton);
|
||||
await act(async () => {
|
||||
await userEvent.click(addButton);
|
||||
});
|
||||
|
||||
const searchInput = await screen.findByTestId(selectors.components.Transforms.searchInput);
|
||||
|
||||
await screen.findByTestId(selectors.components.TransformTab.newTransform('Reduce'));
|
||||
|
||||
await userEvent.type(searchInput, 'add field');
|
||||
|
||||
await screen.findByTestId(selectors.components.TransformTab.newTransform('Add field from calculation'));
|
||||
const reduce = await screen.queryByTestId(selectors.components.TransformTab.newTransform('Reduce'));
|
||||
expect(reduce).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
const setupVizPanelManger = (panelId: string) => {
|
||||
const scene = transformSaveModelToScene({ dashboard: testDashboard as unknown as DashboardDataDTO, meta: {} });
|
||||
const panel = findVizPanelByKey(scene, panelId)!;
|
||||
|
||||
const vizPanelManager = new VizPanelManager(panel.clone());
|
||||
|
||||
// The following happens on DahsboardScene activation. For the needs of this test this activation aint needed hence we hand-call it
|
||||
// @ts-expect-error
|
||||
getDashboardSrv().setCurrent(new DashboardModelCompatibilityWrapper(scene));
|
||||
|
||||
return vizPanelManager;
|
||||
};
|
||||
|
@ -1,16 +1,17 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
import React, { useState } from 'react';
|
||||
import { DragDropContext, DropResult, Droppable } from 'react-beautiful-dnd';
|
||||
|
||||
import { DataTransformerConfig, GrafanaTheme2, IconName, PanelData } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { SceneObjectBase, SceneComponentProps, SceneDataTransformer } from '@grafana/scenes';
|
||||
import { SceneObjectBase, SceneComponentProps, SceneDataTransformer, SceneQueryRunner } 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 { TransformationsDrawer } from './TransformationsDrawer';
|
||||
import { PanelDataPaneTabState, PanelDataPaneTab } from './types';
|
||||
|
||||
interface PanelDataTransformationsTabState extends PanelDataPaneTabState {}
|
||||
@ -29,7 +30,7 @@ export class PanelDataTransformationsTab
|
||||
}
|
||||
|
||||
getItemsCount() {
|
||||
return this.getDataTransformer().state.transformations.length;
|
||||
return this._panelManager.dataTransformer.state.transformations.length;
|
||||
}
|
||||
|
||||
constructor(panelManager: VizPanelManager) {
|
||||
@ -38,60 +39,94 @@ export class PanelDataTransformationsTab
|
||||
this._panelManager = 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 getQueryRunner(): SceneQueryRunner {
|
||||
return this._panelManager.queryRunner;
|
||||
}
|
||||
|
||||
public changeTransformations(transformations: DataTransformerConfig[]) {
|
||||
const dataProvider = this.getDataTransformer();
|
||||
dataProvider.setState({ transformations });
|
||||
dataProvider.reprocessTransformations();
|
||||
public getDataTransformer(): SceneDataTransformer {
|
||||
return this._panelManager.dataTransformer;
|
||||
}
|
||||
|
||||
public onChangeTransformations(transformations: DataTransformerConfig[]) {
|
||||
this._panelManager.changeTransformations(transformations);
|
||||
}
|
||||
}
|
||||
|
||||
export function PanelDataTransformationsTabRendered({ model }: SceneComponentProps<PanelDataTransformationsTab>) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const sourceData = model.getQueryRunner().useState();
|
||||
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>;
|
||||
const [drawerOpen, setDrawerOpen] = useState<boolean>(false);
|
||||
const [confirmModalOpen, setConfirmModalOpen] = useState<boolean>(false);
|
||||
|
||||
const openDrawer = () => setDrawerOpen(true);
|
||||
const closeDrawer = () => setDrawerOpen(false);
|
||||
|
||||
if (!data || !sourceData.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
const transformationsDrawer = (
|
||||
<TransformationsDrawer
|
||||
onClose={closeDrawer}
|
||||
onTransformationAdd={(selected) => {
|
||||
if (selected.value === undefined) {
|
||||
return;
|
||||
}
|
||||
model.onChangeTransformations([...transformations, { id: selected.value, options: {} }]);
|
||||
closeDrawer();
|
||||
}}
|
||||
isOpen={drawerOpen}
|
||||
series={data.series}
|
||||
></TransformationsDrawer>
|
||||
);
|
||||
|
||||
if (transformations.length < 1) {
|
||||
return (
|
||||
<>
|
||||
<EmptyTransformationsMessage onShowPicker={openDrawer}></EmptyTransformationsMessage>
|
||||
{transformationsDrawer}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TransformationsEditor data={data} transformations={transformations} model={model} />
|
||||
<TransformationsEditor data={sourceData.data} transformations={transformations} model={model} />
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
icon="plus"
|
||||
variant="secondary"
|
||||
onClick={() => {}}
|
||||
onClick={openDrawer}
|
||||
data-testid={selectors.components.Transforms.addTransformationButton}
|
||||
>
|
||||
Add another transformation
|
||||
</Button>
|
||||
<Button className={styles.removeAll} icon="times" variant="secondary" onClick={() => {}}>
|
||||
<Button
|
||||
data-testid={selectors.components.Transforms.removeAllTransformationsButton}
|
||||
className={styles.removeAll}
|
||||
icon="times"
|
||||
variant="secondary"
|
||||
onClick={() => setConfirmModalOpen(true)}
|
||||
>
|
||||
Delete all transformations
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ConfirmModal
|
||||
isOpen={false}
|
||||
isOpen={confirmModalOpen}
|
||||
title="Delete all transformations?"
|
||||
body="By deleting all transformations, you will go back to the main selection screen."
|
||||
confirmText="Delete all"
|
||||
onConfirm={() => {}}
|
||||
onDismiss={() => {}}
|
||||
onConfirm={() => {
|
||||
model.onChangeTransformations([]);
|
||||
setConfirmModalOpen(false);
|
||||
}}
|
||||
onDismiss={() => setConfirmModalOpen(false)}
|
||||
/>
|
||||
{transformationsDrawer}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -105,8 +140,24 @@ interface TransformationEditorProps {
|
||||
function TransformationsEditor({ transformations, model, data }: TransformationEditorProps) {
|
||||
const transformationEditorRows = transformations.map((t, i) => ({ id: `${i} - ${t.id}`, transformation: t }));
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result || !result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startIndex = result.source.index;
|
||||
const endIndex = result.destination.index;
|
||||
if (startIndex === endIndex) {
|
||||
return;
|
||||
}
|
||||
const update = Array.from(transformationEditorRows);
|
||||
const [removed] = update.splice(startIndex, 1);
|
||||
update.splice(endIndex, 0, removed);
|
||||
model.onChangeTransformations(update.map((t) => t.transformation));
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={() => {}}>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="transformations-list" direction="vertical">
|
||||
{(provided) => {
|
||||
return (
|
||||
@ -115,12 +166,12 @@ function TransformationsEditor({ transformations, model, data }: TransformationE
|
||||
onChange={(index, transformation) => {
|
||||
const newTransformations = transformations.slice();
|
||||
newTransformations[index] = transformation;
|
||||
model.changeTransformations(newTransformations);
|
||||
model.onChangeTransformations(newTransformations);
|
||||
}}
|
||||
onRemove={(index) => {
|
||||
const newTransformations = transformations.slice();
|
||||
newTransformations.splice(index);
|
||||
model.changeTransformations(newTransformations);
|
||||
newTransformations.splice(index, 1);
|
||||
model.onChangeTransformations(newTransformations);
|
||||
}}
|
||||
configs={transformationEditorRows}
|
||||
data={data}
|
||||
|
@ -0,0 +1,91 @@
|
||||
import React, { FormEvent, useMemo, useState } from 'react';
|
||||
|
||||
import { DataFrame, SelectableValue, standardTransformersRegistry } from '@grafana/data';
|
||||
import { IconButton } from '@grafana/ui';
|
||||
import { TransformationPickerNg } from 'app/features/dashboard/components/TransformationsEditor/TransformationPickerNg';
|
||||
import {
|
||||
FilterCategory,
|
||||
VIEW_ALL_VALUE,
|
||||
} from 'app/features/dashboard/components/TransformationsEditor/TransformationsEditor';
|
||||
|
||||
interface DrawerState {
|
||||
search: string;
|
||||
showIllustrations: boolean;
|
||||
selectedFilter?: FilterCategory;
|
||||
}
|
||||
|
||||
interface TransformationsDrawerProps {
|
||||
series: DataFrame[];
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onTransformationAdd: (selectedItem: SelectableValue<string>) => void;
|
||||
}
|
||||
|
||||
export function TransformationsDrawer(props: TransformationsDrawerProps) {
|
||||
const { isOpen, series, onClose, onTransformationAdd } = props;
|
||||
|
||||
const [drawerState, setDrawerState] = useState<DrawerState>({
|
||||
search: '',
|
||||
showIllustrations: true,
|
||||
});
|
||||
|
||||
const onSearchChange = (e: FormEvent<HTMLInputElement>) =>
|
||||
setDrawerState({ ...drawerState, ...{ search: e.currentTarget.value } });
|
||||
|
||||
const onShowIllustrationsChange = (showIllustrations: boolean): void =>
|
||||
setDrawerState({ ...drawerState, ...{ showIllustrations } });
|
||||
|
||||
const onSelectedFilterChange = (selectedFilter: FilterCategory): void =>
|
||||
setDrawerState({ ...drawerState, ...{ selectedFilter } });
|
||||
|
||||
const allTransformations = useMemo(
|
||||
() => standardTransformersRegistry.list().sort((a, b) => (a.name > b.name ? 1 : b.name > a.name ? -1 : 0)),
|
||||
[]
|
||||
);
|
||||
|
||||
const transformations = allTransformations.filter((t) => {
|
||||
if (
|
||||
drawerState.selectedFilter &&
|
||||
drawerState.selectedFilter !== VIEW_ALL_VALUE &&
|
||||
!t.categories?.has(drawerState.selectedFilter)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return t.name.toLocaleLowerCase().includes(drawerState.search.toLocaleLowerCase());
|
||||
});
|
||||
|
||||
const searchBoxSuffix = (
|
||||
<>
|
||||
{transformations.length} / {allTransformations.length}
|
||||
<IconButton
|
||||
name="times"
|
||||
onClick={() => {
|
||||
setDrawerState({ ...drawerState, ...{ search: '' } });
|
||||
}}
|
||||
tooltip="Clear search"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<TransformationPickerNg
|
||||
data={series}
|
||||
onTransformationAdd={onTransformationAdd}
|
||||
xforms={transformations}
|
||||
search={drawerState.search}
|
||||
noTransforms={false}
|
||||
suffix={drawerState.search !== '' ? searchBoxSuffix : <></>}
|
||||
selectedFilter={drawerState.selectedFilter}
|
||||
onSearchChange={onSearchChange}
|
||||
onSearchKeyDown={() => {}}
|
||||
showIllustrations={drawerState.showIllustrations}
|
||||
onShowIllustrationsChange={onShowIllustrationsChange}
|
||||
onSelectedFilterChange={onSelectedFilterChange}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
@ -503,6 +503,26 @@ describe('VizPanelManager', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('change transformations', () => {
|
||||
it('should update and reprocess transformations', () => {
|
||||
const { scene, panel } = setupTest('panel-3');
|
||||
scene.setState({
|
||||
editPanel: buildPanelEditScene(panel),
|
||||
});
|
||||
|
||||
const vizPanelManager = scene.state.editPanel!.state.panelRef.resolve();
|
||||
vizPanelManager.activate();
|
||||
vizPanelManager.state.panel.state.$data?.activate();
|
||||
|
||||
const reprocessMock = jest.fn();
|
||||
vizPanelManager.dataTransformer.reprocessTransformations = reprocessMock;
|
||||
vizPanelManager.changeTransformations([{ id: 'calculateField', options: {} }]);
|
||||
|
||||
expect(reprocessMock).toHaveBeenCalledTimes(1);
|
||||
expect(vizPanelManager.dataTransformer.state.transformations).toEqual([{ id: 'calculateField', options: {} }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change queries', () => {
|
||||
describe('plugin queries', () => {
|
||||
it('should update queries', () => {
|
||||
|
@ -20,8 +20,9 @@ import {
|
||||
SceneQueryRunner,
|
||||
sceneGraph,
|
||||
SceneDataProvider,
|
||||
SceneDataTransformer,
|
||||
} from '@grafana/scenes';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { DataQuery, DataTransformerConfig } from '@grafana/schema';
|
||||
import { getPluginVersion } from 'app/features/dashboard/state/PanelModel';
|
||||
import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils';
|
||||
import { updateQueries } from 'app/features/query/state/updateQueries';
|
||||
@ -217,6 +218,12 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
|
||||
runner.setState({ queries });
|
||||
}
|
||||
|
||||
public changeTransformations(transformations: DataTransformerConfig[]) {
|
||||
const dataprovider = this.dataTransformer;
|
||||
dataprovider.setState({ transformations });
|
||||
dataprovider.reprocessTransformations();
|
||||
}
|
||||
|
||||
public inspectPanel() {
|
||||
const panel = this.state.panel;
|
||||
const panelId = getPanelIdForVizPanel(panel);
|
||||
@ -237,6 +244,14 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
|
||||
return runner;
|
||||
}
|
||||
|
||||
get dataTransformer(): SceneDataTransformer {
|
||||
const provider = this.state.panel.state.$data;
|
||||
if (!provider || !(provider instanceof SceneDataTransformer)) {
|
||||
throw new Error('Could not find SceneDataTransformer for panel');
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
get panelData(): SceneDataProvider {
|
||||
return this.state.panel.state.$data!;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
TransformationApplicabilityLevels,
|
||||
GrafanaTheme2,
|
||||
standardTransformersRegistry,
|
||||
SelectableValue,
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Card, Drawer, FilterPill, IconButton, Input, Switch, useStyles2 } from '@grafana/ui';
|
||||
@ -26,10 +27,10 @@ const filterCategoriesLabels: Array<[FilterCategory, string]> = [
|
||||
];
|
||||
|
||||
interface TransformationPickerNgProps {
|
||||
onTransformationAdd: Function;
|
||||
setState: Function;
|
||||
onTransformationAdd: (selectedItem: SelectableValue<string>) => void;
|
||||
onSearchChange: FormEventHandler<HTMLInputElement>;
|
||||
onSearchKeyDown: KeyboardEventHandler<HTMLInputElement>;
|
||||
onClose?: () => void;
|
||||
noTransforms: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
xforms: Array<TransformerRegistryItem<any>>;
|
||||
@ -37,6 +38,8 @@ interface TransformationPickerNgProps {
|
||||
suffix: ReactNode;
|
||||
data: DataFrame[];
|
||||
showIllustrations?: boolean;
|
||||
onShowIllustrationsChange?: (showIllustrations: boolean) => void;
|
||||
onSelectedFilterChange?: (category: FilterCategory) => void;
|
||||
selectedFilter?: FilterCategory;
|
||||
}
|
||||
|
||||
@ -44,7 +47,6 @@ export function TransformationPickerNg(props: TransformationPickerNgProps) {
|
||||
const styles = useStyles2(getTransformationPickerStyles);
|
||||
const {
|
||||
suffix,
|
||||
setState,
|
||||
xforms,
|
||||
search,
|
||||
onSearchChange,
|
||||
@ -53,6 +55,9 @@ export function TransformationPickerNg(props: TransformationPickerNgProps) {
|
||||
onTransformationAdd,
|
||||
selectedFilter,
|
||||
data,
|
||||
onClose,
|
||||
onShowIllustrationsChange,
|
||||
onSelectedFilterChange,
|
||||
} = props;
|
||||
|
||||
// Use a callback ref to call "click" on the search input
|
||||
@ -62,7 +67,13 @@ export function TransformationPickerNg(props: TransformationPickerNgProps) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Drawer size="md" onClose={() => setState({ showPicker: false })} title="Add another transformation">
|
||||
<Drawer
|
||||
size="md"
|
||||
onClose={() => {
|
||||
onClose && onClose();
|
||||
}}
|
||||
title="Add another transformation"
|
||||
>
|
||||
<div className={styles.searchWrapper}>
|
||||
<Input
|
||||
data-testid={selectors.components.Transforms.searchInput}
|
||||
@ -76,7 +87,10 @@ export function TransformationPickerNg(props: TransformationPickerNgProps) {
|
||||
/>
|
||||
<div className={styles.showImages}>
|
||||
<span className={styles.illustationSwitchLabel}>Show images</span>{' '}
|
||||
<Switch value={showIllustrations} onChange={() => setState({ showIllustrations: !showIllustrations })} />
|
||||
<Switch
|
||||
value={showIllustrations}
|
||||
onChange={() => onShowIllustrationsChange && onShowIllustrationsChange(!showIllustrations)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -85,7 +99,7 @@ export function TransformationPickerNg(props: TransformationPickerNgProps) {
|
||||
return (
|
||||
<FilterPill
|
||||
key={slug}
|
||||
onClick={() => setState({ selectedFilter: slug })}
|
||||
onClick={() => onSelectedFilterChange && onSelectedFilterChange(slug)}
|
||||
label={label}
|
||||
selected={selectedFilter === slug}
|
||||
/>
|
||||
|
@ -37,7 +37,7 @@ interface TransformationsEditorProps extends Themeable {
|
||||
panel: PanelModel;
|
||||
}
|
||||
|
||||
const VIEW_ALL_VALUE = 'viewAll';
|
||||
export const VIEW_ALL_VALUE = 'viewAll';
|
||||
export type viewAllType = 'viewAll';
|
||||
export type FilterCategory = TransformerCategory | viewAllType;
|
||||
|
||||
@ -359,7 +359,9 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
|
||||
search={search}
|
||||
suffix={suffix}
|
||||
xforms={xforms}
|
||||
setState={this.setState.bind(this)}
|
||||
onClose={() => this.setState({ showPicker: false })}
|
||||
onSelectedFilterChange={(filter) => this.setState({ selectedFilter: filter })}
|
||||
onShowIllustrationsChange={(showIllustrations) => this.setState({ showIllustrations })}
|
||||
onSearchChange={this.onSearchChange}
|
||||
onSearchKeyDown={this.onSearchKeyDown}
|
||||
onTransformationAdd={this.onTransformationAdd}
|
||||
|
Loading…
Reference in New Issue
Block a user