fix up long sidebar
This commit is contained in:
@@ -108,7 +108,7 @@ export function ConnectionLines({ connections, isDragging = false, selected, onC
|
||||
refIds.add(to);
|
||||
});
|
||||
|
||||
const swimlaneX = containerRect.width - 40; // Adjusted for increased right padding (64px)
|
||||
const swimlaneX = containerRect.width - 24; // Adjusted for increased right padding (48px)
|
||||
|
||||
const cardPositions: number[] = [];
|
||||
refIds.forEach((refId) => {
|
||||
|
||||
@@ -195,6 +195,9 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
width: '100%',
|
||||
background: theme.colors.background.primary,
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
borderTopLeftRadius: theme.shape.radius.md,
|
||||
borderTopRightRadius: theme.shape.radius.md,
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
emptyState: css({
|
||||
display: 'flex',
|
||||
|
||||
@@ -1,62 +1,76 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useCallback, useMemo, useState, useEffect } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { DataQuery, DataTransformerConfig, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { DataTransformerConfig, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import {
|
||||
SceneComponentProps,
|
||||
SceneDataQuery,
|
||||
SceneObjectBase,
|
||||
SceneObjectRef,
|
||||
SceneObjectState,
|
||||
SceneObjectUrlSyncConfig,
|
||||
SceneObjectUrlValues,
|
||||
VizPanel,
|
||||
SceneDataQuery,
|
||||
} from '@grafana/scenes';
|
||||
import { useStyles2, useSplitter } from '@grafana/ui';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { getConfig } from 'app/core/config';
|
||||
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 { ExpressionQueryType } from 'app/features/expressions/types';
|
||||
|
||||
import { isExpressionQuery } from '../../../expressions/guards';
|
||||
import { ExpressionQueryType } from '../../../expressions/types';
|
||||
import { getQueryRunnerFor } from '../../utils/utils';
|
||||
|
||||
import { DetailView, QueryLibraryMode } from './DetailView';
|
||||
import { PanelDataAlertingTab } from './PanelDataAlertingTab';
|
||||
import { PanelDataQueriesTab } from './PanelDataQueriesTab';
|
||||
import { PanelDataSidebarRendered } from './PanelDataSidebar';
|
||||
import { PanelDataTransformationsTab } from './PanelDataTransformationsTab';
|
||||
import { QueryTransformList } from './QueryTransformList';
|
||||
import { TransformationsDrawer } from './TransformationsDrawer';
|
||||
import { PanelDataPaneTab, TabId, QueryItem, TransformItem } from './types';
|
||||
import { useQueryTransformItems } from './hooks';
|
||||
import { PanelDataPaneTab, TabId } from './types';
|
||||
import { isDataTransformerConfig, queryItemId, transformItemId } from './utils';
|
||||
|
||||
export interface PanelDataPaneState extends SceneObjectState {
|
||||
tabs: PanelDataPaneTab[];
|
||||
tab: TabId;
|
||||
sidebarCollapsed: boolean;
|
||||
selectedQueryTransform: string | null;
|
||||
panelRef: SceneObjectRef<VizPanel>;
|
||||
transformPickerIndex?: number | null;
|
||||
queryLibraryMode: QueryLibraryMode & { index: number | null };
|
||||
}
|
||||
|
||||
export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
||||
static Component = PanelDataPaneRendered;
|
||||
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['tab'] });
|
||||
static FooterComponent = PanelDataSidebarRendered;
|
||||
|
||||
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['tab', 'selectedQueryTransform'] });
|
||||
|
||||
public static createFor(panel: VizPanel) {
|
||||
const panelRef = panel.getRef();
|
||||
|
||||
const tabs: PanelDataPaneTab[] = [
|
||||
new PanelDataQueriesTab({ panelRef }),
|
||||
new PanelDataTransformationsTab({ panelRef }),
|
||||
new PanelDataQueriesTab({ panelRef: panel.getRef() }),
|
||||
new PanelDataTransformationsTab({ panelRef: panel.getRef() }),
|
||||
];
|
||||
|
||||
if (shouldShowAlertingTab(panel.state.pluginId)) {
|
||||
tabs.push(new PanelDataAlertingTab({ panelRef }));
|
||||
tabs.push(new PanelDataAlertingTab({ panelRef: panel.getRef() }));
|
||||
}
|
||||
const tab = tabs[0]?.tabId ?? TabId.Queries;
|
||||
|
||||
return new PanelDataPane({
|
||||
selectedQueryTransform: null,
|
||||
panelRef,
|
||||
tabs,
|
||||
tab: TabId.Queries,
|
||||
tab,
|
||||
sidebarCollapsed: false,
|
||||
queryLibraryMode: {
|
||||
active: false,
|
||||
mode: 'browse',
|
||||
index: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,6 +78,22 @@ export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
||||
this.setState({ tab: tab.tabId });
|
||||
};
|
||||
|
||||
public onChangeSelected = (selectedId: string | null) => {
|
||||
this.setState({ selectedQueryTransform: selectedId });
|
||||
};
|
||||
|
||||
public onCollapseSidebar = (newState: boolean) => {
|
||||
this.setState({ sidebarCollapsed: newState });
|
||||
};
|
||||
|
||||
public onTransformPicker = (index?: number | null) => {
|
||||
this.setState({ transformPickerIndex: index });
|
||||
};
|
||||
|
||||
public setQueryLibraryMode = (mode: QueryLibraryMode & { index: number | null }) => {
|
||||
this.setState({ queryLibraryMode: mode });
|
||||
}
|
||||
|
||||
public getUrlState() {
|
||||
return { tab: this.state.tab };
|
||||
}
|
||||
@@ -82,29 +112,13 @@ export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
||||
}
|
||||
}
|
||||
|
||||
interface DrawerState {
|
||||
open: boolean;
|
||||
index: number | null;
|
||||
}
|
||||
|
||||
function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
const { tabs, panelRef } = model.useState();
|
||||
const { tabs, selectedQueryTransform, panelRef, transformPickerIndex, queryLibraryMode } = model.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [queryLibraryMode, setQueryLibraryMode] = useState<QueryLibraryMode & { index: number | null }>({
|
||||
active: false,
|
||||
mode: 'browse',
|
||||
index: null,
|
||||
});
|
||||
const [transformDrawerState, setTransformDrawerState] = useState<DrawerState>({
|
||||
open: false,
|
||||
index: null,
|
||||
});
|
||||
const [isAddingTransform, setIsAddingTransform] = useState(false);
|
||||
|
||||
const panel = panelRef.resolve();
|
||||
|
||||
// Subscribe to query runner and tab state changes
|
||||
const panel = panelRef.resolve();
|
||||
const queryRunner = getQueryRunnerFor(panel);
|
||||
const queryRunnerState = queryRunner?.useState();
|
||||
const queriesTab = tabs.find((t): t is PanelDataQueriesTab => t.tabId === TabId.Queries);
|
||||
@@ -114,92 +128,24 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
const queries = queryRunnerState?.queries;
|
||||
const transformations = transformerState?.transformations?.filter(isDataTransformerConfig);
|
||||
|
||||
// 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.
|
||||
useEffect(() => {
|
||||
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 = prevState.queries.map(({ refId }) => refId);
|
||||
if (newState.queries.length !== oldOrderedRefIds.length) {
|
||||
return; // add, remove, something else.
|
||||
}
|
||||
|
||||
let refIdChanges = 0;
|
||||
let updatedQuery: SceneDataQuery | undefined = undefined;
|
||||
for (let i = 0; i < newState.queries.length; i++) {
|
||||
const newQuery = newState.queries[i];
|
||||
const oldRefId = oldOrderedRefIds[i];
|
||||
if (newQuery.refId !== oldRefId) {
|
||||
if (++refIdChanges < 2) {
|
||||
updatedQuery = newQuery;
|
||||
} else {
|
||||
return; // more than 2 refId changes, so it's a reorder or something else.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedQuery) {
|
||||
setSelectedId(queryItemId(updatedQuery));
|
||||
}
|
||||
});
|
||||
}, [queryRunner]);
|
||||
|
||||
// Build separate lists for queries/expressions and transformations
|
||||
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];
|
||||
queryExpressionItems.push({
|
||||
id: queryItemId(query),
|
||||
type: isExpressionQuery(query) ? 'expression' : 'query',
|
||||
data: query,
|
||||
index: i, // Store actual index in queries array
|
||||
});
|
||||
}
|
||||
|
||||
// Add transformations
|
||||
for (let i = 0; i < (transformations?.length ?? 0); i++) {
|
||||
const transform = transformations![i];
|
||||
if (isDataTransformerConfig(transform)) {
|
||||
transformItems.push({
|
||||
id: transformItemId(i),
|
||||
type: 'transform',
|
||||
data: transform,
|
||||
index: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
queryExpressionItems,
|
||||
transformItems,
|
||||
allItems: [...queryExpressionItems, ...transformItems],
|
||||
};
|
||||
}, [queries, transformations]);
|
||||
|
||||
// Auto-select first item if nothing is selected
|
||||
const effectiveSelectedId = useMemo(() => {
|
||||
if (selectedId === null && allItems.length > 0) {
|
||||
return allItems[0].id;
|
||||
}
|
||||
return selectedId;
|
||||
}, [selectedId, allItems]);
|
||||
|
||||
const selectedItem = useMemo(
|
||||
() => allItems.find((item) => item.id === effectiveSelectedId),
|
||||
[allItems, effectiveSelectedId]
|
||||
const { allItems } = useQueryTransformItems(queries, transformations);
|
||||
const handleSelect = useCallback(
|
||||
(id: string | null) => {
|
||||
model.setState({ selectedQueryTransform: id });
|
||||
model.onTransformPicker(null);
|
||||
},
|
||||
[model]
|
||||
);
|
||||
|
||||
const handleSelect = useCallback((id: string) => {
|
||||
setSelectedId(id);
|
||||
setIsAddingTransform(false);
|
||||
}, []);
|
||||
// Auto-select first item if nothing is selected
|
||||
const selectedId = useMemo(() => {
|
||||
if (selectedQueryTransform === null && allItems.length > 0) {
|
||||
return allItems[0].id;
|
||||
}
|
||||
return selectedQueryTransform;
|
||||
}, [selectedQueryTransform, allItems]);
|
||||
|
||||
const selectedItem = useMemo(() => allItems.find((item) => item.id === selectedId), [allItems, selectedId]);
|
||||
|
||||
const updateQuerySelectionOnStateChange = useCallback(
|
||||
(index: number) => {
|
||||
@@ -208,104 +154,19 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
const newQueries = newState.queries;
|
||||
if (newQueries.length > 0) {
|
||||
const selected = newQueries[index] ?? newQueries[0];
|
||||
setSelectedId(queryItemId(selected));
|
||||
handleSelect(queryItemId(selected));
|
||||
}
|
||||
unsub.unsubscribe();
|
||||
});
|
||||
}
|
||||
},
|
||||
[queryRunner]
|
||||
[queryRunner, handleSelect]
|
||||
);
|
||||
|
||||
/** 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 handleGoToQueries = useCallback(() => {
|
||||
// Close the transformation picker
|
||||
setIsAddingTransform(false);
|
||||
// Add a SQL expression
|
||||
if (queriesTab) {
|
||||
updateQuerySelectionOnStateChange(queries?.length ?? 0);
|
||||
queriesTab.onAddExpressionOfType(ExpressionQueryType.sql);
|
||||
}
|
||||
}, [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)) {
|
||||
const prevQuery = newQueries?.[index - 1];
|
||||
setSelectedId(prevQuery ? queryItemId(prevQuery) : 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]
|
||||
);
|
||||
|
||||
// Handler for selecting a query from the query library
|
||||
const handleQueryLibrarySelect = useCallback(
|
||||
(query: DataQuery) => {
|
||||
(query: SceneDataQuery) => {
|
||||
if (!queryRunner || !queriesTab) {
|
||||
return;
|
||||
}
|
||||
@@ -323,7 +184,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
}
|
||||
|
||||
// Create new query with the selected refId
|
||||
const newQuery: DataQuery = {
|
||||
const newQuery: SceneDataQuery = {
|
||||
...query,
|
||||
refId: nextRefId,
|
||||
};
|
||||
@@ -331,45 +192,54 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
updateQuerySelectionOnStateChange(selectedIndex);
|
||||
queriesTab.onAddQuery(newQuery);
|
||||
|
||||
setQueryLibraryMode({ active: false, mode: 'browse', index: null });
|
||||
model.setQueryLibraryMode({ active: false, mode: 'browse', index: null });
|
||||
},
|
||||
[queryRunner, queryLibraryMode.index, queries, updateQuerySelectionOnStateChange, queriesTab]
|
||||
[queryRunner, queryLibraryMode.index, queries, updateQuerySelectionOnStateChange, queriesTab, model]
|
||||
);
|
||||
|
||||
// Handler for saving a query to the query library (stub)
|
||||
const handleQueryLibrarySave = useCallback((_name: string, _description: string) => {
|
||||
// Stub: In real implementation, this would save to the query library
|
||||
setQueryLibraryMode({ active: false, mode: 'browse', index: null });
|
||||
}, []);
|
||||
model.setQueryLibraryMode({ active: false, mode: 'browse', index: null });
|
||||
}, [model]);
|
||||
|
||||
// Handler to close the query library view
|
||||
const handleQueryLibraryClose = useCallback(() => {
|
||||
setQueryLibraryMode({ active: false, mode: 'browse', index: null });
|
||||
}, []);
|
||||
model.setQueryLibraryMode({ active: false, mode: 'browse', index: null });
|
||||
}, [model]);
|
||||
|
||||
// Handler to open query library in a specific mode
|
||||
const handleOpenQueryLibrary = useCallback(
|
||||
(mode: 'browse' | 'save', index?: number) => {
|
||||
let currentQuery: DataQuery | undefined;
|
||||
(mode: QueryLibraryMode["mode"], index?: number) => {
|
||||
let currentQuery: SceneDataQuery | undefined;
|
||||
|
||||
if (
|
||||
mode === 'save' &&
|
||||
selectedItem &&
|
||||
(selectedItem.type === 'query' || selectedItem.type === 'expression') &&
|
||||
'refId' in selectedItem.data
|
||||
(selectedItem?.type === 'query' || selectedItem?.type === 'expression')
|
||||
) {
|
||||
currentQuery = selectedItem.data;
|
||||
}
|
||||
|
||||
setQueryLibraryMode({
|
||||
model.setQueryLibraryMode({
|
||||
active: true,
|
||||
mode,
|
||||
currentQuery,
|
||||
index: index ?? null,
|
||||
});
|
||||
},
|
||||
[selectedItem]
|
||||
[selectedItem, model]
|
||||
);
|
||||
|
||||
const handleGoToQueries = useCallback(() => {
|
||||
// Close the transformation picker
|
||||
model.onTransformPicker(null);
|
||||
// Add a SQL expression
|
||||
if (queriesTab) {
|
||||
updateQuerySelectionOnStateChange(queries?.length ?? 0);
|
||||
queriesTab.onAddExpressionOfType(ExpressionQueryType.sql);
|
||||
}
|
||||
}, [queriesTab, updateQuerySelectionOnStateChange, queries, model]);
|
||||
|
||||
/** TRANSFORMS **/
|
||||
const handleAddTransform = useCallback(
|
||||
(selected: SelectableValue<string>, customOptions?: Record<string, unknown>) => {
|
||||
@@ -378,7 +248,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
}
|
||||
|
||||
if (transformsTab && transformer) {
|
||||
const selectedIndex = transformDrawerState.index ?? transformations?.length ?? 0;
|
||||
const selectedIndex = transformPickerIndex ?? transformations?.length ?? 0;
|
||||
const newTransformation: DataTransformerConfig = {
|
||||
id: selected.value,
|
||||
options: customOptions ?? {},
|
||||
@@ -387,9 +257,8 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
const unsub = transformer.subscribeToState((newState) => {
|
||||
const newTransform = newState.transformations[selectedIndex];
|
||||
|
||||
setSelectedId(!!newTransform ? transformItemId(selectedIndex) : null);
|
||||
setTransformDrawerState({ open: false, index: null });
|
||||
setIsAddingTransform(false);
|
||||
model.onChangeSelected(!!newTransform ? transformItemId(selectedIndex) : null);
|
||||
model.onTransformPicker(null);
|
||||
unsub.unsubscribe();
|
||||
});
|
||||
|
||||
@@ -399,7 +268,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
transformsTab.onChangeTransformations(newTransformations);
|
||||
}
|
||||
},
|
||||
[transformsTab, transformer, transformations, transformDrawerState.index]
|
||||
[transformsTab, transformer, transformPickerIndex, transformations, model]
|
||||
);
|
||||
|
||||
const handleRemoveTransform = useCallback(
|
||||
@@ -411,11 +280,11 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
// Clear selection if removing the selected transformation
|
||||
if (selectedId === transformItemId(index)) {
|
||||
const prevTransform = newTransformations[index - 1];
|
||||
setSelectedId(prevTransform ? transformItemId(index - 1) : null);
|
||||
handleSelect(prevTransform ? transformItemId(index - 1) : null);
|
||||
}
|
||||
}
|
||||
},
|
||||
[transformations, transformsTab, selectedId]
|
||||
[transformations, transformsTab, selectedId, handleSelect]
|
||||
);
|
||||
|
||||
const handleToggleTransformVisibility = useCallback(
|
||||
@@ -429,116 +298,31 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
[transformations, transformsTab]
|
||||
);
|
||||
|
||||
const handleReorderDataSources = useCallback(
|
||||
(startIndex: number, endIndex: number) => {
|
||||
if (queryRunner) {
|
||||
const queries = queryRunner.state.queries || [];
|
||||
const newQueries = Array.from(queries);
|
||||
const [removed] = newQueries.splice(startIndex, 1);
|
||||
newQueries.splice(endIndex, 0, removed);
|
||||
queryRunner.setState({ queries: newQueries });
|
||||
}
|
||||
},
|
||||
[queryRunner]
|
||||
);
|
||||
|
||||
const handleReorderTransforms = useCallback(
|
||||
(startIndex: number, endIndex: number) => {
|
||||
if (transformsTab) {
|
||||
const newTransformations = [...(transformations ?? [])];
|
||||
const [removed] = newTransformations.splice(startIndex, 1);
|
||||
newTransformations.splice(endIndex, 0, removed);
|
||||
transformsTab.onChangeTransformations(newTransformations);
|
||||
}
|
||||
},
|
||||
[transformations, transformsTab]
|
||||
);
|
||||
|
||||
// Get data for transformations drawer
|
||||
const sourceData = queryRunner?.useState();
|
||||
const series = sourceData?.data?.series || [];
|
||||
|
||||
const { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({
|
||||
direction: 'row',
|
||||
initialSize: 0.01,
|
||||
handleSize: 'xs',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.dataPane} data-testid={selectors.components.PanelEditor.DataPane.content}>
|
||||
<div {...containerProps} className={cx(containerProps.className, styles.unifiedLayout)}>
|
||||
<div {...primaryProps} className={cx(primaryProps.className, styles.leftPane)}>
|
||||
<QueryTransformList
|
||||
allItems={allItems}
|
||||
dataSourceItems={queryExpressionItems}
|
||||
transformItems={transformItems}
|
||||
selectedId={effectiveSelectedId}
|
||||
onSelect={handleSelect}
|
||||
onAddQuery={handleAddQuery}
|
||||
onAddFromSavedQueries={(index) => handleOpenQueryLibrary('browse', index)}
|
||||
onAddTransform={(index) => {
|
||||
setIsAddingTransform(true);
|
||||
setSelectedId(null);
|
||||
}}
|
||||
onAddExpression={handleAddExpression}
|
||||
onDuplicateQuery={handleDuplicateQuery}
|
||||
onRemoveQuery={handleRemoveQuery}
|
||||
onToggleQueryVisibility={handleToggleQueryVisibility}
|
||||
onRemoveTransform={handleRemoveTransform}
|
||||
onToggleTransformVisibility={handleToggleTransformVisibility}
|
||||
onReorderDataSources={handleReorderDataSources}
|
||||
onReorderTransforms={handleReorderTransforms}
|
||||
onAddOrganizeFieldsTransform={() =>
|
||||
handleAddTransform(
|
||||
{ value: 'organize' },
|
||||
{
|
||||
excludeByName: {},
|
||||
indexByName: {},
|
||||
renameByName: {},
|
||||
includeByName: {},
|
||||
orderByMode: 'auto',
|
||||
orderBy: [
|
||||
{
|
||||
type: 'name',
|
||||
desc: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
}
|
||||
<div className={cx(styles.unifiedLayout)}>
|
||||
<DetailView
|
||||
selectedItem={selectedItem}
|
||||
panel={panel}
|
||||
tabs={tabs}
|
||||
onRemoveTransform={handleRemoveTransform}
|
||||
onToggleTransformVisibility={handleToggleTransformVisibility}
|
||||
isAddingTransform={transformPickerIndex != null}
|
||||
onAddTransformation={handleAddTransform}
|
||||
onCancelAddTransform={() => model.onTransformPicker(null)}
|
||||
transformationData={series}
|
||||
onGoToQueries={handleGoToQueries}
|
||||
queryLibraryMode={queryLibraryMode}
|
||||
onQueryLibrarySelect={handleQueryLibrarySelect}
|
||||
onQueryLibrarySave={handleQueryLibrarySave}
|
||||
onQueryLibraryClose={handleQueryLibraryClose}
|
||||
onOpenQueryLibrary={handleOpenQueryLibrary}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
{...splitterProps}
|
||||
className={cx(splitterProps.className, styles.splitter)}
|
||||
style={{ ...splitterProps.style, width: '16px' }}
|
||||
/>
|
||||
<div {...secondaryProps}>
|
||||
<DetailView
|
||||
selectedItem={selectedItem}
|
||||
panel={panel}
|
||||
tabs={tabs}
|
||||
onRemoveTransform={handleRemoveTransform}
|
||||
onToggleTransformVisibility={handleToggleTransformVisibility}
|
||||
isAddingTransform={isAddingTransform}
|
||||
onAddTransformation={handleAddTransform}
|
||||
onCancelAddTransform={() => setIsAddingTransform(false)}
|
||||
transformationData={series}
|
||||
onGoToQueries={handleGoToQueries}
|
||||
queryLibraryMode={queryLibraryMode}
|
||||
onQueryLibrarySelect={handleQueryLibrarySelect}
|
||||
onQueryLibrarySave={handleQueryLibrarySave}
|
||||
onQueryLibraryClose={handleQueryLibraryClose}
|
||||
onOpenQueryLibrary={handleOpenQueryLibrary}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TransformationsDrawer
|
||||
isOpen={transformDrawerState.open}
|
||||
onClose={() => setTransformDrawerState({ open: false, index: null })}
|
||||
onTransformationAdd={handleAddTransform}
|
||||
series={series}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -557,10 +341,6 @@ export function shouldShowAlertingTab(pluginId: string) {
|
||||
return isGraph || isTimeseries;
|
||||
}
|
||||
|
||||
// Left pane sizing: cards grow from 180px-300px, plus content padding (48px + 64px = 112px)
|
||||
const LEFT_PANE_MIN = 180 + 112; // 292px (180px card min + 112px padding)
|
||||
const LEFT_PANE_MAX = 300 + 112; // 412px (300px card max + 112px padding)
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
dataPane: css({
|
||||
@@ -570,6 +350,7 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
minHeight: 0,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
paddingLeft: theme.spacing(2),
|
||||
}),
|
||||
unifiedLayout: css({
|
||||
flex: 1,
|
||||
@@ -577,16 +358,5 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
background: theme.colors.background.primary,
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
splitter: css({
|
||||
position: 'relative',
|
||||
background: theme.colors.background.canvas,
|
||||
cursor: 'col-resize',
|
||||
}),
|
||||
leftPane: css({
|
||||
// !important on minWidth to override useSplitter's inline minWidth: 'min-content'
|
||||
minWidth: `${LEFT_PANE_MIN}px !important`,
|
||||
maxWidth: `${LEFT_PANE_MAX}px`,
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -837,7 +837,7 @@ async function setupScene(panelId: string) {
|
||||
deactivators.push(dashboard.activate());
|
||||
deactivators.push(panelEditor.activate());
|
||||
|
||||
const queriesTab = panelEditor.state.dataPane!.state.tabs[0] as PanelDataQueriesTab;
|
||||
const queriesTab = panelEditor.state.dataPane!.state.editorRef.resolve().state.dataPaneTabs[0];
|
||||
deactivators.push(queriesTab.activate());
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useCallback, useState, useEffect, useMemo } from 'react';
|
||||
|
||||
import { DataQuery, DataTransformerConfig, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { SceneComponentProps, SceneDataQuery } from '@grafana/scenes';
|
||||
import { Button, useStyles2 } from '@grafana/ui';
|
||||
import { getConfig } from 'app/core/config';
|
||||
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 { ExpressionQueryType } from '../../../expressions/types';
|
||||
import { getQueryRunnerFor } from '../../utils/utils';
|
||||
|
||||
import { QueryLibraryMode } from './DetailView';
|
||||
import { PanelDataPane } from './PanelDataPane';
|
||||
import { PanelDataQueriesTab } from './PanelDataQueriesTab';
|
||||
import { PanelDataTransformationsTab } from './PanelDataTransformationsTab';
|
||||
import { QueryTransformList } from './QueryTransformList';
|
||||
import { useQueryTransformItems } from './hooks';
|
||||
import { TabId } from './types';
|
||||
import { isDataTransformerConfig, queryItemId, transformItemId } from './utils';
|
||||
|
||||
export function PanelDataSidebarRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
const { panelRef, tabs, selectedQueryTransform, sidebarCollapsed, transformPickerIndex } = model.useState();
|
||||
const styles = useStyles2(getStyles, sidebarCollapsed);
|
||||
|
||||
const panel = panelRef.resolve();
|
||||
|
||||
// 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?.filter(isDataTransformerConfig);
|
||||
|
||||
// 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.
|
||||
useEffect(() => {
|
||||
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 = prevState.queries.map(({ refId }) => refId);
|
||||
if (newState.queries.length !== oldOrderedRefIds.length) {
|
||||
return; // add, remove, something else.
|
||||
}
|
||||
|
||||
let refIdChanges = 0;
|
||||
let updatedQuery: SceneDataQuery | undefined = undefined;
|
||||
for (let i = 0; i < newState.queries.length; i++) {
|
||||
const newQuery = newState.queries[i];
|
||||
const oldRefId = oldOrderedRefIds[i];
|
||||
if (newQuery.refId !== oldRefId) {
|
||||
if (++refIdChanges < 2) {
|
||||
updatedQuery = newQuery;
|
||||
} else {
|
||||
return; // more than 2 refId changes, so it's a reorder or something else.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedQuery) {
|
||||
model.onChangeSelected(queryItemId(updatedQuery));
|
||||
}
|
||||
});
|
||||
}, [queryRunner, model]);
|
||||
|
||||
// Build separate lists for queries/expressions and transformations
|
||||
const { queryExpressionItems, transformItems, allItems } = useQueryTransformItems(queries, transformations);
|
||||
|
||||
// Auto-select first item if nothing is selected
|
||||
const selectedId = useMemo(() => {
|
||||
if (selectedQueryTransform === null && allItems.length > 0) {
|
||||
return allItems[0].id;
|
||||
}
|
||||
return selectedQueryTransform;
|
||||
}, [selectedQueryTransform, allItems]);
|
||||
const selectedItem = useMemo(() => allItems.find((item) => item.id === selectedId), [allItems, selectedId]);
|
||||
|
||||
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];
|
||||
model.onChangeSelected(queryItemId(selected));
|
||||
}
|
||||
unsub.unsubscribe();
|
||||
});
|
||||
}
|
||||
},
|
||||
[queryRunner, model]
|
||||
);
|
||||
|
||||
/** 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)) {
|
||||
const prevQuery = newQueries?.[index - 1];
|
||||
model.onChangeSelected(prevQuery ? queryItemId(prevQuery) : null);
|
||||
}
|
||||
}
|
||||
},
|
||||
[queryRunner, selectedId, queries, model]
|
||||
);
|
||||
|
||||
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]
|
||||
);
|
||||
|
||||
const handleOpenQueryLibrary = useCallback(
|
||||
(mode: QueryLibraryMode["mode"], index?: number) => {
|
||||
let currentQuery: SceneDataQuery | undefined;
|
||||
|
||||
if (
|
||||
mode === 'save' &&
|
||||
(selectedItem?.type === 'query' || selectedItem?.type === 'expression')
|
||||
) {
|
||||
currentQuery = selectedItem.data;
|
||||
}
|
||||
|
||||
model.setQueryLibraryMode({
|
||||
active: true,
|
||||
mode,
|
||||
currentQuery,
|
||||
index: index ?? null,
|
||||
});
|
||||
},
|
||||
[selectedItem, model]
|
||||
);
|
||||
|
||||
/** TRANSFORMS **/
|
||||
const handleAddTransform = useCallback(
|
||||
(selected: SelectableValue<string>, customOptions?: Record<string, unknown>) => {
|
||||
if (!selected.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (transformsTab && transformer) {
|
||||
const selectedIndex = transformPickerIndex ?? transformations?.length ?? 0;
|
||||
const newTransformation: DataTransformerConfig = {
|
||||
id: selected.value,
|
||||
options: customOptions ?? {},
|
||||
};
|
||||
|
||||
const unsub = transformer.subscribeToState((newState) => {
|
||||
const newTransform = newState.transformations[selectedIndex];
|
||||
|
||||
model.onChangeSelected(!!newTransform ? transformItemId(selectedIndex) : null);
|
||||
model.onTransformPicker(null);
|
||||
unsub.unsubscribe();
|
||||
});
|
||||
|
||||
const newTransformations = [...(transformations ?? [])];
|
||||
newTransformations.splice(selectedIndex, 0, newTransformation);
|
||||
|
||||
transformsTab.onChangeTransformations(newTransformations);
|
||||
}
|
||||
},
|
||||
[transformsTab, transformer, transformPickerIndex, transformations, model]
|
||||
);
|
||||
|
||||
const handleRemoveTransform = useCallback(
|
||||
(index: number) => {
|
||||
if (transformsTab) {
|
||||
const newTransformations = transformations?.filter((_, i) => i !== index) ?? [];
|
||||
transformsTab.onChangeTransformations(newTransformations);
|
||||
|
||||
// Clear selection if removing the selected transformation
|
||||
if (selectedId === transformItemId(index)) {
|
||||
const prevTransform = newTransformations[index - 1];
|
||||
model.onChangeSelected(prevTransform ? transformItemId(index - 1) : null);
|
||||
}
|
||||
}
|
||||
},
|
||||
[transformations, transformsTab, selectedId, model]
|
||||
);
|
||||
|
||||
const handleToggleTransformVisibility = useCallback(
|
||||
(index: number) => {
|
||||
if (transformsTab) {
|
||||
const newTransformations =
|
||||
transformations?.map((t, i) => (i === index ? { ...t, disabled: t.disabled ? undefined : true } : t)) ?? [];
|
||||
transformsTab.onChangeTransformations(newTransformations);
|
||||
}
|
||||
},
|
||||
[transformations, transformsTab]
|
||||
);
|
||||
|
||||
const handleReorderDataSources = useCallback(
|
||||
(startIndex: number, endIndex: number) => {
|
||||
if (queryRunner) {
|
||||
const queries = queryRunner.state.queries || [];
|
||||
const newQueries = Array.from(queries);
|
||||
const [removed] = newQueries.splice(startIndex, 1);
|
||||
newQueries.splice(endIndex, 0, removed);
|
||||
queryRunner.setState({ queries: newQueries });
|
||||
}
|
||||
},
|
||||
[queryRunner]
|
||||
);
|
||||
|
||||
const handleReorderTransforms = useCallback(
|
||||
(startIndex: number, endIndex: number) => {
|
||||
if (transformsTab) {
|
||||
const newTransformations = [...(transformations ?? [])];
|
||||
const [removed] = newTransformations.splice(startIndex, 1);
|
||||
newTransformations.splice(endIndex, 0, removed);
|
||||
transformsTab.onChangeTransformations(newTransformations);
|
||||
}
|
||||
},
|
||||
[transformations, transformsTab]
|
||||
);
|
||||
|
||||
// Get data for transformations drawer
|
||||
const sourceData = queryRunner?.useState();
|
||||
const series = sourceData?.data?.series || [];
|
||||
|
||||
if (sidebarCollapsed) {
|
||||
return (
|
||||
<div className={styles.sidebarPane}>
|
||||
<Button
|
||||
icon="angle-right"
|
||||
variant="secondary"
|
||||
onClick={() => model.onCollapseSidebar(false)}
|
||||
tooltip={t('app.features.dashboardScene.panelEdit.panelDataPane.expandSidebar', 'Expand sidebar')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.sidebarPane}>
|
||||
<QueryTransformList
|
||||
allItems={allItems}
|
||||
dataSourceItems={queryExpressionItems}
|
||||
transformItems={transformItems}
|
||||
selectedId={selectedId}
|
||||
onCollapseSidebar={() => model.onCollapseSidebar(true)}
|
||||
onSelect={(id) => {
|
||||
model.onChangeSelected(id)
|
||||
}}
|
||||
onAddQuery={handleAddQuery}
|
||||
onAddFromSavedQueries={(index) => handleOpenQueryLibrary('browse', index)}
|
||||
onAddTransform={(index) => {
|
||||
model.onChangeSelected(null);
|
||||
model.onTransformPicker(index);
|
||||
}}
|
||||
onAddExpression={handleAddExpression}
|
||||
onDuplicateQuery={handleDuplicateQuery}
|
||||
onRemoveQuery={handleRemoveQuery}
|
||||
onToggleQueryVisibility={handleToggleQueryVisibility}
|
||||
onRemoveTransform={handleRemoveTransform}
|
||||
onToggleTransformVisibility={handleToggleTransformVisibility}
|
||||
onReorderDataSources={handleReorderDataSources}
|
||||
onReorderTransforms={handleReorderTransforms}
|
||||
onAddOrganizeFieldsTransform={() =>
|
||||
handleAddTransform(
|
||||
{ value: 'organize' },
|
||||
{
|
||||
excludeByName: {},
|
||||
indexByName: {},
|
||||
renameByName: {},
|
||||
includeByName: {},
|
||||
orderByMode: 'auto',
|
||||
orderBy: [
|
||||
{
|
||||
type: 'name',
|
||||
desc: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
<QueryTransformList
|
||||
allItems={allItems}
|
||||
dataSourceItems={queryExpressionItems}
|
||||
transformItems={transformItems}
|
||||
selectedId={selectedId}
|
||||
onCollapseSidebar={() => model.onCollapseSidebar(true)}
|
||||
onSelect={(newSelectedId) => model.onChangeSelected(newSelectedId)}
|
||||
onAddQuery={handleAddQuery}
|
||||
onAddFromSavedQueries={(index) => handleOpenQueryLibrary('browse', index)}
|
||||
onAddTransform={(index) => {
|
||||
model.onTransformPicker(index);
|
||||
model.onChangeSelected(null);
|
||||
}}
|
||||
onAddExpression={handleAddExpression}
|
||||
onDuplicateQuery={handleDuplicateQuery}
|
||||
onRemoveQuery={handleRemoveQuery}
|
||||
onToggleQueryVisibility={handleToggleQueryVisibility}
|
||||
onRemoveTransform={handleRemoveTransform}
|
||||
onToggleTransformVisibility={handleToggleTransformVisibility}
|
||||
onReorderDataSources={handleReorderDataSources}
|
||||
onReorderTransforms={handleReorderTransforms}
|
||||
onAddOrganizeFieldsTransform={() =>
|
||||
handleAddTransform(
|
||||
{ value: 'organize' },
|
||||
{
|
||||
excludeByName: {},
|
||||
indexByName: {},
|
||||
renameByName: {},
|
||||
includeByName: {},
|
||||
orderByMode: 'auto',
|
||||
orderBy: [
|
||||
{
|
||||
type: 'name',
|
||||
desc: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldShowAlertingTab(pluginId: string) {
|
||||
const { unifiedAlertingEnabled = false } = getConfig();
|
||||
const hasRuleReadPermissions = contextSrv.hasPermission(getRulesPermissions(GRAFANA_RULES_SOURCE_NAME).read);
|
||||
const isAlertingAvailable = unifiedAlertingEnabled && hasRuleReadPermissions;
|
||||
if (!isAlertingAvailable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isGraph = pluginId === 'graph';
|
||||
const isTimeseries = pluginId === 'timeseries';
|
||||
|
||||
return isGraph || isTimeseries;
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2, collapsed: boolean) {
|
||||
return {
|
||||
sidebarPane: css({
|
||||
overflow: 'hidden',
|
||||
height: '100%',
|
||||
width: collapsed ? 49 : 300,
|
||||
padding: collapsed ? theme.spacing(1) : 'unset',
|
||||
borderTopRightRadius: theme.shape.radius.md,
|
||||
borderTop: `1px solid ${theme.colors.border.weak}`,
|
||||
borderRight: `1px solid ${theme.colors.border.weak}`,
|
||||
background: theme.colors.background.primary,
|
||||
}),
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -90,7 +90,7 @@ export function PanelDataTransformationsTabRendered({
|
||||
return;
|
||||
}
|
||||
|
||||
const queriesTab = parent.state.tabs.find((tab) => tab.tabId === TabId.Queries);
|
||||
const queriesTab = parent.state.editorRef.resolve().state.dataPaneTabs.find((tab) => tab.tabId === TabId.Queries);
|
||||
if (!(queriesTab instanceof PanelDataQueriesTab)) {
|
||||
return;
|
||||
}
|
||||
|
||||
+13
-5
@@ -33,6 +33,7 @@ interface QueryTransformListProps {
|
||||
onReorderDataSources?: (startIndex: number, endIndex: number) => void;
|
||||
onReorderTransforms?: (startIndex: number, endIndex: number) => void;
|
||||
onAddOrganizeFieldsTransform?: () => void;
|
||||
onCollapseSidebar: () => void;
|
||||
}
|
||||
|
||||
export const QueryTransformList = memo(
|
||||
@@ -54,6 +55,7 @@ export const QueryTransformList = memo(
|
||||
onReorderDataSources,
|
||||
onReorderTransforms,
|
||||
onAddOrganizeFieldsTransform,
|
||||
onCollapseSidebar,
|
||||
}: QueryTransformListProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
@@ -269,9 +271,13 @@ export const QueryTransformList = memo(
|
||||
<div className={styles.container} onMouseLeave={() => setHovered(null)}>
|
||||
<div className={styles.header}>
|
||||
<Stack justifyContent="space-between" alignItems="center" gap={2}>
|
||||
<span className={styles.headerTitle}>
|
||||
{t('dashboard-scene.query-transform-list.header', 'Pipeline flow')}
|
||||
</span>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<div className={styles.headerTitle} onClick={onCollapseSidebar}>
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
<Icon name="angle-down" />
|
||||
<span>{t('dashboard-scene.query-transform-list.header', 'Pipeline flow')}</span>
|
||||
</Stack>
|
||||
</div>
|
||||
<Button size="sm" onClick={handleToggleAiMode} className={styles.aiModeButton}>
|
||||
<Stack direction="row" gap={0.5} alignItems="center">
|
||||
<Icon name={isAiMode ? 'times' : 'ai'} size="sm" />
|
||||
@@ -572,8 +578,9 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
overflow: 'auto',
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
borderLeft: 'none',
|
||||
}),
|
||||
header: css({
|
||||
...barBase,
|
||||
@@ -588,6 +595,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
fontFamily: theme.typography.fontFamilyMonospace,
|
||||
textTransform: 'uppercase',
|
||||
color: theme.colors.text.primary,
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
sectionLabel: css({
|
||||
cursor: 'pointer',
|
||||
@@ -647,7 +655,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
},
|
||||
}),
|
||||
content: css({
|
||||
padding: theme.spacing(2, 8, 2, 2),
|
||||
padding: theme.spacing(2, 6, 2, 2),
|
||||
position: 'relative',
|
||||
}),
|
||||
contentGradientBorder: css({
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { DataTransformerConfig } from '@grafana/data';
|
||||
import { SceneDataQuery } from '@grafana/scenes';
|
||||
import { isExpressionQuery } from 'app/features/expressions/guards';
|
||||
|
||||
import { QueryItem, TransformItem } from './types';
|
||||
import { isDataTransformerConfig, queryItemId, transformItemId } from './utils';
|
||||
|
||||
export function useQueryTransformItems(queries?: SceneDataQuery[], transformations?: DataTransformerConfig[]) {
|
||||
const result = useMemo(() => {
|
||||
const queryExpressionItems: QueryItem[] = [];
|
||||
const transformItems: TransformItem[] = [];
|
||||
|
||||
// Add queries and expressions
|
||||
for (let i = 0; i < (queries?.length ?? 0); i++) {
|
||||
const query = queries![i];
|
||||
queryExpressionItems.push({
|
||||
id: queryItemId(query),
|
||||
type: isExpressionQuery(query) ? 'expression' : 'query',
|
||||
data: query,
|
||||
index: i, // Store actual index in queries array
|
||||
});
|
||||
}
|
||||
|
||||
// Add transformations
|
||||
for (let i = 0; i < (transformations?.length ?? 0); i++) {
|
||||
const transform = transformations![i];
|
||||
if (isDataTransformerConfig(transform)) {
|
||||
transformItems.push({
|
||||
id: transformItemId(i),
|
||||
type: 'transform',
|
||||
data: transform,
|
||||
index: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
queryExpressionItems,
|
||||
transformItems,
|
||||
allItems: [...queryExpressionItems, ...transformItems],
|
||||
};
|
||||
}, [queries, transformations]);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { NavToolbarActions } from '../scene/NavToolbarActions';
|
||||
import { UnlinkModal } from '../scene/UnlinkModal';
|
||||
import { getDashboardSceneFor, getLibraryPanelBehavior } from '../utils/utils';
|
||||
|
||||
import { PanelDataSidebarRendered } from './PanelDataPane/PanelDataSidebar';
|
||||
import { PanelEditor } from './PanelEditor';
|
||||
import { SaveLibraryVizPanelModal } from './SaveLibraryVizPanelModal';
|
||||
import { useSnappingSplitter } from './splitter/useSnappingSplitter';
|
||||
@@ -111,6 +112,11 @@ function VizAndDataPane({ model }: SceneComponentProps<PanelEditor>) {
|
||||
<controls.Component model={controls} />
|
||||
</div>
|
||||
)}
|
||||
{dataPane && (
|
||||
<div className={styles.sidebar}>
|
||||
<PanelDataSidebarRendered model={dataPane} />
|
||||
</div>
|
||||
)}
|
||||
<div {...containerProps}>
|
||||
<div {...primaryProps} className={cx(primaryProps.className, isScrollingLayout && styles.fixedSizeViz)}>
|
||||
<VizWrapper panel={panel} tableView={tableView} />
|
||||
@@ -181,8 +187,8 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
pageContainer: css({
|
||||
display: 'grid',
|
||||
gridTemplateAreas: `
|
||||
"panels"`,
|
||||
gridTemplateColumns: `1fr`,
|
||||
"sidebar panels"`,
|
||||
gridTemplateColumns: `auto 1fr`,
|
||||
gridTemplateRows: '1fr',
|
||||
height: '100%',
|
||||
[scrollReflowMediaQuery]: {
|
||||
@@ -191,14 +197,24 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
}),
|
||||
pageContainerWithControls: css({
|
||||
gridTemplateAreas: `
|
||||
"controls"
|
||||
"panels"`,
|
||||
"controls controls"
|
||||
"sidebar panels"`,
|
||||
gridTemplateRows: 'auto 1fr',
|
||||
}),
|
||||
container: css({
|
||||
gridArea: 'panels',
|
||||
height: '100%',
|
||||
}),
|
||||
sidebar: css({
|
||||
gridArea: 'sidebar',
|
||||
height: '100%',
|
||||
maxHeight: '100%',
|
||||
overflow: 'auto',
|
||||
}),
|
||||
expandDataSidebar: css({
|
||||
width: theme.spacing(6),
|
||||
padding: theme.spacing(1),
|
||||
}),
|
||||
canvasContent: css({
|
||||
label: 'canvas-content',
|
||||
display: 'flex',
|
||||
|
||||
Reference in New Issue
Block a user