From abd4d8c670e2ee1dca2f81c3c785508293af5a28 Mon Sep 17 00:00:00 2001 From: Jenny Zhu Date: Fri, 30 May 2025 11:18:48 -0400 Subject: [PATCH] fix: ACM filtering and 'Export as CSV' link --- .../alerting/AlertList/DownloadCSVButton.tsx | 70 +++++++++++++++ web/src/components/alerting/AlertsPage.tsx | 86 ++++++++----------- 2 files changed, 104 insertions(+), 52 deletions(-) create mode 100644 web/src/components/alerting/AlertList/DownloadCSVButton.tsx diff --git a/web/src/components/alerting/AlertList/DownloadCSVButton.tsx b/web/src/components/alerting/AlertList/DownloadCSVButton.tsx new file mode 100644 index 00000000..f21d0e6f --- /dev/null +++ b/web/src/components/alerting/AlertList/DownloadCSVButton.tsx @@ -0,0 +1,70 @@ +import { Button, ButtonVariant } from '@patternfly/react-core'; +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { usePerspective } from '../../hooks/usePerspective'; +import { AggregatedAlert } from '../AlertsAggregates'; + +type DownloadCSVButtonProps = { + loaded: boolean; + filteredData: AggregatedAlert[]; +}; + +const DownloadCSVButton: FC = ({ loaded, filteredData }) => { + const { perspective } = usePerspective(); + const { t } = useTranslation(process.env.I18N_NAMESPACE); + + const getTableData = () => { + const csvColumns = ['Name', 'Severity', 'State', 'Total']; + if (perspective === 'acm') { + csvColumns.push('Cluster'); + } + const getCsvRows = () => { + return filteredData?.map((row) => { + const name = row?.name ?? ''; + const severity = row?.severity ?? ''; + const state = Array.from(row?.states || [])?.join(', '); + const total = row?.alerts?.length ?? 0; + const rowData = [name, severity, state, total]; + if (perspective === 'acm') { + const clusters = Array.from( + new Set(row?.alerts?.map((alert) => alert.labels?.cluster) || []), + ); + rowData.push(clusters?.join(', ') ?? ''); + } + return rowData; + }); + }; + return [csvColumns, ...getCsvRows()]; + }; + + const formatToCsv = (tableData, delimiter = ',') => + tableData + ?.map((row) => + row?.map((rowItem) => (isNaN(rowItem) ? `"${rowItem}"` : rowItem)).join(delimiter), + ) + ?.join('\n'); + + let csvData: string; + if (loaded) { + csvData = formatToCsv(getTableData()) ?? undefined; + } + + const downloadCsv = () => { + // csvData should be formatted as comma-seperated values + // (e.g. `"a","b","c", \n"d","e","f", \n"h","i","j"`) + const blobCsvData = new Blob([csvData], { type: 'text/csv' }); + const csvURL = URL.createObjectURL(blobCsvData); + const link = document.createElement('a'); + link.href = csvURL; + link.download = `openshift.csv`; + link.click(); + }; + + return ( + + ); +}; + +export default DownloadCSVButton; diff --git a/web/src/components/alerting/AlertsPage.tsx b/web/src/components/alerting/AlertsPage.tsx index 96a28d95..c4052b32 100644 --- a/web/src/components/alerting/AlertsPage.tsx +++ b/web/src/components/alerting/AlertsPage.tsx @@ -1,42 +1,37 @@ -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import { getLegacyObserveState, usePerspective } from '../hooks/usePerspective'; -import { Alerts, AlertSource } from '../types'; -import { useSelector } from 'react-redux'; - -import * as _ from 'lodash-es'; -import { - alertCluster, - alertingRuleSource, - alertSource, - getAdditionalSources, - SilencesNotLoadedWarning, -} from './AlertUtils'; import { Alert, - AlertSeverity, AlertStates, ListPageFilter, RowFilter, useListPageFilter, } from '@openshift-console/dynamic-plugin-sdk'; -import { alertState, fuzzyCaseInsensitive } from '../utils'; import { Table, TableGridBreakpoint, Th, Thead, Tr } from '@patternfly/react-table'; +import * as _ from 'lodash-es'; +import * as React from 'react'; import { Helmet } from 'react-helmet'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; import { MonitoringState } from '../../reducers/observe'; -import { EmptyBox } from '../console/console-shared/src/components/empty-state/EmptyBox'; import withFallback from '../console/console-shared/error/fallbacks/withFallback'; +import { EmptyBox } from '../console/console-shared/src/components/empty-state/EmptyBox'; import { LoadingBox } from '../console/console-shared/src/components/loading/LoadingBox'; -import { AggregatedAlert, getAggregateAlertsLists } from './AlertsAggregates'; - -import './alert-table.scss'; -import Error from './Error'; +import { getLegacyObserveState, usePerspective } from '../hooks/usePerspective'; +import { Alerts, AlertSource } from '../types'; +import { alertState, fuzzyCaseInsensitive } from '../utils'; import AggregateAlertTableRow from './AlertList/AggregateAlertTableRow'; import useAggregateAlertColumns from './AlertList/hooks/useAggregateAlertColumns'; +import { getAggregateAlertsLists } from './AlertsAggregates'; +import { + alertCluster, + alertSource, + getAdditionalSources, + severityRowFilter, + SilencesNotLoadedWarning, +} from './AlertUtils'; +import Error from './Error'; import useSelectedFilters from './useSelectedFilters'; - -import './alert-table.scss'; import { PageSection, PageSectionVariants } from '@patternfly/react-core/dist/esm'; +import DownloadCSVButton from './AlertList/DownloadCSVButton'; const AlertsPage_: React.FC = () => { const { t } = useTranslation(process.env.I18N_NAMESPACE); @@ -54,8 +49,6 @@ const AlertsPage_: React.FC = () => { getLegacyObserveState(perspective, state)?.get(silencesKey)?.loadError, ); - const aggregatedAlertsList = getAggregateAlertsLists(data); - const alertAdditionalSources = React.useMemo( () => getAdditionalSources(data, alertSource), [data], @@ -78,8 +71,8 @@ const AlertsPage_: React.FC = () => { // TODO: The "name" filter doesn't really fit useListPageFilter's idea of a RowFilter, but // useListPageFilter doesn't yet provide a better way to add a filter like this { - filter: (filter, alert: AggregatedAlert) => - fuzzyCaseInsensitive(filter.selected?.[0], alert.name), + filter: (filter, alert: Alert) => + fuzzyCaseInsensitive(filter.selected?.[0], alert.labels?.alertname), filterGroupName: '', items: [], type: 'name', @@ -96,24 +89,10 @@ const AlertsPage_: React.FC = () => { { id: AlertStates.Pending, title: t('Pending') }, { id: AlertStates.Silenced, title: t('Silenced') }, ], - isMatch: (aggregatedAlert: AggregatedAlert, id: AlertStates) => - aggregatedAlert.states.has(id), + reducer: alertState, type: 'alert-state', }, - { - defaultSelected: [AlertStates.Firing], - filter: (filter, aggregatedAlert: AggregatedAlert) => - _.isEmpty(filter.selected) || filter.selected?.includes(aggregatedAlert?.severity), - filterGroupName: t('Severity'), - items: [ - { id: AlertSeverity.Critical, title: t('Critical') }, - { id: AlertSeverity.Warning, title: t('Warning') }, - { id: AlertSeverity.Info, title: t('Info') }, - { id: AlertSeverity.None, title: t('None') }, - ], - reducer: (aggregatedAlert: AggregatedAlert) => aggregatedAlert.severity, - type: 'alert-severity', - }, + severityRowFilter(t), { defaultSelected: defaultAlertTenant, filter: (filter, alert: Alert) => @@ -126,8 +105,7 @@ const AlertsPage_: React.FC = () => { { id: AlertSource.User, title: t('User') }, ...alertAdditionalSources, ], - isMatch: (aggregatedAlert: AggregatedAlert, id: AlertSource) => - aggregatedAlert.alerts.some((alert) => id === alertingRuleSource(alert.rule)), + reducer: alertSource, type: 'alert-source', }, ]; @@ -156,14 +134,13 @@ const AlertsPage_: React.FC = () => { } as RowFilter); } - const [staticData, filteredData, onFilterChange] = useListPageFilter( - aggregatedAlertsList, - rowFilters, - ); + const [staticData, filteredData, onFilterChange] = useListPageFilter(data, rowFilters); const columns = useAggregateAlertColumns(); const selectedFilters = useSelectedFilters(); + const filteredAggregatedAlerts = getAggregateAlertsLists(filteredData); + return ( <> @@ -178,9 +155,12 @@ const AlertsPage_: React.FC = () => { onFilterChange={onFilterChange} rowFilters={rowFilters} /> + {loaded && filteredAggregatedAlerts?.length > 0 && ( + + )} {silencesLoadError && } - {filteredData?.length > 0 && loaded && ( + {filteredAggregatedAlerts?.length > 0 && loaded && ( @@ -191,7 +171,7 @@ const AlertsPage_: React.FC = () => { ))} - {filteredData.map((aggregatedAlert, index) => ( + {filteredAggregatedAlerts.map((aggregatedAlert, index) => ( { )} {loadError && } - {loaded && filteredData?.length === 0 && !loadError && } + {loaded && filteredAggregatedAlerts?.length === 0 && !loadError && ( + + )} {!loaded && }