From 57ba8dc75da2ee4ef9ea910a2499008bc480e93a Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Wed, 24 Jan 2024 14:14:48 +0100 Subject: [PATCH] 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 --- .../src/selectors/components.ts | 1 + .../PanelDataTransformationsTab.test.tsx | 203 +++++++++++++++--- .../PanelDataTransformationsTab.tsx | 109 +++++++--- .../PanelDataPane/TransformationsDrawer.tsx | 91 ++++++++ .../panel-edit/VizPanelManager.test.tsx | 20 ++ .../panel-edit/VizPanelManager.tsx | 17 +- .../TransformationPickerNg.tsx | 26 ++- .../TransformationsEditor.tsx | 6 +- 8 files changed, 405 insertions(+), 68 deletions(-) create mode 100644 public/app/features/dashboard-scene/panel-edit/PanelDataPane/TransformationsDrawer.tsx diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 73f975940fd..5a772cab53a 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -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: { diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx index 1baa44f4c5c..fd6af7a8aa0 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx @@ -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(); 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(); await screen.findByText('1 - Add field from calculation'); }); + + it('shows show the transformation selection drawer', async () => { + const modelMock = createModelMock(mockData); + render(); + 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(); + 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(); + 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(); + 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(); + 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; +}; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx index 3b1ed5a141e..857eee7de72 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx @@ -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) { 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 {}}>; + const [drawerOpen, setDrawerOpen] = useState(false); + const [confirmModalOpen, setConfirmModalOpen] = useState(false); + + const openDrawer = () => setDrawerOpen(true); + const closeDrawer = () => setDrawerOpen(false); + + if (!data || !sourceData.data) { + return; } - if (!data) { - return; + const transformationsDrawer = ( + { + if (selected.value === undefined) { + return; + } + model.onChangeTransformations([...transformations, { id: selected.value, options: {} }]); + closeDrawer(); + }} + isOpen={drawerOpen} + series={data.series} + > + ); + + if (transformations.length < 1) { + return ( + <> + + {transformationsDrawer} + + ); } return ( <> - + - {}} - 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 ( - {}}> + {(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} diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/TransformationsDrawer.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/TransformationsDrawer.tsx new file mode 100644 index 00000000000..5e9e75dd394 --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/TransformationsDrawer.tsx @@ -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) => void; +} + +export function TransformationsDrawer(props: TransformationsDrawerProps) { + const { isOpen, series, onClose, onTransformationAdd } = props; + + const [drawerState, setDrawerState] = useState({ + search: '', + showIllustrations: true, + }); + + const onSearchChange = (e: FormEvent) => + 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}    + { + setDrawerState({ ...drawerState, ...{ search: '' } }); + }} + tooltip="Clear search" + /> + + ); + + if (!isOpen) { + return; + } + + return ( + } + selectedFilter={drawerState.selectedFilter} + onSearchChange={onSearchChange} + onSearchKeyDown={() => {}} + showIllustrations={drawerState.showIllustrations} + onShowIllustrationsChange={onShowIllustrationsChange} + onSelectedFilterChange={onSelectedFilterChange} + onClose={onClose} + /> + ); +} diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx index ddfd3598680..64b5abfc39a 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx @@ -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', () => { diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx index ba379d338b0..1be3094d4d2 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx @@ -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 { 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 { 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!; } diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx index c23909a5893..09304c25133 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx @@ -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) => void; onSearchChange: FormEventHandler; onSearchKeyDown: KeyboardEventHandler; + onClose?: () => void; noTransforms: boolean; // eslint-disable-next-line @typescript-eslint/no-explicit-any xforms: Array>; @@ -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 ( - setState({ showPicker: false })} title="Add another transformation"> + { + onClose && onClose(); + }} + title="Add another transformation" + >
Show images{' '} - setState({ showIllustrations: !showIllustrations })} /> + onShowIllustrationsChange && onShowIllustrationsChange(!showIllustrations)} + />
@@ -85,7 +99,7 @@ export function TransformationPickerNg(props: TransformationPickerNgProps) { return ( setState({ selectedFilter: slug })} + onClick={() => onSelectedFilterChange && onSelectedFilterChange(slug)} label={label} selected={selectedFilter === slug} /> diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx index 6a2666e5351..b1efc16b8d3 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx @@ -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 this.setState({ showPicker: false })} + onSelectedFilterChange={(filter) => this.setState({ selectedFilter: filter })} + onShowIllustrationsChange={(showIllustrations) => this.setState({ showIllustrations })} onSearchChange={this.onSearchChange} onSearchKeyDown={this.onSearchKeyDown} onTransformationAdd={this.onTransformationAdd}