From e9f4d90337085a3927eac529976084172784c674 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Thu, 4 Dec 2025 09:31:01 -0500 Subject: [PATCH] expression icons in cards --- .../PanelDataPane/AddDataItemMenu.tsx | 10 +-- .../panel-edit/PanelDataPane/DetailView.tsx | 3 +- .../PanelDataPane/DetailViewHeader.tsx | 2 +- .../PanelDataPane/PanelDataPane.tsx | 18 ++-- .../PanelDataPane/QueryTransformCard.tsx | 88 ++++++++++++------- .../PanelDataPane/QueryTransformList.tsx | 37 ++------ .../panel-edit/PanelDataPane/types.ts | 20 ++++- .../components/ExpressionTypeDropdown.tsx | 4 +- public/app/features/expressions/consts.ts | 10 --- public/app/features/expressions/types.ts | 16 +++- 10 files changed, 112 insertions(+), 96 deletions(-) delete mode 100644 public/app/features/expressions/consts.ts diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/AddDataItemMenu.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/AddDataItemMenu.tsx index c59708851b6..74148de7d83 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/AddDataItemMenu.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/AddDataItemMenu.tsx @@ -5,8 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { Button, Dropdown, IconButton, Menu, useStyles2 } from '@grafana/ui'; -import { EXPRESSION_ICON_MAP } from 'app/features/expressions/consts'; -import { ExpressionQueryType } from 'app/features/expressions/types'; +import { ExpressionQueryType, getExpressionIcon } from 'app/features/expressions/types'; interface AddDataItemMenuProps { onAddQuery: (index?: number) => void; @@ -83,12 +82,7 @@ export const AddDataItemMenu = memo( } const expressionSubItems = expressionTypes.map(({ type, label }) => ( - onAddExpression(type, index)} - /> + onAddExpression(type, index)} /> )); const menu = ( diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailView.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailView.tsx index d1b2778d987..65207ab6387 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailView.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailView.tsx @@ -11,8 +11,7 @@ import { DetailViewHeader } from './DetailViewHeader'; import { ExpressionDetailView } from './ExpressionDetailView'; import { PanelDataTransformationsTab, PanelDataTransformationsTabRendered } from './PanelDataTransformationsTab'; import { QueryDetailView } from './QueryDetailView'; -import { QueryTransformItem } from './QueryTransformList'; -import { TabId } from './types'; +import { TabId, QueryTransformItem } from './types'; interface DetailViewProps { selectedItem: QueryTransformItem | undefined; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailViewHeader.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailViewHeader.tsx index 9bfd02d0f87..7c4b3553ff6 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailViewHeader.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailViewHeader.tsx @@ -27,8 +27,8 @@ import { DataSourcePicker } from 'app/features/datasources/components/picker/Dat import { getQueryRunnerFor } from '../../utils/utils'; -import { QueryTransformItem } from './QueryTransformList'; import { SavedQueriesDrawer } from './SavedQueriesDrawer'; +import { QueryTransformItem } from './types'; interface DetailViewHeaderProps { selectedItem: QueryTransformItem; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx index 4c21c61f6c2..a823006c79f 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx @@ -27,10 +27,10 @@ import { DetailView } from './DetailView'; import { PanelDataAlertingTab } from './PanelDataAlertingTab'; import { PanelDataQueriesTab } from './PanelDataQueriesTab'; import { PanelDataTransformationsTab } from './PanelDataTransformationsTab'; -import { QueryTransformList, QueryTransformItem } from './QueryTransformList'; +import { QueryTransformList } from './QueryTransformList'; import { SavedQueriesDrawer } from './SavedQueriesDrawer'; import { TransformationsDrawer } from './TransformationsDrawer'; -import { PanelDataPaneTab, TabId } from './types'; +import { PanelDataPaneTab, TabId, QueryItem, TransformItem } from './types'; import { isDataTransformerConfig, queryItemId, transformItemId } from './utils'; export interface PanelDataPaneState extends SceneObjectState { @@ -147,14 +147,14 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { }, [queryRunner]); // Build separate lists for queries/expressions and transformations - const { dataSourceItems, transformItems, allItems } = useMemo(() => { - const dataSourceItems: QueryTransformItem[] = []; - const transformItems: QueryTransformItem[] = []; + const { queryExpressionItems, transformItems, allItems } = useMemo(() => { + const queryExpressionItems: QueryItem[] = []; + const transformItems: TransformItem[] = []; // Add queries and expressions for (let i = 0; i < (queries?.length ?? 0); i++) { const query = queries![i]; - dataSourceItems.push({ + queryExpressionItems.push({ id: queryItemId(query), type: isExpressionQuery(query) ? 'expression' : 'query', data: query, @@ -176,9 +176,9 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { } return { - dataSourceItems, + queryExpressionItems, transformItems, - allItems: [...dataSourceItems, ...transformItems], + allItems: [...queryExpressionItems, ...transformItems], }; }, [queries, transformations]); @@ -423,7 +423,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) {
void; onDuplicate?: () => void; onRemove?: () => void; onToggleVisibility?: () => void; - onAddQuery: (index: number) => void; - onAddTransform: (index: number) => void; - onAddExpression: (type: ExpressionQueryType, index: number) => void; } export const QueryTransformCard = memo( ({ - item, - type, - index, + item: { data, type, id: itemId, index }, isSelected, onClick, onDuplicate, onRemove, onToggleVisibility, - onAddQuery, - onAddTransform, - onAddExpression, }: QueryTransformCardProps) => { const colors = usePanelDataPaneColors(); const styles = useStyles2(getStyles, colors); - const getName = (): string => { - if ((type === 'query' || type === 'expression') && 'refId' in item) { - return item.refId || `${type === 'expression' ? 'Expression' : 'Query'} ${index + 1}`; - } else if ('id' in item) { - return item.id.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); - } - return ''; - }; - const datasourceIcon = useMemo(() => { - if (type === 'query' && 'datasource' in item && item.datasource) { + if (type === 'query' && 'datasource' in data && data.datasource) { try { - const dsSettings = getDataSourceSrv().getInstanceSettings(item.datasource); + const dsSettings = getDataSourceSrv().getInstanceSettings(data.datasource); return dsSettings?.meta.info.logos.small; } catch { return undefined; } } return undefined; - }, [type, item]); + }, [type, data]); const isHidden = - ((type === 'query' || type === 'expression') && 'hide' in item && item.hide) || - (type === 'transform' && 'disabled' in item && item.disabled); - const icon = type === 'query' ? 'database' : type === 'expression' ? 'code' : 'pivot'; - const typeLabel = type === 'query' ? 'Query' : type === 'expression' ? 'Expression' : 'Transformation'; - const name = getName(); + ((type === 'query' || type === 'expression') && 'hide' in data && data.hide) || + (type === 'transform' && 'disabled' in data && data.disabled); + + const typeLabel = useMemo(() => { + switch (type) { + case 'query': + return t('dashboard-scene.query-transform-card.query.label', 'Query'); + case 'expression': + return t('dashboard-scene.query-transform-card.expression.label', 'Expression'); + case 'transform': + return t('dashboard-scene.query-transform-card.transform.label', 'Transform'); + default: + throw new Error('unreachable'); + } + }, [type]); + + const name = useMemo(() => { + switch (type) { + case 'query': + case 'expression': { + // FIXME untranslated string + return data.refId || `${type === 'expression' ? 'Expression' : 'Query'} ${index + 1}`; + } + case 'transform': + return data.id.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); + default: + throw new Error('unreachable'); + } + }, [type, data, index]); + + const icon = useMemo((): IconName => { + switch (type) { + case 'query': + return 'database'; + case 'expression': { + const type = isExpressionQuery(data) ? data.type : undefined; + return getExpressionIcon(type); + } + case 'transform': + return 'pivot'; + default: + throw new Error('unreachable'); + } + }, [data, type]); const handleAction = (e: React.MouseEvent, action: () => void) => { e.stopPropagation(); @@ -91,7 +111,7 @@ export const QueryTransformCard = memo( onClick={onClick} onKeyDown={handleKeyDown} data-testid={`${type}-card-${index}`} - data-card-id={'refId' in item ? queryItemId(item) : transformItemId(index)} + data-card-id={itemId} > {/* Header with type and action icons */}
void; onAddQuery: (index?: number) => void; @@ -104,7 +97,7 @@ export const QueryTransformList = memo( const conns: Array<{ from: string; to: string }> = []; allItems.forEach((item) => { - if (item.type === 'expression' && 'expression' in item.data && 'refId' in item.data) { + if (item.type === 'expression') { const expr = item.data; if ('expression' in expr && typeof expr.expression === 'string' && 'type' in expr) { @@ -135,7 +128,7 @@ export const QueryTransformList = memo( // Filter connections to only show for selected card const visibleConnections = useMemo(() => { - if (!activeItem || !('refId' in activeItem.data)) { + if (activeItem?.type !== 'query' && activeItem?.type !== 'expression') { return []; } @@ -249,9 +242,7 @@ export const QueryTransformList = memo( visibleConnectionsRefIds.add(conn.from); visibleConnectionsRefIds.add(conn.to); } - return dataSourceItems.filter((item) => { - return 'refId' in item.data && visibleConnectionsRefIds.has(item.data.refId); - }); + return dataSourceItems.filter((item) => visibleConnectionsRefIds.has(item.data.refId)); }, [dataSourceItems, viewingConnections, visibleConnections]); const canAdd = !isDragging && !viewingConnections; @@ -382,18 +373,13 @@ export const QueryTransformList = memo( className={snapshot.isDragging ? styles.dragging : undefined} > handleCardClick(item.id)} - onAddExpression={onAddExpression} - onAddQuery={onAddQuery} - onAddTransform={onAddTransform} {...getHandlers(item)} />
@@ -484,18 +470,13 @@ export const QueryTransformList = memo( className={snapshot.isDragging ? styles.dragging : undefined} > handleCardClick(item.id)} - onAddExpression={onAddExpression} - onAddQuery={onAddQuery} - onAddTransform={onAddTransform} {...getHandlers(item)} />
diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/types.ts b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/types.ts index 816f82590aa..d5d9fed212e 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/types.ts +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/types.ts @@ -1,4 +1,5 @@ -import { SceneObject } from '@grafana/scenes'; +import { DataTransformerConfig } from '@grafana/data'; +import { SceneDataQuery, SceneObject } from '@grafana/scenes'; export enum TabId { Queries = 'queries', @@ -16,3 +17,20 @@ export interface PanelDataPaneTab extends SceneObject { getTabLabel(): string; tabId: TabId; } + +interface QueryTransformItemBase { + id: string; + index: number; +} + +export interface QueryItem extends QueryTransformItemBase { + type: 'query' | 'expression'; + data: SceneDataQuery; +} + +export interface TransformItem extends QueryTransformItemBase { + type: 'transform'; + data: DataTransformerConfig; +} + +export type QueryTransformItem = QueryItem | TransformItem; diff --git a/public/app/features/expressions/components/ExpressionTypeDropdown.tsx b/public/app/features/expressions/components/ExpressionTypeDropdown.tsx index 20bb7f0fb4d..fd43463a825 100644 --- a/public/app/features/expressions/components/ExpressionTypeDropdown.tsx +++ b/public/app/features/expressions/components/ExpressionTypeDropdown.tsx @@ -5,7 +5,7 @@ import { FeatureState, GrafanaTheme2, SelectableValue } from '@grafana/data'; import { Dropdown, FeatureBadge, Icon, Menu, Tooltip, useStyles2 } from '@grafana/ui'; import { ExpressionQueryType, expressionTypes } from 'app/features/expressions/types'; -import { EXPRESSION_ICON_MAP } from '../consts'; +import { getExpressionIcon } from '../types'; interface ExpressionTypeDropdownProps { children: ReactElement; @@ -28,7 +28,7 @@ const ExpressionMenuItem = memo(({ item, onSelect }) => component={() => (
-
diff --git a/public/app/features/expressions/consts.ts b/public/app/features/expressions/consts.ts deleted file mode 100644 index 8bf422f8a9f..00000000000 --- a/public/app/features/expressions/consts.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ExpressionQueryType } from './types'; - -export const EXPRESSION_ICON_MAP = { - [ExpressionQueryType.math]: 'calculator-alt', - [ExpressionQueryType.reduce]: 'compress-arrows', - [ExpressionQueryType.resample]: 'sync', - [ExpressionQueryType.classic]: 'cog', - [ExpressionQueryType.threshold]: 'sliders-v-alt', - [ExpressionQueryType.sql]: 'database', -} as const satisfies Record; diff --git a/public/app/features/expressions/types.ts b/public/app/features/expressions/types.ts index 3a4bd936424..5dd65a9e8d4 100644 --- a/public/app/features/expressions/types.ts +++ b/public/app/features/expressions/types.ts @@ -1,4 +1,4 @@ -import { DataQuery, ReducerID, SelectableValue } from '@grafana/data'; +import { DataQuery, IconName, ReducerID, SelectableValue } from '@grafana/data'; import { config } from 'app/core/config'; import { EvalFunction } from '../alerting/state/alertDef'; @@ -17,6 +17,7 @@ export enum ExpressionQueryType { sql = 'sql', } +// FIXME: should be translated export const getExpressionLabel = (type: ExpressionQueryType) => { switch (type) { case ExpressionQueryType.math: @@ -34,6 +35,19 @@ export const getExpressionLabel = (type: ExpressionQueryType) => { } }; +export const EXPRESSION_ICON_MAP = { + [ExpressionQueryType.math]: 'calculator-alt', + [ExpressionQueryType.reduce]: 'compress-arrows', + [ExpressionQueryType.resample]: 'sync', + [ExpressionQueryType.classic]: 'cog', + [ExpressionQueryType.threshold]: 'sliders-v-alt', + [ExpressionQueryType.sql]: 'database', +} as const satisfies Record; + +export const getExpressionIcon = (type?: ExpressionQueryType): IconName => { + return type && type in EXPRESSION_ICON_MAP ? EXPRESSION_ICON_MAP[type] : 'calculator-alt'; +}; + export const expressionTypes: Array> = [ { value: ExpressionQueryType.math,