From e54a69ac94b8c77fc3d6f2391c915c1d4ed34e91 Mon Sep 17 00:00:00 2001 From: Ivan Necas Date: Mon, 2 Mar 2026 14:09:03 +0100 Subject: [PATCH 1/8] OU-1264: 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. --- config/perses-dashboards.patch.json | 13 ++++ web/package.json | 3 +- .../dashboards/perses/PersesWrapper.tsx | 12 +--- .../dashboards/perses/dashboard-app.tsx | 4 ++ .../perses/useExternalPanelAddition.ts | 67 +++++++++++++++++++ .../ols-tool-ui/AddToDashboardButton.tsx | 56 ++++++++++++++++ .../ols-tool-ui/ExecuteRangeQuery.tsx | 65 ++++++++++++++++++ .../ols-tool-ui/OlsToolUIPersesWrapper.tsx | 47 +++++++++++++ web/src/components/ols-tool-ui/index.ts | 1 + web/src/store/actions.ts | 13 ++++ web/src/store/reducers.ts | 15 +++++ web/src/store/store.ts | 3 + 12 files changed, 289 insertions(+), 10 deletions(-) create mode 100644 web/src/components/dashboards/perses/useExternalPanelAddition.ts create mode 100644 web/src/components/ols-tool-ui/AddToDashboardButton.tsx create mode 100644 web/src/components/ols-tool-ui/ExecuteRangeQuery.tsx create mode 100644 web/src/components/ols-tool-ui/OlsToolUIPersesWrapper.tsx create mode 100644 web/src/components/ols-tool-ui/index.ts 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; From c285543730b179eab0168ed1e49f02f3498a8b6f Mon Sep 17 00:00:00 2001 From: Ivan Necas Date: Wed, 4 Mar 2026 14:22:54 +0100 Subject: [PATCH 2/8] OU-1264: Use query for the dynamic panel name --- web/src/components/ols-tool-ui/AddToDashboardButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/ols-tool-ui/AddToDashboardButton.tsx b/web/src/components/ols-tool-ui/AddToDashboardButton.tsx index 12f5b335e..1ff56d2a8 100644 --- a/web/src/components/ols-tool-ui/AddToDashboardButton.tsx +++ b/web/src/components/ols-tool-ui/AddToDashboardButton.tsx @@ -9,7 +9,7 @@ function createPanelDefinition(query: string): PanelDefinition { kind: 'Panel', spec: { display: { - name: '', + name: query, }, plugin: { kind: 'TimeSeriesChart', From bf75681cc54c799a3c96eec003a72394e2889882 Mon Sep 17 00:00:00 2001 From: Ivan Necas Date: Fri, 20 Mar 2026 18:03:08 +0100 Subject: [PATCH 3/8] OU-1264: use show-timeseries tool for vis --- config/perses-dashboards.patch.json | 4 +- .../ols-tool-ui/AddToDashboardButton.tsx | 56 ------------ ...ecuteRangeQuery.tsx => ShowTimeseries.tsx} | 26 +++--- .../helpers/AddToDashboardButton.tsx | 89 +++++++++++++++++++ .../{ => helpers}/OlsToolUIPersesWrapper.tsx | 2 +- web/src/components/ols-tool-ui/index.ts | 2 +- 6 files changed, 109 insertions(+), 70 deletions(-) delete mode 100644 web/src/components/ols-tool-ui/AddToDashboardButton.tsx rename web/src/components/ols-tool-ui/{ExecuteRangeQuery.tsx => ShowTimeseries.tsx} (58%) create mode 100644 web/src/components/ols-tool-ui/helpers/AddToDashboardButton.tsx rename web/src/components/ols-tool-ui/{ => helpers}/OlsToolUIPersesWrapper.tsx (96%) diff --git a/config/perses-dashboards.patch.json b/config/perses-dashboards.patch.json index a93f9dacf..79d650595 100644 --- a/config/perses-dashboards.patch.json +++ b/config/perses-dashboards.patch.json @@ -126,9 +126,9 @@ "value": { "type": "ols.tool-ui", "properties": { - "id": "mcp-obs/execute-range-query", + "id": "mcp-obs/show-timeseries", "component": { - "$codeRef": "ols-tool-ui.ExecuteRangeQuery" + "$codeRef": "ols-tool-ui.ShowTimeseries" } } } diff --git a/web/src/components/ols-tool-ui/AddToDashboardButton.tsx b/web/src/components/ols-tool-ui/AddToDashboardButton.tsx deleted file mode 100644 index 1ff56d2a8..000000000 --- a/web/src/components/ols-tool-ui/AddToDashboardButton.tsx +++ /dev/null @@ -1,56 +0,0 @@ -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: query, - }, - 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/ShowTimeseries.tsx similarity index 58% rename from web/src/components/ols-tool-ui/ExecuteRangeQuery.tsx rename to web/src/components/ols-tool-ui/ShowTimeseries.tsx index eabd82f83..3b8de4e35 100644 --- a/web/src/components/ols-tool-ui/ExecuteRangeQuery.tsx +++ b/web/src/components/ols-tool-ui/ShowTimeseries.tsx @@ -1,14 +1,16 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; 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'; +import { OlsToolUIPersesWrapper } from './helpers/OlsToolUIPersesWrapper'; +import { AddToDashboardButton } from './helpers/AddToDashboardButton'; type ExecuteRangeQueryTool = { - name: 'execute_range_query'; args: { + title: string; + description: string; query: string; }; }; @@ -17,8 +19,10 @@ const persesTimeRange = { pastDuration: '1h' as DurationString, }; -export const ExecuteRangeQuery: React.FC<{ tool: ExecuteRangeQueryTool }> = ({ tool }) => { - const query = tool.args.query; +export const ShowTimeseries: React.FC<{ tool: ExecuteRangeQueryTool }> = ({ tool }) => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + const { query, title, description } = tool.args; + const queryDescription = t('Query: {{query}}', { query: query }); const definitions = [ { kind: 'PrometheusTimeSeriesQuery', @@ -38,17 +42,20 @@ export const ExecuteRangeQuery: React.FC<{ tool: ExecuteRangeQueryTool }> = ({ t ( + + ), }} definition={{ kind: 'Panel', spec: { queries: [], - display: { name: query }, + display: { name: title, description: `${description}\n\n${queryDescription}` }, plugin: { kind: 'TimeSeriesChart', spec: { - visual: { - stack: 'all', + legend: { + position: 'bottom', }, }, }, @@ -57,9 +64,8 @@ export const ExecuteRangeQuery: React.FC<{ tool: ExecuteRangeQueryTool }> = ({ t /> - ); }; -export default ExecuteRangeQuery; +export default ShowTimeseries; diff --git a/web/src/components/ols-tool-ui/helpers/AddToDashboardButton.tsx b/web/src/components/ols-tool-ui/helpers/AddToDashboardButton.tsx new file mode 100644 index 000000000..807663acb --- /dev/null +++ b/web/src/components/ols-tool-ui/helpers/AddToDashboardButton.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import { IconButton, IconButtonProps, styled } from '@mui/material'; +import { Theme } from '@mui/material/styles'; +import ViewGridPlusIcon from 'mdi-material-ui/ViewGridPlus'; +import { StyledComponent } from '@emotion/styled'; + +import type { PanelDefinition } from '@perses-dev/core'; +import { InfoTooltip } from '@perses-dev/components'; + +import { dashboardsAddPersesPanelExternally } from '../../../store/actions'; + +export const HeaderIconButton: StyledComponent = styled( + IconButton, +)(({ theme }) => ({ + borderRadius: theme.shape.borderRadius, + padding: '4px', +})); + +function createPanelDefinition(query: string, name: string, description: string): PanelDefinition { + return { + kind: 'Panel', + spec: { + display: { + name: name, + description: description, + }, + plugin: { + kind: 'TimeSeriesChart', + spec: {}, + }, + queries: [ + { + kind: 'TimeSeriesQuery', + spec: { + plugin: { + kind: 'PrometheusTimeSeriesQuery', + spec: { + query: query, + }, + }, + }, + }, + ], + }, + }; +} + +type AddToDashboardButtonProps = { + query: string; + name?: string; + description?: string; +}; + +export const AddToDashboardButton: React.FC = ({ + query, + name, + description, +}) => { + const dispatch = useDispatch(); + const { t } = useTranslation(process.env.I18N_NAMESPACE); + const isCustomDashboardOpen: boolean = useSelector( + (s: any) => s.plugins?.mp?.dashboards?.isOpened, + ); + const addToPersesDashboard = React.useCallback(() => { + const panelDefinition = createPanelDefinition(query, name, description); + dispatch(dashboardsAddPersesPanelExternally(panelDefinition)); + }, [query, name, description, dispatch]); + + if (!isCustomDashboardOpen) { + // No dashboard is opened - nothing to add to. + return null; + } + + return ( + + + theme.palette.text.secondary }} + onClick={addToPersesDashboard} + /> + + + ); +}; diff --git a/web/src/components/ols-tool-ui/OlsToolUIPersesWrapper.tsx b/web/src/components/ols-tool-ui/helpers/OlsToolUIPersesWrapper.tsx similarity index 96% rename from web/src/components/ols-tool-ui/OlsToolUIPersesWrapper.tsx rename to web/src/components/ols-tool-ui/helpers/OlsToolUIPersesWrapper.tsx index eb4d2bd24..e3d7cd483 100644 --- a/web/src/components/ols-tool-ui/OlsToolUIPersesWrapper.tsx +++ b/web/src/components/ols-tool-ui/helpers/OlsToolUIPersesWrapper.tsx @@ -7,7 +7,7 @@ import type { DurationString } from '@perses-dev/prometheus-plugin'; import { PersesWrapper, PersesPrometheusDatasourceWrapper, -} from '../dashboards/perses/PersesWrapper'; +} from '../../dashboards/perses/PersesWrapper'; const queryClient = new QueryClient({ defaultOptions: { diff --git a/web/src/components/ols-tool-ui/index.ts b/web/src/components/ols-tool-ui/index.ts index eae5b93f4..27dc949d5 100644 --- a/web/src/components/ols-tool-ui/index.ts +++ b/web/src/components/ols-tool-ui/index.ts @@ -1 +1 @@ -export { ExecuteRangeQuery } from './ExecuteRangeQuery'; +export { ShowTimeseries } from './ShowTimeseries'; From 08bda3ddfba337469ec50776e1575c7a7bf6b5c5 Mon Sep 17 00:00:00 2001 From: Ivan Necas Date: Thu, 26 Mar 2026 11:08:23 +0100 Subject: [PATCH 4/8] fix lint --- .../perses/useExternalPanelAddition.ts | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/web/src/components/dashboards/perses/useExternalPanelAddition.ts b/web/src/components/dashboards/perses/useExternalPanelAddition.ts index 715bda792..81b4e66fa 100644 --- a/web/src/components/dashboards/perses/useExternalPanelAddition.ts +++ b/web/src/components/dashboards/perses/useExternalPanelAddition.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useDashboardActions, useDashboardStore } from '@perses-dev/dashboards'; import { dashboardsOpened, dashboardsPersesPanelExternallyAdded } from '../../../store/actions'; @@ -20,20 +20,23 @@ export function useExternalPanelAddition({ 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); - }; + const addPanelExternally = useCallback( + (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); + }, + [isEditMode, onEditButtonClick, openAddPanel], + ); useEffect(() => { // Listen for external panel addition requests @@ -55,7 +58,14 @@ export function useExternalPanelAddition({ // Clear the externally added panel after applying changes setExternallyAddedPanel(null); } - }, [externallyAddedPanel, addPersesPanelExternally]); + }, [ + dispatch, + dashboardStore.panelGroupOrder, + dashboardStore.panelEditor, + externallyAddedPanel, + addPanelExternally, + addPersesPanelExternally, + ]); // Advertise when custom dashboard is opened/closed useEffect(() => { From 864b8d1533556783e882dd45808377e52c145faa Mon Sep 17 00:00:00 2001 From: Ivan Necas Date: Thu, 26 Mar 2026 11:01:52 +0100 Subject: [PATCH 5/8] Update i18n --- web/locales/en/plugin__monitoring-plugin.json | 59 ++++++++++++++++--- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/web/locales/en/plugin__monitoring-plugin.json b/web/locales/en/plugin__monitoring-plugin.json index de00f6f98..4356b4e90 100644 --- a/web/locales/en/plugin__monitoring-plugin.json +++ b/web/locales/en/plugin__monitoring-plugin.json @@ -166,21 +166,63 @@ "Time range": "Time range", "Refresh interval": "Refresh interval", "Could not parse JSON data for dashboard \"{{dashboard}}\"": "Could not parse JSON data for dashboard \"{{dashboard}}\"", - "Dashboard Variables": "Dashboard Variables", + "Rename Dashboard": "Rename Dashboard", + "Dashboard name": "Dashboard name", + "Renaming...": "Renaming...", + "Rename": "Rename", + "Select project": "Select project", + "Loading...": "Loading...", + "Select namespace": "Select namespace", + "Duplicate": "Duplicate", + "this dashboard": "this dashboard", + "Permanently delete dashboard?": "Permanently delete dashboard?", + "Are you sure you want to delete ": "Are you sure you want to delete ", + "? This action can not be undone.": "? This action can not be undone.", + "Deleting...": "Deleting...", + "Delete": "Delete", + "Must be 75 or fewer characters long": "Must be 75 or fewer characters long", + "Dashboard name '{{dashboardName}}' already exists in '{{projectName}}' project!": "Dashboard name '{{dashboardName}}' already exists in '{{projectName}}' project!", + "Project is required": "Project is required", + "Dashboard name is required": "Dashboard name is required", + "Failed to create dashboard. Please try again.": "Failed to create dashboard. Please try again.", + "Create": "Create", + "Create Dashboard": "Create Dashboard", + "my-new-dashboard": "my-new-dashboard", + "Creating...": "Creating...", + "View and manage dashboards.": "View and manage dashboards.", + "Rename dashboard": "Rename dashboard", + "Duplicate dashboard": "Duplicate dashboard", + "Delete dashboard": "Delete dashboard", + "You don't have permissions to dashboard actions": "You don't have permissions to dashboard actions", + "Dashboard": "Dashboard", + "Project": "Project", + "Created on": "Created on", + "Last Modified": "Last Modified", + "Filter by name": "Filter by name", + "Filter by project": "Filter by project", + "No dashboards found": "No dashboards found", + "No results match the filter criteria. Clear filters to show results.": "No results match the filter criteria. Clear filters to show results.", + "No Perses dashboards are currently available in this project.": "No Perses dashboards are currently available in this project.", + "Clear all filters": "Clear all filters", + "Dashboard not found": "Dashboard not found", + "The dashboard \"{{name}}\" was not found in project \"{{project}}\".": "The dashboard \"{{name}}\" was not found in project \"{{project}}\".", + "Empty Dashboard": "Empty Dashboard", + "To get started add something to your dashboard": "To get started add something to your dashboard", + "Edit": "Edit", + "You don't have permission to edit this dashboard": "You don't have permission to edit this dashboard", "No matching datasource found": "No matching datasource found", "No Dashboard Available in Selected Project": "No Dashboard Available in Selected Project", "To explore data, create a dashboard for this project": "To explore data, create a dashboard for this project", "No Perses Project Available": "No Perses Project Available", "To explore data, create a Perses Project": "To explore data, create a Perses Project", - "Empty Dashboard": "Empty Dashboard", - "To get started add something to your dashboard": "To get started add something to your dashboard", + "Project is required for fetching project dashboards": "Project is required for fetching project dashboards", "No projects found": "No projects found", "No results match the filter criteria.": "No results match the filter criteria.", "Clear filters": "Clear filters", "Select project...": "Select project...", "Projects": "Projects", - "Project": "Project", - "Dashboard": "Dashboard", + "All Projects": "All Projects", + "useToast must be used within ToastProvider": "useToast must be used within ToastProvider", "Refresh off": "Refresh off", "{{count}} second_one": "{{count}} second", "{{count}} second_other": "{{count}} seconds", @@ -203,7 +245,7 @@ "Component(s)": "Component(s)", "Alert": "Alert", "Incidents": "Incidents", - "Clear all filters": "Clear all filters", + "Incident data is updated every few minutes. What you see may be up to 5 minutes old. Refresh the page to view updated information.": "Incident data is updated every few minutes. What you see may be up to 5 minutes old. Refresh the page to view updated information.", "Filter type selection": "Filter type selection", "Incident ID": "Incident ID", "Severity filters": "Severity filters", @@ -264,6 +306,8 @@ "No Units": "No Units", "Metrics": "Metrics", "This dropdown only formats results.": "This dropdown only formats results.", + "Add To Dashboard": "Add To Dashboard", + "Query: {{query}}": "Query: {{query}}", "graph timespan": "graph timespan", "Reset zoom": "Reset zoom", "Displaying with reduced resolution due to large dataset.": "Displaying with reduced resolution due to large dataset.", @@ -303,6 +347,5 @@ "No metrics targets found": "No metrics targets found", "Error loading latest targets data": "Error loading latest targets data", "Search by endpoint or namespace...": "Search by endpoint or namespace...", - "Text": "Text", - "Incident data is updated every few minutes. What you see may be up to 5 minutes old. Refresh the page to view updated information.":"Incident data is updated every few minutes. What you see may be up to 5 minutes old. Refresh the page to view updated information." + "Text": "Text" } \ No newline at end of file From 0d9e9f517085c7f2d8ddcf0eb2d0b70a2a3e630c Mon Sep 17 00:00:00 2001 From: Ivan Necas Date: Fri, 27 Mar 2026 10:29:35 +0100 Subject: [PATCH 6/8] OU-1264 - setup cypress component infra testing --- AGENTS.md | 48 ++++++++++++++++ CONTRIBUTING.md | 17 ++++++ web/cypress.config.ts | 45 +++++++++++++++ web/cypress/CYPRESS_TESTING_GUIDE.md | 72 ++++++++++++++++++++++-- web/cypress/README.md | 18 +++++- web/cypress/component/labels.cy.tsx | 54 ++++++++++++++++++ web/cypress/support/component-index.html | 10 ++++ web/cypress/support/component.ts | 20 +++++++ web/package.json | 2 + 9 files changed, 279 insertions(+), 7 deletions(-) create mode 100644 web/cypress/component/labels.cy.tsx create mode 100644 web/cypress/support/component-index.html create mode 100644 web/cypress/support/component.ts diff --git a/AGENTS.md b/AGENTS.md index 1d09a7e48..16917c5bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -283,6 +283,54 @@ npm run cypress:open For detailed testing instructions, see `web/cypress/CYPRESS_TESTING_GUIDE.md` +### Cypress Component Testing + +#### Overview + +Cypress component tests mount individual React components in isolation, without requiring a running OpenShift cluster. They are useful for testing component rendering, user interactions, and visual behavior with fast feedback. + +- **Test location**: `web/cypress/component/` +- **Support file**: `web/cypress/support/component.ts` +- **Config**: `component` section in `web/cypress.config.ts` + +#### When to Create Component Tests + +- Testing a component's rendering logic (conditional display, empty states) +- Verifying props are handled correctly +- Validating user interactions within a single component +- When E2E tests would be overkill for the behavior under test + +#### Quick Test Commands + +```bash +cd web + +# Interactive mode +npm run cypress:open:component + +# Headless mode - all component tests +npm run cypress:run:component + +# Run a single component test file +npx cypress run --component --spec cypress/component/labels.cy.tsx +``` + +#### Writing a Component Test + +Component test files use the `.cy.tsx` extension and go in `web/cypress/component/`: + +```typescript +import React from 'react'; +import { MyComponent } from '../../src/components/MyComponent'; + +describe('MyComponent', () => { + it('renders correctly', () => { + cy.mount(); + cy.contains('expected text').should('be.visible'); + }); +}); +``` + ### Release Pipeline: - **Konflux**: Handles CI/CD and release automation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2e9d26d38..f74d2cf23 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -411,6 +411,23 @@ cd web/cypress npm run cypress:run --spec "cypress/e2e/**/regression/**" ``` +### Component Tests (Cypress) + +For testing individual React components in isolation (no cluster required): + +- Test files: `web/cypress/component/` (`.cy.tsx` extension) +- Support file: `web/cypress/support/component.ts` + +```bash +cd web + +# Interactive mode +npm run cypress:open:component + +# Headless mode +npm run cypress:run:component +``` + --- ## Internationalization (i18n) diff --git a/web/cypress.config.ts b/web/cypress.config.ts index 6c6a516b5..8a823ddd8 100644 --- a/web/cypress.config.ts +++ b/web/cypress.config.ts @@ -3,6 +3,7 @@ import * as fs from 'fs-extra'; import * as console from 'console'; import * as path from 'path'; import registerCypressGrep from '@cypress/grep/src/plugin'; +import { DefinePlugin } from 'webpack'; export default defineConfig({ screenshotsFolder: './cypress/screenshots', @@ -159,4 +160,48 @@ export default defineConfig({ experimentalMemoryManagement: true, experimentalStudio: true, }, + component: { + devServer: { + framework: 'react', + bundler: 'webpack', + webpackConfig: { + resolve: { + extensions: ['.ts', '.tsx', '.js', '.jsx'], + }, + module: { + rules: [ + { + test: /\.(jsx?|tsx?)$/, + exclude: /node_modules/, + use: { loader: 'swc-loader' }, + }, + { + test: /\.scss$/, + exclude: /node_modules\/(?!(@patternfly|@openshift-console\/plugin-shared)\/).*/, + use: ['style-loader', 'css-loader', 'sass-loader'], + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }, + { + test: /\.(png|jpg|jpeg|gif|svg|woff2?|ttf|eot|otf)(\?.*$|$)/, + type: 'asset/resource', + }, + { + test: /\.m?js/, + resolve: { fullySpecified: false }, + }, + ], + }, + plugins: [ + new DefinePlugin({ + 'process.env.I18N_NAMESPACE': JSON.stringify('plugin__monitoring-plugin'), + }), + ], + }, + }, + specPattern: './cypress/component/**/*.cy.{js,jsx,ts,tsx}', + supportFile: './cypress/support/component.ts', + }, }); \ No newline at end of file diff --git a/web/cypress/CYPRESS_TESTING_GUIDE.md b/web/cypress/CYPRESS_TESTING_GUIDE.md index 697caf6f5..071d03f80 100644 --- a/web/cypress/CYPRESS_TESTING_GUIDE.md +++ b/web/cypress/CYPRESS_TESTING_GUIDE.md @@ -67,6 +67,7 @@ The Monitoring Plugin uses a 3-layer architecture for test organization: ``` cypress/ +├── component/ # Component tests (isolated, no cluster needed) ├── e2e/ │ ├── monitoring/ # Core monitoring tests (Administrator) │ │ ├── 00.bvt_admin.cy.ts @@ -81,7 +82,9 @@ cypress/ │ │ ├── 02.reg_metrics.cy.ts │ │ └── 03.reg_legacy_dashboards.cy.ts │ ├── perses/ # COO/Perses scenarios -│ └── commands/ # Custom Cypress commands +│ ├── commands/ # Custom Cypress commands +│ ├── component.ts # Component test support (mount command) +│ └── component-index.html # HTML template for component mounting └── views/ # Page object models (reusable actions) ``` @@ -92,7 +95,65 @@ cypress/ --- -## Creating Tests +## Component Testing + +Component tests mount individual React components in isolation using Cypress, without requiring a running OpenShift cluster. They provide fast feedback for rendering logic, props handling, and interactions. + +### When to Use Component Tests vs E2E Tests + +| Use Component Tests When | Use E2E Tests When | +|---|---| +| Testing rendering and visual output | Testing full user workflows | +| Verifying props and conditional display | Testing navigation between pages | +| Validating empty/error states | Testing API integration | +| Fast feedback during development | Testing cross-component interactions | + +### Writing Component Tests + +Component test files use the `.cy.tsx` extension and live in `cypress/component/`: + +```typescript +import React from 'react'; +import { Labels } from '../../src/components/labels'; + +describe('Labels', () => { + it('renders "No labels" when labels is empty', () => { + cy.mount(); + cy.contains('No labels').should('be.visible'); + }); + + it('renders a single label', () => { + cy.mount(); + cy.contains('app').should('be.visible'); + cy.contains('monitoring').should('be.visible'); + }); +}); +``` + +### Running Component Tests + +```bash +cd web + +# Interactive mode (GUI) - best for development +npm run cypress:open:component + +# Headless mode - best for CI +npm run cypress:run:component + +# Run a single component test file +npx cypress run --component --spec cypress/component/labels.cy.tsx +``` + +### Key Differences from E2E Tests + +- **No cluster required**: Components are mounted directly in the browser +- **Custom mount command**: Use `cy.mount()` instead of `cy.visit()` +- **Support file**: Uses `cypress/support/component.ts` (not `cypress/support/index.ts`) + +--- + +## Creating E2E Tests ### Workflow @@ -133,11 +194,12 @@ export const runAlertTests = (perspective: string) => { | Scenario | Action | |----------|--------| -| New UI feature | Create new test scenario in support/ | +| New UI feature | Create new E2E test scenario in support/ | | Bug fix | Add test case to existing support file | | Component update | Update existing test scenarios | -| New Perses feature | Create new test scenario in support/ | -| ACM integration | Add test in e2e/coo/ | +| New Perses feature | Create new E2E test scenario in support/ | +| ACM integration | Add E2E test in e2e/coo/ | +| Isolated component logic | Add component test in component/ | ### Best Practices diff --git a/web/cypress/README.md b/web/cypress/README.md index 9e0c0fc87..2c0e5e4a6 100644 --- a/web/cypress/README.md +++ b/web/cypress/README.md @@ -407,20 +407,34 @@ export CYPRESS_SESSION=true --- +## Component Testing + +Cypress component tests mount individual React components in isolation, without a running cluster. They provide fast feedback for testing rendering logic, props handling, and user interactions. See **[CYPRESS_TESTING_GUIDE.md](CYPRESS_TESTING_GUIDE.md)** for more guidance on how +to write and run the tests. + +### Configuration + +Component testing is configured in the `component` section of `web/cypress.config.ts`. It uses a standalone webpack config with `swc-loader` and a custom `mount` command (compatible with React 17) defined in `cypress/support/component.ts`. + +--- + ## Test Organization ### Directory Structure ``` cypress/ -├── e2e/ # Test files by perspective +├── component/ # Component test files (.cy.tsx) +├── e2e/ # E2E test files by perspective │ ├── monitoring/ # Core monitoring (Administrator) │ ├── coo/ # COO-specific tests │ └── virtualization/ # Virtualization integration ├── support/ # Reusable test scenarios │ ├── monitoring/ # Test scenario modules │ ├── perses/ # Perses scenarios -│ └── commands/ # Custom Cypress commands +│ ├── commands/ # Custom Cypress commands +│ ├── component.ts # Component test support +│ └── component-index.html # Component test HTML template ├── views/ # Page object models ├── fixtures/ # Test data and mocks └── E2E_TEST_SCENARIOS.md # Complete test catalog diff --git a/web/cypress/component/labels.cy.tsx b/web/cypress/component/labels.cy.tsx new file mode 100644 index 000000000..f81b8914e --- /dev/null +++ b/web/cypress/component/labels.cy.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Labels } from '../../src/components/labels'; + +// Mock react-i18next to return the key as-is +const mockT = (key: string) => key; +const mockUseTranslation = () => ({ t: mockT }); + +// Stub the module before import +beforeEach(() => { + cy.stub(require('react-i18next'), 'useTranslation').returns(mockUseTranslation()); +}); + +describe('Labels', () => { + it('renders "No labels" when labels is empty', () => { + cy.mount(); + cy.contains('No labels').should('be.visible'); + }); + + it('renders "No labels" when labels is undefined', () => { + cy.mount(); + cy.contains('No labels').should('be.visible'); + }); + + it('renders a single label', () => { + cy.mount(); + cy.contains('app').should('be.visible'); + cy.contains('monitoring').should('be.visible'); + }); + + it('renders multiple labels', () => { + const labels = { + app: 'monitoring', + env: 'production', + team: 'platform', + }; + cy.mount(); + + cy.contains('app').should('be.visible'); + cy.contains('monitoring').should('be.visible'); + cy.contains('env').should('be.visible'); + cy.contains('production').should('be.visible'); + cy.contains('team').should('be.visible'); + cy.contains('platform').should('be.visible'); + }); + + it('renders label with key=value format', () => { + cy.mount(); + cy.get('.pf-v6-c-label').within(() => { + cy.contains('severity'); + cy.contains('='); + cy.contains('critical'); + }); + }); +}); diff --git a/web/cypress/support/component-index.html b/web/cypress/support/component-index.html new file mode 100644 index 000000000..538cb8013 --- /dev/null +++ b/web/cypress/support/component-index.html @@ -0,0 +1,10 @@ + + + + + + + +
+ + diff --git a/web/cypress/support/component.ts b/web/cypress/support/component.ts new file mode 100644 index 000000000..05760ee9b --- /dev/null +++ b/web/cypress/support/component.ts @@ -0,0 +1,20 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import '@patternfly/react-core/dist/styles/base.css'; + +function mount(jsx: React.ReactElement) { + const root = document.querySelector('[data-cy-root]'); + return cy.then(() => { + ReactDOM.render(jsx, root); + }); +} + +declare global { + namespace Cypress { + interface Chainable { + mount: typeof mount; + } + } +} + +Cypress.Commands.add('mount', mount); diff --git a/web/package.json b/web/package.json index 7778f8cab..de38716df 100644 --- a/web/package.json +++ b/web/package.json @@ -9,7 +9,9 @@ "build:standalone": "npm run clean && npm run ts-node node_modules/.bin/webpack --config webpack.standalone.config.ts", "clean": "rm -rf dist", "cypress:open": "cypress open", + "cypress:open:component": "cypress open --component", "cypress:run": "cypress run", + "cypress:run:component": "cypress run --component", "cypress:run:ci": "NO_COLOR=1 npx cypress run --browser chrome", "delete:reports": "rm -rf gui_test_screenshots/* || true", "i18n": "i18n-scripts/build-i18n.sh && node i18n-scripts/set-english-defaults.js", From 771cf7925d9d40b7fe1083c2f0f69f627ca708be Mon Sep 17 00:00:00 2001 From: Ivan Necas Date: Fri, 27 Mar 2026 11:30:00 +0100 Subject: [PATCH 7/8] OU-1264: component tests for ols-tool-ui/ShowTimeseries --- web/cypress.config.ts | 15 +++++++- web/cypress/component/labels.cy.tsx | 10 ----- .../component/mocks/AddToDashboardButton.tsx | 8 ++++ .../mocks/OlsToolUIPersesWrapper.tsx | 5 +++ .../component/mocks/perses-dashboards.tsx | 10 +++++ .../component/mocks/perses-plugin-system.tsx | 2 + .../mocks/perses-prometheus-plugin.ts | 2 + .../ols-tool-ui/ShowTimeseries.cy.tsx | 37 +++++++++++++++++++ web/cypress/support/component.ts | 15 ++++++++ 9 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 web/cypress/component/mocks/AddToDashboardButton.tsx create mode 100644 web/cypress/component/mocks/OlsToolUIPersesWrapper.tsx create mode 100644 web/cypress/component/mocks/perses-dashboards.tsx create mode 100644 web/cypress/component/mocks/perses-plugin-system.tsx create mode 100644 web/cypress/component/mocks/perses-prometheus-plugin.ts create mode 100644 web/cypress/component/ols-tool-ui/ShowTimeseries.cy.tsx diff --git a/web/cypress.config.ts b/web/cypress.config.ts index 8a823ddd8..b2a94f02c 100644 --- a/web/cypress.config.ts +++ b/web/cypress.config.ts @@ -3,7 +3,7 @@ import * as fs from 'fs-extra'; import * as console from 'console'; import * as path from 'path'; import registerCypressGrep from '@cypress/grep/src/plugin'; -import { DefinePlugin } from 'webpack'; +import { DefinePlugin, NormalModuleReplacementPlugin } from 'webpack'; export default defineConfig({ screenshotsFolder: './cypress/screenshots', @@ -167,6 +167,11 @@ export default defineConfig({ webpackConfig: { resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'], + alias: { + '@perses-dev/plugin-system': path.resolve(__dirname, 'cypress/component/mocks/perses-plugin-system.tsx'), + '@perses-dev/dashboards': path.resolve(__dirname, 'cypress/component/mocks/perses-dashboards.tsx'), + '@perses-dev/prometheus-plugin': path.resolve(__dirname, 'cypress/component/mocks/perses-prometheus-plugin.ts'), + }, }, module: { rules: [ @@ -198,6 +203,14 @@ export default defineConfig({ new DefinePlugin({ 'process.env.I18N_NAMESPACE': JSON.stringify('plugin__monitoring-plugin'), }), + new NormalModuleReplacementPlugin( + /helpers\/OlsToolUIPersesWrapper/, + path.resolve(__dirname, 'cypress/component/mocks/OlsToolUIPersesWrapper.tsx'), + ), + new NormalModuleReplacementPlugin( + /helpers\/AddToDashboardButton/, + path.resolve(__dirname, 'cypress/component/mocks/AddToDashboardButton.tsx'), + ), ], }, }, diff --git a/web/cypress/component/labels.cy.tsx b/web/cypress/component/labels.cy.tsx index f81b8914e..fbe7009c1 100644 --- a/web/cypress/component/labels.cy.tsx +++ b/web/cypress/component/labels.cy.tsx @@ -1,15 +1,5 @@ -import React from 'react'; import { Labels } from '../../src/components/labels'; -// Mock react-i18next to return the key as-is -const mockT = (key: string) => key; -const mockUseTranslation = () => ({ t: mockT }); - -// Stub the module before import -beforeEach(() => { - cy.stub(require('react-i18next'), 'useTranslation').returns(mockUseTranslation()); -}); - describe('Labels', () => { it('renders "No labels" when labels is empty', () => { cy.mount(); diff --git a/web/cypress/component/mocks/AddToDashboardButton.tsx b/web/cypress/component/mocks/AddToDashboardButton.tsx new file mode 100644 index 000000000..6f0a17276 --- /dev/null +++ b/web/cypress/component/mocks/AddToDashboardButton.tsx @@ -0,0 +1,8 @@ +export const AddToDashboardButton = ({ query, name, description }) => ( +