Add component to be rendered by the assistant
This commit is contained in:
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useParams } from 'react-router-dom-v5-compat';
|
||||
import { ReactZoomPanPinchRef } from 'react-zoom-pan-pinch';
|
||||
|
||||
import { providePageContext, createAssistantContextItem } from '@grafana/assistant';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { ErrorBoundaryAlert, useStyles2 } from '@grafana/ui';
|
||||
@@ -10,6 +11,7 @@ import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||
import { useNavModel } from 'app/core/hooks/useNavModel';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
|
||||
import { AddPanelAction, DebugAssistantContext } from './components/AssistantComponents';
|
||||
import { ExploreMapCanvas } from './components/ExploreMapCanvas';
|
||||
import { ExploreMapFloatingToolbar } from './components/ExploreMapFloatingToolbar';
|
||||
import { ExploreMapToolbar } from './components/ExploreMapToolbar';
|
||||
@@ -17,6 +19,48 @@ import { TransformProvider } from './context/TransformContext';
|
||||
import { useCanvasPersistence } from './hooks/useCanvasPersistence';
|
||||
import { useRealtimeSync } from './realtime/useRealtimeSync';
|
||||
|
||||
// Register custom components for the Grafana Assistant using providePageContext
|
||||
// This ensures the component context is properly sent to the assistant
|
||||
providePageContext(/.*/, [
|
||||
createAssistantContextItem('component', {
|
||||
components: {
|
||||
AddPanelAction,
|
||||
},
|
||||
namespace: 'exploreMap',
|
||||
prompt: `You have access to an interactive component that helps users add panels to the explore map canvas with pre-configured datasources and queries.
|
||||
|
||||
Component name: exploreMap_AddPanelAction
|
||||
|
||||
Whitelisted props:
|
||||
- type: string - MUST ALWAYS BE "explore" (only explore panels are supported currently)
|
||||
- description: string (optional custom button label)
|
||||
- name: string (optional display name)
|
||||
- namespace: string (optional datasource UID - use this to specify which datasource the panel should use)
|
||||
- metric: string (optional query expression - PromQL for metrics, LogQL for logs, TraceQL for traces, etc.)
|
||||
IMPORTANT: Query must be URL-encoded to handle special characters like parentheses, quotes, braces, etc.
|
||||
|
||||
Usage examples (place directly in response, NEVER in code blocks):
|
||||
<exploreMap_AddPanelAction type="explore" />
|
||||
<exploreMap_AddPanelAction type="explore" namespace="prometheus-uid" metric="up" />
|
||||
<exploreMap_AddPanelAction type="explore" namespace="loki-uid" metric="%7Bjob%3D%22varlogs%22%7D" />
|
||||
<exploreMap_AddPanelAction type="explore" namespace="prometheus-uid" metric="rate%28http_requests_total%5B5m%5D%29" description="HTTP Request Rate" />
|
||||
<exploreMap_AddPanelAction type="explore" namespace="prometheus-uid" metric="histogram_quantile%280.95%2C%20sum%28rate%28http_request_duration_seconds_bucket%5B5m%5D%29%29%20by%20%28le%29%29" description="P95 Latency" />
|
||||
|
||||
CRITICAL RULES:
|
||||
- Components must NEVER be wrapped in code blocks (no backticks or \`\`\`).
|
||||
- **ALWAYS set type="explore"** - this is the only supported panel type currently.
|
||||
- Use namespace prop to specify datasource UID when you know which datasource to use.
|
||||
- Use metric prop to provide initial query expressions.
|
||||
- **ALWAYS URL-encode the metric prop value** to handle special characters: ( ) [ ] { } " ' = < > etc.
|
||||
Examples:
|
||||
- '{job="varlogs"}' becomes '%7Bjob%3D%22varlogs%22%7D'
|
||||
- 'rate(http_requests[5m])' becomes 'rate%28http_requests%5B5m%5D%29'
|
||||
- Place components directly in your response text, not in code blocks.
|
||||
- When users ask to add panels with specific queries, provide the component with namespace and URL-encoded metric props.
|
||||
- You can provide multiple components in a single response for adding multiple panels.`,
|
||||
}),
|
||||
]);
|
||||
|
||||
export default function ExploreMapPage(props: GrafanaRouteComponentProps<{ uid?: string }>) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { chrome } = useGrafana();
|
||||
@@ -58,7 +102,9 @@ export default function ExploreMapPage(props: GrafanaRouteComponentProps<{ uid?:
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={styles.loadingWrapper}>
|
||||
<p>Loading explore map...</p>
|
||||
<p>
|
||||
<Trans i18nKey="explore-map.loading">Loading explore map...</Trans>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -67,6 +113,7 @@ export default function ExploreMapPage(props: GrafanaRouteComponentProps<{ uid?:
|
||||
<ErrorBoundaryAlert>
|
||||
<TransformProvider value={{ transformRef }}>
|
||||
<div className={styles.pageWrapper}>
|
||||
<DebugAssistantContext />
|
||||
<h1 className="sr-only">
|
||||
<Trans i18nKey="nav.explore-map.title">Explore Map</Trans>
|
||||
</h1>
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { useDispatch } from 'app/types/store';
|
||||
|
||||
import { addPanel } from '../../state/crdtSlice';
|
||||
|
||||
/**
|
||||
* Whitelisted props that the Grafana Assistant allows for custom components.
|
||||
* We creatively map these to our needs:
|
||||
* - type: Panel type ('explore', 'metrics', 'logs', 'traces', 'profiles')
|
||||
* - description: Custom button label (optional)
|
||||
* - name: Display name (optional)
|
||||
* - namespace: Datasource UID
|
||||
* - metric: Query expression (PromQL, LogQL, etc.)
|
||||
*/
|
||||
interface AddPanelActionProps {
|
||||
name?: string; // Display name
|
||||
type?: string; // Panel type: 'explore' | 'metrics' | 'logs' | 'traces' | 'profiles'
|
||||
description?: string; // Custom button label
|
||||
properties?: string; // Reserved
|
||||
env?: string; // Reserved
|
||||
site?: string; // Reserved
|
||||
namespace?: string; // Datasource UID
|
||||
metric?: string; // Query expression
|
||||
value?: string; // Reserved
|
||||
unit?: string; // Reserved
|
||||
status?: string; // Reserved
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom component that the Grafana Assistant can use to add panels to the explore map canvas.
|
||||
* The assistant can render this component in its responses to provide interactive "Add Panel" buttons.
|
||||
*
|
||||
* Uses whitelisted props only:
|
||||
* - type: Panel type ('explore', 'metrics', 'logs', 'traces', 'profiles')
|
||||
* - description: Custom button label (optional)
|
||||
* - name: Display name (optional)
|
||||
* - namespace: Datasource UID (optional)
|
||||
* - metric: Query expression (optional)
|
||||
*/
|
||||
export const AddPanelAction: React.FC<AddPanelActionProps> = ({
|
||||
type = 'explore',
|
||||
description,
|
||||
name,
|
||||
namespace, // datasourceUid
|
||||
metric, // query expression
|
||||
}) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const dispatch = useDispatch();
|
||||
const currentUsername = contextSrv.user.name || contextSrv.user.login || 'Unknown';
|
||||
|
||||
// Map type to the internal mode format
|
||||
const getMode = (): 'explore' | 'traces-drilldown' | 'metrics-drilldown' | 'profiles-drilldown' | 'logs-drilldown' => {
|
||||
switch (type?.toLowerCase()) {
|
||||
case 'traces':
|
||||
return 'traces-drilldown';
|
||||
case 'metrics':
|
||||
return 'metrics-drilldown';
|
||||
case 'profiles':
|
||||
return 'profiles-drilldown';
|
||||
case 'logs':
|
||||
return 'logs-drilldown';
|
||||
default:
|
||||
return 'explore';
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddPanel = useCallback(() => {
|
||||
// Decode the metric if it's URL-encoded
|
||||
const decodedQuery = metric ? decodeURIComponent(metric) : undefined;
|
||||
const mode = getMode();
|
||||
|
||||
dispatch(
|
||||
addPanel({
|
||||
viewportSize: {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
},
|
||||
kind: mode,
|
||||
createdBy: currentUsername,
|
||||
datasourceUid: namespace, // Use namespace prop for datasource UID
|
||||
query: decodedQuery, // Decode URL-encoded query expression
|
||||
})
|
||||
);
|
||||
}, [dispatch, currentUsername, type, namespace, metric, getMode]);
|
||||
|
||||
const getButtonLabel = () => {
|
||||
if (description) {
|
||||
return description;
|
||||
}
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
switch (getMode()) {
|
||||
case 'traces-drilldown':
|
||||
return 'Add Traces Panel';
|
||||
case 'metrics-drilldown':
|
||||
return 'Add Metrics Panel';
|
||||
case 'profiles-drilldown':
|
||||
return 'Add Profiles Panel';
|
||||
case 'logs-drilldown':
|
||||
return 'Add Logs Panel';
|
||||
default:
|
||||
return 'Add Explore Panel';
|
||||
}
|
||||
};
|
||||
|
||||
const getButtonIcon = () => {
|
||||
switch (getMode()) {
|
||||
case 'traces-drilldown':
|
||||
case 'metrics-drilldown':
|
||||
case 'profiles-drilldown':
|
||||
case 'logs-drilldown':
|
||||
return 'plus';
|
||||
default:
|
||||
return 'compass';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Button icon={getButtonIcon()} onClick={handleAddPanel} variant="primary" size="sm">
|
||||
{getButtonLabel()}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css({
|
||||
display: 'inline-block',
|
||||
margin: theme.spacing(1, 0),
|
||||
}),
|
||||
};
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { usePageComponents, usePageContext } from '@grafana/assistant';
|
||||
|
||||
/**
|
||||
* Debug component to verify assistant context and component registration.
|
||||
* Add this temporarily to ExploreMapPage to see what's registered.
|
||||
*/
|
||||
export const DebugAssistantContext: React.FC = () => {
|
||||
const pageComponents = usePageComponents();
|
||||
const pageContext = usePageContext();
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.group('🔍 Assistant Debug Info');
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Current URL:', window.location.pathname);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Registered Components:', Object.keys(pageComponents));
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Component Details:', pageComponents);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Page Context Items:', pageContext.length);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Page Context:', pageContext);
|
||||
// eslint-disable-next-line no-console
|
||||
console.groupEnd();
|
||||
}, [pageComponents, pageContext]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
// Re-export components (eslint exception for assistant components barrel file)
|
||||
// eslint-disable-next-line no-barrel-files/no-barrel-files
|
||||
export { AddPanelAction } from './AddPanelAction';
|
||||
// eslint-disable-next-line no-barrel-files/no-barrel-files
|
||||
export { DebugAssistantContext } from './DebugAssistantContext';
|
||||
@@ -15,6 +15,7 @@ import { useDispatch, useSelector } from 'app/types/store';
|
||||
|
||||
import { addPanel } from '../state/crdtSlice';
|
||||
import { selectPanels, selectMapUid } from '../state/selectors';
|
||||
import { AddPanelAction } from './AssistantComponents';
|
||||
|
||||
export function ExploreMapFloatingToolbar() {
|
||||
const styles = useStyles2(getStyles);
|
||||
@@ -103,7 +104,7 @@ export function ExploreMapFloatingToolbar() {
|
||||
const panelsArray = Object.values(panels);
|
||||
|
||||
return createAssistantContextItem('structured', {
|
||||
title: 'Explore Canvas',
|
||||
title: t('explore-map.assistant.canvas-title', 'Explore Canvas'),
|
||||
data: {
|
||||
canvasId: mapUid,
|
||||
panelCount: panelsArray.length,
|
||||
@@ -121,6 +122,59 @@ export function ExploreMapFloatingToolbar() {
|
||||
});
|
||||
}, [panels, mapUid]);
|
||||
|
||||
// Provide component context and additional instructions to the assistant
|
||||
const componentContext = useMemo(() => {
|
||||
return createAssistantContextItem('component', {
|
||||
components: {
|
||||
AddPanelAction,
|
||||
},
|
||||
namespace: 'exploreMap',
|
||||
hidden: false, // Make visible so we can debug
|
||||
prompt: `IMPORTANT: You have an interactive component that can add pre-configured panels.
|
||||
|
||||
Component: exploreMap_AddPanelAction
|
||||
|
||||
Whitelisted props (ONLY these are allowed):
|
||||
- type: MUST ALWAYS BE "explore" (only explore panels supported)
|
||||
- namespace: datasource UID (optional - use to specify which datasource)
|
||||
- metric: query expression (optional - PromQL, LogQL, TraceQL, etc.) - MUST BE URL-ENCODED
|
||||
- description: custom button text (optional)
|
||||
- name: display name (optional)
|
||||
|
||||
Usage (place directly in response, NEVER in code blocks):
|
||||
<exploreMap_AddPanelAction type="explore" />
|
||||
<exploreMap_AddPanelAction type="explore" namespace="prometheus-uid" metric="up" />
|
||||
<exploreMap_AddPanelAction type="explore" namespace="loki-uid" metric="%7Bjob%3D%22varlogs%22%7D" />
|
||||
<exploreMap_AddPanelAction type="explore" namespace="prometheus-uid" metric="rate%28http_requests_total%5B5m%5D%29" description="HTTP Request Rate" />
|
||||
|
||||
CRITICAL:
|
||||
- **ALWAYS use type="explore"** - no other types are supported.
|
||||
- Never wrap components in backticks or code blocks.
|
||||
- Always URL-encode the metric prop to handle special characters like ( ) [ ] { } " ' etc.`,
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Provide additional instructions to the assistant (hidden from UI)
|
||||
const assistantInstructions = useMemo(() => {
|
||||
return createAssistantContextItem('structured', {
|
||||
hidden: true,
|
||||
title: t('explore-map.assistant.capabilities-title', 'Explore Map Capabilities'),
|
||||
data: {
|
||||
capabilities: [
|
||||
t('explore-map.assistant.capability-add-panels', 'You can help users add new panels to the canvas using the exploreMap_AddPanelAction component'),
|
||||
t('explore-map.assistant.capability-analyze', 'Analyze the current panels and suggest additional panels that would complement the existing ones'),
|
||||
t('explore-map.assistant.capability-gaps', 'Identify gaps in observability coverage (missing logs, traces, metrics, or profiles)'),
|
||||
t('explore-map.assistant.capability-layouts', 'Suggest panel layouts or organization strategies'),
|
||||
t('explore-map.assistant.capability-relationships', 'Help users understand relationships between panels based on their queries and datasources'),
|
||||
],
|
||||
instructions: t(
|
||||
'explore-map.assistant.instructions',
|
||||
'When users ask to add panels, use the exploreMap_AddPanelAction component to provide interactive buttons. You can suggest multiple panels at once by providing multiple component instances. Always explain why you are suggesting specific panel types based on the current canvas state.'
|
||||
),
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleOpenAssistant = useCallback(() => {
|
||||
if (!openAssistant) {
|
||||
return;
|
||||
@@ -134,11 +188,14 @@ export function ExploreMapFloatingToolbar() {
|
||||
openAssistant({
|
||||
origin: 'grafana/explore-map',
|
||||
mode: 'assistant',
|
||||
prompt: 'Analyze this explore canvas and summarize what queries and data are being visualized. Identify any patterns or relationships between the panels.',
|
||||
context: [canvasContext],
|
||||
prompt:
|
||||
'Analyze this explore canvas and summarize what queries and data are being visualized. ' +
|
||||
'Identify any patterns or relationships between the panels. ' +
|
||||
'If you notice gaps in observability coverage, suggest additional panels I could add using the interactive component.',
|
||||
context: [canvasContext, componentContext, assistantInstructions],
|
||||
autoSend: true,
|
||||
});
|
||||
}, [openAssistant, panels, mapUid, canvasContext]);
|
||||
}, [openAssistant, panels, mapUid, canvasContext, componentContext, assistantInstructions]);
|
||||
|
||||
const MenuActions = () => (
|
||||
<Menu>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { SerializedExploreState } from '../state/types';
|
||||
import { ExploreMapPanel, SerializedExploreState } from '../state/types';
|
||||
|
||||
import { HybridLogicalClock } from './hlc';
|
||||
import { LWWRegister, createLWWRegister } from './lwwregister';
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
OperationResult,
|
||||
CRDTExploreMapStateJSON,
|
||||
CommentData,
|
||||
BatchOperation,
|
||||
} from './types';
|
||||
|
||||
export class CRDTStateManager {
|
||||
@@ -129,8 +130,8 @@ export class CRDTStateManager {
|
||||
/**
|
||||
* Get all panels for UI rendering
|
||||
*/
|
||||
getAllPanelsForUI() {
|
||||
const panels: Record<string, any> = {};
|
||||
getAllPanelsForUI(): Record<string, ExploreMapPanel> {
|
||||
const panels: Record<string, ExploreMapPanel> = {};
|
||||
for (const panelId of this.getPanelIds()) {
|
||||
const panel = this.getPanelForUI(panelId);
|
||||
if (panel) {
|
||||
@@ -148,7 +149,8 @@ export class CRDTStateManager {
|
||||
exploreId: string,
|
||||
position: { x: number; y: number; width: number; height: number },
|
||||
mode: 'explore' | 'traces-drilldown' | 'metrics-drilldown' | 'profiles-drilldown' | 'logs-drilldown' = 'explore',
|
||||
createdBy?: string
|
||||
createdBy?: string,
|
||||
initialExploreState?: SerializedExploreState
|
||||
): AddPanelOperation {
|
||||
const timestamp = this.clock.tick();
|
||||
return {
|
||||
@@ -163,6 +165,7 @@ export class CRDTStateManager {
|
||||
position,
|
||||
mode,
|
||||
createdBy,
|
||||
initialExploreState,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -441,7 +444,7 @@ export class CRDTStateManager {
|
||||
return {
|
||||
success: false,
|
||||
applied: false,
|
||||
error: `Unknown operation type: ${(operation as any).type}`,
|
||||
error: `Unknown operation type: ${(operation as CRDTOperation).type}`,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -454,7 +457,7 @@ export class CRDTStateManager {
|
||||
}
|
||||
|
||||
private applyAddPanel(operation: AddPanelOperation): OperationResult {
|
||||
const { panelId, exploreId, position, mode, createdBy } = operation.payload;
|
||||
const { panelId, exploreId, position, mode, createdBy, initialExploreState } = operation.payload;
|
||||
const panelMode = mode || 'explore';
|
||||
|
||||
// Add to OR-Set with operation ID as tag
|
||||
@@ -472,7 +475,7 @@ export class CRDTStateManager {
|
||||
width: new LWWRegister(position.width, operation.timestamp),
|
||||
height: new LWWRegister(position.height, operation.timestamp),
|
||||
zIndex: new LWWRegister(zIndex, operation.timestamp),
|
||||
exploreState: new LWWRegister(undefined, operation.timestamp),
|
||||
exploreState: new LWWRegister(initialExploreState, operation.timestamp),
|
||||
mode: new LWWRegister(panelMode, operation.timestamp),
|
||||
iframeUrl: new LWWRegister(undefined, operation.timestamp),
|
||||
createdBy: new LWWRegister(createdBy, operation.timestamp),
|
||||
@@ -643,7 +646,7 @@ export class CRDTStateManager {
|
||||
};
|
||||
}
|
||||
|
||||
private applyBatchOperation(operation: any): OperationResult {
|
||||
private applyBatchOperation(operation: BatchOperation): OperationResult {
|
||||
let anyApplied = false;
|
||||
const errors: string[] = [];
|
||||
|
||||
@@ -726,7 +729,7 @@ export class CRDTStateManager {
|
||||
* Serialize state to JSON
|
||||
*/
|
||||
toJSON(): CRDTExploreMapStateJSON {
|
||||
const panelData: Record<string, any> = {};
|
||||
const panelData: CRDTExploreMapStateJSON['panelData'] = {};
|
||||
|
||||
for (const [panelId, data] of this.state.panelData.entries()) {
|
||||
panelData[panelId] = {
|
||||
|
||||
@@ -172,6 +172,7 @@ export interface AddPanelOperation extends CRDTOperationBase {
|
||||
};
|
||||
mode?: 'explore' | 'traces-drilldown' | 'metrics-drilldown' | 'profiles-drilldown' | 'logs-drilldown';
|
||||
createdBy?: string;
|
||||
initialExploreState?: SerializedExploreState; // Optional initial datasource/query configuration
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { dateTime, DataQuery } from '@grafana/data';
|
||||
import { generateExploreId } from 'app/core/utils/explore';
|
||||
|
||||
import { CRDTStateManager } from '../crdt/state';
|
||||
@@ -218,6 +219,8 @@ const crdtSlice = createSlice({
|
||||
position?: { x: number; y: number; width: number; height: number };
|
||||
kind?: 'explore' | 'traces-drilldown' | 'metrics-drilldown' | 'profiles-drilldown' | 'logs-drilldown';
|
||||
createdBy?: string;
|
||||
datasourceUid?: string;
|
||||
query?: string;
|
||||
}>) => {
|
||||
const manager = getCRDTManager(state);
|
||||
|
||||
@@ -254,7 +257,30 @@ const crdtSlice = createSlice({
|
||||
const panelId = uuidv4();
|
||||
const exploreId = generateExploreId();
|
||||
|
||||
const operation = manager.createAddPanelOperation(panelId, exploreId, position, mode, action.payload.createdBy);
|
||||
// Build initial explore state if datasource/query provided
|
||||
let initialExploreState: SerializedExploreState | undefined;
|
||||
if (action.payload.datasourceUid || action.payload.query) {
|
||||
initialExploreState = {
|
||||
queries: action.payload.query
|
||||
? [
|
||||
{
|
||||
refId: 'A',
|
||||
datasource: { uid: action.payload.datasourceUid, type: 'prometheus' },
|
||||
// Using index signature to add query-specific field
|
||||
...{ expr: action.payload.query },
|
||||
} as DataQuery,
|
||||
]
|
||||
: [],
|
||||
datasourceUid: action.payload.datasourceUid,
|
||||
range: {
|
||||
from: dateTime('now-1h'),
|
||||
to: dateTime('now'),
|
||||
raw: { from: 'now-1h', to: 'now' },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const operation = manager.createAddPanelOperation(panelId, exploreId, position, mode, action.payload.createdBy, initialExploreState);
|
||||
|
||||
// Apply locally
|
||||
manager.applyOperation(operation);
|
||||
|
||||
Reference in New Issue
Block a user