add expressions
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Button, Dropdown, Menu, useStyles2 } from '@grafana/ui';
|
||||
import { ExpressionQueryType } from 'app/features/expressions/types';
|
||||
|
||||
interface AddDataItemMenuProps {
|
||||
onAddQuery: () => void;
|
||||
onAddTransform: () => void;
|
||||
onAddExpression: (type: ExpressionQueryType) => void;
|
||||
}
|
||||
|
||||
export const AddDataItemMenu = memo(({ onAddQuery, onAddTransform, onAddExpression }: AddDataItemMenuProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const expressionTypes = [
|
||||
{ type: ExpressionQueryType.math, label: t('dashboard-scene.add-data-item-menu.expression-math', 'Math') },
|
||||
{ type: ExpressionQueryType.reduce, label: t('dashboard-scene.add-data-item-menu.expression-reduce', 'Reduce') },
|
||||
{
|
||||
type: ExpressionQueryType.resample,
|
||||
label: t('dashboard-scene.add-data-item-menu.expression-resample', 'Resample'),
|
||||
},
|
||||
{
|
||||
type: ExpressionQueryType.classic,
|
||||
label: t('dashboard-scene.add-data-item-menu.expression-classic', 'Classic condition'),
|
||||
},
|
||||
{
|
||||
type: ExpressionQueryType.threshold,
|
||||
label: t('dashboard-scene.add-data-item-menu.expression-threshold', 'Threshold'),
|
||||
},
|
||||
];
|
||||
|
||||
// Add SQL if feature flag is enabled
|
||||
if (config.featureToggles.sqlExpressions) {
|
||||
expressionTypes.push({
|
||||
type: ExpressionQueryType.sql,
|
||||
label: t('dashboard-scene.add-data-item-menu.expression-sql', 'SQL'),
|
||||
});
|
||||
}
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
label={t('dashboard-scene.add-data-item-menu.add-query', 'Query')}
|
||||
icon="database"
|
||||
onClick={onAddQuery}
|
||||
/>
|
||||
<Menu.Item
|
||||
label={t('dashboard-scene.add-data-item-menu.add-transformation', 'Transformation')}
|
||||
icon="process"
|
||||
onClick={onAddTransform}
|
||||
/>
|
||||
<Menu.Group label={t('dashboard-scene.add-data-item-menu.expressions-group', 'Expression')}>
|
||||
{expressionTypes.map((expr) => (
|
||||
<Menu.Item
|
||||
key={expr.type}
|
||||
label={expr.label}
|
||||
icon="calculator-alt"
|
||||
onClick={() => onAddExpression(expr.type)}
|
||||
/>
|
||||
))}
|
||||
</Menu.Group>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Dropdown overlay={menu} placement="top-start">
|
||||
<Button icon="plus" variant="secondary" fullWidth>
|
||||
<Trans i18nKey="dashboard-scene.add-data-item-menu.add-button">Add</Trans>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
AddDataItemMenu.displayName = 'AddDataItemMenu';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css({
|
||||
width: '100%',
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -5,7 +5,9 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { VizPanel } from '@grafana/scenes';
|
||||
import { Container, ScrollContainer, useStyles2 } from '@grafana/ui';
|
||||
import { isExpressionQuery } from 'app/features/expressions/guards';
|
||||
|
||||
import { ExpressionDetailView } from './ExpressionDetailView';
|
||||
import { PanelDataTransformationsTab, PanelDataTransformationsTabRendered } from './PanelDataTransformationsTab';
|
||||
import { QueryDetailView } from './QueryDetailView';
|
||||
import { QueryTransformItem } from './QueryTransformList';
|
||||
@@ -40,6 +42,15 @@ export const DetailView = memo(({ selectedItem, panel, tabs }: DetailViewProps)
|
||||
<QueryDetailView panel={panel} query={query} queryIndex={selectedItem.index} />
|
||||
</ScrollContainer>
|
||||
);
|
||||
} else if (selectedItem.type === 'expression' && 'refId' in selectedItem.data) {
|
||||
const data = selectedItem.data;
|
||||
if (isExpressionQuery(data)) {
|
||||
return (
|
||||
<ScrollContainer>
|
||||
<ExpressionDetailView panel={panel} expression={data} expressionIndex={selectedItem.index} />
|
||||
</ScrollContainer>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const transformsTab = tabs.find((t) => t.tabId === TabId.Transformations);
|
||||
if (transformsTab instanceof PanelDataTransformationsTab && 'id' in selectedItem.data) {
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
CoreApp,
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
DataSourcePluginContextProvider,
|
||||
GrafanaTheme2,
|
||||
PanelData,
|
||||
} from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { VizPanel } from '@grafana/scenes';
|
||||
import { ErrorBoundaryAlert, useStyles2 } from '@grafana/ui';
|
||||
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
|
||||
import { ExpressionQueryEditor } from 'app/features/expressions/ExpressionQueryEditor';
|
||||
import { ExpressionDatasourceUID, ExpressionQuery } from 'app/features/expressions/types';
|
||||
|
||||
import { getQueryRunnerFor } from '../../utils/utils';
|
||||
|
||||
interface ExpressionDetailViewProps {
|
||||
panel: VizPanel;
|
||||
expression: ExpressionQuery;
|
||||
expressionIndex: number;
|
||||
}
|
||||
|
||||
export function ExpressionDetailView({ panel, expression, expressionIndex }: ExpressionDetailViewProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [datasource, setDatasource] = useState<DataSourceApi | null>(null);
|
||||
const [dsSettings, setDsSettings] = useState<DataSourceInstanceSettings | null>(null);
|
||||
const [data, setData] = useState<PanelData | undefined>();
|
||||
|
||||
const queryRunner = getQueryRunnerFor(panel);
|
||||
const queryRunnerState = queryRunner?.useState();
|
||||
const allQueries = queryRunnerState?.queries || [];
|
||||
|
||||
// Load expression datasource
|
||||
useEffect(() => {
|
||||
const loadDatasource = async () => {
|
||||
try {
|
||||
const ds = await getDataSourceSrv().get(ExpressionDatasourceUID);
|
||||
const settings = getDataSourceSrv().getInstanceSettings(ExpressionDatasourceUID);
|
||||
setDatasource(ds);
|
||||
setDsSettings(settings || null);
|
||||
} catch (error) {
|
||||
console.error('Failed to load expression datasource:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadDatasource();
|
||||
}, []);
|
||||
|
||||
// Subscribe to panel data
|
||||
useEffect(() => {
|
||||
if (!queryRunnerState?.data) {
|
||||
return;
|
||||
}
|
||||
// Filter data for this specific expression
|
||||
const panelData = queryRunnerState.data;
|
||||
const filteredSeries = panelData.series.filter((s) => s.refId === expression.refId);
|
||||
const filteredData = {
|
||||
...panelData,
|
||||
series: filteredSeries,
|
||||
error: panelData.errors?.find((e) => e.refId === expression.refId),
|
||||
};
|
||||
setData(filteredData);
|
||||
}, [queryRunnerState?.data, expression.refId]);
|
||||
|
||||
const handleExpressionChange = useCallback(
|
||||
(updatedExpression: ExpressionQuery) => {
|
||||
if (queryRunner) {
|
||||
const queries = queryRunner.state.queries || [];
|
||||
const newQueries = queries.map((q, idx) => (idx === expressionIndex ? updatedExpression : q));
|
||||
queryRunner.setState({ queries: newQueries });
|
||||
}
|
||||
},
|
||||
[queryRunner, expressionIndex]
|
||||
);
|
||||
|
||||
const handleRunQuery = useCallback(() => {
|
||||
if (queryRunner) {
|
||||
queryRunner.runQueries();
|
||||
}
|
||||
}, [queryRunner]);
|
||||
|
||||
if (!datasource || !dsSettings) {
|
||||
return (
|
||||
<div className={styles.loading}>
|
||||
<p>
|
||||
<Trans i18nKey="dashboard-scene.expression-detail-view.loading">Loading expression editor...</Trans>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<QueryOperationRow
|
||||
id={`expression-${expression.refId}`}
|
||||
index={expressionIndex}
|
||||
draggable={false}
|
||||
collapsable={false}
|
||||
isOpen={true}
|
||||
hideHeader={true}
|
||||
>
|
||||
<div className={styles.expressionContent}>
|
||||
<DataSourcePluginContextProvider instanceSettings={dsSettings}>
|
||||
<ErrorBoundaryAlert boundaryName="expression-editor">
|
||||
<ExpressionQueryEditor
|
||||
query={expression}
|
||||
queries={allQueries}
|
||||
// @ts-ignore - Expression datasource is compatible at runtime
|
||||
datasource={datasource}
|
||||
onChange={handleExpressionChange}
|
||||
onRunQuery={handleRunQuery}
|
||||
data={data}
|
||||
app={CoreApp.PanelEditor}
|
||||
/>
|
||||
</ErrorBoundaryAlert>
|
||||
</DataSourcePluginContextProvider>
|
||||
</div>
|
||||
</QueryOperationRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
padding: theme.spacing(2),
|
||||
}),
|
||||
expressionContent: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
loading: css({
|
||||
padding: theme.spacing(2),
|
||||
textAlign: 'center',
|
||||
color: theme.colors.text.secondary,
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -19,6 +19,9 @@ import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { getRulesPermissions } from 'app/features/alerting/unified/utils/access-control';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
|
||||
import { isExpressionQuery } from '../../../expressions/guards';
|
||||
import { ExpressionDatasourceUID, ExpressionQuery, ExpressionQueryType } from '../../../expressions/types';
|
||||
import { getDefaults } from '../../../expressions/utils/expressionTypes';
|
||||
import { getQueryRunnerFor } from '../../utils/utils';
|
||||
|
||||
import { DetailView } from './DetailView';
|
||||
@@ -99,16 +102,25 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
const items: QueryTransformItem[] = useMemo(() => {
|
||||
const result: QueryTransformItem[] = [];
|
||||
|
||||
// Add queries
|
||||
// Add queries and expressions (using actual array index)
|
||||
const queries = queryRunnerState?.queries || [];
|
||||
queries.forEach((query, index) => {
|
||||
queries.forEach((query, actualIndex) => {
|
||||
if ('refId' in query) {
|
||||
result.push({
|
||||
id: `query-${query.refId}`,
|
||||
type: 'query',
|
||||
data: query,
|
||||
index,
|
||||
});
|
||||
if (isExpressionQuery(query)) {
|
||||
result.push({
|
||||
id: `expression-${query.refId}`,
|
||||
type: 'expression',
|
||||
data: query,
|
||||
index: actualIndex, // Store actual index in queries array
|
||||
});
|
||||
} else {
|
||||
result.push({
|
||||
id: `query-${query.refId}`,
|
||||
type: 'query',
|
||||
data: query,
|
||||
index: actualIndex, // Store actual index in queries array
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -166,6 +178,42 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
setTransformDrawerOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleAddExpression = useCallback(
|
||||
(type: ExpressionQueryType) => {
|
||||
if (queryRunner) {
|
||||
const queries = queryRunner.state.queries || [];
|
||||
|
||||
// Get next available refId
|
||||
const existingRefIds = queries.map((q) => q.refId);
|
||||
let nextRefId = 'A';
|
||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
for (let i = 0; i < alphabet.length; i++) {
|
||||
if (!existingRefIds.includes(alphabet[i])) {
|
||||
nextRefId = alphabet[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new expression with defaults
|
||||
const newExpression: ExpressionQuery = getDefaults({
|
||||
refId: nextRefId,
|
||||
type,
|
||||
datasource: { uid: ExpressionDatasourceUID, type: '__expr__' },
|
||||
});
|
||||
|
||||
// Add to queries
|
||||
const newQueries = [...queries, newExpression];
|
||||
queryRunner.setState({ queries: newQueries });
|
||||
queryRunner.runQueries();
|
||||
|
||||
// Select the new expression
|
||||
setSelectedId(`expression-${nextRefId}`);
|
||||
}
|
||||
},
|
||||
[queryRunner]
|
||||
);
|
||||
|
||||
const handleTransformationAdd = useCallback(
|
||||
(selected: SelectableValue<string>) => {
|
||||
if (!selected.value) {
|
||||
@@ -261,6 +309,67 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
[queryRunner]
|
||||
);
|
||||
|
||||
// Expression handlers (use actual index in queries array)
|
||||
const handleDuplicateExpression = useCallback(
|
||||
(index: number) => {
|
||||
if (queryRunner) {
|
||||
const queries = queryRunner.state.queries || [];
|
||||
const expressionToDuplicate = queries[index];
|
||||
|
||||
if (expressionToDuplicate && isExpressionQuery(expressionToDuplicate)) {
|
||||
const existingRefIds = queries.map((q) => q.refId);
|
||||
let newRefId = expressionToDuplicate.refId;
|
||||
let counter = 1;
|
||||
while (existingRefIds.includes(newRefId)) {
|
||||
newRefId = `${expressionToDuplicate.refId}_${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
const duplicatedExpression = {
|
||||
...expressionToDuplicate,
|
||||
refId: newRefId,
|
||||
};
|
||||
|
||||
const newQueries = [...queries, duplicatedExpression];
|
||||
queryRunner.setState({ queries: newQueries });
|
||||
queryRunner.runQueries();
|
||||
|
||||
setSelectedId(`expression-${newRefId}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
[queryRunner]
|
||||
);
|
||||
|
||||
const handleRemoveExpression = useCallback(
|
||||
(index: number) => {
|
||||
if (queryRunner) {
|
||||
const queries = queryRunner.state.queries || [];
|
||||
const newQueries = queries.filter((_, i) => i !== index);
|
||||
queryRunner.setState({ queries: newQueries });
|
||||
queryRunner.runQueries();
|
||||
|
||||
const expressionToRemove = queries[index];
|
||||
if (expressionToRemove && selectedId === `expression-${expressionToRemove.refId}`) {
|
||||
setSelectedId(null);
|
||||
}
|
||||
}
|
||||
},
|
||||
[queryRunner, selectedId]
|
||||
);
|
||||
|
||||
const handleToggleExpressionVisibility = useCallback(
|
||||
(index: number) => {
|
||||
if (queryRunner) {
|
||||
const queries = queryRunner.state.queries || [];
|
||||
const newQueries = queries.map((q, i) => (i === index ? { ...q, hide: !q.hide } : q));
|
||||
queryRunner.setState({ queries: newQueries });
|
||||
queryRunner.runQueries();
|
||||
}
|
||||
},
|
||||
[queryRunner]
|
||||
);
|
||||
|
||||
const handleRemoveTransform = useCallback(
|
||||
(index: number) => {
|
||||
const transformsTab = tabs.find((t) => t.tabId === TabId.Transformations);
|
||||
@@ -300,9 +409,13 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
onSelect={handleSelect}
|
||||
onAddQuery={handleAddQuery}
|
||||
onAddTransform={handleAddTransform}
|
||||
onAddExpression={handleAddExpression}
|
||||
onDuplicateQuery={handleDuplicateQuery}
|
||||
onRemoveQuery={handleRemoveQuery}
|
||||
onToggleQueryVisibility={handleToggleQueryVisibility}
|
||||
onDuplicateExpression={handleDuplicateExpression}
|
||||
onRemoveExpression={handleRemoveExpression}
|
||||
onToggleExpressionVisibility={handleToggleExpressionVisibility}
|
||||
onRemoveTransform={handleRemoveTransform}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+30
-9
@@ -9,7 +9,7 @@ import { Icon, IconButton, Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
interface QueryTransformCardProps {
|
||||
item: SceneDataQuery | DataTransformerConfig;
|
||||
type: 'query' | 'transform';
|
||||
type: 'query' | 'transform' | 'expression';
|
||||
index: number;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
@@ -23,8 +23,8 @@ export const QueryTransformCard = memo(
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const getName = (): string => {
|
||||
if (type === 'query' && 'refId' in item) {
|
||||
return item.refId || `Query ${index + 1}`;
|
||||
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());
|
||||
}
|
||||
@@ -39,13 +39,16 @@ export const QueryTransformCard = memo(
|
||||
} catch {
|
||||
return 'Unknown datasource';
|
||||
}
|
||||
} else if (type === 'expression' && 'type' in item && typeof item.type === 'string') {
|
||||
// Show expression type (Math, Reduce, etc.)
|
||||
return item.type.charAt(0).toUpperCase() + item.type.slice(1);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const isHidden = type === 'query' && 'hide' in item && item.hide;
|
||||
const icon = type === 'query' ? 'database' : 'process';
|
||||
const typeLabel = type === 'query' ? 'Query' : 'Transformation';
|
||||
const isHidden = (type === 'query' || type === 'expression') && 'hide' in item && item.hide;
|
||||
const icon = type === 'query' ? 'database' : type === 'expression' ? 'calculator-alt' : 'process';
|
||||
const typeLabel = type === 'query' ? 'Query' : type === 'expression' ? 'Expression' : 'Transformation';
|
||||
const name = getName();
|
||||
const datasourceName = getDatasourceName();
|
||||
|
||||
@@ -71,14 +74,22 @@ export const QueryTransformCard = memo(
|
||||
data-testid={`${type}-card-${index}`}
|
||||
>
|
||||
{/* Header with type and action icons */}
|
||||
<div className={type === 'query' ? styles.headerQuery : styles.headerTransform}>
|
||||
<div
|
||||
className={
|
||||
type === 'query'
|
||||
? styles.headerQuery
|
||||
: type === 'expression'
|
||||
? styles.headerExpression
|
||||
: styles.headerTransform
|
||||
}
|
||||
>
|
||||
<div className={styles.headerLeft}>
|
||||
<Icon name={icon} className={styles.headerIcon} />
|
||||
<span className={styles.typeLabel}>{typeLabel}</span>
|
||||
</div>
|
||||
<div className={`${styles.actions} ${styles.actionsClass}`}>
|
||||
<Stack gap={0.5}>
|
||||
{type === 'query' && onToggleVisibility && (
|
||||
{(type === 'query' || type === 'expression') && onToggleVisibility && (
|
||||
<IconButton
|
||||
name={isHidden ? 'eye-slash' : 'eye'}
|
||||
size="sm"
|
||||
@@ -92,7 +103,7 @@ export const QueryTransformCard = memo(
|
||||
className={styles.actionButton}
|
||||
/>
|
||||
)}
|
||||
{type === 'query' && onDuplicate && (
|
||||
{(type === 'query' || type === 'expression') && onDuplicate && (
|
||||
<IconButton
|
||||
name="copy"
|
||||
size="sm"
|
||||
@@ -172,6 +183,16 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
: `${theme.visualization.getColorByName('orange')}15`,
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
}),
|
||||
headerExpression: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: theme.spacing(1, 1.5),
|
||||
background: theme.isDark
|
||||
? `${theme.visualization.getColorByName('purple')}20`
|
||||
: `${theme.visualization.getColorByName('purple')}15`,
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
}),
|
||||
headerLeft: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
||||
+34
-11
@@ -2,15 +2,16 @@ import { css } from '@emotion/css';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { DataTransformerConfig, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { SceneDataQuery } from '@grafana/scenes';
|
||||
import { Button, ScrollContainer, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { ScrollContainer, useStyles2 } from '@grafana/ui';
|
||||
import { ExpressionQueryType } from 'app/features/expressions/types';
|
||||
|
||||
import { AddDataItemMenu } from './AddDataItemMenu';
|
||||
import { QueryTransformCard } from './QueryTransformCard';
|
||||
|
||||
export interface QueryTransformItem {
|
||||
id: string;
|
||||
type: 'query' | 'transform';
|
||||
type: 'query' | 'transform' | 'expression';
|
||||
data: SceneDataQuery | DataTransformerConfig;
|
||||
index: number;
|
||||
}
|
||||
@@ -21,9 +22,13 @@ interface QueryTransformListProps {
|
||||
onSelect: (id: string) => void;
|
||||
onAddQuery: () => void;
|
||||
onAddTransform: () => void;
|
||||
onAddExpression: (type: ExpressionQueryType) => void;
|
||||
onDuplicateQuery?: (index: number) => void;
|
||||
onRemoveQuery?: (index: number) => void;
|
||||
onToggleQueryVisibility?: (index: number) => void;
|
||||
onDuplicateExpression?: (index: number) => void;
|
||||
onRemoveExpression?: (index: number) => void;
|
||||
onToggleExpressionVisibility?: (index: number) => void;
|
||||
onRemoveTransform?: (index: number) => void;
|
||||
}
|
||||
|
||||
@@ -34,14 +39,19 @@ export const QueryTransformList = memo(
|
||||
onSelect,
|
||||
onAddQuery,
|
||||
onAddTransform,
|
||||
onAddExpression,
|
||||
onDuplicateQuery,
|
||||
onRemoveQuery,
|
||||
onToggleQueryVisibility,
|
||||
onDuplicateExpression,
|
||||
onRemoveExpression,
|
||||
onToggleExpressionVisibility,
|
||||
onRemoveTransform,
|
||||
}: QueryTransformListProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const queries = items.filter((item) => item.type === 'query');
|
||||
const expressions = items.filter((item) => item.type === 'expression');
|
||||
const transforms = items.filter((item) => item.type === 'transform');
|
||||
|
||||
return (
|
||||
@@ -69,6 +79,26 @@ export const QueryTransformList = memo(
|
||||
</div>
|
||||
)}
|
||||
|
||||
{expressions.length > 0 && (
|
||||
<div className={styles.section}>
|
||||
{expressions.map((item) => (
|
||||
<QueryTransformCard
|
||||
key={item.id}
|
||||
item={item.data}
|
||||
type="expression"
|
||||
index={item.index}
|
||||
isSelected={selectedId === item.id}
|
||||
onClick={() => onSelect(item.id)}
|
||||
onDuplicate={onDuplicateExpression ? () => onDuplicateExpression(item.index) : undefined}
|
||||
onRemove={onRemoveExpression ? () => onRemoveExpression(item.index) : undefined}
|
||||
onToggleVisibility={
|
||||
onToggleExpressionVisibility ? () => onToggleExpressionVisibility(item.index) : undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{transforms.length > 0 && (
|
||||
<div className={styles.section}>
|
||||
{transforms.map((item) => (
|
||||
@@ -89,14 +119,7 @@ export const QueryTransformList = memo(
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<Stack direction="column" gap={1}>
|
||||
<Button icon="plus" variant="secondary" onClick={onAddQuery} fullWidth>
|
||||
<Trans i18nKey="dashboard-scene.query-transform-list.add-query">Add query</Trans>
|
||||
</Button>
|
||||
<Button icon="plus" variant="secondary" onClick={onAddTransform} fullWidth>
|
||||
<Trans i18nKey="dashboard-scene.query-transform-list.add-transformation">Add transformation</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
<AddDataItemMenu onAddQuery={onAddQuery} onAddTransform={onAddTransform} onAddExpression={onAddExpression} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5798,6 +5798,18 @@
|
||||
"label-use-static-key-dimensions": "Use static key dimensions",
|
||||
"name-allow-custom-values": "Allow custom values"
|
||||
},
|
||||
"add-data-item-menu": {
|
||||
"add-button": "Add",
|
||||
"add-query": "Query",
|
||||
"add-transformation": "Transformation",
|
||||
"expression-classic": "Classic condition",
|
||||
"expression-math": "Math",
|
||||
"expression-reduce": "Reduce",
|
||||
"expression-resample": "Resample",
|
||||
"expression-sql": "SQL",
|
||||
"expression-threshold": "Threshold",
|
||||
"expressions-group": "Expression"
|
||||
},
|
||||
"add-to-dashboard": {
|
||||
"message": {
|
||||
"could-navigate-selected-dashboard-please-again": "Could not navigate to the selected dashboard. Please try again.",
|
||||
@@ -5988,6 +6000,9 @@
|
||||
"sql-name": "Transform with SQL",
|
||||
"sql-transformation-description": "Manipulate your data using MySQL-like syntax"
|
||||
},
|
||||
"expression-detail-view": {
|
||||
"loading": "Loading expression editor..."
|
||||
},
|
||||
"general-settings-edit-view": {
|
||||
"editable_options": {
|
||||
"label": {
|
||||
@@ -6250,10 +6265,6 @@
|
||||
"remove-transform": "Remove transformation",
|
||||
"show-response": "Show response"
|
||||
},
|
||||
"query-transform-list": {
|
||||
"add-query": "Add query",
|
||||
"add-transformation": "Add transformation"
|
||||
},
|
||||
"query-variable-editor-form": {
|
||||
"description-examples": "Named capture groups can be used to separate the display text and value (<1>see examples</1> ).",
|
||||
"description-optional": "Optional, if you want to extract part of a series name or metric node segment.",
|
||||
|
||||
Reference in New Issue
Block a user