clean up state logic, add the inline add button
This commit is contained in:
@@ -17,7 +17,12 @@ export function queryIsEmpty(query: DataQuery): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function addQuery(queries: DataQuery[], query?: Partial<DataQuery>, datasource?: DataSourceRef): DataQuery[] {
|
||||
export function addQuery(
|
||||
queries: DataQuery[],
|
||||
query?: Partial<DataQuery>,
|
||||
datasource?: DataSourceRef,
|
||||
index = queries.length
|
||||
): DataQuery[] {
|
||||
const q: DataQuery = {
|
||||
...query,
|
||||
refId: getNextRefId(queries),
|
||||
@@ -28,7 +33,9 @@ export function addQuery(queries: DataQuery[], query?: Partial<DataQuery>, datas
|
||||
q.datasource = datasource;
|
||||
}
|
||||
|
||||
return [...queries, q];
|
||||
queries.splice(index, 0, q);
|
||||
|
||||
return [...queries];
|
||||
}
|
||||
|
||||
export function isDataQuery(url: string): boolean {
|
||||
|
||||
@@ -1,20 +1,37 @@
|
||||
import { memo } from 'react';
|
||||
import { memo, useState } from 'react';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Dropdown, IconButton, Menu, Stack } from '@grafana/ui';
|
||||
import { Dropdown, IconButton, Menu } from '@grafana/ui';
|
||||
import { EXPRESSION_ICON_MAP } from 'app/features/expressions/consts';
|
||||
import { ExpressionQueryType } from 'app/features/expressions/types';
|
||||
|
||||
interface AddDataItemMenuProps {
|
||||
onAddQuery: () => void;
|
||||
onAddFromSavedQueries: () => void;
|
||||
onAddTransform: () => void;
|
||||
onAddExpression: (type: ExpressionQueryType) => void;
|
||||
onAddQuery: (index?: number) => void;
|
||||
onAddFromSavedQueries: (index?: number) => void;
|
||||
onAddTransform: (index?: number) => void;
|
||||
onAddExpression: (type: ExpressionQueryType, index?: number) => void;
|
||||
index?: number;
|
||||
allowedTypes?: Array<'query' | 'transform' | 'expression'>;
|
||||
show?: boolean;
|
||||
}
|
||||
|
||||
export const AddDataItemMenu = memo(
|
||||
({ onAddQuery, onAddFromSavedQueries, onAddTransform, onAddExpression }: AddDataItemMenuProps) => {
|
||||
({
|
||||
onAddQuery,
|
||||
onAddTransform,
|
||||
onAddExpression,
|
||||
onAddFromSavedQueries,
|
||||
index,
|
||||
allowedTypes = ['query', 'expression', 'transform'],
|
||||
show = true,
|
||||
}: AddDataItemMenuProps) => {
|
||||
const [menuShown, setMenuShown] = useState(false);
|
||||
|
||||
if (!show && !menuShown) {
|
||||
return;
|
||||
}
|
||||
|
||||
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') },
|
||||
@@ -41,45 +58,52 @@ export const AddDataItemMenu = memo(
|
||||
}
|
||||
|
||||
const expressionSubItems = expressionTypes.map(({ type, label }) => (
|
||||
<Menu.Item key={type} label={label} icon={EXPRESSION_ICON_MAP[type]} onClick={() => onAddExpression(type)} />
|
||||
<Menu.Item
|
||||
key={type}
|
||||
label={label}
|
||||
icon={EXPRESSION_ICON_MAP[type]}
|
||||
onClick={() => onAddExpression(type, index)}
|
||||
/>
|
||||
));
|
||||
|
||||
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-from-saved-queries', 'From saved queries')}
|
||||
icon="bookmark"
|
||||
onClick={onAddFromSavedQueries}
|
||||
/>
|
||||
<Menu.Item
|
||||
label={t('dashboard-scene.add-data-item-menu.add-transformation', 'Transformation')}
|
||||
icon="process"
|
||||
onClick={onAddTransform}
|
||||
/>
|
||||
<Menu.Item
|
||||
label={t('dashboard-scene.add-data-item-menu.expressions-group', 'Expression')}
|
||||
icon="calculator-alt"
|
||||
childItems={expressionSubItems}
|
||||
/>
|
||||
{allowedTypes.includes('query') && (
|
||||
<Menu.Item
|
||||
label={t('dashboard-scene.add-data-item-menu.add-query', 'Query')}
|
||||
icon="database"
|
||||
onClick={() => onAddQuery(index)}
|
||||
/>
|
||||
)}
|
||||
{allowedTypes.includes('query') && (
|
||||
<Menu.Item
|
||||
label={t('dashboard-scene.add-data-item-menu.add-from-saved-queries', 'From saved queries')}
|
||||
icon="bookmark"
|
||||
onClick={() => onAddFromSavedQueries(index)}
|
||||
/>
|
||||
)}
|
||||
{allowedTypes.includes('transform') && (
|
||||
<Menu.Item
|
||||
label={t('dashboard-scene.add-data-item-menu.add-transformation', 'Transformation')}
|
||||
icon="process"
|
||||
onClick={() => onAddTransform(index)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{allowedTypes.includes('expression') && (
|
||||
<Menu.Item
|
||||
label={t('dashboard-scene.add-data-item-menu.expressions-group', 'Expression')}
|
||||
icon="calculator-alt"
|
||||
childItems={expressionSubItems}
|
||||
/>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack direction="row" justifyContent="center" alignItems="center">
|
||||
<Dropdown overlay={menu} placement="top-start">
|
||||
<IconButton
|
||||
name="plus-circle"
|
||||
size="xxl"
|
||||
variant="secondary"
|
||||
tooltip={t('dashboard-scene.add-data-item-menu.add-button', 'Add')}
|
||||
/>
|
||||
</Dropdown>
|
||||
</Stack>
|
||||
<Dropdown overlay={menu} placement="top-end" onVisibleChange={(shown) => setMenuShown(shown)}>
|
||||
<IconButton name="plus" size="xs" variant="primary" tooltip={t('dashboard-scene.add-data-item-menu.add-button', 'Add')} />
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
|
||||
import { useCallback, useMemo, useState, useEffect } from 'react';
|
||||
|
||||
import { DataQuery, DataTransformerConfig, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
SceneObjectUrlSyncConfig,
|
||||
SceneObjectUrlValues,
|
||||
VizPanel,
|
||||
SceneDataTransformer,
|
||||
SceneDataQuery,
|
||||
} from '@grafana/scenes';
|
||||
import { useStyles2, useSplitter } from '@grafana/ui';
|
||||
@@ -21,8 +20,7 @@ import { getRulesPermissions } from 'app/features/alerting/unified/utils/access-
|
||||
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 { ExpressionQueryType } from '../../../expressions/types';
|
||||
import { getQueryRunnerFor } from '../../utils/utils';
|
||||
|
||||
import { DetailView } from './DetailView';
|
||||
@@ -85,32 +83,45 @@ export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
||||
}
|
||||
}
|
||||
|
||||
interface DrawerState {
|
||||
open: boolean;
|
||||
index: number | null;
|
||||
}
|
||||
|
||||
function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
const { tabs, panelRef } = model.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [transformDrawerOpen, setTransformDrawerOpen] = useState(false);
|
||||
const [savedQueriesDrawerOpen, setSavedQueriesDrawerOpen] = useState(false);
|
||||
const [savedQueriesDrawerState, setSavedQueriesDrawerState] = useState<DrawerState>({
|
||||
open: false,
|
||||
index: null,
|
||||
});
|
||||
const [transformDrawerState, setTransformDrawerState] = useState<DrawerState>({
|
||||
open: false,
|
||||
index: null,
|
||||
});
|
||||
|
||||
const panel = panelRef.resolve();
|
||||
|
||||
// Subscribe to query runner state changes
|
||||
// Subscribe to query runner and tab state changes
|
||||
const queryRunner = getQueryRunnerFor(panel);
|
||||
const queryRunnerState = queryRunner?.useState();
|
||||
const queriesTab = tabs.find((t): t is PanelDataQueriesTab => t.tabId === TabId.Queries);
|
||||
const transformsTab = tabs.find((t): t is PanelDataTransformationsTab => t.tabId === TabId.Transformations);
|
||||
const transformer = transformsTab?.getDataTransformer();
|
||||
const transformerState = transformer?.useState();
|
||||
const queries = queryRunnerState?.queries;
|
||||
const transformations = transformerState?.transformations;
|
||||
|
||||
// the selectedId is based on the refId of the query. refId is a user-editable property, so it can change,
|
||||
// which will break the selectId and result in the UI going into a deselected state. to avoid this,
|
||||
// we can subscribe to changes and detect if a single refId just changed, and then assume that change
|
||||
// is a rename of the currently selected query.
|
||||
const orderedRefIds = useRef<string[]>(queryRunnerState?.queries.map(({ refId }) => refId) ?? []);
|
||||
useEffect(() => {
|
||||
queryRunner?.subscribeToState((newState) => {
|
||||
queryRunner?.subscribeToState((newState, prevState) => {
|
||||
// loop over the new queries and confirm that the refIds are the same. if not, then a mutation
|
||||
// occurred, but we need to figure out if it was a rename or a reorder
|
||||
|
||||
const oldOrderedRefIds = orderedRefIds.current;
|
||||
orderedRefIds.current = newState.queries.map(({ refId }) => refId);
|
||||
|
||||
const oldOrderedRefIds = prevState.queries.map(({ refId }) => refId);
|
||||
if (newState.queries.length !== oldOrderedRefIds.length) {
|
||||
return; // add, remove, something else.
|
||||
}
|
||||
@@ -135,17 +146,12 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
});
|
||||
}, [queryRunner]);
|
||||
|
||||
// Subscribe to data transformer state changes
|
||||
const dataTransformer = panel.state.$data instanceof SceneDataTransformer ? panel.state.$data : null;
|
||||
const transformerState = dataTransformer?.useState();
|
||||
|
||||
// Build separate lists for queries/expressions and transformations
|
||||
const { dataSourceItems, transformItems, allItems } = useMemo(() => {
|
||||
const dataSourceItems: QueryTransformItem[] = [];
|
||||
const transformItems: QueryTransformItem[] = [];
|
||||
|
||||
// Add queries and expressions
|
||||
const queries = queryRunnerState?.queries;
|
||||
for (let i = 0; i < (queries?.length ?? 0); i++) {
|
||||
const query = queries![i];
|
||||
dataSourceItems.push({
|
||||
@@ -157,7 +163,6 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
}
|
||||
|
||||
// Add transformations
|
||||
const transformations = transformerState?.transformations;
|
||||
for (let i = 0; i < (transformations?.length ?? 0); i++) {
|
||||
const transform = transformations![i];
|
||||
if (isDataTransformerConfig(transform)) {
|
||||
@@ -175,7 +180,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
transformItems,
|
||||
allItems: [...dataSourceItems, ...transformItems],
|
||||
};
|
||||
}, [queryRunnerState?.queries, transformerState?.transformations]);
|
||||
}, [queries, transformations]);
|
||||
|
||||
// Auto-select first item if nothing is selected
|
||||
const effectiveSelectedId = useMemo(() => {
|
||||
@@ -185,48 +190,120 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
return selectedId;
|
||||
}, [selectedId, allItems]);
|
||||
|
||||
const selectedItem = useMemo(() => {
|
||||
return allItems.find((item) => item.id === effectiveSelectedId);
|
||||
}, [allItems, effectiveSelectedId]);
|
||||
const selectedItem = useMemo(
|
||||
() => allItems.find((item) => item.id === effectiveSelectedId),
|
||||
[allItems, effectiveSelectedId]
|
||||
);
|
||||
|
||||
const handleSelect = useCallback((id: string) => {
|
||||
setSelectedId(id);
|
||||
}, []);
|
||||
|
||||
const handleAddQuery = useCallback(() => {
|
||||
const queriesTab = tabs.find((t) => t.tabId === TabId.Queries);
|
||||
if (queriesTab instanceof PanelDataQueriesTab) {
|
||||
queriesTab.addQueryClick();
|
||||
// Select the new query after a short delay
|
||||
setTimeout(() => {
|
||||
const newQueries = getQueryRunnerFor(panel)?.state.queries || [];
|
||||
if (newQueries.length > 0) {
|
||||
setSelectedId(queryItemId(newQueries[newQueries.length - 1]));
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, [tabs, panel]);
|
||||
const updateQuerySelectionOnStateChange = useCallback(
|
||||
(index: number) => {
|
||||
if (queryRunner) {
|
||||
const unsub = queryRunner.subscribeToState((newState) => {
|
||||
const newQueries = newState.queries;
|
||||
if (newQueries.length > 0) {
|
||||
const selected = newQueries[index] ?? newQueries[0];
|
||||
setSelectedId(queryItemId(selected));
|
||||
}
|
||||
unsub.unsubscribe();
|
||||
});
|
||||
}
|
||||
},
|
||||
[queryRunner]
|
||||
);
|
||||
|
||||
const handleAddFromSavedQueries = useCallback(() => {
|
||||
setSavedQueriesDrawerOpen(true);
|
||||
}, []);
|
||||
/** QUERIES AND EXPRESSIONS **/
|
||||
const handleAddQuery = useCallback(
|
||||
(index?: number) => {
|
||||
if (queriesTab) {
|
||||
updateQuerySelectionOnStateChange(index ?? queries?.length ?? 0);
|
||||
queriesTab.addQueryClick(index);
|
||||
}
|
||||
},
|
||||
[queries, queriesTab, updateQuerySelectionOnStateChange]
|
||||
);
|
||||
|
||||
const handleAddExpression = useCallback(
|
||||
(type: ExpressionQueryType, index?: number) => {
|
||||
if (queriesTab) {
|
||||
updateQuerySelectionOnStateChange(index ?? queries?.length ?? 0);
|
||||
queriesTab.onAddExpressionOfType(type, index);
|
||||
}
|
||||
},
|
||||
[queriesTab, updateQuerySelectionOnStateChange, queries]
|
||||
);
|
||||
|
||||
const handleDuplicateQuery = useCallback(
|
||||
(index: number) => {
|
||||
if (queryRunner && queriesTab) {
|
||||
const queryToDuplicate = queries?.[index];
|
||||
if (queryToDuplicate) {
|
||||
// Create a copy with a new refId
|
||||
let newRefId = queryToDuplicate.refId;
|
||||
let counter = 1;
|
||||
while (queries.some((q) => q.refId === newRefId)) {
|
||||
newRefId = `${queryToDuplicate.refId}_${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
const duplicatedQuery = {
|
||||
...queryToDuplicate,
|
||||
refId: newRefId,
|
||||
};
|
||||
|
||||
updateQuerySelectionOnStateChange(index + 1);
|
||||
queriesTab.onAddQuery(duplicatedQuery, index + 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
[queryRunner, queriesTab, queries, updateQuerySelectionOnStateChange]
|
||||
);
|
||||
|
||||
const handleRemoveQuery = useCallback(
|
||||
(index: number) => {
|
||||
if (queryRunner) {
|
||||
const deletedQuery = queries?.[index];
|
||||
const newQueries = queries?.filter((_, i) => i !== index);
|
||||
queryRunner.setState({ queries: newQueries });
|
||||
queryRunner.runQueries();
|
||||
|
||||
// Clear selection if removing the selected query
|
||||
if (deletedQuery && selectedId === queryItemId(deletedQuery)) {
|
||||
setSelectedId(null);
|
||||
}
|
||||
}
|
||||
},
|
||||
[queryRunner, selectedId, queries]
|
||||
);
|
||||
|
||||
const handleToggleQueryVisibility = useCallback(
|
||||
(index: number) => {
|
||||
if (queryRunner) {
|
||||
const newQueries = queries?.map((q, i) => (i === index ? { ...q, hide: !q.hide } : q));
|
||||
queryRunner.setState({ queries: newQueries });
|
||||
queryRunner.runQueries();
|
||||
}
|
||||
},
|
||||
[queryRunner, queries]
|
||||
);
|
||||
|
||||
// This is a stub for the saved queries drawer
|
||||
const handleSelectSavedQuery = useCallback(
|
||||
(query: DataQuery) => {
|
||||
if (!queryRunner) {
|
||||
if (!queryRunner || !queriesTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queries = queryRunner.state.queries || [];
|
||||
const selectedIndex = savedQueriesDrawerState.index ?? queries?.length ?? 0;
|
||||
|
||||
// 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])) {
|
||||
if (!queries?.some(q => q.refId === alphabet[i])) {
|
||||
nextRefId = alphabet[i];
|
||||
break;
|
||||
}
|
||||
@@ -238,211 +315,47 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
refId: nextRefId,
|
||||
};
|
||||
|
||||
queryRunner.setState({ queries: [...queries, newQuery] });
|
||||
updateQuerySelectionOnStateChange(selectedIndex);
|
||||
queriesTab.onAddQuery(newQuery);
|
||||
|
||||
// Select the new query after a short delay
|
||||
setTimeout(() => {
|
||||
setSelectedId(queryItemId(newQuery));
|
||||
}, 100);
|
||||
|
||||
setSavedQueriesDrawerOpen(false);
|
||||
setSavedQueriesDrawerState({ open: false, index: null });
|
||||
},
|
||||
[queryRunner]
|
||||
[queryRunner, savedQueriesDrawerState.index, queries, updateQuerySelectionOnStateChange, queriesTab]
|
||||
);
|
||||
|
||||
const handleAddTransform = useCallback(() => {
|
||||
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(queryItemId(newExpression));
|
||||
}
|
||||
},
|
||||
[queryRunner]
|
||||
);
|
||||
|
||||
const handleTransformationAdd = useCallback(
|
||||
/** TRANSFORMS **/
|
||||
const handleAddTransform = useCallback(
|
||||
(selected: SelectableValue<string>) => {
|
||||
if (!selected.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transformsTab = tabs.find((t) => t.tabId === TabId.Transformations);
|
||||
if (transformsTab instanceof PanelDataTransformationsTab) {
|
||||
const transformations = (transformsTab.getDataTransformer().state.transformations || []).filter(
|
||||
isDataTransformerConfig
|
||||
);
|
||||
if (transformsTab && transformer) {
|
||||
const selectedIndex = transformDrawerState.index ?? transformations?.length ?? 0;
|
||||
const newTransformation: DataTransformerConfig = {
|
||||
id: selected.value,
|
||||
options: {},
|
||||
};
|
||||
|
||||
transformsTab.onChangeTransformations([...transformations, newTransformation]);
|
||||
setTransformDrawerOpen(false);
|
||||
const unsub = transformer.subscribeToState((newState) => {
|
||||
const newTransform = newState.transformations[selectedIndex];
|
||||
|
||||
// Select the newly added transformation
|
||||
setTimeout(() => {
|
||||
setSelectedId(transformItemId(transformations.length));
|
||||
}, 100);
|
||||
setSelectedId(!!newTransform ? transformItemId(selectedIndex) : null);
|
||||
setTransformDrawerState({ open: false, index: null });
|
||||
unsub.unsubscribe();
|
||||
});
|
||||
|
||||
const newTransformations = [...(transformations?.filter(isDataTransformerConfig) ?? [])];
|
||||
newTransformations.splice(selectedIndex, 0, newTransformation);
|
||||
|
||||
transformsTab.onChangeTransformations(newTransformations);
|
||||
}
|
||||
},
|
||||
[tabs]
|
||||
);
|
||||
|
||||
const handleDuplicateQuery = useCallback(
|
||||
(index: number) => {
|
||||
if (queryRunner) {
|
||||
const queries = queryRunner.state.queries || [];
|
||||
const queryToDuplicate = queries[index];
|
||||
if (queryToDuplicate) {
|
||||
// Create a copy with a new refId
|
||||
const existingRefIds = queries.map((q) => q.refId);
|
||||
let newRefId = queryToDuplicate.refId;
|
||||
let counter = 1;
|
||||
while (existingRefIds.includes(newRefId)) {
|
||||
newRefId = `${queryToDuplicate.refId}_${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
const duplicatedQuery = {
|
||||
...queryToDuplicate,
|
||||
refId: newRefId,
|
||||
};
|
||||
|
||||
const newQueries = [...queries, duplicatedQuery];
|
||||
queryRunner.setState({ queries: newQueries });
|
||||
queryRunner.runQueries();
|
||||
|
||||
// Select the new query
|
||||
setSelectedId(queryItemId(duplicatedQuery));
|
||||
}
|
||||
}
|
||||
},
|
||||
[queryRunner]
|
||||
);
|
||||
|
||||
const handleRemoveQuery = useCallback(
|
||||
(index: number) => {
|
||||
if (queryRunner) {
|
||||
const queries = queryRunner.state.queries || [];
|
||||
const newQueries = queries.filter((_, i) => i !== index);
|
||||
queryRunner.setState({ queries: newQueries });
|
||||
queryRunner.runQueries();
|
||||
|
||||
// Clear selection if removing the selected query
|
||||
if (selectedId === queryItemId(queries[index])) {
|
||||
setSelectedId(null);
|
||||
}
|
||||
}
|
||||
},
|
||||
[queryRunner, selectedId]
|
||||
);
|
||||
|
||||
const handleToggleQueryVisibility = 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]
|
||||
);
|
||||
|
||||
// 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(queryItemId(duplicatedExpression));
|
||||
}
|
||||
}
|
||||
},
|
||||
[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 === queryItemId(expressionToRemove)) {
|
||||
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]
|
||||
[transformsTab, transformer, transformations, transformDrawerState.index]
|
||||
);
|
||||
|
||||
const handleRemoveTransform = useCallback(
|
||||
(index: number) => {
|
||||
const transformsTab = tabs.find((t): t is PanelDataTransformationsTab => t.tabId === TabId.Transformations);
|
||||
if (transformsTab) {
|
||||
const transformations = (transformsTab.getDataTransformer().state.transformations || []).filter(
|
||||
isDataTransformerConfig
|
||||
@@ -456,12 +369,11 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
}
|
||||
}
|
||||
},
|
||||
[tabs, selectedId]
|
||||
[transformsTab, selectedId]
|
||||
);
|
||||
|
||||
const handleToggleTransformVisibility = useCallback(
|
||||
(index: number) => {
|
||||
const transformsTab = tabs.find((t): t is PanelDataTransformationsTab => t.tabId === TabId.Transformations);
|
||||
if (transformsTab) {
|
||||
const transformations = (transformsTab.getDataTransformer().state.transformations || []).filter(
|
||||
isDataTransformerConfig
|
||||
@@ -472,7 +384,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
transformsTab.onChangeTransformations(newTransformations);
|
||||
}
|
||||
},
|
||||
[tabs]
|
||||
[transformsTab]
|
||||
);
|
||||
|
||||
const handleReorderDataSources = useCallback(
|
||||
@@ -490,7 +402,6 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
|
||||
const handleReorderTransforms = useCallback(
|
||||
(startIndex: number, endIndex: number) => {
|
||||
const transformsTab = tabs.find((t): t is PanelDataTransformationsTab => t.tabId === TabId.Transformations);
|
||||
if (transformsTab) {
|
||||
const transformations = (transformsTab.getDataTransformer().state.transformations || []).filter(
|
||||
isDataTransformerConfig
|
||||
@@ -501,7 +412,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
transformsTab.onChangeTransformations(newTransformations);
|
||||
}
|
||||
},
|
||||
[tabs]
|
||||
[transformsTab]
|
||||
);
|
||||
|
||||
// Get data for transformations drawer
|
||||
@@ -525,16 +436,12 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
selectedId={effectiveSelectedId}
|
||||
onSelect={handleSelect}
|
||||
onAddQuery={handleAddQuery}
|
||||
onAddFromSavedQueries={handleAddFromSavedQueries}
|
||||
onAddTransform={handleAddTransform}
|
||||
onAddFromSavedQueries={index => setSavedQueriesDrawerState({ open: true, index: index ?? null })}
|
||||
onAddTransform={(index) => setTransformDrawerState({ open: true, index: index ?? null })}
|
||||
onAddExpression={handleAddExpression}
|
||||
onDuplicateQuery={handleDuplicateQuery}
|
||||
onRemoveQuery={handleRemoveQuery}
|
||||
onToggleQueryVisibility={handleToggleQueryVisibility}
|
||||
// TODO: can all the expression stuff just be handled with the query handlers since expressions are queries?
|
||||
onDuplicateExpression={handleDuplicateExpression}
|
||||
onRemoveExpression={handleRemoveExpression}
|
||||
onToggleExpressionVisibility={handleToggleExpressionVisibility}
|
||||
onRemoveTransform={handleRemoveTransform}
|
||||
onToggleTransformVisibility={handleToggleTransformVisibility}
|
||||
onReorderDataSources={handleReorderDataSources}
|
||||
@@ -557,14 +464,14 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
</div>
|
||||
</div>
|
||||
<TransformationsDrawer
|
||||
isOpen={transformDrawerOpen}
|
||||
onClose={() => setTransformDrawerOpen(false)}
|
||||
onTransformationAdd={handleTransformationAdd}
|
||||
isOpen={transformDrawerState.open}
|
||||
onClose={() => setTransformDrawerState({ open: false, index: null })}
|
||||
onTransformationAdd={handleAddTransform}
|
||||
series={series}
|
||||
/>
|
||||
<SavedQueriesDrawer
|
||||
isOpen={savedQueriesDrawerOpen}
|
||||
onClose={() => setSavedQueriesDrawerOpen(false)}
|
||||
isOpen={savedQueriesDrawerState.open}
|
||||
onClose={() => setSavedQueriesDrawerState({ open: false, index: null })}
|
||||
onSelectQuery={handleSelectSavedQuery}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+15
-7
@@ -273,17 +273,25 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
|
||||
};
|
||||
}
|
||||
|
||||
public addQueryClick = () => {
|
||||
public addQueryClick = (index?: number) => {
|
||||
const queries = this.getQueries();
|
||||
this.onQueriesChange(addQuery(queries, this.newQuery()));
|
||||
const dsSettings = this.state.dsSettings;
|
||||
this.onQueriesChange(
|
||||
addQuery(
|
||||
queries,
|
||||
this.newQuery(),
|
||||
dsSettings ? getDataSourceRef(dsSettings) : { type: undefined, uid: undefined },
|
||||
index
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
public onAddQuery = (query: Partial<DataQuery>) => {
|
||||
public onAddQuery = (query: Partial<DataQuery>, index?: number) => {
|
||||
const queries = this.getQueries();
|
||||
const dsSettings = this.state.dsSettings;
|
||||
|
||||
this.onQueriesChange(
|
||||
addQuery(queries, query, dsSettings ? getDataSourceRef(dsSettings) : { type: undefined, uid: undefined })
|
||||
addQuery(queries, query, dsSettings ? getDataSourceRef(dsSettings) : { type: undefined, uid: undefined }, index)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -291,7 +299,7 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
|
||||
return (dsSettings.meta.backend || dsSettings.meta.alerting || dsSettings.meta.mixed) === true;
|
||||
}
|
||||
|
||||
public onAddExpressionOfType = (type: ExpressionQueryType) => {
|
||||
public onAddExpressionOfType = (type: ExpressionQueryType, index?: number) => {
|
||||
const queries = this.getQueries();
|
||||
// Create base expression query with the specified type
|
||||
const baseQuery = expressionDatasource.newQuery();
|
||||
@@ -299,7 +307,7 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
|
||||
// Apply defaults specific to the expression type
|
||||
const queryWithDefaults = getDefaults(queryWithType);
|
||||
|
||||
this.onQueriesChange(addQuery(queries, queryWithDefaults));
|
||||
this.onQueriesChange(addQuery(queries, queryWithDefaults, undefined, index));
|
||||
};
|
||||
|
||||
public renderExtraActions() {
|
||||
@@ -411,7 +419,7 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps<Panel
|
||||
<>
|
||||
<Button
|
||||
icon="plus"
|
||||
onClick={model.addQueryClick}
|
||||
onClick={(ev) => model.addQueryClick()}
|
||||
variant="secondary"
|
||||
data-testid={selectors.components.QueryTab.addQuery}
|
||||
>
|
||||
|
||||
+17
-1
@@ -7,6 +7,7 @@ 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 { usePanelDataPaneColors } from './theme';
|
||||
import { queryItemId, transformItemId } from './utils';
|
||||
@@ -20,10 +21,25 @@ interface QueryTransformCardProps {
|
||||
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, isSelected, onClick, onDuplicate, onRemove, onToggleVisibility }: QueryTransformCardProps) => {
|
||||
({
|
||||
item,
|
||||
type,
|
||||
index,
|
||||
isSelected,
|
||||
onClick,
|
||||
onDuplicate,
|
||||
onRemove,
|
||||
onToggleVisibility,
|
||||
onAddQuery,
|
||||
onAddTransform,
|
||||
onAddExpression,
|
||||
}: QueryTransformCardProps) => {
|
||||
const colors = usePanelDataPaneColors();
|
||||
const styles = useStyles2(getStyles, colors);
|
||||
|
||||
|
||||
+159
-69
@@ -1,6 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { DataTransformerConfig, GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
@@ -19,22 +19,21 @@ export interface QueryTransformItem {
|
||||
index: number;
|
||||
}
|
||||
|
||||
const CARD_HEIGHT = 70;
|
||||
|
||||
interface QueryTransformListProps {
|
||||
allItems: QueryTransformItem[];
|
||||
dataSourceItems: QueryTransformItem[];
|
||||
transformItems: QueryTransformItem[];
|
||||
selectedId: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
onAddQuery: () => void;
|
||||
onAddFromSavedQueries: () => void;
|
||||
onAddTransform: () => void;
|
||||
onAddExpression: (type: ExpressionQueryType) => void;
|
||||
onAddQuery: (index?: number) => void;
|
||||
onAddFromSavedQueries: (index?: number) => void;
|
||||
onAddTransform: (index?: number) => void;
|
||||
onAddExpression: (type: ExpressionQueryType, index?: number) => 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;
|
||||
onToggleTransformVisibility?: (index: number) => void;
|
||||
onReorderDataSources?: (startIndex: number, endIndex: number) => void;
|
||||
@@ -55,9 +54,6 @@ export const QueryTransformList = memo(
|
||||
onDuplicateQuery,
|
||||
onRemoveQuery,
|
||||
onToggleQueryVisibility,
|
||||
onDuplicateExpression,
|
||||
onRemoveExpression,
|
||||
onToggleExpressionVisibility,
|
||||
onRemoveTransform,
|
||||
onToggleTransformVisibility,
|
||||
onReorderDataSources,
|
||||
@@ -65,6 +61,11 @@ export const QueryTransformList = memo(
|
||||
}: QueryTransformListProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [hovered, setHovered] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('hovered changed:', hovered);
|
||||
}, [hovered]);
|
||||
|
||||
const onDragStart = () => {
|
||||
setIsDragging(true);
|
||||
@@ -143,20 +144,13 @@ export const QueryTransformList = memo(
|
||||
|
||||
const getHandlers = (item: QueryTransformItem) => {
|
||||
switch (item.type) {
|
||||
case 'expression':
|
||||
case 'query':
|
||||
return {
|
||||
onDuplicate: onDuplicateQuery ? () => onDuplicateQuery(item.index) : undefined,
|
||||
onRemove: onRemoveQuery ? () => onRemoveQuery(item.index) : undefined,
|
||||
onToggleVisibility: onToggleQueryVisibility ? () => onToggleQueryVisibility(item.index) : undefined,
|
||||
};
|
||||
case 'expression':
|
||||
return {
|
||||
onDuplicate: onDuplicateExpression ? () => onDuplicateExpression(item.index) : undefined,
|
||||
onRemove: onRemoveExpression ? () => onRemoveExpression(item.index) : undefined,
|
||||
onToggleVisibility: onToggleExpressionVisibility
|
||||
? () => onToggleExpressionVisibility(item.index)
|
||||
: undefined,
|
||||
};
|
||||
case 'transform':
|
||||
return {
|
||||
onDuplicate: undefined,
|
||||
@@ -181,7 +175,7 @@ export const QueryTransformList = memo(
|
||||
}, [allItems, dataSourceItems, transformItems]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.container} onMouseLeave={() => setHovered(null)}>
|
||||
<div className={styles.header}>
|
||||
<Stack justifyContent="space-between" alignItems="center" gap={2}>
|
||||
<span className={styles.headerTitle}>
|
||||
@@ -212,36 +206,80 @@ export const QueryTransformList = memo(
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
className={isDraggingFromOtherSection ? styles.droppableInvalid : undefined}
|
||||
onMouseMove={ev => {
|
||||
const rect = ev.currentTarget.getBoundingClientRect();
|
||||
const y = ev.clientY - rect.top;
|
||||
let hoveredIdx = Math.floor(y / CARD_HEIGHT);
|
||||
if (hoveredIdx < 0) {
|
||||
hoveredIdx = 0;
|
||||
}
|
||||
if (hoveredIdx > dataSourceItems.length) {
|
||||
hoveredIdx = dataSourceItems.length;
|
||||
}
|
||||
const hoveredId = hoveredIdx === dataSourceItems.length ? 'queries-last' : dataSourceItems[hoveredIdx].id;
|
||||
setHovered(hoveredId);
|
||||
}}
|
||||
className={cx(styles.cardList, isDraggingFromOtherSection ? styles.droppableInvalid : undefined)}
|
||||
>
|
||||
<Stack direction="column" gap={2}>
|
||||
{dataSourceItems.map((item, index) => (
|
||||
<Draggable key={item.id} draggableId={item.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
className={snapshot.isDragging ? styles.dragging : undefined}
|
||||
>
|
||||
<QueryTransformCard
|
||||
item={item.data}
|
||||
type={item.type}
|
||||
index={item.index}
|
||||
isSelected={selectedId === item.id}
|
||||
onClick={() => onSelect(item.id)}
|
||||
{...getHandlers(item)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
<div key={item.id} className={styles.cardContainer}>
|
||||
<Draggable draggableId={item.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
className={snapshot.isDragging ? styles.dragging : undefined}
|
||||
>
|
||||
<QueryTransformCard
|
||||
item={item.data}
|
||||
type={item.type}
|
||||
index={item.index}
|
||||
isSelected={selectedId === item.id}
|
||||
onClick={() => onSelect(item.id)}
|
||||
onAddExpression={onAddExpression}
|
||||
onAddQuery={onAddQuery}
|
||||
onAddTransform={onAddTransform}
|
||||
{...getHandlers(item)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
<div className={styles.floatingButton}>
|
||||
<AddDataItemMenu
|
||||
onAddQuery={onAddQuery}
|
||||
onAddTransform={onAddTransform}
|
||||
onAddExpression={onAddExpression}
|
||||
onAddFromSavedQueries={onAddFromSavedQueries}
|
||||
index={index}
|
||||
allowedTypes={['query', 'expression']}
|
||||
show={hovered === item.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</Stack>
|
||||
|
||||
<div className={cx(styles.cardContainer, styles.cardContainerLast)}>
|
||||
<div className={styles.floatingButton}>
|
||||
<AddDataItemMenu
|
||||
onAddQuery={onAddQuery}
|
||||
onAddFromSavedQueries={onAddFromSavedQueries}
|
||||
onAddTransform={onAddTransform}
|
||||
onAddExpression={onAddExpression}
|
||||
allowedTypes={['query', 'expression']}
|
||||
index={transformItems.length}
|
||||
show={hovered === 'queries-last'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Droppable>
|
||||
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
@@ -261,45 +299,80 @@ export const QueryTransformList = memo(
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
className={isDraggingFromOtherSection ? styles.droppableInvalid : undefined}
|
||||
className={cx(styles.cardList, isDraggingFromOtherSection ? styles.droppableInvalid : undefined)}
|
||||
onMouseMove={ev => {
|
||||
const rect = ev.currentTarget.getBoundingClientRect();
|
||||
const y = ev.clientY - rect.top;
|
||||
let hoveredIdx = Math.floor(((y - 16 + (CARD_HEIGHT / 2)) / CARD_HEIGHT));
|
||||
if (hoveredIdx < 0) {
|
||||
hoveredIdx = 0;
|
||||
}
|
||||
if (hoveredIdx > transformItems.length) {
|
||||
hoveredIdx = transformItems.length;
|
||||
}
|
||||
const hoveredId = hoveredIdx === transformItems.length ? 'transformations-last' : transformItems[hoveredIdx].id;
|
||||
setHovered(hoveredId);
|
||||
}}
|
||||
>
|
||||
<Stack direction="column" gap={2}>
|
||||
{transformItems.map((item, index) => (
|
||||
<Draggable key={item.id} draggableId={item.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
className={snapshot.isDragging ? styles.dragging : undefined}
|
||||
>
|
||||
<QueryTransformCard
|
||||
item={item.data}
|
||||
type={item.type}
|
||||
index={item.index}
|
||||
isSelected={selectedId === item.id}
|
||||
onClick={() => onSelect(item.id)}
|
||||
{...getHandlers(item)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
<div key={item.id} className={styles.cardContainer}>
|
||||
<Draggable key={item.id} draggableId={item.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
className={snapshot.isDragging ? styles.dragging : undefined}
|
||||
>
|
||||
<QueryTransformCard
|
||||
item={item.data}
|
||||
type={item.type}
|
||||
index={item.index}
|
||||
isSelected={selectedId === item.id}
|
||||
onClick={() => onSelect(item.id)}
|
||||
onAddExpression={onAddExpression}
|
||||
onAddQuery={onAddQuery}
|
||||
onAddTransform={onAddTransform}
|
||||
{...getHandlers(item)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
<div className={styles.floatingButton}>
|
||||
<AddDataItemMenu
|
||||
onAddQuery={onAddQuery}
|
||||
onAddTransform={onAddTransform}
|
||||
onAddExpression={onAddExpression}
|
||||
onAddFromSavedQueries={onAddFromSavedQueries}
|
||||
index={index}
|
||||
allowedTypes={['transform']}
|
||||
show={hovered === item.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</Stack>
|
||||
<div className={cx(styles.cardContainer, styles.cardContainerLast)}>
|
||||
<div className={styles.floatingButton}>
|
||||
<AddDataItemMenu
|
||||
onAddQuery={onAddQuery}
|
||||
onAddFromSavedQueries={onAddFromSavedQueries}
|
||||
onAddTransform={onAddTransform}
|
||||
onAddExpression={onAddExpression}
|
||||
allowedTypes={['transform']}
|
||||
index={transformItems.length}
|
||||
show={hovered === 'transformations-last'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Droppable>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<AddDataItemMenu
|
||||
onAddQuery={onAddQuery}
|
||||
onAddFromSavedQueries={onAddFromSavedQueries}
|
||||
onAddTransform={onAddTransform}
|
||||
onAddExpression={onAddExpression}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
</DragDropContext>
|
||||
@@ -384,7 +457,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
minHeight: '100%',
|
||||
}),
|
||||
content: css({
|
||||
padding: `${theme.spacing(2)} ${theme.spacing(8)} ${theme.spacing(2)} ${theme.spacing(2)}`,
|
||||
padding: theme.spacing(2, 8, 2, 2),
|
||||
position: 'relative',
|
||||
}),
|
||||
dragging: css({
|
||||
@@ -434,5 +507,22 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(0.5),
|
||||
}),
|
||||
cardList: css({
|
||||
paddingLeft: theme.spacing(4),
|
||||
marginLeft: theme.spacing(-2),
|
||||
position: 'relative',
|
||||
}),
|
||||
cardContainer: css({
|
||||
position: 'relative',
|
||||
overflowX: 'visible',
|
||||
}),
|
||||
cardContainerLast: css({
|
||||
marginTop: theme.spacing(2),
|
||||
}),
|
||||
floatingButton: css({
|
||||
position: 'absolute',
|
||||
top: theme.spacing(-2),
|
||||
left: theme.spacing(-2.5),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user