Show the creator of a panel
This commit is contained in:
@@ -4,6 +4,7 @@ import { useCallback, useState } from 'react';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Button, ButtonGroup, Dropdown, Menu, MenuItem, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import pyroscopeIconSvg from 'app/plugins/datasource/grafana-pyroscope-datasource/img/grafana_pyroscope_icon.svg';
|
||||
import lokiIconSvg from 'app/plugins/datasource/loki/img/loki_icon.svg';
|
||||
import prometheusLogoSvg from 'app/plugins/datasource/prometheus/img/prometheus_logo.svg';
|
||||
@@ -16,6 +17,7 @@ export function ExploreMapFloatingToolbar() {
|
||||
const styles = useStyles2(getStyles);
|
||||
const dispatch = useDispatch();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const currentUsername = contextSrv.user.name || contextSrv.user.login || 'Unknown';
|
||||
|
||||
const handleAddPanel = useCallback(() => {
|
||||
dispatch(
|
||||
@@ -24,10 +26,11 @@ export function ExploreMapFloatingToolbar() {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
},
|
||||
createdBy: currentUsername,
|
||||
})
|
||||
);
|
||||
setIsOpen(false);
|
||||
}, [dispatch]);
|
||||
}, [dispatch, currentUsername]);
|
||||
|
||||
const handleAddTracesDrilldownPanel = useCallback(() => {
|
||||
dispatch(
|
||||
@@ -37,10 +40,11 @@ export function ExploreMapFloatingToolbar() {
|
||||
height: window.innerHeight,
|
||||
},
|
||||
kind: 'traces-drilldown',
|
||||
createdBy: currentUsername,
|
||||
})
|
||||
);
|
||||
setIsOpen(false);
|
||||
}, [dispatch]);
|
||||
}, [dispatch, currentUsername]);
|
||||
|
||||
const handleAddMetricsDrilldownPanel = useCallback(() => {
|
||||
dispatch(
|
||||
@@ -50,10 +54,11 @@ export function ExploreMapFloatingToolbar() {
|
||||
height: window.innerHeight,
|
||||
},
|
||||
kind: 'metrics-drilldown',
|
||||
createdBy: currentUsername,
|
||||
})
|
||||
);
|
||||
setIsOpen(false);
|
||||
}, [dispatch]);
|
||||
}, [dispatch, currentUsername]);
|
||||
|
||||
const handleAddProfilesDrilldownPanel = useCallback(() => {
|
||||
dispatch(
|
||||
@@ -63,10 +68,11 @@ export function ExploreMapFloatingToolbar() {
|
||||
height: window.innerHeight,
|
||||
},
|
||||
kind: 'profiles-drilldown',
|
||||
createdBy: currentUsername,
|
||||
})
|
||||
);
|
||||
setIsOpen(false);
|
||||
}, [dispatch]);
|
||||
}, [dispatch, currentUsername]);
|
||||
|
||||
const handleAddLogsDrilldownPanel = useCallback(() => {
|
||||
dispatch(
|
||||
@@ -76,10 +82,11 @@ export function ExploreMapFloatingToolbar() {
|
||||
height: window.innerHeight,
|
||||
},
|
||||
kind: 'logs-drilldown',
|
||||
createdBy: currentUsername,
|
||||
})
|
||||
);
|
||||
setIsOpen(false);
|
||||
}, [dispatch]);
|
||||
}, [dispatch, currentUsername]);
|
||||
|
||||
const MenuActions = () => (
|
||||
<Menu>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Rnd, RndDragCallback, RndResizeCallback } from 'react-rnd';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { Button, useStyles2 } from '@grafana/ui';
|
||||
import { Button, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { useDispatch, useSelector } from 'app/types/store';
|
||||
|
||||
import { splitClose } from '../../explore/state/main';
|
||||
@@ -64,9 +64,6 @@ function ExploreMapPanelContainerComponent({ panel }: ExploreMapPanelContainerPr
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaX = data.x - dragStartPos.x;
|
||||
const deltaY = data.y - dragStartPos.y;
|
||||
|
||||
// If dragging multiple panels, store the drag offset in local state
|
||||
// This will cause other panels to visually move without CRDT operations
|
||||
if (isSelected && selectedPanelIds.length > 1) {
|
||||
@@ -190,6 +187,18 @@ function ExploreMapPanelContainerComponent({ panel }: ExploreMapPanelContainerPr
|
||||
[dispatch, panel.id]
|
||||
);
|
||||
|
||||
const handleInfoClick = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
// Build tooltip content for panel info
|
||||
const getInfoTooltipContent = useCallback(() => {
|
||||
if (panel.createdBy) {
|
||||
return t('explore-map.panel.info.created-by', 'Created by: {{creator}}', { creator: panel.createdBy });
|
||||
}
|
||||
return t('explore-map.panel.info.no-creator', 'Creator unknown');
|
||||
}, [panel.createdBy]);
|
||||
|
||||
return (
|
||||
<Rnd
|
||||
ref={rndRef}
|
||||
@@ -214,6 +223,16 @@ function ExploreMapPanelContainerComponent({ panel }: ExploreMapPanelContainerPr
|
||||
{t('explore-map.panel.title', 'Explore Panel {{id}}', { id: panel.id.slice(0, 8) })}
|
||||
</div>
|
||||
<div className={styles.panelActions}>
|
||||
<Tooltip content={getInfoTooltipContent()} placement="bottom">
|
||||
<Button
|
||||
icon="info-circle"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
fill="text"
|
||||
onClick={handleInfoClick}
|
||||
aria-label={t('explore-map.panel.info', 'Panel information')}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Button
|
||||
icon="copy"
|
||||
variant="secondary"
|
||||
|
||||
@@ -121,6 +121,7 @@ export class CRDTStateManager {
|
||||
exploreState: data.exploreState.get(),
|
||||
mode: data.mode.get(),
|
||||
iframeUrl: data.iframeUrl.get(),
|
||||
createdBy: data.createdBy.get(),
|
||||
remoteVersion: data.remoteVersion,
|
||||
};
|
||||
}
|
||||
@@ -146,7 +147,8 @@ export class CRDTStateManager {
|
||||
panelId: string,
|
||||
exploreId: string,
|
||||
position: { x: number; y: number; width: number; height: number },
|
||||
mode: 'explore' | 'traces-drilldown' | 'metrics-drilldown' | 'profiles-drilldown' | 'logs-drilldown' = 'explore'
|
||||
mode: 'explore' | 'traces-drilldown' | 'metrics-drilldown' | 'profiles-drilldown' | 'logs-drilldown' = 'explore',
|
||||
createdBy?: string
|
||||
): AddPanelOperation {
|
||||
const timestamp = this.clock.tick();
|
||||
return {
|
||||
@@ -160,6 +162,7 @@ export class CRDTStateManager {
|
||||
exploreId,
|
||||
position,
|
||||
mode,
|
||||
createdBy,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -451,7 +454,7 @@ export class CRDTStateManager {
|
||||
}
|
||||
|
||||
private applyAddPanel(operation: AddPanelOperation): OperationResult {
|
||||
const { panelId, exploreId, position, mode } = operation.payload;
|
||||
const { panelId, exploreId, position, mode, createdBy } = operation.payload;
|
||||
const panelMode = mode || 'explore';
|
||||
|
||||
// Add to OR-Set with operation ID as tag
|
||||
@@ -472,6 +475,7 @@ export class CRDTStateManager {
|
||||
exploreState: new LWWRegister(undefined, operation.timestamp),
|
||||
mode: new LWWRegister(panelMode, operation.timestamp),
|
||||
iframeUrl: new LWWRegister(undefined, operation.timestamp),
|
||||
createdBy: new LWWRegister(createdBy, operation.timestamp),
|
||||
remoteVersion: 0,
|
||||
});
|
||||
}
|
||||
@@ -697,6 +701,7 @@ export class CRDTStateManager {
|
||||
exploreState: otherPanelData.exploreState.clone(),
|
||||
mode: otherPanelData.mode.clone(),
|
||||
iframeUrl: otherPanelData.iframeUrl.clone(),
|
||||
createdBy: otherPanelData.createdBy.clone(),
|
||||
remoteVersion: otherPanelData.remoteVersion,
|
||||
});
|
||||
} else {
|
||||
@@ -709,6 +714,7 @@ export class CRDTStateManager {
|
||||
myPanelData.exploreState.merge(otherPanelData.exploreState);
|
||||
myPanelData.mode.merge(otherPanelData.mode);
|
||||
myPanelData.iframeUrl.merge(otherPanelData.iframeUrl);
|
||||
myPanelData.createdBy.merge(otherPanelData.createdBy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,6 +740,7 @@ export class CRDTStateManager {
|
||||
exploreState: data.exploreState.toJSON(),
|
||||
mode: data.mode.toJSON(),
|
||||
iframeUrl: data.iframeUrl.toJSON(),
|
||||
createdBy: data.createdBy.toJSON(),
|
||||
remoteVersion: data.remoteVersion,
|
||||
};
|
||||
}
|
||||
@@ -789,6 +796,7 @@ export class CRDTStateManager {
|
||||
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),
|
||||
createdBy: data.createdBy ? LWWRegister.fromJSON(data.createdBy) : new LWWRegister(undefined, defaultTimestamp),
|
||||
remoteVersion: data.remoteVersion || 0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ export interface CRDTPanelData {
|
||||
// Iframe URL for traces-drilldown panels
|
||||
iframeUrl: LWWRegister<string | undefined>;
|
||||
|
||||
// Creator metadata (username of who created the panel)
|
||||
createdBy: LWWRegister<string | undefined>;
|
||||
|
||||
// Local counter incremented only for remote explore state updates
|
||||
remoteVersion: number;
|
||||
}
|
||||
@@ -117,6 +120,7 @@ export interface CRDTExploreMapStateJSON {
|
||||
exploreState: { value: SerializedExploreState | undefined; timestamp: HLCTimestamp };
|
||||
mode: { value: 'explore' | 'traces-drilldown' | 'metrics-drilldown' | 'profiles-drilldown' | 'logs-drilldown'; timestamp: HLCTimestamp };
|
||||
iframeUrl: { value: string | undefined; timestamp: HLCTimestamp };
|
||||
createdBy?: { value: string | undefined; timestamp: HLCTimestamp };
|
||||
remoteVersion?: number;
|
||||
}>;
|
||||
zIndexCounter: {
|
||||
@@ -167,6 +171,7 @@ export interface AddPanelOperation extends CRDTOperationBase {
|
||||
height: number;
|
||||
};
|
||||
mode?: 'explore' | 'traces-drilldown' | 'metrics-drilldown' | 'profiles-drilldown' | 'logs-drilldown';
|
||||
createdBy?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -217,6 +217,7 @@ const crdtSlice = createSlice({
|
||||
viewportSize?: { width: number; height: number };
|
||||
position?: { x: number; y: number; width: number; height: number };
|
||||
kind?: 'explore' | 'traces-drilldown' | 'metrics-drilldown' | 'profiles-drilldown' | 'logs-drilldown';
|
||||
createdBy?: string;
|
||||
}>) => {
|
||||
const manager = getCRDTManager(state);
|
||||
|
||||
@@ -253,7 +254,7 @@ const crdtSlice = createSlice({
|
||||
const panelId = uuidv4();
|
||||
const exploreId = generateExploreId();
|
||||
|
||||
const operation = manager.createAddPanelOperation(panelId, exploreId, position, mode);
|
||||
const operation = manager.createAddPanelOperation(panelId, exploreId, position, mode, action.payload.createdBy);
|
||||
|
||||
// Apply locally
|
||||
manager.applyOperation(operation);
|
||||
@@ -513,7 +514,8 @@ const crdtSlice = createSlice({
|
||||
width: sourcePanel.position.width,
|
||||
height: sourcePanel.position.height,
|
||||
},
|
||||
sourcePanel.mode || 'explore'
|
||||
sourcePanel.mode || 'explore',
|
||||
sourcePanel.createdBy
|
||||
);
|
||||
|
||||
manager.applyOperation(addOperation);
|
||||
|
||||
@@ -37,6 +37,10 @@ export interface ExploreMapPanel {
|
||||
* including query parameters to restore state on reload
|
||||
*/
|
||||
iframeUrl?: string;
|
||||
/**
|
||||
* Username of the user who created this panel
|
||||
*/
|
||||
createdBy?: string;
|
||||
}
|
||||
|
||||
export interface CanvasViewport {
|
||||
|
||||
Reference in New Issue
Block a user