From abe0e9f1258071efc69c413093409de5fe0c0c3a Mon Sep 17 00:00:00 2001 From: Gabriel Bernal Date: Tue, 17 Mar 2026 12:51:00 +0100 Subject: [PATCH 1/4] 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 ccd837b6e..7a2253e1a 100644 --- a/web/package.json +++ b/web/package.json @@ -198,7 +198,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 c8557b24e..0723eba78 100644 --- a/web/src/components/Incidents/IncidentsPage.tsx +++ b/web/src/components/Incidents/IncidentsPage.tsx @@ -50,7 +50,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 b412c4f42..05f19a8cb 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; @@ -1288,7 +1288,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(); @@ -1367,14 +1368,18 @@ const MetricsPage_: FC = () => { return ( <> - {t('Metrics')} - { - dispatch(queryBrowserDeleteAllQueries()); - setNamespace(namespace); - }} - /> - + {displayNamespaceSelector && ( + <> + {t('Metrics')} + { + dispatch(queryBrowserDeleteAllQueries()); + setNamespace(namespace); + }} + /> + + )} + {t('This dropdown only formats results.')}}> @@ -1435,6 +1440,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 c090bb72c..4a731bc35 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 8d1470399..a41f42b5d 100644 --- a/web/src/components/metrics/promql-expression-input.tsx +++ b/web/src/components/metrics/promql-expression-input.tsx @@ -357,6 +357,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 fc78afd0f0f0b5f40b0e9607ab5d56d3300f1757 Mon Sep 17 00:00:00 2001 From: Gabriel Bernal Date: Tue, 17 Mar 2026 13:41:32 +0100 Subject: [PATCH 2/4] feat: unset query timeouts so they are defined in the backend Signed-off-by: Gabriel Bernal --- web/src/components/Incidents/api.ts | 10 ++++++++-- 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, 20 insertions(+), 7 deletions(-) diff --git a/web/src/components/Incidents/api.ts b/web/src/components/Incidents/api.ts index 70517b972..7f9ec7579 100644 --- a/web/src/components/Incidents/api.ts +++ b/web/src/components/Incidents/api.ts @@ -1,8 +1,14 @@ /* eslint-disable max-len */ -import { PrometheusEndpoint, PrometheusResponse } from '@openshift-console/dynamic-plugin-sdk'; +import { + consoleFetchJSON, + PrometheusEndpoint, + PrometheusResponse, +} from '@openshift-console/dynamic-plugin-sdk'; import { getPrometheusBasePath, buildPrometheusUrl } from '../utils'; import { PROMETHEUS_QUERY_INTERVAL_SECONDS } from './utils'; + +const NO_TIMEOUT = -1; /** * Creates a Prometheus alerts query string from grouped alert values. * The function dynamically includes any properties in the input objects that have the "src_" prefix, @@ -96,5 +102,5 @@ export const fetchDataForIncidentsAndAlerts = ( }); } - return fetch(url); + return consoleFetchJSON(url, 'GET', {}, NO_TIMEOUT); }; 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 2b2be26cbd24d7052093a56bed2c72ed4f314aa7 Mon Sep 17 00:00:00 2001 From: Gabriel Bernal Date: Tue, 17 Mar 2026 16:09:01 +0100 Subject: [PATCH 3/4] 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 4a731bc35..8e70ec1f9 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': From 0722e8030d9ffea68b40658699cec2b7f6c903bd Mon Sep 17 00:00:00 2001 From: PeterYurkovich Date: Tue, 14 Apr 2026 12:16:09 -0400 Subject: [PATCH 4/4] chore: manually cherry pick 4.22 changes --- Dockerfile | 2 +- Dockerfile.dev-mcp | 2 +- Dockerfile.konflux | 57 --- devspace.yaml | 2 +- web/src/components/MetricsPage.tsx | 329 ++++++++++-------- .../components/alerting/AlertsDetailsPage.tsx | 10 +- .../components/alerting/SilenceCreatePage.tsx | 5 +- .../console-shared/error/error-boundary.tsx | 47 +-- web/src/components/console/graphs/bar.tsx | 9 +- .../components/console/utils/button-bar.jsx | 70 ---- web/src/components/console/utils/router.ts | 66 ---- .../dashboards/legacy/bar-chart.tsx | 6 +- .../legacy/dashboard-skeleton-legacy.tsx | 4 +- .../legacy/legacy-dashboard-page.tsx | 12 +- .../dashboards/legacy/legacy-dashboard.tsx | 17 +- .../legacy/legacy-variable-dropdowns.tsx | 29 +- .../dashboards/legacy/useLegacyDashboards.ts | 104 ++++-- .../legacy/useLegacyDashboardsProject.tsx | 59 ++++ .../perses/hooks/useDashboardsData.ts | 77 ++-- web/src/components/hooks/usePerspective.tsx | 23 +- web/src/components/query-browser.tsx | 5 +- web/src/components/query-params.ts | 3 + .../redirects/prometheus-redirect-page.tsx | 5 +- 23 files changed, 478 insertions(+), 465 deletions(-) delete mode 100644 Dockerfile.konflux delete mode 100644 web/src/components/console/utils/button-bar.jsx delete mode 100644 web/src/components/console/utils/router.ts create mode 100644 web/src/components/dashboards/legacy/useLegacyDashboardsProject.tsx diff --git a/Dockerfile b/Dockerfile index c0e7f1bc7..3a1e65f8f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ ENV HUSKY=0 COPY web/package.json web/package-lock.json web/ COPY Makefile Makefile -RUN make install-frontend +RUN make install-frontend-ci COPY web/ web/ RUN make build-frontend diff --git a/Dockerfile.dev-mcp b/Dockerfile.dev-mcp index b2df023e2..d3791c004 100644 --- a/Dockerfile.dev-mcp +++ b/Dockerfile.dev-mcp @@ -16,7 +16,7 @@ RUN make update-plugin-name ENV I18N_NAMESPACE="plugin__monitoring-console-plugin" -RUN make install-frontend +RUN make install-frontend-ci RUN make build-frontend FROM brew.registry.redhat.io/rh-osbs/openshift-golang-builder:rhel_9_golang_1.24 as go-builder diff --git a/Dockerfile.konflux b/Dockerfile.konflux deleted file mode 100644 index ba20c4237..000000000 --- a/Dockerfile.konflux +++ /dev/null @@ -1,57 +0,0 @@ -FROM registry.redhat.io/ubi9/nodejs-22:9.6-1755075210 AS web-builder - -WORKDIR /opt/app-root - -USER 0 - -ENV HUSKY=0 -COPY web/package*.json web/ -COPY Makefile Makefile -RUN make install-frontend-ci - -COPY web/ web/ -COPY config/ config/ -COPY scripts/update-plugin-name.sh scripts/update-plugin-name.sh -RUN make update-plugin-name -ENV I18N_NAMESPACE="plugin__monitoring-console-plugin" -RUN make build-frontend - -FROM brew.registry.redhat.io/rh-osbs/openshift-golang-builder:rhel_9_1.24 as go-builder - -WORKDIR /opt/app-root - -COPY Makefile Makefile -COPY go.mod go.mod -COPY go.sum go.sum - -RUN make install-backend - -COPY cmd/ cmd/ -COPY pkg/ pkg/ - -ENV GOEXPERIMENT=strictfipsruntime -ENV CGO_ENABLED=1 - -RUN make build-backend BUILD_OPTS="-tags strictfipsruntime" - -FROM registry.redhat.io/rhel9-2-els/rhel:9.2 - -RUN mkdir /licenses -COPY LICENSE /licenses/. - -USER 1001 - -COPY --from=web-builder /opt/app-root/web/dist /opt/app-root/web/dist -COPY --from=web-builder /opt/app-root/config /opt/app-root/config -COPY --from=go-builder /opt/app-root/plugin-backend /opt/app-root - -ENTRYPOINT ["/opt/app-root/plugin-backend", "-static-path", "/opt/app-root/web/dist", "-config-path", "/opt/app-root/config"] - -LABEL com.redhat.component="coo-monitoring-console-plugin" \ - name="openshift/monitoring-console-plugin" \ - version="v0.3.0" \ - summary="OpenShift monitoring plugin to view and explore metrics and alerts" \ - io.openshift.tags="openshift,observability-ui,metrics,alerts" \ - io.k8s.display-name="OpenShift console monitoring plugin" \ - maintainer="Observability UI Team " \ - description="OpenShift monitoring plugin to view and explore metrics and alerts" diff --git a/devspace.yaml b/devspace.yaml index 2c263ea28..c69072ee2 100644 --- a/devspace.yaml +++ b/devspace.yaml @@ -36,7 +36,7 @@ dev: # Use the instance selector that CMO & helm add app.kubernetes.io/component: monitoring-plugin # Replace the container image with this dev-optimized image (allows to skip image building during development) - devImage: quay.io/rh-ee-pyurkovi/monitoring-plugin:devspace-2 + devImage: quay.io/rh-ee-pyurkovi/monitoring-plugin:v4.19-devspace # Sync files between the local filesystem and the development container sync: - path: ./web/dist:/opt/app-root/web/dist diff --git a/web/src/components/MetricsPage.tsx b/web/src/components/MetricsPage.tsx index 05f19a8cb..d6c8325b4 100644 --- a/web/src/components/MetricsPage.tsx +++ b/web/src/components/MetricsPage.tsx @@ -85,11 +85,6 @@ import { import { getPrometheusBasePath, buildPrometheusUrl } from './utils'; import { AsyncComponent } from './console/utils/async'; import { usePoll } from './console/utils/poll-hook'; -import { - getAllQueryArguments, - getQueryArgument, - setAllQueryArguments, -} from './console/utils/router'; import { useSafeFetch } from './console/utils/safe-fetch-hook'; import { @@ -123,6 +118,7 @@ import { MonitoringProvider } from '../contexts/MonitoringContext'; import { DataTestIDs } from './data-test'; import { useMonitoring } from '../hooks/useMonitoring'; import { useMonitoringNamespace } from './hooks/useMonitoringNamespace'; +import { useSearchParams } from 'react-router-dom-v5-compat'; // Stores information about the currently focused query input let focusedQuery; @@ -130,12 +126,10 @@ let focusedQuery; const predefinedQueriesAdmin: SelectOptionProps[] = [ { name: 'CPU Usage', - // eslint-disable-next-line max-len value: `sum(node_namespace_pod_container:container_cpu_usage_seconds_total:sum_irate) by (pod)`, }, { name: 'Memory Usage', - // eslint-disable-next-line max-len value: `sum(container_memory_working_set_bytes{container!=""}) by (pod)`, }, { @@ -710,135 +704,170 @@ export const QueryTable: FC = ({ index, namespace, customDataso setSortBy({}); }, [namespace, query]); - if (!isEnabled || !isExpanded || !query) { - return null; - } - - if (error) { - return ; - } - - if (!data) { - return ; - } - - // Add any data series from `series` (those displayed in the graph) that are not in `data.result`. - // This happens for queries that exclude a series currently, but included that same series at some - // point during the graph's range. - const expiredSeries = _.differenceWith(series, data.result, (s, r) => _.isEqual(s, r.metric)); - const result = expiredSeries.length - ? [...data.result, ...expiredSeries.map((metric) => ({ metric }))] - : data.result; - - if (!result || result.length === 0) { - return ( -
- {t('No datapoints found.')} -
- ); - } + const isUnused = !isEnabled || !isExpanded || !query; + const isError = !!error; + const isLoading = !data; + const result = useMemo(() => { + if (isUnused || isError || isLoading) { + return []; + } + // Add any data series from `series` (those displayed in the graph) that are not + // in `data.result`.This happens for queries that exclude a series currently, but + // included that same series at some point during the graph's range. + const expiredSeries = _.differenceWith(series, data.result, (s, r) => _.isEqual(s, r.metric)); + return expiredSeries.length + ? [...data.result, ...expiredSeries.map((metric) => ({ metric }))] + : data.result; + }, [data?.result, series, isUnused, isError, isLoading]); + const isEmptyGraph = !result || result.length === 0; + + const tableData = useMemo(() => { + if (isUnused || isError || isLoading || isEmptyGraph) { + return {}; + } + const transforms: ITransform[] = [sortable, wrappable]; - const transforms: ITransform[] = [sortable, wrappable]; - - const buttonCell = (labels) => ({ title: }); - - let columns, rows; - if (data.resultType === 'scalar') { - columns = [ - '', - { - title: t('Value'), - transforms, - cellTransforms: [ - (data: IFormatterValueType) => { - const val = data?.title ? data.title : data; - return !Number.isNaN(Number(val)) ? valueFormat(Number(val)) : val; - }, - ], - }, - ]; - rows = [[buttonCell({}), _.get(result, '[1]')]]; - } else if (data.resultType === 'string') { - columns = [ - { - title: t('Value'), - transforms, - cellTransforms: [ - (data: IFormatterValueType) => { - const val = data?.title ? data.title : data; - return !Number.isNaN(Number(val)) ? valueFormat(Number(val)) : val; - }, - ], - }, - ]; - rows = [[result?.[1]]]; - } else { - const allLabelKeys = _.uniq(_.flatMap(result, ({ metric }) => Object.keys(metric))).sort(); - - columns = [ - '', - ...allLabelKeys.map((k) => ({ - title: {k === '__name__' ? t('Name') : k}, - transforms, - })), - { - title: t('Value'), - transforms, - cellTransforms: [ - (data: IFormatterValueType) => { - const val = data?.title ? data.title : data; - return !Number.isNaN(Number(val)) ? valueFormat(Number(val)) : val; - }, - ], - }, - ]; + const buttonCell = (labels) => ({ title: }); - let rowMapper; - if (data.resultType === 'matrix') { - rowMapper = ({ metric, values }) => [ + let columns, rows; + if (data.resultType === 'scalar') { + columns = [ '', - ..._.map(allLabelKeys, (k) => metric[k]), { - title: ( - <> - {_.map(values, ([time, v]) => ( -
- {v} @{time} -
- ))} - - ), + title: t('Value'), + transforms, + cellTransforms: [ + (data: IFormatterValueType) => { + const val = data?.title ? data.title : data; + return !Number.isNaN(Number(val)) ? valueFormat(Number(val)) : val; + }, + ], + }, + ]; + rows = [[buttonCell({}), _.get(result, '[1]')]]; + } else if (data.resultType === 'string') { + columns = [ + { + title: t('Value'), + transforms, + cellTransforms: [ + (data: IFormatterValueType) => { + const val = data?.title ? data.title : data; + return !Number.isNaN(Number(val)) ? valueFormat(Number(val)) : val; + }, + ], }, ]; + rows = [[result?.[1]]]; } else { - rowMapper = ({ metric, value }) => [ - buttonCell(metric), - ..._.map(allLabelKeys, (k) => metric[k]), - _.get(value, '[1]', { title: {t('None')} }), + const allLabelKeys = _.uniq(_.flatMap(result, ({ metric }) => Object.keys(metric))).sort(); + + columns = [ + '', + ...allLabelKeys.map((k) => ({ + title: {k === '__name__' ? t('Name') : k}, + transforms, + })), + { + title: t('Value'), + transforms, + cellTransforms: [ + (data: IFormatterValueType) => { + const val = data?.title ? data.title : data; + return !Number.isNaN(Number(val)) ? valueFormat(Number(val)) : val; + }, + ], + }, ]; - } - rows = _.map(result, rowMapper); - if (sortBy) { - // Sort Values column numerically and sort all the other columns alphabetically - const valuesColIndex = allLabelKeys.length + 1; - const sort = - sortBy.index === valuesColIndex - ? (cells) => { - const v = Number(cells[valuesColIndex]); - return Number.isNaN(v) ? 0 : v; - } - : `${sortBy.index}`; - rows = _.orderBy(rows, [sort], [sortBy.direction]); + let rowMapper; + if (data.resultType === 'matrix') { + rowMapper = ({ metric, values }) => [ + '', + ..._.map(allLabelKeys, (k) => metric[k]), + { + title: ( + <> + {_.map(values, ([time, v]) => ( +
+ {v} @{time} +
+ ))} + + ), + }, + ]; + } else { + rowMapper = ({ metric, value }) => [ + buttonCell(metric), + ..._.map(allLabelKeys, (k) => metric[k]), + _.get(value, '[1]', { title: {t('None')} }), + ]; + } + + rows = _.map(result, rowMapper); + if (sortBy) { + // Sort Values column numerically and sort all the other columns alphabetically + const valuesColIndex = allLabelKeys.length + 1; + const sort = + sortBy.index === valuesColIndex + ? (cells) => { + const v = Number(cells[valuesColIndex]); + return Number.isNaN(v) ? 0 : v; + } + : `${sortBy.index}`; + rows = _.orderBy(rows, [sort], [sortBy.direction]); + } } - } - // Dispatch query table result so QueryKebab can access it for data export - dispatch(queryBrowserPatchQuery(index, { queryTableData: { columns, rows } })); + const onSort = (e, i, direction) => setSortBy({ index: i, direction }); - const onSort = (e, i, direction) => setSortBy({ index: i, direction }); + const tableRows = rows.slice((page - 1) * perPage, page * perPage).map((cells) => ({ cells })); - const tableRows = rows.slice((page - 1) * perPage, page * perPage).map((cells) => ({ cells })); + return { + onSort, + tableRows, + columns, + rows, + }; + }, [ + data?.resultType, + isEmptyGraph, + index, + isUnused, + isError, + isLoading, + page, + perPage, + result, + sortBy, + t, + valueFormat, + ]); + + useEffect(() => { + if (tableData.columns && tableData.rows) { + dispatch( + queryBrowserPatchQuery(index, { + queryTableData: { columns: tableData.columns, rows: tableData.rows }, + }), + ); + } + }, [dispatch, index, tableData?.columns, tableData?.rows]); + + if (isUnused) { + return null; + } else if (isError) { + return ; + } else if (isLoading) { + return ; + } else if (isEmptyGraph) { + return ( +
+ {t('No datapoints found.')} +
+ ); + } return ( <> @@ -854,19 +883,19 @@ export const QueryTable: FC = ({ index, namespace, customDataso - {columns.map((col, columnIndex) => { + {tableData?.columns.map((col, columnIndex) => { const sortParams = columnIndex !== 0 ? { sort: { sortBy, - onSort, + onSort: tableData?.onSort, columnIndex, }, } @@ -880,15 +909,15 @@ export const QueryTable: FC = ({ index, namespace, customDataso - {tableRows.map((row, rowIndex) => ( + {tableData?.tableRows.map((row, rowIndex) => ( {row.cells?.map((cell, cellIndex) => (
- {columns[cellIndex].cellTransforms - ? columns[cellIndex].cellTransforms[0]( + {tableData?.columns[cellIndex].cellTransforms + ? tableData?.columns[cellIndex].cellTransforms[0]( typeof cell === 'string' ? cell : cell?.title, ) : typeof cell === 'string' @@ -902,7 +931,7 @@ export const QueryTable: FC = ({ index, namespace, customDataso
getObserveState(plugin, state).queryBrowser?.queries, ); - // Initialize queries from URL parameters + // Initialize queries from URL parameters on first render useEffect(() => { + if (!isFirstRender) { + return; + } dispatch(queryBrowserDeleteAllQueries()); - const searchParams = getAllQueryArguments(); - for (let i = 0; _.has(searchParams, `query${i}`); i++) { - const query = searchParams[`query${i}`]; + for (let i = 0; queryParams.has(`query${i}`); i++) { + const query = queryParams.get(`query${i}`); dispatch( queryBrowserPatchQuery(i, { isEnabled: true, @@ -1082,7 +1115,8 @@ const QueryBrowserWrapper: FC<{ }), ); } - }, [dispatch]); + setFirstRenderFalse(); + }, [dispatch, queryParams, isFirstRender, setFirstRenderFalse]); /* eslint-disable react-hooks/exhaustive-deps */ // Use React.useMemo() to prevent these two arrays being recreated on every render, which would @@ -1099,13 +1133,16 @@ const QueryBrowserWrapper: FC<{ // Update the URL parameters when the queries shown in the graph change useEffect(() => { - const newParams: Record = {}; - _.each(queryStrings, (q, i) => (newParams[`query${i}`] = q || '')); + if (isFirstRender) { + return; + } + const newParams = new URLSearchParams(removeQueryKeys(queryParams)); + queryStrings.forEach((query, i) => newParams.set(`query${i}`, query || '')); if (customDataSourceName) { - newParams.datasource = customDataSourceName; + newParams.set(QueryParams.Datasource, customDataSourceName); } - setAllQueryArguments(newParams); - }, [queryStrings, customDataSourceName]); + setQueryParams(newParams); + }, [queryStrings, customDataSourceName, isFirstRender, queryParams]); if (hideGraphs) { return null; @@ -1178,6 +1215,16 @@ const QueryBrowserWrapper: FC<{ ); }; +const removeQueryKeys = (searchParams: URLSearchParams): URLSearchParams => { + const newParams = new URLSearchParams(searchParams); + for (const key of searchParams.keys()) { + if (key.startsWith('query')) { + newParams.delete(key); + } + } + return newParams; +}; + const AddQueryButton: FC = () => { const { t } = useTranslation(process.env.I18N_NAMESPACE); @@ -1288,6 +1335,7 @@ const GraphUnitsDropDown: FC = () => { const MetricsPage_: FC = () => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const [units, setUnits] = useQueryParam(QueryParams.Units, StringParam); + const [customDataSourceName] = useQueryParam(QueryParams.Datasource, StringParam); const { setNamespace } = useMonitoringNamespace(); const { displayNamespaceSelector } = useMonitoring(); @@ -1307,7 +1355,6 @@ const MetricsPage_: FC = () => { }, [dispatch]); const [customDataSource, setCustomDataSource] = useState(undefined); const [customDataSourceIsResolved, setCustomDataSourceIsResolved] = useState(false); - const customDataSourceName = getQueryArgument(QueryParams.Datasource); const [extensions, extensionsResolved] = useResolvedExtensions(isDataSource); const hasExtensions = !_.isEmpty(extensions); const [customDatasourceError, setCustomDataSourceError] = useState(false); diff --git a/web/src/components/alerting/AlertsDetailsPage.tsx b/web/src/components/alerting/AlertsDetailsPage.tsx index 1db080929..af9859b0c 100644 --- a/web/src/components/alerting/AlertsDetailsPage.tsx +++ b/web/src/components/alerting/AlertsDetailsPage.tsx @@ -16,9 +16,8 @@ import type { FC, ReactNode } from 'react'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { Link, useNavigate, useParams } from 'react-router-dom-v5-compat'; +import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom-v5-compat'; import { ExternalLink, LinkifyExternal } from '../console/utils/link'; -import { getAllQueryArguments } from '../console/utils/router'; import { getAlertsUrl, getObserveState, @@ -97,6 +96,7 @@ const AlertsDetailsPage_: FC = () => { const navigate = useNavigate(); const { plugin } = useMonitoring(); const { namespace } = useMonitoringNamespace(); + const [queryParams] = useSearchParams(); const { perspective } = usePerspective(); @@ -111,8 +111,9 @@ const AlertsDetailsPage_: FC = () => { // Search for an alert that matches all of the labels in the URL parameters. We expect there to be // only one such alert that matches, so don't display any alert if multiple matches were found. - const queryParams = getAllQueryArguments(); - const foundAlerts = _.filter(ruleAlerts, (a) => _.isMatch(a.labels, queryParams)); + const foundAlerts = _.filter(ruleAlerts, (a) => + _.isMatch(a.labels, Object.fromEntries(queryParams)), + ); const alert = foundAlerts.length === 1 ? foundAlerts[0] : undefined; const state = alertState(alert); @@ -121,7 +122,6 @@ const AlertsDetailsPage_: FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps const labels: PrometheusLabels = useMemo(() => alert?.labels, [labelsMemoKey]); - // eslint-disable-next-line camelcase const runbookURL = alert?.annotations?.runbook_url; const sourceId = rule?.sourceId; diff --git a/web/src/components/alerting/SilenceCreatePage.tsx b/web/src/components/alerting/SilenceCreatePage.tsx index 8058ff469..625c98107 100644 --- a/web/src/components/alerting/SilenceCreatePage.tsx +++ b/web/src/components/alerting/SilenceCreatePage.tsx @@ -1,20 +1,21 @@ import * as _ from 'lodash-es'; import { useTranslation } from 'react-i18next'; -import { getAllQueryArguments } from '../console/utils/router'; import { SilenceForm } from './SilenceForm'; import { MonitoringProvider } from '../../contexts/MonitoringContext'; import { useMonitoring } from '../../hooks/useMonitoring'; import { LoadingBox } from '../console/console-shared/src/components/loading/LoadingBox'; import { useMonitoringNamespace } from '../hooks/useMonitoringNamespace'; +import { useSearchParams } from 'react-router-dom-v5-compat'; const CreateSilencePage = () => { const { accessCheckLoading, useAlertsTenancy } = useMonitoring(); const { t } = useTranslation(process.env.I18N_NAMESPACE); + const [queryParams] = useSearchParams(); // Set the activeNamespace to be the namespace query parameter if it is set useMonitoringNamespace(); - const matchers = _.map(getAllQueryArguments(), (value, name) => ({ + const matchers = _.map(Object.fromEntries(queryParams), (value, name) => ({ name, value, isRegex: false, diff --git a/web/src/components/console/console-shared/error/error-boundary.tsx b/web/src/components/console/console-shared/error/error-boundary.tsx index dd2f832ba..32d40fa98 100644 --- a/web/src/components/console/console-shared/error/error-boundary.tsx +++ b/web/src/components/console/console-shared/error/error-boundary.tsx @@ -1,9 +1,15 @@ -import type { ComponentType, FC } from 'react'; +import { ErrorBoundaryFallbackProps } from '@openshift-console/dynamic-plugin-sdk'; +import type { ComponentType, ReactNode, FC } from 'react'; import { Component } from 'react'; -import { history } from '../../utils/router'; +import { useLocation } from 'react-router-dom-v5-compat'; type ErrorBoundaryProps = { FallbackComponent?: ComponentType; + children?: ReactNode; +}; + +type ErrorBoundaryInnerProps = ErrorBoundaryProps & { + locationPathname?: string; }; /** Needed for tests -- should not be imported by application logic */ @@ -15,10 +21,7 @@ export type ErrorBoundaryState = { const DefaultFallback: FC = () =>
; -class ErrorBoundary extends Component { - // eslint-disable-next-line - unlisten: () => void = () => {}; - +class ErrorBoundaryInner extends Component { readonly defaultState: ErrorBoundaryState = { hasError: false, error: { @@ -36,15 +39,15 @@ class ErrorBoundary extends Component { this.state = this.defaultState; } - componentDidMount() { - this.unlisten = history.listen(() => { - // reset state to default when location changes + componentDidUpdate(prevProps: ErrorBoundaryInnerProps) { + // Reset error state when location changes + if ( + this.state.hasError && + prevProps.locationPathname && + this.props.locationPathname !== prevProps.locationPathname + ) { this.setState(this.defaultState); - }); - } - - componentWillUnmount() { - this.unlisten(); + } } componentDidCatch(error, errorInfo) { @@ -74,11 +77,15 @@ class ErrorBoundary extends Component { } } -export default ErrorBoundary; +// Functional wrapper to handle location changes +const ErrorBoundary: FC = ({ children, FallbackComponent }) => { + const location = useLocation(); -type ErrorBoundaryFallbackProps = { - errorMessage: string; - componentStack: string; - stack: string; - title: string; + return ( + + {children} + + ); }; + +export default ErrorBoundary; diff --git a/web/src/components/console/graphs/bar.tsx b/web/src/components/console/graphs/bar.tsx index 53ab2f8f6..aa65dc8f9 100644 --- a/web/src/components/console/graphs/bar.tsx +++ b/web/src/components/console/graphs/bar.tsx @@ -49,11 +49,12 @@ const barTheme = { }, }; +const Label: FC = ({ metric }) => <>{Object.values(metric).join()}; + const BarChart: FC = ({ barSpacing = 15, barWidth = DEFAULT_BAR_WIDTH, data = [], - LabelComponent, loading = false, noLink = false, query, @@ -85,11 +86,7 @@ const BarChart: FC = ({ data.map((datum, index) => (
- {LabelComponent ? ( - - ) : ( - datum.x - )} +
{ - return Children.map(children, (c) => { - if (!_.isObject(c) || c.type !== 'button') { - return c; - } - - return cloneElement(c, { disabled: c.props.disabled || disabled }); - }); -}; - -const ErrorMessage = ({ message }) => { - const { t } = useTranslation(process.env.I18N_NAMESPACE); - return ( - - - - - {message} - - - - - ); -}; -const InfoMessage = ({ message }) => ( - -); -const SuccessMessage = ({ message }) => ; - -export const ButtonBar = ({ - children, - className, - errorMessage, - infoMessage, - successMessage, - inProgress, -}) => { - return ( -
- - {successMessage && } - {errorMessage && } - {injectDisabled(children, inProgress)} - {inProgress && } - {infoMessage && } - -
- ); -}; - -ButtonBar.propTypes = { - children: PropTypes.node.isRequired, - successMessage: PropTypes.string, - errorMessage: PropTypes.node, - infoMessage: PropTypes.string, - inProgress: PropTypes.bool, - className: PropTypes.string, -}; diff --git a/web/src/components/console/utils/router.ts b/web/src/components/console/utils/router.ts deleted file mode 100644 index 170f84932..000000000 --- a/web/src/components/console/utils/router.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as _ from 'lodash-es'; -import { createBrowserHistory, History } from 'history'; - -export const history: History = createBrowserHistory(); - -export const getQueryArgument = (arg: string) => - new URLSearchParams(window.location.search).get(arg); - -export const getAllQueryArguments = () => { - const all: { [key: string]: string } = {}; - const params = new URLSearchParams(window.location.search); - - for (const [k, v] of params.entries()) { - all[k] = v; - } - - return all; -}; - -export const setQueryArgument = (k: string, v: string) => { - const params = new URLSearchParams(window.location.search); - if (params.get(k) !== v) { - params.set(k, v); - const url = new URL(window.location.href); - history.replace(`${url.pathname}?${params.toString()}${url.hash}`); - } -}; - -export const setQueryArguments = (newParams: { [k: string]: string }) => { - const params = new URLSearchParams(window.location.search); - let update = false; - _.each(newParams, (v, k) => { - if (params.get(k) !== v) { - update = true; - params.set(k, v); - } - }); - if (update) { - const url = new URL(window.location.href); - history.replace(`${url.pathname}?${params.toString()}${url.hash}`); - } -}; - -export const setAllQueryArguments = (newParams: { [k: string]: string }) => { - const params = new URLSearchParams(); - let update = false; - _.each(newParams, (v, k) => { - if (params.get(k) !== v) { - update = true; - params.set(k, v); - } - }); - if (update) { - const url = new URL(window.location.href); - history.replace(`${url.pathname}?${params.toString()}${url.hash}`); - } -}; - -export const removeQueryArgument = (k: string) => { - const params = new URLSearchParams(window.location.search); - if (params.has(k)) { - params.delete(k); - const url = new URL(window.location.href); - history.replace(`${url.pathname}?${params.toString()}${url.hash}`); - } -}; diff --git a/web/src/components/dashboards/legacy/bar-chart.tsx b/web/src/components/dashboards/legacy/bar-chart.tsx index d0b4e1ce1..9dae8b041 100644 --- a/web/src/components/dashboards/legacy/bar-chart.tsx +++ b/web/src/components/dashboards/legacy/bar-chart.tsx @@ -1,18 +1,14 @@ -import * as _ from 'lodash-es'; import type { FC } from 'react'; -import { Bar, LabelComponentProps } from '../../console/graphs/bar'; +import { Bar } from '../../console/graphs/bar'; import { CustomDataSource } from '@openshift-console/dynamic-plugin-sdk/lib/extensions/dashboard-data-source'; -const Label: FC = ({ metric }) => <>{_.values(metric).join()}; - const BarChart: FC = ({ customDataSource, pollInterval, query }) => ( ); diff --git a/web/src/components/dashboards/legacy/dashboard-skeleton-legacy.tsx b/web/src/components/dashboards/legacy/dashboard-skeleton-legacy.tsx index 681d5b510..e6bc610af 100644 --- a/web/src/components/dashboards/legacy/dashboard-skeleton-legacy.tsx +++ b/web/src/components/dashboards/legacy/dashboard-skeleton-legacy.tsx @@ -29,7 +29,7 @@ const HeaderTop: FC = memo(() => { type MonitoringDashboardsLegacyPageProps = PropsWithChildren<{ boardItems: CombinedDashboardMetadata[]; - changeBoard: (dashboardName: string) => void; + changeBoard: (params: { newBoard?: string; initialLoad?: boolean; newProject?: string }) => void; dashboardName: string; }>; @@ -39,7 +39,7 @@ export const DashboardSkeletonLegacy: FC = const onChangeBoard = useCallback( (selectedDashboard: string) => { - changeBoard(selectedDashboard); + changeBoard({ newBoard: selectedDashboard }); }, [changeBoard], ); diff --git a/web/src/components/dashboards/legacy/legacy-dashboard-page.tsx b/web/src/components/dashboards/legacy/legacy-dashboard-page.tsx index 7fa810db0..c52eb0a7c 100644 --- a/web/src/components/dashboards/legacy/legacy-dashboard-page.tsx +++ b/web/src/components/dashboards/legacy/legacy-dashboard-page.tsx @@ -10,7 +10,6 @@ import ErrorAlert from './error'; import { DashboardSkeletonLegacy } from './dashboard-skeleton-legacy'; import { useLegacyDashboards } from './useLegacyDashboards'; import { MonitoringProvider } from '../../../contexts/MonitoringContext'; -import { useMonitoringNamespace } from '../../hooks/useMonitoringNamespace'; import { useMonitoring } from '../../../hooks/useMonitoring'; import { StringParam, useQueryParam } from 'use-query-params'; import { QueryParams } from '../../../components/query-params'; @@ -20,7 +19,6 @@ type LegacyDashboardsPageProps = { }; const LegacyDashboardsPage_: FC = ({ urlBoard }) => { - const { namespace, setNamespace } = useMonitoringNamespace(); const { legacyDashboardsError, legacyRows, @@ -28,14 +26,20 @@ const LegacyDashboardsPage_: FC = ({ urlBoard }) => { legacyDashboardsMetadata, changeLegacyDashboard, legacyDashboard, - } = useLegacyDashboards(namespace, urlBoard); + } = useLegacyDashboards(urlBoard); const { perspective } = usePerspective(); const { displayNamespaceSelector } = useMonitoring(); const { t } = useTranslation(process.env.I18N_NAMESPACE); return ( <> - {displayNamespaceSelector && setNamespace(ns)} />} + {displayNamespaceSelector && ( + { + changeLegacyDashboard({ newProject: ns }); + }} + /> + )} = memo(({ panel, perspective }) => { const [isChartLoading, setIsChartLoading] = useState(panel.type === 'graph'); const customDataSourceName = panel.datasource?.name; const [extensions, extensionsResolved] = useResolvedExtensions(isDataSource); + const [, setEndTimeParam] = useQueryParam(QueryParams.EndTime, StringParam); + const [, setTimeRangeParam] = useQueryParam(QueryParams.TimeRange, StringParam); const hasExtensions = !_.isEmpty(extensions); const formatSeriesTitle = useCallback( @@ -249,12 +250,10 @@ const Card: FC = memo(({ panel, perspective }) => { }); }, [extensions, extensionsResolved, customDataSourceName, hasExtensions]); - const handleZoom = useCallback((timeRange: number, endTime: number) => { - setQueryArguments({ - [QueryParams.EndTime]: endTime.toString(), - [QueryParams.TimeRange]: timeRange.toString(), - }); - }, []); + const handleZoom = (timeRange: number, endTime: number) => { + setEndTimeParam(endTime.toString()); + setTimeRangeParam(timeRange.toString()); + }; const panelBreakpoints = useMemo(() => { const panelSpan = getPanelSpan(panel); diff --git a/web/src/components/dashboards/legacy/legacy-variable-dropdowns.tsx b/web/src/components/dashboards/legacy/legacy-variable-dropdowns.tsx index 45a4abc40..b9fa73d24 100644 --- a/web/src/components/dashboards/legacy/legacy-variable-dropdowns.tsx +++ b/web/src/components/dashboards/legacy/legacy-variable-dropdowns.tsx @@ -24,12 +24,11 @@ import { useDispatch, useSelector } from 'react-redux'; import { SingleTypeaheadDropdown } from '../../console/utils/single-typeahead-dropdown'; import { getPrometheusBasePath, buildPrometheusUrl, ALL_NAMESPACES_KEY } from '../../utils'; -import { getQueryArgument, setQueryArgument } from '../../console/utils/router'; import { useSafeFetch } from '../../console/utils/safe-fetch-hook'; import { dashboardsPatchVariable, dashboardsVariableOptionsLoaded } from '../../../store/actions'; import { getTimeRanges, isTimeoutError, QUERY_CHUNK_SIZE } from '../../utils'; -import { getObserveState } from '../../hooks/usePerspective'; +import { getObserveState, usePerspective } from '../../hooks/usePerspective'; import { MonitoringState } from '../../../store/store'; import { DEFAULT_GRAPH_SAMPLES, MONITORING_DASHBOARDS_VARIABLE_ALL_OPTION_KEY } from './utils'; import { @@ -38,6 +37,7 @@ import { } from '@openshift-console/dynamic-plugin-sdk/lib/extensions/dashboard-data-source'; import { useMonitoring } from '../../../hooks/useMonitoring'; import { useDeepMemo } from '../../hooks/useDeepMemo'; +import { StringParam, useQueryParam } from 'use-query-params'; const intervalVariableRegExps = ['__interval', '__rate_interval', '__auto_interval_[a-z]+']; @@ -58,10 +58,8 @@ export const evaluateVariableTemplate = ( const allVariables = { ...variables, __range: range, - /* eslint-disable camelcase */ __range_ms: range, __range_s: range, - /* eslint-enable camelcase */ }; // Handle the special "interval" variables @@ -110,7 +108,9 @@ const LegacyDashboardsVariableOption = ({ value, isSelected, ...rest }) => const LegacyDashboardsVariableDropdown: FC = ({ id, name }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const { plugin, accessCheckLoading, useMetricsTenancy } = useMonitoring(); + const { perspective } = usePerspective(); const [namespace] = useActiveNamespace(); + const [queryParam, setQueryParam] = useQueryParam(name, StringParam); const timespan = useSelector( (state: MonitoringState) => getObserveState(plugin, state).dashboards.timespan, @@ -138,6 +138,9 @@ const LegacyDashboardsVariableDropdown: FC = ({ id, name const [extensions, extensionsResolved] = useResolvedExtensions(isDataSource); const hasExtensions = !_.isEmpty(extensions); + // Don't set namespace param while in dev perspective + const shouldSetQueryParam = !(perspective === 'dev' && name === 'namespace'); + const getURL = useCallback( async (prometheusProps) => { try { @@ -254,19 +257,25 @@ const LegacyDashboardsVariableDropdown: FC = ({ id, name ]); useEffect(() => { - if (variable?.value && variable?.value !== getQueryArgument(name)) { - setQueryArgument(name, variable?.value); + if (variable?.value !== queryParam) { + // Default to using the query param to allow for sharable links + if (queryParam) { + dispatch(dashboardsPatchVariable(name, { value: queryParam })); + // set the url if it isn't set + } else if (variable?.value && shouldSetQueryParam) { + setQueryParam(variable?.value); + } } - }, [name, variable?.value]); + }, [name, variable?.value, queryParam, setQueryParam, dispatch, shouldSetQueryParam]); const onChange = useCallback( (v: string) => { - if (v !== variable?.value) { - setQueryArgument(name, v); + if (v !== variable?.value && shouldSetQueryParam) { + setQueryParam(v); dispatch(dashboardsPatchVariable(name, { value: v })); } }, - [dispatch, name, variable?.value], + [dispatch, name, variable?.value, setQueryParam, shouldSetQueryParam], ); if (variable?.isHidden || (!isError && _.isEmpty(variable?.options))) { diff --git a/web/src/components/dashboards/legacy/useLegacyDashboards.ts b/web/src/components/dashboards/legacy/useLegacyDashboards.ts index 5eefaad06..5bd451d0a 100644 --- a/web/src/components/dashboards/legacy/useLegacyDashboards.ts +++ b/web/src/components/dashboards/legacy/useLegacyDashboards.ts @@ -2,7 +2,7 @@ import * as _ from 'lodash-es'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; -import { useNavigate } from 'react-router-dom-v5-compat'; +import { useNavigate, useSearchParams } from 'react-router-dom-v5-compat'; import { NumberParam, useQueryParam } from 'use-query-params'; import { DashboardsClearVariables, @@ -11,7 +11,6 @@ import { dashboardsSetPollInterval, dashboardsSetTimespan, } from '../../../store/actions'; -import { getAllQueryArguments, getQueryArgument } from '../../console/utils/router'; import { useSafeFetch } from '../../console/utils/safe-fetch-hook'; import { useBoolean } from '../../hooks/useBoolean'; import { getLegacyDashboardsUrl, usePerspective } from '../../hooks/usePerspective'; @@ -23,10 +22,12 @@ import { MONITORING_DASHBOARDS_DEFAULT_TIMESPAN, MONITORING_DASHBOARDS_VARIABLE_ALL_OPTION_KEY, } from './utils'; +import { useLegacyDashboardsProject } from './useLegacyDashboardsProject'; -export const useLegacyDashboards = (namespace: string, urlBoard: string) => { +export const useLegacyDashboards = (urlBoard: string) => { const { t } = useTranslation('plugin__monitoring-plugin'); const { perspective } = usePerspective(); + const { project } = useLegacyDashboardsProject(); // eslint-disable-next-line react-hooks/exhaustive-deps const safeFetch = useCallback(useSafeFetch(), []); @@ -37,6 +38,7 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { const [initialLoad, , , setInitialLoaded] = useBoolean(true); const dispatch = useDispatch(); const navigate = useNavigate(); + const [queryParams] = useSearchParams(); useEffect(() => { safeFetch('/api/console/monitoring-dashboard-config') @@ -56,7 +58,7 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { // Move namespace filtering out of the fetch response call to avoid race conditions const legacyDashboards = useMemo(() => { let items = unfilteredLegacyDashboards; - if (namespace && namespace !== ALL_NAMESPACES_KEY) { + if (project && project !== ALL_NAMESPACES_KEY) { items = _.filter( items, (item) => item.metadata?.labels['console.openshift.io/odc-dashboard'] === 'true', @@ -78,7 +80,7 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { } }; return _.sortBy(_.map(items, getBoardData), (v) => _.toLower(v?.data?.title)); - }, [namespace, unfilteredLegacyDashboards, setLegacyDashboardsError, t]); + }, [project, unfilteredLegacyDashboards, setLegacyDashboardsError, t]); const legacyRows = useMemo(() => { const data = _.find(legacyDashboards, { name: urlBoard })?.data; @@ -117,26 +119,51 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { }, [legacyDashboards, legacyDashboardsLoading]); const changeLegacyDashboard = useCallback( - (newBoard: string, forceRefresh = false) => { - if (!newBoard) { - // If the board is being cleared then don't do anything - return; - } - - const allVariables = getAllVariables(legacyDashboards, newBoard, namespace); + ({ + newBoard, + newProject, + initialLoad = false, + }: { + newBoard?: string; + newProject?: string; + initialLoad?: boolean; + }) => { + const dashboardProject = newProject ? newProject : project; - const queryArguments = getAllQueryArguments(); - const params = new URLSearchParams(queryArguments); + const url = getLegacyDashboardsUrl(perspective, newBoard, dashboardProject); - const url = getLegacyDashboardsUrl(perspective, newBoard, namespace); + let params: URLSearchParams; + if (initialLoad) { + params = new URLSearchParams(queryParams); + if (perspective === 'dev') { + params.delete(QueryParams.Namespace); + params.delete(QueryParams.OpenshiftProject); + } + } else { + params = new URLSearchParams(); + } - if (newBoard !== urlBoard || forceRefresh) { - if (!params.has(QueryParams.Dashboard) || params.get(QueryParams.Dashboard) !== newBoard) { + if (newBoard !== urlBoard || newProject !== project || initialLoad) { + if ( + perspective === 'dev' && + (!params.has(QueryParams.Dashboard) || params.get(QueryParams.Dashboard) !== newBoard) + ) { params.set(QueryParams.Dashboard, newBoard); - navigate(`${url}?${params.toString()}`, { replace: true }); } + if (perspective !== 'dev') { + if (params.get(QueryParams.OpenshiftProject) !== ALL_NAMESPACES_KEY) { + params.delete(QueryParams.Namespace); + } + params.set(QueryParams.OpenshiftProject, dashboardProject); + } + const srt = `${url}?${params.toString()}`; + navigate(srt, { replace: true }); - dispatch(dashboardsPatchAllVariables(allVariables)); + dispatch( + dashboardsPatchAllVariables( + getAllVariables(params, legacyDashboards, dashboardProject, newBoard), + ), + ); // Set time range and poll interval options to their defaults or from the query params if // available @@ -151,7 +178,16 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { ); } }, - [perspective, urlBoard, dispatch, navigate, namespace, legacyDashboards, refreshInterval], + [ + perspective, + urlBoard, + dispatch, + navigate, + project, + refreshInterval, + queryParams, + legacyDashboards, + ], ); useEffect(() => { @@ -161,10 +197,14 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { initialLoad) && !_.isEmpty(legacyDashboards) ) { - changeLegacyDashboard(urlBoard || legacyDashboards?.[0]?.name, initialLoad); + changeLegacyDashboard({ + newBoard: urlBoard || legacyDashboards?.[0]?.name, + initialLoad: initialLoad, + newProject: project, + }); setInitialLoaded(); } - }, [legacyDashboards, changeLegacyDashboard, initialLoad, setInitialLoaded, urlBoard]); + }, [legacyDashboards, changeLegacyDashboard, initialLoad, setInitialLoaded, urlBoard, project]); useEffect(() => { if (initialLoad || _.isEmpty(legacyDashboards)) { @@ -173,10 +213,13 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { const currentBoard = urlBoard || legacyDashboards?.[0]?.name; if (currentBoard) { - const allVariables = getAllVariables(legacyDashboards, currentBoard, namespace); - dispatch(dashboardsPatchAllVariables(allVariables)); + dispatch( + dashboardsPatchAllVariables( + getAllVariables(queryParams, legacyDashboards, project, currentBoard), + ), + ); } - }, [namespace, legacyDashboards, urlBoard, dispatch, initialLoad]); + }, [project, legacyDashboards, urlBoard, dispatch, initialLoad, queryParams]); // Clear variables on unmount useEffect(() => { @@ -196,14 +239,18 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { }; }; -const getAllVariables = (boards: Board[], newBoardName: string, namespace: string) => { +const getAllVariables = ( + params: URLSearchParams, + boards: Board[], + namespace: string, + newBoardName: string, +) => { const data = _.find(boards, { name: newBoardName })?.data; - const allVariables = {}; _.each(data?.templating?.list, (v) => { if (v.type === 'query' || v.type === 'interval') { // Look for query param that is equal to the variable name - let value = getQueryArgument(v.name); + let value = params.get(v.name); // Look for an option that should be selected by default if (value === null) { @@ -229,6 +276,5 @@ const getAllVariables = (boards: Board[], newBoardName: string, namespace: strin }; } }); - return allVariables; }; diff --git a/web/src/components/dashboards/legacy/useLegacyDashboardsProject.tsx b/web/src/components/dashboards/legacy/useLegacyDashboardsProject.tsx new file mode 100644 index 000000000..70f7b666a --- /dev/null +++ b/web/src/components/dashboards/legacy/useLegacyDashboardsProject.tsx @@ -0,0 +1,59 @@ +import { useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk'; +import { useEffect } from 'react'; +import { QueryParams } from '../../query-params'; +import { StringParam, useQueryParam } from 'use-query-params'; +import { useDispatch, useSelector } from 'react-redux'; +import { MonitoringState } from '../../../store/store'; +import { getObserveState, usePerspective } from '../../hooks/usePerspective'; +import { useMonitoring } from '../../../hooks/useMonitoring'; +import { useParams } from 'react-router-dom-v5-compat'; +import { dashboardsPatchVariable } from '../../../store/actions'; + +export const useLegacyDashboardsProject = () => { + const { perspective } = usePerspective(); + const [activeNamespace, setActiveNamespace] = useActiveNamespace(); + const { ns: routeNamespace } = useParams<{ ns?: string }>(); + 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 (perspective !== 'dev') { + if (!openshiftProject) { + setOpenshiftProject(activeNamespace); + } + } else { + if (routeNamespace && activeNamespace !== routeNamespace) { + setActiveNamespace(routeNamespace); + } + if (variableNamespace && variableNamespace !== routeNamespace) { + dispatch( + dashboardsPatchVariable('namespace', { + // Dashboards space variable shouldn't use the ALL_NAMESPACES_KEY + value: routeNamespace, + }), + ); + } + } + }, [ + activeNamespace, + setActiveNamespace, + openshiftProject, + setOpenshiftProject, + dispatch, + variableNamespace, + perspective, + routeNamespace, + ]); + + return { + project: perspective === 'dev' ? routeNamespace || activeNamespace : openshiftProject, + }; +}; diff --git a/web/src/components/dashboards/perses/hooks/useDashboardsData.ts b/web/src/components/dashboards/perses/hooks/useDashboardsData.ts index ca700d6a5..897ac4b9b 100644 --- a/web/src/components/dashboards/perses/hooks/useDashboardsData.ts +++ b/web/src/components/dashboards/perses/hooks/useDashboardsData.ts @@ -1,14 +1,14 @@ -import { useMemo, useCallback, useEffect } from 'react'; +import { useMemo, useCallback, useRef } from 'react'; import { DashboardResource } from '@perses-dev/core'; -import { useNavigate } from 'react-router-dom-v5-compat'; import { StringParam, useQueryParam } from 'use-query-params'; -import { getAllQueryArguments } from '../../../console/utils/router'; import { useBoolean } from '../../../hooks/useBoolean'; -import { getDashboardsUrl, usePerspective } from '../../../hooks/usePerspective'; +import { getDashboardUrl, usePerspective } from '../../../hooks/usePerspective'; import { QueryParams } from '../../../query-params'; import { useActiveProject } from '../project/useActiveProject'; import { usePerses } from './usePerses'; +import { useNavigate, useSearchParams } from 'react-router-dom-v5-compat'; +import { ALL_NAMESPACES_KEY } from '../../../utils'; // This hook syncs with mutliple external API's, redux, and URL state. Its a lot, but needs to all // be in a single location @@ -16,6 +16,7 @@ export const useDashboardsData = () => { const navigate = useNavigate(); const { perspective } = usePerspective(); const { activeProject, setActiveProject } = useActiveProject(); + const [queryParams] = useSearchParams(); // track initial page load to prevent a full page loading state when swapping dashboards // or projects @@ -39,13 +40,33 @@ export const useDashboardsData = () => { return true; }, [persesProjectsLoading, persesDashboardsLoading, initialPageLoad, setInitialPageLoadFalse]); + const prevDashboardsRef = useRef([]); + const prevMetadataRef = useRef([]); + // Homogenize data needed for dashboards dropdown between legacy and perses dashboards // to enable both to use the same component const combinedDashboardsMetadata = useMemo(() => { if (combinedInitialLoad) { return []; } - return persesDashboards.map((persesDashboard) => { + + // Check if dashboards data has actually changed to avoid recreation + const dashboardsChanged = + persesDashboards.length !== prevDashboardsRef.current.length || + persesDashboards.some((dashboard, i) => { + const prevDashboard = prevDashboardsRef.current[i]; + return ( + dashboard?.metadata?.name !== prevDashboard?.metadata?.name || + dashboard?.spec?.display?.name !== prevDashboard?.spec?.display?.name || + dashboard?.metadata?.project !== prevDashboard?.metadata?.project + ); + }); + + if (!dashboardsChanged && prevMetadataRef.current.length > 0) { + return prevMetadataRef.current; + } + + const newMetadata = persesDashboards.map((persesDashboard) => { const name = persesDashboard?.metadata?.name; const displayName = persesDashboard?.spec?.display?.name || name; @@ -57,10 +78,17 @@ export const useDashboardsData = () => { persesDashboard, }; }); + + prevDashboardsRef.current = persesDashboards; + prevMetadataRef.current = newMetadata; + return newMetadata; }, [persesDashboards, combinedInitialLoad]); // Retrieve dashboard metadata for the currently selected project const activeProjectDashboardsMetadata = useMemo(() => { + if (activeProject === ALL_NAMESPACES_KEY) { + return combinedDashboardsMetadata; + } return combinedDashboardsMetadata.filter((combinedDashboardMetadata) => { return combinedDashboardMetadata.project === activeProject; }); @@ -72,37 +100,36 @@ export const useDashboardsData = () => { // If the board is being cleared then don't do anything return; } - const queryArguments = getAllQueryArguments(); + const params = new URLSearchParams(queryParams); + + params.delete(QueryParams.Edit); - const params = new URLSearchParams(queryArguments); - params.set(QueryParams.Project, activeProject); + const dashboard = combinedDashboardsMetadata.find((item) => item.name === newBoard); + + const projectToUse = + activeProject === ALL_NAMESPACES_KEY ? dashboard?.project : activeProject; + + if (projectToUse) { + params.set(QueryParams.Project, projectToUse); + } params.set(QueryParams.Dashboard, newBoard); + if (dashboard?.persesDashboard?.spec?.duration) { + params.set(QueryParams.Start, dashboard.persesDashboard.spec.duration); + } + if (dashboard?.persesDashboard?.spec?.refreshInterval) { + params.set(QueryParams.Refresh, dashboard.persesDashboard.spec.refreshInterval); + } - let url = getDashboardsUrl(perspective); + let url = getDashboardUrl(perspective); url = `${url}?${params.toString()}`; if (newBoard !== dashboardName) { navigate(url, { replace: true }); } }, - [perspective, dashboardName, navigate, activeProject], + [perspective, dashboardName, navigate, activeProject, combinedDashboardsMetadata, queryParams], ); - // If a dashboard hasn't been selected yet, or if the current project doesn't have a - // matching board name then display the board present in the URL parameters or the first - // board in the dropdown list - useEffect(() => { - const metadataMatch = activeProjectDashboardsMetadata.find((activeProjectDashboardMetadata) => { - return ( - activeProjectDashboardMetadata.project === activeProject && - activeProjectDashboardMetadata.name === dashboardName - ); - }); - if (!dashboardName || !metadataMatch) { - changeBoard(activeProjectDashboardsMetadata?.[0]?.name); - } - }, [dashboardName, changeBoard, activeProject, activeProjectDashboardsMetadata]); - return { persesAvailable, persesProjectsLoading, diff --git a/web/src/components/hooks/usePerspective.tsx b/web/src/components/hooks/usePerspective.tsx index 8e70ec1f9..f3eb45d8a 100644 --- a/web/src/components/hooks/usePerspective.tsx +++ b/web/src/components/hooks/usePerspective.tsx @@ -14,7 +14,7 @@ import { } from '../utils'; import { GraphUnits } from '../metrics/units'; import { QueryParams } from '../query-params'; -import { MonitoringState } from 'src/store/store'; +import { MonitoringState } from '../../store/store'; export type UrlRoot = 'monitoring' | 'dev-monitoring' | 'multicloud/monitoring' | 'virt-monitoring'; @@ -277,7 +277,7 @@ export const getMutlipleQueryBrowserUrl = ( export const getLegacyDashboardsUrl = ( perspective: Perspective, - boardName: string, + boardName?: string, namespace?: string, ) => { switch (perspective) { @@ -286,14 +286,27 @@ export const getLegacyDashboardsUrl = ( case 'dev': return `/dev-monitoring/ns/${namespace}`; case 'virtualization-perspective': - return `/virt-monitoring/dashboards/${boardName}`; + return `/virt-monitoring/dashboards` + (boardName ? `/${boardName}` : ''); case 'admin': default: - return `/monitoring/dashboards/${boardName}`; + return `/monitoring/dashboards` + (boardName ? `/${boardName}` : ''); } }; -export const getDashboardsUrl = (perspective: Perspective) => { +export const getDashboardUrl = (perspective: Perspective) => { + switch (perspective) { + case 'virtualization-perspective': + return `/virt-monitoring/v2/dashboards/view`; + case 'admin': + return `/monitoring/v2/dashboards/view`; + case 'acm': + return `/multicloud/monitoring/v2/dashboards/view`; + default: + return ''; + } +}; + +export const getDashboardsListUrl = (perspective: Perspective) => { switch (perspective) { case 'virtualization-perspective': return `/virt-monitoring/v2/dashboards`; diff --git a/web/src/components/query-browser.tsx b/web/src/components/query-browser.tsx index b775bb1b2..aacdaa2a7 100644 --- a/web/src/components/query-browser.tsx +++ b/web/src/components/query-browser.tsx @@ -1025,10 +1025,9 @@ const QueryBrowser_: FC = ({ data-test={DataTestIDs.MetricGraph} > {error && } - {isGraphDataEmpty && !(hideControls && (updating || hasReceivedData.current)) && ( + {isGraphDataEmpty && !(hideControls && updating && hasReceivedData.current) ? ( - )} - {!isGraphDataEmpty && width > 0 && ( + ) : ( <> {disableZoom ? ( { const { urlRoot } = usePerspective(); - const params = getAllQueryArguments(); + const [params] = useSearchParams(); // leaving perspective redirect to future work return ; };