diff --git a/config/perses-dashboards.patch.json b/config/perses-dashboards.patch.json
index 2b90e7ac9..a93f9dacf 100644
--- a/config/perses-dashboards.patch.json
+++ b/config/perses-dashboards.patch.json
@@ -119,5 +119,18 @@
"component": { "$codeRef": "DashboardPage" }
}
}
+ },
+ {
+ "op": "add",
+ "path": "/extensions/1",
+ "value": {
+ "type": "ols.tool-ui",
+ "properties": {
+ "id": "mcp-obs/execute-range-query",
+ "component": {
+ "$codeRef": "ols-tool-ui.ExecuteRangeQuery"
+ }
+ }
+ }
}
]
diff --git a/web/package.json b/web/package.json
index f6906b0e5..7778f8cab 100644
--- a/web/package.json
+++ b/web/package.json
@@ -182,7 +182,8 @@
"TargetsPage": "./components/targets-page",
"PrometheusRedirectPage": "./components/redirects/prometheus-redirect-page",
"DevRedirects": "./components/redirects/dev-redirects",
- "MonitoringContext": "./contexts/MonitoringContext"
+ "MonitoringContext": "./contexts/MonitoringContext",
+ "ols-tool-ui": "./components/ols-tool-ui"
},
"dependencies": {
"@console/pluginAPI": "*"
diff --git a/web/src/components/dashboards/perses/PersesWrapper.tsx b/web/src/components/dashboards/perses/PersesWrapper.tsx
index 47589122a..0491ea382 100644
--- a/web/src/components/dashboards/perses/PersesWrapper.tsx
+++ b/web/src/components/dashboards/perses/PersesWrapper.tsx
@@ -343,7 +343,6 @@ export function useRemotePluginLoader(): PluginLoader {
export function PersesWrapper({ children, project }: PersesWrapperProps) {
const { theme } = usePatternFlyTheme();
- const [dashboardName] = useQueryParam(QueryParams.Dashboard, StringParam);
const muiTheme = getTheme(theme, {
shape: {
borderRadius: 6,
@@ -371,13 +370,7 @@ export function PersesWrapper({ children, project }: PersesWrapperProps) {
variant="default"
>
- {!project ? (
- <>{children}>
- ) : (
-
- {children}
-
- )}
+ {!project ? <>{children}> : {children}}
@@ -385,7 +378,8 @@ export function PersesWrapper({ children, project }: PersesWrapperProps) {
);
}
-function InnerWrapper({ children, project, dashboardName }) {
+function InnerWrapper({ children, project }) {
+ const [dashboardName] = useQueryParam(QueryParams.Dashboard, StringParam);
const { data } = usePluginBuiltinVariableDefinitions();
const { persesDashboard, persesDashboardLoading } = useFetchPersesDashboard(
project,
diff --git a/web/src/components/dashboards/perses/dashboard-app.tsx b/web/src/components/dashboards/perses/dashboard-app.tsx
index 62b9e4616..a7981ea2c 100644
--- a/web/src/components/dashboards/perses/dashboard-app.tsx
+++ b/web/src/components/dashboards/perses/dashboard-app.tsx
@@ -24,11 +24,13 @@ import {
useDiscardChangesConfirmationDialog,
useEditMode,
} from '@perses-dev/dashboards';
+
import { OCPDashboardToolbar } from './dashboard-toolbar';
import { useUpdateDashboardMutation } from './dashboard-api';
import { useTranslation } from 'react-i18next';
import { useToast } from './ToastProvider';
import { useSearchParams } from 'react-router-dom-v5-compat';
+import { useExternalPanelAddition } from './useExternalPanelAddition';
export interface DashboardAppProps {
dashboardResource: DashboardResource | EphemeralDashboardResource;
@@ -124,6 +126,8 @@ export const OCPDashboardApp = (props: DashboardAppProps): ReactElement => {
}
};
+ useExternalPanelAddition({ isEditMode, onEditButtonClick });
+
const updateDashboardMutation = useUpdateDashboardMutation();
const onSave = useCallback(
diff --git a/web/src/components/dashboards/perses/useExternalPanelAddition.ts b/web/src/components/dashboards/perses/useExternalPanelAddition.ts
new file mode 100644
index 000000000..715bda792
--- /dev/null
+++ b/web/src/components/dashboards/perses/useExternalPanelAddition.ts
@@ -0,0 +1,67 @@
+import { useEffect, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { useDashboardActions, useDashboardStore } from '@perses-dev/dashboards';
+import { dashboardsOpened, dashboardsPersesPanelExternallyAdded } from '../../../store/actions';
+
+interface UseExternalPanelAdditionOptions {
+ isEditMode: boolean;
+ onEditButtonClick: () => void;
+}
+
+export function useExternalPanelAddition({
+ isEditMode,
+ onEditButtonClick,
+}: UseExternalPanelAdditionOptions) {
+ const dispatch = useDispatch();
+ const addPersesPanelExternally: any = useSelector(
+ (s: any) => s.plugins?.mp?.dashboards?.addPersesPanelExternally,
+ );
+ const { openAddPanel } = useDashboardActions();
+ const dashboardStore = useDashboardStore();
+ const [externallyAddedPanel, setExternallyAddedPanel] = useState(null);
+
+ const addPanelExternally = (panelDefinition: any): void => {
+ // Simulate opening a panel to add the pane so that we can use it to programatically
+ // add a panel to the dashboard from an external source (AI assistant).
+ if (!isEditMode) {
+ onEditButtonClick();
+ }
+ openAddPanel();
+ // Wrap the panelDefinition with the groupId structure
+ const change = {
+ groupId: 0,
+ panelDefinition,
+ };
+ setExternallyAddedPanel(change);
+ };
+
+ useEffect(() => {
+ // Listen for external panel addition requests
+ if (addPersesPanelExternally) {
+ addPanelExternally(addPersesPanelExternally);
+ dispatch(dashboardsPersesPanelExternallyAdded());
+ }
+
+ // Apply externally added panel
+ if (externallyAddedPanel) {
+ const groupId = dashboardStore.panelGroupOrder[0];
+ externallyAddedPanel.groupId = groupId;
+
+ // Use the temporary panelEditor to add changes to the dashboard.
+ const panelEditor = dashboardStore.panelEditor;
+ panelEditor.applyChanges(externallyAddedPanel);
+ panelEditor.close();
+
+ // Clear the externally added panel after applying changes
+ setExternallyAddedPanel(null);
+ }
+ }, [externallyAddedPanel, addPersesPanelExternally]);
+
+ // Advertise when custom dashboard is opened/closed
+ useEffect(() => {
+ dispatch(dashboardsOpened(true));
+ return () => {
+ dispatch(dashboardsOpened(false));
+ };
+ }, [dispatch]);
+}
diff --git a/web/src/components/ols-tool-ui/AddToDashboardButton.tsx b/web/src/components/ols-tool-ui/AddToDashboardButton.tsx
new file mode 100644
index 000000000..12f5b335e
--- /dev/null
+++ b/web/src/components/ols-tool-ui/AddToDashboardButton.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import type { PanelDefinition } from '@perses-dev/core';
+import { Button } from '@patternfly/react-core';
+import { dashboardsAddPersesPanelExternally } from '../../store/actions';
+
+function createPanelDefinition(query: string): PanelDefinition {
+ return {
+ kind: 'Panel',
+ spec: {
+ display: {
+ name: '',
+ },
+ plugin: {
+ kind: 'TimeSeriesChart',
+ spec: {},
+ },
+ queries: [
+ {
+ kind: 'TimeSeriesQuery',
+ spec: {
+ plugin: {
+ kind: 'PrometheusTimeSeriesQuery',
+ spec: {
+ query: query,
+ },
+ },
+ },
+ },
+ ],
+ },
+ };
+}
+
+type AddToDashboardButtonProps = {
+ query: string;
+};
+
+export const AddToDashboardButton: React.FC = ({ query }) => {
+ const dispatch = useDispatch();
+
+ const isCustomDashboardOpen: boolean = useSelector(
+ (s: any) => s.plugins?.mp?.dashboards?.isOpened,
+ );
+
+ const addToPersesDashboard = React.useCallback(() => {
+ const panelDefinition = createPanelDefinition(query);
+ dispatch(dashboardsAddPersesPanelExternally(panelDefinition));
+ }, [query, dispatch]);
+
+ if (!isCustomDashboardOpen) {
+ return null;
+ }
+
+ return ;
+};
diff --git a/web/src/components/ols-tool-ui/ExecuteRangeQuery.tsx b/web/src/components/ols-tool-ui/ExecuteRangeQuery.tsx
new file mode 100644
index 000000000..eabd82f83
--- /dev/null
+++ b/web/src/components/ols-tool-ui/ExecuteRangeQuery.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { DataQueriesProvider } from '@perses-dev/plugin-system';
+import type { DurationString } from '@perses-dev/prometheus-plugin';
+import { Panel } from '@perses-dev/dashboards';
+
+import { OlsToolUIPersesWrapper } from './OlsToolUIPersesWrapper';
+import { AddToDashboardButton } from './AddToDashboardButton';
+
+type ExecuteRangeQueryTool = {
+ name: 'execute_range_query';
+ args: {
+ query: string;
+ };
+};
+
+const persesTimeRange = {
+ pastDuration: '1h' as DurationString,
+};
+
+export const ExecuteRangeQuery: React.FC<{ tool: ExecuteRangeQueryTool }> = ({ tool }) => {
+ const query = tool.args.query;
+ const definitions = [
+ {
+ kind: 'PrometheusTimeSeriesQuery',
+ spec: {
+ query: query,
+ },
+ },
+ ];
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+};
+
+export default ExecuteRangeQuery;
diff --git a/web/src/components/ols-tool-ui/OlsToolUIPersesWrapper.tsx b/web/src/components/ols-tool-ui/OlsToolUIPersesWrapper.tsx
new file mode 100644
index 000000000..eb4d2bd24
--- /dev/null
+++ b/web/src/components/ols-tool-ui/OlsToolUIPersesWrapper.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { VariableProvider } from '@perses-dev/dashboards';
+import { TimeRangeProviderBasic } from '@perses-dev/plugin-system';
+import type { DurationString } from '@perses-dev/prometheus-plugin';
+
+import {
+ PersesWrapper,
+ PersesPrometheusDatasourceWrapper,
+} from '../dashboards/perses/PersesWrapper';
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ refetchOnWindowFocus: false,
+ },
+ },
+});
+
+interface OlsToolUIPersesWrapperProps {
+ children: React.ReactNode;
+ height?: string;
+ initialTimeRange?: {
+ pastDuration: DurationString;
+ };
+}
+
+export const OlsToolUIPersesWrapper: React.FC = ({
+ children,
+ initialTimeRange = { pastDuration: '1h' as DurationString },
+ height = '300px',
+}) => {
+ return (
+
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+};
diff --git a/web/src/components/ols-tool-ui/index.ts b/web/src/components/ols-tool-ui/index.ts
new file mode 100644
index 000000000..eae5b93f4
--- /dev/null
+++ b/web/src/components/ols-tool-ui/index.ts
@@ -0,0 +1 @@
+export { ExecuteRangeQuery } from './ExecuteRangeQuery';
diff --git a/web/src/store/actions.ts b/web/src/store/actions.ts
index ef45c45cf..2169b1193 100644
--- a/web/src/store/actions.ts
+++ b/web/src/store/actions.ts
@@ -1,3 +1,4 @@
+import type { PanelDefinition } from '@perses-dev/core';
import { action, ActionType as Action } from 'typesafe-actions';
import { Alert, Rule, Silence } from '@openshift-console/dynamic-plugin-sdk';
@@ -17,6 +18,9 @@ export enum ActionType {
DashboardsSetPollInterval = 'v2/dashboardsSetPollInterval',
DashboardsSetTimespan = 'v2/dashboardsSetTimespan',
DashboardsVariableOptionsLoaded = 'v2/dashboardsVariableOptionsLoaded',
+ DashboardsOpened = 'dashboardsPersesDashboardsOpened',
+ DashboardsAddPersesPanelExternally = 'dashboardsAddPersesPanelExternally',
+ DashboardsPersesPanelExternallyAdded = 'dashboardsPersesPanelExternallyAdded',
QueryBrowserAddQuery = 'queryBrowserAddQuery',
QueryBrowserDuplicateQuery = 'queryBrowserDuplicateQuery',
QueryBrowserDeleteAllQueries = 'queryBrowserDeleteAllQueries',
@@ -68,6 +72,15 @@ export const dashboardsSetTimespan = (timespan: number) =>
export const dashboardsVariableOptionsLoaded = (key: string, newOptions: string[]) =>
action(ActionType.DashboardsVariableOptionsLoaded, { key, newOptions });
+export const dashboardsOpened = (isOpened: boolean) =>
+ action(ActionType.DashboardsOpened, { isOpened });
+
+export const dashboardsPersesPanelExternallyAdded = () =>
+ action(ActionType.DashboardsPersesPanelExternallyAdded, {});
+
+export const dashboardsAddPersesPanelExternally = (panelDefinition: PanelDefinition) =>
+ action(ActionType.DashboardsAddPersesPanelExternally, { panelDefinition });
+
export const alertingSetLoading = (datasource: string, identifier: string) =>
action(ActionType.AlertingSetLoading, {
datasource,
diff --git a/web/src/store/reducers.ts b/web/src/store/reducers.ts
index 852fd2dbe..fe9b7588e 100644
--- a/web/src/store/reducers.ts
+++ b/web/src/store/reducers.ts
@@ -81,6 +81,21 @@ const monitoringReducer = produce((draft: ObserveState, action: ObserveAction):
break;
}
+ case ActionType.DashboardsOpened: {
+ draft.dashboards.isOpened = action.payload.isOpened;
+ break;
+ }
+
+ case ActionType.DashboardsAddPersesPanelExternally: {
+ draft.dashboards.addPersesPanelExternally = action.payload.panelDefinition;
+ break;
+ }
+
+ case ActionType.DashboardsPersesPanelExternallyAdded: {
+ draft.dashboards.addPersesPanelExternally = null;
+ break;
+ }
+
case ActionType.AlertingSetRulesLoaded: {
const { datasource, identifier, rules, alerts } = action.payload;
diff --git a/web/src/store/store.ts b/web/src/store/store.ts
index 9dfd6040e..974a1f4d0 100644
--- a/web/src/store/store.ts
+++ b/web/src/store/store.ts
@@ -1,5 +1,6 @@
import * as _ from 'lodash-es';
+import type { PanelDefinition } from '@perses-dev/core';
import { MONITORING_DASHBOARDS_DEFAULT_TIMESPAN } from '../components/dashboards/legacy/utils';
import { Alert, PrometheusLabels, Rule } from '@openshift-console/dynamic-plugin-sdk';
import { Silences } from '../components/types';
@@ -27,6 +28,8 @@ export type ObserveState = {
pollInterval: number;
timespan: number;
variables: Record;
+ isOpened: boolean;
+ addPersesPanelExternally: PanelDefinition;
};
incidentsData: {
incidents: Array;