From 4780503c45ee46590088194f70af081731eca286 Mon Sep 17 00:00:00 2001 From: Gabriel Bernal Date: Tue, 17 Mar 2026 12:51:00 +0100 Subject: [PATCH 1/3] feat: re enable dev console views Signed-off-by: Gabriel Bernal --- web/console-extensions.json | 132 ++++++++++-------- web/package.json | 1 - .../Incidents/IncidentsDetailsRowTable.tsx | 2 +- .../components/Incidents/IncidentsPage.tsx | 2 +- web/src/components/MetricsPage.tsx | 39 ++++-- .../alerting/AlertDetail/SilencedByTable.tsx | 6 +- .../AlertList/AggregateAlertTableRow.tsx | 4 +- .../alerting/AlertList/AlertTableRow.tsx | 6 +- .../alerting/AlertRulesDetailsPage.tsx | 15 +- .../components/alerting/AlertRulesPage.tsx | 4 +- web/src/components/alerting/AlertUtils.tsx | 30 ++-- web/src/components/alerting/AlertingPage.tsx | 10 +- .../components/alerting/AlertsDetailsPage.tsx | 8 +- web/src/components/alerting/AlertsPage.tsx | 21 ++- .../components/alerting/SilenceCreatePage.tsx | 4 +- web/src/components/alerting/SilenceForm.tsx | 11 +- .../alerting/SilencesDetailsPage.tsx | 12 +- web/src/components/alerting/SilencesPage.tsx | 26 +++- web/src/components/alerting/SilencesUtils.tsx | 10 +- .../console/graphs/promethues-graph.tsx | 4 +- .../components/dashboards/legacy/graph.tsx | 3 + .../legacy/legacy-dashboard-page.tsx | 33 ++++- .../dashboards/legacy/legacy-dashboard.tsx | 19 ++- .../dashboards/legacy/useLegacyDashboards.ts | 69 +++++---- .../dashboards/legacy/useOpenshiftProject.ts | 74 ---------- .../hooks/useMonitoringNamespace.ts | 27 ++++ web/src/components/hooks/usePerspective.tsx | 111 +++++++++++---- web/src/components/hooks/useQueryNamespace.ts | 23 --- .../metrics/promql-expression-input.tsx | 1 + web/src/components/query-browser.tsx | 20 ++- .../components/redirects/dev-redirects.tsx | 85 ----------- web/src/contexts/MonitoringContext.tsx | 8 ++ web/src/hooks/useAlerts.ts | 4 +- web/src/hooks/useMonitoring.ts | 11 +- 34 files changed, 438 insertions(+), 397 deletions(-) delete mode 100644 web/src/components/dashboards/legacy/useOpenshiftProject.ts create mode 100644 web/src/components/hooks/useMonitoringNamespace.ts delete mode 100644 web/src/components/hooks/useQueryNamespace.ts delete mode 100644 web/src/components/redirects/dev-redirects.tsx diff --git a/web/console-extensions.json b/web/console-extensions.json index 40133d2a3..4fa5c4dab 100644 --- a/web/console-extensions.json +++ b/web/console-extensions.json @@ -7,11 +7,7 @@ "href": "/monitoring/alerts", "perspective": "admin", "section": "observe", - "startsWith": [ - "monitoring/alertrules", - "monitoring/silences", - "monitoring/incidents" - ] + "startsWith": ["monitoring/alertrules", "monitoring/silences", "monitoring/incidents"] } }, { @@ -20,10 +16,7 @@ "data-quickstart-id": "qs-nav-monitoring" }, "id": "observe-virt-perspective", - "insertBefore": [ - "compute-virt-perspective", - "usermanagement-virt-perspective" - ], + "insertBefore": ["compute-virt-perspective", "usermanagement-virt-perspective"], "name": "%console-app~Observe%", "perspective": "virtualization-perspective" }, @@ -163,10 +156,7 @@ "type": "console.page/route", "properties": { "exact": false, - "path": [ - "/monitoring/targets", - "/monitoring/targets/:scrapeUrl" - ], + "path": ["/monitoring/targets", "/monitoring/targets/:scrapeUrl"], "component": { "$codeRef": "TargetsPage.MpCmoTargetsPage" } @@ -176,9 +166,7 @@ "type": "console.page/route", "properties": { "exact": false, - "path": [ - "/monitoring/query-browser" - ], + "path": ["/monitoring/query-browser"], "component": { "$codeRef": "MetricsPage.MpCmoMetricsPage" } @@ -188,9 +176,7 @@ "type": "console.page/route", "properties": { "exact": false, - "path": [ - "/monitoring/graph" - ], + "path": ["/monitoring/graph"], "component": { "$codeRef": "PrometheusRedirectPage" } @@ -200,10 +186,7 @@ "type": "console.page/route", "properties": { "exact": false, - "path": [ - "/monitoring/dashboards", - "/monitoring/dashboards/:dashboardName" - ], + "path": ["/monitoring/dashboards", "/monitoring/dashboards/:dashboardName"], "component": { "$codeRef": "LegacyDashboardsPage.MpCmoLegacyDashboardsPage" } @@ -213,9 +196,7 @@ "type": "console.page/route", "properties": { "exact": false, - "path": [ - "/monitoring/alertrules/:id" - ], + "path": ["/monitoring/alertrules/:id"], "component": { "$codeRef": "AlertRulesDetailsPage.MpCmoAlertRulesDetailsPage" } @@ -225,9 +206,7 @@ "type": "console.page/route", "properties": { "exact": false, - "path": [ - "/monitoring/alerts/:ruleID" - ], + "path": ["/monitoring/alerts/:ruleID"], "component": { "$codeRef": "AlertsDetailsPage.MpCmoAlertsDetailsPage" } @@ -237,9 +216,7 @@ "type": "console.page/route", "properties": { "exact": false, - "path": [ - "/virt-monitoring" - ], + "path": ["/virt-monitoring"], "component": { "$codeRef": "AlertingPage.MpCmoAlertingPage" } @@ -279,10 +256,7 @@ "type": "console.page/route", "properties": { "exact": false, - "path": [ - "/virt-monitoring/targets", - "/virt-monitoring/targets/:scrapeUrl" - ], + "path": ["/virt-monitoring/targets", "/virt-monitoring/targets/:scrapeUrl"], "component": { "$codeRef": "TargetsPage.MpCmoTargetsPage" } @@ -292,9 +266,7 @@ "type": "console.page/route", "properties": { "exact": false, - "path": [ - "/virt-monitoring/query-browser" - ], + "path": ["/virt-monitoring/query-browser"], "component": { "$codeRef": "MetricsPage.MpCmoMetricsPage" } @@ -304,9 +276,7 @@ "type": "console.page/route", "properties": { "exact": false, - "path": [ - "/virt-monitoring/graph" - ], + "path": ["/virt-monitoring/graph"], "component": { "$codeRef": "PrometheusRedirectPage" } @@ -316,10 +286,7 @@ "type": "console.page/route", "properties": { "exact": false, - "path": [ - "/virt-monitoring/dashboards", - "/virt-monitoring/dashboards/:dashboardName" - ], + "path": ["/virt-monitoring/dashboards", "/virt-monitoring/dashboards/:dashboardName"], "component": { "$codeRef": "LegacyDashboardsPage.MpCmoLegacyDashboardsPage" } @@ -329,9 +296,7 @@ "type": "console.page/route", "properties": { "exact": false, - "path": [ - "/virt-monitoring/alertrules/:id" - ], + "path": ["/virt-monitoring/alertrules/:id"], "component": { "$codeRef": "AlertRulesDetailsPage.MpCmoAlertRulesDetailsPage" } @@ -341,9 +306,7 @@ "type": "console.page/route", "properties": { "exact": false, - "path": [ - "/virt-monitoring/alerts/:ruleID" - ], + "path": ["/virt-monitoring/alerts/:ruleID"], "component": { "$codeRef": "AlertsDetailsPage.MpCmoAlertsDetailsPage" } @@ -355,7 +318,7 @@ "exact": false, "path": "/dev-monitoring/ns/:ns/alerts/:ruleID", "component": { - "$codeRef": "DevRedirects.AlertRedirect" + "$codeRef": "AlertsDetailsPage.MpCmoAlertsDetailsPage" } } }, @@ -365,7 +328,7 @@ "exact": false, "path": "/dev-monitoring/ns/:ns/rules/:id", "component": { - "$codeRef": "DevRedirects.RulesRedirect" + "$codeRef": "AlertRulesDetailsPage.MpCmoAlertRulesDetailsPage" } } }, @@ -375,7 +338,7 @@ "exact": false, "path": "/dev-monitoring/ns/:ns/silences/:id", "component": { - "$codeRef": "DevRedirects.SilenceRedirect" + "$codeRef": "SilencesDetailsPage.MpCmoSilencesDetailsPage" } } }, @@ -385,7 +348,7 @@ "exact": false, "path": "/dev-monitoring/ns/:ns/silences/:id/edit", "component": { - "$codeRef": "DevRedirects.SilenceEditRedirect" + "$codeRef": "SilenceEditPage.MpCmoSilenceEditPage" } } }, @@ -395,18 +358,63 @@ "exact": false, "path": "/dev-monitoring/ns/:ns/silences/~new", "component": { - "$codeRef": "DevRedirects.SilenceNewRedirect" + "$codeRef": "SilenceCreatePage.MpCmoCreateSilencePage" } } }, { - "type": "console.page/route", + "type": "console.tab", "properties": { - "exact": false, - "path": "/dev-monitoring/ns/:ns/metrics", + "contextId": "dev-console-observe", + "name": "%plugin__monitoring-plugin~Silences%", + "href": "silences", + "component": { + "$codeRef": "SilencesPage.MpCmoSilencesPage" + } + } + }, + { + "type": "console.tab", + "properties": { + "contextId": "dev-console-observe", + "name": "%plugin__monitoring-plugin~Metrics%", + "href": "metrics", + "component": { + "$codeRef": "MetricsPage.MpCmoDevMetricsPage" + } + } + }, + { + "type": "console.tab", + "properties": { + "contextId": "dev-console-observe", + "name": "%plugin__monitoring-plugin~Alerts%", + "href": "alerts", + "component": { + "$codeRef": "AlertsPage.MpCmoAlertsPage" + } + } + }, + { + "type": "console.tab", + "properties": { + "contextId": "dev-console-observe", + "name": "%plugin__monitoring-plugin~Alerting rules%", + "href": "alertrules", + "component": { + "$codeRef": "AlertRulesPage.MpCmoAlertRulesPage" + } + } + }, + { + "type": "console.tab", + "properties": { + "contextId": "dev-console-observe", + "name": "%plugin__monitoring-plugin~Dashboards%", + "href": "dashboards", "component": { - "$codeRef": "DevRedirects.MetricsRedirect" + "$codeRef": "LegacyDashboardsPage.MpCmoLegacyDevDashboardsPage" } } } -] \ No newline at end of file +] diff --git a/web/package.json b/web/package.json index 9cfa68fab..54d880d8a 100644 --- a/web/package.json +++ b/web/package.json @@ -182,7 +182,6 @@ "IncidentsPage": "./components/Incidents/IncidentsPage", "TargetsPage": "./components/targets-page", "PrometheusRedirectPage": "./components/redirects/prometheus-redirect-page", - "DevRedirects": "./components/redirects/dev-redirects", "MonitoringContext": "./contexts/MonitoringContext" }, "dependencies": { diff --git a/web/src/components/Incidents/IncidentsDetailsRowTable.tsx b/web/src/components/Incidents/IncidentsDetailsRowTable.tsx index e29e8a095..7cda18099 100644 --- a/web/src/components/Incidents/IncidentsDetailsRowTable.tsx +++ b/web/src/components/Incidents/IncidentsDetailsRowTable.tsx @@ -1,7 +1,7 @@ import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; import { ResourceIcon, Timestamp, useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk'; import { Bullseye, Spinner } from '@patternfly/react-core'; -import { Link } from 'react-router-dom'; +import { Link } from 'react-router-dom-v5-compat'; import { ALL_NAMESPACES_KEY, RuleResource } from '../utils'; import { useTranslation } from 'react-i18next'; import { getRuleUrl, usePerspective } from '../hooks/usePerspective'; diff --git a/web/src/components/Incidents/IncidentsPage.tsx b/web/src/components/Incidents/IncidentsPage.tsx index 8cb739c48..55b4ea686 100644 --- a/web/src/components/Incidents/IncidentsPage.tsx +++ b/web/src/components/Incidents/IncidentsPage.tsx @@ -53,7 +53,7 @@ import { setIncidentsActiveFilters, setIncidentsLastRefreshTime, } from '../../store/actions'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom-v5-compat'; import { changeDaysFilter } from './utils'; import { parsePrometheusDuration } from '../console/console-shared/src/datetime/prometheus'; import withFallback from '../console/console-shared/error/fallbacks/withFallback'; diff --git a/web/src/components/MetricsPage.tsx b/web/src/components/MetricsPage.tsx index 397dea5ea..ebb2ceff3 100644 --- a/web/src/components/MetricsPage.tsx +++ b/web/src/components/MetricsPage.tsx @@ -122,7 +122,7 @@ import { ALL_NAMESPACES_KEY } from './utils'; import { MonitoringProvider } from '../contexts/MonitoringContext'; import { DataTestIDs } from './data-test'; import { useMonitoring } from '../hooks/useMonitoring'; -import { useQueryNamespace } from './hooks/useQueryNamespace'; +import { useMonitoringNamespace } from './hooks/useMonitoringNamespace'; // Stores information about the currently focused query input let focusedQuery; @@ -1323,7 +1323,8 @@ const GraphUnitsDropDown: FC = () => { const MetricsPage_: FC = () => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const [units, setUnits] = useQueryParam(QueryParams.Units, StringParam); - const { setNamespace } = useQueryNamespace(); + const { setNamespace } = useMonitoringNamespace(); + const { displayNamespaceSelector } = useMonitoring(); const dispatch = useDispatch(); @@ -1402,14 +1403,18 @@ const MetricsPage_: FC = () => { return ( <> - {t('Metrics')} - { - dispatch(queryBrowserDeleteAllQueries()); - setNamespace(namespace); - }} - /> - + {displayNamespaceSelector && ( + <> + {t('Metrics')} + { + dispatch(queryBrowserDeleteAllQueries()); + setNamespace(namespace); + }} + /> + + )} + {t('This dropdown only formats results.')}}> @@ -1470,6 +1475,20 @@ export const MpCmoMetricsPage: React.FC = () => { ); }; +export const MpCmoDevMetricsPage: React.FC = () => { + return ( + + + + ); +}; + type QueryTableProps = { index: number; namespace?: string; diff --git a/web/src/components/alerting/AlertDetail/SilencedByTable.tsx b/web/src/components/alerting/AlertDetail/SilencedByTable.tsx index 87b347024..be8224688 100644 --- a/web/src/components/alerting/AlertDetail/SilencedByTable.tsx +++ b/web/src/components/alerting/AlertDetail/SilencedByTable.tsx @@ -8,6 +8,7 @@ import { getSilenceAlertUrl, usePerspective, } from '../../hooks/usePerspective'; +import { useMonitoringNamespace } from '../../hooks/useMonitoringNamespace'; import { DataViewTable, DataViewTr, @@ -26,11 +27,12 @@ export const SilencedByList: FC<{ silences: Silence[] }> = ({ silences }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const { perspective } = usePerspective(); const navigate = useNavigate(); + const { namespace } = useMonitoringNamespace(); const [isModalOpen, , setModalOpen, setModalClosed] = useBoolean(false); const [silence, setSilence] = useState(null); const editSilence = (event: MouseEvent, rowIndex: number) => { - navigate(getEditSilenceAlertUrl(perspective, silences.at(rowIndex)?.id)); + navigate(getEditSilenceAlertUrl(perspective, silences.at(rowIndex)?.id, namespace)); }; const rowActions = (silence: Silence): IAction[] => { @@ -73,7 +75,7 @@ export const SilencedByList: FC<{ silences: Silence[] }> = ({ silences }) => { {silence.name} diff --git a/web/src/components/alerting/AlertList/AggregateAlertTableRow.tsx b/web/src/components/alerting/AlertList/AggregateAlertTableRow.tsx index f20958bb4..3eb830a71 100644 --- a/web/src/components/alerting/AlertList/AggregateAlertTableRow.tsx +++ b/web/src/components/alerting/AlertList/AggregateAlertTableRow.tsx @@ -4,6 +4,7 @@ import type { FC } from 'react'; import { useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { getRuleUrl, usePerspective } from '../../../components/hooks/usePerspective'; +import { useMonitoringNamespace } from '../../hooks/useMonitoringNamespace'; import { AggregatedAlert } from '../AlertsAggregates'; import { AlertState, SeverityBadge } from '../AlertUtils'; import AlertTableRow from './AlertTableRow'; @@ -26,6 +27,7 @@ const AggregateAlertTableRow: AggregateAlertTableRowProps = ({ const [isExpanded, setIsExpanded] = useState(false); const { t } = useTranslation(process.env.I18N_NAMESPACE); const { perspective } = usePerspective(); + const { namespace } = useMonitoringNamespace(); const title = aggregatedAlert.name; const isACMPerspective = perspective === 'acm'; @@ -93,7 +95,7 @@ const AggregateAlertTableRow: AggregateAlertTableRowProps = ({ diff --git a/web/src/components/alerting/AlertList/AlertTableRow.tsx b/web/src/components/alerting/AlertList/AlertTableRow.tsx index a84c52cb1..88542f647 100644 --- a/web/src/components/alerting/AlertList/AlertTableRow.tsx +++ b/web/src/components/alerting/AlertList/AlertTableRow.tsx @@ -28,6 +28,7 @@ import { getNewSilenceAlertUrl, usePerspective, } from '../../../components/hooks/usePerspective'; +import { useMonitoringNamespace } from '../../hooks/useMonitoringNamespace'; import { Link } from 'react-router-dom-v5-compat'; import { DataTestIDs } from '../../data-test'; @@ -35,6 +36,7 @@ const AlertTableRow: FC<{ alert: Alert }> = ({ alert }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const { perspective } = usePerspective(); const navigate = useNavigate(); + const { namespace } = useMonitoringNamespace(); const state = alertState(alert); @@ -46,7 +48,7 @@ const AlertTableRow: FC<{ alert: Alert }> = ({ alert }) => { dropdownItems.unshift( navigate(getNewSilenceAlertUrl(perspective, alert))} + onClick={() => navigate(getNewSilenceAlertUrl(perspective, alert, namespace))} data-test={DataTestIDs.SilenceAlertDropdownItem} > {t('Silence alert')} @@ -83,7 +85,7 @@ const AlertTableRow: FC<{ alert: Alert }> = ({ alert }) => { diff --git a/web/src/components/alerting/AlertRulesDetailsPage.tsx b/web/src/components/alerting/AlertRulesDetailsPage.tsx index 91993c97e..6fc654670 100644 --- a/web/src/components/alerting/AlertRulesDetailsPage.tsx +++ b/web/src/components/alerting/AlertRulesDetailsPage.tsx @@ -60,6 +60,7 @@ import { getQueryBrowserUrl, usePerspective, } from '../hooks/usePerspective'; +import { useMonitoringNamespace } from '../hooks/useMonitoringNamespace'; import KebabDropdown from '../kebab-dropdown'; import { Labels } from '../labels'; import { ToggleGraph } from '../MetricsPage'; @@ -89,6 +90,7 @@ export const ActiveAlerts: FC = ({ alerts, ruleID }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const { perspective } = usePerspective(); const navigate = useNavigate(); + const { namespace } = useMonitoringNamespace(); return ( @@ -108,7 +110,7 @@ export const ActiveAlerts: FC = ({ alerts, ruleID }) => {
{alertDescription(a)} @@ -126,7 +128,7 @@ export const ActiveAlerts: FC = ({ alerts, ruleID }) => { navigate(getNewSilenceAlertUrl(perspective, a))} + onClick={() => navigate(getNewSilenceAlertUrl(perspective, a, namespace))} > {t('Silence alert')} , @@ -141,7 +143,8 @@ export const ActiveAlerts: FC = ({ alerts, ruleID }) => { }; const AlertRulesDetailsPage_: FC = () => { const { t } = useTranslation(process.env.I18N_NAMESPACE); - const params = useParams<{ ns?: string; id: string }>(); + const params = useParams<{ id: string }>(); + const { namespace } = useMonitoringNamespace(); const { rules, rulesAlertLoading } = useAlerts(); @@ -184,7 +187,10 @@ const AlertRulesDetailsPage_: FC = () => { - + {t('Alerting rules')} @@ -310,6 +316,7 @@ const AlertRulesDetailsPage_: FC = () => { to={getQueryBrowserUrl({ perspective: perspective, query: rule?.query, + namespace, })} > diff --git a/web/src/components/alerting/AlertRulesPage.tsx b/web/src/components/alerting/AlertRulesPage.tsx index 2e6903d2d..281579d6c 100644 --- a/web/src/components/alerting/AlertRulesPage.tsx +++ b/web/src/components/alerting/AlertRulesPage.tsx @@ -41,6 +41,7 @@ import { severityRowFilter } from './AlertUtils'; import { MonitoringProvider } from '../../contexts/MonitoringContext'; import { DataTestIDs } from '../data-test'; import { useAlerts } from '../../hooks/useAlerts'; +import { useMonitoringNamespace } from '../hooks/useMonitoringNamespace'; const StateCounts: FC<{ alerts: PrometheusAlert[] }> = ({ alerts }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); @@ -85,6 +86,7 @@ const alertStateFilter = (t): RowFilter => ({ const RuleTableRow: FC> = ({ obj }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const { perspective } = usePerspective(); + const { namespace } = useMonitoringNamespace(); const title: string = obj.annotations?.description || obj.annotations?.message; @@ -97,7 +99,7 @@ const RuleTableRow: FC> = ({ obj }) => { diff --git a/web/src/components/alerting/AlertUtils.tsx b/web/src/components/alerting/AlertUtils.tsx index 13a213fb0..16e767a2b 100644 --- a/web/src/components/alerting/AlertUtils.tsx +++ b/web/src/components/alerting/AlertUtils.tsx @@ -1,5 +1,3 @@ -import type { FC, ReactNode } from 'react'; -import { memo } from 'react'; import { Action, Alert, @@ -15,18 +13,15 @@ import { SilenceStates, Timestamp, } from '@openshift-console/dynamic-plugin-sdk'; -import { AlertSource } from '../types'; -import * as _ from 'lodash-es'; -import { useTranslation } from 'react-i18next'; import { - Alert as PFAlert, - Popover, Button, DescriptionList, + DescriptionListDescription, DescriptionListGroup, DescriptionListTerm, - DescriptionListDescription, Label, + Alert as PFAlert, + Popover, Tooltip, } from '@patternfly/react-core'; import { @@ -38,11 +33,6 @@ import { OutlinedBellIcon, SeverityUndefinedIcon, } from '@patternfly/react-icons'; -import { FormatSeriesTitle, QueryBrowser } from '../query-browser'; -import { Link } from 'react-router-dom-v5-compat'; -import { TFunction } from 'i18next'; -import { getQueryBrowserUrl, usePerspective } from '../hooks/usePerspective'; -import { NamespaceModel } from '../console/models'; import { t_global_border_color_status_info_default, t_global_color_status_danger_default, @@ -53,6 +43,17 @@ import { t_global_text_color_disabled, t_global_text_color_subtle, } from '@patternfly/react-tokens'; +import { TFunction } from 'i18next'; +import * as _ from 'lodash-es'; +import type { FC, ReactNode } from 'react'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom-v5-compat'; +import { NamespaceModel } from '../console/models'; +import { getQueryBrowserUrl, usePerspective } from '../hooks/usePerspective'; +import { useMonitoringNamespace } from '../hooks/useMonitoringNamespace'; +import { FormatSeriesTitle, QueryBrowser } from '../query-browser'; +import { AlertSource } from '../types'; export const getAdditionalSources = ( data: Array, @@ -247,13 +248,14 @@ export const Graph: FC = ({ }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const { perspective } = usePerspective(); + const { namespace } = useMonitoringNamespace(); // 3 times the rule's duration, but not less than 30 minutes const timespan = Math.max(3 * ruleDuration, 30 * 60) * 1000; const GraphLink = () => query && perspective !== 'acm' ? ( - + {t('Inspect')} ) : null; diff --git a/web/src/components/alerting/AlertingPage.tsx b/web/src/components/alerting/AlertingPage.tsx index a5a68321a..c9b2b0a39 100644 --- a/web/src/components/alerting/AlertingPage.tsx +++ b/web/src/components/alerting/AlertingPage.tsx @@ -9,11 +9,11 @@ import { } from '@openshift-console/dynamic-plugin-sdk'; import { MonitoringProvider } from '../../contexts/MonitoringContext'; import { useMonitoring } from '../../hooks/useMonitoring'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom-v5-compat'; import { AlertResource, RuleResource, SilenceResource } from '../utils'; import { useDispatch } from 'react-redux'; import { alertingClearSelectorData } from '../../store/actions'; -import { useQueryNamespace } from '../hooks/useQueryNamespace'; +import { useMonitoringNamespace } from '../hooks/useMonitoringNamespace'; const CmoAlertsPage = lazy(() => import(/* webpackChunkName: "CmoAlertsPage" */ './AlertsPage').then((module) => ({ @@ -58,10 +58,8 @@ const namespacedPages = [ const AlertingPage: FC = () => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const dispatch = useDispatch(); - const { useAlertsTenancy, accessCheckLoading } = useMonitoring(); - const [perspective] = useActivePerspective(); - const { setNamespace } = useQueryNamespace(); + const { setNamespace } = useMonitoringNamespace(); const { plugin, prometheus } = useMonitoring(); @@ -97,7 +95,7 @@ const AlertingPage: FC = () => { return ( <> - {namespacedPages.includes(pathname) && !accessCheckLoading && useAlertsTenancy && ( + {namespacedPages.includes(pathname) && ( { dispatch(alertingClearSelectorData(prometheus, namespace)); diff --git a/web/src/components/alerting/AlertsDetailsPage.tsx b/web/src/components/alerting/AlertsDetailsPage.tsx index 682fa5d76..1db080929 100644 --- a/web/src/components/alerting/AlertsDetailsPage.tsx +++ b/web/src/components/alerting/AlertsDetailsPage.tsx @@ -26,6 +26,7 @@ import { getRuleUrl, usePerspective, } from '../hooks/usePerspective'; +import { useMonitoringNamespace } from '../hooks/useMonitoringNamespace'; import { AlertResource, alertState, RuleResource } from '../utils'; import { MonitoringProvider } from '../../contexts/MonitoringContext'; @@ -95,6 +96,7 @@ const AlertsDetailsPage_: FC = () => { const params = useParams<{ ruleID: string }>(); const navigate = useNavigate(); const { plugin } = useMonitoring(); + const { namespace } = useMonitoringNamespace(); const { perspective } = usePerspective(); @@ -156,7 +158,7 @@ const AlertsDetailsPage_: FC = () => { - + {t('Alerts')} @@ -190,7 +192,7 @@ const AlertsDetailsPage_: FC = () => { {state !== AlertStates.Silenced && ( {a.labels.alertname} @@ -244,7 +250,7 @@ const SilencedAlertsList: FC = ({ alerts }) => { dropdownItems={[ navigate(getRuleUrl(perspective, a.rule))} + onClick={() => navigate(getRuleUrl(perspective, a.rule, namespace))} > {t('View alerting rule')} , diff --git a/web/src/components/alerting/SilencesPage.tsx b/web/src/components/alerting/SilencesPage.tsx index 5e6080f72..a0b773bf5 100644 --- a/web/src/components/alerting/SilencesPage.tsx +++ b/web/src/components/alerting/SilencesPage.tsx @@ -29,15 +29,16 @@ import withFallback from '../console/console-shared/error/fallbacks/withFallback import { EmptyBox } from '../console/console-shared/src/components/empty-state/EmptyBox'; import { useBoolean } from '../hooks/useBoolean'; import { getFetchSilenceUrl, getNewSilenceUrl, usePerspective } from '../hooks/usePerspective'; -import { fuzzyCaseInsensitive, silenceCluster, silenceState } from '../utils'; +import { ALL_NAMESPACES_KEY, fuzzyCaseInsensitive, silenceCluster, silenceState } from '../utils'; import { SelectedSilencesContext, SilenceTableRow } from './SilencesUtils'; import { MonitoringProvider } from '../../contexts/MonitoringContext'; import { DataTestIDs } from '../data-test'; import { useAlerts } from '../../hooks/useAlerts'; +import { useMonitoringNamespace } from '../hooks/useMonitoringNamespace'; const SilencesPage_: FC = () => { const { t } = useTranslation(process.env.I18N_NAMESPACE); - + const { namespace } = useMonitoringNamespace(); const { perspective } = usePerspective(); const [selectedSilences, setSelectedSilences] = useState(new Set()); @@ -98,7 +99,20 @@ const SilencesPage_: FC = () => { return filters; }, [perspective, t, silenceClusterLabels]); - const [staticData, filteredData, onFilterChange] = useListPageFilter(silences?.data, rowFilters); + /** + * Filters silences based on the selected namespace. + * "All Projects": returns all silences, including those without a namespace matcher. + */ + const namespacedSilences = + ALL_NAMESPACES_KEY === namespace + ? silences?.data + : silences?.data?.filter((s) => + s.matchers.some((m) => m.name === 'namespace' && m.value === namespace), + ); + const [staticData, filteredData, onFilterChange] = useListPageFilter( + namespacedSilences, + rowFilters, + ); const columns = useMemo>>(() => { const cols: Array> = [ @@ -273,6 +287,7 @@ const ExpireAllSilencesButton: FC = ({ setErrorMes const { trigger: refetchSilencesAndAlerts } = useAlerts(); const { perspective } = usePerspective(); + const { namespace } = useMonitoringNamespace(); const [isInProgress, , setInProgress, setNotInProgress] = useBoolean(false); @@ -282,7 +297,7 @@ const ExpireAllSilencesButton: FC = ({ setErrorMes setInProgress(); Promise.allSettled( [...selectedSilences].map((silenceID: string) => - consoleFetchJSON.delete(getFetchSilenceUrl(perspective, silenceID)), + consoleFetchJSON.delete(getFetchSilenceUrl(perspective, silenceID, namespace)), ), ).then((values) => { setNotInProgress(); @@ -320,9 +335,10 @@ const SilenceTableRowWithCheckbox: FC> = ({ obj }) => ( const CreateSilenceButton: FC = memo(() => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const { perspective } = usePerspective(); + const { namespace } = useMonitoringNamespace(); return ( - + diff --git a/web/src/components/alerting/SilencesUtils.tsx b/web/src/components/alerting/SilencesUtils.tsx index b42661b3d..fb39d64ff 100644 --- a/web/src/components/alerting/SilencesUtils.tsx +++ b/web/src/components/alerting/SilencesUtils.tsx @@ -49,6 +49,7 @@ import { getSilenceAlertUrl, usePerspective, } from '../hooks/usePerspective'; +import { useMonitoringNamespace } from '../hooks/useMonitoringNamespace'; import { silenceMatcherEqualitySymbol, SilenceResource, silenceState } from '../utils'; import { SeverityCounts, StateTimestamp } from './AlertUtils'; import { DataTestIDs } from '../data-test'; @@ -56,6 +57,7 @@ import { DataTestIDs } from '../data-test'; export const SilenceTableRow: FC = ({ obj, showCheckbox }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const { perspective } = usePerspective(); + const { namespace } = useMonitoringNamespace(); const { createdBy, endsAt, firingAlerts, id, name, startsAt, matchers } = obj; const state = silenceState(obj); @@ -105,7 +107,7 @@ export const SilenceTableRow: FC = ({ obj, showCheckbox }) {name} @@ -204,12 +206,13 @@ export const SilenceDropdown: FC = ({ silence, toggleText const { t } = useTranslation(process.env.I18N_NAMESPACE); const { perspective } = usePerspective(); const navigate = useNavigate(); + const { namespace } = useMonitoringNamespace(); const [isOpen, setIsOpen, , setClosed] = useBoolean(false); const [isModalOpen, , setModalOpen, setModalClosed] = useBoolean(false); const editSilence = () => { - navigate(getEditSilenceAlertUrl(perspective, silence.id)); + navigate(getEditSilenceAlertUrl(perspective, silence.id, namespace)); }; const dropdownItems = @@ -278,6 +281,7 @@ export const ExpireSilenceModal: FC = ({ }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const { perspective } = usePerspective(); + const { namespace } = useMonitoringNamespace(); const [isInProgress, , setInProgress, setNotInProgress] = useBoolean(false); const [success, , setSuccess] = useBoolean(false); @@ -285,7 +289,7 @@ export const ExpireSilenceModal: FC = ({ const expireSilence = () => { setInProgress(); - const url = getFetchSilenceUrl(perspective, silenceID); + const url = getFetchSilenceUrl(perspective, silenceID, namespace); consoleFetchJSON .delete(url) .then(() => { diff --git a/web/src/components/console/graphs/promethues-graph.tsx b/web/src/components/console/graphs/promethues-graph.tsx index d97e22ee5..29212a566 100644 --- a/web/src/components/console/graphs/promethues-graph.tsx +++ b/web/src/components/console/graphs/promethues-graph.tsx @@ -7,6 +7,7 @@ import { Link } from 'react-router-dom-v5-compat'; import { Title } from '@patternfly/react-core'; import { getMutlipleQueryBrowserUrl, usePerspective } from '../../hooks/usePerspective'; +import { useMonitoringNamespace } from '../../hooks/useMonitoringNamespace'; import { RootState } from '../../../store/store'; const getActiveNamespace = ({ UI }: RootState): string => UI.get('activeNamespace'); @@ -24,6 +25,7 @@ const PrometheusGraphLink_: FC = ({ ariaChartLinkLabel, }) => { const { perspective } = usePerspective(); + const { namespace } = useMonitoringNamespace(); const queries = _.compact(_.castArray(query)); if (!queries.length) { return <>{children}; @@ -31,7 +33,7 @@ const PrometheusGraphLink_: FC = ({ const params = new URLSearchParams(); queries.forEach((q, index) => params.set(`query${index}`, q)); - const url = getMutlipleQueryBrowserUrl(perspective, params); + const url = getMutlipleQueryBrowserUrl(perspective, params, namespace); return ( void; pollInterval: number; queries: string[]; showLegend?: boolean; @@ -27,6 +28,7 @@ const Graph: FC = ({ customDataSource, formatSeriesTitle, isStack, + onLoadingChange, pollInterval, queries, showLegend, @@ -60,6 +62,7 @@ const Graph: FC = ({ formatSeriesTitle={formatSeriesTitle} hideControls isStack={isStack} + onLoadingChange={onLoadingChange} onZoom={onZoom} pollInterval={pollInterval} queries={queries} diff --git a/web/src/components/dashboards/legacy/legacy-dashboard-page.tsx b/web/src/components/dashboards/legacy/legacy-dashboard-page.tsx index c102b1eb5..7fa810db0 100644 --- a/web/src/components/dashboards/legacy/legacy-dashboard-page.tsx +++ b/web/src/components/dashboards/legacy/legacy-dashboard-page.tsx @@ -10,14 +10,17 @@ import ErrorAlert from './error'; import { DashboardSkeletonLegacy } from './dashboard-skeleton-legacy'; import { useLegacyDashboards } from './useLegacyDashboards'; import { MonitoringProvider } from '../../../contexts/MonitoringContext'; -import { useOpenshiftProject } from './useOpenshiftProject'; +import { useMonitoringNamespace } from '../../hooks/useMonitoringNamespace'; +import { useMonitoring } from '../../../hooks/useMonitoring'; +import { StringParam, useQueryParam } from 'use-query-params'; +import { QueryParams } from '../../../components/query-params'; type LegacyDashboardsPageProps = { urlBoard: string; }; const LegacyDashboardsPage_: FC = ({ urlBoard }) => { - const { project, setProject } = useOpenshiftProject(); + const { namespace, setNamespace } = useMonitoringNamespace(); const { legacyDashboardsError, legacyRows, @@ -25,13 +28,14 @@ const LegacyDashboardsPage_: FC = ({ urlBoard }) => { legacyDashboardsMetadata, changeLegacyDashboard, legacyDashboard, - } = useLegacyDashboards(project, urlBoard); + } = useLegacyDashboards(namespace, urlBoard); const { perspective } = usePerspective(); + const { displayNamespaceSelector } = useMonitoring(); const { t } = useTranslation(process.env.I18N_NAMESPACE); return ( <> - setProject(namespace)} /> + {displayNamespaceSelector && setNamespace(ns)} />} { ); }; + +// Small wrapper to be able to use the query params provided by the monitoring provider +const DashboardQueryWrapper = () => { + const [dashboard] = useQueryParam(QueryParams.Dashboard, StringParam); + + return ; +}; + +export const MpCmoLegacyDevDashboardsPage: FC = () => { + return ( + + + + ); +}; diff --git a/web/src/components/dashboards/legacy/legacy-dashboard.tsx b/web/src/components/dashboards/legacy/legacy-dashboard.tsx index 02ced1e49..b1e04f4e0 100644 --- a/web/src/components/dashboards/legacy/legacy-dashboard.tsx +++ b/web/src/components/dashboards/legacy/legacy-dashboard.tsx @@ -15,6 +15,7 @@ import { Flex, FlexItem, ExpandableSectionToggle, + Spinner, } from '@patternfly/react-core'; import type { FC } from 'react'; import { memo, useRef, useState, useCallback, useEffect, useMemo } from 'react'; @@ -36,6 +37,7 @@ import { getObserveState, usePerspective, } from '../../hooks/usePerspective'; +import { useMonitoringNamespace } from '../../hooks/useMonitoringNamespace'; import KebabDropdown from '../../kebab-dropdown'; import { MonitoringState } from '../../../store/store'; import { evaluateVariableTemplate, Variable } from './legacy-variable-dropdowns'; @@ -47,7 +49,6 @@ import { isDataSource, } from '@openshift-console/dynamic-plugin-sdk/lib/extensions/dashboard-data-source'; import { t_global_font_size_heading_h2 } from '@patternfly/react-tokens'; -import { GraphEmpty } from '../../../components/console/graphs/graph-empty'; import { GraphUnits } from '../../../components/metrics/units'; import { LegacyDashboardPageTestIDs } from '../../../components/data-test'; import { useMonitoring } from '../../../hooks/useMonitoring'; @@ -63,6 +64,7 @@ const QueryBrowserLink = ({ }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const { perspective } = usePerspective(); + const { namespace } = useMonitoringNamespace(); const params = new URLSearchParams(); queries.forEach((q, i) => params.set(`query${i}`, q)); @@ -77,7 +79,7 @@ const QueryBrowserLink = ({ return ( {t('Inspect')} @@ -121,6 +123,7 @@ const Card: FC = memo(({ panel, perspective }) => { const [isError, setIsError] = useState(false); const [dataSourceInfoLoading, setDataSourceInfoLoading] = useState(true); const [customDataSource, setCustomDataSource] = useState(undefined); + const [isChartLoading, setIsChartLoading] = useState(panel.type === 'graph'); const customDataSourceName = panel.datasource?.name; const [extensions, extensionsResolved] = useResolvedExtensions(isDataSource); const hasExtensions = !_.isEmpty(extensions); @@ -304,6 +307,7 @@ const Card: FC = memo(({ panel, perspective }) => { actions={{ actions: ( <> + {(isLoading || isChartLoading) && } {!isLoading && ( = memo(({ panel, perspective }) => { {t('Error loading card')} ) : ( -
- {isLoading || !wasEverVisible ? ( - - ) : ( +
+ {!isLoading && wasEverVisible && ( <> {panel.type === 'grafana-piechart-panel' && ( = memo(({ panel, perspective }) => { { const { t } = useTranslation('plugin__monitoring-plugin'); @@ -34,7 +34,7 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { const [legacyDashboardsError, setLegacyDashboardsError] = useState(); const [refreshInterval] = useQueryParam(QueryParams.RefreshInterval, NumberParam); const [legacyDashboardsLoading, , , setLegacyDashboardsLoaded] = useBoolean(true); - const [initialLoad, , setInitialUnloaded, setInitialLoaded] = useBoolean(true); + const [initialLoad, , , setInitialLoaded] = useBoolean(true); const dispatch = useDispatch(); const navigate = useNavigate(); @@ -117,7 +117,7 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { }, [legacyDashboards, legacyDashboardsLoading]); const changeLegacyDashboard = useCallback( - (newBoard: string) => { + (newBoard: string, forceRefresh = false) => { if (!newBoard) { // If the board is being cleared then don't do anything return; @@ -128,11 +128,12 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { const queryArguments = getAllQueryArguments(); const params = new URLSearchParams(queryArguments); - const url = `${getLegacyDashboardsUrl(perspective, newBoard)}?${params.toString()}`; + const url = getLegacyDashboardsUrl(perspective, newBoard, namespace); - if (newBoard !== urlBoard || initialLoad) { - if (params.get(QueryParams.Dashboard) !== newBoard) { - navigate(url, { replace: true }); + if (newBoard !== urlBoard || forceRefresh) { + if (!params.has(QueryParams.Dashboard) || params.get(QueryParams.Dashboard) !== newBoard) { + params.set(QueryParams.Dashboard, newBoard); + navigate(`${url}?${params.toString()}`, { replace: true }); } dispatch(dashboardsPatchAllVariables(allVariables)); @@ -150,16 +151,7 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { ); } }, - [ - perspective, - urlBoard, - dispatch, - navigate, - namespace, - legacyDashboards, - initialLoad, - refreshInterval, - ], + [perspective, urlBoard, dispatch, navigate, namespace, legacyDashboards, refreshInterval], ); useEffect(() => { @@ -169,19 +161,22 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { initialLoad) && !_.isEmpty(legacyDashboards) ) { - changeLegacyDashboard(urlBoard || legacyDashboards?.[0]?.name); + changeLegacyDashboard(urlBoard || legacyDashboards?.[0]?.name, initialLoad); setInitialLoaded(); } }, [legacyDashboards, changeLegacyDashboard, initialLoad, setInitialLoaded, urlBoard]); useEffect(() => { - // Basically perform a full reload when changing a namespace to force the variables and the - // dashboard to reset. This is needed for when we transition between ALL_NS and a normal - // namespace, but is performed quickly and should help insure consistency when transitioning - // between any namespaces - setInitialUnloaded(); - /* eslint-disable react-hooks/exhaustive-deps */ - }, [namespace]); + if (initialLoad || _.isEmpty(legacyDashboards)) { + return; + } + + const currentBoard = urlBoard || legacyDashboards?.[0]?.name; + if (currentBoard) { + const allVariables = getAllVariables(legacyDashboards, currentBoard, namespace); + dispatch(dashboardsPatchAllVariables(allVariables)); + } + }, [namespace, legacyDashboards, urlBoard, dispatch, initialLoad]); // Clear variables on unmount useEffect(() => { diff --git a/web/src/components/dashboards/legacy/useOpenshiftProject.ts b/web/src/components/dashboards/legacy/useOpenshiftProject.ts deleted file mode 100644 index 10a8cfb69..000000000 --- a/web/src/components/dashboards/legacy/useOpenshiftProject.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk'; -import { useCallback, useEffect } from 'react'; -import { QueryParams } from '../../query-params'; -import { StringParam, useQueryParam } from 'use-query-params'; -import { useDispatch, useSelector } from 'react-redux'; -import { dashboardsPatchVariable } from '../../../store/actions'; -import { MonitoringState } from '../../../store/store'; -import { getObserveState } from '../../hooks/usePerspective'; -import { useMonitoring } from '../../../hooks/useMonitoring'; -import { ALL_NAMESPACES_KEY } from '../../utils'; - -export const useOpenshiftProject = () => { - const [activeNamespace, setActiveNamespace] = useActiveNamespace(); - const [openshiftProject, setOpenshiftProject] = useQueryParam( - QueryParams.OpenshiftProject, - StringParam, - ); - const { plugin } = useMonitoring(); - const variableNamespace = useSelector( - (state: MonitoringState) => - getObserveState(plugin, state).dashboards.variables['namespace']?.value ?? '', - ); - const dispatch = useDispatch(); - - useEffect(() => { - // If the URL parameter is set, but the activeNamespace doesn't match it, then - // set the activeNamespace to match the URL parameter - if (openshiftProject && openshiftProject !== activeNamespace) { - setActiveNamespace(openshiftProject); - if (variableNamespace !== openshiftProject && openshiftProject !== ALL_NAMESPACES_KEY) { - dispatch( - dashboardsPatchVariable('namespace', { - // Dashboards space variable shouldn't use the ALL_NAMESPACES_KEY - value: openshiftProject, - }), - ); - } - return; - } - if (!openshiftProject) { - setOpenshiftProject(activeNamespace); - if (variableNamespace !== activeNamespace && openshiftProject !== ALL_NAMESPACES_KEY) { - // Dashboards space variable shouldn't use the ALL_NAMESPACES_KEY - dispatch( - dashboardsPatchVariable('namespace', { - value: activeNamespace, - }), - ); - } - return; - } - }, [ - activeNamespace, - setActiveNamespace, - openshiftProject, - setOpenshiftProject, - dispatch, - variableNamespace, - ]); - - const setProject = useCallback( - (namespace: string) => { - setActiveNamespace(namespace); - setOpenshiftProject(namespace); - dispatch(dashboardsPatchVariable('namespace', { value: namespace })); - }, - [setActiveNamespace, setOpenshiftProject, dispatch], - ); - - return { - project: openshiftProject, - setProject, - }; -}; diff --git a/web/src/components/hooks/useMonitoringNamespace.ts b/web/src/components/hooks/useMonitoringNamespace.ts new file mode 100644 index 000000000..b42b2939c --- /dev/null +++ b/web/src/components/hooks/useMonitoringNamespace.ts @@ -0,0 +1,27 @@ +import { useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk'; +import { useEffect } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; +import { useMonitoring } from '../../hooks/useMonitoring'; + +/** + * Utility hook to synchronize the namespace route in the URL with the activeNamespace + * the console uses. It checks for namespace in the following order: + * 1. Route param `:ns` (used in dev console routes like /dev-monitoring/ns/:ns/...) + * 2. Active namespace from console SDK + */ +export const useMonitoringNamespace = () => { + const { ns: routeNamespace } = useParams<{ ns?: string }>(); + const [activeNamespace, setActiveNamespace] = useActiveNamespace(); + const { displayNamespaceSelector } = useMonitoring(); + + useEffect(() => { + if (routeNamespace && activeNamespace !== routeNamespace) { + setActiveNamespace(routeNamespace); + } + }, [routeNamespace, activeNamespace, setActiveNamespace, displayNamespaceSelector]); + + return { + namespace: routeNamespace || activeNamespace, + setNamespace: setActiveNamespace, + }; +}; diff --git a/web/src/components/hooks/usePerspective.tsx b/web/src/components/hooks/usePerspective.tsx index b2794782d..344b52b4a 100644 --- a/web/src/components/hooks/usePerspective.tsx +++ b/web/src/components/hooks/usePerspective.tsx @@ -5,6 +5,7 @@ import * as _ from 'lodash-es'; import { ALERTMANAGER_BASE_PATH, ALERTMANAGER_PROXY_PATH, + ALERTMANAGER_TENANCY_BASE_PATH, AlertResource, labelsToParams, MonitoringPlugins, @@ -61,119 +62,155 @@ export const usePerspective = (): usePerspectiveReturn => { } }; -export const getAlertsUrl = (perspective: Perspective) => { +export const getAlertsUrl = (perspective: Perspective, namespace?: string) => { switch (perspective) { case 'acm': return `/multicloud${AlertResource.url}`; + case 'dev': + return `/dev-monitoring/ns/${namespace}/alerts`; case 'virtualization-perspective': - return `/virt-monitoring/alerts`; + return AlertResource.virtUrl; case 'admin': default: return AlertResource.url; } }; -// There is no equivalent rules list page in the developer perspective -export const getAlertRulesUrl = (perspective: Perspective) => { +export const getAlertRulesUrl = (perspective: Perspective, namespace?: string) => { switch (perspective) { case 'acm': return `/multicloud${RuleResource.url}`; + case 'dev': + return `/dev-monitoring/ns/${namespace}/alertrules`; case 'virtualization-perspective': - return `/virt-monitoring/alertrules`; + return RuleResource.virtUrl; case 'admin': default: return RuleResource.url; } }; -export const getSilencesUrl = (perspective: Perspective) => { +export const getSilencesUrl = (perspective: Perspective, namespace?: string) => { switch (perspective) { case 'acm': return `/multicloud${SilenceResource.url}`; + case 'dev': + return `/dev-monitoring/ns/${namespace}/silences`; case 'virtualization-perspective': - return `/virt-monitoring/silences`; + return SilenceResource.virtUrl; case 'admin': default: return SilenceResource.url; } }; -export const getNewSilenceAlertUrl = (perspective: Perspective, alert: PrometheusAlert) => { +export const getNewSilenceAlertUrl = ( + perspective: Perspective, + alert: PrometheusAlert, + namespace?: string, +) => { switch (perspective) { case 'acm': return `/multicloud${SilenceResource.url}/~new?${labelsToParams(alert.labels)}`; + case 'dev': + return `/dev-monitoring/ns/${namespace}/silences/~new?${labelsToParams(alert.labels)}`; case 'virtualization-perspective': - return `/virt-monitoring/silences/~new?${labelsToParams(alert.labels)}`; + return `${SilenceResource.virtUrl}/~new?${labelsToParams(alert.labels)}`; case 'admin': default: return `${SilenceResource.url}/~new?${labelsToParams(alert.labels)}`; } }; -export const getNewSilenceUrl = (perspective: Perspective) => { +export const getNewSilenceUrl = (perspective: Perspective, namespace?: string) => { switch (perspective) { case 'acm': return `/multicloud${SilenceResource.url}/~new`; + case 'dev': + return `/dev-monitoring/ns/${namespace}/silences/~new`; case 'virtualization-perspective': - return `/virt-monitoring/silences/~new`; + return `${SilenceResource.virtUrl}/~new`; case 'admin': default: return `${SilenceResource.url}/~new`; } }; -export const getRuleUrl = (perspective: Perspective, rule: Rule) => { +export const getRuleUrl = (perspective: Perspective, rule: Rule, namespace?: string) => { switch (perspective) { case 'acm': return `/multicloud${RuleResource.url}/${_.get(rule, 'id')}`; + case 'dev': + return `/dev-monitoring/ns/${namespace}/rules/${rule?.id}`; case 'virtualization-perspective': - return `/virt-monitoring/alertrules/${rule?.id}`; + return `${RuleResource.virtUrl}/${rule?.id}`; case 'admin': default: return `${RuleResource.url}/${_.get(rule, 'id')}`; } }; -export const getSilenceAlertUrl = (perspective: Perspective, id: string) => { +export const getSilenceAlertUrl = (perspective: Perspective, id: string, namespace?: string) => { switch (perspective) { case 'acm': return `/multicloud${SilenceResource.url}/${id}`; + case 'dev': + return `/dev-monitoring/ns/${namespace}/silences/${id}`; case 'virtualization-perspective': - return `/virt-monitoring/silences/${id}`; + return `${SilenceResource.virtUrl}/${id}`; case 'admin': default: return `${SilenceResource.url}/${id}`; } }; -export const getEditSilenceAlertUrl = (perspective: Perspective, id: string) => { +export const getEditSilenceAlertUrl = ( + perspective: Perspective, + id: string, + namespace?: string, +) => { switch (perspective) { case 'acm': return `/multicloud${SilenceResource.url}/${id}/edit`; + case 'dev': + return `/dev-monitoring/ns/${namespace}/silences/${id}/edit`; case 'virtualization-perspective': - return `/virt-monitoring/silences/${id}/edit`; + return `${SilenceResource.virtUrl}/${id}/edit`; case 'admin': default: return `${SilenceResource.url}/${id}/edit`; } }; -export const getAlertUrl = (perspective: Perspective, alert: PrometheusAlert, ruleID: string) => { +export const getAlertUrl = ( + perspective: Perspective, + alert: PrometheusAlert, + ruleID: string, + namespace?: string, +) => { switch (perspective) { case 'acm': return `/multicloud${AlertResource.url}/${ruleID}?${labelsToParams(alert.labels)}`; + case 'dev': + return `/dev-monitoring/ns/${namespace}/alerts/${ruleID}?${labelsToParams(alert.labels)}`; case 'virtualization-perspective': - return `/virt-monitoring/alerts/${ruleID}?${labelsToParams(alert.labels)}`; + return `${AlertResource.virtUrl}/${ruleID}?${labelsToParams(alert.labels)}`; case 'admin': default: return `${AlertResource.url}/${ruleID}?${labelsToParams(alert.labels)}`; } }; -export const getFetchSilenceUrl = (perspective: Perspective, silenceID: string) => { +export const getFetchSilenceUrl = ( + perspective: Perspective, + silenceID: string, + namespace?: string, +) => { switch (perspective) { case 'acm': return `${ALERTMANAGER_PROXY_PATH}/api/v2/silence/${silenceID}`; + case 'dev': + return `${ALERTMANAGER_TENANCY_BASE_PATH}/api/v2/silence/${silenceID}?namespace=${namespace}`; case 'virtualization-perspective': return `${ALERTMANAGER_BASE_PATH}/api/v2/silence/${silenceID}`; default: @@ -196,42 +233,60 @@ export const getObserveState = (plugin: MonitoringPlugins, state: MonitoringStat export const getQueryBrowserUrl = ({ perspective, query, + namespace, units, }: { perspective: Perspective; query: string; + namespace?: string; units?: GraphUnits; }) => { const unitsQueryParam = units ? `&${QueryParams.Units}=${units}` : ''; switch (perspective) { - case 'virtualization-perspective': - return `/virt-monitoring/query-browser?query0=${encodeURIComponent(query)}${unitsQueryParam}`; case 'acm': return ''; + case 'dev': + return `/dev-monitoring/ns/${namespace}/metrics?query0=${encodeURIComponent( + query, + )}${unitsQueryParam}`; + case 'virtualization-perspective': + return `/virt-monitoring/query-browser?query0=${encodeURIComponent(query)}${unitsQueryParam}`; case 'admin': default: return `/monitoring/query-browser?query0=${encodeURIComponent(query)}${unitsQueryParam}`; } }; -export const getMutlipleQueryBrowserUrl = (perspective: Perspective, params: URLSearchParams) => { +export const getMutlipleQueryBrowserUrl = ( + perspective: Perspective, + params: URLSearchParams, + namespace?: string, +) => { switch (perspective) { - case 'virtualization-perspective': - return `/virt-monitoring/query-browser?${params.toString()}`; case 'acm': return ''; + case 'dev': + return `/dev-monitoring/ns/${namespace}/metrics?${params.toString()}`; + case 'virtualization-perspective': + return `/virt-monitoring/query-browser?${params.toString()}`; case 'admin': default: return `/monitoring/query-browser?${params.toString()}`; } }; -export const getLegacyDashboardsUrl = (perspective: Perspective, boardName: string) => { +export const getLegacyDashboardsUrl = ( + perspective: Perspective, + boardName: string, + namespace?: string, +) => { switch (perspective) { - case 'virtualization-perspective': - return `/virt-monitoring/dashboards/${boardName}`; case 'acm': return ''; + case 'dev': + return `/dev-monitoring/ns/${namespace}/dashboards`; + case 'virtualization-perspective': + return `/virt-monitoring/dashboards/${boardName}`; case 'admin': default: return `/monitoring/dashboards/${boardName}`; diff --git a/web/src/components/hooks/useQueryNamespace.ts b/web/src/components/hooks/useQueryNamespace.ts deleted file mode 100644 index 8151c2901..000000000 --- a/web/src/components/hooks/useQueryNamespace.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useEffect } from 'react'; -import { StringParam, useQueryParam } from 'use-query-params'; -import { QueryParams } from '../query-params'; -import { useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk'; - -// Utility hook to syncronize the namespace parameter in the URL with the activeNamespace -// the console uses. It will return the namespace parameter if set or the activeNamespace if -// it isn't set. -export const useQueryNamespace = () => { - const [queryNamespace, setQueryNamespace] = useQueryParam(QueryParams.Namespace, StringParam); - const [activeNamespace, setActiveNamespace] = useActiveNamespace(); - - useEffect(() => { - if (queryNamespace && activeNamespace !== queryNamespace) { - setActiveNamespace(queryNamespace); - } - }, [queryNamespace, activeNamespace, setActiveNamespace, setQueryNamespace]); - - return { - namespace: queryNamespace || activeNamespace, - setNamespace: setQueryNamespace, - }; -}; diff --git a/web/src/components/metrics/promql-expression-input.tsx b/web/src/components/metrics/promql-expression-input.tsx index 4f2900464..55ebe2a6e 100644 --- a/web/src/components/metrics/promql-expression-input.tsx +++ b/web/src/components/metrics/promql-expression-input.tsx @@ -358,6 +358,7 @@ export const PromQLExpressionInput: FC = ({ .then((response) => { const metrics = response?.data; setMetricNames(metrics); + setErrorMessage(undefined); }) .catch((err) => { if (err.name !== 'AbortError') { diff --git a/web/src/components/query-browser.tsx b/web/src/components/query-browser.tsx index 0ab30202f..b775bb1b2 100644 --- a/web/src/components/query-browser.tsx +++ b/web/src/components/query-browser.tsx @@ -45,7 +45,7 @@ import { ChartLineIcon } from '@patternfly/react-icons'; import classNames from 'classnames'; import * as _ from 'lodash-es'; import type { FC, Ref, ReactNode, KeyboardEvent, MouseEvent, ComponentType } from 'react'; -import { memo, useState, useEffect, useCallback, useLayoutEffect } from 'react'; +import { memo, useState, useEffect, useCallback, useLayoutEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; @@ -596,6 +596,7 @@ const QueryBrowser_: FC = ({ GraphLink, hideControls, isStack = false, + onLoadingChange, onZoom, pollInterval, queries, @@ -638,6 +639,12 @@ const QueryBrowser_: FC = ({ const [graphData, setGraphData] = useState(null); const [samples, setSamples] = useState(maxSamplesForSpan); const [updating, setUpdating] = useState(true); + // Track if we ever received valid data to prevent flickering "No datapoints" during refresh + const hasReceivedData = useRef(false); + + useEffect(() => { + onLoadingChange?.(updating); + }, [updating, onLoadingChange]); const [containerRef, width] = useRefWidth(); @@ -808,6 +815,10 @@ const QueryBrowser_: FC = ({ ); setGraphData(newGraphData); onDataChange?.(newGraphData); + // Mark that we've received valid data to prevent flickering during refresh + if (newGraphData && newGraphData.some((d) => d.length > 0)) { + hasReceivedData.current = true; + } setIsDisconnectedEnabled(dataIsDisconnected); @@ -932,7 +943,7 @@ const QueryBrowser_: FC = ({ <> {hideControls ? ( - <>{updating && } + <>{updating && !onLoadingChange && } ) : ( @@ -1014,7 +1025,9 @@ const QueryBrowser_: FC = ({ data-test={DataTestIDs.MetricGraph} > {error && } - {isGraphDataEmpty && } + {isGraphDataEmpty && !(hideControls && (updating || hasReceivedData.current)) && ( + + )} {!isGraphDataEmpty && width > 0 && ( <> {disableZoom ? ( @@ -1102,6 +1115,7 @@ export type QueryBrowserProps = { GraphLink?: ComponentType; hideControls?: boolean; isStack?: boolean; + onLoadingChange?: (isLoading: boolean) => void; onZoom?: GraphOnZoom; pollInterval?: number; queries: string[]; diff --git a/web/src/components/redirects/dev-redirects.tsx b/web/src/components/redirects/dev-redirects.tsx deleted file mode 100644 index db2c8bd19..000000000 --- a/web/src/components/redirects/dev-redirects.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import type { FC } from 'react'; -import { Navigate, useParams } from 'react-router-dom-v5-compat'; -import { - getAlertRulesUrl, - getAlertsUrl, - getEditSilenceAlertUrl, - getLegacyDashboardsUrl, - getSilenceAlertUrl, -} from '../hooks/usePerspective'; -import { QueryParams } from '../query-params'; -import { SilenceResource } from '../utils'; - -export const DashboardRedirect: FC = () => { - const pathParams = useParams<{ ns: string }>(); - - const queryParams = new URLSearchParams(window.location.search); - queryParams.append(QueryParams.OpenshiftProject, pathParams.ns); - - const dashboardName = queryParams.get(QueryParams.Dashboard); - queryParams.delete(QueryParams.Dashboard); - - return ; -}; - -export const AlertRedirect: FC = () => { - const pathParams = useParams<{ ns: string; ruleID: string }>(); - - const queryParams = new URLSearchParams(window.location.search); - queryParams.append(QueryParams.Namespace, pathParams.ns); - - return ( - - ); -}; - -export const RulesRedirect: FC = () => { - const pathParams = useParams<{ ns: string; id: string }>(); - - const queryParams = new URLSearchParams(window.location.search); - queryParams.append(QueryParams.Namespace, pathParams.ns); - - return ( - - ); -}; - -export const SilenceRedirect: FC = () => { - const pathParams = useParams<{ ns: string; id: string }>(); - - const queryParams = new URLSearchParams(window.location.search); - queryParams.append(QueryParams.Namespace, pathParams.ns); - - return ( - - ); -}; - -export const SilenceEditRedirect: FC = () => { - const pathParams = useParams<{ ns: string; id: string }>(); - - const queryParams = new URLSearchParams(window.location.search); - queryParams.append(QueryParams.Namespace, pathParams.ns); - - return ( - - ); -}; - -export const SilenceNewRedirect: FC = () => { - const pathParams = useParams<{ ns: string }>(); - - const queryParams = new URLSearchParams(window.location.search); - queryParams.append(QueryParams.Namespace, pathParams.ns); - - return ; -}; - -export const MetricsRedirect: FC = () => { - const pathParams = useParams<{ ns: string }>(); - - const queryParams = new URLSearchParams(window.location.search); - queryParams.append(QueryParams.Namespace, pathParams.ns); - - return ; -}; diff --git a/web/src/contexts/MonitoringContext.tsx b/web/src/contexts/MonitoringContext.tsx index fb48a4316..c1b4449ae 100644 --- a/web/src/contexts/MonitoringContext.tsx +++ b/web/src/contexts/MonitoringContext.tsx @@ -19,6 +19,11 @@ type MonitoringContextType = { useMetricsTenancy: boolean; /** Dictates if the users access is being loaded. */ accessCheckLoading: boolean; + /** + * Dictates if the namespace selector is shown inside the view, + * in some perspectives the selector already exist outside monitoring components scope + */ + displayNamespaceSelector: boolean; }; export const MonitoringContext = React.createContext({ @@ -27,12 +32,14 @@ export const MonitoringContext = React.createContext({ useAlertsTenancy: false, useMetricsTenancy: false, accessCheckLoading: true, + displayNamespaceSelector: true, }); export const MonitoringProvider: React.FC<{ monitoringContext: { plugin: MonitoringPlugins; prometheus: Prometheus; + displayNamespaceSelector?: boolean; }; }> = ({ children, monitoringContext }) => { const [allNamespaceAlertsTenancy, alertAccessCheckLoading] = useAccessReview({ @@ -56,6 +63,7 @@ export const MonitoringProvider: React.FC<{ useAlertsTenancy: monitoringContext.prometheus === 'cmo' && !allNamespaceAlertsTenancy, useMetricsTenancy: monitoringContext.prometheus === 'cmo' && !allNamespaceMeticsTenancy, accessCheckLoading: alertAccessCheckLoading || metricsAccessCheckLoading, + displayNamespaceSelector: monitoringContext.displayNamespaceSelector ?? true, }; }, [ monitoringContext, diff --git a/web/src/hooks/useAlerts.ts b/web/src/hooks/useAlerts.ts index 34cd2080d..af1af20be 100644 --- a/web/src/hooks/useAlerts.ts +++ b/web/src/hooks/useAlerts.ts @@ -26,14 +26,14 @@ import { } from '../components/alerting/AlertUtils'; import { MonitoringState } from '../store/store'; import { getObserveState } from '../components/hooks/usePerspective'; -import { useQueryNamespace } from '../components/hooks/useQueryNamespace'; +import { useMonitoringNamespace } from '../components/hooks/useMonitoringNamespace'; const POLLING_INTERVAL_MS = 15 * 1000; // 15 seconds export const useAlerts = (props?: { dontUseTenancy?: boolean }) => { // Retrieve external information which dictates which alerts to load and use const { plugin } = useMonitoring(); - const { namespace } = useQueryNamespace(); + const { namespace } = useMonitoringNamespace(); const { prometheus, useAlertsTenancy, accessCheckLoading } = useMonitoring(); const overriddenNamespace = props?.dontUseTenancy || !useAlertsTenancy ? ALL_NAMESPACES_KEY : namespace; diff --git a/web/src/hooks/useMonitoring.ts b/web/src/hooks/useMonitoring.ts index a21002ac1..b3fa3f7e8 100644 --- a/web/src/hooks/useMonitoring.ts +++ b/web/src/hooks/useMonitoring.ts @@ -2,13 +2,20 @@ import { useContext } from 'react'; import { MonitoringContext } from '../contexts/MonitoringContext'; export const useMonitoring = () => { - const { prometheus, plugin, useAlertsTenancy, useMetricsTenancy, accessCheckLoading } = - useContext(MonitoringContext); + const { + prometheus, + plugin, + useAlertsTenancy, + useMetricsTenancy, + accessCheckLoading, + displayNamespaceSelector, + } = useContext(MonitoringContext); return { prometheus, plugin, useAlertsTenancy, useMetricsTenancy, accessCheckLoading, + displayNamespaceSelector, }; }; From d75154a5fa7c9e2baf6df4b4d4fc3a02b574d955 Mon Sep 17 00:00:00 2001 From: Gabriel Bernal Date: Tue, 17 Mar 2026 13:41:32 +0100 Subject: [PATCH 2/3] feat: unset query timeouts so they are defined in the backend Signed-off-by: Gabriel Bernal --- web/src/components/Incidents/api.ts | 5 ++++- web/src/components/console/utils/safe-fetch-hook.ts | 5 ++++- web/src/components/fetch-alerts.tsx | 9 ++++++--- web/src/components/proxied-fetch.ts | 3 ++- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/web/src/components/Incidents/api.ts b/web/src/components/Incidents/api.ts index 0b8c71841..59dafa135 100644 --- a/web/src/components/Incidents/api.ts +++ b/web/src/components/Incidents/api.ts @@ -8,6 +8,9 @@ import { import { getPrometheusBasePath, buildPrometheusUrl } from '../utils'; import { PROMETHEUS_QUERY_INTERVAL_SECONDS } from './utils'; +// Disable client-side timeout (-1) to let the backend control query timeouts +const NO_TIMEOUT = -1; + const MAX_URL_LENGTH = 2048; /** @@ -136,7 +139,7 @@ export const fetchDataForIncidentsAndAlerts = async ( } as PrometheusResponse); } - return consoleFetchJSON(url); + return consoleFetchJSON(url, 'GET', {}, NO_TIMEOUT); }); const responses = await Promise.all(promises); diff --git a/web/src/components/console/utils/safe-fetch-hook.ts b/web/src/components/console/utils/safe-fetch-hook.ts index aa50773ae..e8787352b 100644 --- a/web/src/components/console/utils/safe-fetch-hook.ts +++ b/web/src/components/console/utils/safe-fetch-hook.ts @@ -1,6 +1,9 @@ import { useEffect, useRef } from 'react'; import { consoleFetchJSON } from '@openshift-console/dynamic-plugin-sdk'; +// Disable client-side timeout (-1) to let the backend control query timeouts +const NO_TIMEOUT = -1; + export const useSafeFetch = () => { const controller = useRef(); useEffect(() => { @@ -9,5 +12,5 @@ export const useSafeFetch = () => { }, []); return (url: string): Promise => - consoleFetchJSON(url, 'get', { signal: controller.current.signal as AbortSignal }); + consoleFetchJSON(url, 'GET', { signal: controller.current.signal as AbortSignal }, NO_TIMEOUT); }; diff --git a/web/src/components/fetch-alerts.tsx b/web/src/components/fetch-alerts.tsx index 50701f33f..9088c21dd 100644 --- a/web/src/components/fetch-alerts.tsx +++ b/web/src/components/fetch-alerts.tsx @@ -1,5 +1,8 @@ import { consoleFetchJSON, PrometheusRulesResponse } from '@openshift-console/dynamic-plugin-sdk'; +// Disable client-side timeout (-1) to let the backend control query timeouts +const NO_TIMEOUT = -1; + // Merges Prometheus monitoring alerts with external sources export const fetchAlerts = async ( prometheusURL: string, @@ -10,7 +13,7 @@ export const fetchAlerts = async ( namespace?: string, ): Promise => { if (!externalAlertsFetch || externalAlertsFetch.length === 0) { - return consoleFetchJSON(prometheusURL); + return consoleFetchJSON(prometheusURL, 'GET', {}, NO_TIMEOUT); } const resolvedExternalAlertsSources = externalAlertsFetch.map((extensionProperties) => ({ @@ -22,7 +25,7 @@ export const fetchAlerts = async ( try { const groups = await Promise.allSettled([ - consoleFetchJSON(prometheusURL), + consoleFetchJSON(prometheusURL, 'GET', {}, NO_TIMEOUT), ...resolvedExternalAlertsSources.map((source) => source.fetch(namespace)), ]).then((results) => results @@ -39,6 +42,6 @@ export const fetchAlerts = async ( return { data: { groups }, status: 'success' }; } catch { - return consoleFetchJSON(prometheusURL); + return consoleFetchJSON(prometheusURL, 'GET', {}, NO_TIMEOUT); } }; diff --git a/web/src/components/proxied-fetch.ts b/web/src/components/proxied-fetch.ts index 54f250704..8f60453a9 100644 --- a/web/src/components/proxied-fetch.ts +++ b/web/src/components/proxied-fetch.ts @@ -43,7 +43,8 @@ export const proxiedFetch = (url: string, init?: RequestInitWithTimeout): Pro return response.json(); }); - const timeout = init?.timeout ?? 30 * 1000; + // Disable client-side timeout by default (-1) to let the backend control query timeouts + const timeout = init?.timeout ?? -1; if (timeout <= 0) { return fetchPromise; From 945912ad00513809bf00ae808a0e6e0dc9c6a9cc Mon Sep 17 00:00:00 2001 From: Gabriel Bernal Date: Tue, 17 Mar 2026 16:09:01 +0100 Subject: [PATCH 3/3] fix: make dashboards the default dev observe view Signed-off-by: Gabriel Bernal --- web/console-extensions.json | 2 +- web/src/components/hooks/usePerspective.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/console-extensions.json b/web/console-extensions.json index 4fa5c4dab..1126fffd7 100644 --- a/web/console-extensions.json +++ b/web/console-extensions.json @@ -411,7 +411,7 @@ "properties": { "contextId": "dev-console-observe", "name": "%plugin__monitoring-plugin~Dashboards%", - "href": "dashboards", + "href": "", "component": { "$codeRef": "LegacyDashboardsPage.MpCmoLegacyDevDashboardsPage" } diff --git a/web/src/components/hooks/usePerspective.tsx b/web/src/components/hooks/usePerspective.tsx index 344b52b4a..8380986f4 100644 --- a/web/src/components/hooks/usePerspective.tsx +++ b/web/src/components/hooks/usePerspective.tsx @@ -284,7 +284,7 @@ export const getLegacyDashboardsUrl = ( case 'acm': return ''; case 'dev': - return `/dev-monitoring/ns/${namespace}/dashboards`; + return `/dev-monitoring/ns/${namespace}`; case 'virtualization-perspective': return `/virt-monitoring/dashboards/${boardName}`; case 'admin':