diff --git a/README.md b/README.md index 25b273ac..0d900f13 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ Feature flags are used by the mcp mode to dictate the specific features which ar |-------------------|-------------| | acm-alerting | 4.14+ | | perses-dashboards | 4.14+ | -| incidents | 4.17+ | +| incidents | 4.19+ | | dev-config | | #### ACM diff --git a/cmd/plugin-backend.go b/cmd/plugin-backend.go index d7dae2fc..513f8bdf 100644 --- a/cmd/plugin-backend.go +++ b/cmd/plugin-backend.go @@ -14,7 +14,7 @@ var ( portArg = flag.Int("port", 0, "server port to listen on (default: 9443)\nports 9444 and 9445 reserved for other use") certArg = flag.String("cert", "", "cert file path to enable TLS (disabled by default)") keyArg = flag.String("key", "", "private key file path to enable TLS (disabled by default)") - featuresArg = flag.String("features", "", "enabled features, comma separated.\noptions: ['acm-alerting', 'incidents', 'dev-config', 'perses-dashboards']") + featuresArg = flag.String("features", "", "enabled features, comma separated.\noptions: ['acm-alerting', 'dev-config', 'perses-dashboards']") staticPathArg = flag.String("static-path", "", "static files path to serve frontend (default: './web/dist')") configPathArg = flag.String("config-path", "", "config files path (default: './config')") pluginConfigArg = flag.String("plugin-config-path", "", "plugin yaml configuration") diff --git a/config/incidents.patch.json b/config/incidents.patch.json deleted file mode 100644 index 6f4cf154..00000000 --- a/config/incidents.patch.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "op": "add", - "path": "/extensions/1", - "value": { - "type": "console.page/route", - "properties": { - "exact": false, - "path": "/monitoring/incidents", - "component": { "$codeRef": "IncidentsPage" } - } - } - }, - { - "op": "add", - "path": "/extensions/1", - "value": { - "type": "console.navigation/href", - "flags": { - "required": ["PROMETHEUS", "MONITORING", "CAN_GET_NS"] - }, - "properties": { - "id": "incidents", - "name": "%plugin__monitoring-console-plugin~Incidents%", - "href": "/monitoring/incidents", - "perspective": "admin", - "section": "observe", - "insertAfter": "targets" - } - } - } -] diff --git a/pkg/server.go b/pkg/server.go index dc616aea..45dd02c8 100644 --- a/pkg/server.go +++ b/pkg/server.go @@ -45,7 +45,6 @@ type Feature string const ( AcmAlerting Feature = "acm-alerting" - Incidents Feature = "incidents" DevConfig Feature = "dev-config" PersesDashboards Feature = "perses-dashboards" ) diff --git a/scripts/build-image.sh b/scripts/build-image.sh index 9aedc1a8..74bd54b9 100755 --- a/scripts/build-image.sh +++ b/scripts/build-image.sh @@ -20,6 +20,9 @@ if [[ "$OSTYPE" == "darwin"* ]] && [[ "$DOCKER_FILE_NAME" == "Dockerfile.mcp" ]] make update-plugin-name export I18N_NAMESPACE='plugin__monitoring-console-plugin' + printf "${YELLOW}Installing Frontend${ENDCOLOR}\n" + make install-frontend + printf "${YELLOW}Building Frontend${ENDCOLOR}\n" make build-frontend fi diff --git a/web/package-lock.json b/web/package-lock.json index 88ea8c56..fff0c0d5 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -29,11 +29,11 @@ "@patternfly/react-core": "5.4.0", "@patternfly/react-icons": "^5.4.0", "@patternfly/react-table": "5.4.13", - "@perses-dev/components": "^0.50.0", - "@perses-dev/dashboards": "^0.50.0", - "@perses-dev/panels-plugin": "^0.50.0", - "@perses-dev/plugin-system": "^0.50.0", - "@perses-dev/prometheus-plugin": "^0.50.0", + "@perses-dev/components": "^0.50.3", + "@perses-dev/dashboards": "^0.50.3", + "@perses-dev/panels-plugin": "^0.50.3", + "@perses-dev/plugin-system": "^0.50.3", + "@perses-dev/prometheus-plugin": "^0.50.3", "@prometheus-io/codemirror-promql": "^0.37.0", "@tanstack/react-query": "^4.36.1", "classnames": "2.x", @@ -1547,9 +1547,9 @@ "license": "MIT" }, "node_modules/@perses-dev/components": { - "version": "0.50.0", - "resolved": "https://registry.npmjs.org/@perses-dev/components/-/components-0.50.0.tgz", - "integrity": "sha512-mi+0bNOZwzzZ9r9hgHvOYqpCk/WqZLAbCyI/U9oJ1b/WnF4zTMiZVjy3goyk6YufrUZY5rDWjnUYkUjCwJii6g==", + "version": "0.50.3", + "resolved": "https://registry.npmjs.org/@perses-dev/components/-/components-0.50.3.tgz", + "integrity": "sha512-X53/tFRPjZUgv6EWM3ot+5l82li/k4zBxbTnsQA2vb7YrOJvrUzidWf0+jHSxS/3ua8Tfd8HHxdGk1HhpoIftg==", "license": "Apache-2.0", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.4.0", @@ -1557,7 +1557,7 @@ "@codemirror/lang-json": "^6.0.1", "@fontsource/lato": "^4.5.10", "@mui/x-date-pickers": "^7.23.1", - "@perses-dev/core": "0.50.0", + "@perses-dev/core": "0.50.3", "@tanstack/react-table": "^8.20.5", "@uiw/react-codemirror": "^4.19.1", "date-fns": "^2.28.0", @@ -1579,48 +1579,10 @@ "react-dom": "^17.0.2 || ^18.0.0" } }, - "node_modules/@perses-dev/components/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@perses-dev/components/node_modules/notistack": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/notistack/-/notistack-2.0.8.tgz", - "integrity": "sha512-/IY14wkFp5qjPgKNvAdfL5Jp6q90+MjgKTPh4c81r/lW70KeuX6b9pE/4f8L4FG31cNudbN9siiFS5ql1aSLRw==", - "license": "MIT", - "dependencies": { - "clsx": "^1.1.0", - "hoist-non-react-statics": "^3.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/notistack" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "@mui/material": "^5.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, "node_modules/@perses-dev/core": { - "version": "0.50.0", - "resolved": "https://registry.npmjs.org/@perses-dev/core/-/core-0.50.0.tgz", - "integrity": "sha512-dUrCJmfEs6UirizSnEHhrFbR9QNxBFRa7WoSCsjyXRaCEQLEf0cDnxvLQrlygFPpTQsZjHMB9DCiEvgq83Vj+Q==", + "version": "0.50.3", + "resolved": "https://registry.npmjs.org/@perses-dev/core/-/core-0.50.3.tgz", + "integrity": "sha512-z3D/hJkAM9zzn1NZnBtMxSlj2ycTkrkbn0mYwgWFKra61kzUKbP0MIK/m9MTfkmyL8G2TPs3qxozirirX84nDQ==", "license": "Apache-2.0", "dependencies": { "date-fns": "^2.28.0", @@ -1635,14 +1597,14 @@ } }, "node_modules/@perses-dev/dashboards": { - "version": "0.50.0", - "resolved": "https://registry.npmjs.org/@perses-dev/dashboards/-/dashboards-0.50.0.tgz", - "integrity": "sha512-FSjUVj4h4zkYwfrzJ2Ewld1QmML9aL8mRo/iWpfpMXPu6lGu2UyfzHtPv0ClzjRFIg7eZq+neixigMWBy6QPsg==", + "version": "0.50.3", + "resolved": "https://registry.npmjs.org/@perses-dev/dashboards/-/dashboards-0.50.3.tgz", + "integrity": "sha512-B8ZI3iFCwWqYe8kMO6OJMJcbFY/1F44ZYMq+lmGR3bQzpvHquQdwkDYinNn+SjNt6vXkNlw8qA9s2i1V/fr8FA==", "license": "Apache-2.0", "dependencies": { - "@perses-dev/components": "0.50.0", - "@perses-dev/core": "0.50.0", - "@perses-dev/plugin-system": "0.50.0", + "@perses-dev/components": "0.50.3", + "@perses-dev/core": "0.50.3", + "@perses-dev/plugin-system": "0.50.3", "@types/react-grid-layout": "^1.3.2", "date-fns": "^2.28.0", "immer": "^9.0.15", @@ -1653,6 +1615,7 @@ "use-immer": "^0.7.0", "use-query-params": "^2.1.1", "use-resize-observer": "^9.0.0", + "yaml": "^2.7.0", "zustand": "^4.3.3" }, "peerDependencies": { @@ -1662,16 +1625,28 @@ "react-dom": "^17.0.2 || ^18.0.0" } }, + "node_modules/@perses-dev/dashboards/node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/@perses-dev/panels-plugin": { - "version": "0.50.0", - "resolved": "https://registry.npmjs.org/@perses-dev/panels-plugin/-/panels-plugin-0.50.0.tgz", - "integrity": "sha512-6A00dfVp1dZ8qlOQk0EILlxnVHNAJSTc8ErgyQZhKQKoMU4v0d2iv2W2ZvQ0HI3dH5R0Bp8cmUNSB90v9XIp2Q==", + "version": "0.50.3", + "resolved": "https://registry.npmjs.org/@perses-dev/panels-plugin/-/panels-plugin-0.50.3.tgz", + "integrity": "sha512-MuiSj/wXofU7MI1/oBR/yebqZcG3omwFcqzD7xAR6TmieU5venSzK45yW4GJdta1CH5sTE9RcdmYgbledHHQxg==", "license": "Apache-2.0", "dependencies": { "@mui/x-data-grid": "^7.23.1", - "@perses-dev/components": "0.50.0", - "@perses-dev/core": "0.50.0", - "@perses-dev/plugin-system": "0.50.0", + "@perses-dev/components": "0.50.3", + "@perses-dev/core": "0.50.3", + "@perses-dev/plugin-system": "0.50.3", "color-hash": "^2.0.2", "date-fns": "^2.28.0", "dompurify": "^2.4.0", @@ -1687,13 +1662,13 @@ } }, "node_modules/@perses-dev/plugin-system": { - "version": "0.50.0", - "resolved": "https://registry.npmjs.org/@perses-dev/plugin-system/-/plugin-system-0.50.0.tgz", - "integrity": "sha512-Ja9MC/wERy5i661iXLisgEbvLSLXWJWRPIRfWdLNmR5E1SYusoz7VNXLw43KzzAOOKJECPGRjTrhNhXO4gMSBA==", + "version": "0.50.3", + "resolved": "https://registry.npmjs.org/@perses-dev/plugin-system/-/plugin-system-0.50.3.tgz", + "integrity": "sha512-ISlPxF86VazoaplFqnky9V/e7rsGoEaFiDDJVwHCS5SwUEF+SXz6W7wWnNlt70eKrVtb0m71FWlVg7+XQrdEMw==", "license": "Apache-2.0", "dependencies": { - "@perses-dev/components": "0.50.0", - "@perses-dev/core": "0.50.0", + "@perses-dev/components": "0.50.3", + "@perses-dev/core": "0.50.3", "date-fns": "^2.30.0", "immer": "^9.0.15", "react-hook-form": "^7.46.1", @@ -1709,16 +1684,16 @@ } }, "node_modules/@perses-dev/prometheus-plugin": { - "version": "0.50.0", - "resolved": "https://registry.npmjs.org/@perses-dev/prometheus-plugin/-/prometheus-plugin-0.50.0.tgz", - "integrity": "sha512-U24a0OI5PmtBDv1CrRW9j3qLkOhvS/xR5ts8k0QUHf7q6twi5qM4Ah8rSLTp395oO+9AUC7ucJq0FGcGNilLUw==", + "version": "0.50.3", + "resolved": "https://registry.npmjs.org/@perses-dev/prometheus-plugin/-/prometheus-plugin-0.50.3.tgz", + "integrity": "sha512-3sy1oZT74aPZdttYBU7KsLjMuo2rdxNq5Cujvq1iA0kpzKbdTBBoriGM0ege8wGC6/xUQV9N9sp/VhTVECtZGQ==", "license": "Apache-2.0", "dependencies": { "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.2.0", - "@perses-dev/components": "0.50.0", - "@perses-dev/core": "0.50.0", - "@perses-dev/plugin-system": "0.50.0", + "@perses-dev/components": "0.50.3", + "@perses-dev/core": "0.50.3", + "@perses-dev/plugin-system": "0.50.3", "@prometheus-io/codemirror-promql": "^0.43.0", "@uiw/react-codemirror": "^4.19.1", "date-fns": "^2.28.0", @@ -3453,9 +3428,9 @@ } }, "node_modules/bignumber.js": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", "license": "MIT", "engines": { "node": "*" @@ -6928,6 +6903,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -8963,6 +8947,37 @@ "node": ">=0.10.0" } }, + "node_modules/notistack": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/notistack/-/notistack-3.0.2.tgz", + "integrity": "sha512-0R+/arLYbK5Hh7mEfR2adt0tyXJcCC9KkA2hc56FeWik2QN6Bm/S4uW+BjzDARsJth5u06nTjelSw/VSnB1YEA==", + "license": "MIT", + "dependencies": { + "clsx": "^1.1.0", + "goober": "^2.0.33" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/notistack" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/notistack/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/now-and-later": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", diff --git a/web/package.json b/web/package.json index 9e40cfab..56b6c937 100644 --- a/web/package.json +++ b/web/package.json @@ -46,11 +46,11 @@ "@patternfly/react-core": "5.4.0", "@patternfly/react-icons": "^5.4.0", "@patternfly/react-table": "5.4.13", - "@perses-dev/components": "^0.50.0", - "@perses-dev/dashboards": "^0.50.0", - "@perses-dev/panels-plugin": "^0.50.0", - "@perses-dev/plugin-system": "^0.50.0", - "@perses-dev/prometheus-plugin": "^0.50.0", + "@perses-dev/components": "^0.50.3", + "@perses-dev/dashboards": "^0.50.3", + "@perses-dev/panels-plugin": "^0.50.3", + "@perses-dev/plugin-system": "^0.50.3", + "@perses-dev/prometheus-plugin": "^0.50.3", "@prometheus-io/codemirror-promql": "^0.37.0", "@tanstack/react-query": "^4.36.1", "classnames": "2.x", @@ -110,7 +110,8 @@ "tough-cookie": "^4.1.3", "sanitize-html": "^2.12.1", "path-to-regexp": "^1.9.0", - "cross-spawn": "^7.0.5" + "cross-spawn": "^7.0.5", + "notistack": "^3.0.2" }, "resolutions": { "@types/react": "17.0.83" @@ -122,8 +123,7 @@ "description": "This plugin adds the monitoring UI to the OpenShift web console", "exposedModules": { "MonitoringRouter": "./components/router", - "MonitoringReducer": "./reducers/observe", - "IncidentsPage": "./components/Incidents/IncidentsPage" + "MonitoringReducer": "./reducers/observe" }, "dependencies": { "@console/pluginAPI": "*" diff --git a/web/src/actions/observe.ts b/web/src/actions/observe.ts index e70d471d..9f349fbc 100644 --- a/web/src/actions/observe.ts +++ b/web/src/actions/observe.ts @@ -29,14 +29,6 @@ export enum ActionType { QueryBrowserToggleAllSeries = 'queryBrowserToggleAllSeries', SetAlertCount = 'SetAlertCount', ToggleGraphs = 'toggleGraphs', - SetIncidents = 'setIncidents', - SetIncidentsActiveFilters = 'setIncidentsActiveFilters', - SetChooseIncident = 'setChooseIncident', - SetAlertsData = 'setAlertsData', - SetAlertsTableData = 'setAlertsTableData', - SetAlertsAreLoading = 'setAlertsAreLoading', - SetIncidentsChartSelection = 'setIncidentsChartSelection', - SetFilteredIncidentsData = 'setFilteredIncidentsData', } export type Perspective = 'admin' | 'dev' | 'acm' | 'virtualization-perspective'; @@ -136,28 +128,6 @@ export const queryBrowserToggleSeries = (index: number, labels: { [key: string]: export const setAlertCount = (alertCount) => action(ActionType.SetAlertCount, { alertCount }); -export const setIncidents = (incidents) => action(ActionType.SetIncidents, incidents); - -export const setIncidentsActiveFilters = (incidentsActiveFilters) => - action(ActionType.SetIncidentsActiveFilters, incidentsActiveFilters); - -export const setChooseIncident = (incidentGroupId) => - action(ActionType.SetChooseIncident, incidentGroupId); - -export const setAlertsData = (alertsData) => action(ActionType.SetAlertsData, alertsData); - -export const setAlertsTableData = (alertsTableData) => - action(ActionType.SetAlertsTableData, alertsTableData); - -export const setAlertsAreLoading = (alertsAreLoading) => - action(ActionType.SetAlertsAreLoading, alertsAreLoading); - -export const setIncidentsChartSelection = (incidentsChartSelectedId) => - action(ActionType.SetIncidentsChartSelection, incidentsChartSelectedId); - -export const setFilteredIncidentsData = (filteredIncidentsData) => - action(ActionType.SetFilteredIncidentsData, filteredIncidentsData); - const actions = { alertingErrored, alertingLoaded, @@ -187,14 +157,6 @@ const actions = { queryBrowserToggleSeries, setAlertCount, toggleGraphs, - setIncidents, - setIncidentsActiveFilters, - setChooseIncident, - setAlertsData, - setAlertsTableData, - setAlertsAreLoading, - setIncidentsChartSelection, - setFilteredIncidentsData, }; export type ObserveAction = Action; diff --git a/web/src/components/Incidents/AlertsChart/AlertsChart.jsx b/web/src/components/Incidents/AlertsChart/AlertsChart.jsx deleted file mode 100644 index 22927497..00000000 --- a/web/src/components/Incidents/AlertsChart/AlertsChart.jsx +++ /dev/null @@ -1,196 +0,0 @@ -import * as React from 'react'; - -import { - Chart, - ChartAxis, - ChartBar, - ChartGroup, - ChartLabel, - ChartLegend, - ChartTooltip, - ChartVoronoiContainer, -} from '@patternfly/react-charts'; -import { Card, CardTitle, EmptyState, EmptyStateBody } from '@patternfly/react-core'; -import { createAlertsChartBars, formatDate, generateDateArray } from '../utils'; -import { getResizeObserver } from '@patternfly/react-core'; -import { useDispatch, useSelector } from 'react-redux'; -import global_danger_color_100 from '@patternfly/react-tokens/dist/esm/global_danger_color_100'; -import global_info_color_100 from '@patternfly/react-tokens/dist/esm/global_info_color_100'; -import global_warning_color_100 from '@patternfly/react-tokens/dist/esm/global_warning_color_100'; -import * as _ from 'lodash-es'; -import { setAlertsAreLoading } from '../../../actions/observe'; - -const AlertsChart = ({ chartDays, theme }) => { - const dispatch = useDispatch(); - const [chartData, setChartData] = React.useState([]); - const [chartContainerHeight, setChartContainerHeight] = React.useState(); - const [chartHeight, setChartHeight] = React.useState(); - const alertsData = useSelector((state) => - state.plugins.mcp.getIn(['incidentsData', 'alertsData']), - ); - const alertsAreLoading = useSelector((state) => - state.plugins.mcp.getIn(['incidentsData', 'alertsAreLoading']), - ); - const filteredData = useSelector((state) => - state.plugins.mcp.getIn(['incidentsData', 'filteredIncidentsData']), - ); - const incidentGroupId = useSelector((state) => - state.plugins.mcp.getIn(['incidentsData', 'incidentGroupId']), - ); - React.useEffect(() => { - setChartContainerHeight(chartData?.length < 5 ? 300 : chartData?.length * 60); - setChartHeight(chartData?.length < 5 ? 250 : chartData?.length * 55); - }, [chartData]); - const dateValues = generateDateArray(chartDays); - - React.useEffect(() => { - setChartData(alertsData.map((alert) => createAlertsChartBars(alert, theme, dateValues))); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [alertsData]); - - React.useEffect(() => { - //state when chosen incident not passing filters or the is no data - if ( - _.isEmpty(filteredData) || - !filteredData.find((incident) => incident.group_id === incidentGroupId) - ) { - dispatch(setAlertsAreLoading({ alertsAreLoading: true })); - } //state when chosen incident passing filters - else if (filteredData.find((incident) => incident.group_id === incidentGroupId)) { - dispatch(setAlertsAreLoading({ alertsAreLoading: false })); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [filteredData]); - - const [width, setWidth] = React.useState(0); - const containerRef = React.useRef(null); - const handleResize = () => { - if (containerRef.current && containerRef.current.clientWidth) { - setWidth(containerRef.current.clientWidth); - } - }; - React.useEffect(() => { - const observer = getResizeObserver(containerRef.current, handleResize); - handleResize(); - return () => observer(); - }, []); - - return ( - -
- Alerts Timeline - {alertsAreLoading ? ( - - Select an incident in the chart above to see alerts. - - ) : ( -
- } /> - } - labels={({ datum }) => { - if (datum.nodata) { - return null; - } - return `Alert Severity: ${datum.severity} - Alert Name: ${datum.name ? datum.name : '---'} - Namespace: ${datum.namespace ? datum.namespace : '---'} - Layer: ${datum.layer ? datum.layer : '---'} - Component: ${datum.component} - Start: ${formatDate(new Date(datum.y0), true)} - End: ${ - datum.alertstate === 'firing' ? '---' : formatDate(new Date(datum.y), true) - }`; - }} - /> - } - domainPadding={{ x: [30, 25] }} - legendData={[ - { - name: 'Critical', - symbol: { - fill: theme === 'light' ? global_danger_color_100.var : '#C9190B', - }, - }, - { - name: 'Info', - symbol: { - fill: theme === 'light' ? global_info_color_100.var : '#06C', - }, - }, - { - name: 'Warning', - symbol: { - fill: theme === 'light' ? global_warning_color_100.var : '#F0AB00', - }, - }, - ]} - legendComponent={ - - } - /> - } - legendPosition="bottom-left" - //this should be always less than the container height - height={chartHeight} - padding={{ - bottom: 75, // Adjusted to accommodate legend - left: 50, - right: 25, // Adjusted to accommodate tooltip - top: 50, - }} - width={width} - > - - new Date(t).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) - } - tickValues={dateValues} - tickLabelComponent={ - - } - /> - - {chartData.map((bar, index) => { - return ( - //we have several arrays and for each array we make a ChartBar - datum.fill, - stroke: ({ datum }) => datum.fill, - fillOpacity: ({ datum }) => (datum.nodata ? 0 : 1), - }, - }} - /> - ); - })} - - -
- )} -
-
- ); -}; - -export default AlertsChart; diff --git a/web/src/components/Incidents/IncidentsChart/IncidentsChart.jsx b/web/src/components/Incidents/IncidentsChart/IncidentsChart.jsx deleted file mode 100644 index e469d602..00000000 --- a/web/src/components/Incidents/IncidentsChart/IncidentsChart.jsx +++ /dev/null @@ -1,201 +0,0 @@ -import * as React from 'react'; - -import { - Chart, - ChartAxis, - ChartBar, - ChartGroup, - ChartLabel, - ChartLegend, - ChartThemeColor, - ChartTooltip, - ChartVoronoiContainer, -} from '@patternfly/react-charts'; -import { Bullseye, Card, CardTitle, Spinner } from '@patternfly/react-core'; -import { createIncidentsChartBars, formatDate, generateDateArray } from '../utils'; -import { getResizeObserver } from '@patternfly/react-core'; -import { useDispatch, useSelector } from 'react-redux'; -import { setChooseIncident } from '../../../actions/observe'; -import global_danger_color_100 from '@patternfly/react-tokens/dist/esm/global_danger_color_100'; -import global_info_color_100 from '@patternfly/react-tokens/dist/esm/global_info_color_100'; -import global_warning_color_100 from '@patternfly/react-tokens/dist/esm/global_warning_color_100'; -import { setAlertsAreLoading } from '../../../actions/observe'; - -const IncidentsChart = ({ incidentsData, chartDays, theme }) => { - const dispatch = useDispatch(); - const [isLoading, setIsLoading] = React.useState(true); - const [chartData, setChartData] = React.useState(); - const [chartContainerHeight, setChartContainerHeight] = React.useState(); - const [chartHeight, setChartHeight] = React.useState(); - React.useEffect(() => { - setChartContainerHeight(chartData?.length < 5 ? 300 : chartData?.length * 60); - setChartHeight(chartData?.length < 5 ? 250 : chartData?.length * 55); - }, [chartData]); - const [width, setWidth] = React.useState(0); - const containerRef = React.useRef(null); - const handleResize = () => { - if (containerRef.current && containerRef.current.clientWidth) { - setWidth(containerRef.current.clientWidth); - } - }; - React.useEffect(() => { - const observer = getResizeObserver(containerRef.current, handleResize); - handleResize(); - return () => observer(); - }, []); - React.useEffect(() => { - setIsLoading(false); - setChartData( - incidentsData.map((incident) => createIncidentsChartBars(incident, theme, dateValues)), - ); - }, [incidentsData]); - const dateValues = generateDateArray(chartDays); - - const selectedId = useSelector((state) => - state.plugins.mcp.getIn(['incidentsData', 'incidentGroupId']), - ); - - const isHidden = (group_id) => selectedId !== '' && selectedId !== group_id; - const clickHandler = (data, datum) => { - if (datum.datum.group_id === selectedId) { - dispatch( - setChooseIncident({ - incidentGroupId: '', - }), - ); - dispatch(setAlertsAreLoading({ alertsAreLoading: true })); - } else { - dispatch( - setChooseIncident({ - incidentGroupId: datum.datum.group_id, - }), - ); - } - }; - - function getOpacity(datum) { - return (datum.fillOpacity = isHidden(datum.group_id) ? '0.3' : '1'); - } - - return ( - -
- Incidents Timeline - {isLoading ? ( - - - - ) : ( -
- } - /> - } - labels={({ datum }) => { - if (datum.nodata) { - return null; - } - return `Severity: ${datum.name} - Component: ${datum.componentList?.join(', ')} - Incident ID: ${datum.group_id} - Start: ${formatDate(new Date(datum.y0), true)} - End: ${datum.firing ? '---' : formatDate(new Date(datum.y), true)}`; - }} - /> - } - domainPadding={{ x: [30, 25] }} - legendData={[ - { - name: 'Critical', - symbol: { - fill: theme === 'light' ? global_danger_color_100.var : '#C9190B', - }, - }, - { - name: 'Info', - symbol: { - fill: theme === 'light' ? global_info_color_100.var : '#06C', - }, - }, - { - name: 'Warning', - symbol: { - fill: theme === 'light' ? global_warning_color_100.var : '#F0AB00', - }, - }, - ]} - legendComponent={ - - } - /> - } - legendPosition="bottom-left" - //this should be always less than the container height - height={chartHeight} - padding={{ - bottom: 75, // Adjusted to accommodate legend - left: 50, - right: 25, // Adjusted to accommodate tooltip - top: 50, - }} - width={width} - themeColor={ChartThemeColor.purple} - > - - new Date(t).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) - } - tickValues={dateValues} - tickLabelComponent={ - - } - /> - - {chartData.map((bar) => { - return ( - //we have several arrays and for each array we make a ChartBar - datum.fill, - stroke: ({ datum }) => datum.fill, - fillOpacity: ({ datum }) => (datum.nodata ? 0 : getOpacity(datum)), - }, - }} - events={[ - { - eventHandlers: { - onClick: (props, datum) => clickHandler(props, datum), - }, - }, - ]} - /> - ); - })} - - -
- )} -
-
- ); -}; - -export default IncidentsChart; diff --git a/web/src/components/Incidents/IncidentsDetailsRowTable.jsx b/web/src/components/Incidents/IncidentsDetailsRowTable.jsx deleted file mode 100644 index 9bb7efa6..00000000 --- a/web/src/components/Incidents/IncidentsDetailsRowTable.jsx +++ /dev/null @@ -1,201 +0,0 @@ -import React from 'react'; -import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; -import { - GreenCheckCircleIcon, - isAlertingRulesSource, - PrometheusEndpoint, - Timestamp, - useActiveNamespace, - useResolvedExtensions, -} from '@openshift-console/dynamic-plugin-sdk'; -import { BellIcon, ExclamationCircleIcon, InfoCircleIcon } from '@patternfly/react-icons'; -import ExclamationTriangleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon'; -import { Bullseye, DropdownItem, Icon, Spinner, Tooltip } from '@patternfly/react-core'; -import { Link } from 'react-router-dom'; -import { AlertResource, getAlertsAndRules } from '../utils'; -import { MonitoringResourceIcon } from '../alerting/AlertUtils'; -import { getPrometheusURL } from '../console/graphs/helpers'; -import { fetchAlerts } from '../fetch-alerts'; -import KebabDropdown from '../kebab-dropdown'; -import { useTranslation } from 'react-i18next'; -import { - getAlertUrl, - getNewSilenceAlertUrl, - getRuleUrl, - usePerspective, -} from '../hooks/usePerspective'; -import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; -import './incidents-styles.css'; - -const IncidentsDetailsRowTable = ({ alerts }) => { - const [namespace] = useActiveNamespace(); - const { perspective } = usePerspective(); - const [alertsWithMatchedData, setAlertsWithMatchedData] = React.useState([]); - const [customExtensions] = useResolvedExtensions(isAlertingRulesSource); - const { t } = useTranslation(process.env.I18N_NAMESPACE); - - const alertsSource = React.useMemo( - () => - customExtensions - .filter((extension) => extension.properties.contextId === 'observe-alerting') - .map((extension) => extension.properties), - [customExtensions], - ); - - function findMatchingAlertsWithId(alertsArray, rulesArray) { - // Map over alerts and find matching rules - return alertsArray.map((alert) => { - const match = rulesArray.find((rule) => alert.alertname === rule.name); - - if (match) { - return { ...alert, rule: match }; - } - return alert; - }); - } - - React.useEffect(() => { - const url = getPrometheusURL({ endpoint: PrometheusEndpoint.RULES }); - const poller = () => { - fetchAlerts(url, alertsSource) - .then(({ data }) => { - const { rules } = getAlertsAndRules(data); - //match rules fetched with alerts passed to this component by alertname - setAlertsWithMatchedData(findMatchingAlertsWithId(alerts, rules)); - }) - .catch((e) => { - // eslint-disable-next-line no-console - console.log(e); - }); - }; - poller(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [alerts]); - - return ( - - - - - - - - - - - - - {!alertsWithMatchedData ? ( - - - - ) : ( - alertsWithMatchedData?.map((alertDetails, rowIndex) => { - return ( - - - - - - - - - - ); - }) - )} - -
{t('Alert Name')}{t('Namespace')}{t('Severity')}{t('State')}{t('Start')}{t('End')}
- - - {alertDetails.alertname} - - {(!alertDetails?.rule || alertDetails.resolved) && ( - No details can be shown for inactive alerts.}> - - - )} - {alertDetails.namespace || '---'} - {alertDetails.severity === 'critical' ? ( - <> - - - - Critical - - ) : alertDetails.severity === 'warning' ? ( - <> - - - - Warning - - ) : ( - <> - - - - Info - - )} - - {!alertDetails.resolved ? ( - <> - - Firing - - ) : ( - <> - - Resolved - - )} - - - - {!alertDetails.resolved ? ( - '---' - ) : ( - - )} - - - - {t('Silence alert')} - - , - - - {t('View alerting rule')} - - , - ]} - /> -
- ); -}; - -export default IncidentsDetailsRowTable; diff --git a/web/src/components/Incidents/IncidentsHeader/IncidentsHeader.jsx b/web/src/components/Incidents/IncidentsHeader/IncidentsHeader.jsx deleted file mode 100644 index 8fd43047..00000000 --- a/web/src/components/Incidents/IncidentsHeader/IncidentsHeader.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import * as React from 'react'; -import IncidentsChart from '../IncidentsChart/IncidentsChart'; -import AlertsChart from '../AlertsChart/AlertsChart'; - -export const IncidentsHeader = ({ incidentsData, chartDays, theme }) => { - return ( -
- - -
- ); -}; diff --git a/web/src/components/Incidents/IncidentsPage.jsx b/web/src/components/Incidents/IncidentsPage.jsx deleted file mode 100644 index 9f2505d7..00000000 --- a/web/src/components/Incidents/IncidentsPage.jsx +++ /dev/null @@ -1,440 +0,0 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import * as React from 'react'; -import { IncidentsHeader } from './IncidentsHeader/IncidentsHeader'; -import { useSafeFetch } from '../console/utils/safe-fetch-hook'; -import { createAlertsQuery, fetchDataForIncidentsAndAlerts } from './api'; -import { useTranslation } from 'react-i18next'; -import { - Bullseye, - Button, - Select, - SelectList, - SelectOption, - Spinner, - Toolbar, - ToolbarContent, - ToolbarFilter, - ToolbarItem, - MenuToggle, - Badge, - Title, - PageSection, - PageSectionVariants, -} from '@patternfly/react-core'; -import { Helmet } from 'react-helmet'; -import { IncidentsTable } from './IncidentsTable'; -import { - getIncidentsTimeRanges, - processIncidents, - processIncidentsForAlerts, -} from './processIncidents'; -import { - filterIncident, - onDeleteGroupIncidentFilterChip, - onDeleteIncidentFilterChip, - onIncidentFiltersSelect, - parseUrlParams, - updateBrowserUrl, - usePatternFlyTheme, -} from './utils'; -import { groupAlertsForTable, processAlerts } from './processAlerts'; -import { CompressArrowsAltIcon, CompressIcon, FilterIcon } from '@patternfly/react-icons'; -import { useDispatch, useSelector } from 'react-redux'; -import { - setAlertsAreLoading, - setAlertsData, - setAlertsTableData, - setFilteredIncidentsData, - setIncidents, - setIncidentsActiveFilters, -} from '../../actions/observe'; -import { useLocation } from 'react-router-dom'; -import { usePerspective } from '../hooks/usePerspective'; -import { changeDaysFilter } from './utils'; -import { parsePrometheusDuration } from '../console/console-shared/src/datetime/prometheus'; -import withFallback from '../console/console-shared/error/fallbacks/withFallback'; - -const IncidentsPage = () => { - const { t } = useTranslation(process.env.I18N_NAMESPACE); - const dispatch = useDispatch(); - const location = useLocation(); - const urlParams = parseUrlParams(location.search); - const { perspective } = usePerspective(); - const { theme } = usePatternFlyTheme(); - // loading states - const [incidentsAreLoading, setIncidentsAreLoading] = React.useState(true); - // days span is where we store the value for creating time ranges for - // fetch incidents/alerts based on the length of time ranges - // when days filter changes we set a new days span -> calculate new time range and fetch new data - const [daysSpan, setDaysSpan] = React.useState(); - const [timeRanges, setTimeRanges] = React.useState([]); - // data that is used for processing to serve it to the alerts table and chart - const [incidentForAlertProcessing, setIncidentForAlertProcessing] = React.useState([]); - const [hideCharts, setHideCharts] = React.useState(false); - - const [incidentFilterIsExpanded, setIncidentIsExpanded] = React.useState(false); - const [daysFilterIsExpanded, setDaysFilterIsExpanded] = React.useState(false); - - const onIncidentFilterToggle = (ev) => { - ev.stopPropagation(); - setIncidentIsExpanded(!incidentFilterIsExpanded); - }; - const onToggleClick = (ev) => { - ev.stopPropagation(); - setDaysFilterIsExpanded(!daysFilterIsExpanded); - }; - - const incidentsInitialState = useSelector((state) => - state.plugins.mcp.getIn(['incidentsData', 'incidentsInitialState']), - ); - - const incidents = useSelector((state) => state.plugins.mcp.getIn(['incidentsData', 'incidents'])); - - const incidentsActiveFilters = useSelector((state) => - state.plugins.mcp.getIn(['incidentsData', 'incidentsActiveFilters']), - ); - - const incidentGroupId = useSelector((state) => - state.plugins.mcp.getIn(['incidentsData', 'incidentGroupId']), - ); - - const alertsData = useSelector((state) => - state.plugins.mcp.getIn(['incidentsData', 'alertsData']), - ); - - const alertsAreLoading = useSelector((state) => - state.plugins.mcp.getIn(['incidentsData', 'alertsAreLoading']), - ); - - const filteredData = useSelector((state) => - state.plugins.mcp.getIn(['incidentsData', 'filteredIncidentsData']), - ); - React.useEffect(() => { - const hasUrlParams = Object.keys(urlParams).length > 0; - - if (hasUrlParams) { - // If URL parameters exist, update incidentsActiveFilters based on them - dispatch( - setIncidentsActiveFilters({ - incidentsActiveFilters: { - ...incidentsInitialState, - ...urlParams, - }, - }), - ); - } else { - // If no URL parameters exist, set the URL based on incidentsInitialState - updateBrowserUrl(incidentsInitialState); - dispatch( - setIncidentsActiveFilters({ - incidentsActiveFilters: { - ...incidentsInitialState, - }, - }), - ); - } - }, []); - - React.useEffect(() => { - updateBrowserUrl(incidentsActiveFilters); - }, [incidentsActiveFilters]); - - React.useEffect(() => { - dispatch( - setFilteredIncidentsData({ - filteredIncidentsData: filterIncident(incidentsActiveFilters, incidents), - }), - ); - }, [incidentsActiveFilters.incidentFilters]); - - const now = Date.now(); - const safeFetch = useSafeFetch(); - const title = t('Incidents'); - - React.useEffect(() => { - setTimeRanges(getIncidentsTimeRanges(daysSpan, now)); - }, [daysSpan]); - - React.useEffect(() => { - setDaysSpan( - parsePrometheusDuration( - incidentsActiveFilters.days.length > 0 - ? incidentsActiveFilters.days[0].split(' ')[0] + 'd' - : '', - ), - ); - }, [incidentsActiveFilters.days]); - - React.useEffect(() => { - (async () => { - Promise.all( - timeRanges.map(async (range) => { - const response = await fetchDataForIncidentsAndAlerts( - safeFetch, - range, - createAlertsQuery(incidentForAlertProcessing), - perspective, - ); - return response.data.result; - }), - ) - .then((results) => { - const aggregatedData = results.reduce((acc, result) => acc.concat(result), []); - dispatch( - setAlertsData({ - alertsData: processAlerts(aggregatedData, incidentForAlertProcessing), - }), - ); - dispatch(setAlertsAreLoading({ alertsAreLoading: false })); - }) - .catch((err) => { - // eslint-disable-next-line no-console - console.log(err); - }); - })(); - }, [incidentForAlertProcessing]); - - React.useEffect(() => { - dispatch( - setAlertsTableData({ - alertsTableData: groupAlertsForTable(alertsData), - }), - ); - }, [alertsAreLoading]); - - React.useEffect(() => { - (async () => { - Promise.all( - timeRanges.map(async (range) => { - const response = await fetchDataForIncidentsAndAlerts( - safeFetch, - range, - 'cluster:health:components:map', - perspective, - ); - return response.data.result; - }), - ) - .then((results) => { - const aggregatedData = results.reduce((acc, result) => acc.concat(result), []); - dispatch( - setIncidents({ - incidents: processIncidents(aggregatedData), - }), - ); - dispatch( - setFilteredIncidentsData({ - filteredIncidentsData: filterIncident( - urlParams ? incidentsActiveFilters : incidentsInitialState, - processIncidents(aggregatedData), - ), - }), - ); - setIncidentsAreLoading(false); - }) - .catch((err) => { - // eslint-disable-next-line no-console - console.log(err); - }); - })(); - }, [timeRanges]); - - React.useEffect(() => { - if (incidentGroupId) { - Promise.all( - timeRanges.map(async (range) => { - const response = await fetchDataForIncidentsAndAlerts( - safeFetch, - range, - `cluster:health:components:map{group_id='${incidentGroupId}'}`, - perspective, - ); - return response.data.result; - }), - ) - .then((results) => { - const aggregatedData = results.reduce((acc, result) => acc.concat(result), []); - setIncidentForAlertProcessing(processIncidentsForAlerts(aggregatedData)); - dispatch(setAlertsAreLoading({ alertsAreLoading: true })); - setIncidentsAreLoading(false); - }) - .catch((err) => { - // eslint-disable-next-line no-console - console.log(err); - }); - } - }, [incidentGroupId, timeRanges]); - - const onSelect = (_event, value) => { - if (value) { - changeDaysFilter(value, dispatch, incidentsActiveFilters); - } - - setDaysFilterIsExpanded(false); - }; - - return ( - <> - - {title} - - - {t('Incidents')} - - {alertsAreLoading && incidentsAreLoading ? ( - - - - ) : ( - - - onDeleteIncidentFilterChip('', '', incidentsActiveFilters, dispatch) - } - > - - - - onDeleteIncidentFilterChip(category, chip, incidentsActiveFilters, dispatch) - } - deleteChipGroup={(category) => - onDeleteGroupIncidentFilterChip(category, incidentsActiveFilters, dispatch) - } - categoryName="Filters" - > - - - - - - - - - - - - {hideCharts ? ( - '' - ) : ( - - )} - - - )} - - ); -}; - -const incidentsPageWithFallback = withFallback(IncidentsPage); - -export default incidentsPageWithFallback; diff --git a/web/src/components/Incidents/IncidentsTable.jsx b/web/src/components/Incidents/IncidentsTable.jsx deleted file mode 100644 index 931b6b78..00000000 --- a/web/src/components/Incidents/IncidentsTable.jsx +++ /dev/null @@ -1,137 +0,0 @@ -import React from 'react'; -import { Table, Thead, Tr, Th, Tbody, Td, ExpandableRowContent } from '@patternfly/react-table'; -import { - Bullseye, - Card, - CardBody, - EmptyState, - EmptyStateBody, - EmptyStateIcon, - Label, -} from '@patternfly/react-core'; -import InfoCircleIcon from '@patternfly/react-icons/dist/esm/icons/info-circle-icon'; -import IncidentsDetailsRowTable from './IncidentsDetailsRowTable'; -import { BellIcon, BellSlashIcon, SearchIcon } from '@patternfly/react-icons'; -import { useSelector } from 'react-redux'; -import * as _ from 'lodash-es'; - -export const IncidentsTable = ({ namespace }) => { - const columnNames = { - checkbox: '', - component: 'Component', - severity: 'Severity', - state: 'State', - }; - const [expandedAlerts, setExpandedAlerts] = React.useState([]); - const setAlertExpanded = (alert, isExpanding = true) => - setExpandedAlerts((prevExpanded) => { - const otherAlertExpanded = prevExpanded.filter((r) => r !== alert.component); - return isExpanding ? [...otherAlertExpanded, alert.component] : otherAlertExpanded; - }); - const isAlertExpanded = (alert) => expandedAlerts.includes(alert.component); - const alertsTableData = useSelector((state) => - state.plugins.mcp.getIn(['incidentsData', 'alertsTableData']), - ); - const alertsAreLoading = useSelector((state) => - state.plugins.mcp.getIn(['incidentsData', 'alertsAreLoading']), - ); - - return ( - - - - - - - - - - - {_.isEmpty(alertsTableData) || alertsAreLoading ? ( - - - - ) : ( - alertsTableData.map((alert, rowIndex) => { - return ( - - - - - - - {alert.alertsExpandedRowData && ( - - - - )} - - ); - }) - )} -
- {columnNames.component}{columnNames.severity}{columnNames.state}
- - - - - - No incidents selected. - - -
setAlertExpanded(alert, !isAlertExpanded(alert)), - expandId: 'alert-expandable', - } - : undefined - } - /> - {alert.component} - {alert.critical > 0 ? ( - - ) : ( - '' - )} - {alert.warning > 0 ? ( - - ) : ( - '' - )} - {alert.info > 0 ? ( - - ) : ( - '' - )} - - {alert.alertstate === 'resolved' ? ( - - ) : ( - - )} -
- - - -
-
-
- ); -}; diff --git a/web/src/components/Incidents/api.js b/web/src/components/Incidents/api.js deleted file mode 100644 index 9adea268..00000000 --- a/web/src/components/Incidents/api.js +++ /dev/null @@ -1,78 +0,0 @@ -/* eslint-disable max-len */ - -import { PrometheusEndpoint } from '@openshift-console/dynamic-plugin-sdk'; -import { getPrometheusURL } from '../console/graphs/helpers'; -/** - * 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, - * but the prefix is removed from the keys in the final query string. - * - * @param {Object[]} groupedAlertsValues - Array of grouped alert objects. - * Each alert object should contain various properties, including "src_" prefixed properties, - * as well as "layer" and "component" for constructing the meta fields in the query. - * - * @param {string} groupedAlertsValues[].layer - The layer of the alert, used in the absent condition. - * @param {string} groupedAlertsValues[].component - The component of the alert, used in the absent condition. - * @returns {string} - A string representing the combined Prometheus alerts query. - * Each alert query is formatted as `(ALERTS{key="value", ...} + on () group_left (component, layer) (absent(meta{layer="value", component="value"})))` - * and multiple queries are joined by "or". - * - * @example - * const alerts = [ - * { - * src_alertname: "AlertmanagerReceiversNotConfigured", - * src_namespace: "openshift-monitoring", - * src_severity: "warning", - * layer: "core", - * component: "monitoring" - * }, - * { - * src_alertname: "AnotherAlert", - * src_namespace: "default", - * src_severity: "critical", - * layer: "app", - * component: "frontend" - * } - * ]; - * - * const query = createAlertsQuery(alerts); - * // Returns: - * // '(ALERTS{alertname="AlertmanagerReceiversNotConfigured", namespace="openshift-monitoring", severity="warning"} + on () group_left (component, layer) (absent(meta{layer="core", component="monitoring"}))) or - * // (ALERTS{alertname="AnotherAlert", namespace="default", severity="critical"} + on () group_left (component, layer) (absent(meta{layer="app", component="frontend"})))' - */ -export const createAlertsQuery = (groupedAlertsValues) => { - const alertsQuery = groupedAlertsValues - .map((query) => { - // Dynamically get all keys starting with "src_" - const srcKeys = Object.keys(query).filter((key) => key.startsWith('src_')); - - // Create the alertParts array using the dynamically discovered src_ keys, - // but remove the "src_" prefix from the keys in the final query string. - const alertParts = srcKeys - .filter((key) => query[key]) // Only include keys that are present in the query object - .map((key) => `${key.replace('src_', '')}="${query[key]}"`) // Remove "src_" prefix from keys - .join(', '); - - // Construct the query string for each grouped alert - return `(ALERTS{${alertParts}} + on () group_left (component, layer) (absent(meta{layer="${query.layer}", component="${query.component}"})))`; - }) - .join(' or '); // Join all individual alert queries with "or" - - return alertsQuery; -}; - -export const fetchDataForIncidentsAndAlerts = (fetch, range, customQuery, perspective) => { - return fetch( - getPrometheusURL( - { - endpoint: PrometheusEndpoint.QUERY_RANGE, - endTime: range.endTime, - namespace: '', - query: customQuery, - samples: 288, - timespan: range.duration - 1, - }, - perspective, - ), - ); -}; diff --git a/web/src/components/Incidents/incidents-styles.css b/web/src/components/Incidents/incidents-styles.css deleted file mode 100644 index a4a78aa6..00000000 --- a/web/src/components/Incidents/incidents-styles.css +++ /dev/null @@ -1,3 +0,0 @@ -.expanded-details-text-margin { - margin-left: 2px - } \ No newline at end of file diff --git a/web/src/components/Incidents/processAlerts.js b/web/src/components/Incidents/processAlerts.js deleted file mode 100644 index ed2d098c..00000000 --- a/web/src/components/Incidents/processAlerts.js +++ /dev/null @@ -1,222 +0,0 @@ -/* eslint-disable max-len */ - -import { sortObjectsByEarliestTimestamp } from './processIncidents'; - -/** - * Groups alert objects by their `alertname`, `namespace`, and `component` fields and merges their values - * while removing duplicates. Alerts with the same combination of `alertname`, `namespace`, and `component` - * are combined, with values being deduplicated. - * - * @param {Array} objects - Array of alert objects to be grouped. Each object contains a `metric` field - * with properties such as `alertname`, `namespace`, `component`, and an array of `values`. - * @param {Object} objects[].metric - The metric information of the alert. - * @param {string} objects[].metric.alertname - The name of the alert. - * @param {string} objects[].metric.namespace - The namespace in which the alert is raised. - * @param {string} objects[].metric.component - The component associated with the alert. - * @param {Array>} objects[].values - The array of values corresponding to the alert, where - * each value is a tuple containing a timestamp and a value (e.g., [timestamp, value]). - * - * @returns {Array} - An array of grouped alert objects. Each object contains a unique combination of - * `alertname`, `namespace`, and `component`, with deduplicated values. - * @returns {Object} return[].metric - The metric information of the grouped alert. - * @returns {Array>} return[].values - The deduplicated array of values for the grouped alert. - * - * @example - * const alerts = [ - * { metric: { alertname: "Alert1", namespace: "ns1", component: "comp1" }, values: [[12345, "2"], [12346, "2"]] }, - * { metric: { alertname: "Alert1", namespace: "ns1", component: "comp1" }, values: [[12346, "2"], [12347, "2"]] } - * ]; - * const groupedAlerts = groupAlerts(alerts); - * // Returns an array where the two alerts are grouped together with deduplicated values. - */ -export function groupAlerts(objects) { - // Step 1: Filter out all non firing alerts - const filteredObjects = objects.filter((obj) => obj.metric.alertstate === 'firing'); - const groupedObjects = new Map(); - // Group by 3 values to make sure were not losing data'component' - for (const obj of filteredObjects) { - const key = - obj.metric.alertname + obj.metric.namespace + obj.metric.component + obj.metric.severity; - - // If the key already exists in the map, merge the values after deduplication - if (groupedObjects.has(key)) { - const existingObj = groupedObjects.get(key); - - // Deduplicate the incoming obj.values before concatenating - const existingValuesSet = new Set(existingObj.values.map((v) => JSON.stringify(v))); - const newValues = obj.values.filter((v) => !existingValuesSet.has(JSON.stringify(v))); - - // Concatenate non-duplicate values - existingObj.values = existingObj.values.concat(newValues); - - // Ensure metric uniqueness based on fields like alertname, severity, etc. - const existingMetricsSet = new Set(JSON.stringify(existingObj.metric)); - if (!existingMetricsSet.has(JSON.stringify(obj.metric))) { - groupedObjects.set(key, { - ...existingObj, - values: existingObj.values, - }); - } - } else { - // Otherwise, create a new entry with deduplicated values - groupedObjects.set(key, { - metric: obj.metric, - values: [...new Set(obj.values.map((v) => JSON.stringify(v)))].map((v) => JSON.parse(v)), - }); - } - } - - return Array.from(groupedObjects.values()); -} - -/** - * Processes a list of alert data, filters out 'Watchdog' alerts, groups them by component, - * and converts their timestamps to JavaScript `Date` objects. Additionally, it computes - * the start and end times for when the alerts started and ended firing. - * - * @param {Array} data - An array of alert objects containing metric and values information. - * @param {Object} data[].metric - The metric object containing alert metadata. - * @param {string} data[].metric.alertname - The name of the alert. - * @param {string} data[].metric.namespace - The namespace from which the alert originated. - * @param {string} data[].metric.severity - The severity level of the alert (e.g., "warning", "critical"). - * @param {string} data[].metric.component - The component associated with the alert. - * @param {string} data[].metric.layer - The layer to which the alert belongs. - * @param {string} data[].metric.name - The name of the alert. - * @param {string} data[].metric.alertstate - The current state of the alert (e.g., "firing"). - * @param {Array>} data[].values - An array of values, where each value is an array - * containing a timestamp (as a number) and a string value. - * - * @returns {Array} - An array of processed alert objects, where each object includes metadata and processed values. - * Each alert object also contains the start and end firing times of the alert, as well as an `x` field - * representing its position in the firing list. - * - * @example - * const data = [ - * { - * metric: { - * alertname: "ClusterOperatorDegraded", - * namespace: "openshift-cluster-version", - * severity: "warning", - * component: "machine-config", - * layer: "compute", - * name: "machine-config", - * alertstate: "firing" - * }, - * values: [[1627897545, "2"], [1627897545, "3"]] - * }, - * { - * metric: { - * alertname: "Watchdog", - * namespace: "openshift-monitoring", - * severity: "info", - * component: "monitoring", - * layer: "monitoring", - * name: "watchdog", - * alertstate: "firing" - * }, - * values: [[1627897545, "1"]] - * } - * ]; - * - * const result = processAlerts(data); - * // Output: - * // [ - * // { - * // alertname: "ClusterOperatorDegraded", - * // namespace: "openshift-cluster-version", - * // severity: "warning", - * // component: "machine-config", - * // layer: "compute", - * // name: "machine-config", - * // alertstate: "firing", - * // values: [[Date, "2"], [Date, "3"]], - * // alertsStartFiring: Date, - * // alertsEndFiring: Date, - * // x: 1 - * // } - * // ] - */ - -export function processAlerts(data, selectedIncidents) { - const firing = groupAlerts(data).filter((alert) => alert.metric.alertname !== 'Watchdog'); - - // Extract the first and last timestamps from selectedIncidents - const timestamps = selectedIncidents.flatMap((incident) => - incident.values.map((value) => new Date(value[0])), - ); - - const firstTimestamp = new Date(Math.min(...timestamps)); - const lastTimestamp = new Date(Math.max(...timestamps)); - - return sortObjectsByEarliestTimestamp(firing).map((alert, index) => { - // Filter values based on firstTimestamp and lastTimestamp keep only values within range - const processedValues = alert.values - .map((value) => { - const timestamp = new Date(value[0] * 1000); - return [timestamp, value[1]]; - }) - .filter(([date]) => date >= firstTimestamp && date <= lastTimestamp); - - const sortedValues = processedValues.sort((a, b) => a[0] - b[0]); - - const alertsStartFiring = sortedValues[0][0]; - const alertsEndFiring = sortedValues[sortedValues.length - 1][0]; - const resolved = new Date() - alertsEndFiring > 10 * 60 * 1000; - - return { - alertname: alert.metric.alertname, - namespace: alert.metric.namespace, - severity: alert.metric.severity, - component: alert.metric.component, - layer: alert.metric.layer, - name: alert.metric.name, - alertstate: resolved ? 'resolved' : 'firing', - values: sortedValues, - alertsStartFiring, - alertsEndFiring, - resolved, - x: firing.length - index, - }; - }); -} - -export const groupAlertsForTable = (alerts) => { - // group alerts by the component and coun - const groupedAlerts = alerts.reduce((acc, alert) => { - const { component, alertstate, severity, layer } = alert; - const existingGroup = acc.find((group) => group.component === component); - if (existingGroup) { - existingGroup.alertsExpandedRowData.push(alert); - if (severity === 'warning') existingGroup.warning += 1; - else if (severity === 'info') existingGroup.info += 1; - else if (severity === 'critical') existingGroup.critical += 1; - } else { - acc.push({ - component, - alertstate, - layer, - warning: severity === 'warning' ? 1 : 0, - info: severity === 'info' ? 1 : 0, - critical: severity === 'critical' ? 1 : 0, - alertsExpandedRowData: [alert], - }); - } - - return acc; - }, []); - // Update alertstate for each grouped component - groupedAlerts.forEach((group) => { - const hasFiring = group.alertsExpandedRowData.some((alert) => alert.alertstate === 'firing'); - const allResolved = group.alertsExpandedRowData.every( - (alert) => alert.alertstate === 'resolved', - ); - - if (hasFiring) { - group.alertstate = 'firing'; - } else if (allResolved) { - group.alertstate = 'resolved'; - } - }); - - return groupedAlerts; -}; diff --git a/web/src/components/Incidents/processIncidents.ts b/web/src/components/Incidents/processIncidents.ts deleted file mode 100644 index ebe05b4e..00000000 --- a/web/src/components/Incidents/processIncidents.ts +++ /dev/null @@ -1,208 +0,0 @@ -/* eslint-disable max-len */ - -// Define the interface for Metric -interface Metric { - group_id: string; // The unique ID for grouping - component: string; // Component name - componentList?: string[]; // List of all unique components - [key: string]: any; // Allow other dynamic fields in Metric -} - -interface Incident { - metric: Metric; - values: Array<[number, string]>; -} - -interface ProcessedIncident { - component: string; - componentList?: string[]; - group_id: string; - severity: string; - alertname: string; - namespace: string; - name: string; - layer: string; - values: Array<[Date, string]>; - x: number; - informative: boolean; - critical: string; - warning: string; - resolved: boolean; - firing: boolean; -} - -//this will be moved to the utils.js file when I convert them to the Typescript -export function sortObjectsByEarliestTimestamp(incidents: Incident[]): Incident[] { - return incidents.sort((a, b) => { - const earliestA = Math.min(...a.values.map((value) => value[0])); - const earliestB = Math.min(...b.values.map((value) => value[0])); - return earliestA - earliestB; - }); -} - -export function processIncidents(data: Incident[]): ProcessedIncident[] { - const incidents = groupById(data).filter( - (incident) => incident.metric.src_alertname !== 'Watchdog', - ); - const sortedIncidents = sortObjectsByEarliestTimestamp(incidents); - - return sortedIncidents.map((incident, index) => { - const processedValues = incident.values.map((value) => { - const timestamp = value[0]; - const date = new Date(timestamp * 1000); - return [date, value[1]] as [Date, string]; - }); - - // Determine severity flags based on values array - let critical = false; - let warning = false; - let informative = false; - - incident.values.forEach((value) => { - const severity = value[1]; // Second index of the value array - if (severity === '2') critical = true; - if (severity === '1') warning = true; - if (severity === '0') informative = true; - }); - - const timestamps = incident.values.map((value) => value[0]); // Extract timestamps - const lastTimestamp = Math.max(...timestamps); // Last timestamp in seconds - const currentDate = new Date(); - const currentTimestamp = Math.floor(currentDate.valueOf() / 1000); // Current time in seconds - - // Firing and resolved logic - const firing = currentTimestamp - lastTimestamp <= 10 * 60; - const resolved = !firing; - - // Persistent logic based on the first occurrence - - const srcProperties = getSrcProperties(incident.metric); - - return { - component: incident.metric.component, - componentList: incident.metric.componentList, - group_id: incident.metric.group_id, - layer: incident.metric.layer, - values: processedValues, - x: incidents.length - index, - critical, // Updated based on 'values' array - warning, // Updated based on 'values' array - informative, // Updated based on 'values' array - resolved: resolved, - firing: firing, - ...srcProperties, - } as unknown as ProcessedIncident; - }); -} - -/** - * Extracts properties from the metric that start with 'src_' and returns them in an object. - * - * @param metric - The metric object from which source properties are extracted. - * @returns An object containing only the properties from metric that start with 'src_'. - */ - -function getSrcProperties(metric: Metric): Partial { - return Object.keys(metric) - .filter((key) => key.startsWith('src_')) - .reduce((acc, key) => { - acc[key] = metric[key as keyof Metric]; - return acc; - }, {} as Partial); -} - -/** - * Groups a list of alert objects by their `group_id` field, merges their values, and deduplicates. - * Creates a combined object with deduplicated values and lists of components and layers for each unique `group_id`. - * - * @param objects - Array of alert objects to group by `group_id`. - * @returns Array of grouped alert objects with deduplicated values and combined properties. - */ - -export function groupById(objects: Incident[]): Incident[] { - const groupedObjects = new Map(); - - for (const obj of objects) { - const key = obj.metric.group_id; - - const existingObj = groupedObjects.get(key); - - if (existingObj) { - const existingValuesSet = new Set(existingObj.values.map((v) => JSON.stringify(v))); - const newValues = obj.values.filter((v) => !existingValuesSet.has(JSON.stringify(v))); - - existingObj.values = existingObj.values.concat(newValues); - - // Add or update the componentList - if (!existingObj.metric.componentList) { - existingObj.metric.componentList = [existingObj.metric.component]; - } - - if ( - existingObj.metric.component !== obj.metric.component && - !existingObj.metric.componentList.includes(obj.metric.component) - ) { - existingObj.metric.componentList.push(obj.metric.component); - } - - groupedObjects.set(key, { - ...existingObj, - values: existingObj.values, - }); - } else { - groupedObjects.set(key, { - metric: { - ...obj.metric, - componentList: [obj.metric.component], // Initialize componentList with the current component - }, - values: [...new Set(obj.values.map((v) => JSON.stringify(v)))].map((v) => JSON.parse(v)), - }); - } - } - - return Array.from(groupedObjects.values()); -} - -const QUERY_CHUNK_SIZE = 24 * 60 * 60 * 1000; - -/** - * Calculates time ranges for incidents based on a given timespan, split into daily intervals. - * - * @param timespan - The total timespan for which to calculate ranges. - * @param maxEndTime - The maximum end time for the ranges, defaulting to the current time. - * @returns Array of time range objects, each with `endTime` and `duration` for a daily interval. - */ - -export const getIncidentsTimeRanges = ( - timespan: number, - maxEndTime: number = Date.now(), -): Array<{ endTime: number; duration: number }> => { - const startTime = maxEndTime - timespan; - const timeRanges = [{ endTime: startTime + QUERY_CHUNK_SIZE, duration: QUERY_CHUNK_SIZE }]; - - while (timeRanges.length > 0 && timeRanges[timeRanges.length - 1].endTime < maxEndTime) { - const lastRange = timeRanges[timeRanges.length - 1]; - const nextEndTime = lastRange.endTime + QUERY_CHUNK_SIZE; - timeRanges.push({ endTime: nextEndTime, duration: QUERY_CHUNK_SIZE }); - } - - return timeRanges; -}; - -export const processIncidentsForAlerts = (incidents) => { - return incidents.map((incident, index) => { - // Process the values - const processedValues = incident.values.map((value): [Date, string] => { - const timestamp = value[0]; - const date = new Date(timestamp * 1000); - return [date, value[1]]; - }); - - // Return the processed incident - return { - ...incident.metric, - values: processedValues, - x: incidents.length - index, - }; - }); -}; diff --git a/web/src/components/Incidents/utils.ts b/web/src/components/Incidents/utils.ts deleted file mode 100644 index e6080f40..00000000 --- a/web/src/components/Incidents/utils.ts +++ /dev/null @@ -1,567 +0,0 @@ -/* eslint-disable max-len */ -import { useEffect, useState } from 'react'; -import { setIncidentsActiveFilters } from '../../actions/observe'; -import global_danger_color_100 from '@patternfly/react-tokens/dist/esm/global_danger_color_100'; -import global_info_color_100 from '@patternfly/react-tokens/dist/esm/global_info_color_100'; -import global_warning_color_100 from '@patternfly/react-tokens/dist/esm/global_warning_color_100'; - -type Timestamps = [Date, string]; - -type SpanDates = [Date]; - -type Theme = 'dark' | 'light'; - -type AlertsIntervalsArray = [Date, Date, 'data' | 'nodata']; - -type Incident = { - component: string; - componentList: Array; - critical: boolean; - informative: boolean; - warning: boolean; - resolved: boolean; - layer: string; - firing: boolean; - group_id: string; - src_severity: string; - src_alertname: string; - src_namespace: string; - x: number; - values: Array; -}; - -type Alert = { - alertname: string; - alertsStartFiring: Date; - alertsEndFiring: Date; - alertstate: string; - component: string; - layer: string; - name: string; - namespace: string; - resolved: boolean; - severity: 'critical' | 'warning' | 'info'; - x: number; - values: Array; -}; - -type DaysFilters = '1 day' | '3 days' | '7 days' | '15 days'; - -type IncidentFilters = 'Critical' | 'Warning' | 'Firing' | 'Informative' | 'Resolved'; - -type IncidentFiltersCombined = { - days: Array; - incidentFilters: Array; -}; - -/** - * Consolidates and merges intervals based on severity rankings. - * @param {Object} data - The input data containing timestamps and severity levels. - * @param {string[]} dateArray - The array of date strings defining the boundary. - * @returns {Array} - The consolidated intervals. - */ - -function consolidateAndMergeIntervals(data: Incident, dateArray: SpanDates) { - const severityRank = { 2: 2, 1: 1, 0: 0 }; - const filteredValues = filterAndSortValues(data, severityRank); - return generateIntervalsWithGaps(filteredValues, dateArray); -} - -/** - * Filters and sorts values by severity, keeping only the highest severity for each timestamp. - * @param {Object} data - The input data containing timestamps and severities. - * @param {Object} severityRank - An object mapping severity levels to their ranking values. - * @returns {Array} - An array of sorted timestamps with their severities. - */ -function filterAndSortValues( - data: Incident, - severityRank: Record, -): Array<[Date, string]> { - const highestSeverityValues: Record = data.values.reduce( - (acc: Record, [timestamp, severity]) => { - const timestampStr = timestamp.toISOString(); - - if (!acc[timestampStr] || severityRank[severity] > severityRank[acc[timestampStr]]) { - acc[timestampStr] = severity; - } - return acc; - }, - {}, - ); - - return Object.entries(highestSeverityValues) - .map(([timestamp, severity]) => [new Date(timestamp), severity] as [Date, string]) - .sort((a, b) => a[0].getTime() - b[0].getTime()); -} - -/** - * Generates intervals while handling gaps with "nodata". - * @param {Array} filteredValues - The sorted array of timestamps with severities. - * @param {string[]} dateArray - The array defining the start and end boundaries. - * @returns {Array} - The list of consolidated intervals. - */ -function generateIntervalsWithGaps(filteredValues: Array, dateArray: SpanDates) { - const intervals = []; - const startBoundary = new Date(dateArray[0]); - const endBoundary = new Date(dateArray[dateArray.length - 1]); - - let currentStart = filteredValues[0] ? filteredValues[0][0] : startBoundary.toISOString(); - let currentSeverity = filteredValues[0] ? filteredValues[0][1] : 'nodata'; - - if (!filteredValues.length) { - intervals.push([startBoundary.toISOString(), endBoundary.toISOString(), 'nodata']); - return intervals; - } - - const firstTimestamp = new Date(filteredValues[0][0]); - if (firstTimestamp > startBoundary) { - intervals.push([ - startBoundary.toISOString(), - new Date(firstTimestamp.getTime() - 1).toISOString(), - 'nodata', - ]); - } - - for (let i = 0; i < filteredValues.length; i++) { - const [timestamp, severity] = filteredValues[i]; - - if (i > 0 && hasGap(filteredValues, i)) { - intervals.push(createNodataInterval(filteredValues, i)); - } - - if (currentSeverity !== severity || i === 0) { - if (i > 0) { - const endDate = new Date(timestamp); - endDate.setMilliseconds(endDate.getMilliseconds() - 1); - intervals.push([currentStart, endDate.toISOString(), currentSeverity]); - } - currentStart = timestamp; - currentSeverity = severity; - } - } - - const lastEndDate = new Date(filteredValues[filteredValues.length - 1][0]); - intervals.push([currentStart, lastEndDate.toISOString(), currentSeverity]); - - if (lastEndDate < endBoundary) { - intervals.push([ - new Date(lastEndDate.getTime() + 1).toISOString(), - endBoundary.toISOString(), - 'nodata', - ]); - } - - return intervals; -} - -/** - * Checks if there is a gap larger than 5 minutes between consecutive timestamps. - * @param {Array} filteredValues - The array of filtered timestamps and severities. - * @param {number} index - The current index in the array. - * @returns {boolean} - Whether a gap exists. - */ -function hasGap(filteredValues: Array, index: number) { - const previousTimestamp = new Date(filteredValues[index - 1][0]); - const currentTimestamp = new Date(filteredValues[index][0]); - return (currentTimestamp.getTime() - previousTimestamp.getTime()) / 1000 / 60 > 5; -} - -/** - * Creates a "nodata" interval to fill gaps between timestamps. - * @param {Array} filteredValues - The array of filtered timestamps and severities. - * @param {number} index - The current index in the array. - * @returns {Array} - The "nodata" interval. - */ -function createNodataInterval(filteredValues: Array, index: number) { - const previousTimestamp = new Date(filteredValues[index - 1][0]); - const currentTimestamp = new Date(filteredValues[index][0]); - - const gapStart = new Date(previousTimestamp); - gapStart.setMilliseconds(gapStart.getMilliseconds() + 1); - - const gapEnd = new Date(currentTimestamp); - gapEnd.setMilliseconds(gapEnd.getMilliseconds() - 1); - - return [gapStart.toISOString(), gapEnd.toISOString(), 'nodata']; -} - -/** - * Creates an array of incident data for chart bars, ensuring that when two severities have the same time range, the lower severity is removed. - * - * @param {Object} incident - The incident data containing values with timestamps and severity levels. - * @returns {Array} - An array of incident objects with `y0`, `y`, `x`, and `name` fields representing the bars for the chart. - */ -export const createIncidentsChartBars = ( - incident: Incident, - theme: Theme, - dateArray: SpanDates, -) => { - const groupedData = consolidateAndMergeIntervals(incident, dateArray); - const data = []; - const getSeverityName = (value) => { - return value === '2' ? 'Critical' : value === '1' ? 'Warning' : 'Info'; - }; - const barChartColorScheme = { - critical: theme === 'light' ? global_danger_color_100.var : '#C9190B', - info: theme === 'light' ? global_info_color_100.var : '#06C', - warning: theme === 'light' ? global_warning_color_100.var : '#F0AB00', - }; - for (let i = 0; i < groupedData.length; i++) { - const severity = getSeverityName(groupedData[i][2]); - - data.push({ - y0: new Date(groupedData[i][0]), - y: new Date(groupedData[i][1]), - x: incident.x, - name: severity, - firing: incident.firing, - componentList: incident.componentList || [], - group_id: incident.group_id, - nodata: groupedData[i][2] === 'nodata' ? true : false, - fill: - severity === 'Critical' - ? barChartColorScheme.critical - : severity === 'Warning' - ? barChartColorScheme.warning - : barChartColorScheme.info, - }); - } - - return data; -}; - -function consolidateAndMergeAlertIntervals(data: Alert, dateArray: SpanDates) { - const sortedValues = data.values.sort( - (a, b) => new Date(a[0]).getTime() - new Date(b[0]).getTime(), - ); - - const intervals: Array = []; - let currentStart = sortedValues[0][0], - previousTimestamp = new Date(currentStart); - - for (let i = 1; i < sortedValues.length; i++) { - const currentTimestamp = new Date(sortedValues[i][0]); - const timeDifference = (currentTimestamp.getTime() - previousTimestamp.getTime()) / 60000; // Convert to minutes - - if (timeDifference > 5) { - intervals.push([currentStart, sortedValues[i - 1][0], 'data']); - intervals.push([ - new Date(previousTimestamp.getTime() + 1), - new Date(currentTimestamp.getTime() - 1), - 'nodata', - ]); - currentStart = sortedValues[i][0]; - } - previousTimestamp = currentTimestamp; - } - - intervals.push([currentStart, sortedValues[sortedValues.length - 1][0], 'data']); - - // Handle gaps before and after the detected intervals - const startBoundary = new Date(dateArray[0]), - endBoundary = new Date(dateArray[dateArray.length - 1]); - const firstIntervalStart = new Date(intervals[0][0]), - lastIntervalEnd = new Date(intervals[intervals.length - 1][1]); - - if (firstIntervalStart > startBoundary) { - intervals.unshift([startBoundary, new Date(firstIntervalStart.getTime() - 1), 'nodata']); - } - if (lastIntervalEnd < endBoundary) { - intervals.push([new Date(lastIntervalEnd.getTime() + 1), endBoundary, 'nodata']); - } - - return intervals; -} - -export const createAlertsChartBars = (alert: Alert, theme: Theme, dateValues: SpanDates) => { - const groupedData = consolidateAndMergeAlertIntervals(alert, dateValues); - const barChartColorScheme = { - critical: theme === 'light' ? global_danger_color_100.var : '#C9190B', - info: theme === 'light' ? global_info_color_100.var : '#06C', - warning: theme === 'light' ? global_warning_color_100.var : '#F0AB00', - }; - - const data = []; - - for (let i = 0; i < groupedData.length; i++) { - data.push({ - y0: new Date(groupedData[i][0]), - y: new Date(groupedData[i][1]), - x: alert.x, - severity: alert.severity[0].toUpperCase() + alert.severity.slice(1), - name: alert.alertname, - namespace: alert.namespace, - layer: alert.layer, - component: alert.component, - nodata: groupedData[i][2] === 'nodata' ? true : false, - alertstate: alert.alertstate, - fill: - alert.severity === 'critical' - ? barChartColorScheme.critical - : alert.severity === 'warning' - ? barChartColorScheme.warning - : barChartColorScheme.info, - }); - } - - return data; -}; - -export const formatDate = (date: Date, isTime: boolean) => { - const userLocale = navigator.language || 'en-US'; - const dateString = date?.toLocaleDateString(userLocale, { - day: 'numeric', - month: 'short', - year: 'numeric', - }); - const timeString = date?.toLocaleTimeString(userLocale, { - hour: '2-digit', - minute: '2-digit', - }); - return isTime ? `${dateString}, ${timeString}` : dateString; -}; - -/** - * Generates an array of dates, each representing midnight (00:00:00) of the past `days` number of days, starting from today. - * - * @param {number} days - The number of days for which to generate the date array. The array will contain dates starting from `days` ago up to today. - * @returns {Array} An array of `Date` objects, each set to midnight (00:00:00) in UTC, for the past `days` number of days. - * - * @description - * This function creates an array of `Date` objects, starting from `days` ago up to the current day. Each date in the array is set to midnight (00:00:00) to represent the start of the day. - * - * The function works by subtracting days from the current date and setting the time to 00:00:00 for each day. - * - * @example - * // Generate an array of 7 days (last 7 days including today) - * const dateArray = generateDateArray(7); - * // Output example: - * // [ - * // 2024-09-06T00:00:00.000Z, - * // 2024-09-07T00:00:00.000Z, - * // 2024-09-08T00:00:00.000Z, - * // 2024-09-09T00:00:00.000Z, - * // 2024-09-10T00:00:00.000Z, - * // 2024-09-11T00:00:00.000Z, - * // 2024-09-12T00:00:00.000Z - * // ] - */ -export function generateDateArray(days: number) { - const currentDate = new Date(); - - const dateArray = []; - for (let i = 0; i < days; i++) { - const newDate = new Date(currentDate); - newDate.setDate(currentDate.getDate() - (days - 1 - i)); - newDate.setHours(0, 0, 0, 0); - dateArray.push(newDate); - } - - return dateArray; -} - -/** - * Filters incidents based on the specified filters. - * - * @param {Object} filters - An object containing filter criteria. - * @param {string[]} filters.incidentFilters - An array of strings representing filter conditions such as "Critical", etc. - * @param {Array} incidents - An array of incidents to be filtered. - * @returns {Array} A filtered array of incidents that match at least one of the specified filters. - * - * The `conditions` object maps filter keys to incident properties. If no filters are applied, all incidents are returned. - * Filters are case-sensitive and must match the keys defined in the `conditions` object. - * - * Example usage: - * ```javascript - * const filters = { incidentFilters: ["Critical", "Firing"] }; - * const filteredIncidents = filterIncident(filters, incidents); - * ``` - */ -export function filterIncident(filters: IncidentFiltersCombined, incidents: Array) { - const conditions = { - Critical: 'critical', - Warning: 'warning', - Informative: 'informative', - Firing: 'firing', - Resolved: 'resolved', - }; - - return incidents.filter((incident) => { - // If no filters are applied, return all incidents - if (!filters.incidentFilters.length) { - return incident; - } - - // Normalize user-provided filters to match keys in conditions - const normalizedFilters = filters.incidentFilters.map((filter) => filter.trim()); - - // Separate filters into categories - const severityFilters = ['Critical', 'Warning', 'Informative'].filter((key) => - normalizedFilters.includes(key), - ); - const statusFilters = ['Firing', 'Resolved'].filter((key) => normalizedFilters.includes(key)); - - // Match severity filters (OR behavior within the category) - const isSeverityMatch = - severityFilters.length > 0 - ? severityFilters.some((filter) => incident[conditions[filter]] === true) - : true; // True if no severity filters - - // Match status filters (OR behavior within the category) - const isStatusMatch = - statusFilters.length > 0 - ? statusFilters.some((filter) => incident[conditions[filter]] === true) - : true; // True if no status filters - - // Combine conditions with AND behavior between categories - return isSeverityMatch && isStatusMatch; - }); -} - -export const onDeleteIncidentFilterChip = ( - type: 'Filters' | '', - id: IncidentFilters, - filters: IncidentFiltersCombined, - setFilters, -) => { - if (type === 'Filters') { - setFilters( - setIncidentsActiveFilters({ - incidentsActiveFilters: { - incidentFilters: filters.incidentFilters.filter((fil) => fil !== id), - days: filters.days, - }, - }), - ); - } else { - setFilters( - setIncidentsActiveFilters({ - incidentsActiveFilters: { - incidentFilters: [], - days: ['7 days'], - }, - }), - ); - } -}; - -export const onDeleteGroupIncidentFilterChip = (filters: IncidentFiltersCombined, setFilters) => { - setFilters( - setIncidentsActiveFilters({ - incidentsActiveFilters: { - incidentFilters: [], - days: filters.days, - }, - }), - ); -}; - -export const makeIncidentUrlParams = (params: IncidentFiltersCombined) => { - const processedParams = Object.entries(params).reduce((acc, [key, value]) => { - if (Array.isArray(value)) { - if (value.length > 0) { - acc[key] = value.join(','); - } - } else { - acc[key] = value; - } - return acc; - }, {}); - - return new URLSearchParams(processedParams).toString(); -}; - -export const updateBrowserUrl = (params: IncidentFiltersCombined) => { - const queryString = makeIncidentUrlParams(params); - - // Construct the new URL with the query string - const newUrl = `${window.location.origin}${window.location.pathname}?${queryString}`; - - window.history.replaceState(null, '', newUrl); -}; - -export const changeDaysFilter = (days: DaysFilters, dispatch, filters: IncidentFiltersCombined) => { - dispatch( - setIncidentsActiveFilters({ - incidentsActiveFilters: { days: [days], incidentFilters: filters.incidentFilters }, - }), - ); -}; - -export const onIncidentFiltersSelect = ( - event, - selection: IncidentFilters, - dispatch, - incidentsActiveFilters: IncidentFiltersCombined, -) => { - onSelect(event, selection, dispatch, incidentsActiveFilters); -}; - -const onSelect = ( - event, - selection: IncidentFilters, - dispatch, - incidentsActiveFilters: IncidentFiltersCombined, -) => { - const checked = event.target.checked; - - dispatch((dispatch) => { - const prevSelections = incidentsActiveFilters.incidentFilters || []; - - const updatedSelections = checked - ? [...prevSelections, selection] - : prevSelections.filter((value) => value !== selection); - - dispatch( - setIncidentsActiveFilters({ - incidentsActiveFilters: { - ...incidentsActiveFilters, - incidentFilters: updatedSelections, - }, - }), - ); - }); -}; - -export const parseUrlParams = (search) => { - const params = new URLSearchParams(search); - const result = {}; - const arrayKeys = ['days', 'incidentFilters']; - - params.forEach((value, key) => { - if (arrayKeys.includes(key)) { - result[key] = value.includes(',') ? value.split(',') : [value]; - } else { - result[key] = value; - } - }); - - return result; -}; - -const PF_THEME_DARK_CLASS = 'pf-v5-theme-dark'; -const PF_THEME_DARK_CLASS_LEGACY = 'pf-theme-dark'; // legacy class name needed to support PF4 -/** - * The @openshift-console/dynamic-plugin-sdk package does not expose the theme setting of the user preferences, - * therefore check if the root element has the PatternFly css class set for the dark theme. - */ -function getTheme() { - const classList = document.documentElement.classList; - if (classList.contains(PF_THEME_DARK_CLASS) || classList.contains(PF_THEME_DARK_CLASS_LEGACY)) { - return 'dark'; - } - return 'light'; -} -/** - * In case the user sets "system default" theme in the user preferences, update the theme if the system theme changes. - */ -export function usePatternFlyTheme() { - const [theme, setTheme] = useState(getTheme()); - useEffect(() => { - const reloadTheme = () => setTheme(getTheme()); - const mq = window.matchMedia('(prefers-color-scheme: dark)'); - mq.addEventListener('change', reloadTheme); - return () => mq.removeEventListener('change', reloadTheme); - }, [setTheme]); - return { theme }; -} diff --git a/web/src/components/metrics.tsx b/web/src/components/MetricsPage.tsx similarity index 96% rename from web/src/components/metrics.tsx rename to web/src/components/MetricsPage.tsx index db809dc3..f3182767 100644 --- a/web/src/components/metrics.tsx +++ b/web/src/components/MetricsPage.tsx @@ -2,12 +2,12 @@ import { PrometheusData, PrometheusEndpoint, PrometheusLabels, + PrometheusResponse, useActiveNamespace, useResolvedExtensions, YellowExclamationTriangleIcon, } from '@openshift-console/dynamic-plugin-sdk'; import { - ActionGroup, Bullseye, Button, Dropdown, @@ -17,6 +17,8 @@ import { EmptyStateBody, EmptyStateIcon, EmptyStateVariant, + Flex, + FlexItem, Grid, GridItem, MenuToggle, @@ -26,6 +28,8 @@ import { SelectOptionProps, Split, SplitItem, + Stack, + StackItem, Switch, Title, Tooltip, @@ -87,6 +91,9 @@ import { isDataSource, } from '@openshift-console/dynamic-plugin-sdk/lib/extensions/dashboard-data-source'; import { MonitoringState } from '../reducers/observe'; +import { TypeaheadSelect } from './TypeaheadSelect'; +import withFallback from './console/console-shared/error/fallbacks/withFallback'; +import { LoadingInline } from './console/console-shared/src/components/loading/LoadingInline'; import { DropDownPollInterval } from './dropdown-poll-interval'; import { useBoolean } from './hooks/useBoolean'; import { getLegacyObserveState, usePerspective } from './hooks/usePerspective'; @@ -95,9 +102,6 @@ import { colors, Error, QueryBrowser } from './query-browser'; import { QueryParams } from './query-params'; import TablePagination from './table-pagination'; import { PrometheusAPIError } from './types'; -import { TypeaheadSelect } from './TypeaheadSelect'; -import { LoadingInline } from './console/console-shared/src/components/loading/LoadingInline'; -import withFallback from './console/console-shared/error/fallbacks/withFallback'; // Stores information about the currently focused query input let focusedQuery; @@ -239,7 +243,7 @@ export const PreDefinedQueriesDropdown = () => { }; return ( - + @@ -320,14 +324,13 @@ export const ToggleGraph: React.FC = () => { const icon = hideGraphs ? : ; return ( - + + + + + ); }; @@ -384,24 +387,19 @@ const SeriesButton: React.FC = ({ index, labels }) => { ); if (isSeriesEmpty) { - return
; + return null; } const title = isDisabled ? t('Show series') : t('Hide series'); return ( -
-
+