Skip to content

Commit c47a449

Browse files
committed
mcpToolUI basic implementation
Provides a hook to visualize an mcp-tool call with Perses elements. It also adds an ability to add the panel from the OLS chat into opened dashboards.
1 parent 21d103a commit c47a449

12 files changed

Lines changed: 289 additions & 10 deletions

config/perses-dashboards.patch.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,5 +119,18 @@
119119
"component": { "$codeRef": "DashboardPage" }
120120
}
121121
}
122+
},
123+
{
124+
"op": "add",
125+
"path": "/extensions/1",
126+
"value": {
127+
"type": "ols.tool-ui",
128+
"properties": {
129+
"id": "mcp-obs/execute-range-query",
130+
"component": {
131+
"$codeRef": "ols-tool-ui.ExecuteRangeQuery"
132+
}
133+
}
134+
}
122135
}
123136
]

web/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,8 @@
182182
"TargetsPage": "./components/targets-page",
183183
"PrometheusRedirectPage": "./components/redirects/prometheus-redirect-page",
184184
"DevRedirects": "./components/redirects/dev-redirects",
185-
"MonitoringContext": "./contexts/MonitoringContext"
185+
"MonitoringContext": "./contexts/MonitoringContext",
186+
"ols-tool-ui": "./components/ols-tool-ui"
186187
},
187188
"dependencies": {
188189
"@console/pluginAPI": "*"

web/src/components/dashboards/perses/PersesWrapper.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,6 @@ export function useRemotePluginLoader(): PluginLoader {
343343

344344
export function PersesWrapper({ children, project }: PersesWrapperProps) {
345345
const { theme } = usePatternFlyTheme();
346-
const [dashboardName] = useQueryParam(QueryParams.Dashboard, StringParam);
347346
const muiTheme = getTheme(theme, {
348347
shape: {
349348
borderRadius: 6,
@@ -371,21 +370,16 @@ export function PersesWrapper({ children, project }: PersesWrapperProps) {
371370
variant="default"
372371
>
373372
<PluginRegistry pluginLoader={pluginLoader}>
374-
{!project ? (
375-
<>{children}</>
376-
) : (
377-
<InnerWrapper project={project} dashboardName={dashboardName}>
378-
{children}
379-
</InnerWrapper>
380-
)}
373+
{!project ? <>{children}</> : <InnerWrapper project={project}>{children}</InnerWrapper>}
381374
</PluginRegistry>
382375
</SnackbarProvider>
383376
</ChartsProvider>
384377
</ThemeProvider>
385378
);
386379
}
387380

388-
function InnerWrapper({ children, project, dashboardName }) {
381+
function InnerWrapper({ children, project }) {
382+
const [dashboardName] = useQueryParam(QueryParams.Dashboard, StringParam);
389383
const { data } = usePluginBuiltinVariableDefinitions();
390384
const { persesDashboard, persesDashboardLoading } = useFetchPersesDashboard(
391385
project,

web/src/components/dashboards/perses/dashboard-app.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ import {
2424
useDiscardChangesConfirmationDialog,
2525
useEditMode,
2626
} from '@perses-dev/dashboards';
27+
2728
import { OCPDashboardToolbar } from './dashboard-toolbar';
2829
import { useUpdateDashboardMutation } from './dashboard-api';
2930
import { useTranslation } from 'react-i18next';
3031
import { useToast } from './ToastProvider';
3132
import { useSearchParams } from 'react-router-dom-v5-compat';
33+
import { useExternalPanelAddition } from './useExternalPanelAddition';
3234

3335
export interface DashboardAppProps {
3436
dashboardResource: DashboardResource | EphemeralDashboardResource;
@@ -124,6 +126,8 @@ export const OCPDashboardApp = (props: DashboardAppProps): ReactElement => {
124126
}
125127
};
126128

129+
useExternalPanelAddition({ isEditMode, onEditButtonClick });
130+
127131
const updateDashboardMutation = useUpdateDashboardMutation();
128132

129133
const onSave = useCallback(
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { useEffect, useState } from 'react';
2+
import { useDispatch, useSelector } from 'react-redux';
3+
import { useDashboardActions, useDashboardStore } from '@perses-dev/dashboards';
4+
import { dashboardsOpened, dashboardsPersesPanelExternallyAdded } from '../../../store/actions';
5+
6+
interface UseExternalPanelAdditionOptions {
7+
isEditMode: boolean;
8+
onEditButtonClick: () => void;
9+
}
10+
11+
export function useExternalPanelAddition({
12+
isEditMode,
13+
onEditButtonClick,
14+
}: UseExternalPanelAdditionOptions) {
15+
const dispatch = useDispatch();
16+
const addPersesPanelExternally: any = useSelector(
17+
(s: any) => s.plugins?.mp?.dashboards?.addPersesPanelExternally,
18+
);
19+
const { openAddPanel } = useDashboardActions();
20+
const dashboardStore = useDashboardStore();
21+
const [externallyAddedPanel, setExternallyAddedPanel] = useState(null);
22+
23+
const addPanelExternally = (panelDefinition: any): void => {
24+
// Simulate opening a panel to add the pane so that we can use it to programatically
25+
// add a panel to the dashboard from an external source (AI assistant).
26+
if (!isEditMode) {
27+
onEditButtonClick();
28+
}
29+
openAddPanel();
30+
// Wrap the panelDefinition with the groupId structure
31+
const change = {
32+
groupId: 0,
33+
panelDefinition,
34+
};
35+
setExternallyAddedPanel(change);
36+
};
37+
38+
useEffect(() => {
39+
// Listen for external panel addition requests
40+
if (addPersesPanelExternally) {
41+
addPanelExternally(addPersesPanelExternally);
42+
dispatch(dashboardsPersesPanelExternallyAdded());
43+
}
44+
45+
// Apply externally added panel
46+
if (externallyAddedPanel) {
47+
const groupId = dashboardStore.panelGroupOrder[0];
48+
externallyAddedPanel.groupId = groupId;
49+
50+
// Use the temporary panelEditor to add changes to the dashboard.
51+
const panelEditor = dashboardStore.panelEditor;
52+
panelEditor.applyChanges(externallyAddedPanel);
53+
panelEditor.close();
54+
55+
// Clear the externally added panel after applying changes
56+
setExternallyAddedPanel(null);
57+
}
58+
}, [externallyAddedPanel, addPersesPanelExternally]);
59+
60+
// Advertise when custom dashboard is opened/closed
61+
useEffect(() => {
62+
dispatch(dashboardsOpened(true));
63+
return () => {
64+
dispatch(dashboardsOpened(false));
65+
};
66+
}, [dispatch]);
67+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react';
2+
import { useDispatch, useSelector } from 'react-redux';
3+
import type { PanelDefinition } from '@perses-dev/core';
4+
import { Button } from '@patternfly/react-core';
5+
import { dashboardsAddPersesPanelExternally } from '../../store/actions';
6+
7+
function createPanelDefinition(query: string): PanelDefinition {
8+
return {
9+
kind: 'Panel',
10+
spec: {
11+
display: {
12+
name: '',
13+
},
14+
plugin: {
15+
kind: 'TimeSeriesChart',
16+
spec: {},
17+
},
18+
queries: [
19+
{
20+
kind: 'TimeSeriesQuery',
21+
spec: {
22+
plugin: {
23+
kind: 'PrometheusTimeSeriesQuery',
24+
spec: {
25+
query: query,
26+
},
27+
},
28+
},
29+
},
30+
],
31+
},
32+
};
33+
}
34+
35+
type AddToDashboardButtonProps = {
36+
query: string;
37+
};
38+
39+
export const AddToDashboardButton: React.FC<AddToDashboardButtonProps> = ({ query }) => {
40+
const dispatch = useDispatch();
41+
42+
const isCustomDashboardOpen: boolean = useSelector(
43+
(s: any) => s.plugins?.mp?.dashboards?.isOpened,
44+
);
45+
46+
const addToPersesDashboard = React.useCallback(() => {
47+
const panelDefinition = createPanelDefinition(query);
48+
dispatch(dashboardsAddPersesPanelExternally(panelDefinition));
49+
}, [query, dispatch]);
50+
51+
if (!isCustomDashboardOpen) {
52+
return null;
53+
}
54+
55+
return <Button onClick={addToPersesDashboard}>Add to dashboard</Button>;
56+
};
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import React from 'react';
2+
import { DataQueriesProvider } from '@perses-dev/plugin-system';
3+
import type { DurationString } from '@perses-dev/prometheus-plugin';
4+
import { Panel } from '@perses-dev/dashboards';
5+
6+
import { OlsToolUIPersesWrapper } from './OlsToolUIPersesWrapper';
7+
import { AddToDashboardButton } from './AddToDashboardButton';
8+
9+
type ExecuteRangeQueryTool = {
10+
name: 'execute_range_query';
11+
args: {
12+
query: string;
13+
};
14+
};
15+
16+
const persesTimeRange = {
17+
pastDuration: '1h' as DurationString,
18+
};
19+
20+
export const ExecuteRangeQuery: React.FC<{ tool: ExecuteRangeQueryTool }> = ({ tool }) => {
21+
const query = tool.args.query;
22+
const definitions = [
23+
{
24+
kind: 'PrometheusTimeSeriesQuery',
25+
spec: {
26+
query: query,
27+
},
28+
},
29+
];
30+
31+
return (
32+
<>
33+
<OlsToolUIPersesWrapper initialTimeRange={persesTimeRange}>
34+
<DataQueriesProvider
35+
definitions={definitions}
36+
options={{ suggestedStepMs: 15000, mode: 'range' }}
37+
>
38+
<Panel
39+
panelOptions={{
40+
hideHeader: false,
41+
}}
42+
definition={{
43+
kind: 'Panel',
44+
spec: {
45+
queries: [],
46+
display: { name: query },
47+
plugin: {
48+
kind: 'TimeSeriesChart',
49+
spec: {
50+
visual: {
51+
stack: 'all',
52+
},
53+
},
54+
},
55+
},
56+
}}
57+
/>
58+
</DataQueriesProvider>
59+
</OlsToolUIPersesWrapper>
60+
<AddToDashboardButton query={query} />
61+
</>
62+
);
63+
};
64+
65+
export default ExecuteRangeQuery;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from 'react';
2+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3+
import { VariableProvider } from '@perses-dev/dashboards';
4+
import { TimeRangeProviderBasic } from '@perses-dev/plugin-system';
5+
import type { DurationString } from '@perses-dev/prometheus-plugin';
6+
7+
import {
8+
PersesWrapper,
9+
PersesPrometheusDatasourceWrapper,
10+
} from '../dashboards/perses/PersesWrapper';
11+
12+
const queryClient = new QueryClient({
13+
defaultOptions: {
14+
queries: {
15+
retry: false,
16+
refetchOnWindowFocus: false,
17+
},
18+
},
19+
});
20+
21+
interface OlsToolUIPersesWrapperProps {
22+
children: React.ReactNode;
23+
height?: string;
24+
initialTimeRange?: {
25+
pastDuration: DurationString;
26+
};
27+
}
28+
29+
export const OlsToolUIPersesWrapper: React.FC<OlsToolUIPersesWrapperProps> = ({
30+
children,
31+
initialTimeRange = { pastDuration: '1h' as DurationString },
32+
height = '300px',
33+
}) => {
34+
return (
35+
<QueryClientProvider client={queryClient}>
36+
<PersesWrapper project={null}>
37+
<TimeRangeProviderBasic initialTimeRange={initialTimeRange}>
38+
<VariableProvider>
39+
<PersesPrometheusDatasourceWrapper queries={[]}>
40+
<div style={{ width: '100%', height: height }}>{children}</div>
41+
</PersesPrometheusDatasourceWrapper>
42+
</VariableProvider>
43+
</TimeRangeProviderBasic>
44+
</PersesWrapper>
45+
</QueryClientProvider>
46+
);
47+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ExecuteRangeQuery } from './ExecuteRangeQuery';

web/src/store/actions.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { PanelDefinition } from '@perses-dev/core';
12
import { action, ActionType as Action } from 'typesafe-actions';
23

34
import { Alert, Rule, Silence } from '@openshift-console/dynamic-plugin-sdk';
@@ -17,6 +18,9 @@ export enum ActionType {
1718
DashboardsSetPollInterval = 'v2/dashboardsSetPollInterval',
1819
DashboardsSetTimespan = 'v2/dashboardsSetTimespan',
1920
DashboardsVariableOptionsLoaded = 'v2/dashboardsVariableOptionsLoaded',
21+
DashboardsOpened = 'dashboardsPersesDashboardsOpened',
22+
DashboardsAddPersesPanelExternally = 'dashboardsAddPersesPanelExternally',
23+
DashboardsPersesPanelExternallyAdded = 'dashboardsPersesPanelExternallyAdded',
2024
QueryBrowserAddQuery = 'queryBrowserAddQuery',
2125
QueryBrowserDuplicateQuery = 'queryBrowserDuplicateQuery',
2226
QueryBrowserDeleteAllQueries = 'queryBrowserDeleteAllQueries',
@@ -68,6 +72,15 @@ export const dashboardsSetTimespan = (timespan: number) =>
6872
export const dashboardsVariableOptionsLoaded = (key: string, newOptions: string[]) =>
6973
action(ActionType.DashboardsVariableOptionsLoaded, { key, newOptions });
7074

75+
export const dashboardsOpened = (isOpened: boolean) =>
76+
action(ActionType.DashboardsOpened, { isOpened });
77+
78+
export const dashboardsPersesPanelExternallyAdded = () =>
79+
action(ActionType.DashboardsPersesPanelExternallyAdded, {});
80+
81+
export const dashboardsAddPersesPanelExternally = (panelDefinition: PanelDefinition) =>
82+
action(ActionType.DashboardsAddPersesPanelExternally, { panelDefinition });
83+
7184
export const alertingSetLoading = (datasource: string, identifier: string) =>
7285
action(ActionType.AlertingSetLoading, {
7386
datasource,

0 commit comments

Comments
 (0)