From 0654f374340cfad3927835f87c5b78cc719072c5 Mon Sep 17 00:00:00 2001 From: Pini Shahmurov Date: Sun, 29 Dec 2024 16:39:48 +0200 Subject: [PATCH] =?UTF-8?q?Impl=20[Alerts=20history]=20Add=20=E2=80=9CExpa?= =?UTF-8?q?ndable=20Alert=20Table=E2=80=9D=20with=20Expanded=20Rows=20(#29?= =?UTF-8?q?82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/TabsSlider/TabsSlider.js | 2 +- src/components/ActionBar/ActionBar.js | 1 + src/components/Alerts/Alerts.js | 11 ++- src/components/Alerts/AlertsFilters.js | 18 ++-- src/components/Alerts/AlertsView.js | 65 +++++++++----- src/components/Alerts/alerts.scss | 21 ++--- src/components/Alerts/alerts.util.js | 14 +-- src/components/DetailsAlerts/DetailsAlerts.js | 86 +++++++++++++++++-- .../DetailsAlertsMetrics.js | 85 ++++++++---------- .../ModelEndpoints/modelEndpoints.util.js | 3 +- src/elements/AlertsTableRow/AlertsTableRow.js | 74 ++++++++++------ .../AlertsTableRow/AlertsTableRow.scss | 57 +++++------- src/hooks/useAlertsPageData.js | 11 +-- src/utils/createAlertsContent.js | 9 +- 14 files changed, 272 insertions(+), 185 deletions(-) diff --git a/src/common/TabsSlider/TabsSlider.js b/src/common/TabsSlider/TabsSlider.js index dc3b30202..a12fa3cc5 100644 --- a/src/common/TabsSlider/TabsSlider.js +++ b/src/common/TabsSlider/TabsSlider.js @@ -191,7 +191,7 @@ const TabsSlider = ({ className={tabClassName} data-tab={tab.id} to={generateUrlFromRouterPath( - `${window.location.pathname?.replace(/^$|([^/]+$)/, tab.id)}${location.search ?? ''}` + `${window.location.pathname?.replace(/^$|([^/]+$)/, tab.id)}${location.search ?? ''}${tab.query ?? ''}` )} onClick={() => onSelectTab(tab)} key={tab.id} diff --git a/src/components/ActionBar/ActionBar.js b/src/components/ActionBar/ActionBar.js index a8f0fe58b..5f3860bde 100644 --- a/src/components/ActionBar/ActionBar.js +++ b/src/components/ActionBar/ActionBar.js @@ -365,6 +365,7 @@ const ActionBar = ({ onChange={(dates, isPredefined, optionId) => handleDateChange(dates, isPredefined, optionId, input, formState) } + timeFrameLimit={filtersConfig[DATES_FILTER].timeFrameLimit} type="date-range-time" withLabels /> diff --git a/src/components/Alerts/Alerts.js b/src/components/Alerts/Alerts.js index 63acec3d6..b49d2f27e 100644 --- a/src/components/Alerts/Alerts.js +++ b/src/components/Alerts/Alerts.js @@ -25,8 +25,11 @@ import AlertsView from './AlertsView' import { ALERTS_PAGE } from '../../constants' import { createAlertRowData } from '../../utils/createAlertsContent' -import { getAlertsFiltersConfig, parseAlertsQueryParamsCallback } from './alerts.util' -import { generatePageData } from './alerts.util' +import { + getAlertsFiltersConfig, + generatePageData, + parseAlertsQueryParamsCallback +} from './alerts.util' import { getJobLogs } from '../../utils/getJobLogs.util' import projectsAction from '../../actions/projects' import { useAlertsPageData } from '../../hooks/useAlertsPageData' @@ -34,11 +37,11 @@ import { useFiltersFromSearchParams } from '../../hooks/useFiltersFromSearchPara const Alerts = () => { const [selectedAlert, setSelectedAlert] = useState({}) - const { id: projectId } = useParams() const [, setProjectsRequestErrorMessage] = useState('') const alertsStore = useSelector(state => state.alertsStore) const filtersStore = useSelector(store => store.filtersStore) const dispatch = useDispatch() + const { id: projectId } = useParams() const params = useParams() const isCrossProjects = useMemo(() => projectId === '*', [projectId]) @@ -93,7 +96,7 @@ const Alerts = () => { ) const pageData = useMemo( - () => generatePageData(handleFetchJobLogs, selectedAlert), + () => generatePageData(selectedAlert, handleFetchJobLogs), [handleFetchJobLogs, selectedAlert] ) diff --git a/src/components/Alerts/AlertsFilters.js b/src/components/Alerts/AlertsFilters.js index aae4bc896..8bab9f6ff 100644 --- a/src/components/Alerts/AlertsFilters.js +++ b/src/components/Alerts/AlertsFilters.js @@ -50,7 +50,7 @@ import { SEVERITY } from '../../constants' -const AlertsFilters = ({ isCrossProjects }) => { +const AlertsFilters = ({ isAlertsPage, isCrossProjects }) => { const form = useForm() const { values: { [ENTITY_TYPE]: entityType } @@ -94,13 +94,15 @@ const AlertsFilters = ({ isCrossProjects }) => { )} -
- -
+ {isAlertsPage && ( +
+ +
+ )} {(entityType === FILTER_ALL_ITEMS || entityType === MODEL_MONITORING_APPLICATION) && (
diff --git a/src/components/Alerts/AlertsView.js b/src/components/Alerts/AlertsView.js index 7804b476c..cc5ae9d50 100644 --- a/src/components/Alerts/AlertsView.js +++ b/src/components/Alerts/AlertsView.js @@ -17,6 +17,7 @@ illegal under applicable law, and the grant of the foregoing license under the Apache 2.0 license is conditioned upon your compliance with such restriction. */ +import classNames from 'classnames' import PropTypes from 'prop-types' import ActionBar from '../ActionBar/ActionBar' @@ -32,9 +33,12 @@ import { ALERTS_FILTERS, ALERTS_PAGE } from '../../constants' import { getNoDataMessage } from '../../utils/getNoDataMessage' import { getCloseDetailsAlertLink } from '../../utils/link-helper.util' +import './alerts.scss' + const AlertsView = ({ alertsFiltersConfig, alertsStore, + isAlertsPage = true, filters, filtersStore, handleCancel, @@ -46,16 +50,22 @@ const AlertsView = ({ requestErrorMessage, selectedAlert, setSearchParams, - tableContent + selectedRowData, + tableContent, + toggleRow }) => { + const content = classNames('content', !isAlertsPage && 'alerts-table__content') + return ( <>
-
- -
-
-
+ {isAlertsPage && ( +
+ +
+ )} +
+
- +
{alertsStore.loading ? ( @@ -88,27 +98,36 @@ const AlertsView = ({ <> getCloseDetailsAlertLink()} //TODO: the getCloseDetailsLink will be updated with ML-8368 + getCloseDetailsLink={() => getCloseDetailsAlertLink()} pageData={pageData} retryRequest={handleRefreshWithFilters} - selectedItem={selectedAlert} + selectedItem={isAlertsPage ? selectedAlert : {}} tableClassName="alerts-table" handleCancel={handleCancel} hideActionsMenu tableHeaders={tableContent[0]?.content ?? []} withActionMenu={false} > - {tableContent.map((tableItem, index) => ( - {}} - rowIndex={index} - rowItem={tableItem} - actionsMenu={[]} - selectedItem={selectedAlert} - /> - ))} + {tableContent.map((tableItem, index) => { + const isRowSelected = tableItem?.data?.id === selectedAlert?.id && !isAlertsPage + const selectedRowClassName = `${isRowSelected ? 'alert-row__cell--expanded-selected-cell' : ''} ` + return ( + {}} + filters={filters} + isRowSelected={isRowSelected} + rowIndex={index} + rowItem={tableItem} + actionsMenu={[]} + toggleRow={toggleRow} + selectedItem={selectedAlert} + selectedRowData={selectedRowData} + /> + ) + })}
{ +export const getAlertsFiltersConfig = (timeFrameLimit = false) => { return { [NAME_FILTER]: { label: 'Alert Name:', initialValue: '' }, [DATES_FILTER]: { label: 'Start time:', - initialValue: getDatePickerFilterValue(datePickerPastOptions, PAST_24_HOUR_DATE_OPTION) + initialValue: getDatePickerFilterValue(datePickerPastOptions, PAST_24_HOUR_DATE_OPTION), + timeFrameLimit: timeFrameLimit ? TIME_FRAME_LIMITS.MONTH : Infinity }, [PROJECTS_FILTER]: { label: 'Project:', initialValue: FILTER_ALL_ITEMS, isModal: true }, [ENTITY_TYPE]: { label: 'Entity Type:', initialValue: FILTER_ALL_ITEMS, isModal: true }, @@ -86,7 +88,7 @@ export const parseAlertsQueryParamsCallback = (paramName, paramValue) => { return paramValue } -export const generatePageData = (handleFetchJobLogs, selectedAlert) => { +export const generatePageData = (selectedAlert, handleFetchJobLogs = () => {}) => { return { page: ALERTS_PAGE, details: { @@ -109,8 +111,8 @@ export const allProjectsOption = [ export const filterAlertsEntityTypeOptions = [ { label: upperFirst(FILTER_ALL_ITEMS), id: FILTER_ALL_ITEMS }, { label: upperFirst(JOB), id: JOB_KIND_JOB }, - { label: upperFirst(ENDPOINT), id: 'model-endpoint-result' }, - { label: upperFirst(APPLICATION), id: 'model-monitoring-application' } + { label: upperFirst(ENDPOINT), id: MODEL_ENDPOINT_RESULT }, + { label: upperFirst(APPLICATION), id: MODEL_MONITORING_APPLICATION } ] export const filterAlertsSeverityOptions = [ diff --git a/src/components/DetailsAlerts/DetailsAlerts.js b/src/components/DetailsAlerts/DetailsAlerts.js index 881dbd832..935f9a1e2 100644 --- a/src/components/DetailsAlerts/DetailsAlerts.js +++ b/src/components/DetailsAlerts/DetailsAlerts.js @@ -17,16 +17,86 @@ illegal under applicable law, and the grant of the foregoing license under the Apache 2.0 license is conditioned upon your compliance with such restriction. */ +import React, { useCallback, useMemo, useState } from 'react' +import { useSelector } from 'react-redux' -import React from 'react' +import AlertsView from '../Alerts/AlertsView' + +import { createAlertRowData } from '../../utils/createAlertsContent' +import { + generatePageData, + getAlertsFiltersConfig, + parseAlertsQueryParamsCallback +} from '../../components/Alerts/alerts.util' +import { useAlertsPageData } from '../../hooks/useAlertsPageData' +import { useFiltersFromSearchParams } from '../../hooks/useFiltersFromSearchParams.hook' + +const DetailsAlerts = () => { + const [selectedAlert, setSelectedAlert] = useState({}) + const alertsStore = useSelector(state => state.alertsStore) + const filtersStore = useSelector(store => store.filtersStore) + + const alertsFiltersConfig = useMemo(() => getAlertsFiltersConfig(true), []) + + const alertsFilters = useFiltersFromSearchParams( + alertsFiltersConfig, + parseAlertsQueryParamsCallback + ) + + const { + handleRefreshAlerts, + paginatedAlerts, + paginationConfigAlertsRef, + requestErrorMessage, + refreshAlerts, + setAlerts, + setSearchParams + } = useAlertsPageData(alertsFilters, false) + + const handleRefreshWithFilters = useCallback( + filters => { + setAlerts([]) + + return refreshAlerts(filters) + }, + [refreshAlerts, setAlerts] + ) + + const tableContent = useMemo(() => { + return paginatedAlerts.map(alert => createAlertRowData(alert, false, true)) + }, [paginatedAlerts]) + + const pageData = useMemo(() => generatePageData(selectedAlert), [selectedAlert]) + + const toggleRow = useCallback( + (e, item) => { + setSelectedAlert(prev => { + const selectedAlert = tableContent.find(({ data }) => data?.id === item?.id) + return prev?.id !== item.id ? selectedAlert?.data || {} : {} + }) + }, + [tableContent] + ) -const DetailsAlerts = ({ selectedItem }) => { return ( -
-

Alerts

-

This tab shows alerts for the selected item.

-
+ ) } - -export default DetailsAlerts +export default React.memo(DetailsAlerts) diff --git a/src/components/DetailsDrillDownAlert/DetailsAlertsMetrics.js b/src/components/DetailsDrillDownAlert/DetailsAlertsMetrics.js index c90891f37..a0bee1bd8 100644 --- a/src/components/DetailsDrillDownAlert/DetailsAlertsMetrics.js +++ b/src/components/DetailsDrillDownAlert/DetailsAlertsMetrics.js @@ -34,6 +34,7 @@ import modelEndpointsActions from '../../actions/modelEndpoints' import { groupMetricByApplication } from '../../elements/MetricsSelector/metricsSelector.util' import { + CUSTOM_RANGE_DATE_OPTION, datePickerPastOptions, PAST_24_HOUR_DATE_OPTION, TIME_FRAME_LIMITS @@ -41,13 +42,13 @@ import { import { ReactComponent as MetricsIcon } from 'igz-controls/images/metrics-icon.svg' -const DetailsAlertsMetrics = ({ selectedItem }) => { +const DetailsAlertsMetrics = ({ selectedItem, filters, isAlertsPage = true }) => { const [metrics, setMetrics] = useState([]) const [requestErrorMessage, setRequestErrorMessage] = useState('') const metricsContainerRef = useRef(null) const metricsValuesAbortController = useRef(new AbortController()) const prevSelectedEndPointNameRef = useRef('') - const [metricOptionsAreLoaded, setMetricOptionsAreLoaded] = useState(false) + const detailsStore = useSelector(store => store.detailsStore) const dispatch = useDispatch() @@ -82,12 +83,6 @@ const DetailsAlertsMetrics = ({ selectedItem }) => { handleChangeDates(past24hoursOption.handler(), true, PAST_24_HOUR_DATE_OPTION) }, [handleChangeDates]) - useEffect(() => { - dispatch( - modelEndpointsActions.fetchModelEndpointMetrics(selectedItem.project, selectedItem.uid) - ).then(() => setMetricOptionsAreLoaded(true)) - }, [dispatch, selectedItem.project, selectedItem.uid]) - const fetchData = useCallback( (params, projectName, uid) => { metricsValuesAbortController.current = new AbortController() @@ -111,36 +106,27 @@ const DetailsAlertsMetrics = ({ selectedItem }) => { prevSelectedEndPointNameRef.current = selectedItem.uid return } + const params = { name: [selectedItem.fullName] } - if ( - metricOptionsAreLoaded && - selectedItem?.uid && - detailsStore.metricsOptions.all.length > 0 && - detailsStore.metricsOptions.selectedByEndpoint[selectedItem?.uid] - ) { - const params = { name: [selectedItem.fullName] } - - if (detailsStore.dates.value[0] && detailsStore.dates.value[1]) { - params.start = detailsStore.dates.value[0].getTime() - params.end = detailsStore.dates.value[1].getTime() - } + if (isAlertsPage && detailsStore.dates.value[0] && detailsStore.dates.value[1]) { + params.start = detailsStore.dates.value[0].getTime() + params.end = detailsStore.dates.value[1].getTime() + } - fetchData(params, selectedItem.project, selectedItem.uid).then() - } else { - setMetrics([]) + if (!isAlertsPage) { + if (filters?.dates?.initialSelectedOptionId === CUSTOM_RANGE_DATE_OPTION) { + params.start = filters?.dates.value[0].getTime() + params.end = filters?.dates.value[1].getTime() + } else { + params.start = filters?.dates.value[0].getTime() + params.end = Date.now() + } } - }, [ - selectedItem, - metricOptionsAreLoaded, - detailsStore.metricsOptions, - detailsStore.dates.value, - fetchData, - setMetrics - ]) + fetchData(params, selectedItem.project, selectedItem.uid).then() + }, [isAlertsPage, filters, selectedItem, detailsStore.dates.value, fetchData]) useEffect(() => { fetchMetrics() - return () => { metricsValuesAbortController.current?.abort(REQUEST_CANCELED) setMetrics([]) @@ -148,20 +134,22 @@ const DetailsAlertsMetrics = ({ selectedItem }) => { }, [fetchMetrics, setMetrics]) return ( -
-
- -
+
+ {isAlertsPage && ( +
+ +
+ )} {generatedMetrics.length === 0 ? ( !detailsStore.loadingCounter ? ( @@ -175,10 +163,10 @@ const DetailsAlertsMetrics = ({ selectedItem }) => { ) ) : null ) : ( -
+
{generatedMetrics.map(([applicationName, applicationMetrics]) => ( -
{applicationName}
+ {isAlertsPage &&
{applicationName}
} {applicationMetrics.map(metric => !metric.data || isEmpty(metric.points) ? ( @@ -195,6 +183,7 @@ const DetailsAlertsMetrics = ({ selectedItem }) => { } DetailsAlertsMetrics.propTypes = { + isAlertsPage: PropTypes.bool, selectedItem: PropTypes.object.isRequired } diff --git a/src/components/ModelsPage/ModelEndpoints/modelEndpoints.util.js b/src/components/ModelsPage/ModelEndpoints/modelEndpoints.util.js index 443e7b9e3..a6e367126 100644 --- a/src/components/ModelsPage/ModelEndpoints/modelEndpoints.util.js +++ b/src/components/ModelsPage/ModelEndpoints/modelEndpoints.util.js @@ -59,7 +59,8 @@ const detailsMenu = [ { label: 'alerts', id: 'alerts', - icon: + icon: , + query: '?entity-type=model-endpoint-result' //TODO: temp solution for query params } ] diff --git a/src/elements/AlertsTableRow/AlertsTableRow.js b/src/elements/AlertsTableRow/AlertsTableRow.js index 51a1bf731..d477657d5 100644 --- a/src/elements/AlertsTableRow/AlertsTableRow.js +++ b/src/elements/AlertsTableRow/AlertsTableRow.js @@ -17,11 +17,12 @@ illegal under applicable law, and the grant of the foregoing license under the Apache 2.0 license is conditioned upon your compliance with such restriction. */ -import { useMemo, useRef } from 'react' +import React, { useMemo, useRef } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' import { useParams } from 'react-router-dom' +import DetailsAlertsMetrics from '../../components/DetailsDrillDownAlert/DetailsAlertsMetrics' import TableCell from '../TableCell/TableCell' import { ALERTS_PAGE, DETAILS_OVERVIEW_TAB } from '../../constants' @@ -29,11 +30,16 @@ import { getIdentifierMethod } from '../../utils/getUniqueIdentifier' import './AlertsTableRow.scss' -// TODO: rowIsExpanded logic will be part of ML-8516 -const AlertsTableRow = ({ handleExpandRow, handleSelectItem, rowItem, selectedItem }) => { +const AlertsTableRow = ({ + className, + isRowSelected, + filters, + rowItem, + selectedItem, + toggleRow +}) => { const parent = useRef() const params = useParams() - const getIdentifier = useMemo(() => getIdentifierMethod(ALERTS_PAGE), []) const rowClassNames = classnames( 'alert-row', @@ -44,38 +50,50 @@ const AlertsTableRow = ({ handleExpandRow, handleSelectItem, rowItem, selectedIt getIdentifier(selectedItem, true) === rowItem?.data?.ui?.identifierUnique && 'table-row_active' ) - return ( - - <> - {rowItem.content.map((value, index) => { - return ( + <> + + {rowItem.content.map( + (value, index) => !value.hidden && ( - + + + ) - ) - })} - - + )} + + {isRowSelected && ( + + + + + + )} + ) } AlertsTableRow.propTypes = { + className: PropTypes.string, handleSelectItem: PropTypes.func.isRequired, + isRowSelected: PropTypes.bool, mainRowItemsCount: PropTypes.number, rowIndex: PropTypes.number.isRequired, rowItem: PropTypes.shape({}).isRequired, diff --git a/src/elements/AlertsTableRow/AlertsTableRow.scss b/src/elements/AlertsTableRow/AlertsTableRow.scss index 2d575840c..4e8802c66 100644 --- a/src/elements/AlertsTableRow/AlertsTableRow.scss +++ b/src/elements/AlertsTableRow/AlertsTableRow.scss @@ -2,50 +2,20 @@ @import '~igz-controls/scss/borders'; .alert-row { - .table-body__cell.alert-row-notification-cell { - min-width: 150px; - } + flex-wrap: wrap; + border: 1px solid #e2e7ff; - &__item-info-notification { - display: flex; - gap: 5px; + > * { + border-bottom-color: white !important; } - &__details-alert-icon-cell { - display: flex; - gap: 8px; - align-items: center; - - svg { - width: 20px; - height: 20px; - } - } - - &__details-alert-header { - display: flex; - align-self: center; - min-width: 160px; - font-weight: 500; - font-size: 15px; - line-height: 18px; - } - - &__details-alert-logs { - display: flex; - align-items: center; - justify-content: space-between; - } - - &__popup-header { - margin: 0; - padding-bottom: 12px; - font-size: 18px; + .table-body__cell.alert-row-notification-cell { + min-width: 150px; } &__item-info-notification { - flex-wrap: wrap; display: flex; + flex-wrap: wrap; gap: 5px; } @@ -95,6 +65,19 @@ } } } + + td:first-child.alert-row__cell--expanded-selected-cell { + svg { + transform: rotate(90deg); + } + } + + &__expanded-row { + display: flex; + flex-direction: column; + justify-items: center; + margin-top: 8px; + } } .notifications-item { diff --git a/src/hooks/useAlertsPageData.js b/src/hooks/useAlertsPageData.js index 158c46e67..0621588bd 100644 --- a/src/hooks/useAlertsPageData.js +++ b/src/hooks/useAlertsPageData.js @@ -25,7 +25,7 @@ import { BE_PAGE, BE_PAGE_SIZE, FILTER_ALL_ITEMS, PROJECTS_FILTER } from '../con import { usePagination } from './usePagination.hook' import { fetchAlerts } from '../reducers/alertsReducer' -export const useAlertsPageData = filters => { +export const useAlertsPageData = (filters, isAlertsPage) => { const [alerts, setAlerts] = useState([]) const [requestErrorMessage, setRequestErrorMessage] = useState('') @@ -38,10 +38,11 @@ export const useAlertsPageData = filters => { const refreshAlerts = useCallback( filters => { setAlerts([]) - abortControllerRef.current = new AbortController() - const projectName = - params.projectName || filters?.[PROJECTS_FILTER]?.toLowerCase?.() !== FILTER_ALL_ITEMS + + const projectName = !isAlertsPage + ? params.projectName || params.id + : filters?.[PROJECTS_FILTER]?.toLowerCase?.() !== FILTER_ALL_ITEMS ? filters?.[PROJECTS_FILTER]?.toLowerCase?.() : params.id @@ -71,7 +72,7 @@ export const useAlertsPageData = filters => { paginationConfigAlertsRef.current.paginationResponse = response.pagination }) }, - [dispatch, params.id, params.projectName] + [dispatch, isAlertsPage, params.id, params.projectName] ) const [handleRefreshAlerts, paginatedAlerts, , setSearchParams] = usePagination({ diff --git a/src/utils/createAlertsContent.js b/src/utils/createAlertsContent.js index f32db7336..7a32be2c3 100644 --- a/src/utils/createAlertsContent.js +++ b/src/utils/createAlertsContent.js @@ -192,14 +192,14 @@ const getNotificationData = notifications => } }) -export const createAlertRowData = ({ ...alert }, isCrossProjects) => { +export const createAlertRowData = ({ ...alert }, isCrossProjects, showExpandButton = false) => { const { name } = alert const getLink = alert => { const queryString = window.location.search const { alertName, entity_kind: entityType, entity_id, id: alertId, job, project, uid } = alert - if (entityType === MODEL_ENDPOINT_RESULT) { + if (entityType === MODEL_ENDPOINT_RESULT) { const [endpointId, , , name] = entity_id.split('.') return `/projects/*/alerts/${project}/${alertName}/${alertId}/${name}/${endpointId}/${DETAILS_ALERT_APPLICATION}${queryString}` } @@ -268,9 +268,10 @@ export const createAlertRowData = ({ ...alert }, isCrossProjects) => { headerLabel: 'Alert Name', value: name, className: 'table-cell-name', - getLink: () => getLink(alert), + getLink: () => (!showExpandButton ? getLink(alert) : ''), tooltip: name, - type: 'link' + type: 'link', + showExpandButton }, { id: `projectName.${alert.id}`,