diff --git a/package.json b/package.json index f84231f54a..120549d7d8 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "final-form-arrays": "^3.1.0", "fs-extra": "^10.0.0", "identity-obj-proxy": "^3.0.0", - "iguazio.dashboard-react-controls": "2.2.7", + "iguazio.dashboard-react-controls": "2.2.8", "is-wsl": "^1.1.0", "js-base64": "^2.5.2", "js-yaml": "^4.1.0", diff --git a/src/App.js b/src/App.js index ecb2b08b5b..5cbbbc66b1 100755 --- a/src/App.js +++ b/src/App.js @@ -102,7 +102,7 @@ const FeatureVectors = lazyRetry( const ProjectsJobsMonitoring = lazyRetry( () => import('./components/ProjectsJobsMonitoring/ProjectsJobsMonitoring') ) -const ProjectsAlerts = lazyRetry(() => import('./components/ProjectsAlerts/ProjectsAlerts')) +const ProjectsAlerts = lazyRetry(() => import('./components/Alerts/Alerts')) const JobsMonitoring = lazyRetry( () => import('./components/ProjectsJobsMonitoring/JobsMonitoring/JobsMonitoring') ) diff --git a/src/components/ProjectsAlerts/ProjectsAlerts.js b/src/api/alerts-api.js similarity index 52% rename from src/components/ProjectsAlerts/ProjectsAlerts.js rename to src/api/alerts-api.js index c023ef3e8f..84904257fc 100644 --- a/src/components/ProjectsAlerts/ProjectsAlerts.js +++ b/src/api/alerts-api.js @@ -17,30 +17,29 @@ 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 { useCallback, useMemo } from 'react' - -import ProjectAlertsView from './ProjectsAlertsView' - -import { getAlertsFiltersConfig, parseAlertsQueryParamsCallback } from './alerts.util' -import { useFiltersFromSearchParams } from '../../hooks/useFiltersFromSearchParams.hook' - -const ProjectsAlerts = () => { - const alertsFiltersConfig = useMemo(() => getAlertsFiltersConfig(), []) - - const alertsFilters = useFiltersFromSearchParams( - alertsFiltersConfig, - parseAlertsQueryParamsCallback - ) - - const refreshAlertsCallback = useCallback(() => {}, []) - - return ( - - ) +import { mainHttpClient } from '../httpClient' + +const alertsApi = { + getAlerts: (project, filters, config = {}) => { + // TODO:ML-8514 update newConfig + const newConfig = { + ...config, + params: { + ...config.params + } + } + return mainHttpClient.get(`/projects/${project}/alert-activations`, newConfig) + }, + getAlert: (project, alertName, config) => { + // TODO:ML-8514 update newConfig + const newConfig = { + ...config, + params: { + ...config.params + } + } + return mainHttpClient.get('/projects/{project}/alerts/{alertName}/activations', newConfig) + } } -export default ProjectsAlerts +export default alertsApi diff --git a/src/components/Alerts/Alerts.js b/src/components/Alerts/Alerts.js new file mode 100644 index 0000000000..2251491a8f --- /dev/null +++ b/src/components/Alerts/Alerts.js @@ -0,0 +1,137 @@ +/* +Copyright 2019 Iguazio Systems Ltd. + +Licensed under the Apache License, Version 2.0 (the "License") with +an addition restriction as set forth herein. You may not use this +file except in compliance with the License. You may obtain a copy of +the License at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing +permissions and limitations under the License. + +In addition, you may not use the software for any purposes that are +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 { useCallback, useMemo, useRef, useState } from 'react' +import { useParams, useSearchParams } from 'react-router-dom' +import { useDispatch, useSelector } from 'react-redux' + +import AlertsView from './AlertsView' + +import { getAlertsFiltersConfig, parseAlertsQueryParamsCallback } from './alerts.util' +import { useFiltersFromSearchParams } from '../../hooks/useFiltersFromSearchParams.hook' + +import { fetchAlerts } from '../../reducers/alertsReducer' +import { useVirtualization } from '../../hooks/useVirtualization.hook' +import { createAlertRowData } from '../../utils/createAlertsContent' +import { useInitialTableFetch } from '../../hooks/useInitialTableFetch.hook' + +import cssVariables from './alerts.scss' + +const Alerts = () => { + const [alerts, setAlerts] = useState([]) + const [requestErrorMessage, setRequestErrorMessage] = useState('') + const [selectedAlert] = useState({}) + const [selectedRowData] = useState({}) + const params = useParams() + const [, setSearchParams] = useSearchParams() + const dispatch = useDispatch() + const alertsStore = useSelector(state => state.alertsStore) + const filtersStore = useSelector(store => store.filtersStore) + + const abortControllerRef = useRef(new AbortController()) + + const alertsFiltersConfig = useMemo(() => getAlertsFiltersConfig(), []) + + const alertsFilters = useFiltersFromSearchParams( + alertsFiltersConfig, + parseAlertsQueryParamsCallback + ) + + const fetchData = useCallback( + filters => { + abortControllerRef.current = new AbortController() + dispatch( + fetchAlerts({ + project: params.id, + filters, + config: { + ui: { + controller: abortControllerRef.current, + setRequestErrorMessage + }, + params: { + format: 'minimal' + } + } + }) + ) + .unwrap() + .then(alerts => { + if (alerts?.length > 0) { + setAlerts(alerts) + } else { + setAlerts([]) + } + }) + }, + [dispatch, params.id] + ) + + const tableContent = useMemo(() => { + return alerts.map(alert => createAlertRowData(alert)) + }, [alerts]) + + const refreshAlertsCallback = useCallback( + filters => { + setAlerts([]) + + return fetchData(filters) + }, + [fetchData] + ) + + useInitialTableFetch({ + fetchData, + filters: alertsFilters + }) + + const virtualizationConfig = useVirtualization({ + rowsData: { + content: tableContent, + expandedRowsData: selectedRowData, + selectedItem: selectedAlert + }, + heightData: { + headerRowHeight: cssVariables.$alertsHeaderRowHeight, + rowHeight: cssVariables.$alertsRowHeight, + rowHeightExtended: cssVariables.$alertsRowHeightExtended + }, + activateTableScroll: true + }) + + return ( + []} // TODO + filters={alertsFilters} + filtersStore={filtersStore} + pageData={{}} //TODO + refreshAlertsCallback={refreshAlertsCallback} + requestErrorMessage={requestErrorMessage} + selectedAlert={selectedAlert} + setSearchParams={setSearchParams} + tableContent={tableContent} + virtualizationConfig={virtualizationConfig} + /> + ) +} + +export default Alerts diff --git a/src/components/ProjectsAlerts/ProjectsAlertsFilters.js b/src/components/Alerts/AlertsFilters.js similarity index 79% rename from src/components/ProjectsAlerts/ProjectsAlertsFilters.js rename to src/components/Alerts/AlertsFilters.js index 6e12f3f64c..f869f716b5 100644 --- a/src/components/ProjectsAlerts/ProjectsAlertsFilters.js +++ b/src/components/Alerts/AlertsFilters.js @@ -17,12 +17,13 @@ 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 } from 'react' +import { useCallback, useEffect, useMemo } from 'react' -import { truncate } from 'lodash' +import { truncate, upperFirst } from 'lodash' import { useForm, useFormState } from 'react-final-form' import { useSelector } from 'react-redux' +import StatusFilter from '../../common/StatusFilter/StatusFilter' import { FormSelect, FormInput } from 'igz-controls/components' import { FormOnChange } from 'iguazio.dashboard-react-controls/dist/components' @@ -45,12 +46,11 @@ import { FILTER_ALL_ITEMS, JOB, JOB_NAME, - PROJECT_FILTER, + PROJECTS_FILTER, SEVERITY } from '../../constants' -import StatusFilter from '../../common/StatusFilter/StatusFilter' -const ProjectsAlertsFilters = () => { +const AlertsFilters = () => { const form = useForm() const { values: { [ENTITY_TYPE]: entityType } @@ -67,6 +67,22 @@ const ProjectsAlertsFilters = () => { })) }, [projectStore.projectsNames.data]) + const getFieldsToReset = useCallback(entityType => { + const fieldsByType = { + [FILTER_ALL_ITEMS]: [ENTITY_ID], + [APPLICATION]: [ENTITY_ID], + [JOB]: [JOB_NAME], + [ENDPOINT]: [ENDPOINT_APPLICATION, ENDPOINT_RESULT] + } + const allFields = [ENTITY_ID, JOB_NAME, ENDPOINT_APPLICATION, ENDPOINT_RESULT] + + return allFields.filter(field => !(fieldsByType[entityType] ?? []).includes(field)) + }, []) + + useEffect(() => { + getFieldsToReset(entityType).forEach(field => form.change(field, '')) + }, [entityType, form, getFieldsToReset]) + const handleInputChange = (value, inputName) => { form.change(inputName, value || '') } @@ -74,7 +90,7 @@ const ProjectsAlertsFilters = () => { return ( <>
- +
{ {(entityType === FILTER_ALL_ITEMS || entityType === APPLICATION) && (
- + handleInputChange(value, ENTITY_ID)} name={ENTITY_ID} />
)} {entityType === JOB && (
- + handleInputChange(value, JOB_NAME)} name={JOB_NAME} />
)} @@ -132,4 +148,4 @@ const ProjectsAlertsFilters = () => { ) } -export default ProjectsAlertsFilters +export default AlertsFilters diff --git a/src/components/Alerts/AlertsView.js b/src/components/Alerts/AlertsView.js new file mode 100644 index 0000000000..4722681f75 --- /dev/null +++ b/src/components/Alerts/AlertsView.js @@ -0,0 +1,128 @@ +/* +Copyright 2019 Iguazio Systems Ltd. + +Licensed under the Apache License, Version 2.0 (the "License") with +an addition restriction as set forth herein. You may not use this +file except in compliance with the License. You may obtain a copy of +the License at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing +permissions and limitations under the License. + +In addition, you may not use the software for any purposes that are +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 PropTypes from 'prop-types' + +import ActionBar from '../ActionBar/ActionBar' +import AlertsTableRow from '../../elements/AlertsTableRow/AlertsTableRow' +import Breadcrumbs from '../../common/Breadcrumbs/Breadcrumbs' +import Loader from '../../common/Loader/Loader' +import NoData from '../../common/NoData/NoData' +import AlertsFilters from './AlertsFilters' +import Table from '../Table/Table' + +import { getNoDataMessage } from '../../utils/getNoDataMessage' +import { ALERTS_FILTERS, ALERTS_PAGE } from '../../constants' +import { VIRTUALIZATION_CONFIG } from '../../types' +import { isRowRendered } from '../../hooks/useVirtualization.hook' + +const AlertsView = ({ + actionsMenu, + alertsFiltersConfig, + alertsStore, + filters, + filtersStore, + pageData, + refreshAlertsCallback, + requestErrorMessage, + selectedAlert, + setSearchParams, + tableContent, + virtualizationConfig +}) => { + return ( + <> +
+
+ +
+
+
+
+ + + +
+ {alertsStore.loading ? ( + + ) : tableContent.length === 0 ? ( + + ) : ( + + {tableContent.map( + (tableItem, index) => + isRowRendered(virtualizationConfig, index) && ( + {}} + rowIndex={index} + rowItem={tableItem} + actionsMenu={[]} + selectedItem={selectedAlert} + /> + ) + )} +
+ )} +
+
+
+ + ) +} + +AlertsView.propTypes = { + alertsFiltersConfig: PropTypes.object.isRequired, + filters: PropTypes.object.isRequired, + filtersStore: PropTypes.object.isRequired, + refreshAlertsCallback: PropTypes.func.isRequired, + requestErrorMessage: PropTypes.string.isRequired, + setSearchParams: PropTypes.func.isRequired, + tableContent: PropTypes.arrayOf(PropTypes.object).isRequired, + virtualizationConfig: VIRTUALIZATION_CONFIG.isRequired +} +export default AlertsView diff --git a/src/components/Alerts/alerts.scss b/src/components/Alerts/alerts.scss new file mode 100644 index 0000000000..eb9b553a95 --- /dev/null +++ b/src/components/Alerts/alerts.scss @@ -0,0 +1,16 @@ +@import '~igz-controls/scss/variables'; +@import 'src/scss/mixins'; + +$alertsRowHeight: $rowHeight; +$alertsHeaderRowHeight: $headerRowHeight; +$alertsRowHeightExtended: $rowHeightExtended; + +.alerts-table { + @include rowsHeight($alertsHeaderRowHeight, $alertsRowHeight, $alertsRowHeightExtended); +} + +:export { + alertsRowHeight: $alertsRowHeight; + alertsHeaderRowHeight: $alertsHeaderRowHeight; + alertsRowHeightExtended: $alertsRowHeightExtended; +} diff --git a/src/components/ProjectsAlerts/alerts.util.js b/src/components/Alerts/alerts.util.js similarity index 86% rename from src/components/ProjectsAlerts/alerts.util.js rename to src/components/Alerts/alerts.util.js index 3f4a87d27b..6b6bba376f 100644 --- a/src/components/ProjectsAlerts/alerts.util.js +++ b/src/components/Alerts/alerts.util.js @@ -30,7 +30,7 @@ import { JOB_KIND_JOB, JOB_NAME, NAME_FILTER, - PROJECT_FILTER, + PROJECTS_FILTER, SEVERITY } from '../../constants' import { @@ -41,16 +41,16 @@ import { export const getAlertsFiltersConfig = () => { return { - [NAME_FILTER]: { label: 'Alert name', initialValue: '' }, + [NAME_FILTER]: { label: 'Alert Name:', initialValue: '' }, [DATES_FILTER]: { label: 'Start time:', initialValue: getDatePickerFilterValue(datePickerPastOptions, PAST_24_HOUR_DATE_OPTION) }, - [PROJECT_FILTER]: { label: 'Project:', initialValue: FILTER_ALL_ITEMS, isModal: true }, + [PROJECTS_FILTER]: { label: 'Project:', initialValue: FILTER_ALL_ITEMS, isModal: true }, [ENTITY_TYPE]: { label: 'Entity Type', initialValue: FILTER_ALL_ITEMS, isModal: true }, [ENTITY_ID]: { label: 'Entity ID:', initialValue: '', isModal: true }, - [JOB_NAME]: { label: 'Job name:', initialValue: '', isModal: true }, - [ENDPOINT_APPLICATION]: { label: 'Endpoint:', initialValue: '', isModal: true }, + [JOB_NAME]: { label: 'Job Name:', initialValue: '', isModal: true }, + [ENDPOINT_APPLICATION]: { label: 'Application Name:', initialValue: '', isModal: true }, [ENDPOINT_RESULT]: { label: 'Result:', initialValue: '', isModal: true }, [SEVERITY]: { label: 'Severity:', initialValue: [FILTER_ALL_ITEMS], isModal: true }, [EVENT_TYPE]: { label: 'Event Type:', initialValue: FILTER_ALL_ITEMS, isModal: true } @@ -65,6 +65,15 @@ export const parseAlertsQueryParamsCallback = (paramName, paramValue) => { return filteredStatuses?.length ? filteredStatuses : null } + + if (paramName === ENTITY_TYPE) { + return filterAlertsEntityTypeOptions.find(type => type.id === paramValue)?.id + } + + if (paramName === EVENT_TYPE) { + return filterAlertsEventTypeOptions.find(type => type.id === paramValue)?.id + } + return paramValue } diff --git a/src/components/ProjectsAlerts/ProjectsAlertsView.js b/src/components/ProjectsAlerts/ProjectsAlertsView.js deleted file mode 100644 index fd0c787711..0000000000 --- a/src/components/ProjectsAlerts/ProjectsAlertsView.js +++ /dev/null @@ -1,63 +0,0 @@ -/* -Copyright 2019 Iguazio Systems Ltd. - -Licensed under the Apache License, Version 2.0 (the "License") with -an addition restriction as set forth herein. You may not use this -file except in compliance with the License. You may obtain a copy of -the License at http://www.apache.org/licenses/LICENSE-2.0. - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -implied. See the License for the specific language governing -permissions and limitations under the License. - -In addition, you may not use the software for any purposes that are -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 Breadcrumbs from '../../common/Breadcrumbs/Breadcrumbs' -import ActionBar from '../ActionBar/ActionBar' -import ProjectsAlertsFilters from './ProjectsAlertsFilters' - -import { ALERTS_FILTERS, ALERTS_PAGE } from '../../constants' - -import PropTypes from 'prop-types' - -const ProjectAlertsView = ({ alertsFiltersConfig, filters, refreshAlertsCallback }) => { - return ( - <> -
-
- -
-
-
-
- - - -
-
-
-
- - ) -} - -ProjectAlertsView.propTypes = { - alertsFiltersConfig: PropTypes.object.isRequired, - filters: PropTypes.object.isRequired, - refreshAlertsCallback: PropTypes.func.isRequired -} -export default ProjectAlertsView diff --git a/src/components/Table/table.scss b/src/components/Table/table.scss index d55267ef06..5a204f9d26 100644 --- a/src/components/Table/table.scss +++ b/src/components/Table/table.scss @@ -99,6 +99,12 @@ flex: 1; max-width: 150px; } + + &-notification { + .notification-fail svg > * { + fill: $silver; + } + } } &:has(.actions-menu__container-active) { @@ -109,16 +115,16 @@ &.table { &__scrolled { .table-cell-name { - &:after { - content: ''; + &::after { position: absolute; top: 0; - bottom: 0; right: 0; + bottom: 0; width: 5px; background-color: inherit; border-right: $secondaryBorder; - box-shadow: 2px 0px 2px -1px rgba($black, 0.2); + box-shadow: 2px 0 2px -1px rgba($black, 0.2); + content: ''; } } @@ -197,10 +203,10 @@ &_hidden { font-size: 0; - * { + + * { visibility: hidden; } - .chip { visibility: hidden; @@ -230,9 +236,9 @@ a { position: relative; + width: 100%; margin: 0; text-decoration: none; - width: 100%; span { display: block; @@ -266,6 +272,11 @@ .chip_short { max-width: 100px; } + + .severity-cell { + display: flex; + gap: 4px; + } } } } diff --git a/src/constants.js b/src/constants.js index b5b8184b21..0e06766451 100644 --- a/src/constants.js +++ b/src/constants.js @@ -96,6 +96,12 @@ export const JOBS_MONITORING_SCHEDULED_TAB = 'scheduled' export const ALERTS_PAGE = 'ALERTS' export const ALERTS_FILTERS = 'alerts' +export const MODEL_ENDPOINT_RESULT = 'model-endpoint-result' +export const MODEL_MONITORING_APPLICATION = 'model-monitoring-application' +export const SEVERITY_LOW = 'low' +export const SEVERITY_MEDIUM = 'medium' +export const SEVERITY_HIGH = 'high' +export const SEVERITY_CRITICAL = 'critical' export const MODELS_PAGE = 'MODELS' export const MODELS_TAB = 'models' @@ -508,6 +514,7 @@ export const LABELS_FILTER = 'labels' export const NAME_FILTER = 'name' export const DATES_FILTER = 'dates' export const PROJECT_FILTER = 'project' +export const PROJECTS_FILTER = 'projects-list' export const TYPE_FILTER = 'type' export const SHOW_UNTAGGED_FILTER = 'showUntagged' export const SORT_BY = 'sortBy' diff --git a/src/elements/AlertsTableRow/AlertsTableRow.js b/src/elements/AlertsTableRow/AlertsTableRow.js new file mode 100644 index 0000000000..5527bc74e3 --- /dev/null +++ b/src/elements/AlertsTableRow/AlertsTableRow.js @@ -0,0 +1,74 @@ +/* +Copyright 2019 Iguazio Systems Ltd. + +Licensed under the Apache License, Version 2.0 (the "License") with +an addition restriction as set forth herein. You may not use this +file except in compliance with the License. You may obtain a copy of +the License at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing +permissions and limitations under the License. + +In addition, you may not use the software for any purposes that are +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 { useRef } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { useParams } from 'react-router-dom' + +import TableCell from '../TableCell/TableCell' + +import { DETAILS_OVERVIEW_TAB } from '../../constants' + +// TODO: rowIsExpanded logic will be part of ML-8516 +// TODO: selected row logic will be part of ML-8104 +const AlertsTableRow = ({ handleExpandRow, handleSelectItem, rowItem, selectedItem }) => { + const parent = useRef() + const params = useParams() + + const rowClassNames = classnames('table-row', 'table-body-row', 'parent-row') + + return ( + + <> + {rowItem.content.map((value, index) => { + return ( + !value.hidden && ( + + ) + ) + })} + + + ) +} + +AlertsTableRow.propTypes = { + handleSelectItem: PropTypes.func.isRequired, + mainRowItemsCount: PropTypes.number, + rowIndex: PropTypes.number.isRequired, + rowItem: PropTypes.shape({}).isRequired, + selectedItem: PropTypes.shape({}).isRequired +} + +export default AlertsTableRow diff --git a/src/reducers/alertsReducer.js b/src/reducers/alertsReducer.js new file mode 100644 index 0000000000..e8db44ac21 --- /dev/null +++ b/src/reducers/alertsReducer.js @@ -0,0 +1,109 @@ +/* +Copyright 2019 Iguazio Systems Ltd. + +Licensed under the Apache License, Version 2.0 (the "License") with +an addition restriction as set forth herein. You may not use this +file except in compliance with the License. You may obtain a copy of +the License at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing +permissions and limitations under the License. + +In addition, you may not use the software for any purposes that are +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 { createSlice, createAsyncThunk } from '@reduxjs/toolkit' + +import alertsApi from '../api/alerts-api' +import { defaultPendingHandler } from './redux.util' +import { parseAlerts } from '../utils/parseAlert' +import { largeResponseCatchHandler } from '../utils/largeResponseCatchHandler' + +const initialState = { + alerts: [], + error: null, + loading: false +} + +export const fetchAlert = createAsyncThunk( + 'fetchAlert', + ({ project, filters, config }, thunkAPI) => { + return alertsApi + .getAlert(project, filters, config) + .then(({ data }) => { + return parseAlerts(data.alerts) + }) + .catch(error => { + largeResponseCatchHandler( + error, + 'Failed to fetch alerts', + thunkAPI.dispatch, + config?.ui?.setRequestErrorMessage + ) + }) + } +) +export const fetchAlerts = createAsyncThunk( + 'fetchAlerts', + ({ project, filters, config }, thunkAPI) => { + return alertsApi + .getAlerts(project, filters, config) + .then(({ data }) => { + return parseAlerts(data.alerts) + }) + .catch(error => { + largeResponseCatchHandler( + error, + 'Failed to fetch alerts', + thunkAPI.dispatch, + config?.ui?.setRequestErrorMessage + ) + }) + } +) + +const alertsSlice = createSlice({ + name: 'alertsStore', + initialState, + reducers: { + removeAlerts(state) { + state.alerts = initialState.alerts + state.error = null + state.loading = false + } + }, + extraReducers: builder => { + builder + .addCase(fetchAlert.pending, defaultPendingHandler) + .addCase(fetchAlert.fulfilled, (state, action) => { + state.alerts = action.payload + state.loading = false + }) + .addCase(fetchAlert.rejected, (state, action) => { + state.alerts = [] + state.error = action.payload + state.loading = false + }) + .addCase(fetchAlerts.pending, state => { + state.loading = true + state.error = null + }) + .addCase(fetchAlerts.fulfilled, (state, action) => { + state.loading = false + state.alerts = action.payload + }) + .addCase(fetchAlerts.rejected, (state, action) => { + state.loading = false + state.error = action.payload + }) + } +}) + +export const { removeAlerts } = alertsSlice.actions + +export default alertsSlice.reducer diff --git a/src/store/toolkitStore.js b/src/store/toolkitStore.js index abbc5df990..137061bc99 100644 --- a/src/store/toolkitStore.js +++ b/src/store/toolkitStore.js @@ -19,6 +19,7 @@ such restriction. */ import { configureStore } from '@reduxjs/toolkit' +import alertsStore from '../reducers/alertsReducer' import appStore from '../reducers/appReducer' import artifactsStore from '../reducers/artifactsReducer' import detailsStore from '../reducers/detailsReducer' @@ -36,6 +37,7 @@ import workflowsStore from '../reducers/workflowReducer' const toolkitStore = configureStore({ reducer: { + alertsStore, appStore, artifactsStore, detailsStore, diff --git a/src/utils/createAlertsContent.js b/src/utils/createAlertsContent.js new file mode 100644 index 0000000000..e9abb189d6 --- /dev/null +++ b/src/utils/createAlertsContent.js @@ -0,0 +1,228 @@ +/* +Copyright 2019 Iguazio Systems Ltd. + +Licensed under the Apache License, Version 2.0 (the "License") with +an addition restriction as set forth herein. You may not use this +file except in compliance with the License. You may obtain a copy of +the License at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing +permissions and limitations under the License. + +In addition, you may not use the software for any purposes that are +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 { upperFirst } from 'lodash' +import { formatDatetime } from './datetime' + +import { ReactComponent as Application } from 'igz-controls/images/entity-type-application.svg' +import { ReactComponent as Endpoint } from 'igz-controls/images/entity-type-endpoint.svg' +import { ReactComponent as Critical } from 'igz-controls/images/severity-critical.svg' +import { ReactComponent as Email } from 'igz-controls/images/email-icon.svg' +import { ReactComponent as Git } from 'igz-controls/images/git-icon.svg' +import { ReactComponent as High } from 'igz-controls/images/severity-high.svg' +import { ReactComponent as Job } from 'igz-controls/images/entity-type-job.svg' +import { ReactComponent as Low } from 'igz-controls/images/severity-low.svg' +import { ReactComponent as Normal } from 'igz-controls/images/severity-normal.svg' +import { ReactComponent as Slack } from 'igz-controls/images/slack-icon-colored.svg' +import { ReactComponent as Webhook } from 'igz-controls/images/webhook-icon.svg' + +import { + APPLICATION, + ENDPOINT, + JOB, + MODEL_ENDPOINT_RESULT, + MODEL_MONITORING_APPLICATION, + SEVERITY, + SEVERITY_CRITICAL, + SEVERITY_HIGH, + SEVERITY_LOW, + SEVERITY_MEDIUM +} from '../constants' + +const getEntityTypeData = entityType => { + switch (entityType) { + case MODEL_ENDPOINT_RESULT: + return { + value: , + tooltip: upperFirst(ENDPOINT) + } + case MODEL_MONITORING_APPLICATION: + return { + value: , + tooltip: upperFirst(APPLICATION) + } + case JOB: + return { + value: , + tooltip: upperFirst(JOB) + } + default: + return { + value: + } + } +} + +const getSeverityData = severity => { + switch (severity) { + case SEVERITY_LOW: + return { + value: ( +
+ + {upperFirst(SEVERITY_LOW)} +
+ ), + tooltip: upperFirst(SEVERITY_LOW) + } + case SEVERITY_MEDIUM: + return { + value: ( +
+ + {upperFirst(SEVERITY_MEDIUM)} +
+ ), + tooltip: upperFirst(SEVERITY_MEDIUM) + } + case SEVERITY_HIGH: + return { + value: ( +
+ + {upperFirst(SEVERITY_HIGH)} +
+ ), + tooltip: upperFirst(SEVERITY_HIGH) + } + case SEVERITY_CRITICAL: + return { + value: ( +
+ + {upperFirst(SEVERITY_CRITICAL)} +
+ ), + tooltip: upperFirst(SEVERITY_CRITICAL) + } + default: + return { + value: + } + } +} + +const alertsNotifications = { + webhook: , + git: , + slack: , + email: +} + +const getNotificationData = notifications => + notifications.map(notification => { + const tableCellClassName = classNames('table-cell-notification__content', { + 'notification-fail': notification.err !== '' + }) + + return { + icon:
{alertsNotifications[notification.kind]}
, + tooltip: upperFirst(notification.kind) + } + }) + +export const createAlertRowData = ({ name, ...alert }) => { + alert.id = alert.id.slice(-6) // Use the last 6 characters of the database ID as the alert ID + + return { + data: { + ...alert + }, + content: [ + { + id: `alertName.${alert.id}`, + headerId: 'alertName', + headerLabel: 'Alert Name', + value: name, + className: 'table-cell-1', + getLink: () => {}, //TODO: Implement in ML-8368 + showStatus: true, + tooltip: name, + type: 'link' + }, + { + id: `projectName.${alert.id}`, + headerId: 'projectName', + headerLabel: 'Project name', + value: alert.project, + className: 'table-cell-1' + }, + { + id: `eventType.${alert.id}`, + headerId: 'eventType', + headerLabel: 'Event Type', + value: alert.event_kind?.split('-')?.join(' '), + className: 'table-cell-1' + }, + { + id: `entityId.${alert.id}`, + headerId: 'entityId', + headerLabel: 'Entity ID', + value: alert.entity_id, + className: 'table-cell-1' + }, + { + id: `entityType.${alert.id}`, + headerId: 'entityType', + headerLabel: 'Entity Type', + value: getEntityTypeData(alert.entity_kind).value, + className: 'table-cell-small', + tooltip: getEntityTypeData(alert.entity_kind).tooltip + }, + { + id: `timestamp.${alert.id}`, + headerId: 'timestamp', + headerLabel: 'Timestamp', + value: formatDatetime(alert.activation_time, '-'), + className: 'table-cell-1' + }, + { + id: `severity.${alert.id}`, + headerId: SEVERITY, + headerLabel: upperFirst(SEVERITY), + value: getSeverityData(alert.severity).value, + tooltip: getSeverityData(alert.severity).tooltip, + className: 'table-cell-1' + }, + { + id: `criteriaCount.${alert.id}`, + headerId: 'criteriaCount', + headerLabel: 'Trigger criteria count', + value: alert.criteria?.count, + className: 'table-cell-1' + }, + { + id: `criteriaTime.${alert.id}`, + headerId: 'criteriaTime', + headerLabel: 'Trigger criteria time period', + value: alert.criteria?.period, + className: 'table-cell-1' + }, + { + id: `notifications.${alert.id}`, + headerId: 'notifications', + headerLabel: 'Notifications', + value: getNotificationData(alert.notifications), + className: 'table-cell-small table-cell-notification', + type: 'icons' + } + ] + } +} diff --git a/src/utils/parseAlert.js b/src/utils/parseAlert.js new file mode 100644 index 0000000000..aa112a6178 --- /dev/null +++ b/src/utils/parseAlert.js @@ -0,0 +1,31 @@ +/* +Copyright 2019 Iguazio Systems Ltd. + +Licensed under the Apache License, Version 2.0 (the "License") with +an addition restriction as set forth herein. You may not use this +file except in compliance with the License. You may obtain a copy of +the License at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing +permissions and limitations under the License. + +In addition, you may not use the software for any purposes that are +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. +*/ +export const parseAlerts = alerts => { + return alerts.map(alert => { + return { + ...alert, + ui: { + ...alert, + identifier: `${alert.id.slice(-6)}`, + identifierUnique: `${alert.name}.${alert.id.slice(-6)}` + } + } + }) +} diff --git a/tests/mockServer/data/alerts.json b/tests/mockServer/data/alerts.json new file mode 100644 index 0000000000..c882070214 --- /dev/null +++ b/tests/mockServer/data/alerts.json @@ -0,0 +1,468 @@ +{ + "activations": [ + { + "id": "a1b2c3d4-5662c7a4470ef28ed1df450e80d37624de7e1749", + "name": "alert_1", + "project": "workflow-proj", + "activation_time": "2024-10-23T10:15:30Z", + "notifications": [ + { + "kind": "email", + "err": "" + }, + { + "kind": "webhook", + "err": "" + }, + { + "kind": "git", + "err": "" + }, + { + "kind": "slack", + "err": "" + } + ], + "entity_id": "5662c7a4470ef28ed1df450e80d37624de7e1749.histogram-data-drift.general_drift", + "entity_kind": "model-endpoint-result", + "criteria": { + "count": 3, + "period": "30m" + }, + "event_kind": "data-drift-detected", + "severity": "medium", + "number_of_events": 15 + }, + { + "id": "b2c3d4e5-6772c8b5581ef39fd2df561f91e47635f8f2e263", + "name": "alert_2", + "project": "tutorial-admin", + "activation_time": "2024-10-24T12:20:30Z", + "notifications": [ + { + "kind": "email", + "err": "fail" + }, + { + "kind": "webhook", + "err": "fail" + }, + { + "kind": "git", + "err": "fail" + }, + { + "kind": "slack", + "err": "fail" + } + ], + "entity_id": "trainer-train.5a8037bd46cf192ce7e94b25f06a8d13", + "entity_kind": "job", + "criteria": { + "count": 10, + "period": "2h" + }, + "event_kind": "concept-drift-suspected", + "severity": "low", + "number_of_events": 7 + }, + { + "id": "c3d4e5f6-7883d9c6692ef40fe3df672a02f58746f9f3e374", + "name": "alert_3", + "project": "workflow-proj", + "activation_time": "2024-10-25T14:45:00Z", + "notifications": [ + { + "kind": "webhook", + "err": "" + } + ], + "entity_id": "application-1", + "entity_kind": "model-monitoring-application", + "criteria": { + "count": 2, + "period": "1h" + }, + "event_kind": "mm-app-anomaly-detected", + "severity": "high", + "number_of_events": 12 + }, + { + "id": "d4e5f6g7-8994ea1a780f0b1fg4df783b13f69857a0d4e485", + "name": "alert_4", + "project": "analysis-hub", + "activation_time": "2024-10-26T08:10:20Z", + "notifications": [ + { + "kind": "email", + "err": "" + }, + { + "kind": "git", + "err": "Authentication failed" + } + ], + "entity_id": "d5e6f4b3970ef28ed1df450e80d37624de7e1749.feature-importance.general_drift", + "entity_kind": "model-endpoint-result", + "criteria": { + "count": 8, + "period": "1h" + }, + "event_kind": "model-performance-detected", + "severity": "low", + "number_of_events": 5 + }, + { + "id": "e5f6g7h8-9105fb2b891f1c2hg5df894c24f70968b1e5f596", + "name": "alert_5", + "project": "reporting-app", + "activation_time": "2024-10-27T16:55:10Z", + "notifications": [ + { + "kind": "slack", + "err": "" + } + ], + "entity_id": "batch-inference-v2-infer.7ad15b839fc206e4ba9f02c84f31e57a", + "entity_kind": "job", + "criteria": { + "count": 4, + "period": "3h" + }, + "event_kind": "system-performance-suspected", + "severity": "high", + "number_of_events": 20 + }, + { + "id": "f6g7h8i9-1026fc3c902f2d3hi6df915d35g81079c2f6g717", + "name": "alert_6", + "project": "tutorial-admin", + "activation_time": "2024-10-28T09:25:40Z", + "notifications": [ + { + "kind": "email", + "err": "SMTP server not reachable" + } + ], + "entity_id": "a2b4c6d8970ef28ed1df450e80d37624de7e1749.statistical-shift.general_drift", + "entity_kind": "model-endpoint-result", + "criteria": { + "count": 6, + "period": "2h" + }, + "event_kind": "mm-app-failed", + "severity": "high", + "number_of_events": 3 + }, + { + "id": "g7h8i9j0-2137gd4d013f3e4ij7df026e46h92180d3g7h829", + "name": "alert_7", + "project": "workflow-proj", + "activation_time": "2024-10-29T07:30:50Z", + "notifications": [ + { + "kind": "webhook", + "err": "" + } + ], + "entity_id": "c6d3e7f0970ef28ed1df450e80d37624de7e1749.data-quality-issue.drift", + "entity_kind": "model-endpoint-result", + "criteria": { + "count": 1, + "period": "30m" + }, + "event_kind": "model-performance-suspected", + "severity": "low", + "number_of_events": 4 + }, + { + "id": "h8i9j0k1-3248he5e124g4f5jk8df137f57i032c4h8i9310", + "name": "alert_8", + "project": "analysis-hub", + "activation_time": "2024-10-30T11:15:10Z", + "notifications": [ + { + "kind": "slack", + "err": "Failed to send notification due to network issue" + } + ], + "entity_id": "batch-inference-v1-infer.c3b47590a6821d7ef9f2ab04c8e163a5", + "entity_kind": "job", + "criteria": { + "count": 9, + "period": "4h" + }, + "event_kind": "concept-drift-detected", + "severity": "medium", + "number_of_events": 16 + }, + { + "id": "i9j0k1l2-4359if6f235h5g6kl9df248g68j143d5i9j0421", + "name": "alert_9", + "project": "reporting-app", + "activation_time": "2024-10-31T17:05:35Z", + "notifications": [ + { + "kind": "email", + "err": "" + }, + { + "kind": "webhook", + "err": "" + } + ], + "entity_id": "d9e8f5b4970ef28ed1df450e80d37624de7e1749.input-missing-values.general_drift", + "entity_kind": "model-endpoint-result", + "criteria": { + "count": 7, + "period": "1h" + }, + "event_kind": "data-drift-suspected", + "severity": "high", + "number_of_events": 18 + }, + { + "id": "j0k1l2m3-5460jg7g346i6h7lm0df359h79k254e6j0k1532", + "name": "alert_10", + "project": "workflow-proj", + "activation_time": "2024-11-01T06:20:15Z", + "notifications": [ + { + "kind": "git", + "err": "" + } + ], + "entity_id": "c2a3e1f8970ef28ed1df450e80d37624de7e1749.class-distribution.general_drift", + "entity_kind": "model-endpoint-result", + "criteria": { + "count": 5, + "period": "2h" + }, + "event_kind": "system-performance-detected", + "severity": "medium", + "number_of_events": 9 + }, + { + "id": "k1l2m3n4-6571kh8h457j7i8mn1df460i80l365f7k1l2643", + "name": "alert_11", + "project": "analysis-hub", + "activation_time": "2024-11-02T14:10:50Z", + "notifications": [ + { + "kind": "email", + "err": "" + }, + { + "kind": "webhook", + "err": "API endpoint not responding" + } + ], + "entity_id": "f3d4b5a1970ef28ed1df450e80d37624de7e1749.outlier-detection.general_drift", + "entity_kind": "model-endpoint-result", + "criteria": { + "count": 10, + "period": "3h" + }, + "event_kind": "mm-app-anomaly-suspected", + "severity": "high", + "number_of_events": 13 + }, + { + "id": "l2m3n4o5-7682li9i568k8jno2df571j91m476g8l2m3754", + "name": "alert_12", + "project": "tutorial-admin", + "activation_time": "2024-11-03T18:35:25Z", + "notifications": [ + { + "kind": "slack", + "err": "" + } + ], + "entity_id": "tutorial-function.af2de87229ab49139748db785e0c3d6b", + "entity_kind": "job", + "criteria": { + "count": 3, + "period": "1h" + }, + "event_kind": "failed", + "severity": "low", + "number_of_events": 14 + }, + { + "id": "m3n4o5p6-8793mj0j679l9knp3df682k02n587h9m3n4875", + "name": "alert_13", + "project": "workflow-proj", + "activation_time": "2024-11-04T09:05:00Z", + "notifications": [ + { + "kind": "email", + "err": "SMTP server not reachable" + } + ], + "entity_id": "d5e6f4b3970ef28ed1df450e80d37624de7e1749.feature-importance.general_drift", + "entity_kind": "model-endpoint-result", + "criteria": { + "count": 8, + "period": "4h" + }, + "event_kind": "concept-drift-suspected", + "severity": "high", + "number_of_events": 6 + }, + { + "id": "n4o5p6q7-9804nk1k780m0lop4df793l13o698i0n4o5986", + "name": "alert_14", + "project": "reporting-app", + "activation_time": "2024-11-05T13:15:45Z", + "notifications": [ + { + "kind": "webhook", + "err": "" + } + ], + "entity_id": "application-2", + "entity_kind": "model-monitoring-application", + "criteria": { + "count": 2, + "period": "30m" + }, + "event_kind": "mm-app-failed", + "severity": "high", + "number_of_events": 11 + }, + { + "id": "o5p6q7r8-0915ol2l891n1mp5df804m24p709j1o5p6097", + "name": "alert_15", + "project": "tutorial-admin", + "activation_time": "2024-11-06T12:25:20Z", + "notifications": [ + { + "kind": "git", + "err": "" + } + ], + "entity_id": "get-data.a23bcd3ef5d9f0174abc8e09324a76bc", + "entity_kind": "job", + "criteria": { + "count": 5, + "period": "2h" + }, + "event_kind": "data-drift-detected", + "severity": "low", + "number_of_events": 2 + }, + { + "id": "p6q7r8s9-1026pm3m902o2nq6df915n35q810k2p6q7108", + "name": "alert_16", + "project": "workflow-proj", + "activation_time": "2024-11-07T14:00:35Z", + "notifications": [ + { + "kind": "slack", + "err": "Failed to send notification due to network issue" + } + ], + "entity_id": "c9e7f6a3970ef28ed1df450e80d37624de7e1749.null-value-frequency.general_drift", + "entity_kind": "model-endpoint-result", + "criteria": { + "count": 7, + "period": "1h" + }, + "event_kind": "system-performance-suspected", + "severity": "high", + "number_of_events": 17 + }, + { + "id": "q7r8s9t0-2137qn4n013p3or7df026o46r92190l3q7r8219", + "name": "alert_17", + "project": "analysis-hub", + "activation_time": "2024-11-08T08:40:55Z", + "notifications": [ + { + "kind": "email", + "err": "" + }, + { + "kind": "webhook", + "err": "" + } + ], + "entity_id": "b4d5f8e7970ef28ed1df450e80d37624de7e1749.imbalance-ratio.general_drift", + "entity_kind": "model-endpoint-result", + "criteria": { + "count": 9, + "period": "3h" + }, + "event_kind": "model-performance-detected", + "severity": "high", + "number_of_events": 19 + }, + { + "id": "r8s9t0u1-3248ro5o124q4ps8df137p57s032m4r8s9320", + "name": "alert_18", + "project": "reporting-app", + "activation_time": "2024-11-09T11:50:30Z", + "notifications": [ + { + "kind": "webhook", + "err": "" + } + ], + "entity_id": "get-dev.af12d780b9ce4a6c0357ef3941b5ad68", + "entity_kind": "job", + "criteria": { + "count": 6, + "period": "4h" + }, + "event_kind": "concept-drift-detected", + "severity": "low", + "number_of_events": 8 + }, + { + "id":"s9t0u1v2-4359sp6p235r4qt9df248q68t143n5s9t0421", + "name": "alert_19", + "project": "tutorial-admin", + "activation_time": "2024-11-10T10:10:10Z", + "notifications": [ + { + "kind": "git", + "err": "" + } + ], + "entity_id": "model-endpoint-19.app-19.result-19", + "entity_kind": "model-endpoint-result", + "criteria": { + "count": 4, + "period": "2h" + }, + "event_kind": "mm-app-anomaly-detected", + "severity": "high", + "number_of_events": 10 + }, + { + "id": "t0u1v2w3-5460tq7q346s5ru0df359r79u254o6t0u1532", + "name": "alert_20", + "project": "workflow-proj", + "activation_time": "2024-11-11T13:30:45Z", + "notifications": [ + { + "kind": "email", + "err": "" + }, + { + "kind": "slack", + "err": "Failed to send notification due to network issue" + } + ], + "entity_id": "d5e6f4b3970ef28ed1df450e80d37624de7e1749.feature-importance.general_drift", + "entity_kind": "model-endpoint-result", + "criteria": { + "count": 1, + "period": "30m" + }, + "event_kind": "mm-app-failed", + "severity": "high", + "number_of_events": 5 + } + ] +} diff --git a/tests/mockServer/mock.js b/tests/mockServer/mock.js index 11b4429dad..131c71bdf6 100644 --- a/tests/mockServer/mock.js +++ b/tests/mockServer/mock.js @@ -44,6 +44,7 @@ import { } from 'lodash' import mime from 'mime-types' +import alerts from './data/alerts.json' import frontendSpec from './data/frontendSpec.json' import projects from './data/projects.json' import projectsSummary from './data/summary.json' @@ -875,6 +876,13 @@ function getRun(req, res) { res.send({ data: run_prj_uid }) } +// TODO:ML-8368 add getAlert controller + +function getAlerts(req, res) { + // TODO:ML-8514 Update getAlerts to support both parameters and query strings. + res.send({ alerts: alerts.activations }) +} + function patchRun(req, res) { const collectedRun = runs.runs .filter(run => run.metadata.project === req.params.project) @@ -2614,6 +2622,7 @@ app.get(`${mlrunAPIIngress}/project-summaries/:project`, getProjectSummary) app.get(`${mlrunAPIIngress}/projects/:project/runs`, getRuns) app.get(`${mlrunAPIIngress}/projects/*/runs`, getRuns) +app.get(`${mlrunAPIIngress}/projects/*/alert-activations`, getAlerts) app.get(`${mlrunAPIIngress}/run/:project/:uid`, getRun) app.patch(`${mlrunAPIIngress}/run/:project/:uid`, patchRun) app.delete(`${mlrunAPIIngress}/projects/:project/runs/:uid`, deleteRun)