From 5099e882276227f4b4c8e695f35156b7873d81b3 Mon Sep 17 00:00:00 2001 From: Ludovic Viaud Date: Wed, 12 Jul 2023 18:35:49 +0200 Subject: [PATCH] Transformation redesign (#70834) Transformation redesign --- .betterer.results | 10 +- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + .../src/selectors/components.ts | 11 +- .../src/components/FilterPill/FilterPill.tsx | 1 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + .../PanelEditor/PanelEditorTabs.tsx | 7 +- .../TransformationEditor.tsx | 4 +- .../TransformationOperationRow.tsx | 31 +- .../TransformationsEditor.test.tsx | 50 ++- .../TransformationsEditor.tsx | 399 ++++++++++++++++-- .../plugins/components/PluginStateInfo.tsx | 11 +- 14 files changed, 475 insertions(+), 63 deletions(-) diff --git a/.betterer.results b/.betterer.results index d18fb807162..74fadb4922c 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2178,16 +2178,14 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/features/dashboard/components/TransformationsEditor/TransformationEditor.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"], - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "2"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"], - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "2"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "4"] + [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ], "public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 3b35e348ec3..f7dc58851fc 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -124,6 +124,7 @@ Experimental features might be changed or removed without prior notice. | `prometheusIncrementalQueryInstrumentation` | Adds RudderStack events to incremental queries | | `logsExploreTableVisualisation` | A table visualisation for logs in Explore | | `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | +| `transformationsRedesign` | Enables the transformations redesign | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index fb31e139fc7..e470a82ca52 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -111,4 +111,5 @@ export interface FeatureToggles { prometheusIncrementalQueryInstrumentation?: boolean; logsExploreTableVisualisation?: boolean; awsDatasourcesTempCredentials?: boolean; + transformationsRedesign?: boolean; } diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 05d789ae69e..572988f638a 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -208,13 +208,13 @@ export const Components = { alertV2: (severity: string) => `data-testid Alert ${severity}`, }, TransformTab: { - content: 'Transform editor tab content', - newTransform: (name: string) => `New transform ${name}`, - transformationEditor: (name: string) => `Transformation editor ${name}`, - transformationEditorDebugger: (name: string) => `Transformation editor debugger ${name}`, + content: 'data-testid Transform editor tab content', + newTransform: (name: string) => `data-testid New transform ${name}`, + transformationEditor: (name: string) => `data-testid Transformation editor ${name}`, + transformationEditorDebugger: (name: string) => `data-testid Transformation editor debugger ${name}`, }, Transforms: { - card: (name: string) => `New transform ${name}`, + card: (name: string) => `data-testid New transform ${name}`, Reduce: { modeLabel: 'Transform mode label', calculationsLabel: 'Transform calculations label', @@ -241,6 +241,7 @@ export const Components = { }, }, searchInput: 'search transformations', + addTransformationButton: 'data-testid add transformation button', }, NavBar: { Configuration: { diff --git a/packages/grafana-ui/src/components/FilterPill/FilterPill.tsx b/packages/grafana-ui/src/components/FilterPill/FilterPill.tsx index 52f37c7d869..c047112c322 100644 --- a/packages/grafana-ui/src/components/FilterPill/FilterPill.tsx +++ b/packages/grafana-ui/src/components/FilterPill/FilterPill.tsx @@ -41,6 +41,7 @@ const getStyles = (theme: GrafanaTheme2) => { height: 32px; position: relative; border: 1px solid ${theme.colors.background.secondary}; + white-space: nowrap; &:hover { background: ${theme.colors.action.hover}; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 535790a5be4..6c2c2995a77 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -634,5 +634,12 @@ var ( Stage: FeatureStageExperimental, Owner: awsDatasourcesSquad, }, + { + Name: "transformationsRedesign", + Description: "Enables the transformations redesign", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaObservabilityMetricsSquad, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 8334b67b32c..05fcc46eea3 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -92,3 +92,4 @@ vizAndWidgetSplit,experimental,@grafana/dashboards-squad,false,false,false,true prometheusIncrementalQueryInstrumentation,experimental,@grafana/observability-metrics,false,false,false,true logsExploreTableVisualisation,experimental,@grafana/observability-logs,false,false,false,true awsDatasourcesTempCredentials,experimental,@grafana/aws-datasources,false,false,false,false +transformationsRedesign,experimental,@grafana/observability-metrics,false,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 93a768fd187..53e962cc790 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -378,4 +378,8 @@ const ( // FlagAwsDatasourcesTempCredentials // Support temporary security credentials in AWS plugins for Grafana Cloud customers FlagAwsDatasourcesTempCredentials = "awsDatasourcesTempCredentials" + + // FlagTransformationsRedesign + // Enables the transformations redesign + FlagTransformationsRedesign = "transformationsRedesign" ) diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx index 85d0274fb69..01d124c602a 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx @@ -28,8 +28,13 @@ export const PanelEditorTabs = React.memo(({ panel, dashboard, tabs, onChangeTab const instrumentedOnChangeTab = useCallback( (tab: PanelEditorTab) => { + let eventName = 'panel_editor_tabs_changed'; + if (config.featureToggles.transformationsRedesign) { + eventName = 'transformations_redesign_' + eventName; + } + if (!tab.active) { - reportInteraction('panel_editor_tabs_changed', { tab_id: tab.id }); + reportInteraction(eventName, { tab_id: tab.id }); } onChangeTab(tab); diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationEditor.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationEditor.tsx index 26fe38488a2..833550b632a 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationEditor.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationEditor.tsx @@ -74,12 +74,12 @@ export const TransformationEditor = ({ ); return ( -
+
{editor} {debugMode && (
Transformation input data
diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx index dd4cde96514..b423c811502 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx @@ -3,7 +3,7 @@ import { useToggle } from 'react-use'; import { DataFrame, DataTransformerConfig, TransformerRegistryItem, FrameMatcherID } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; -import { HorizontalGroup } from '@grafana/ui'; +import { ConfirmModal, HorizontalGroup } from '@grafana/ui'; import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp'; import { QueryOperationAction, @@ -13,6 +13,7 @@ import { QueryOperationRow, QueryOperationRowRenderProps, } from 'app/core/components/QueryOperationRow/QueryOperationRow'; +import config from 'app/core/config'; import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo'; import { TransformationEditor } from './TransformationEditor'; @@ -38,6 +39,7 @@ export const TransformationOperationRow = ({ uiConfig, onChange, }: TransformationOperationRowProps) => { + const [showDeleteModal, setShowDeleteModal] = useToggle(false); const [showDebug, toggleDebug] = useToggle(false); const [showHelp, toggleHelp] = useToggle(false); const disabled = !!configs[index].transformation.disabled; @@ -73,7 +75,12 @@ export const TransformationOperationRow = ({ const instrumentToggleCallback = useCallback( (callback: (e: React.MouseEvent) => void, toggleId: string, active: boolean | undefined) => (e: React.MouseEvent) => { - reportInteraction('panel_editor_tabs_transformations_toggle', { + let eventName = 'panel_editor_tabs_transformations_toggle'; + if (config.featureToggles.transformationsRedesign) { + eventName = 'transformations_redesign_' + eventName; + } + + reportInteraction(eventName, { action: active ? 'off' : 'on', toggleId, transformationId: configs[index].transformation.id, @@ -115,7 +122,25 @@ export const TransformationOperationRow = ({ onClick={instrumentToggleCallback(() => onDisableToggle(index), 'disabled', disabled)} active={disabled} /> - onRemove(index)} /> + (config.featureToggles.transformationsRedesign ? setShowDeleteModal(true) : onRemove(index))} + /> + + {config.featureToggles.transformationsRedesign && ( + { + setShowDeleteModal(false); + onRemove(index); + }} + onDismiss={() => setShowDeleteModal(false)} + /> + )} ); }; diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.test.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.test.tsx index 83586953eb1..16aa54cf8ce 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.test.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.test.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { DataTransformerConfig, standardTransformersRegistry } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; +import config from 'app/core/config'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; import { PanelModel } from '../../state'; @@ -20,30 +21,43 @@ describe('TransformationsEditor', () => { standardTransformersRegistry.setInit(getStandardTransformers); describe('when no transformations configured', () => { - it('renders transformations selection list', () => { + function renderList() { setup(); - const cards = screen.getAllByLabelText(/^New transform/i); + const cards = screen.getAllByTestId(/New transform/i); expect(cards.length).toEqual(standardTransformersRegistry.list().length); + } + + it('renders transformations selection list', renderList); + it('renders transformations selection list with transformationsRedesign feature toggled on', () => { + config.featureToggles.transformationsRedesign = true; + renderList(); + config.featureToggles.transformationsRedesign = false; }); }); describe('when transformations configured', () => { - it('renders transformation editors', () => { + function renderEditors() { setup([ { id: 'reduce', options: {}, }, ]); - const editors = screen.getAllByLabelText(/^Transformation editor/); + const editors = screen.getAllByTestId(/Transformation editor/); expect(editors).toHaveLength(1); + } + + it('renders transformation editors', renderEditors); + it('renders transformation editors with transformationsRedesign feature toggled on', () => { + config.featureToggles.transformationsRedesign = true; + renderEditors(); + config.featureToggles.transformationsRedesign = false; }); }); describe('when Add transformation clicked', () => { - it('renders transformations picker', async () => { - const buttonLabel = 'Add transformation'; + async function renderPicker() { setup([ { id: 'reduce', @@ -51,17 +65,24 @@ describe('TransformationsEditor', () => { }, ]); - const addTransformationButton = screen.getByText(buttonLabel); + const addTransformationButton = screen.getByTestId(selectors.components.Transforms.addTransformationButton); await userEvent.click(addTransformationButton); - const search = screen.getByLabelText(selectors.components.Transforms.searchInput); + const search = screen.getByTestId(selectors.components.Transforms.searchInput); expect(search).toBeDefined(); + } + + it('renders transformations picker', renderPicker); + it('renders transformation picker with transformationsRedesign feature toggled on', async () => { + config.featureToggles.transformationsRedesign = true; + await renderPicker(); + config.featureToggles.transformationsRedesign = false; }); }); describe('actions', () => { describe('debug', () => { - it('should show/hide debugger', async () => { + async function showHideDebugger() { setup([ { id: 'reduce', @@ -70,12 +91,19 @@ describe('TransformationsEditor', () => { ]); const debuggerSelector = selectors.components.TransformTab.transformationEditorDebugger('Reduce'); - expect(screen.queryByLabelText(debuggerSelector)).toBeNull(); + expect(screen.queryByTestId(debuggerSelector)).toBeNull(); const debugButton = screen.getByLabelText(selectors.components.QueryEditorRow.actionButton('Debug')); await userEvent.click(debugButton); - expect(screen.getByLabelText(debuggerSelector)).toBeInTheDocument(); + expect(screen.getByTestId(debuggerSelector)).toBeInTheDocument(); + } + + it('should show/hide debugger', showHideDebugger); + it('renders transformation editors with transformationsRedesign feature toggled on', async () => { + config.featureToggles.transformationsRedesign = true; + await showHideDebugger(); + config.featureToggles.transformationsRedesign = false; }); }); }); diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx index 4b9cdcb9bfd..f3b7f49da1a 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx @@ -12,25 +12,33 @@ import { SelectableValue, standardTransformersRegistry, TransformerRegistryItem, + TransformerCategory, + DataTransformerID, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { reportInteraction } from '@grafana/runtime'; import { Alert, Button, + ConfirmModal, Container, CustomScrollbar, + FilterPill, Themeable, VerticalGroup, withTheme, Input, + Icon, IconButton, useStyles2, Card, + Switch, } from '@grafana/ui'; import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider'; +import config from 'app/core/config'; import { getDocsLink } from 'app/core/utils/docsLinks'; import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo'; +import { categoriesLabels } from 'app/features/transformers/utils'; import { AppNotificationSeverity } from '../../../../types'; import { PanelModel } from '../../state'; @@ -45,11 +53,26 @@ interface TransformationsEditorProps extends Themeable { panel: PanelModel; } +type viewAllType = 'viewAll'; +const viewAllValue = 'viewAll'; +const viewAllLabel = 'View all'; + +type FilterCategory = TransformerCategory | viewAllType; + +const filterCategoriesLabels: Array<[FilterCategory, string]> = [ + [viewAllValue, viewAllLabel], + ...(Object.entries(categoriesLabels) as Array<[FilterCategory, string]>), +]; + interface State { data: DataFrame[]; transformations: TransformationsEditorTransformation[]; search: string; showPicker?: boolean; + scrollTop?: number; + showRemoveAllModal?: boolean; + selectedFilter?: FilterCategory; + showIllustrations?: boolean; } class UnThemedTransformationsEditor extends React.PureComponent { @@ -67,6 +90,8 @@ class UnThemedTransformationsEditor extends React.PureComponent, prevState: Readonly): void { + if (config.featureToggles.transformationsRedesign) { + const prevHasTransforms = prevState.transformations.length > 0; + const prevShowPicker = !prevHasTransforms || prevState.showPicker; + + const currentHasTransforms = this.state.transformations.length > 0; + const currentShowPicker = !currentHasTransforms || this.state.showPicker; + + if (prevShowPicker !== currentShowPicker) { + // kindOfZero will be a random number between 0 and 0.5. It will be rounded to 0 by the scrollable component. + // We cannot always use 0 as it will not trigger a rerender of the scrollable component consistently + // due to React changes detection algo. + const kindOfZero = Math.random() / 2; + + this.setState({ scrollTop: currentShowPicker ? kindOfZero : Number.MAX_SAFE_INTEGER }); + } + } + } + onChange(transformations: TransformationsEditorTransformation[]) { this.setState({ transformations }); this.props.panel.setTransformations(transformations.map((t) => t.transformation)); @@ -145,7 +189,12 @@ class UnThemedTransformationsEditor extends React.PureComponent) => { - reportInteraction('panel_editor_tabs_transformations_management', { + let eventName = 'panel_editor_tabs_transformations_management'; + if (config.featureToggles.transformationsRedesign) { + eventName = 'transformations_redesign_' + eventName; + } + + reportInteraction(eventName, { action: 'add', transformationId: selectable.value, }); @@ -165,21 +214,31 @@ class UnThemedTransformationsEditor extends React.PureComponent { + onTransformationChange = (idx: number, dataConfig: DataTransformerConfig) => { const { transformations } = this.state; const next = Array.from(transformations); - reportInteraction('panel_editor_tabs_transformations_management', { + let eventName = 'panel_editor_tabs_transformations_management'; + if (config.featureToggles.transformationsRedesign) { + eventName = 'transformations_redesign_' + eventName; + } + + reportInteraction(eventName, { action: 'change', transformationId: next[idx].transformation.id, }); - next[idx].transformation = config; + next[idx].transformation = dataConfig; this.onChange(next); }; onTransformationRemove = (idx: number) => { const { transformations } = this.state; const next = Array.from(transformations); - reportInteraction('panel_editor_tabs_transformations_management', { + let eventName = 'panel_editor_tabs_transformations_management'; + if (config.featureToggles.transformationsRedesign) { + eventName = 'transformations_redesign_' + eventName; + } + + reportInteraction(eventName, { action: 'remove', transformationId: next[idx].transformation.id, }); @@ -187,6 +246,11 @@ class UnThemedTransformationsEditor extends React.PureComponent { + this.onChange([]); + this.setState({ showRemoveAllModal: false }); + }; + onDragEnd = (result: DropResult) => { const { transformations } = this.state; @@ -230,10 +294,20 @@ class UnThemedTransformationsEditor extends React.PureComponent (a.name > b.name ? 1 : b.name > a.name ? -1 : 0)); + if (this.state.selectedFilter !== viewAllValue) { + xforms = xforms.filter( + (t) => + t.categories && + this.state.selectedFilter && + t.categories.has(this.state.selectedFilter as TransformerCategory) + ); + } + if (search) { const lower = search.toLowerCase(); const filtered = xforms.filter((t) => { @@ -272,6 +346,107 @@ class UnThemedTransformationsEditor extends React.PureComponent ( + <> + {config.featureToggles.transformationsRedesign && ( + <> + {!noTransforms && ( + + )} +
+ + Transformations{' '} + + +  allow you to manipulate your data before a visualization is applied. +
+ + )} + + {!config.featureToggles.transformationsRedesign && ( + + )} + + {!config.featureToggles.transformationsRedesign && + xforms.map((t) => { + return ( + { + this.onTransformationAdd({ value: t.id }); + }} + /> + ); + })} + + {config.featureToggles.transformationsRedesign && ( +
+ +
+ Show images{' '} + this.setState({ showIllustrations: !this.state.showIllustrations })} + /> +
+
+ )} + + {config.featureToggles.transformationsRedesign && ( +
+ {filterCategoriesLabels.map(([slug, label]) => { + return ( + this.setState({ selectedFilter: slug })} + label={label} + selected={this.state.selectedFilter === slug} + /> + ); + })} +
+ )} + + {config.featureToggles.transformationsRedesign && ( + { + this.onTransformationAdd({ value: id }); + }} + /> + )} +
+ + ); + return ( <> {noTransforms && ( @@ -312,29 +487,7 @@ class UnThemedTransformationsEditor extends React.PureComponent )} {showPicker ? ( - - - - {xforms.map((t) => { - return ( - { - this.onTransformationAdd({ value: t.id }); - }} - /> - ); - })} - + ) : ( )} @@ -351,6 +505,7 @@ class UnThemedTransformationsEditor extends React.PureComponent - -
+ + +
{hasTransforms && alert ? ( ) : null} - {hasTransforms && this.renderTransformationEditors()} + {hasTransforms && config.featureToggles.transformationsRedesign && !this.state.showPicker && ( +
+ Transformations in use{' '} + + this.onTransformationRemoveAll()} + onDismiss={() => this.setState({ showRemoveAllModal: false })} + /> +
+ )} + {hasTransforms && + (!config.featureToggles.transformationsRedesign || !this.state.showPicker) && + this.renderTransformationEditors()} {this.renderTransformsPicker()}
@@ -391,7 +570,7 @@ function TransformationCard({ transform, onClick }: TransformationCardProps) { return ( {transform.name} @@ -411,7 +590,159 @@ const getStyles = (theme: GrafanaTheme2) => { margin: 0; padding: ${theme.spacing(1)}; `, + grid: css` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + grid-auto-rows: 1fr; + gap: ${theme.spacing(2)} ${theme.spacing(1)}; + width: 100%; + `, + newCard: css` + grid-template-rows: min-content 0 1fr 0; + `, + badge: css` + padding: 4px 3px; + `, + heading: css` + font-weight: 400; + + > button { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: no-wrap; + } + `, + description: css` + font-size: 12px; + display: flex; + flex-direction: column; + justify-content: space-between; + `, + image: css` + display: block; + max-width: 100%; + margin-top: ${theme.spacing(2)}; + `, + searchWrapper: css` + display: flex; + flex-wrap: wrap; + column-gap: 27px; + row-gap: 16px; + width: 100%; + `, + searchInput: css` + flex-grow: 1; + width: initial; + `, + showImages: css` + flex-basis: 0; + display: flex; + gap: 8px; + align-items: center; + `, + pickerInformationLine: css` + font-size: 16px; + margin-bottom: ${theme.spacing(2)}; + `, + pickerInformationLineHighlight: css` + vertical-align: middle; + `, + illustationSwitchLabel: css` + white-space: nowrap; + `, + filterWrapper: css` + padding: ${theme.spacing(1)} 0; + display: flex; + flex-wrap: wrap; + row-gap: ${theme.spacing(1)}; + column-gap: ${theme.spacing(0.5)}; + `, + listInformationLineWrapper: css` + display: flex; + justify-content: space-between; + margin-bottom: 24px; + `, + listInformationLineText: css` + font-size: 16px; + `, + pluginStateInfoWrapper: css` + margin-left: 5px; + `, }; }; +interface TransformationsGridProps { + transformations: Array>; + showIllustrations?: boolean; + onClick: (id: string) => void; +} + +function TransformationsGrid({ showIllustrations, transformations, onClick }: TransformationsGridProps) { + const styles = useStyles2(getStyles); + + return ( +
+ {transformations.map((transform) => ( + onClick(transform.id)} + > + + <> + {transform.name} + + + + + + + <> + {getTransformationsRedesignDescriptions(transform.id)} + {showIllustrations && ( + + {transform.name} + + )} + + + + ))} +
+ ); +} + +const getImagePath = (id: string) => { + const folder = config.theme2.isDark ? 'dark' : 'light'; + + return `public/img/transformations/${folder}/${id}.svg`; +}; + +const getTransformationsRedesignDescriptions = (id: string): string => { + const overrides: { [key: string]: string } = { + [DataTransformerID.concatenate]: 'Combine all fields into a single frame.', + [DataTransformerID.configFromData]: 'Set unit, min, max and more.', + [DataTransformerID.fieldLookup]: 'Use a field value to lookup countries, states, or airports.', + [DataTransformerID.filterFieldsByName]: 'Removes part of the query results using a regex pattern.', + [DataTransformerID.filterByRefId]: 'Filter out queries in panels that have multiple queries.', + [DataTransformerID.filterByValue]: 'Removes rows of the query results using user-defined filters.', + [DataTransformerID.groupBy]: 'Group the data by a field value then process calculations.', + [DataTransformerID.groupingToMatrix]: 'Summarizes and reorganizes data based on three fields.', + [DataTransformerID.joinByField]: 'Combine rows from 2+ tables, based on a related field.', + [DataTransformerID.labelsToFields]: 'Groups series by time and return labels or tags as fields.', + [DataTransformerID.merge]: 'Merge multiple series. Values will be combined into one row.', + [DataTransformerID.organize]: 'Allows the user to re-order, hide, or rename fields / columns.', + [DataTransformerID.partitionByValues]: 'Splits a one-frame dataset into multiple series.', + [DataTransformerID.prepareTimeSeries]: 'Will stretch data frames from the wide format into the long format.', + [DataTransformerID.reduce]: 'Reduce all rows or data points to a single value (ex. max, mean).', + [DataTransformerID.renameByRegex]: 'Reduce all rows or data points to a single value (ex. max, mean).', + [DataTransformerID.seriesToRows]: 'Merge multiple series. Return time, metric and values as a row.', + }; + + return overrides[id] || standardTransformersRegistry.getIfExists(id)?.description || ''; +}; + export const TransformationsEditor = withTheme(UnThemedTransformationsEditor); diff --git a/public/app/features/plugins/components/PluginStateInfo.tsx b/public/app/features/plugins/components/PluginStateInfo.tsx index 29a63a3b47b..e27e1f0243f 100644 --- a/public/app/features/plugins/components/PluginStateInfo.tsx +++ b/public/app/features/plugins/components/PluginStateInfo.tsx @@ -5,6 +5,7 @@ import { Badge, BadgeProps } from '@grafana/ui'; interface Props { state?: PluginState; + className?: string; } export const PluginStateInfo = (props: Props) => { @@ -14,7 +15,15 @@ export const PluginStateInfo = (props: Props) => { return null; } - return ; + return ( + + ); }; function getFeatureStateInfo(state?: PluginState): BadgeProps | null {