Add Traces Drilldown panel
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { Button, useStyles2 } from '@grafana/ui';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Button, ButtonGroup, Dropdown, Menu, MenuItem, useStyles2 } from '@grafana/ui';
|
||||
import { useDispatch } from 'app/types/store';
|
||||
|
||||
import { addPanel } from '../state/crdtSlice';
|
||||
@@ -11,6 +11,7 @@ import { addPanel } from '../state/crdtSlice';
|
||||
export function ExploreMapFloatingToolbar() {
|
||||
const styles = useStyles2(getStyles);
|
||||
const dispatch = useDispatch();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleAddPanel = useCallback(() => {
|
||||
dispatch(
|
||||
@@ -21,13 +22,51 @@ export function ExploreMapFloatingToolbar() {
|
||||
},
|
||||
})
|
||||
);
|
||||
setIsOpen(false);
|
||||
}, [dispatch]);
|
||||
|
||||
const handleAddTracesDrilldownPanel = useCallback(() => {
|
||||
dispatch(
|
||||
addPanel({
|
||||
viewportSize: {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
},
|
||||
kind: 'traces-drilldown',
|
||||
})
|
||||
);
|
||||
setIsOpen(false);
|
||||
}, [dispatch]);
|
||||
|
||||
const MenuActions = () => (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
label={t('explore-map.toolbar.add-panel', 'Add Explore panel')}
|
||||
icon="plus"
|
||||
onClick={handleAddPanel}
|
||||
/>
|
||||
<MenuItem
|
||||
label={t('explore-map.toolbar.add-traces-drilldown-panel', 'Add Traces Drilldown panel')}
|
||||
icon="drilldown"
|
||||
onClick={handleAddTracesDrilldownPanel}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.floatingToolbar}>
|
||||
<Button icon="plus" onClick={handleAddPanel} variant="primary">
|
||||
<Trans i18nKey="explore-map.toolbar.add-panel">Add Panel</Trans>
|
||||
</Button>
|
||||
<ButtonGroup>
|
||||
<Button icon="plus" onClick={handleAddPanel} variant="primary">
|
||||
<Trans i18nKey="explore-map.toolbar.add-panel">Add panel</Trans>
|
||||
</Button>
|
||||
<Dropdown overlay={MenuActions} placement="bottom-end" onVisibleChange={setIsOpen}>
|
||||
<Button
|
||||
aria-label={t('explore-map.toolbar.add-panel-dropdown', 'Add panel options')}
|
||||
variant="primary"
|
||||
icon={isOpen ? 'angle-up' : 'angle-down'}
|
||||
/>
|
||||
</Dropdown>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import { usePanelStateSync } from '../hooks/usePanelStateSync';
|
||||
// import { useExploreStateSync } from '../hooks/useExploreStateSync';
|
||||
import { selectPanels } from '../state/selectors';
|
||||
|
||||
import { ExploreMapTracesDrilldownPanel } from './ExploreMapTracesDrilldownPanel';
|
||||
|
||||
interface ExploreMapPanelContentProps {
|
||||
panelId: string;
|
||||
exploreId: string;
|
||||
@@ -118,8 +120,14 @@ export function ExploreMapPanelContent({ panelId, exploreId, width, height }: Ex
|
||||
return panels[panelId];
|
||||
});
|
||||
|
||||
// Initialize Explore pane on mount
|
||||
// Initialize Explore pane on mount (only for standard Explore panels)
|
||||
useEffect(() => {
|
||||
if (panel?.mode === 'traces-drilldown') {
|
||||
// Traces drilldown panels don't need Explore initialization
|
||||
setIsInitialized(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const initializePane = async () => {
|
||||
// Use saved state if available, otherwise defaults
|
||||
const savedState = panel?.exploreState;
|
||||
@@ -133,7 +141,7 @@ export function ExploreMapPanelContent({ panelId, exploreId, width, height }: Ex
|
||||
queries: savedState?.queries || [],
|
||||
range: savedState?.range || DEFAULT_RANGE,
|
||||
eventBridge: eventBus,
|
||||
compact: savedState?.compact || false,
|
||||
compact: savedState?.compact ?? false,
|
||||
})
|
||||
);
|
||||
setIsInitialized(true);
|
||||
@@ -146,7 +154,11 @@ export function ExploreMapPanelContent({ panelId, exploreId, width, height }: Ex
|
||||
eventBus.removeAllListeners();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dispatch, exploreId, eventBus]);
|
||||
}, [dispatch, exploreId, eventBus, panel?.exploreState, panel?.mode]);
|
||||
|
||||
if (panel?.mode === 'traces-drilldown') {
|
||||
return <ExploreMapTracesDrilldownPanel exploreId={exploreId} width={width} height={height} />;
|
||||
}
|
||||
|
||||
// Wait for Redux state to be initialized
|
||||
if (!isInitialized) {
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { useDispatch, useSelector } from 'app/types/store';
|
||||
|
||||
import { updatePanelIframeUrl } from '../state/crdtSlice';
|
||||
import { selectPanels } from '../state/selectors';
|
||||
|
||||
interface ExploreMapTracesDrilldownPanelProps {
|
||||
exploreId: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export function ExploreMapTracesDrilldownPanel({ exploreId, width, height }: ExploreMapTracesDrilldownPanelProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const dispatch = useDispatch();
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
// Find the panel with this exploreId to get saved state
|
||||
const panel = useSelector((state) => {
|
||||
const panels = selectPanels(state.exploreMapCRDT);
|
||||
return Object.values(panels).find((p) => p.exploreId === exploreId);
|
||||
});
|
||||
|
||||
// Build iframe URL for traces drilldown - use saved URL if available, otherwise default
|
||||
// IMPORTANT: Only compute this once on mount to prevent iframe reloads
|
||||
const [tracesDrilldownUrl] = useState(() => {
|
||||
// If we have a saved iframe URL from previous session, use it
|
||||
if (panel?.mode === 'traces-drilldown' && panel?.iframeUrl) {
|
||||
return panel.iframeUrl;
|
||||
}
|
||||
// Otherwise, construct the default URL
|
||||
const origin = window.location.origin;
|
||||
const subUrl = config.appSubUrl || '';
|
||||
const appPath = '/a/grafana-exploretraces-app';
|
||||
return `${origin}${subUrl}${appPath}`;
|
||||
});
|
||||
|
||||
// Initialize immediately for traces drilldown panels
|
||||
useEffect(() => {
|
||||
setIsInitialized(true);
|
||||
}, []);
|
||||
|
||||
// Auto-save iframe URL changes for traces-drilldown panels
|
||||
useEffect(() => {
|
||||
if (!isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const panelId = panel?.id;
|
||||
if (!panelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let lastSavedUrl = panel?.iframeUrl || '';
|
||||
|
||||
const checkAndSaveUrl = () => {
|
||||
const iframe = iframeRef.current;
|
||||
if (!iframe) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const currentUrl = iframe.contentWindow?.location.href;
|
||||
if (currentUrl && currentUrl !== lastSavedUrl) {
|
||||
lastSavedUrl = currentUrl;
|
||||
dispatch(
|
||||
updatePanelIframeUrl({
|
||||
panelId,
|
||||
iframeUrl: currentUrl,
|
||||
})
|
||||
);
|
||||
} else if (!currentUrl) {
|
||||
console.log('No URL detected');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Cannot read iframe URL:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// Start checking periodically (every 2 seconds)
|
||||
const intervalId = setInterval(checkAndSaveUrl, 2000);
|
||||
|
||||
// Also check immediately
|
||||
setTimeout(checkAndSaveUrl, 100);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
// Intentionally not including panel.iframeUrl to avoid re-creating interval on every save
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isInitialized, panel?.id, dispatch]);
|
||||
|
||||
if (!isInitialized) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.loading}>
|
||||
<Trans i18nKey="explore-map.panel.initializing">Initializing...</Trans>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
style={{
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={tracesDrilldownUrl}
|
||||
title={t('explore-map.panel.traces-drilldown', 'Traces Drilldown')}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}),
|
||||
loading: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
color: theme.colors.text.secondary,
|
||||
fontSize: theme.typography.body.fontSize,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { SerializedExploreState } from '../state/types';
|
||||
|
||||
import { HybridLogicalClock } from './hlc';
|
||||
import { LWWRegister, createLWWRegister } from './lwwregister';
|
||||
import { ORSet } from './orset';
|
||||
@@ -20,11 +23,11 @@ import {
|
||||
UpdatePanelSizeOperation,
|
||||
UpdatePanelZIndexOperation,
|
||||
UpdatePanelExploreStateOperation,
|
||||
UpdatePanelIframeUrlOperation,
|
||||
UpdateTitleOperation,
|
||||
OperationResult,
|
||||
CRDTExploreMapStateJSON,
|
||||
} from './types';
|
||||
import { SerializedExploreState } from '../state/types';
|
||||
|
||||
export class CRDTStateManager {
|
||||
private state: CRDTExploreMapState;
|
||||
@@ -111,6 +114,8 @@ export class CRDTStateManager {
|
||||
zIndex: data.zIndex.get(),
|
||||
},
|
||||
exploreState: data.exploreState.get(),
|
||||
mode: data.mode.get(),
|
||||
iframeUrl: data.iframeUrl.get(),
|
||||
remoteVersion: data.remoteVersion,
|
||||
};
|
||||
}
|
||||
@@ -135,7 +140,8 @@ export class CRDTStateManager {
|
||||
createAddPanelOperation(
|
||||
panelId: string,
|
||||
exploreId: string,
|
||||
position: { x: number; y: number; width: number; height: number }
|
||||
position: { x: number; y: number; width: number; height: number },
|
||||
mode: 'explore' | 'traces-drilldown' = 'explore'
|
||||
): AddPanelOperation {
|
||||
const timestamp = this.clock.tick();
|
||||
return {
|
||||
@@ -148,6 +154,7 @@ export class CRDTStateManager {
|
||||
panelId,
|
||||
exploreId,
|
||||
position,
|
||||
mode,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -279,6 +286,31 @@ export class CRDTStateManager {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an update panel iframe URL operation
|
||||
*/
|
||||
createUpdatePanelIframeUrlOperation(
|
||||
panelId: string,
|
||||
iframeUrl: string | undefined
|
||||
): UpdatePanelIframeUrlOperation | null {
|
||||
if (!this.state.panels.contains(panelId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timestamp = this.clock.tick();
|
||||
return {
|
||||
type: 'update-panel-iframe-url',
|
||||
mapUid: this.mapUid,
|
||||
operationId: uuidv4(),
|
||||
timestamp,
|
||||
nodeId: this.nodeId,
|
||||
payload: {
|
||||
panelId,
|
||||
iframeUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an update title operation
|
||||
*/
|
||||
@@ -317,6 +349,8 @@ export class CRDTStateManager {
|
||||
return this.applyUpdatePanelZIndex(operation);
|
||||
case 'update-panel-explore-state':
|
||||
return this.applyUpdatePanelExploreState(operation);
|
||||
case 'update-panel-iframe-url':
|
||||
return this.applyUpdatePanelIframeUrl(operation);
|
||||
case 'update-title':
|
||||
return this.applyUpdateTitle(operation);
|
||||
case 'batch':
|
||||
@@ -338,7 +372,8 @@ export class CRDTStateManager {
|
||||
}
|
||||
|
||||
private applyAddPanel(operation: AddPanelOperation): OperationResult {
|
||||
const { panelId, exploreId, position } = operation.payload;
|
||||
const { panelId, exploreId, position, mode } = operation.payload;
|
||||
const panelMode = mode || 'explore';
|
||||
|
||||
// Add to OR-Set with operation ID as tag
|
||||
this.state.panels.add(panelId, operation.operationId);
|
||||
@@ -356,6 +391,8 @@ export class CRDTStateManager {
|
||||
height: new LWWRegister(position.height, operation.timestamp),
|
||||
zIndex: new LWWRegister(zIndex, operation.timestamp),
|
||||
exploreState: new LWWRegister(undefined, operation.timestamp),
|
||||
mode: new LWWRegister(panelMode, operation.timestamp),
|
||||
iframeUrl: new LWWRegister(undefined, operation.timestamp),
|
||||
remoteVersion: 0,
|
||||
});
|
||||
}
|
||||
@@ -446,6 +483,22 @@ export class CRDTStateManager {
|
||||
};
|
||||
}
|
||||
|
||||
private applyUpdatePanelIframeUrl(operation: UpdatePanelIframeUrlOperation): OperationResult {
|
||||
const { panelId, iframeUrl } = operation.payload;
|
||||
const panelData = this.state.panelData.get(panelId);
|
||||
|
||||
if (!panelData) {
|
||||
return { success: true, applied: false, error: 'Panel not found' };
|
||||
}
|
||||
|
||||
const updated = panelData.iframeUrl.set(iframeUrl, operation.timestamp);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
applied: updated,
|
||||
};
|
||||
}
|
||||
|
||||
private applyUpdateTitle(operation: UpdateTitleOperation): OperationResult {
|
||||
const { title } = operation.payload;
|
||||
const updated = this.state.title.set(title, operation.timestamp);
|
||||
@@ -502,6 +555,8 @@ export class CRDTStateManager {
|
||||
height: otherPanelData.height.clone(),
|
||||
zIndex: otherPanelData.zIndex.clone(),
|
||||
exploreState: otherPanelData.exploreState.clone(),
|
||||
mode: otherPanelData.mode.clone(),
|
||||
iframeUrl: otherPanelData.iframeUrl.clone(),
|
||||
remoteVersion: otherPanelData.remoteVersion,
|
||||
});
|
||||
} else {
|
||||
@@ -512,6 +567,8 @@ export class CRDTStateManager {
|
||||
myPanelData.height.merge(otherPanelData.height);
|
||||
myPanelData.zIndex.merge(otherPanelData.zIndex);
|
||||
myPanelData.exploreState.merge(otherPanelData.exploreState);
|
||||
myPanelData.mode.merge(otherPanelData.mode);
|
||||
myPanelData.iframeUrl.merge(otherPanelData.iframeUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,6 +592,8 @@ export class CRDTStateManager {
|
||||
height: data.height.toJSON(),
|
||||
zIndex: data.zIndex.toJSON(),
|
||||
exploreState: data.exploreState.toJSON(),
|
||||
mode: data.mode.toJSON(),
|
||||
iframeUrl: data.iframeUrl.toJSON(),
|
||||
remoteVersion: data.remoteVersion,
|
||||
};
|
||||
}
|
||||
@@ -561,6 +620,8 @@ export class CRDTStateManager {
|
||||
|
||||
// Load panel data
|
||||
for (const [panelId, data] of Object.entries(json.panelData)) {
|
||||
// Get the first timestamp from existing registers for defaults
|
||||
const defaultTimestamp = data.positionX?.timestamp || { nodeId: manager.nodeId, counter: 0, wallClock: Date.now() };
|
||||
manager.state.panelData.set(panelId, {
|
||||
id: data.id,
|
||||
exploreId: data.exploreId,
|
||||
@@ -570,6 +631,8 @@ export class CRDTStateManager {
|
||||
height: LWWRegister.fromJSON(data.height),
|
||||
zIndex: LWWRegister.fromJSON(data.zIndex),
|
||||
exploreState: LWWRegister.fromJSON(data.exploreState),
|
||||
mode: data.mode ? LWWRegister.fromJSON(data.mode) : new LWWRegister('explore', defaultTimestamp),
|
||||
iframeUrl: data.iframeUrl ? LWWRegister.fromJSON(data.iframeUrl) : new LWWRegister(undefined, defaultTimestamp),
|
||||
remoteVersion: data.remoteVersion || 0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
* feature, enabling conflict-free collaborative editing.
|
||||
*/
|
||||
|
||||
import { SerializedExploreState } from '../state/types';
|
||||
|
||||
import { HLCTimestamp } from './hlc';
|
||||
import { LWWRegister } from './lwwregister';
|
||||
import { ORSet } from './orset';
|
||||
import { PNCounter } from './pncounter';
|
||||
import { HLCTimestamp } from './hlc';
|
||||
import { SerializedExploreState } from '../state/types';
|
||||
|
||||
/**
|
||||
* CRDT state for a single panel
|
||||
@@ -29,6 +30,12 @@ export interface CRDTPanelData {
|
||||
// CRDT-replicated explore state
|
||||
exploreState: LWWRegister<SerializedExploreState | undefined>;
|
||||
|
||||
// Panel mode (explore or traces-drilldown)
|
||||
mode: LWWRegister<'explore' | 'traces-drilldown'>;
|
||||
|
||||
// Iframe URL for traces-drilldown panels
|
||||
iframeUrl: LWWRegister<string | undefined>;
|
||||
|
||||
// Local counter incremented only for remote explore state updates
|
||||
remoteVersion: number;
|
||||
}
|
||||
@@ -91,6 +98,8 @@ export interface CRDTExploreMapStateJSON {
|
||||
height: { value: number; timestamp: HLCTimestamp };
|
||||
zIndex: { value: number; timestamp: HLCTimestamp };
|
||||
exploreState: { value: SerializedExploreState | undefined; timestamp: HLCTimestamp };
|
||||
mode: { value: 'explore' | 'traces-drilldown'; timestamp: HLCTimestamp };
|
||||
iframeUrl: { value: string | undefined; timestamp: HLCTimestamp };
|
||||
remoteVersion?: number;
|
||||
}>;
|
||||
zIndexCounter: {
|
||||
@@ -109,6 +118,7 @@ export type CRDTOperationType =
|
||||
| 'update-panel-size'
|
||||
| 'update-panel-zindex'
|
||||
| 'update-panel-explore-state'
|
||||
| 'update-panel-iframe-url'
|
||||
| 'update-title'
|
||||
| 'batch'; // For batching multiple operations
|
||||
|
||||
@@ -137,6 +147,7 @@ export interface AddPanelOperation extends CRDTOperationBase {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
mode?: 'explore' | 'traces-drilldown';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -197,6 +208,17 @@ export interface UpdatePanelExploreStateOperation extends CRDTOperationBase {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update panel iframe URL operation
|
||||
*/
|
||||
export interface UpdatePanelIframeUrlOperation extends CRDTOperationBase {
|
||||
type: 'update-panel-iframe-url';
|
||||
payload: {
|
||||
panelId: string;
|
||||
iframeUrl: string | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update map title operation
|
||||
*/
|
||||
@@ -227,6 +249,7 @@ export type CRDTOperation =
|
||||
| UpdatePanelSizeOperation
|
||||
| UpdatePanelZIndexOperation
|
||||
| UpdatePanelExploreStateOperation
|
||||
| UpdatePanelIframeUrlOperation
|
||||
| UpdateTitleOperation
|
||||
| BatchOperation;
|
||||
|
||||
|
||||
@@ -202,6 +202,7 @@ const crdtSlice = createSlice({
|
||||
addPanel: (state, action: PayloadAction<{
|
||||
viewportSize?: { width: number; height: number };
|
||||
position?: { x: number; y: number; width: number; height: number };
|
||||
kind?: 'explore' | 'traces-drilldown';
|
||||
}>) => {
|
||||
const manager = getCRDTManager(state);
|
||||
|
||||
@@ -225,8 +226,9 @@ const crdtSlice = createSlice({
|
||||
// Create operation
|
||||
const panelId = uuidv4();
|
||||
const exploreId = generateExploreId();
|
||||
const mode = action.payload.kind || 'explore';
|
||||
|
||||
const operation = manager.createAddPanelOperation(panelId, exploreId, position);
|
||||
const operation = manager.createAddPanelOperation(panelId, exploreId, position, mode);
|
||||
|
||||
// Apply locally
|
||||
manager.applyOperation(operation);
|
||||
@@ -398,6 +400,29 @@ const crdtSlice = createSlice({
|
||||
state.pendingOperations.push(operation);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update panel iframe URL
|
||||
*/
|
||||
updatePanelIframeUrl: (
|
||||
state,
|
||||
action: PayloadAction<{ panelId: string; iframeUrl: string | undefined }>
|
||||
) => {
|
||||
const manager = getCRDTManager(state);
|
||||
|
||||
const operation = manager.createUpdatePanelIframeUrlOperation(
|
||||
action.payload.panelId,
|
||||
action.payload.iframeUrl
|
||||
);
|
||||
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
|
||||
manager.applyOperation(operation);
|
||||
saveCRDTManager(state, manager);
|
||||
state.pendingOperations.push(operation);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update map title
|
||||
*/
|
||||
@@ -435,7 +460,8 @@ const crdtSlice = createSlice({
|
||||
y: sourcePanel.position.y + offset,
|
||||
width: sourcePanel.position.width,
|
||||
height: sourcePanel.position.height,
|
||||
}
|
||||
},
|
||||
sourcePanel.mode || 'explore'
|
||||
);
|
||||
|
||||
manager.applyOperation(addOperation);
|
||||
@@ -563,6 +589,7 @@ export const {
|
||||
updatePanelSize,
|
||||
bringPanelToFront,
|
||||
savePanelExploreState,
|
||||
updatePanelIframeUrl,
|
||||
updateMapTitle,
|
||||
duplicatePanel,
|
||||
clearPendingOperations,
|
||||
|
||||
@@ -15,6 +15,11 @@ import {
|
||||
interface AddPanelPayload {
|
||||
position?: Partial<PanelPosition>;
|
||||
viewportSize?: { width: number; height: number };
|
||||
/**
|
||||
* Optional panel mode. Defaults to 'explore' for backward compatibility.
|
||||
* Use 'traces-drilldown' to embed the Explore Traces drilldown app.
|
||||
*/
|
||||
mode?: 'explore' | 'traces-drilldown';
|
||||
}
|
||||
|
||||
const exploreMapSlice = createSlice({
|
||||
@@ -24,6 +29,7 @@ const exploreMapSlice = createSlice({
|
||||
addPanel: (state, action: PayloadAction<AddPanelPayload>) => {
|
||||
const panelId = uuidv4();
|
||||
const exploreId = generateExploreId();
|
||||
const mode = action.payload.mode ?? 'explore';
|
||||
|
||||
// Calculate center of current viewport in canvas coordinates
|
||||
const viewportSize = action.payload.viewportSize || { width: 1920, height: 1080 };
|
||||
@@ -45,6 +51,7 @@ const exploreMapSlice = createSlice({
|
||||
state.panels[panelId] = {
|
||||
id: panelId,
|
||||
exploreId: exploreId,
|
||||
mode,
|
||||
position: { ...defaultPosition, ...action.payload.position },
|
||||
};
|
||||
state.nextZIndex++;
|
||||
@@ -58,11 +65,18 @@ const exploreMapSlice = createSlice({
|
||||
|
||||
updatePanelPosition: (
|
||||
state,
|
||||
action: PayloadAction<{ panelId: string; position: Partial<PanelPosition> }>
|
||||
action: PayloadAction<{ panelId: string; position: Partial<PanelPosition & { iframeUrl?: string }> }>
|
||||
) => {
|
||||
const panel = state.panels[action.payload.panelId];
|
||||
if (panel) {
|
||||
panel.position = { ...panel.position, ...action.payload.position };
|
||||
// Handle position updates
|
||||
const { iframeUrl, ...positionUpdates } = action.payload.position;
|
||||
panel.position = { ...panel.position, ...positionUpdates };
|
||||
|
||||
// Handle iframe URL updates separately
|
||||
if (iframeUrl !== undefined) {
|
||||
panel.iframeUrl = iframeUrl;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -176,6 +190,7 @@ const exploreMapSlice = createSlice({
|
||||
state.panels[newPanelId] = {
|
||||
id: newPanelId,
|
||||
exploreId: newExploreId,
|
||||
mode: sourcePanel.mode ?? 'explore',
|
||||
position: {
|
||||
...sourcePanel.position,
|
||||
x: sourcePanel.position.x + 30,
|
||||
@@ -207,7 +222,16 @@ const exploreMapSlice = createSlice({
|
||||
return {
|
||||
...initialExploreMapState,
|
||||
...loadedState,
|
||||
panels: loadedState.panels || {},
|
||||
panels: Object.fromEntries(
|
||||
Object.entries(loadedState.panels || {}).map(([panelId, panel]) => [
|
||||
panelId,
|
||||
{
|
||||
...panel,
|
||||
// Default to 'explore' for canvases saved before panel modes existed
|
||||
mode: panel.mode ?? 'explore',
|
||||
},
|
||||
])
|
||||
),
|
||||
selectedPanelIds: [],
|
||||
cursors: {},
|
||||
};
|
||||
|
||||
@@ -23,6 +23,17 @@ export interface ExploreMapPanel {
|
||||
position: PanelPosition;
|
||||
exploreState?: SerializedExploreState;
|
||||
remoteVersion?: number; // Increments only on remote explore state updates
|
||||
/**
|
||||
* Panel mode determines what kind of content is rendered inside the panel.
|
||||
* - 'explore': standard Explore pane (current behavior)
|
||||
* - 'traces-drilldown': Traces Drilldown app
|
||||
*/
|
||||
mode: 'explore' | 'traces-drilldown';
|
||||
/**
|
||||
* For iframe-based panels (like traces-drilldown), store the complete URL
|
||||
* including query parameters to restore state on reload
|
||||
*/
|
||||
iframeUrl?: string;
|
||||
}
|
||||
|
||||
export interface CanvasViewport {
|
||||
|
||||
Reference in New Issue
Block a user