expression icons in cards
This commit is contained in:
@@ -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 }) => (
|
||||
<Menu.Item
|
||||
key={type}
|
||||
label={label}
|
||||
icon={EXPRESSION_ICON_MAP[type]}
|
||||
onClick={() => onAddExpression(type, index)}
|
||||
/>
|
||||
<Menu.Item key={type} label={label} icon={getExpressionIcon(type)} onClick={() => onAddExpression(type, index)} />
|
||||
));
|
||||
|
||||
const menu = (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<PanelDataPane>) {
|
||||
}, [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<PanelDataPane>) {
|
||||
}
|
||||
|
||||
return {
|
||||
dataSourceItems,
|
||||
queryExpressionItems,
|
||||
transformItems,
|
||||
allItems: [...dataSourceItems, ...transformItems],
|
||||
allItems: [...queryExpressionItems, ...transformItems],
|
||||
};
|
||||
}, [queries, transformations]);
|
||||
|
||||
@@ -423,7 +423,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
<div {...primaryProps} className={cx(primaryProps.className, styles.leftPane)}>
|
||||
<QueryTransformList
|
||||
allItems={allItems}
|
||||
dataSourceItems={dataSourceItems}
|
||||
dataSourceItems={queryExpressionItems}
|
||||
transformItems={transformItems}
|
||||
selectedId={effectiveSelectedId}
|
||||
onSelect={handleSelect}
|
||||
|
||||
+54
-34
@@ -2,74 +2,94 @@ import { css } from '@emotion/css';
|
||||
import clsx from 'clsx';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
import { DataTransformerConfig, GrafanaTheme2 } from '@grafana/data';
|
||||
import { GrafanaTheme2, IconName } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { SceneDataQuery } from '@grafana/scenes';
|
||||
import { Icon, IconButton, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { ExpressionQueryType } from 'app/features/expressions/types';
|
||||
import { isExpressionQuery } from 'app/features/expressions/guards';
|
||||
import { getExpressionIcon } from 'app/features/expressions/types';
|
||||
|
||||
import { usePanelDataPaneColors } from './theme';
|
||||
import { queryItemId, transformItemId } from './utils';
|
||||
import { QueryTransformItem } from './types';
|
||||
|
||||
interface QueryTransformCardProps {
|
||||
item: SceneDataQuery | DataTransformerConfig;
|
||||
type: 'query' | 'transform' | 'expression';
|
||||
index: number;
|
||||
item: QueryTransformItem;
|
||||
isSelected: boolean;
|
||||
onClick: () => 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 */}
|
||||
<div
|
||||
|
||||
+9
-28
@@ -2,9 +2,8 @@ import { css, cx } from '@emotion/css';
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd';
|
||||
import { HTMLAttributes, memo, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { DataTransformerConfig, GrafanaTheme2 } from '@grafana/data';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { SceneDataQuery } from '@grafana/scenes';
|
||||
import { Button, Icon, ScrollContainer, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { ExpressionQueryType } from 'app/features/expressions/types';
|
||||
|
||||
@@ -12,20 +11,14 @@ import { AddDataItemMenu } from './AddDataItemMenu';
|
||||
import { AiModeCard } from './AiModeCard';
|
||||
import { ConnectionLines } from './ConnectionLines';
|
||||
import { QueryTransformCard } from './QueryTransformCard';
|
||||
|
||||
export interface QueryTransformItem {
|
||||
id: string;
|
||||
type: 'query' | 'transform' | 'expression';
|
||||
data: SceneDataQuery | DataTransformerConfig;
|
||||
index: number;
|
||||
}
|
||||
import { QueryItem, QueryTransformItem, TransformItem } from './types';
|
||||
|
||||
const CARD_HEIGHT = 70;
|
||||
|
||||
interface QueryTransformListProps {
|
||||
allItems: QueryTransformItem[];
|
||||
dataSourceItems: QueryTransformItem[];
|
||||
transformItems: QueryTransformItem[];
|
||||
dataSourceItems: QueryItem[];
|
||||
transformItems: TransformItem[];
|
||||
selectedId: string | null;
|
||||
onSelect: (id: string) => 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}
|
||||
>
|
||||
<QueryTransformCard
|
||||
item={item.data}
|
||||
type={item.type}
|
||||
index={item.index}
|
||||
item={item}
|
||||
isSelected={
|
||||
isAiMode
|
||||
? selectedContextIds.includes(item.id)
|
||||
: selectedId === item.id
|
||||
}
|
||||
onClick={() => handleCardClick(item.id)}
|
||||
onAddExpression={onAddExpression}
|
||||
onAddQuery={onAddQuery}
|
||||
onAddTransform={onAddTransform}
|
||||
{...getHandlers(item)}
|
||||
/>
|
||||
</div>
|
||||
@@ -484,18 +470,13 @@ export const QueryTransformList = memo(
|
||||
className={snapshot.isDragging ? styles.dragging : undefined}
|
||||
>
|
||||
<QueryTransformCard
|
||||
item={item.data}
|
||||
type={item.type}
|
||||
index={item.index}
|
||||
item={item}
|
||||
isSelected={
|
||||
isAiMode
|
||||
? selectedContextIds.includes(item.id)
|
||||
: selectedId === item.id
|
||||
}
|
||||
onClick={() => handleCardClick(item.id)}
|
||||
onAddExpression={onAddExpression}
|
||||
onAddQuery={onAddQuery}
|
||||
onAddTransform={onAddTransform}
|
||||
{...getHandlers(item)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<ExpressionMenuItemProps>(({ item, onSelect }) =>
|
||||
component={() => (
|
||||
<div className={styles.expressionTypeItem} role="menuitem">
|
||||
<div className={styles.expressionTypeItemContent} data-testid={`expression-type-${value}`}>
|
||||
<Icon className={styles.icon} name={EXPRESSION_ICON_MAP[value!]} aria-hidden="true" />
|
||||
<Icon className={styles.icon} name={getExpressionIcon(value!)} aria-hidden="true" />
|
||||
{label}
|
||||
{value === ExpressionQueryType.sql && <FeatureBadge featureState={FeatureState.preview} />}
|
||||
</div>
|
||||
|
||||
@@ -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<ExpressionQueryType, string>;
|
||||
@@ -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<ExpressionQueryType, string>;
|
||||
|
||||
export const getExpressionIcon = (type?: ExpressionQueryType): IconName => {
|
||||
return type && type in EXPRESSION_ICON_MAP ? EXPRESSION_ICON_MAP[type] : 'calculator-alt';
|
||||
};
|
||||
|
||||
export const expressionTypes: Array<SelectableValue<ExpressionQueryType>> = [
|
||||
{
|
||||
value: ExpressionQueryType.math,
|
||||
|
||||
Reference in New Issue
Block a user