mirror of
https://github.com/grafana/grafana.git
synced 2025-12-21 12:04:45 +08:00
Compare commits
3 Commits
docs/add-t
...
alexspence
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd16965d31 | ||
|
|
9d6f980320 | ||
|
|
f9b4fc71e5 |
@@ -24,6 +24,16 @@ import { testDashboard } from '../testfiles/testDashboard';
|
||||
|
||||
import { PanelDataTransformationsTab, PanelDataTransformationsTabRendered } from './PanelDataTransformationsTab';
|
||||
|
||||
// Mock getDashboardSceneFor to return a mock dashboard
|
||||
jest.mock('../../utils/utils', () => ({
|
||||
...jest.requireActual('../../utils/utils'),
|
||||
getDashboardSceneFor: jest.fn(() => ({
|
||||
state: {
|
||||
uid: 'test-dashboard-uid',
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
function createModelMock(
|
||||
panelData: PanelData,
|
||||
transformations?: DataTransformerConfig[],
|
||||
@@ -33,6 +43,15 @@ function createModelMock(
|
||||
getDataTransformer: () => new SceneDataTransformer({ data: panelData, transformations: transformations || [] }),
|
||||
getQueryRunner: () => new SceneQueryRunner({ queries: [], data: panelData }),
|
||||
onChangeTransformations: onChangeTransformationsMock,
|
||||
state: {
|
||||
panelRef: {
|
||||
resolve: () => ({
|
||||
state: {
|
||||
key: 'test-panel-key',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as unknown as PanelDataTransformationsTab;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,13 +18,14 @@ import { Button, ButtonGroup, ConfirmModal, Tab, useStyles2 } from '@grafana/ui'
|
||||
import { TransformationOperationRows } from 'app/features/dashboard/components/TransformationsEditor/TransformationOperationRows';
|
||||
import { ExpressionQueryType } from 'app/features/expressions/types';
|
||||
|
||||
import { getQueryRunnerFor } from '../../utils/utils';
|
||||
import { getDashboardSceneFor, getQueryRunnerFor } from '../../utils/utils';
|
||||
|
||||
import { EmptyTransformationsMessage } from './EmptyTransformationsMessage';
|
||||
import { PanelDataPane } from './PanelDataPane';
|
||||
import { PanelDataQueriesTab } from './PanelDataQueriesTab';
|
||||
import { TransformationsDrawer } from './TransformationsDrawer';
|
||||
import { PanelDataPaneTab, TabId, PanelDataTabHeaderProps } from './types';
|
||||
import { usePersistedTransformationState } from './usePersistedTransformationState';
|
||||
import { findSqlExpression, scrollToQueryRow } from './utils';
|
||||
|
||||
const SET_TIMEOUT = 750;
|
||||
@@ -73,6 +74,11 @@ export function PanelDataTransformationsTabRendered({ model }: SceneComponentPro
|
||||
const sourceData = model.getQueryRunner().useState();
|
||||
const { data, transformations: transformsWrongType } = model.getDataTransformer().useState();
|
||||
|
||||
// Get dashboard and panel IDs for scoping session storage
|
||||
const dashboard = getDashboardSceneFor(model);
|
||||
const panel = model.state.panelRef.resolve();
|
||||
const storageKey = `${dashboard.state.uid}-${panel.state.key || 'unknown'}`;
|
||||
|
||||
// Type guard to ensure transformations are DataTransformerConfig[]
|
||||
const transformations = useMemo<DataTransformerConfig[]>(() => {
|
||||
return Array.isArray(transformsWrongType)
|
||||
@@ -83,11 +89,13 @@ export function PanelDataTransformationsTabRendered({ model }: SceneComponentPro
|
||||
: [];
|
||||
}, [transformsWrongType]);
|
||||
|
||||
const [drawerOpen, setDrawerOpen] = useState<boolean>(false);
|
||||
const [confirmModalOpen, setConfirmModalOpen] = useState<boolean>(false);
|
||||
const { isOpen: getRowCollapseState, setIsOpen: setRowCollapseState } = usePersistedTransformationState(storageKey);
|
||||
|
||||
const openDrawer = () => setDrawerOpen(true);
|
||||
const closeDrawer = () => setDrawerOpen(false);
|
||||
const [confirmModalOpen, setConfirmModalOpen] = useState<boolean>(false);
|
||||
const [pickerDrawerOpen, setPickerDrawerOpen] = useState<boolean>(false);
|
||||
|
||||
const openPickerDrawer = () => setPickerDrawerOpen(true);
|
||||
const closePickerDrawer = () => setPickerDrawerOpen(false);
|
||||
|
||||
const onGoToQueries = useCallback(() => {
|
||||
const parent = model.parent;
|
||||
@@ -134,15 +142,15 @@ export function PanelDataTransformationsTabRendered({ model }: SceneComponentPro
|
||||
|
||||
const transformationsDrawer = (
|
||||
<TransformationsDrawer
|
||||
onClose={closeDrawer}
|
||||
onClose={closePickerDrawer}
|
||||
onTransformationAdd={(selected) => {
|
||||
if (selected.value === undefined) {
|
||||
return;
|
||||
}
|
||||
model.onChangeTransformations([...transformations, { id: selected.value, options: {} }]);
|
||||
closeDrawer();
|
||||
closePickerDrawer();
|
||||
}}
|
||||
isOpen={drawerOpen}
|
||||
isOpen={pickerDrawerOpen}
|
||||
series={data.series}
|
||||
/>
|
||||
);
|
||||
@@ -151,7 +159,7 @@ export function PanelDataTransformationsTabRendered({ model }: SceneComponentPro
|
||||
return (
|
||||
<>
|
||||
<EmptyTransformationsMessage
|
||||
onShowPicker={openDrawer}
|
||||
onShowPicker={openPickerDrawer}
|
||||
onGoToQueries={onGoToQueries}
|
||||
onAddTransformation={onAddTransformation}
|
||||
/>
|
||||
@@ -162,12 +170,18 @@ export function PanelDataTransformationsTabRendered({ model }: SceneComponentPro
|
||||
|
||||
return (
|
||||
<>
|
||||
<TransformationsEditor data={sourceData.data} transformations={transformations} model={model} />
|
||||
<TransformationsEditor
|
||||
data={sourceData.data}
|
||||
transformations={transformations}
|
||||
model={model}
|
||||
getIsOpen={getRowCollapseState}
|
||||
setIsOpen={setRowCollapseState}
|
||||
/>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
icon="plus"
|
||||
variant="secondary"
|
||||
onClick={openDrawer}
|
||||
onClick={openPickerDrawer}
|
||||
data-testid={selectors.components.Transforms.addTransformationButton}
|
||||
>
|
||||
<Trans i18nKey="dashboard-scene.panel-data-transformations-tab-rendered.add-another-transformation">
|
||||
@@ -212,9 +226,11 @@ interface TransformationEditorProps {
|
||||
transformations: DataTransformerConfig[];
|
||||
model: PanelDataTransformationsTab;
|
||||
data: PanelData;
|
||||
getIsOpen: (id: string) => boolean | undefined;
|
||||
setIsOpen: (id: string, isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
function TransformationsEditor({ transformations, model, data }: TransformationEditorProps) {
|
||||
function TransformationsEditor({ transformations, model, data, getIsOpen, setIsOpen }: TransformationEditorProps) {
|
||||
const transformationEditorRows = transformations.map((t, i) => ({ id: `${i} - ${t.id}`, transformation: t }));
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
@@ -252,6 +268,8 @@ function TransformationsEditor({ transformations, model, data }: TransformationE
|
||||
}}
|
||||
configs={transformationEditorRows}
|
||||
data={data}
|
||||
getIsOpen={getIsOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
></TransformationOperationRows>
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* Session storage key prefix for persisting transformation row collapse states.
|
||||
* Full key format: grafana.panelEditor.transformations.${dashboardUID}-${panelKey}
|
||||
*/
|
||||
export const TRANSFORMATION_ROWS_STATE_KEY = 'grafana.panelEditor.transformations';
|
||||
|
||||
/**
|
||||
* Persists transformation row collapse states in sessionStorage.
|
||||
* Scoped by dashboard UID and panel ID for uniqueness across dashboards.
|
||||
* Reads/writes directly from sessionStorage.
|
||||
*/
|
||||
export function usePersistedTransformationState(storageKey: string) {
|
||||
const fullStorageKey = `${TRANSFORMATION_ROWS_STATE_KEY}.${storageKey}`;
|
||||
|
||||
const isOpen = useCallback(
|
||||
(transformationId: string): boolean | undefined => {
|
||||
try {
|
||||
const stored = sessionStorage.getItem(fullStorageKey);
|
||||
if (!stored) {
|
||||
return undefined;
|
||||
}
|
||||
const rowStates = JSON.parse(stored);
|
||||
return rowStates[transformationId];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
[fullStorageKey]
|
||||
);
|
||||
|
||||
const setIsOpen = useCallback(
|
||||
(transformationId: string, open: boolean) => {
|
||||
try {
|
||||
const stored = sessionStorage.getItem(fullStorageKey);
|
||||
const rowStates = stored ? JSON.parse(stored) : {};
|
||||
rowStates[transformationId] = open;
|
||||
sessionStorage.setItem(fullStorageKey, JSON.stringify(rowStates));
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(`Failed to persist transformation state for ${transformationId}:`, error);
|
||||
}
|
||||
// Silently fail in production if storage unavailable or quota exceeded
|
||||
}
|
||||
},
|
||||
[fullStorageKey]
|
||||
);
|
||||
|
||||
return { isOpen, setIsOpen };
|
||||
}
|
||||
@@ -36,6 +36,8 @@ interface TransformationOperationRowProps {
|
||||
configs: TransformationsEditorTransformation[];
|
||||
onRemove: (index: number) => void;
|
||||
onChange: (index: number, config: DataTransformerConfig) => void;
|
||||
isOpen?: boolean;
|
||||
onOpenChanged: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const TransformationOperationRow = ({
|
||||
@@ -46,6 +48,8 @@ export const TransformationOperationRow = ({
|
||||
configs,
|
||||
uiConfig,
|
||||
onChange,
|
||||
isOpen,
|
||||
onOpenChanged,
|
||||
}: TransformationOperationRowProps) => {
|
||||
const [showDeleteModal, setShowDeleteModal] = useToggle(false);
|
||||
const [showDebug, toggleShowDebug] = useToggle(false);
|
||||
@@ -227,6 +231,9 @@ export const TransformationOperationRow = ({
|
||||
draggable
|
||||
actions={renderActions}
|
||||
disabled={disabled}
|
||||
isOpen={isOpen}
|
||||
onOpen={() => onOpenChanged(true)}
|
||||
onClose={() => onOpenChanged(false)}
|
||||
expanderMessages={{
|
||||
close: 'Collapse transformation row',
|
||||
open: 'Expand transformation row',
|
||||
|
||||
@@ -9,6 +9,8 @@ interface TransformationOperationRowsProps {
|
||||
configs: TransformationsEditorTransformation[];
|
||||
onRemove: (index: number) => void;
|
||||
onChange: (index: number, config: DataTransformerConfig) => void;
|
||||
getIsOpen?: (id: string) => boolean | undefined;
|
||||
setIsOpen?: (id: string, isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const TransformationOperationRows = ({
|
||||
@@ -16,6 +18,8 @@ export const TransformationOperationRows = ({
|
||||
onChange,
|
||||
onRemove,
|
||||
configs,
|
||||
getIsOpen,
|
||||
setIsOpen,
|
||||
}: TransformationOperationRowsProps) => {
|
||||
return (
|
||||
<>
|
||||
@@ -36,6 +40,8 @@ export const TransformationOperationRows = ({
|
||||
uiConfig={uiConfig}
|
||||
onRemove={onRemove}
|
||||
onChange={onChange}
|
||||
isOpen={getIsOpen?.(t.id)}
|
||||
onOpenChanged={(isOpen) => setIsOpen?.(t.id, isOpen)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user