-
Notifications
You must be signed in to change notification settings - Fork 45
mcpToolUI basic implementation for OpenShift Lightpseed + obs-mcp + Perses #797
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n web/src/components/dashboards/perses/useExternalPanelAddition.tsRepository: openshift/monitoring-plugin Length of output: 2689 Move render-phase side effects into The hook implementation (lines 39–56 of 🤖 Prompt for AI Agents |
||
|
|
||
| const updateDashboardMutation = useUpdateDashboardMutation(); | ||
|
|
||
| const onSave = useCallback( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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]); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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: '', | ||||||||||||||
| }, | ||||||||||||||
|
Comment on lines
+11
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Externally added panels will be untitled. Line 12 sets ✅ Suggested fix display: {
- name: '',
+ name: query,
},📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| plugin: { | ||||||||||||||
| kind: 'TimeSeriesChart', | ||||||||||||||
| spec: {}, | ||||||||||||||
| }, | ||||||||||||||
| queries: [ | ||||||||||||||
| { | ||||||||||||||
| kind: 'TimeSeriesQuery', | ||||||||||||||
| spec: { | ||||||||||||||
| plugin: { | ||||||||||||||
| kind: 'PrometheusTimeSeriesQuery', | ||||||||||||||
| spec: { | ||||||||||||||
| query: query, | ||||||||||||||
| }, | ||||||||||||||
| }, | ||||||||||||||
| }, | ||||||||||||||
| }, | ||||||||||||||
| ], | ||||||||||||||
| }, | ||||||||||||||
| }; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| type AddToDashboardButtonProps = { | ||||||||||||||
| query: string; | ||||||||||||||
| }; | ||||||||||||||
|
|
||||||||||||||
| export const AddToDashboardButton: React.FC<AddToDashboardButtonProps> = ({ 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 <Button onClick={addToPersesDashboard}>Add to dashboard</Button>; | ||||||||||||||
| }; | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <> | ||
| <OlsToolUIPersesWrapper initialTimeRange={persesTimeRange}> | ||
| <DataQueriesProvider | ||
| definitions={definitions} | ||
| options={{ suggestedStepMs: 15000, mode: 'range' }} | ||
| > | ||
|
Comment on lines
+9
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 16-37 hardcodes range ( ✅ Suggested fix type ExecuteRangeQueryTool = {
name: 'execute_range_query';
args: {
query: string;
+ duration?: DurationString;
+ step?: string;
};
};
-const persesTimeRange = {
- pastDuration: '1h' as DurationString,
-};
-
export const ExecuteRangeQuery: React.FC<{ tool: ExecuteRangeQueryTool }> = ({ tool }) => {
const query = tool.args.query;
+ const pastDuration = tool.args.duration ?? ('1h' as DurationString);
+ const suggestedStepMs = tool.args.step ? parseInt(tool.args.step, 10) * 1000 : 15000;
@@
- <OlsToolUIPersesWrapper initialTimeRange={persesTimeRange}>
+ <OlsToolUIPersesWrapper initialTimeRange={{ pastDuration }}>
<DataQueriesProvider
definitions={definitions}
- options={{ suggestedStepMs: 15000, mode: 'range' }}
+ options={{ suggestedStepMs, mode: 'range' }}
>🤖 Prompt for AI Agents |
||
| <Panel | ||
| panelOptions={{ | ||
| hideHeader: false, | ||
| }} | ||
| definition={{ | ||
| kind: 'Panel', | ||
| spec: { | ||
| queries: [], | ||
| display: { name: query }, | ||
| plugin: { | ||
| kind: 'TimeSeriesChart', | ||
| spec: { | ||
| visual: { | ||
| stack: 'all', | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }} | ||
| /> | ||
| </DataQueriesProvider> | ||
| </OlsToolUIPersesWrapper> | ||
| <AddToDashboardButton query={query} /> | ||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| export default ExecuteRangeQuery; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<OlsToolUIPersesWrapperProps> = ({ | ||
| children, | ||
| initialTimeRange = { pastDuration: '1h' as DurationString }, | ||
| height = '300px', | ||
| }) => { | ||
| return ( | ||
| <QueryClientProvider client={queryClient}> | ||
| <PersesWrapper project={null}> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n web/src/components/ols-tool-ui/OlsToolUIPersesWrapper.tsx | head -50Repository: openshift/monitoring-plugin Length of output: 1789 🏁 Script executed: # Find PersesWrapper definition
rg -l "PersesWrapper" --type ts --type tsx | head -10Repository: openshift/monitoring-plugin Length of output: 98 🏁 Script executed: # Search for PersesWrapperProps type definition
rg "PersesWrapperProps" --type ts --type tsx -A 5Repository: openshift/monitoring-plugin Length of output: 98 🏁 Script executed: find web/src/components/dashboards/perses -name "*.tsx" -o -name "*.ts" | xargs cat -nRepository: openshift/monitoring-plugin Length of output: 50384 🏁 Script executed: # Try broader search with different approach
rg "PersesWrapper" -A 20 --max-count 5Repository: openshift/monitoring-plugin Length of output: 14616 Update The Change the interface definition instead of the usage: interface PersesWrapperProps {
children?: React.ReactNode;
- project: string;
+ project: string | null;
}🤖 Prompt for AI Agents |
||
| <TimeRangeProviderBasic initialTimeRange={initialTimeRange}> | ||
| <VariableProvider> | ||
| <PersesPrometheusDatasourceWrapper queries={[]}> | ||
| <div style={{ width: '100%', height: height }}>{children}</div> | ||
| </PersesPrometheusDatasourceWrapper> | ||
| </VariableProvider> | ||
| </TimeRangeProviderBasic> | ||
| </PersesWrapper> | ||
| </QueryClientProvider> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { ExecuteRangeQuery } from './ExecuteRangeQuery'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 }); | ||
|
|
||
|
Comment on lines
+75
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cd web/src/store && wc -l actions.tsRepository: openshift/monitoring-plugin Length of output: 85 🏁 Script executed: cd web/src/store && head -100 actions.ts | tail -30Repository: openshift/monitoring-plugin Length of output: 1135 🏁 Script executed: cd web/src/store && sed -n '70,90p' actions.tsRepository: openshift/monitoring-plugin Length of output: 894 🏁 Script executed: cd web/src/store && grep -n "type Actions\|type ObserveAction" actions.ts | head -5Repository: openshift/monitoring-plugin Length of output: 140 🏁 Script executed: cd web/src/store && sed -n '210,240p' actions.tsRepository: openshift/monitoring-plugin Length of output: 1909 🏁 Script executed: cd web/src/store && sed -n '210,254p' actions.tsRepository: openshift/monitoring-plugin Length of output: 2523
Lines 75-83 introduce Add the three missing entries to the
🤖 Prompt for AI Agents |
||
| export const alertingSetLoading = (datasource: string, identifier: string) => | ||
| action(ActionType.AlertingSetLoading, { | ||
| datasource, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<string, Variable>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| isOpened: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| addPersesPanelExternally: PanelDefinition; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+31
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: rg -n -C2 'isOpened|addPersesPanelExternally' web/src/store/store.ts web/src/store/reducers.ts web/src/store/actions.tsRepository: openshift/monitoring-plugin Length of output: 1650 🏁 Script executed: rg -n -B5 -A15 'export const defaultObserveState' web/src/store/store.tsRepository: openshift/monitoring-plugin Length of output: 703 State type is inconsistent with reducer behavior and defaults.
🐛 Proposed fix export type ObserveState = {
dashboards: {
endTime: number | null;
pollInterval: number;
timespan: number;
variables: Record<string, Variable>;
isOpened: boolean;
- addPersesPanelExternally: PanelDefinition;
+ addPersesPanelExternally: PanelDefinition | null;
};
@@
export const defaultObserveState: ObserveState = {
dashboards: {
endTime: null,
pollInterval: 30 * 1000,
timespan: MONITORING_DASHBOARDS_DEFAULT_TIMESPAN,
variables: {},
+ isOpened: false,
+ addPersesPanelExternally: null,
},📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| incidentsData: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| incidents: Array<any>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's check the current state of the file at the mentioned lines wc -l web/src/components/dashboards/perses/PersesWrapper.tsxRepository: openshift/monitoring-plugin
Length of output: 129
🏁 Script executed:
Repository: openshift/monitoring-plugin
Length of output: 241
🏁 Script executed:
Repository: openshift/monitoring-plugin
Length of output: 535
🏁 Script executed:
Repository: openshift/monitoring-plugin
Length of output: 241
🏁 Script executed:
Repository: openshift/monitoring-plugin
Length of output: 114
🏁 Script executed:
Repository: openshift/monitoring-plugin
Length of output: 660
🏁 Script executed:
Repository: openshift/monitoring-plugin
Length of output: 226
🏁 Script executed:
Repository: openshift/monitoring-plugin
Length of output: 1858
Consider typing
InnerWrapperprops for better code clarity and maintainability.Line 381 destructures untyped props. While the current TypeScript configuration (
strict: false) allows implicitanytypes, adding explicit types improves clarity and protects against future stricter TS settings.🔧 Suggested improvement
📝 Committable suggestion
🤖 Prompt for AI Agents