From 7d58361594b89435a5d565708e775443355ace7e Mon Sep 17 00:00:00 2001 From: mariana-furyk <58301139+mariana-furyk@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:06:51 +0200 Subject: [PATCH 01/12] Impl [Real time pipelines] Filter by 'function kind' (#2898) --- src/components/ModelsPage/Models/Models.js | 2 +- .../ModelsPage/RealTimePipelines/RealTimePipelines.js | 2 +- src/reducers/artifactsReducer.js | 8 ++------ 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/components/ModelsPage/Models/Models.js b/src/components/ModelsPage/Models/Models.js index f2c33da03f..ec2a93dbf1 100644 --- a/src/components/ModelsPage/Models/Models.js +++ b/src/components/ModelsPage/Models/Models.js @@ -187,7 +187,7 @@ const Models = ({ fetchModelFeatureVector }) => { filters: {}, config: { signal: abortControllerRef.current.signal, - params: { format: 'minimal' } + params: { format: 'minimal', kind: 'serving' } } }) ) diff --git a/src/components/ModelsPage/RealTimePipelines/RealTimePipelines.js b/src/components/ModelsPage/RealTimePipelines/RealTimePipelines.js index 9ebfd2632e..26763f650a 100644 --- a/src/components/ModelsPage/RealTimePipelines/RealTimePipelines.js +++ b/src/components/ModelsPage/RealTimePipelines/RealTimePipelines.js @@ -93,7 +93,7 @@ const RealTimePipelines = () => { project: params.projectName, filters, config: { - params: { format: 'minimal' }, + params: { format: 'minimal', kind: 'serving' }, ui: { controller: abortControllerRef.current, setRequestErrorMessage diff --git a/src/reducers/artifactsReducer.js b/src/reducers/artifactsReducer.js index c5838439d6..40ac5a10dc 100644 --- a/src/reducers/artifactsReducer.js +++ b/src/reducers/artifactsReducer.js @@ -22,7 +22,7 @@ import { defaultPendingHandler, hideLoading, showLoading } from './redux.util' import artifactsApi from '../api/artifacts-api' import functionsApi from '../api/functions-api' import modelEndpointsApi from '../api/modelEndpoints-api' -import { ARTIFACTS_TAB, DATASETS_TAB, FUNCTION_TYPE_SERVING, MODELS_TAB } from '../constants' +import { ARTIFACTS_TAB, DATASETS_TAB, MODELS_TAB } from '../constants' import { filterArtifacts } from '../utils/filterArtifacts' import { generateArtifacts } from '../utils/generateArtifacts' import { parseModelEndpoints } from '../utils/parseModelEndpoints' @@ -221,11 +221,7 @@ export const fetchArtifactsFunctions = createAsyncThunk( return functionsApi .getFunctions(project, filters, config, null) .then(({ data }) => { - return parseFunctions( - data.funcs.filter( - func => func.kind === FUNCTION_TYPE_SERVING && func.metadata.tag?.length - ) - ) + return parseFunctions(data.funcs) }) .catch(error => { largeResponseCatchHandler( From c12c59ace0eb810fd1b9b05f11f6f10a67ab9352 Mon Sep 17 00:00:00 2001 From: Ilank <63646693+ilan7empest@users.noreply.github.com> Date: Tue, 3 Dec 2024 09:40:38 +0200 Subject: [PATCH 02/12] Fix [Batch run] Add validation checks for secrets in Batch run wizard (#2897) --- src/elements/FormVolumesTable/formVolumesTable.util.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/elements/FormVolumesTable/formVolumesTable.util.js b/src/elements/FormVolumesTable/formVolumesTable.util.js index 013bee83f5..7544e494d2 100644 --- a/src/elements/FormVolumesTable/formVolumesTable.util.js +++ b/src/elements/FormVolumesTable/formVolumesTable.util.js @@ -18,6 +18,8 @@ under the Apache 2.0 license is conditioned upon your compliance with such restriction. */ import { isNil, map } from 'lodash' +import { getValidationRules } from 'igz-controls/utils/validation.util' + import { V3IO_VOLUME_TYPE, CONFIG_MAP_VOLUME_TYPE, @@ -165,7 +167,9 @@ export const generateVolumeInputsData = ( label: volumeTypeInputLabels[selectedType], required: true, textHidden: true, - type: 'input' + type: 'input', + validationRules: + selectedType === SECRET_VOLUME_TYPE ? getValidationRules('project.secrets.key') : [] } default: return null From 6a9eafad620be36f8124a2ac54a4d85e62b1f7ee Mon Sep 17 00:00:00 2001 From: Ilank <63646693+ilan7empest@users.noreply.github.com> Date: Tue, 3 Dec 2024 09:41:00 +0200 Subject: [PATCH 03/12] Fix [Workflows] Success message on failed request (#2899) --- src/elements/WorkflowsTable/WorkflowsTable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/elements/WorkflowsTable/WorkflowsTable.js b/src/elements/WorkflowsTable/WorkflowsTable.js index 3a60911ff7..cdffe90d8e 100644 --- a/src/elements/WorkflowsTable/WorkflowsTable.js +++ b/src/elements/WorkflowsTable/WorkflowsTable.js @@ -399,7 +399,7 @@ const WorkflowsTable = React.forwardRef( const handleRerun = useCallback( workflow => { dispatch(workflowsActions.rerunWorkflow(workflow.project, workflow.id)) - .then( + .then(() => dispatch( setNotification({ status: 200, From 3b7c09f9b603b51df1897eaf0f7f0d5744bc4ed4 Mon Sep 17 00:00:00 2001 From: Taras-Hlukhovetskyi <155433425+Taras-Hlukhovetskyi@users.noreply.github.com> Date: Tue, 3 Dec 2024 16:45:37 +0200 Subject: [PATCH 04/12] Fix [Project settings] no message after being removed from project members (#2905) --- .../ProjectSettings/ProjectSettings.js | 44 +++++++++++++------ src/elements/MembersPopUp/MembersPopUp.js | 8 +--- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/components/ProjectSettings/ProjectSettings.js b/src/components/ProjectSettings/ProjectSettings.js index 45b08bca96..9dcd893e1c 100644 --- a/src/components/ProjectSettings/ProjectSettings.js +++ b/src/components/ProjectSettings/ProjectSettings.js @@ -171,17 +171,30 @@ const ProjectSettings = () => { } }, [fetchProjectIdAndOwner, fetchProjectMembers, params.projectName, projectMembershipIsEnabled]) - const changeMembersCallback = jobId => { + const changeMembersCallback = (jobId, userIsValid) => { const fetchJob = () => { - projectsIguazioApi.getProjectJob(jobId).then(response => { - if (response.data.data.attributes.state !== COMPLETED_STATE) { - setTimeout(fetchJob, 1000) - } else { - fetchProjectMembers(membersState.projectInfo.id, membersState.projectInfo.owner).then( - () => { - membersDispatch({ - type: membersActions.GET_PROJECT_USERS_DATA_END - }) + projectsIguazioApi + .getProjectJob(jobId) + .then(response => { + if (response.data.data.attributes.state !== COMPLETED_STATE) { + setTimeout(fetchJob, 1000) + } else { + if (userIsValid) { + fetchProjectMembers(membersState.projectInfo.id, membersState.projectInfo.owner).then( + () => { + membersDispatch({ + type: membersActions.GET_PROJECT_USERS_DATA_END + }) + dispatch( + setNotification({ + status: 200, + id: Math.random(), + message: 'Members updated successfully' + }) + ) + } + ) + } else { dispatch( setNotification({ status: 200, @@ -189,10 +202,15 @@ const ProjectSettings = () => { message: 'Members updated successfully' }) ) + navigate('/projects/') } - ) - } - }) + } + }) + .catch(error => { + membersDispatch({ + type: membersActions.GET_PROJECT_USERS_DATA_END + }) + }) } membersDispatch({ diff --git a/src/elements/MembersPopUp/MembersPopUp.js b/src/elements/MembersPopUp/MembersPopUp.js index ea14880449..7e72600f76 100644 --- a/src/elements/MembersPopUp/MembersPopUp.js +++ b/src/elements/MembersPopUp/MembersPopUp.js @@ -37,7 +37,6 @@ import { getRoleOptions, initialNewMembersRole, DELETE_MODIFICATION } from './me import { isIgzVersionCompatible } from '../../utils/isIgzVersionCompatible' import { membersActions } from './membersReducer' import { showErrorNotification } from '../../utils/notifications.util' -import { useNavigate } from 'react-router-dom' import { USER_GROUP_ROLE, USER_ROLE } from '../../constants' @@ -62,7 +61,6 @@ const MembersPopUp = ({ changeMembersCallback, membersDispatch, membersState }) role: 'All' }) const dispatch = useDispatch() - const navigate = useNavigate() const handleOnClose = () => { setConfirmDiscard(false) @@ -195,11 +193,7 @@ const MembersPopUp = ({ changeMembersCallback, membersDispatch, membersState }) 'Project Security Admin' ) ?? false - if (validMember || userIsProjectSecurityAdmin) { - changeMembersCallback(response.data.data.id) - } else { - navigate('/projects/') - } + changeMembersCallback(response.data.data.id, validMember || userIsProjectSecurityAdmin) }) .catch(error => { const customErrorMsg = From 45922bf76cf17272503a831f98d73ba7632521e2 Mon Sep 17 00:00:00 2001 From: Andrew Mavdryk Date: Tue, 3 Dec 2024 16:45:54 +0200 Subject: [PATCH 05/12] Impl [Jobs] Pagination (#2904) --- package.json | 2 +- src/actions/jobs.js | 4 +- src/common/Chip/Chip.js | 2 +- src/common/Pagination/Pagination.js | 315 ++++++++++++++++++ src/common/Pagination/pagination.scss | 82 +++++ src/common/TagFilter/TagFilter.js | 11 +- src/components/ActionBar/ActionBar.js | 27 +- .../AddToFeatureVectorPage.js | 16 +- .../AddToFeatureVectorView.js | 38 +-- src/components/Datasets/Datasets.js | 16 +- src/components/Datasets/DatasetsView.js | 11 +- .../Details/DetailsHeader/DetailsHeader.js | 20 +- .../FeatureStore/FeatureSets/FeatureSets.js | 13 +- .../FeatureSets/FeatureSetsView.js | 11 +- .../FeatureVectors/FeatureVectors.js | 13 +- .../FeatureVectors/FeatureVectorsView.js | 9 +- .../FeatureStore/Features/Features.js | 43 +-- .../FeatureStore/Features/FeaturesView.js | 15 +- src/components/Files/Files.js | 16 +- src/components/Files/FilesView.js | 9 +- src/components/FilterMenu/FilterMenu.js | 22 +- src/components/FunctionsPage/Functions.js | 72 ++-- src/components/FunctionsPage/FunctionsView.js | 46 ++- .../FunctionsPage/functions.util.js | 8 +- src/components/Jobs/Jobs.js | 82 +++-- .../Jobs/MonitorJobs/MonitorJobs.js | 54 ++- .../ModelEndpoints/ModelEndpoints.js | 12 +- src/components/ModelsPage/Models/Models.js | 23 +- .../ModelsPage/Models/ModelsView.js | 11 +- .../RealTimePipelines/RealTimePipelines.js | 4 +- .../JobsMonitoring/JobsMonitoring.js | 53 ++- .../ProjectsJobsMonitoring.js | 96 +++--- src/components/Workflow/Workflow.js | 4 +- src/constants.js | 11 + .../ArtifactsTableRow/ArtifactsTableRow.js | 16 +- .../FeatureStoreTableRow.js | 14 +- .../FunctionsTableRow/FunctionsTableRow.js | 18 +- src/elements/JobsTable/JobsTable.js | 80 ++--- src/elements/TableCell/TableCell.js | 14 +- src/elements/TableLinkCell/TableLinkCell.js | 9 +- src/hooks/groupContent.hook.js | 133 +++++--- src/hooks/useFiltersFromSearchParams.hook.js | 12 - src/hooks/useJobsPageData.js | 103 ++++-- src/hooks/usePagination.hook.js | 217 ++++++++++++ src/layout/Content/Content.js | 221 ------------ src/layout/Content/content.util.js | 42 --- src/scss/main.scss | 4 +- src/types.js | 17 + src/utils/createJobsContent.js | 19 +- src/utils/filter.util.js | 19 +- src/utils/jobs.util.js | 31 +- src/utils/link-helper.util.js | 22 +- src/utils/parseFunctions.js | 1 - tests/mockServer/data/run.json | 52 +-- tests/mockServer/data/runs.json | 52 +-- tests/mockServer/mock.js | 73 +++- 56 files changed, 1388 insertions(+), 952 deletions(-) create mode 100644 src/common/Pagination/Pagination.js create mode 100644 src/common/Pagination/pagination.scss create mode 100644 src/hooks/usePagination.hook.js delete mode 100644 src/layout/Content/Content.js delete mode 100644 src/layout/Content/content.util.js diff --git a/package.json b/package.json index 0d1db383ad..f84231f54a 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.6", + "iguazio.dashboard-react-controls": "2.2.7", "is-wsl": "^1.1.0", "js-base64": "^2.5.2", "js-yaml": "^4.1.0", diff --git a/src/actions/jobs.js b/src/actions/jobs.js index 8634c28844..5761c47275 100644 --- a/src/actions/jobs.js +++ b/src/actions/jobs.js @@ -235,7 +235,7 @@ const jobsActions = { .then(({ data }) => { dispatch(jobsActions.fetchAllJobRunsSuccess(data.runs || [])) - return data.runs + return data }) .catch(error => { dispatch(jobsActions.fetchAllJobRunsFailure(error)) @@ -374,7 +374,7 @@ const jobsActions = { dispatch(jobsActions.fetchJobsSuccess(newJobs)) dispatch(jobsActions.setJobsData(data.runs || [])) - return newJobs + return { ...data, runs: newJobs } }) .catch(error => { dispatch(jobsActions.fetchJobsFailure(error)) diff --git a/src/common/Chip/Chip.js b/src/common/Chip/Chip.js index 5b23de71ac..aa3515c679 100644 --- a/src/common/Chip/Chip.js +++ b/src/common/Chip/Chip.js @@ -117,7 +117,7 @@ const Chip = React.forwardRef( setChipsSizes(state => ({ ...state, [chipIndex]: - chipRef.current.getBoundingClientRect().width + + (chipRef.current?.getBoundingClientRect?.()?.width ?? 0) + parseFloat(marginLeft) + parseFloat(marginRight) })) diff --git a/src/common/Pagination/Pagination.js b/src/common/Pagination/Pagination.js new file mode 100644 index 0000000000..47a916a5fa --- /dev/null +++ b/src/common/Pagination/Pagination.js @@ -0,0 +1,315 @@ +/* +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 React, { useCallback, useMemo, useRef } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { useNavigate, useParams, useSearchParams } from 'react-router-dom' +import { isEmpty, max, min } from 'lodash' + +import { RoundedIcon } from 'igz-controls/components' + +import { + BE_PAGE, + BE_PAGE_SIZE, + FE_PAGE, + FE_PAGE_END, + FE_PAGE_SIZE, + FE_PAGE_START, + ITEMS_COUNT_END, + ITEMS_COUNT_START +} from '../../constants' +import { PAGINATION_CONFIG } from '../../types' +import { getDefaultCloseDetailsLink } from '../../utils/link-helper.util' + +import { ReactComponent as DoubleArrow } from 'igz-controls/images/pagination-double-arrow.svg' +import { ReactComponent as Arrow } from 'igz-controls/images/pagination-arrow.svg' + +import './pagination.scss' + +const threeDotsString = '...' + +const Pagination = ({ page, paginationConfig, selectedItem = {} }) => { + const [, setSearchParams] = useSearchParams() + const navigate = useNavigate() + const params = useParams() + const paginationPagesRef = useRef() + const leftSideRef = useRef(0) + const rightSideRef = useRef(0) + + // Total pages are now calculated based on start and end pages + const totalPagesCount = useMemo( + () => paginationConfig[FE_PAGE_END] - paginationConfig[FE_PAGE_START] + 1, + [paginationConfig] + ) + + const navigationDisableState = useMemo(() => { + return { + prevBtn: paginationConfig[FE_PAGE] === 1, + prevDoubleBtn: paginationConfig[BE_PAGE] === 1, + nextBtn: + paginationConfig[FE_PAGE] === paginationConfig[FE_PAGE_END] && + !paginationConfig.paginationResponse?.['page-token'], + nextDoubleBtn: !paginationConfig.paginationResponse?.['page-token'] + } + }, [paginationConfig]) + + const prevDoubleBtnTooltip = useMemo(() => { + const visiblePagesCount = paginationConfig[BE_PAGE_SIZE] / paginationConfig[FE_PAGE_SIZE] + const firstPrevPage = paginationConfig[FE_PAGE_START] - visiblePagesCount + const lastPrevPage = firstPrevPage + visiblePagesCount - 1 + return firstPrevPage && lastPrevPage ? `Load pages ${firstPrevPage}-${lastPrevPage}` : '' + }, [paginationConfig]) + + const handlePageChange = useCallback(() => { + if (!isEmpty(selectedItem)) { + navigate(getDefaultCloseDetailsLink(params, page), { replace: true }) + } + }, [navigate, page, params, selectedItem]) + + const paginationItems = useMemo(() => { + if (!paginationConfig[FE_PAGE]) return [] + + const items = [] + const firstPage = paginationConfig[FE_PAGE_START] + const lastPage = paginationConfig[FE_PAGE_END] + + // Always show the first page in the dynamic range + items.push(firstPage) + + if (totalPagesCount <= 7) { + // If total pages within range are 7 or fewer, show all + for (let i = firstPage + 1; i <= lastPage; i++) { + items.push(i) + } + } else { + const isFirstFourRange = paginationConfig[FE_PAGE] < paginationConfig[FE_PAGE_START] + 4 + const isLastFourRange = paginationConfig[FE_PAGE] > paginationConfig[FE_PAGE_END] - 4 + let leftSide = Math.max(firstPage + 1, paginationConfig[FE_PAGE] - (isFirstFourRange ? 2 : 1)) + let rightSide = Math.min(lastPage - 1, paginationConfig[FE_PAGE] + (isLastFourRange ? 2 : 1)) + + if (paginationConfig[FE_PAGE] <= firstPage + 3) { + // Case when activePage is close to the start + rightSide = firstPage + 4 + } else if (paginationConfig[FE_PAGE] >= lastPage - 3) { + // Case when activePage is close to the end + leftSide = lastPage - 4 + } + + rightSideRef.current = rightSide + leftSideRef.current = leftSide + + if (leftSide > firstPage + 1) { + items.push(threeDotsString) + } + + for (let i = leftSide; i <= rightSide; i++) { + items.push(i) + } + + if (rightSide < lastPage - 1) { + items.push(threeDotsString) + } + + // Always show the last page in the dynamic range + items.push(lastPage) + } + + return items + }, [paginationConfig, totalPagesCount]) + + const goToPage = page => { + setSearchParams(prevSearchParams => { + prevSearchParams.set(FE_PAGE, page) + + return prevSearchParams + }) + + handlePageChange() + } + + const goToNextBePage = () => { + setSearchParams(prevSearchParams => { + prevSearchParams.set(BE_PAGE, paginationConfig[BE_PAGE] + 1) + return prevSearchParams + }) + + handlePageChange() + } + + const goToPrevBePage = customFePage => { + setSearchParams(prevSearchParams => { + prevSearchParams.set(BE_PAGE, paginationConfig[BE_PAGE] - 1) + + if (customFePage) { + prevSearchParams.set(FE_PAGE, customFePage) + } + + return prevSearchParams + }) + + handlePageChange() + } + + const goToNextFePage = () => { + if (paginationConfig[FE_PAGE] === paginationConfig[FE_PAGE_END]) { + goToNextBePage() + } else { + setSearchParams(prevSearchParams => { + prevSearchParams.set(FE_PAGE, paginationConfig[FE_PAGE] + 1) + + return prevSearchParams + }) + + handlePageChange() + } + } + + const goToPrevFePage = () => { + if (paginationConfig[FE_PAGE] === paginationConfig[FE_PAGE_START]) { + goToPrevBePage(paginationConfig[FE_PAGE] - 1) + } else { + setSearchParams(prevSearchParams => { + prevSearchParams.set(FE_PAGE, paginationConfig[FE_PAGE] - 1) + + return prevSearchParams + }) + + handlePageChange() + } + } + + const getPageNumberStyle = useCallback((paginationItems, paginationItem, index) => { + const maxPage = max(paginationItems) + const paginationItemWidth = `${maxPage.toString().length}ch` + + return { width: paginationItemWidth } + }, []) + + const getPaginationPagesStyle = useCallback( + (paginationItems, paginationItem, index) => { + const maxFePages = paginationConfig[BE_PAGE_SIZE] / paginationConfig[FE_PAGE_SIZE] + const maxPage = max(paginationItems) + const maxPageCount = min([7, maxPage, maxFePages]) + const paginationPagesWidth = `${maxPageCount * 40}px` + + return { minWidth: paginationPagesWidth } + }, + [paginationConfig] + ) + + return ( +
+ {paginationConfig.isNewResponse && ( + <> +
+ Showing {paginationConfig[ITEMS_COUNT_START]} - {paginationConfig[ITEMS_COUNT_END]} +
+
+ goToPrevBePage()} + tooltipText={!navigationDisableState.prevDoubleBtn ? prevDoubleBtnTooltip : ''} + disabled={navigationDisableState.prevDoubleBtn} + > + + + goToPrevFePage()} + tooltipText={!navigationDisableState.prevBtn ? 'Previous page' : ''} + disabled={navigationDisableState.prevBtn} + > + + + +
+ {paginationItems.map( + (pageItem, index) => + pageItem && ( + + ) + )} +
+ + goToNextFePage()} + tooltipText={!navigationDisableState.nextBtn ? 'Next page' : ''} + disabled={navigationDisableState.nextBtn} + > + + + goToNextBePage()} + tooltipText={ + !navigationDisableState.nextDoubleBtn + ? `Load page ${paginationConfig[FE_PAGE_END] + 1}+` + : '' + } + disabled={navigationDisableState.nextDoubleBtn} + > + + +
+
+ + )} +
+ ) +} + +Pagination.propTypes = { + page: PropTypes.string.isRequired, + paginationConfig: PAGINATION_CONFIG.isRequired, + selectedItem: PropTypes.shape({}) +} + +export default Pagination diff --git a/src/common/Pagination/pagination.scss b/src/common/Pagination/pagination.scss new file mode 100644 index 0000000000..a39d10b793 --- /dev/null +++ b/src/common/Pagination/pagination.scss @@ -0,0 +1,82 @@ +@import '~igz-controls/scss/colors'; +@import '~igz-controls/scss/variables'; + +.pagination { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 4px 4px; + + .pagination-items-count, .pagination-items-selector { + width: 140px + } + + .pagination-items-selector { + display: flex; + justify-content: right; + } + + .pagination-navigation { + flex-grow: 1; + display: flex; + justify-content: center; + } + + .pagination-pages { + display: flex; + justify-content: center; + } + + .pagination-btn { + color: $primary; + cursor: default; + box-sizing: border-box; + display: flex; + justify-content: center; + border: none; + padding: 4px 5px; + margin: 0 2px; + font-size: 14px; + font-weight: normal; + + &:hover:not(.pagination-btn_active):not(:disabled) { + cursor: pointer; + } + + &:disabled { + color: $bombay; + cursor: default; + } + + &.pagination-btn_active { + color: $malibu; + font-weight: bold; + } + + .pagination-page-number { + padding: 2px 3px; + box-sizing: content-box; + border-bottom: 2px solid transparent; + border-top: 2px solid transparent; + } + + &.pagination-page-btn { + &:hover:not(.pagination-btn_active) { + .pagination-page-number { + border-bottom: 2px solid $primary; + } + } + } + + &.pagination-dots { + cursor: default; + display: inline-flex; + } + } + + .pagination-navigate-btn { + &.pagination-navigate-prev-btn { + transform: rotate(180deg); + } + } +} diff --git a/src/common/TagFilter/TagFilter.js b/src/common/TagFilter/TagFilter.js index 45cfb4042b..dde03dc66b 100644 --- a/src/common/TagFilter/TagFilter.js +++ b/src/common/TagFilter/TagFilter.js @@ -23,10 +23,11 @@ import { useNavigate, useParams } from 'react-router-dom' import TagFilterDropdown from './TagFilterDropdown' -import { ReactComponent as Caret } from 'igz-controls/images/dropdown.svg' - +import { getDefaultCloseDetailsLink } from '../../utils/link-helper.util' import { KEY_CODES, TAG_FILTER_LATEST } from '../../constants' +import { ReactComponent as Caret } from 'igz-controls/images/dropdown.svg' + import './tagFilter.scss' const TagFilter = ({ label, onChange, page, tagFilterOptions, value }) => { @@ -74,11 +75,7 @@ const TagFilter = ({ label, onChange, page, tagFilterOptions, value }) => { }) if (params.jobId || params.name) { - navigate( - `/projects/${params.projectName}/${page.toLowerCase()}${ - params.pageTab ? `/${params.pageTab}` : '' - }` - ) + navigate(getDefaultCloseDetailsLink(params, page)) } setTagFilter(searchItem?.id || event.target.value || TAG_FILTER_LATEST) diff --git a/src/components/ActionBar/ActionBar.js b/src/components/ActionBar/ActionBar.js index a0fba4db87..de9c470f85 100644 --- a/src/components/ActionBar/ActionBar.js +++ b/src/components/ActionBar/ActionBar.js @@ -26,7 +26,7 @@ import { Form } from 'react-final-form' import { createForm } from 'final-form' import { isEmpty, isEqual, isNil, mapValues, pickBy } from 'lodash' import { useDispatch, useSelector } from 'react-redux' -import { useNavigate, useParams, useSearchParams } from 'react-router-dom' +import { useNavigate, useParams } from 'react-router-dom' import DatePicker from '../../common/DatePicker/DatePicker' import FilterMenuModal from '../FilterMenuModal/FilterMenuModal' @@ -54,22 +54,23 @@ import { ReactComponent as ExpandIcon } from 'igz-controls/images/expand.svg' import { ReactComponent as RefreshIcon } from 'igz-controls/images/refresh.svg' const ActionBar = ({ + actionButtons = [], + allRowsAreExpanded, autoRefreshIsEnabled = false, autoRefreshIsStopped = false, - actionButtons = [], cancelRequest = null, children, - expand, filters, filtersConfig, - handleExpandAll, handleRefresh, hidden = false, navigateLink, page, removeSelectedItem = null, + setSearchParams, setSelectedRowData = null, tab = '', + toggleAllRows, withRefreshButton = true, withoutExpandButton }) => { @@ -80,7 +81,6 @@ const ActionBar = ({ const dispatch = useDispatch() const params = useParams() const navigate = useNavigate() - const [, setSearchParams] = useSearchParams() const actionBarClassNames = classnames('action-bar', hidden && 'action-bar_hidden') @@ -214,8 +214,8 @@ const ActionBar = ({ saveFilters(newFilters) removeSelectedItem && dispatch(removeSelectedItem({})) setSelectedRowData && setSelectedRowData({}) - handleExpandAll && handleExpandAll(true) - handleRefresh(newFilters) + toggleAllRows && toggleAllRows(true) + handleRefresh(newFilters, true) } }, [ @@ -229,7 +229,7 @@ const ActionBar = ({ filtersStore.groupBy, removeSelectedItem, setSelectedRowData, - handleExpandAll, + toggleAllRows, handleRefresh, navigate, navigateLink @@ -380,10 +380,10 @@ const ActionBar = ({ {!withoutExpandButton && filtersStore.groupBy !== GROUP_BY_NONE && ( handleExpandAll()} + tooltipText={allRowsAreExpanded ? 'Collapse' : 'Expand all'} + onClick={() => toggleAllRows(allRowsAreExpanded)} > - {expand ? : } + {allRowsAreExpanded ? : } )} @@ -410,20 +410,21 @@ ActionBar.propTypes = { }) ]) ), + allRowsAreExpanded: PropTypes.bool, autoRefreshIsEnabled: PropTypes.bool, autoRefreshIsStopped: PropTypes.bool, cancelRequest: PropTypes.func, - expand: PropTypes.bool, filters: PropTypes.object.isRequired, filtersConfig: FILTERS_CONFIG.isRequired, - handleExpandAll: PropTypes.func, handleRefresh: PropTypes.func.isRequired, hidden: PropTypes.bool, navigateLink: PropTypes.string, page: PropTypes.string.isRequired, removeSelectedItem: PropTypes.func, + setSearchParams: PropTypes.func.isRequired, setSelectedRowData: PropTypes.func, tab: PropTypes.string, + toggleAllRows: PropTypes.func, withRefreshButton: PropTypes.bool, withoutExpandButton: PropTypes.bool } diff --git a/src/components/AddToFeatureVectorPage/AddToFeatureVectorPage.js b/src/components/AddToFeatureVectorPage/AddToFeatureVectorPage.js index 7cae64951e..bf5ce105b0 100644 --- a/src/components/AddToFeatureVectorPage/AddToFeatureVectorPage.js +++ b/src/components/AddToFeatureVectorPage/AddToFeatureVectorPage.js @@ -19,7 +19,7 @@ such restriction. */ import React, { useCallback, useEffect, useRef, useState, useMemo } from 'react' import { connect, useDispatch, useSelector } from 'react-redux' -import { useNavigate, useParams } from 'react-router-dom' +import { useNavigate, useParams, useSearchParams } from 'react-router-dom' import AddToFeatureVectorView from './AddToFeatureVectorView' import FeaturesTablePanel from '../../elements/FeaturesTablePanel/FeaturesTablePanel' @@ -71,6 +71,7 @@ const AddToFeatureVectorPage = ({ const addToFeatureVectorPageRef = useRef(null) const abortControllerRef = useRef(new AbortController()) const params = useParams() + const [, setSearchParams] = useSearchParams() const navigate = useNavigate() const tableStore = useSelector(store => store.tableStore) const filtersStore = useSelector(store => store.filtersStore) @@ -215,7 +216,7 @@ const AddToFeatureVectorPage = ({ handleRefresh(addToFeatureVectorFilters) }, [addToFeatureVectorFilters, handleRefresh]) - const handleRemoveFeature = useCallback( + const collapseRowCallback = useCallback( feature => { const newStoreSelectedRowData = { ...featureStore.features.selectedRowData.content @@ -231,7 +232,7 @@ const AddToFeatureVectorPage = ({ [featureStore.features.selectedRowData.content, removeFeature, selectedRowData] ) - const handleRequestOnExpand = useCallback( + const expandRowCallback = useCallback( async feature => { const featureIdentifier = getFeatureIdentifier(feature) @@ -272,11 +273,11 @@ const AddToFeatureVectorPage = ({ [fetchFeature, tableStore.isTablePanelOpen] ) - const { latestItems, handleExpandRow } = useGroupContent( + const { latestItems, toggleRow } = useGroupContent( content, getFeatureIdentifier, - handleRemoveFeature, - handleRequestOnExpand, + collapseRowCallback, + expandRowCallback, null, FEATURE_STORE_PAGE, FEATURES_TAB @@ -349,15 +350,16 @@ const AddToFeatureVectorPage = ({ filters={addToFeatureVectorFilters} filtersConfig={filtersConfig} filtersStore={filtersStore} - handleExpandRow={handleExpandRow} handleRefresh={handleRefresh} handleRefreshWithFilters={handleRefreshWithFilters} pageData={pageData} ref={addToFeatureVectorPageRef} requestErrorMessage={requestErrorMessage} selectedRowData={selectedRowData} + setSearchParams={setSearchParams} tableContent={tableContent} tableStore={tableStore} + toggleRow={toggleRow} virtualizationConfig={virtualizationConfig} /> ) diff --git a/src/components/AddToFeatureVectorPage/AddToFeatureVectorView.js b/src/components/AddToFeatureVectorPage/AddToFeatureVectorView.js index ad25a97c65..0f6b6dec6f 100644 --- a/src/components/AddToFeatureVectorPage/AddToFeatureVectorView.js +++ b/src/components/AddToFeatureVectorPage/AddToFeatureVectorView.js @@ -27,10 +27,7 @@ import NoData from '../../common/NoData/NoData' import Table from '../Table/Table' import FeatureStoreTableRow from '../../elements/FeatureStoreTableRow/FeatureStoreTableRow' -import { - ADD_TO_FEATURE_VECTOR_TAB, - FEATURE_STORE_PAGE -} from '../../constants' +import { ADD_TO_FEATURE_VECTOR_TAB, FEATURE_STORE_PAGE } from '../../constants' import { VIRTUALIZATION_CONFIG } from '../../types' import { getNoDataMessage } from '../../utils/getNoDataMessage' import { isRowRendered } from '../../hooks/useVirtualization.hook' @@ -48,14 +45,15 @@ const AddToFeatureVectorView = React.forwardRef( filters, filtersConfig, filtersStore, - handleExpandRow, handleRefresh, handleRefreshWithFilters, pageData, requestErrorMessage, selectedRowData, + setSearchParams, tableContent, tableStore, + toggleRow, virtualizationConfig }, ref @@ -66,17 +64,18 @@ const AddToFeatureVectorView = React.forwardRef(
- - - -
+ + + +
{(featureStore.loading || featureStore.features.loading) && }
@@ -110,14 +109,14 @@ const AddToFeatureVectorView = React.forwardRef( isRowRendered(virtualizationConfig, index) && ( ) )} @@ -138,14 +137,15 @@ AddToFeatureVectorView.propTypes = { filters: PropTypes.object.isRequired, filtersConfig: PropTypes.object.isRequired, filtersStore: PropTypes.object.isRequired, - handleExpandRow: PropTypes.func.isRequired, handleRefresh: PropTypes.func.isRequired, handleRefreshWithFilters: PropTypes.func.isRequired, pageData: PropTypes.object.isRequired, requestErrorMessage: PropTypes.string.isRequired, selectedRowData: PropTypes.object.isRequired, + setSearchParams: PropTypes.func.isRequired, tableContent: PropTypes.arrayOf(PropTypes.object).isRequired, tableStore: PropTypes.object.isRequired, + toggleRow: PropTypes.func.isRequired, virtualizationConfig: VIRTUALIZATION_CONFIG.isRequired } diff --git a/src/components/Datasets/Datasets.js b/src/components/Datasets/Datasets.js index 068642c7fd..971571383b 100644 --- a/src/components/Datasets/Datasets.js +++ b/src/components/Datasets/Datasets.js @@ -19,7 +19,7 @@ such restriction. */ import React, { useCallback, useEffect, useRef, useState, useMemo } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { useLocation, useNavigate, useParams } from 'react-router-dom' +import { useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom' import DatasetsView from './DatasetsView' import AddArtifactTagPopUp from '../../elements/AddArtifactTagPopUp/AddArtifactTagPopUp' @@ -81,6 +81,7 @@ const Datasets = () => { const location = useLocation() const navigate = useNavigate() const params = useParams() + const [, setSearchParams] = useSearchParams() const filters = useFiltersFromSearchParams(filtersConfig) const abortControllerRef = useRef(new AbortController()) const tagAbortControllerRef = useRef(new AbortController()) @@ -252,7 +253,7 @@ const Datasets = () => { handleRefresh(filters) } - const handleExpand = useCallback( + const expandRowCallback = useCallback( (dataset, content) => { const dataSetIdentifier = getArtifactIdentifier(dataset) @@ -272,7 +273,7 @@ const Datasets = () => { [params.projectName] ) - const handleRemoveRowData = useCallback( + const collapseRowCallback = useCallback( dataset => { const newStoreSelectedRowData = { ...artifactsStore.dataSets.selectedRowData.content @@ -288,11 +289,11 @@ const Datasets = () => { [artifactsStore.dataSets.selectedRowData.content, dispatch, selectedRowData] ) - const { latestItems, handleExpandRow } = useGroupContent( + const { latestItems, toggleRow } = useGroupContent( datasets, getArtifactIdentifier, - handleRemoveRowData, - handleExpand, + collapseRowCallback, + expandRowCallback, null, DATASETS_PAGE ) @@ -410,7 +411,6 @@ const Datasets = () => { filters={filters} filtersStore={filtersStore} getAndSetSelectedArtifact={getAndSetSelectedArtifact} - handleExpandRow={handleExpandRow} handleRefresh={handleRefresh} handleRefreshWithFilters={handleRefreshWithFilters} handleRegisterDataset={handleRegisterDataset} @@ -421,10 +421,12 @@ const Datasets = () => { selectedDataset={selectedDataset} selectedRowData={selectedRowData} setMaxArtifactsErrorIsShown={setMaxArtifactsErrorIsShown} + setSearchParams={setSearchParams} setSelectedDatasetMin={setSelectedDatasetMin} sortProps={{ sortTable, selectedColumnName, getSortingIcon }} tableContent={sortedTableContent} tableHeaders={sortedTableHeaders} + toggleRow={toggleRow} viewMode={viewMode} virtualizationConfig={virtualizationConfig} /> diff --git a/src/components/Datasets/DatasetsView.js b/src/components/Datasets/DatasetsView.js index f1e058eeeb..99dabcf433 100644 --- a/src/components/Datasets/DatasetsView.js +++ b/src/components/Datasets/DatasetsView.js @@ -52,7 +52,6 @@ const DatasetsView = React.forwardRef( filters, filtersStore, getAndSetSelectedArtifact, - handleExpandRow, handleRefresh, handleRefreshWithFilters, handleRegisterDataset, @@ -62,10 +61,12 @@ const DatasetsView = React.forwardRef( selectedDataset, selectedRowData, setMaxArtifactsErrorIsShown, + setSearchParams, setSelectedDatasetMin, sortProps, tableContent, tableHeaders, + toggleRow, viewMode = null, virtualizationConfig }, @@ -94,6 +95,7 @@ const DatasetsView = React.forwardRef( filtersConfig={filtersConfig} handleRefresh={handleRefresh} page={DATASETS_PAGE} + setSearchParams={setSearchParams} withRefreshButton withoutExpandButton > @@ -141,12 +143,12 @@ const DatasetsView = React.forwardRef( isRowRendered(virtualizationConfig, index) && ( ) )} @@ -184,10 +186,9 @@ DatasetsView.propTypes = { artifactsStore: PropTypes.object.isRequired, datasets: PropTypes.arrayOf(PropTypes.object).isRequired, detailsFormInitialValues: PropTypes.object.isRequired, - getAndSetSelectedArtifact: PropTypes.func.isRequired, filters: PropTypes.object.isRequired, filtersStore: PropTypes.object.isRequired, - handleExpandRow: PropTypes.func.isRequired, + getAndSetSelectedArtifact: PropTypes.func.isRequired, handleRefresh: PropTypes.func.isRequired, handleRefreshWithFilters: PropTypes.func.isRequired, handleRegisterDataset: PropTypes.func.isRequired, @@ -197,10 +198,12 @@ DatasetsView.propTypes = { selectedDataset: PropTypes.object.isRequired, selectedRowData: PropTypes.object.isRequired, setMaxArtifactsErrorIsShown: PropTypes.func.isRequired, + setSearchParams: PropTypes.func.isRequired, setSelectedDatasetMin: PropTypes.func.isRequired, sortProps: SORT_PROPS, tableContent: PropTypes.arrayOf(PropTypes.object).isRequired, tableHeaders: PropTypes.arrayOf(PropTypes.object).isRequired, + toggleRow: PropTypes.func.isRequired, viewMode: PropTypes.string, virtualizationConfig: VIRTUALIZATION_CONFIG.isRequired } diff --git a/src/components/Details/DetailsHeader/DetailsHeader.js b/src/components/Details/DetailsHeader/DetailsHeader.js index 08c58396f7..998779a172 100644 --- a/src/components/Details/DetailsHeader/DetailsHeader.js +++ b/src/components/Details/DetailsHeader/DetailsHeader.js @@ -39,7 +39,7 @@ import { formatDatetime } from '../../../utils' import { TERTIARY_BUTTON } from 'igz-controls/constants' import { ACTIONS_MENU } from '../../../types' import { getViewMode } from '../../../utils/helper' -import { generateUrlFromRouterPath } from '../../../utils/link-helper.util' +import { generateUrlFromRouterPath, getDefaultCloseDetailsLink } from '../../../utils/link-helper.util' import { getFilteredSearchParams } from '../../../utils/filter.util' import { ReactComponent as Close } from 'igz-controls/images/close.svg' @@ -134,11 +134,13 @@ const DetailsHeader = ({ {isDetailsScreen && !pageData.details.hideBackBtn && !isDetailsPopUp && ( @@ -328,12 +330,8 @@ const DetailsHeader = ({ data-testid="details-close-btn" to={ getCloseDetailsLink - ? generateUrlFromRouterPath( - getCloseDetailsLink(window.location, selectedItem.name) - ) - : `/projects/${params.projectName}/${pageData.page.toLowerCase()}${ - params.pageTab ? `/${params.pageTab}` : tab ? `/${tab}` : '' - }` + ? getCloseDetailsLink(selectedItem.name) + : getDefaultCloseDetailsLink(params, pageData.page, tab) } onClick={handleCancelClick} > diff --git a/src/components/FeatureStore/FeatureSets/FeatureSets.js b/src/components/FeatureStore/FeatureSets/FeatureSets.js index 5ed0c5798e..c897586118 100644 --- a/src/components/FeatureStore/FeatureSets/FeatureSets.js +++ b/src/components/FeatureStore/FeatureSets/FeatureSets.js @@ -162,7 +162,7 @@ const FeatureSets = ({ handleRefresh(featureSetsFilters) }, [featureSetsFilters, handleRefresh]) - const handleRemoveFeatureSet = useCallback( + const collapseRowCallback = useCallback( featureSet => { const newStoreSelectedRowData = { ...featureStore.featureSets.selectedRowData.content @@ -179,7 +179,7 @@ const FeatureSets = ({ [featureStore.featureSets.selectedRowData.content, selectedRowData, removeFeatureSet] ) - const handleRequestOnExpand = useCallback( + const expandRowCallback = useCallback( item => { const featureSetIdentifier = getFeatureSetIdentifier(item) @@ -218,11 +218,11 @@ const FeatureSets = ({ [fetchExpandedFeatureSet, featureSetsFilters.tag, params.projectName] ) - const { latestItems, handleExpandRow } = useGroupContent( + const { latestItems, toggleRow } = useGroupContent( featureSets, getFeatureSetIdentifier, - handleRemoveFeatureSet, - handleRequestOnExpand, + collapseRowCallback, + expandRowCallback, null, FEATURE_STORE_PAGE, FEATURE_SETS_TAB @@ -431,7 +431,6 @@ const FeatureSets = ({ featureStore={featureStore} filtersStore={filtersStore} filters={featureSetsFilters} - handleExpandRow={handleExpandRow} handleRefreshWithFilters={handleRefreshWithFilters} handleRefresh={handleRefresh} pageData={pageData} @@ -440,8 +439,10 @@ const FeatureSets = ({ selectedFeatureSet={selectedFeatureSet} selectedRowData={selectedRowData} setFeatureSetsPanelIsOpen={setFeatureSetsPanelIsOpen} + setSearchParams={setSearchParams} setSelectedFeatureSetMin={handleSelectFeatureSet} tableContent={tableContent} + toggleRow={toggleRow} virtualizationConfig={virtualizationConfig} /> ) diff --git a/src/components/FeatureStore/FeatureSets/FeatureSetsView.js b/src/components/FeatureStore/FeatureSets/FeatureSetsView.js index ddc1ca7e5f..6a82618ce3 100644 --- a/src/components/FeatureStore/FeatureSets/FeatureSetsView.js +++ b/src/components/FeatureStore/FeatureSets/FeatureSetsView.js @@ -52,7 +52,6 @@ const FeatureSetsView = React.forwardRef( featureStore, filtersStore, filters, - handleExpandRow, handleRefresh, handleRefreshWithFilters, pageData, @@ -60,8 +59,10 @@ const FeatureSetsView = React.forwardRef( selectedFeatureSet, selectedRowData, setFeatureSetsPanelIsOpen, + setSearchParams, setSelectedFeatureSetMin, tableContent, + toggleRow, virtualizationConfig }, { featureStoreRef } @@ -85,6 +86,7 @@ const FeatureSetsView = React.forwardRef( filtersConfig={filtersConfig} handleRefresh={handleRefresh} page={FEATURE_STORE_PAGE} + setSearchParams={setSearchParams} tab={FEATURE_SETS_TAB} withoutExpandButton > @@ -125,13 +127,13 @@ const FeatureSetsView = React.forwardRef( isRowRendered(virtualizationConfig, index) && ( ) )} @@ -161,7 +163,6 @@ FeatureSetsView.propTypes = { featureSetsPanelIsOpen: PropTypes.bool.isRequired, featureStore: PropTypes.object.isRequired, filtersStore: PropTypes.object.isRequired, - handleExpandRow: PropTypes.func.isRequired, handleRefresh: PropTypes.func.isRequired, handleRefreshWithFilters: PropTypes.func.isRequired, pageData: PropTypes.object.isRequired, @@ -169,8 +170,10 @@ FeatureSetsView.propTypes = { selectedFeatureSet: PropTypes.object.isRequired, selectedRowData: PropTypes.object.isRequired, setFeatureSetsPanelIsOpen: PropTypes.func.isRequired, + setSearchParams: PropTypes.func.isRequired, setSelectedFeatureSetMin: PropTypes.func.isRequired, tableContent: PropTypes.arrayOf(PropTypes.object).isRequired, + toggleRow: PropTypes.func.isRequired, virtualizationConfig: VIRTUALIZATION_CONFIG.isRequired } diff --git a/src/components/FeatureStore/FeatureVectors/FeatureVectors.js b/src/components/FeatureStore/FeatureVectors/FeatureVectors.js index 94f27e54f3..65b0b9c31c 100644 --- a/src/components/FeatureStore/FeatureVectors/FeatureVectors.js +++ b/src/components/FeatureStore/FeatureVectors/FeatureVectors.js @@ -245,7 +245,7 @@ const FeatureVectors = ({ handleRefresh(featureVectorsFilters) }, [featureVectorsFilters, handleRefresh]) - const handleRemoveFeatureVector = useCallback( + const collapseRowCallback = useCallback( featureVector => { const newStoreSelectedRowData = { ...featureStore.featureVectors.selectedRowData.content @@ -262,7 +262,7 @@ const FeatureVectors = ({ [featureStore.featureVectors.selectedRowData.content, selectedRowData, removeFeatureVector] ) - const handleRequestOnExpand = useCallback( + const expandRowCallback = useCallback( featureVector => { const featureVectorIdentifier = getFeatureVectorIdentifier(featureVector) @@ -302,11 +302,11 @@ const FeatureVectors = ({ [fetchFeatureVector, featureVectorsFilters.tag, params.projectName] ) - const { latestItems, handleExpandRow } = useGroupContent( + const { latestItems, toggleRow } = useGroupContent( featureVectors, getFeatureVectorIdentifier, - handleRemoveFeatureVector, - handleRequestOnExpand, + collapseRowCallback, + expandRowCallback, null, FEATURE_STORE_PAGE, FEATURE_VECTORS_TAB @@ -494,7 +494,6 @@ const FeatureVectors = ({ featureVectors={featureVectors} filters={featureVectorsFilters} filtersStore={filtersStore} - handleExpandRow={handleExpandRow} handleRefresh={handleRefresh} handleRefreshWithFilters={handleRefreshWithFilters} pageData={pageData} @@ -503,8 +502,10 @@ const FeatureVectors = ({ selectedFeatureVector={selectedFeatureVector} selectedRowData={selectedRowData} setCreateVectorPopUpIsOpen={setCreateVectorPopUpIsOpen} + setSearchParams={setSearchParams} setSelectedFeatureVector={handleSelectFeatureVector} tableContent={tableContent} + toggleRow={toggleRow} virtualizationConfig={virtualizationConfig} /> ) diff --git a/src/components/FeatureStore/FeatureVectors/FeatureVectorsView.js b/src/components/FeatureStore/FeatureVectors/FeatureVectorsView.js index ea31815b60..616854aba3 100644 --- a/src/components/FeatureStore/FeatureVectors/FeatureVectorsView.js +++ b/src/components/FeatureStore/FeatureVectors/FeatureVectorsView.js @@ -48,7 +48,6 @@ const FeatureVectorsView = React.forwardRef( featureVectors, filters, filtersStore, - handleExpandRow, handleRefresh, handleRefreshWithFilters, pageData, @@ -56,8 +55,10 @@ const FeatureVectorsView = React.forwardRef( selectedFeatureVector, selectedRowData, setCreateVectorPopUpIsOpen, + setSearchParams, setSelectedFeatureVector, tableContent, + toggleRow, virtualizationConfig }, { featureStoreRef } @@ -79,6 +80,7 @@ const FeatureVectorsView = React.forwardRef( filtersConfig={filtersConfig} handleRefresh={handleRefresh} page={FEATURE_STORE_PAGE} + setSearchParams={setSearchParams} tab={FEATURE_VECTORS_TAB} withoutExpandButton > @@ -117,13 +119,13 @@ const FeatureVectorsView = React.forwardRef( isRowRendered(virtualizationConfig, index) && ( ) )} @@ -152,7 +154,6 @@ FeatureVectorsView.propTypes = { featureVectors: PropTypes.arrayOf(PropTypes.object).isRequired, filters: PropTypes.object.isRequired, filtersStore: PropTypes.object.isRequired, - handleExpandRow: PropTypes.func.isRequired, handleRefresh: PropTypes.func.isRequired, handleRefreshWithFilters: PropTypes.func.isRequired, pageData: PropTypes.object.isRequired, @@ -160,8 +161,10 @@ FeatureVectorsView.propTypes = { selectedFeatureVector: PropTypes.object.isRequired, selectedRowData: PropTypes.object.isRequired, setCreateVectorPopUpIsOpen: PropTypes.func.isRequired, + setSearchParams: PropTypes.func.isRequired, setSelectedFeatureVector: PropTypes.func.isRequired, tableContent: PropTypes.arrayOf(PropTypes.object).isRequired, + toggleRow: PropTypes.func.isRequired, virtualizationConfig: VIRTUALIZATION_CONFIG.isRequired } diff --git a/src/components/FeatureStore/Features/Features.js b/src/components/FeatureStore/Features/Features.js index bcc14d8bfd..328a742e1f 100644 --- a/src/components/FeatureStore/Features/Features.js +++ b/src/components/FeatureStore/Features/Features.js @@ -18,7 +18,7 @@ under the Apache 2.0 license is conditioned upon your compliance with such restriction. */ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' -import { useParams } from 'react-router-dom' +import { useParams, useSearchParams } from 'react-router-dom' import { connect, useDispatch, useSelector } from 'react-redux' import { mapValues, map } from 'lodash' @@ -67,6 +67,7 @@ const Features = ({ const [selectedRowData, setSelectedRowData] = useState({}) const [requestErrorMessage, setRequestErrorMessage] = useState('') const params = useParams() + const [, setSearchParams] = useSearchParams() const featureStore = useSelector(store => store.featureStore) const filtersStore = useSelector(store => store.filtersStore) const featuresFilters = useFiltersFromSearchParams(filtersConfig) @@ -101,11 +102,7 @@ const Features = ({ return mapValues(prevSelectedRowData, feature => ({ ...feature, content: map(feature.content, contentItem => - createFeaturesRowData( - contentItem.data, - tableStore.isTablePanelOpen, - false - ) + createFeaturesRowData(contentItem.data, tableStore.isTablePanelOpen, false) ) })) }) @@ -186,7 +183,7 @@ const Features = ({ handleRefresh(featuresFilters) }, [featuresFilters, handleRefresh]) - const handleRemoveFeature = useCallback( + const collapseRowCallback = useCallback( feature => { const newStoreSelectedRowData = feature.data.ui.type === 'feature' @@ -210,7 +207,7 @@ const Features = ({ ] ) - const handleRequestOnExpand = useCallback( + const expandRowCallback = useCallback( feature => { const featureIdentifier = getFeatureIdentifier(feature) const fetchData = feature.ui?.type === 'feature' ? fetchFeature : fetchEntity @@ -226,11 +223,7 @@ const Features = ({ .then(result => { if (result?.length > 0) { const content = [...result].map(contentItem => - createFeaturesRowData( - contentItem, - tableStore.isTablePanelOpen, - false - ) + createFeaturesRowData(contentItem, tableStore.isTablePanelOpen, false) ) setSelectedRowData(state => ({ ...state, @@ -256,11 +249,11 @@ const Features = ({ [fetchEntity, fetchFeature, tableStore.isTablePanelOpen] ) - const { latestItems, handleExpandRow } = useGroupContent( + const { latestItems, toggleRow } = useGroupContent( features, getFeatureIdentifier, - handleRemoveFeature, - handleRequestOnExpand, + collapseRowCallback, + expandRowCallback, null, FEATURE_STORE_PAGE, FEATURES_TAB @@ -272,18 +265,9 @@ const Features = ({ return createFeaturesRowData(contentItem, tableStore.isTablePanelOpen, true) }) : features.map(contentItem => - createFeaturesRowData( - contentItem, - tableStore.isTablePanelOpen, - false - ) + createFeaturesRowData(contentItem, tableStore.isTablePanelOpen, false) ) - }, [ - features, - filtersStore.groupBy, - latestItems, - tableStore.isTablePanelOpen - ]) + }, [features, filtersStore.groupBy, latestItems, tableStore.isTablePanelOpen]) const getPopUpTemplate = useCallback( action => { @@ -356,20 +340,21 @@ const Features = ({ return ( ) diff --git a/src/components/FeatureStore/Features/FeaturesView.js b/src/components/FeatureStore/Features/FeaturesView.js index 1871bded40..854784e2e9 100644 --- a/src/components/FeatureStore/Features/FeaturesView.js +++ b/src/components/FeatureStore/Features/FeaturesView.js @@ -39,19 +39,20 @@ const FeaturesView = React.forwardRef( ( { actionsMenu, - features, featureStore, + features, filters, filtersStore, getPopUpTemplate, - handleExpandRow, handleRefresh, handleRefreshWithFilters, pageData, requestErrorMessage, selectedRowData, + setSearchParams, tableContent, tableStore, + toggleRow, virtualizationConfig }, { featureStoreRef } @@ -75,6 +76,7 @@ const FeaturesView = React.forwardRef( filtersConfig={filtersConfig} handleRefresh={handleRefresh} page={FEATURE_STORE_PAGE} + setSearchParams={setSearchParams} tab={FEATURES_TAB} withoutExpandButton > @@ -112,14 +114,14 @@ const FeaturesView = React.forwardRef( isRowRendered(virtualizationConfig, index) && ( ) )} @@ -134,19 +136,20 @@ const FeaturesView = React.forwardRef( FeaturesView.propTypes = { actionsMenu: PropTypes.array.isRequired, - features: PropTypes.arrayOf(PropTypes.object).isRequired, featureStore: PropTypes.object.isRequired, + features: PropTypes.arrayOf(PropTypes.object).isRequired, filters: PropTypes.object.isRequired, filtersStore: PropTypes.object.isRequired, getPopUpTemplate: PropTypes.func.isRequired, - handleExpandRow: PropTypes.func.isRequired, handleRefresh: PropTypes.func.isRequired, handleRefreshWithFilters: PropTypes.func.isRequired, pageData: PropTypes.object.isRequired, requestErrorMessage: PropTypes.string.isRequired, selectedRowData: PropTypes.object.isRequired, + setSearchParams: PropTypes.func.isRequired, tableContent: PropTypes.arrayOf(PropTypes.object).isRequired, tableStore: PropTypes.object.isRequired, + toggleRow: PropTypes.func.isRequired, virtualizationConfig: VIRTUALIZATION_CONFIG.isRequired } diff --git a/src/components/Files/Files.js b/src/components/Files/Files.js index 7fae84a117..a281f2a778 100644 --- a/src/components/Files/Files.js +++ b/src/components/Files/Files.js @@ -19,7 +19,7 @@ such restriction. */ import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { useLocation, useNavigate, useParams } from 'react-router-dom' +import { useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom' import { isEmpty } from 'lodash' import AddArtifactTagPopUp from '../../elements/AddArtifactTagPopUp/AddArtifactTagPopUp' @@ -82,6 +82,7 @@ const Files = () => { const location = useLocation() const navigate = useNavigate() const params = useParams() + const [, setSearchParams] = useSearchParams() const viewMode = getViewMode(window.location.search) const filters = useFiltersFromSearchParams(filtersConfig) const abortControllerRef = useRef(new AbortController()) @@ -220,7 +221,7 @@ const Files = () => { ] ) - const handleRemoveRowData = useCallback( + const collapseRowCallback = useCallback( file => { const newStoreSelectedRowData = { ...artifactsStore.files.selectedRowData.content @@ -236,7 +237,7 @@ const Files = () => { [artifactsStore.files.selectedRowData.content, dispatch, selectedRowData] ) - const handleExpand = useCallback( + const expandRowCallback = useCallback( (file, content) => { const fileIdentifier = getArtifactIdentifier(file) @@ -254,11 +255,11 @@ const Files = () => { [params.projectName] ) - const { latestItems, handleExpandRow } = useGroupContent( + const { latestItems, toggleRow } = useGroupContent( files, getArtifactIdentifier, - handleRemoveRowData, - handleExpand, + collapseRowCallback, + expandRowCallback, null, FILES_PAGE ) @@ -409,7 +410,6 @@ const Files = () => { filters={filters} filtersStore={filtersStore} getAndSetSelectedArtifact={getAndSetSelectedArtifact} - handleExpandRow={handleExpandRow} handleRefresh={handleRefresh} handleRefreshWithFilters={handleRefreshWithFilters} handleRegisterArtifact={handleRegisterArtifact} @@ -421,10 +421,12 @@ const Files = () => { selectedFile={selectedFile} selectedRowData={selectedRowData} setMaxArtifactsErrorIsShown={setMaxArtifactsErrorIsShown} + setSearchParams={setSearchParams} setSelectedFileMin={setSelectedFileMin} sortProps={{ sortTable, selectedColumnName, getSortingIcon }} tableContent={sortedTableContent} tableHeaders={sortedTableHeaders} + toggleRow={toggleRow} viewMode={viewMode} virtualizationConfig={virtualizationConfig} /> diff --git a/src/components/Files/FilesView.js b/src/components/Files/FilesView.js index db6f6d51a2..caf0828c9c 100644 --- a/src/components/Files/FilesView.js +++ b/src/components/Files/FilesView.js @@ -52,7 +52,6 @@ const FilesView = React.forwardRef( filters, filtersStore, getAndSetSelectedArtifact, - handleExpandRow, handleRefresh, handleRefreshWithFilters, handleRegisterArtifact, @@ -63,10 +62,12 @@ const FilesView = React.forwardRef( selectedFile, selectedRowData, setMaxArtifactsErrorIsShown, + setSearchParams, setSelectedFileMin, sortProps, tableContent, tableHeaders, + toggleRow, viewMode = null, virtualizationConfig }, @@ -95,6 +96,7 @@ const FilesView = React.forwardRef( filtersConfig={filtersConfig} handleRefresh={handleRefresh} page={FILES_PAGE} + setSearchParams={setSearchParams} withRefreshButton withoutExpandButton > @@ -140,13 +142,13 @@ const FilesView = React.forwardRef( isRowRendered(virtualizationConfig, index) && ( ) )} @@ -187,7 +189,6 @@ FilesView.propTypes = { filters: PropTypes.object.isRequired, filtersStore: PropTypes.object.isRequired, getAndSetSelectedArtifact: PropTypes.func.isRequired, - handleExpandRow: PropTypes.func.isRequired, handleRefresh: PropTypes.func.isRequired, handleRefreshWithFilters: PropTypes.func.isRequired, handleRegisterArtifact: PropTypes.func.isRequired, @@ -198,10 +199,12 @@ FilesView.propTypes = { selectedFile: PropTypes.object.isRequired, selectedRowData: PropTypes.object.isRequired, setMaxArtifactsErrorIsShown: PropTypes.func.isRequired, + setSearchParams: PropTypes.func.isRequired, setSelectedFileMin: PropTypes.func.isRequired, sortProps: SORT_PROPS, tableContent: PropTypes.arrayOf(PropTypes.object).isRequired, tableHeaders: PropTypes.arrayOf(PropTypes.object).isRequired, + toggleRow: PropTypes.func.isRequired, viewMode: PropTypes.string, virtualizationConfig: VIRTUALIZATION_CONFIG.isRequired } diff --git a/src/components/FilterMenu/FilterMenu.js b/src/components/FilterMenu/FilterMenu.js index 2ad145b177..bf7ddb6fd8 100644 --- a/src/components/FilterMenu/FilterMenu.js +++ b/src/components/FilterMenu/FilterMenu.js @@ -19,7 +19,7 @@ such restriction. */ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react' import PropTypes from 'prop-types' -import { useNavigate, useParams } from 'react-router-dom' +import { useParams } from 'react-router-dom' import { useDispatch, useSelector } from 'react-redux' import { cloneDeep, isEqual } from 'lodash' @@ -52,10 +52,11 @@ import { STATUS_FILTER, TAG_FILTER } from '../../constants' +import detailsActions from '../../actions/details' import { filterSelectOptions, tagFilterOptions } from './filterMenu.settings' import { generateProjectsList } from '../../utils/projects' +import { getDefaultCloseDetailsLink } from '../../utils/link-helper.util' import { removeFilters, setFilterProjectOptions, setFilters } from '../../reducers/filtersReducer' -import detailsActions from '../../actions/details' import './filterMenu.scss' @@ -66,11 +67,11 @@ const FilterMenu = ({ cancelRequest = () => {}, expand = false, filters, - handleExpandAll = () => {}, hidden = false, onChange, page, tab = '', + toggleAllRows = () => {}, withoutExpandButton = false }) => { const [labels, setLabels] = useState('') @@ -78,7 +79,6 @@ const FilterMenu = ({ const [entities, setEntities] = useState('') const [tagOptions, setTagOptions] = useState(tagFilterOptions) const [autoRefresh, setAutoRefresh] = useState(AUTO_REFRESH_ID) - const navigate = useNavigate() const params = useParams() const selectOptions = useMemo(() => cloneDeep(filterSelectOptions), []) const dispatch = useDispatch() @@ -149,18 +149,14 @@ const FilterMenu = ({ cancelRequest(REQUEST_CANCELED) } else { if ((params.jobId || params.name) && !isRefreshed) { - navigate( - `/projects/${params.projectName}/${page.toLowerCase()}${ - params.pageTab ? `/${params.pageTab}` : tab ? `/${tab}` : '' - }` - ) + getDefaultCloseDetailsLink(params, page, tab) } - handleExpandAll && handleExpandAll(true) + toggleAllRows && toggleAllRows(true) onChange(data) } }, - [params, page, tab, handleExpandAll, onChange, navigate, changes.counter, cancelRequest] + [params, page, tab, toggleAllRows, onChange, changes.counter, cancelRequest] ) const filtersHelper = async (changes, dispatch) => { @@ -487,7 +483,7 @@ const FilterMenu = ({ handleExpandAll()} + onClick={() => toggleAllRows(expand)} > {expand ? : } @@ -505,11 +501,11 @@ FilterMenu.propTypes = { cancelRequest: PropTypes.func, expand: PropTypes.bool, filters: PropTypes.arrayOf(PropTypes.shape({})).isRequired, - handleExpandAll: PropTypes.func, hidden: PropTypes.bool, onChange: PropTypes.func.isRequired, page: PropTypes.string.isRequired, tab: PropTypes.string, + toggleAllRows: PropTypes.func, withoutExpandButton: PropTypes.bool } diff --git a/src/components/FunctionsPage/Functions.js b/src/components/FunctionsPage/Functions.js index 1e9baf56d4..a68cda3b97 100644 --- a/src/components/FunctionsPage/Functions.js +++ b/src/components/FunctionsPage/Functions.js @@ -20,7 +20,7 @@ such restriction. import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { connect, useDispatch, useSelector } from 'react-redux' import { isEmpty } from 'lodash' -import { useLocation, useNavigate, useParams } from 'react-router-dom' +import { useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom' import FunctionsView from './FunctionsView' import JobWizard from '../JobWizard/JobWizard' @@ -90,7 +90,7 @@ const Functions = ({ const [jobWizardIsOpened, setJobWizardIsOpened] = useState(false) const [jobWizardMode, setJobWizardMode] = useState(null) const filtersStore = useSelector(store => store.filtersStore) - const [selectedRowData, setSelectedRowData] = useState({}) + const [expandedRowsData, setExpandedRowsData] = useState({}) const [requestErrorMessage, setRequestErrorMessage] = useState('') const [deletingFunctions, setDeletingFunctions] = useState({}) const abortControllerRef = useRef(new AbortController()) @@ -99,6 +99,7 @@ const Functions = ({ const terminatePollRef = useRef(null) const { isDemoMode, isStagingMode } = useMode() const params = useParams() + const [, setSearchParams] = useSearchParams() const navigate = useNavigate() const location = useLocation() const dispatch = useDispatch() @@ -197,7 +198,7 @@ const Functions = ({ filters => { setFunctions([]) setSelectedFunctionMin({}) - setSelectedRowData({}) + setExpandedRowsData({}) return fetchData(filters) }, @@ -208,7 +209,7 @@ const Functions = ({ (func, content) => { const funcIdentifier = getFunctionIdentifier(func) - setSelectedRowData(state => { + setExpandedRowsData(state => { return { ...state, [funcIdentifier]: { @@ -225,33 +226,36 @@ const Functions = ({ const handleCollapse = useCallback( func => { const funcIdentifier = getFunctionIdentifier(func.data) - const newPageDataSelectedRowData = { ...selectedRowData } + const newPageDataSelectedRowData = { ...expandedRowsData } delete newPageDataSelectedRowData[funcIdentifier] - setSelectedRowData(newPageDataSelectedRowData) + setExpandedRowsData(newPageDataSelectedRowData) }, - [selectedRowData] + [expandedRowsData] ) - const handleExpandAllCallback = (collapse, content) => { - const newSelectedRowData = {} - if (collapse) { - setSelectedRowData({}) - } else { - Object.entries(content).forEach(([key, value]) => { - newSelectedRowData[key] = { - content: value.map(contentItem => - createFunctionsRowData(contentItem, params.projectName, false) - ) - } - }) - } + const handleExpandAllCallback = useCallback( + (collapse, content) => { + const newSelectedRowData = {} + if (collapse) { + setExpandedRowsData({}) + } else { + Object.entries(content).forEach(([key, value]) => { + newSelectedRowData[key] = { + content: value.map(contentItem => + createFunctionsRowData(contentItem, params.projectName, false) + ) + } + }) + } - setSelectedRowData(newSelectedRowData) - } + setExpandedRowsData(newSelectedRowData) + }, + [params.projectName] + ) - const { latestItems, handleExpandRow, expand, handleExpandAll } = useGroupContent( + const { latestItems, allRowsAreExpanded, toggleRow, toggleAllRows } = useGroupContent( functions, getFunctionIdentifier, handleCollapse, @@ -501,7 +505,6 @@ const Functions = ({ useInitialTableFetch({ fetchData, - setExpandedRowsData: setSelectedRowData, createRowData: rowItem => createFunctionsRowData(rowItem, params.projectName), filters: functionsFilters }) @@ -512,7 +515,7 @@ const Functions = ({ return () => { setSelectedFunctionMin({}) setFunctions([]) - setSelectedRowData({}) + setExpandedRowsData({}) abortController.abort(REQUEST_CANCELED) } }, [params.projectName]) @@ -526,7 +529,7 @@ const Functions = ({ useEffect(() => { checkForSelectedFunction( params.funcName, - selectedRowData, + expandedRowsData, functions, params.hash, params.tag, @@ -543,7 +546,7 @@ const Functions = ({ params.hash, params.projectName, params.tag, - selectedRowData + expandedRowsData ]) useEffect(() => { @@ -674,7 +677,7 @@ const Functions = ({ const virtualizationConfig = useVirtualization({ rowsData: { content: tableContent, - expandedRowsData: selectedRowData, + expandedRowsData, selectedItem: selectedFunction }, heightData: { @@ -688,14 +691,15 @@ const Functions = ({ return ( ) diff --git a/src/components/FunctionsPage/FunctionsView.js b/src/components/FunctionsPage/FunctionsView.js index 0138c25373..5844ba80cf 100644 --- a/src/components/FunctionsPage/FunctionsView.js +++ b/src/components/FunctionsPage/FunctionsView.js @@ -31,11 +31,7 @@ import NoData from '../../common/NoData/NoData' import Table from '../Table/Table' import { ConfirmDialog } from 'igz-controls/components' -import { - FUNCTIONS_PAGE, - PANEL_CREATE_MODE, - PANEL_EDIT_MODE -} from '../../constants' +import { FUNCTIONS_PAGE, PANEL_CREATE_MODE, PANEL_EDIT_MODE } from '../../constants' import { SECONDARY_BUTTON } from 'igz-controls/constants' import { FILTERS_CONFIG, VIRTUALIZATION_CONFIG } from '../../types' import { getNoDataMessage } from '../../utils/getNoDataMessage' @@ -43,14 +39,15 @@ import { isRowRendered } from '../../hooks/useVirtualization.hook' const FunctionsView = ({ actionsMenu, + allRowsAreExpanded, closePanel, confirmData, createFunctionSuccess, editableItem, - expand, + expandedRowsData, + filters, filtersChangeCallback, filtersStore, - filters, functions, functionsFiltersConfig, functionsPanelIsOpen, @@ -59,16 +56,16 @@ const FunctionsView = ({ handleCancel, handleDeployFunctionFailure, handleDeployFunctionSuccess, - handleExpandAll, - handleExpandRow, handleSelectFunction, isDemoMode, pageData, - retryRequest, requestErrorMessage, + retryRequest, selectedFunction, - selectedRowData, + setSearchParams, tableContent, + toggleAllRows, + toggleRow, virtualizationConfig }) => { const params = useParams() @@ -82,13 +79,14 @@ const FunctionsView = ({
) @@ -180,7 +178,6 @@ const FunctionsView = ({ message={confirmData.message} /> )} - ) } @@ -192,11 +189,12 @@ FunctionsView.defaultPropTypes = { FunctionsView.propTypes = { actionsMenu: PropTypes.func.isRequired, + allRowsAreExpanded: PropTypes.bool.isRequired, closePanel: PropTypes.func.isRequired, confirmData: PropTypes.object, createFunctionSuccess: PropTypes.func.isRequired, editableItem: PropTypes.object, - expand: PropTypes.bool.isRequired, + expandedRowsData: PropTypes.object.isRequired, filtersChangeCallback: PropTypes.func.isRequired, filtersStore: PropTypes.object.isRequired, functions: PropTypes.arrayOf(PropTypes.object).isRequired, @@ -207,15 +205,15 @@ FunctionsView.propTypes = { handleCancel: PropTypes.func.isRequired, handleDeployFunctionFailure: PropTypes.func.isRequired, handleDeployFunctionSuccess: PropTypes.func.isRequired, - handleExpandAll: PropTypes.func.isRequired, - handleExpandRow: PropTypes.func.isRequired, handleSelectFunction: PropTypes.func.isRequired, pageData: PropTypes.object.isRequired, - retryRequest: PropTypes.func.isRequired, requestErrorMessage: PropTypes.string.isRequired, + retryRequest: PropTypes.func.isRequired, selectedFunction: PropTypes.object.isRequired, - selectedRowData: PropTypes.object.isRequired, + setSearchParams: PropTypes.func.isRequired, tableContent: PropTypes.arrayOf(PropTypes.object).isRequired, + toggleAllRows: PropTypes.func.isRequired, + toggleRow: PropTypes.func.isRequired, virtualizationConfig: VIRTUALIZATION_CONFIG.isRequired } diff --git a/src/components/FunctionsPage/functions.util.js b/src/components/FunctionsPage/functions.util.js index e256fc1170..145426c51e 100644 --- a/src/components/FunctionsPage/functions.util.js +++ b/src/components/FunctionsPage/functions.util.js @@ -502,7 +502,7 @@ const chooseOrFetchFunction = (selectedFunction, dispatch, fetchFunction, funcMi export const checkForSelectedFunction = ( name, - selectedRowData, + expandedRowsData, functions, hash, tag, @@ -513,7 +513,7 @@ export const checkForSelectedFunction = ( ) => { queueMicrotask(() => { if (name || hash) { - const functionsList = selectedRowData?.[name]?.content || functions + const functionsList = expandedRowsData?.[name]?.content || functions if (functionsList.length > 0) { const searchItem = searchFunctionItem( @@ -559,11 +559,11 @@ export const searchFunctionItem = ( item = functions.find(func => { if (withFunctionTag) { - [name, tag] = paramsHash.split(':') + ;[name, tag] = paramsHash.split(':') return isEqual(func.tag, tag) && isEqual(func.name, name) } else { - [name, hash] = paramsHash.split('@') + ;[name, hash] = paramsHash.split('@') return isEqual(func.hash, hash) && isEqual(func.name, name) } diff --git a/src/components/Jobs/Jobs.js b/src/components/Jobs/Jobs.js index 5be62f5a54..2d5bdb63f3 100755 --- a/src/components/Jobs/Jobs.js +++ b/src/components/Jobs/Jobs.js @@ -20,6 +20,7 @@ such restriction. import React, { useEffect, useState, useMemo, useLayoutEffect, useCallback } from 'react' import { connect, useSelector } from 'react-redux' import { useNavigate, useParams, Outlet, useLocation } from 'react-router-dom' +import { defaultsDeep } from 'lodash' import Breadcrumbs from '../../common/Breadcrumbs/Breadcrumbs' import ContentMenu from '../../elements/ContentMenu/ContentMenu' @@ -70,22 +71,46 @@ const Jobs = ({ fetchAllJobRuns, fetchJobFunction, fetchJobs }) => { const artifactsStore = useSelector(store => store.artifactsStore) const appStore = useSelector(store => store.appStore) + const initialTabData = useMemo(() => { + return { + [MONITOR_JOBS_TAB]: { + filtersConfig: getJobsFiltersConfig(params.jobName), + modalFilters: , + parseQueryParamsCallback: parseJobsQueryParamsCallback + }, + [MONITOR_WORKFLOWS_TAB]: { + filtersConfig: getWorkflowsFiltersConfig(), + modalFilters: , + parseQueryParamsCallback: parseWorkflowsQueryParamsCallback + }, + [SCHEDULE_TAB]: { + filtersConfig: getScheduledFiltersConfig(), + modalFilters: , + parseQueryParamsCallback: parseScheduledQueryParamsCallback + } + } + }, [params.jobName]) + const { abortControllerRef, abortJobRef, abortingJobs, editableItem, getWorkflows, + handleMonitoring, + handleRefreshJobs, + handleRerunJob, jobRuns, - jobs, jobWizardIsOpened, jobWizardMode, - handleMonitoring, - handleRerunJob, + jobs, + paginatedJobs, + paginationConfigJobsRef, refreshJobs, refreshScheduled, requestErrorMessage, scheduledJobs, + searchParams, setAbortingJobs, setEditableItem, setJobRuns, @@ -93,8 +118,9 @@ const Jobs = ({ fetchAllJobRuns, fetchJobFunction, fetchJobs }) => { setJobWizardMode, setJobs, setScheduledJobs, + setSearchParams, terminateAbortTasksPolling - } = useJobsPageData(fetchAllJobRuns, fetchJobFunction, fetchJobs) + } = useJobsPageData(fetchAllJobRuns, fetchJobFunction, fetchJobs, initialTabData, selectedTab) const handleActionsMenuClick = () => { setJobWizardMode(PANEL_CREATE_MODE) @@ -109,27 +135,21 @@ const Jobs = ({ fetchAllJobRuns, fetchJobFunction, fetchJobs }) => { ) const tabData = useMemo(() => { - return { - [MONITOR_JOBS_TAB]: { - filtersConfig: getJobsFiltersConfig(params.jobName), - handleRefresh: refreshJobs, - modalFilters: , - parseQueryParamsCallback: parseJobsQueryParamsCallback + return defaultsDeep( + { + [MONITOR_JOBS_TAB]: { + handleRefresh: handleRefreshJobs + }, + [MONITOR_WORKFLOWS_TAB]: { + handleRefresh: getWorkflows + }, + [SCHEDULE_TAB]: { + handleRefresh: refreshScheduled + } }, - [MONITOR_WORKFLOWS_TAB]: { - filtersConfig: getWorkflowsFiltersConfig(), - handleRefresh: getWorkflows, - modalFilters: , - parseQueryParamsCallback: parseWorkflowsQueryParamsCallback - }, - [SCHEDULE_TAB]: { - filtersConfig: getScheduledFiltersConfig(), - handleRefresh: refreshScheduled, - modalFilters: , - parseQueryParamsCallback: parseScheduledQueryParamsCallback - } - } - }, [getWorkflows, params.jobName, refreshJobs, refreshScheduled]) + initialTabData + ) + }, [getWorkflows, handleRefreshJobs, initialTabData, refreshScheduled]) useLayoutEffect(() => { setSelectedTab( @@ -212,11 +232,12 @@ const Jobs = ({ fetchAllJobRuns, fetchJobFunction, fetchJobs }) => { filtersConfig={tabData[selectedTab].filtersConfig} handleRefresh={tabData[selectedTab].handleRefresh} hidden={Boolean(params.jobId || params.workflowId)} + key={selectedTab} page={JOBS_MONITORING_PAGE} + setSearchParams={setSearchParams} tab={selectedTab} withRefreshButton withoutExpandButton - key={selectedTab} > {tabData[selectedTab].modalFilters} @@ -231,15 +252,18 @@ const Jobs = ({ fetchAllJobRuns, fetchJobFunction, fetchJobs }) => { getWorkflows, handleRerunJob, jobRuns, + jobWizardIsOpened, + jobWizardMode, jobs, jobsFiltersConfig: tabData[MONITOR_JOBS_TAB].filtersConfig, - jobWizardMode, - jobWizardIsOpened, + paginatedJobs, + paginationConfigJobsRef, refreshJobs, refreshScheduled, requestErrorMessage, - scheduledJobs, scheduledFiltersConfig: tabData[SCHEDULE_TAB].filtersConfig, + scheduledJobs, + searchParams, setAbortingJobs, setConfirmData, setEditableItem, @@ -248,8 +272,8 @@ const Jobs = ({ fetchAllJobRuns, fetchJobFunction, fetchJobs }) => { setJobWizardMode, setJobs, setScheduledJobs, - terminateAbortTasksPolling, tabData, + terminateAbortTasksPolling, workflowsFiltersConfig: tabData[MONITOR_WORKFLOWS_TAB].filtersConfig }} > diff --git a/src/components/Jobs/MonitorJobs/MonitorJobs.js b/src/components/Jobs/MonitorJobs/MonitorJobs.js index 16b73b0d12..c6101293a5 100644 --- a/src/components/Jobs/MonitorJobs/MonitorJobs.js +++ b/src/components/Jobs/MonitorJobs/MonitorJobs.js @@ -17,7 +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 React, { useEffect, useMemo, useRef, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useParams } from 'react-router-dom' import { useDispatch } from 'react-redux' @@ -27,7 +27,6 @@ import TableTop from '../../../elements/TableTop/TableTop' import { GROUP_BY_NONE, MONITOR_JOBS_TAB, REQUEST_CANCELED } from '../../../constants' import { JobsContext } from '../Jobs' import { createJobsMonitorTabContent } from '../../../utils/createJobsContent' -import { fetchInitialJobs } from './monitorJobs.util' import { setFilters } from '../../../reducers/filtersReducer' import { useMode } from '../../../hooks/mode.hook' import { useFiltersFromSearchParams } from '../../../hooks/useFiltersFromSearchParams.hook' @@ -45,13 +44,15 @@ const MonitorJobs = () => { jobRuns, jobs, jobsFiltersConfig, + paginatedJobs, refreshJobs, requestErrorMessage, + searchParams, setAbortingJobs, setJobRuns, setJobs, - terminateAbortTasksPolling, - tabData + tabData, + terminateAbortTasksPolling } = React.useContext(JobsContext) const jobsAreInitializedRef = useRef(false) @@ -61,27 +62,20 @@ const MonitorJobs = () => { ) const tableContent = useMemo( - () => - createJobsMonitorTabContent(params.jobName ? jobRuns : jobs, params.jobName, isStagingMode), - [isStagingMode, jobRuns, jobs, params.jobName] + () => createJobsMonitorTabContent(paginatedJobs, params.jobName, isStagingMode), + [isStagingMode, paginatedJobs, params.jobName] ) - useEffect(() => { - fetchInitialJobs( - filters, - selectedJob, - params.jobId, - refreshJobs, - jobsAreInitializedRef - ) - }, [ - dispatch, - filters, - params.jobId, - params.projectName, - refreshJobs, - selectedJob - ]) + const getBackLink = useCallback( + useSavedParams => { + let queryParams = useSavedParams + ? getSavedSearchParams(window.location.search) + : `?${searchParams.toString()}` + + return `/projects/${params.projectName}/jobs/${MONITOR_JOBS_TAB}${queryParams}` + }, + [params.projectName, searchParams] + ) useEffect(() => { dispatch(setFilters({ groupBy: GROUP_BY_NONE })) @@ -107,23 +101,19 @@ const MonitorJobs = () => { return ( <> - {params.jobName && ( - - )} + {params.jobName && } { const dispatch = useDispatch() const abortControllerRef = useRef(new AbortController()) const modelEndpointsRef = useRef(null) + const [, setSearchParams] = useSearchParams() const filters = useFiltersFromSearchParams(filtersConfig) const { handleMonitoring, toggleConvertedYaml } = useModelsPage() @@ -174,7 +171,7 @@ const ModelEndpoints = () => { [dispatch, fetchData] ) - useInitialTableFetch({fetchData: fetchInitialData, filters }) + useInitialTableFetch({ fetchData: fetchInitialData, filters }) useEffect(() => { return () => { @@ -263,6 +260,7 @@ const ModelEndpoints = () => { handleRefresh={handleRefresh} navigateLink={`/projects/${params.projectName}/models/${MODEL_ENDPOINTS_TAB}${window.location.search}`} page={MODELS_PAGE} + setSearchParams={setSearchParams} tab={MODEL_ENDPOINTS_TAB} withoutExpandButton > diff --git a/src/components/ModelsPage/Models/Models.js b/src/components/ModelsPage/Models/Models.js index ec2a93dbf1..db655f6318 100644 --- a/src/components/ModelsPage/Models/Models.js +++ b/src/components/ModelsPage/Models/Models.js @@ -20,7 +20,7 @@ such restriction. import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react' import { connect, useDispatch, useSelector } from 'react-redux' import { chain, isEmpty, isNil } from 'lodash' -import { useLocation, useNavigate, useParams } from 'react-router-dom' +import { useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom' import AddArtifactTagPopUp from '../../../elements/AddArtifactTagPopUp/AddArtifactTagPopUp' import DeployModelPopUp from '../../../elements/DeployModelPopUp/DeployModelPopUp' @@ -94,6 +94,7 @@ const Models = ({ fetchModelFeatureVector }) => { const location = useLocation() const navigate = useNavigate() const params = useParams() + const [, setSearchParams] = useSearchParams() const viewMode = getViewMode(window.location.search) const { toggleConvertedYaml } = useModelsPage() const filters = useFiltersFromSearchParams(filtersConfig) @@ -274,7 +275,7 @@ const Models = ({ fetchModelFeatureVector }) => { ] ) - const handleRemoveRowData = useCallback( + const collapseRowCallback = useCallback( model => { const newStoreSelectedRowData = { ...artifactsStore.models.selectedRowData @@ -290,7 +291,7 @@ const Models = ({ fetchModelFeatureVector }) => { [artifactsStore.models.selectedRowData, dispatch, selectedRowData] ) - const handleExpand = useCallback( + const expandRowCallback = useCallback( (model, content) => { const modelIdentifier = getArtifactIdentifier(model) @@ -310,11 +311,11 @@ const Models = ({ fetchModelFeatureVector }) => { [params.projectName] ) - const { latestItems, handleExpandRow } = useGroupContent( + const { latestItems, toggleRow } = useGroupContent( models, getArtifactIdentifier, - handleRemoveRowData, - handleExpand, + collapseRowCallback, + expandRowCallback, null, MODELS_PAGE, MODELS_TAB @@ -371,12 +372,7 @@ const Models = ({ fetchModelFeatureVector }) => { } useInitialTableFetch({ - createRowData: rowItem => - createModelsRowData( - rowItem, - params.projectName, - frontendSpec - ), + createRowData: rowItem => createModelsRowData(rowItem, params.projectName, frontendSpec), fetchData, fetchTags, filterModalName: MODELS_TAB, @@ -524,7 +520,6 @@ const Models = ({ fetchModelFeatureVector }) => { filters={filters} filtersStore={filtersStore} getAndSetSelectedArtifact={getAndSetSelectedArtifact} - handleExpandRow={handleExpandRow} handleRefresh={handleRefresh} handleRefreshWithFilters={handleRefreshWithFilters} handleRegisterModel={handleRegisterModel} @@ -538,10 +533,12 @@ const Models = ({ fetchModelFeatureVector }) => { selectedModel={selectedModel} selectedRowData={selectedRowData} setMaxArtifactsErrorIsShown={setMaxArtifactsErrorIsShown} + setSearchParams={setSearchParams} setSelectedModelMin={setSelectedModelMin} sortProps={{ sortTable, selectedColumnName, getSortingIcon }} tableContent={sortedTableContent} tableHeaders={sortedTableHeaders} + toggleRow={toggleRow} viewMode={viewMode} virtualizationConfig={virtualizationConfig} /> diff --git a/src/components/ModelsPage/Models/ModelsView.js b/src/components/ModelsPage/Models/ModelsView.js index 0724164f99..29e9c757e9 100644 --- a/src/components/ModelsPage/Models/ModelsView.js +++ b/src/components/ModelsPage/Models/ModelsView.js @@ -50,7 +50,6 @@ const ModelsView = React.forwardRef( filters, filtersStore, getAndSetSelectedArtifact, - handleExpandRow, handleRefresh, handleRefreshWithFilters, handleRegisterModel, @@ -63,10 +62,12 @@ const ModelsView = React.forwardRef( selectedModel, selectedRowData, setMaxArtifactsErrorIsShown, + setSearchParams, setSelectedModelMin, sortProps = null, tableContent, tableHeaders, + toggleRow, viewMode = null, virtualizationConfig }, @@ -99,6 +100,7 @@ const ModelsView = React.forwardRef( filtersConfig={filtersConfig} handleRefresh={handleRefresh} page={MODELS_PAGE} + setSearchParams={setSearchParams} tab={MODELS_TAB} withRefreshButton withoutExpandButton @@ -148,13 +150,13 @@ const ModelsView = React.forwardRef( isRowRendered(virtualizationConfig, index) && ( ) )} @@ -189,7 +191,6 @@ ModelsView.propTypes = { filters: PropTypes.object.isRequired, filtersStore: PropTypes.object.isRequired, getAndSetSelectedArtifact: PropTypes.func.isRequired, - handleExpandRow: PropTypes.func.isRequired, handleRefresh: PropTypes.func.isRequired, handleRefreshWithFilters: PropTypes.func.isRequired, handleRegisterModel: PropTypes.func.isRequired, @@ -202,10 +203,12 @@ ModelsView.propTypes = { selectedModel: PropTypes.object.isRequired, selectedRowData: PropTypes.object.isRequired, setMaxArtifactsErrorIsShown: PropTypes.func.isRequired, + setSearchParams: PropTypes.func.isRequired, setSelectedModelMin: PropTypes.func.isRequired, sortProps: SORT_PROPS, tableContent: PropTypes.arrayOf(PropTypes.object).isRequired, tableHeaders: PropTypes.arrayOf(PropTypes.object).isRequired, + toggleRow: PropTypes.func.isRequired, viewMode: PropTypes.string, virtualizationConfig: VIRTUALIZATION_CONFIG.isRequired } diff --git a/src/components/ModelsPage/RealTimePipelines/RealTimePipelines.js b/src/components/ModelsPage/RealTimePipelines/RealTimePipelines.js index 26763f650a..4ade11efb9 100644 --- a/src/components/ModelsPage/RealTimePipelines/RealTimePipelines.js +++ b/src/components/ModelsPage/RealTimePipelines/RealTimePipelines.js @@ -19,7 +19,7 @@ such restriction. */ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { useNavigate, useParams } from 'react-router-dom' +import { useNavigate, useParams, useSearchParams } from 'react-router-dom' import classnames from 'classnames' import { isNil } from 'lodash' @@ -63,6 +63,7 @@ const RealTimePipelines = () => { const pipelinesRef = useRef(null) const pageData = useMemo(() => generatePageData(params.pipelineId), [params.pipelineId]) const { toggleConvertedYaml } = useModelsPage() + const [, setSearchParams] = useSearchParams() const filters = useFiltersFromSearchParams(filtersConfig) const filterMenuClassNames = classnames( @@ -190,6 +191,7 @@ const RealTimePipelines = () => { handleRefresh={handleRefresh} navigateLink={`/projects/${params.projectName}/models/${REAL_TIME_PIPELINES_TAB}${window.location.search}`} page={MODELS_PAGE} + setSearchParams={setSearchParams} tab={REAL_TIME_PIPELINES_TAB} withoutExpandButton /> diff --git a/src/components/ProjectsJobsMonitoring/JobsMonitoring/JobsMonitoring.js b/src/components/ProjectsJobsMonitoring/JobsMonitoring/JobsMonitoring.js index ada7ac21ad..6a0a562538 100644 --- a/src/components/ProjectsJobsMonitoring/JobsMonitoring/JobsMonitoring.js +++ b/src/components/ProjectsJobsMonitoring/JobsMonitoring/JobsMonitoring.js @@ -17,9 +17,8 @@ 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, { useState, useMemo, useEffect, useRef } from 'react' +import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react' import { useParams } from 'react-router-dom' -import { isEmpty } from 'lodash' import JobsTable from '../../../elements/JobsTable/JobsTable' import TableTop from '../../../elements/TableTop/TableTop' @@ -37,7 +36,6 @@ import { getSavedSearchParams } from '../../../utils/filter.util' const JobsMonitoring = () => { const [selectedJob, setSelectedJob] = useState({}) - const [dataIsLoaded, setDataIsLoaded] = useState(false) const params = useParams() const { isStagingMode } = useMode() const { @@ -53,7 +51,9 @@ const JobsMonitoring = () => { setJobRuns, setJobs, terminateAbortTasksPolling, - tabData + tabData, + paginatedJobs, + searchParams } = React.useContext(ProjectJobsMonitoringContext) const jobsAreInitializedRef = useRef(false) @@ -63,48 +63,36 @@ const JobsMonitoring = () => { ) const tableContent = useMemo( - () => - createJobsMonitoringContent(params.jobName ? jobRuns : jobs, params.jobName, isStagingMode), - [isStagingMode, jobRuns, jobs, params.jobName] + () => createJobsMonitoringContent(paginatedJobs, params.jobName, isStagingMode), + [isStagingMode, paginatedJobs, params.jobName] ) - useEffect(() => { - if (isEmpty(selectedJob) && !params.jobId && !dataIsLoaded) { - // TODO QP: check for double request after deleting job from details, after pagination will be merged + const getBackLink = useCallback( + (useSavedParams = false) => { + let queryParams = useSavedParams + ? getSavedSearchParams(window.location.search) + : `?${searchParams.toString()}` - refreshJobs(filters) - setDataIsLoaded(true) - } - }, [params.jobId, params.jobName, refreshJobs, selectedJob, filters, dataIsLoaded]) + return `/projects/*/${JOBS_MONITORING_PAGE}/${JOBS_MONITORING_JOBS_TAB}${queryParams}` + }, + [searchParams] + ) useEffect(() => { const abortController = abortControllerRef.current return () => { - setDataIsLoaded(false) setJobs([]) setJobRuns([]) abortController.abort(REQUEST_CANCELED) jobsAreInitializedRef.current = false terminateAbortTasksPolling() } - }, [ - params.jobName, - params.jobId, - terminateAbortTasksPolling, - abortControllerRef, - setJobs, - setJobRuns - ]) + }, [terminateAbortTasksPolling, abortControllerRef, setJobs, setJobRuns]) return ( <> - {params.jobName && ( - - )} + {params.jobName && } { filtersConfig={jobsFiltersConfig} jobRuns={jobRuns} jobs={jobs} + paginatedJobs={paginatedJobs} requestErrorMessage={requestErrorMessage} - navigateLink={`/projects/*/${JOBS_MONITORING_PAGE}/${JOBS_MONITORING_JOBS_TAB}${window.location.search}`} - refreshJobs={() => - refreshJobs(filters) - } + navigateLink={getBackLink()} + refreshJobs={() => refreshJobs(filters)} selectedJob={selectedJob} setAbortingJobs={setAbortingJobs} setJobRuns={setJobRuns} diff --git a/src/components/ProjectsJobsMonitoring/ProjectsJobsMonitoring.js b/src/components/ProjectsJobsMonitoring/ProjectsJobsMonitoring.js index d3d915862d..7476e11323 100644 --- a/src/components/ProjectsJobsMonitoring/ProjectsJobsMonitoring.js +++ b/src/components/ProjectsJobsMonitoring/ProjectsJobsMonitoring.js @@ -20,6 +20,7 @@ such restriction. import React, { useLayoutEffect, useMemo, useState } from 'react' import { connect, useSelector } from 'react-redux' import { Outlet, useLocation, useNavigate, useParams } from 'react-router-dom' +import { defaultsDeep } from 'lodash' import ContentMenu from '../../elements/ContentMenu/ContentMenu' import { ConfirmDialog } from 'igz-controls/components' @@ -65,22 +66,52 @@ const ProjectsJobsMonitoring = ({ fetchAllJobRuns, fetchJobFunction, fetchJobs } const navigate = useNavigate() const artifactsStore = useSelector(store => store.artifactsStore) const jobsStore = useSelector(store => store.jobsStore) + + const jobsFiltersConfig = useMemo( + () => getJobsFiltersConfig(params.jobName, true), + [params.jobName] + ) + const scheduledFiltersConfig = useMemo(() => getScheduledFiltersConfig(true), []) + const workflowsFiltersConfig = useMemo(() => getWorkflowsFiltersConfig(true), []) + const initialTabData = useMemo(() => { + return { + [JOBS_MONITORING_JOBS_TAB]: { + filtersConfig: jobsFiltersConfig, + modalFilters: , + parseQueryParamsCallback: parseJobsQueryParamsCallback + }, + [JOBS_MONITORING_WORKFLOWS_TAB]: { + filtersConfig: workflowsFiltersConfig, + modalFilters: , + parseQueryParamsCallback: parseWorkflowsQueryParamsCallback + }, + [JOBS_MONITORING_SCHEDULED_TAB]: { + filtersConfig: scheduledFiltersConfig, + modalFilters: , + parseQueryParamsCallback: parseScheduledQueryParamsCallback + } + } + }, [jobsFiltersConfig, scheduledFiltersConfig, workflowsFiltersConfig]) const { abortControllerRef, abortJobRef, abortingJobs, editableItem, getWorkflows, + handleMonitoring, + handleRefreshJobs, + handleRerunJob, jobRuns, - jobs, jobWizardIsOpened, jobWizardMode, - handleMonitoring, - handleRerunJob, + jobs, + paginatedJobs, + paginationConfigJobsRef, refreshJobs, refreshScheduled, requestErrorMessage, scheduledJobs, + searchParams, setAbortingJobs, setEditableItem, setJobRuns, @@ -88,8 +119,9 @@ const ProjectsJobsMonitoring = ({ fetchAllJobRuns, fetchJobFunction, fetchJobs } setJobWizardMode, setJobs, setScheduledJobs, + setSearchParams, terminateAbortTasksPolling - } = useJobsPageData(fetchAllJobRuns, fetchJobFunction, fetchJobs) + } = useJobsPageData(fetchAllJobRuns, fetchJobFunction, fetchJobs, initialTabData, selectedTab) const handleTabChange = tabName => { setSelectedCard(STATS_TOTAL_CARD) @@ -107,42 +139,22 @@ const ProjectsJobsMonitoring = ({ fetchAllJobRuns, fetchJobFunction, fetchJobs } ) }, [location.pathname]) - const jobsFiltersConfig = useMemo( - () => getJobsFiltersConfig(params.jobName, true), - [params.jobName] - ) - const scheduledFiltersConfig = useMemo(() => getScheduledFiltersConfig(true), []) - const workflowsFiltersConfig = useMemo(() => getWorkflowsFiltersConfig(true), []) - const tabData = useMemo(() => { - return { - [JOBS_MONITORING_JOBS_TAB]: { - filtersConfig: jobsFiltersConfig, - handleRefresh: refreshJobs, - modalFilters: , - parseQueryParamsCallback: parseJobsQueryParamsCallback - }, - [JOBS_MONITORING_WORKFLOWS_TAB]: { - filtersConfig: workflowsFiltersConfig, - handleRefresh: getWorkflows, - modalFilters: , - parseQueryParamsCallback: parseWorkflowsQueryParamsCallback + return defaultsDeep( + { + [JOBS_MONITORING_JOBS_TAB]: { + handleRefresh: handleRefreshJobs + }, + [JOBS_MONITORING_WORKFLOWS_TAB]: { + handleRefresh: getWorkflows + }, + [JOBS_MONITORING_SCHEDULED_TAB]: { + handleRefresh: refreshScheduled + } }, - [JOBS_MONITORING_SCHEDULED_TAB]: { - filtersConfig: scheduledFiltersConfig, - handleRefresh: refreshScheduled, - modalFilters: , - parseQueryParamsCallback: parseScheduledQueryParamsCallback - } - } - }, [ - getWorkflows, - jobsFiltersConfig, - refreshJobs, - refreshScheduled, - scheduledFiltersConfig, - workflowsFiltersConfig - ]) + initialTabData + ) + }, [getWorkflows, handleRefreshJobs, initialTabData, refreshScheduled]) const filters = useFiltersFromSearchParams( tabData[selectedTab]?.filtersConfig, @@ -172,11 +184,12 @@ const ProjectsJobsMonitoring = ({ fetchAllJobRuns, fetchJobFunction, fetchJobs } filtersConfig={tabData[selectedTab].filtersConfig} handleRefresh={tabData[selectedTab].handleRefresh} hidden={Boolean(params.jobId || params.workflowId)} + key={selectedTab} page={JOBS_MONITORING_PAGE} + setSearchParams={setSearchParams} tab={selectedTab} withRefreshButton withoutExpandButton - key={selectedTab} > {tabData[selectedTab].modalFilters} @@ -197,11 +210,14 @@ const ProjectsJobsMonitoring = ({ fetchAllJobRuns, fetchJobFunction, fetchJobs } jobs, jobsFiltersConfig, jobsMonitoringData, - requestErrorMessage, + paginatedJobs, + paginationConfigJobsRef, refreshJobs, refreshScheduled, + requestErrorMessage, scheduledFiltersConfig, scheduledJobs, + searchParams, selectedCard, setAbortingJobs, setConfirmData, diff --git a/src/components/Workflow/Workflow.js b/src/components/Workflow/Workflow.js index a5cd3d0b1c..a7cee12c22 100644 --- a/src/components/Workflow/Workflow.js +++ b/src/components/Workflow/Workflow.js @@ -263,7 +263,7 @@ const Workflow = ({
getCloseDetailsLink(window.location, params.workflowId)} + getCloseDetailsLink={() => getCloseDetailsLink(params.workflowId)} handleCancel={handleCancel} pageData={pageData} retryRequest={refreshJobs} @@ -275,7 +275,7 @@ const Workflow = ({ ) : ( getCloseDetailsLink(window.location, params.workflowId)} + getCloseDetailsLink={() => getCloseDetailsLink(params.workflowId)} handleCancel={handleCancel} hideActionsMenu pageData={pageData} diff --git a/src/constants.js b/src/constants.js index 3435c6290e..d5e276ce68 100644 --- a/src/constants.js +++ b/src/constants.js @@ -68,6 +68,17 @@ export const FAILED_STATE = 'failed' export const VIEW_SEARCH_PARAMETER = 'view' +/*=========== PAGINATION =============*/ + +export const BE_PAGE = 'bePage' +export const FE_PAGE = 'fePage' +export const BE_PAGE_SIZE = 'bePageSize' +export const FE_PAGE_SIZE = 'fePageSize' +export const FE_PAGE_START = 'fePageStart' +export const FE_PAGE_END = 'fePageEnd' +export const ITEMS_COUNT_START = 'itemsCountStart' +export const ITEMS_COUNT_END = 'itemsCountEnd' + /*=========== PAGES & TABS =============*/ export const PROJECTS_PAGE = 'PROJECTS' diff --git a/src/elements/ArtifactsTableRow/ArtifactsTableRow.js b/src/elements/ArtifactsTableRow/ArtifactsTableRow.js index 654f101ff0..990883a05e 100644 --- a/src/elements/ArtifactsTableRow/ArtifactsTableRow.js +++ b/src/elements/ArtifactsTableRow/ArtifactsTableRow.js @@ -40,15 +40,15 @@ import { isRowExpanded, PARENT_ROW_EXPANDED_CLASS } from '../../utils/tableRows. const ArtifactsTableRow = ({ actionsMenu, - handleExpandRow = null, handleSelectItem = () => {}, hideActionsMenu = false, - rowIndex, mainRowItemsCount = 1, + rowIndex, rowItem, selectedItem, selectedRowData, - tab = '' + tab = '', + toggleRow = null }) => { const parent = useRef() const params = useParams() @@ -90,7 +90,6 @@ const ArtifactsTableRow = ({ className={cellClassName} data={data} firstCell={index === 0} - handleExpandRow={handleExpandRow} item={rowItem} key={data.id} link={ @@ -101,6 +100,7 @@ const ArtifactsTableRow = ({ selectItem={handleSelectItem} selectedItem={selectedItem} showExpandButton + toggleRow={toggleRow} /> ) ) @@ -198,15 +198,15 @@ const ArtifactsTableRow = ({ ) ) @@ -229,13 +229,13 @@ const ArtifactsTableRow = ({ ArtifactsTableRow.propTypes = { actionsMenu: ACTIONS_MENU.isRequired, - handleExpandRow: PropTypes.func, handleSelectItem: PropTypes.func, mainRowItemsCount: PropTypes.number, rowIndex: PropTypes.number.isRequired, rowItem: PropTypes.shape({}).isRequired, selectedItem: PropTypes.shape({}).isRequired, - tab: PropTypes.string + tab: PropTypes.string, + toggleRow: PropTypes.func } export default React.memo(ArtifactsTableRow) diff --git a/src/elements/FeatureStoreTableRow/FeatureStoreTableRow.js b/src/elements/FeatureStoreTableRow/FeatureStoreTableRow.js index 1e79452f16..02c59f5369 100644 --- a/src/elements/FeatureStoreTableRow/FeatureStoreTableRow.js +++ b/src/elements/FeatureStoreTableRow/FeatureStoreTableRow.js @@ -37,7 +37,6 @@ import { isRowExpanded, PARENT_ROW_EXPANDED_CLASS } from '../../utils/tableRows. const FeatureStoreTableRow = ({ actionsMenu, - handleExpandRow = () => {}, handleSelectItem = () => {}, hideActionsMenu = false, mainRowItemsCount = 1, @@ -45,7 +44,8 @@ const FeatureStoreTableRow = ({ rowIndex, rowItem, selectedItem = {}, - selectedRowData + selectedRowData, + toggleRow = () => {} }) => { const parent = useRef() const params = useParams() @@ -88,7 +88,6 @@ const FeatureStoreTableRow = ({ className={cellClassName} data={data} firstCell={index === 0} - handleExpandRow={handleExpandRow} item={rowItem} key={data.id} link={ @@ -99,6 +98,7 @@ const FeatureStoreTableRow = ({ selectItem={handleSelectItem} selectedItem={selectedItem} showExpandButton={data.showExpandButton} + toggleRow={toggleRow} /> ) ) @@ -188,14 +188,14 @@ const FeatureStoreTableRow = ({ className={cellClassNames} data={value} firstCell={index === 0} - handleExpandRow={handleExpandRow} item={rowItem.data} key={value.id} link={value.getLink?.(params.tab ?? DETAILS_OVERVIEW_TAB)} onClick={value.handleClick} - selectedItem={selectedItem} selectItem={handleSelectItem} + selectedItem={selectedItem} showExpandButton={value.showExpandButton} + toggleRow={toggleRow} /> ) ) @@ -213,7 +213,6 @@ const FeatureStoreTableRow = ({ FeatureStoreTableRow.propTypes = { actionsMenu: ACTIONS_MENU.isRequired, - handleExpandRow: PropTypes.func, handleSelectItem: PropTypes.func, hideActionsMenu: PropTypes.bool, mainRowItemsCount: PropTypes.number, @@ -221,7 +220,8 @@ FeatureStoreTableRow.propTypes = { rowIndex: PropTypes.number.isRequired, rowItem: PropTypes.shape({}).isRequired, selectedItem: PropTypes.shape({}), - selectedRowData: PropTypes.shape({}).isRequired + selectedRowData: PropTypes.shape({}).isRequired, + toggleRow: PropTypes.func } export default React.memo(FeatureStoreTableRow) diff --git a/src/elements/FunctionsTableRow/FunctionsTableRow.js b/src/elements/FunctionsTableRow/FunctionsTableRow.js index c9b4629184..9a24bf97ab 100644 --- a/src/elements/FunctionsTableRow/FunctionsTableRow.js +++ b/src/elements/FunctionsTableRow/FunctionsTableRow.js @@ -34,20 +34,20 @@ import { isRowExpanded, PARENT_ROW_EXPANDED_CLASS } from '../../utils/tableRows. const FunctionsTableRow = ({ actionsMenu, - handleExpandRow, + expandedRowsData, handleSelectItem, mainRowItemsCount = 1, rowIndex, rowItem, selectedItem, - selectedRowData, + toggleRow, withQuickActions = false }) => { const parent = useRef() const params = useParams() const rowIsExpanded = useMemo( - () => isRowExpanded(parent, selectedRowData, rowItem), - [rowItem, selectedRowData] + () => isRowExpanded(parent, expandedRowsData, rowItem), + [rowItem, expandedRowsData] ) const rowClassNames = classnames( 'table-row', @@ -82,12 +82,12 @@ const FunctionsTableRow = ({ className={cellClassName} data={data} firstCell={index === 0} - handleExpandRow={handleExpandRow} item={rowItem} key={data.id} selectItem={handleSelectItem} selectedItem={selectedItem} showExpandButton + toggleRow={toggleRow} /> ) ) @@ -98,7 +98,7 @@ const FunctionsTableRow = ({
- {selectedRowData[rowItem.data.ui.identifier]?.content.map((func, index) => { + {expandedRowsData[rowItem.data.ui.identifier]?.content.map((func, index) => { const subRowClassNames = classnames( 'table-row', 'table-body-row', @@ -163,13 +163,13 @@ const FunctionsTableRow = ({ ) ) @@ -189,12 +189,12 @@ const FunctionsTableRow = ({ FunctionsTableRow.propTypes = { actionsMenu: ACTIONS_MENU.isRequired, + expandedRowsData: PropTypes.object.isRequired, handleSelectItem: PropTypes.func.isRequired, mainRowItemsCount: PropTypes.number, rowIndex: PropTypes.number.isRequired, rowItem: PropTypes.shape({}).isRequired, selectedItem: PropTypes.shape({}).isRequired, - selectedRowData: PropTypes.object.isRequired, withQuickActions: PropTypes.bool } diff --git a/src/elements/JobsTable/JobsTable.js b/src/elements/JobsTable/JobsTable.js index a0af2fecc7..2edff985d8 100644 --- a/src/elements/JobsTable/JobsTable.js +++ b/src/elements/JobsTable/JobsTable.js @@ -23,8 +23,12 @@ import { isEmpty } from 'lodash' import { useDispatch, useSelector } from 'react-redux' import { useLocation, useNavigate, useParams } from 'react-router-dom' +import Details from '../../components/Details/Details' +import JobWizard from '../../components/JobWizard/JobWizard' import JobsTableRow from '../JobsTableRow/JobsTableRow' import Loader from '../../common/Loader/Loader' +import NoData from '../../common/NoData/NoData' +import Pagination from '../../common/Pagination/Pagination' import Table from '../../components/Table/Table' import { @@ -34,9 +38,6 @@ import { MONITOR_JOBS_TAB, PANEL_RERUN_MODE } from '../../constants' -import Details from '../../components/Details/Details' -import JobWizard from '../../components/JobWizard/JobWizard' -import NoData from '../../common/NoData/NoData' import detailsActions from '../../actions/details' import getState from '../../utils/getState' import jobsActions from '../../actions/jobs' @@ -49,7 +50,6 @@ import { getCloseDetailsLink, isDetailsTabExists } from '../../utils/link-helper import { getJobLogs } from '../../utils/getJobLogs.util' import { getNoDataMessage } from '../../utils/getNoDataMessage' import { isJobKindLocal, pollAbortingJobs } from '../../components/Jobs/jobs.util' -import { isRowRendered, useVirtualization } from '../../hooks/useVirtualization.hook' import { openPopUp } from 'igz-controls/utils/common.util' import { parseJob } from '../../utils/parseJob' import { setFilters } from '../../reducers/filtersReducer' @@ -58,8 +58,6 @@ import { showErrorNotification } from '../../utils/notifications.util' import { usePods } from '../../hooks/usePods.hook' import { toggleYaml } from '../../reducers/appReducer' -import cssVariables from './jobsTable.scss' - const JobsTable = React.forwardRef( ( { @@ -70,6 +68,7 @@ const JobsTable = React.forwardRef( jobRuns, jobs, navigateLink, + paginatedJobs, refreshJobs, requestErrorMessage, selectedJob, @@ -96,6 +95,7 @@ const JobsTable = React.forwardRef( handleRerunJob, jobWizardIsOpened, jobWizardMode, + paginationConfigJobsRef, setConfirmData, setEditableItem, setJobWizardIsOpened, @@ -452,23 +452,10 @@ const JobsTable = React.forwardRef( } }, [params.jobId, selectedJob, setSelectedJob]) - const virtualizationConfig = useVirtualization({ - rowsData: { - content: tableContent - }, - heightData: { - headerRowHeight: cssVariables.monitorJobsHeaderRowHeight, - rowHeight: cssVariables.monitorJobsRowHeight, - rowHeightExtended: cssVariables.monitorJobsRowHeightExtended - } - }) - return ( <> {jobsStore.loading && } - {((params.jobName && jobRuns.length === 0) || (jobs.length === 0 && !params.jobName)) && - !jobsStore.loading && - filters ? ( + {paginatedJobs.length === 0 && !jobsStore.loading && filters ? ( ) : ( - isEmpty(selectedJob) && ( - setSelectedJob({})} - pageData={pageData} - retryRequest={handleRefreshWithFilters} - selectedItem={selectedJob} - tab={MONITOR_JOBS_TAB} - tableClassName="monitor-jobs-table" - tableHeaders={tableContent[0]?.content ?? []} - virtualizationConfig={virtualizationConfig} - > - {tableContent.map( - (tableItem, index) => - isRowRendered(virtualizationConfig, index) && ( - - ) - )} -
+ isEmpty(selectedJob) && + !jobsStore.loading && ( + <> + setSelectedJob({})} + pageData={pageData} + retryRequest={handleRefreshWithFilters} + selectedItem={selectedJob} + tab={MONITOR_JOBS_TAB} + tableClassName="monitor-jobs-table" + tableHeaders={tableContent[0]?.content ?? []} + > + {tableContent.map((tableItem, index) => ( + + ))} +
+ + ) )} {!isEmpty(selectedJob) && (
getCloseDetailsLink(window.location, params.jobName)} + getCloseDetailsLink={() => getCloseDetailsLink(params.jobName)} handleCancel={() => { setSelectedJob({}) dispatch(setFilters({ saveFilters: true })) @@ -536,6 +523,7 @@ JobsTable.propTypes = { jobRuns: PropTypes.array.isRequired, jobs: PropTypes.array.isRequired, navigateLink: PropTypes.string.isRequired, + paginatedJobs: PropTypes.array.isRequired, refreshJobs: PropTypes.func.isRequired, requestErrorMessage: PropTypes.string.isRequired, selectedJob: PropTypes.object.isRequired, diff --git a/src/elements/TableCell/TableCell.js b/src/elements/TableCell/TableCell.js index a8d4d87541..575c4bb14a 100644 --- a/src/elements/TableCell/TableCell.js +++ b/src/elements/TableCell/TableCell.js @@ -42,7 +42,6 @@ const TableCell = ({ className = '', data, firstCell, - handleExpandRow = null, item = { target_path: '', schema: '' @@ -51,7 +50,8 @@ const TableCell = ({ onClick = null, selectItem = () => {}, selectedItem = {}, - showExpandButton = false + showExpandButton = false, + toggleRow = null }) => { const dispatch = useDispatch() const params = useParams() @@ -73,12 +73,12 @@ const TableCell = ({ ) } else if (firstCell && !link) { @@ -102,7 +102,7 @@ const TableCell = ({ )} {showExpandButton && ( - handleExpandRow(e, item)} className="expand-arrow" /> + toggleRow && toggleRow(e, item)} className="expand-arrow" /> )} ) @@ -214,12 +214,12 @@ TableCell.propTypes = { className: PropTypes.string, data: PropTypes.shape({}).isRequired, firstCell: PropTypes.bool, - handleExpandRow: PropTypes.func, item: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.bool]), link: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), selectItem: PropTypes.func, selectedItem: PropTypes.shape({}), - showExpandButton: PropTypes.bool + showExpandButton: PropTypes.bool, + toggleRow: PropTypes.func } export default TableCell diff --git a/src/elements/TableLinkCell/TableLinkCell.js b/src/elements/TableLinkCell/TableLinkCell.js index be5755afc6..77ee3a04ee 100644 --- a/src/elements/TableLinkCell/TableLinkCell.js +++ b/src/elements/TableLinkCell/TableLinkCell.js @@ -33,12 +33,12 @@ import './tableLinkCell.scss' const TableLinkCell = ({ className = '', data = {}, - handleExpandRow, item, link, selectItem, selectedItem = {}, - showExpandButton = false + showExpandButton = false, + toggleRow = null }) => { const tableCellClassNames = classnames( 'table-body__cell', @@ -122,7 +122,7 @@ const TableLinkCell = ({ {showExpandButton && ( { - handleExpandRow(e, item) + toggleRow && toggleRow(e, item) }} className="expand-arrow" /> @@ -138,7 +138,8 @@ TableLinkCell.propTypes = { link: PropTypes.string.isRequired, selectItem: PropTypes.func.isRequired, selectedItem: PropTypes.shape({}), - showExpandButton: PropTypes.bool + showExpandButton: PropTypes.bool, + toggleRow: PropTypes.func } export default TableLinkCell diff --git a/src/hooks/groupContent.hook.js b/src/hooks/groupContent.hook.js index cad2589167..2fc149c991 100644 --- a/src/hooks/groupContent.hook.js +++ b/src/hooks/groupContent.hook.js @@ -17,99 +17,129 @@ 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, useState, useEffect } from 'react' +import { useCallback, useState, useLayoutEffect } from 'react' import { useParams } from 'react-router-dom' import { useSelector } from 'react-redux' import { GROUP_BY_NAME, GROUP_BY_NONE } from '../constants' import { PARENT_ROW_EXPANDED_CLASS } from '../utils/tableRows.util' import { generateGroupLatestItem } from '../utils/generateGroupLatestItem' +import { getPaginatedContent } from './usePagination.hook' export const useGroupContent = ( content, getIdentifier, - handleRemoveRequestData, - handleRequestOnExpand, + collapseRowCallback, + expandRowCallback, selectedItem, page, pageTab, - handleExpandAllCallback + toggleAllRowsCallback, + paginationConfig = null ) => { const [groupedContent, setGroupedContent] = useState({}) const [latestItems, setLatestItems] = useState([]) - const [expandedItems, setExpandedItems] = useState(0) - const [expand, setExpand] = useState(false) + const [expandedRowsCount, setExpandedRowsCount] = useState(0) + const [allRowsAreExpanded, setAllRowsAreExpanded] = useState(false) const params = useParams() const filtersStore = useSelector(store => store.filtersStore) + const toggleAllRowsByContent = useCallback( + (collapseRows, groupedContentLocal) => { + if (filtersStore.groupBy !== GROUP_BY_NONE) { + const rows = [...document.getElementsByClassName('parent-row')] + + if (collapseRows) { + rows.forEach(row => row.classList.remove(PARENT_ROW_EXPANDED_CLASS)) + + setExpandedRowsCount(0) + toggleAllRowsCallback && toggleAllRowsCallback(true) + } else { + rows.forEach(row => row.classList.add(PARENT_ROW_EXPANDED_CLASS)) + + setExpandedRowsCount(Object.keys(groupedContentLocal).length) + toggleAllRowsCallback && toggleAllRowsCallback(false, groupedContentLocal) + } + } + }, + [filtersStore.groupBy, toggleAllRowsCallback] + ) + + const toggleAllRows = useCallback( + collapseRows => { + toggleAllRowsByContent(collapseRows, groupedContent) + }, + [groupedContent, toggleAllRowsByContent] + ) + const handleGroupByName = useCallback(() => { const groupedItems = {} - content.forEach(contentItem => { - const identifier = getIdentifier(contentItem) + if (paginationConfig && paginationConfig.isNewResponse) { + const pageContent = getPaginatedContent(content, paginationConfig) + + pageContent.forEach(contentItem => { + const identifier = getIdentifier(contentItem) + + groupedItems[identifier] ??= [] + groupedItems[identifier].push(contentItem) + }) - groupedItems[identifier] ??= [] - groupedItems[identifier].push(contentItem) - }) + setGroupedContent(groupedItems) + setLatestItems(generateGroupLatestItem(groupedItems, getIdentifier, selectedItem)) + toggleAllRowsByContent(false, groupedItems) + } else if (!paginationConfig) { + content.forEach(contentItem => { + const identifier = getIdentifier(contentItem) - setGroupedContent(groupedItems) - setLatestItems(generateGroupLatestItem(groupedItems, getIdentifier, selectedItem)) - }, [content, getIdentifier, selectedItem]) + groupedItems[identifier] ??= [] + groupedItems[identifier].push(contentItem) + }) + + setGroupedContent(groupedItems) + setLatestItems(generateGroupLatestItem(groupedItems, getIdentifier, selectedItem)) + } + }, [content, getIdentifier, paginationConfig, selectedItem, toggleAllRowsByContent]) const handleGroupByNone = useCallback(() => { const rows = [...document.getElementsByClassName('parent-row')] rows.forEach(row => row.classList.remove(PARENT_ROW_EXPANDED_CLASS)) - setExpand(false) + setAllRowsAreExpanded(false) setGroupedContent({}) }, []) - const handleExpandRow = (e, item) => { + const toggleRow = (e, item) => { const parentRow = e.target.closest('.parent-row') if (parentRow.classList.contains(PARENT_ROW_EXPANDED_CLASS)) { parentRow.classList.remove(PARENT_ROW_EXPANDED_CLASS) - handleRemoveRequestData && handleRemoveRequestData(item) + collapseRowCallback && collapseRowCallback(item) - setExpandedItems(prev => --prev) + setExpandedRowsCount(prev => --prev) } else { parentRow.classList.remove('table-row_active') parentRow.classList.add(PARENT_ROW_EXPANDED_CLASS) - handleRequestOnExpand && handleRequestOnExpand(item, groupedContent) + expandRowCallback && expandRowCallback(item, groupedContent) - setExpandedItems(prev => ++prev) + setExpandedRowsCount(prev => ++prev) } } - const handleExpandAll = useCallback( - collapseRows => { - if (filtersStore.groupBy !== GROUP_BY_NONE) { - const rows = [...document.getElementsByClassName('parent-row')] - - if (collapseRows || expand) { - rows.forEach(row => row.classList.remove(PARENT_ROW_EXPANDED_CLASS)) - - setExpandedItems(0) - handleExpandAllCallback && handleExpandAllCallback(true) - } else { - rows.forEach(row => row.classList.add(PARENT_ROW_EXPANDED_CLASS)) - - setExpandedItems(Object.keys(groupedContent).length) - handleExpandAllCallback && handleExpandAllCallback(false, groupedContent) - } - } - }, - [expand, filtersStore.groupBy, groupedContent, handleExpandAllCallback] - ) - - useEffect(() => { + useLayoutEffect(() => { return () => { - setExpandedItems(0) + setExpandedRowsCount(0) + } + }, [params.jobId, params.pipelineId, content]) + + useLayoutEffect(() => { + if (Object.keys(groupedContent).length > 0) { + setAllRowsAreExpanded(expandedRowsCount === Object.keys(groupedContent).length) } - }, [params.jobId, params.pipelineId, groupedContent]) + }, [expandedRowsCount, groupedContent]) - useEffect(() => { + useLayoutEffect(() => { if (filtersStore.groupBy === GROUP_BY_NAME) { handleGroupByName() } else if (filtersStore.groupBy === GROUP_BY_NONE) { @@ -121,18 +151,11 @@ export const useGroupContent = ( } }, [handleGroupByName, handleGroupByNone, filtersStore.groupBy]) - useEffect(() => { - if (Object.keys(groupedContent).length > 0) { - setExpand(expandedItems === Object.keys(groupedContent).length) - } - }, [expandedItems, groupedContent]) - return { + allRowsAreExpanded, groupedContent, + toggleAllRows, latestItems, - expand, - expandedItems, - handleExpandRow, - handleExpandAll + toggleRow } } diff --git a/src/hooks/useFiltersFromSearchParams.hook.js b/src/hooks/useFiltersFromSearchParams.hook.js index 02e32659b7..723589fbd8 100644 --- a/src/hooks/useFiltersFromSearchParams.hook.js +++ b/src/hooks/useFiltersFromSearchParams.hook.js @@ -77,17 +77,5 @@ export const useFiltersFromSearchParams = ( return getFiltersFromSearchParams(filtersConfig, searchParams, paramsParsingCallback) }, [filtersConfig, paramsParsingCallback, searchParams]) - // TODO QP: test with pagination and if it won't work correctly fall back to useState, but in this case need to fix double requests - - // const [filters, setFilters] = useState( - // getFiltersFromSearchParams(filtersConfig, searchParams, paramsParsingCallback) - // ) - - // useLayoutEffect(() => { - // if (filtersConfig) { - // setFilters(getFiltersFromSearchParams(filtersConfig, searchParams, paramsParsingCallback)) - // } - // }, [dispatch, filtersConfig, paramsParsingCallback, searchParams]) - return filters } diff --git a/src/hooks/useJobsPageData.js b/src/hooks/useJobsPageData.js index de816fad35..9acd566952 100644 --- a/src/hooks/useJobsPageData.js +++ b/src/hooks/useJobsPageData.js @@ -20,22 +20,42 @@ such restriction. import { useCallback, useRef, useState } from 'react' import { useParams } from 'react-router-dom' import { useDispatch, useSelector } from 'react-redux' +import { isEmpty } from 'lodash' import { monitorJob, pollAbortingJobs, rerunJob } from '../components/Jobs/jobs.util' -import { getJobKindFromLabels } from '../utils/jobs.util' -import { parseJob } from '../utils/parseJob' +import { + BE_PAGE, + BE_PAGE_SIZE, + FILTER_ALL_ITEMS, + GROUP_BY_WORKFLOW, + JOB_KIND_LOCAL, + JOBS_MONITORING_JOBS_TAB, + MONITOR_JOBS_TAB, + SCHEDULE_TAB +} from '../constants' import jobsActions from '../actions/jobs' import workflowActions from '../actions/workflow' -import { FILTER_ALL_ITEMS, GROUP_BY_WORKFLOW, JOB_KIND_LOCAL, SCHEDULE_TAB } from '../constants' +import { getJobKindFromLabels } from '../utils/jobs.util' +import { usePagination } from './usePagination.hook' +import { parseJob } from '../utils/parseJob' +import { useFiltersFromSearchParams } from './useFiltersFromSearchParams.hook' -export const useJobsPageData = (fetchAllJobRuns, fetchJobFunction, fetchJobs) => { +export const useJobsPageData = ( + fetchAllJobRuns, + fetchJobFunction, + fetchJobs, + initialTabData, + selectedTab +) => { const [jobRuns, setJobRuns] = useState([]) const [editableItem, setEditableItem] = useState(null) const [jobWizardMode, setJobWizardMode] = useState(null) const [jobWizardIsOpened, setJobWizardIsOpened] = useState(false) const [jobs, setJobs] = useState([]) const [abortingJobs, setAbortingJobs] = useState({}) + const paginationConfigJobsRef = useRef({}) + const paginationConfigRunsRef = useRef({}) const abortControllerRef = useRef(new AbortController()) const abortJobRef = useRef(null) const params = useParams() @@ -44,6 +64,11 @@ export const useJobsPageData = (fetchAllJobRuns, fetchJobFunction, fetchJobs) => const dispatch = useDispatch() const appStore = useSelector(store => store.appStore) + const filters = useFiltersFromSearchParams( + initialTabData[selectedTab]?.filtersConfig, + initialTabData[selectedTab]?.parseQueryParamsCallback + ) + const terminateAbortTasksPolling = useCallback(() => { abortJobRef?.current?.() setAbortingJobs({}) @@ -62,25 +87,33 @@ export const useJobsPageData = (fetchAllJobRuns, fetchJobFunction, fetchJobs) => terminateAbortTasksPolling() const fetchData = params.jobName ? fetchAllJobRuns : fetchJobs - const newParams = !params.jobName && { - 'partition-by': 'project_and_name', - 'partition-sort-by': 'updated' + const projectName = filters.project?.toLowerCase?.() || params.projectName || '*' + const config = { + ui: { + controller: abortControllerRef.current, + setRequestErrorMessage + }, + params: {} } - fetchData( - filters.project?.toLowerCase?.() || params.projectName || '*', - filters, - { - ui: { - controller: abortControllerRef.current, - setRequestErrorMessage - }, - params: { ...newParams } - }, - params.jobName ?? false - ).then(jobs => { - if (jobs) { - const parsedJobs = jobs + if (!params.jobName) { + config.params['partition-by'] = 'project_and_name' + config.params['partition-sort-by'] = 'updated' + } + + if (!params.jobName && !isEmpty(paginationConfigJobsRef.current)) { + config.params.page = paginationConfigJobsRef.current[BE_PAGE] + config.params['page-size'] = paginationConfigJobsRef.current[BE_PAGE_SIZE] + } + + if (params.jobName && !isEmpty(paginationConfigRunsRef.current)) { + config.params.page = paginationConfigRunsRef.current[BE_PAGE] + config.params['page-size'] = paginationConfigRunsRef.current[BE_PAGE_SIZE] + } + + fetchData(projectName, filters, config, params.jobName ?? false).then(response => { + if (response?.runs) { + const parsedJobs = response.runs .map(job => parseJob(job)) .filter(job => { const type = getJobKindFromLabels(job.labels) ?? JOB_KIND_LOCAL @@ -116,8 +149,10 @@ export const useJobsPageData = (fetchAllJobRuns, fetchJobFunction, fetchJobs) => if (params.jobName) { setJobRuns(parsedJobs) + paginationConfigRunsRef.current.paginationResponse = response.pagination } else { setJobs(parsedJobs) + paginationConfigJobsRef.current.paginationResponse = response.pagination } } }) @@ -216,6 +251,25 @@ export const useJobsPageData = (fetchAllJobRuns, fetchJobFunction, fetchJobs) => [fetchJobFunction, dispatch] ) + const [handleRefreshJobs, paginatedJobs, searchJobsParams, setSearchJobsParams] = usePagination({ + hidden: + ![MONITOR_JOBS_TAB, JOBS_MONITORING_JOBS_TAB].includes(selectedTab) || + Boolean(params.jobName), + content: jobs, + refreshContent: refreshJobs, + filters, + paginationConfigRef: paginationConfigJobsRef, + resetPaginationTrigger: `${params.projectName}_${selectedTab}` + }) + const [handleRefreshRuns, paginatedRuns, searchRunsParams, setSearchRunsParams] = usePagination({ + hidden: ![MONITOR_JOBS_TAB, JOBS_MONITORING_JOBS_TAB].includes(selectedTab) || !params.jobName, + content: jobRuns, + refreshContent: refreshJobs, + filters, + paginationConfigRef: paginationConfigRunsRef, + resetPaginationTrigger: `${params.projectName}_${selectedTab}_${params.jobName}` + }) + return { abortControllerRef, abortJobRef, @@ -223,15 +277,19 @@ export const useJobsPageData = (fetchAllJobRuns, fetchJobFunction, fetchJobs) => editableItem, getWorkflows, handleMonitoring, + handleRefreshJobs: params.jobName ? handleRefreshRuns : handleRefreshJobs, handleRerunJob, jobRuns, - jobs, jobWizardIsOpened, jobWizardMode, + jobs, + paginatedJobs: params.jobName ? paginatedRuns : paginatedJobs, + paginationConfigJobsRef: params.jobName ? paginationConfigRunsRef : paginationConfigJobsRef, refreshJobs, refreshScheduled, requestErrorMessage, scheduledJobs, + searchParams: params.jobName ? searchRunsParams : searchJobsParams, setAbortingJobs, setEditableItem, setJobRuns, @@ -239,6 +297,7 @@ export const useJobsPageData = (fetchAllJobRuns, fetchJobFunction, fetchJobs) => setJobWizardMode, setJobs, setScheduledJobs, + setSearchParams: params.jobName ? setSearchRunsParams : setSearchJobsParams, terminateAbortTasksPolling } } diff --git a/src/hooks/usePagination.hook.js b/src/hooks/usePagination.hook.js new file mode 100644 index 0000000000..4771199031 --- /dev/null +++ b/src/hooks/usePagination.hook.js @@ -0,0 +1,217 @@ +/* +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, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import { useSearchParams } from 'react-router-dom' +import { chunk, debounce, isEqual, isNull } from 'lodash' + +import { + BE_PAGE, + BE_PAGE_SIZE, + FE_PAGE, + FE_PAGE_END, + FE_PAGE_SIZE, + FE_PAGE_START, + ITEMS_COUNT_END, + ITEMS_COUNT_START +} from '../constants' + +export const usePagination = ({ + bePageSize = 1000, + content = [], + fePageSize = 50, + filters, + hidden = false, + paginationConfigRef, + refreshContent, + resetPaginationTrigger +}) => { + const [searchParams, setSearchParams] = useSearchParams() + const [paginatedContent, setPaginatedContent] = useState([]) + const resetPaginationTriggerRef = useRef(resetPaginationTrigger) + + const refreshContentDebounced = useMemo(() => { + return debounce(filters => refreshContent(filters)) + }, [refreshContent]) + + const resetPagination = useCallback( + resetSearchParams => { + setSearchParams( + prevSearchParams => { + if (resetSearchParams) { + prevSearchParams.set(BE_PAGE, 1) + prevSearchParams.set(FE_PAGE, 1) + } + + return prevSearchParams + }, + { replace: true } + ) + + paginationConfigRef.current = { + [BE_PAGE_SIZE]: bePageSize, + [FE_PAGE_SIZE]: fePageSize, + [BE_PAGE]: 1, + [FE_PAGE]: 1, + isNewResponse: false, + paginationResponse: null + } + }, + [bePageSize, fePageSize, paginationConfigRef, setSearchParams] + ) + + useLayoutEffect(() => { + if (!hidden) { + const paginationResponse = paginationConfigRef.current.paginationResponse || null + const newPaginationConfig = { + [BE_PAGE_SIZE]: bePageSize, + [FE_PAGE_SIZE]: fePageSize, + [BE_PAGE]: parseInt(searchParams.get(BE_PAGE)) || 1, + [FE_PAGE]: parseInt(searchParams.get(FE_PAGE)) || 1, + paginationResponse + } + + const fePageStart = (bePageSize * (newPaginationConfig[BE_PAGE] - 1)) / fePageSize + 1 + const fePageEnd = fePageStart + Math.ceil(content.length / fePageSize) - 1 + const bePage = newPaginationConfig[BE_PAGE] + const bePageFromPaginationResponse = parseInt(paginationResponse?.page) + newPaginationConfig.isNewResponse = bePage === bePageFromPaginationResponse + + if ( + (bePageFromPaginationResponse && + bePage === bePageFromPaginationResponse && + fePageEnd >= fePageStart) || + (!searchParams.has(FE_PAGE) && !searchParams.has(BE_PAGE)) + ) { + newPaginationConfig[FE_PAGE_START] = fePageStart + newPaginationConfig[FE_PAGE_END] = fePageEnd + + const fePage = newPaginationConfig[FE_PAGE] + newPaginationConfig[FE_PAGE] = + fePage && fePage >= fePageStart && fePage <= fePageEnd ? fePage : fePageStart + + setSearchParams( + prevSearchParams => { + prevSearchParams.set(BE_PAGE, newPaginationConfig[BE_PAGE]) + prevSearchParams.set(FE_PAGE, newPaginationConfig[FE_PAGE]) + return prevSearchParams + }, + { replace: true } + ) + } + + setPaginatedContent(prevPaginatedContent => { + const newPaginatedContent = getPaginatedContent( + content, + newPaginationConfig, + FE_PAGE, + BE_PAGE + ) + + const prevItemsCount = + (newPaginationConfig[FE_PAGE] - 1) * newPaginationConfig[FE_PAGE_SIZE] + const itemsCountStart = newPaginatedContent.length === 0 ? 0 : prevItemsCount + 1 + const itemsCountEnd = prevItemsCount + newPaginatedContent.length + + if (itemsCountStart && itemsCountEnd) { + newPaginationConfig[ITEMS_COUNT_START] = itemsCountStart + newPaginationConfig[ITEMS_COUNT_END] = itemsCountEnd + } + + paginationConfigRef.current = { + ...paginationConfigRef.current, + ...newPaginationConfig + } + + return isEqual(prevPaginatedContent, newPaginatedContent) + ? prevPaginatedContent + : newPaginatedContent + }) + } + }, [bePageSize, fePageSize, paginationConfigRef, content, searchParams, setSearchParams, hidden]) + + useEffect(() => { + if (resetPaginationTrigger !== resetPaginationTriggerRef.current) { + resetPaginationTriggerRef.current = resetPaginationTrigger + + if (paginationConfigRef.current.paginationResponse) { + resetPagination(false) + } + } + }, [hidden, paginationConfigRef, resetPagination, resetPaginationTrigger]) + + useEffect(() => { + const paginationResponse = paginationConfigRef.current?.paginationResponse + + if ( + !hidden && + searchParams.get(BE_PAGE) && + (!paginationResponse || + (paginationResponse?.page && + paginationResponse?.page !== parseInt(searchParams.get(BE_PAGE)))) + ) { + refreshContentDebounced(filters) + } + }, [filters, hidden, paginationConfigRef, refreshContentDebounced, searchParams]) + + useEffect(() => { + if ( + !hidden && + content.length === 0 && + isNull(paginationConfigRef.current.paginationResponse?.page) && + parseInt(searchParams.get(BE_PAGE)) > 1 + ) { + setSearchParams( + prevSearchParams => { + prevSearchParams.set(BE_PAGE, 1) + prevSearchParams.set(FE_PAGE, 1) + return prevSearchParams + }, + { replace: true } + ) + + paginationConfigRef.current.paginationResponse = null + } + }, [paginationConfigRef, content, searchParams, setSearchParams, hidden]) + + const handleRefresh = (filters, filtersChange) => { + if (filtersChange && (searchParams.get(BE_PAGE) !== '1' || searchParams.get(FE_PAGE) !== '1')) { + resetPagination(true) + } else { + refreshContent(filters) + } + } + + return [handleRefresh, paginatedContent, searchParams, setSearchParams, resetPagination] +} + +export const getPaginatedContent = ( + content, + paginationConfig, + fePage = FE_PAGE, + bePage = BE_PAGE +) => { + const contentByPages = chunk(content, paginationConfig[FE_PAGE_SIZE]) + const convertedFePage = + paginationConfig[fePage] - + (paginationConfig[bePage] * paginationConfig[BE_PAGE_SIZE] - paginationConfig[BE_PAGE_SIZE]) / + paginationConfig[FE_PAGE_SIZE] + + return contentByPages[convertedFePage - 1] ?? [] +} diff --git a/src/layout/Content/Content.js b/src/layout/Content/Content.js deleted file mode 100644 index 515e94a3ce..0000000000 --- a/src/layout/Content/Content.js +++ /dev/null @@ -1,221 +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 React, { useCallback, useEffect, useMemo, useState } from 'react' -import { useLocation, useNavigate, useParams } from 'react-router-dom' -import PropTypes from 'prop-types' -import classnames from 'classnames' -import { connect, useDispatch } from 'react-redux' -import { isEmpty } from 'lodash' - -import Breadcrumbs from '../../common/Breadcrumbs/Breadcrumbs' -import FilterMenu from '../../components/FilterMenu/FilterMenu' -import Table from '../../components/Table/Table' -import ContentMenu from '../../elements/ContentMenu/ContentMenu' -import NoData from '../../common/NoData/NoData' -import PageActionsMenu from '../../common/PageActionsMenu/PageActionsMenu' -import PreviewModal from '../../elements/PreviewModal/PreviewModal' -import TableTop from '../../elements/TableTop/TableTop' - -import { generateContentActionsMenu } from './content.util' -import { getNoDataMessage } from '../../utils/getNoDataMessage' -import { isProjectValid } from '../../utils/link-helper.util' -import { - ADD_TO_FEATURE_VECTOR_TAB, - FEATURE_STORE_PAGE, - GROUP_BY_NONE, - MODELS_PAGE -} from '../../constants' -import { useGroupContent } from '../../hooks/groupContent.hook' -import { toggleYaml } from '../../reducers/appReducer' - -import { ReactComponent as Yaml } from 'igz-controls/images/yaml.svg' - -const Content = ({ - applyDetailsChanges, - artifactsStore, - cancelRequest = () => {}, - children, - content, - filtersChangeCallback = null, - filtersStore, - getIdentifier, - handleActionsMenuClick = () => {}, - handleCancel = () => {}, - handleRemoveRequestData, - handleSelectItem = () => {}, - header, - loading, - pageData, - projectStore, - refresh, - selectedItem = {}, - tableTop = null -}) => { - const dispatch = useDispatch() - const [showActionsMenu, setShowActionsMenu] = useState(false) - const navigate = useNavigate() - const params = useParams() - const location = useLocation() - const { groupedContent, expand, handleExpandRow, handleExpandAll } = useGroupContent( - content, - getIdentifier, - handleRemoveRequestData, - pageData.handleRequestOnExpand, - null, - pageData.page, - params.pageTab - ) - - const contentClassName = classnames('content') - const filterMenuClassNames = classnames( - 'content__action-bar-wrapper', - pageData.hideFilterMenu && 'content__action-bar-wrapper_hidden' - ) - - const toggleConvertedYaml = useCallback( - data => { - return dispatch(toggleYaml(data)) - }, - [dispatch] - ) - - const actionsMenu = useMemo(() => { - return generateContentActionsMenu(pageData.actionsMenu, [ - { - label: 'View YAML', - icon: , - onClick: toggleConvertedYaml - } - ]) - }, [pageData.actionsMenu, toggleConvertedYaml]) - - useEffect(() => { - if (!pageData.hidePageActionMenu) { - setShowActionsMenu(true) - } else if (showActionsMenu) { - setShowActionsMenu(false) - } - }, [pageData.hidePageActionMenu, showActionsMenu]) - - useEffect(() => { - isProjectValid(navigate, projectStore.projects, params.projectName) - }, [navigate, params.projectName, projectStore.projects]) - - return ( - <> -
- {header ? header : } - -
-
- {[FEATURE_STORE_PAGE, MODELS_PAGE].includes(pageData.page) && - !location.pathname.includes(ADD_TO_FEATURE_VECTOR_TAB) && ( - - )} - -
- {tableTop && ( - - {tableTop.children} - - )} -
- -
- {children ? ( - children - ) : loading ? null : (filtersStore.groupBy !== GROUP_BY_NONE && - isEmpty(groupedContent)) || - content.length === 0 ? ( - - ) : ( - <> - - - )} - - - {artifactsStore?.preview?.isPreview && ( - - )} - - ) -} - -Content.propTypes = { - cancelRequest: PropTypes.func, - content: PropTypes.arrayOf(PropTypes.shape({})).isRequired, - filtersChangeCallback: PropTypes.func, - getIdentifier: PropTypes.func.isRequired, - handleActionsMenuClick: PropTypes.func, - handleCancel: PropTypes.func, - handleSelectItem: PropTypes.func, - loading: PropTypes.bool.isRequired, - pageData: PropTypes.shape({}).isRequired, - refresh: PropTypes.func.isRequired, - selectedItem: PropTypes.shape({}), - tableTop: PropTypes.shape({ - link: PropTypes.string.isRequired, - text: PropTypes.string.isRequired - }) -} - -export default connect( - ({ artifactsStore, filtersStore, projectStore }) => ({ - artifactsStore, - filtersStore, - projectStore - }), - null -)(Content) diff --git a/src/layout/Content/content.util.js b/src/layout/Content/content.util.js deleted file mode 100644 index 627144d281..0000000000 --- a/src/layout/Content/content.util.js +++ /dev/null @@ -1,42 +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. -*/ - -export const generateGroupedItems = (content, selectedRowData, getIdentifier) => { - const groupedItems = {} - - content.forEach(contentItem => { - const identifier = getIdentifier(contentItem) - - if (selectedRowData?.[identifier]?.content) { - groupedItems[identifier] = selectedRowData[identifier]?.content - } else { - groupedItems[identifier] ??= [] - groupedItems[identifier].push(contentItem) - } - }) - - return groupedItems -} - -export const generateContentActionsMenu = (actionsMenu, predefinedActions) => { - return typeof actionsMenu === 'function' - ? item => [...predefinedActions, ...(actionsMenu(item) ?? [])] - : [...predefinedActions, ...(actionsMenu ?? [])] -} diff --git a/src/scss/main.scss b/src/scss/main.scss index cd67965506..a6d74c0ccb 100644 --- a/src/scss/main.scss +++ b/src/scss/main.scss @@ -45,13 +45,13 @@ body { position: relative; flex-direction: column; min-width: 800px; - padding: 15px 24px 30px; + padding: 15px 24px 10px; background-color: $white; @include jobsFlex; @media screen and (min-width: 1300px) { - padding: 15px 64px 30px; + padding: 15px 64px 15px; } &__header { diff --git a/src/types.js b/src/types.js index 22decb02e3..dff16525ce 100644 --- a/src/types.js +++ b/src/types.js @@ -19,10 +19,14 @@ such restriction. */ import PropTypes from 'prop-types' import { + BE_PAGE, + BE_PAGE_SIZE, DENSITY_CHUNKY, DENSITY_DENSE, DENSITY_MEDIUM, DENSITY_NORMAL, + FE_PAGE, + FE_PAGE_SIZE, PANEL_CREATE_MODE, PANEL_EDIT_MODE, PANEL_FUNCTION_CREATE_MODE, @@ -290,3 +294,16 @@ export const STATUS_LIST = PropTypes.arrayOf( disabled: PropTypes.bool }) ) + +export const PAGINATION_CONFIG = PropTypes.shape({ + [BE_PAGE_SIZE]: PropTypes.number, + [FE_PAGE_SIZE]: PropTypes.number, + [BE_PAGE]: PropTypes.number, + [FE_PAGE]: PropTypes.number, + isNewResponse: PropTypes.bool, + paginationResponse: PropTypes.shape({ + page: PropTypes.number, + 'page-size': PropTypes.number, + 'page-token': PropTypes.string + }) +}) diff --git a/src/utils/createJobsContent.js b/src/utils/createJobsContent.js index 452e1d3b9a..c16d0be07b 100644 --- a/src/utils/createJobsContent.js +++ b/src/utils/createJobsContent.js @@ -21,13 +21,16 @@ import JobPopUp from '../elements/DetailsPopUp/JobPopUp/JobPopUp' import FunctionPopUp from '../elements/DetailsPopUp/FunctionPopUp/FunctionPopUp' import { + BE_PAGE, ERROR_STATE, + FE_PAGE, JOB_KIND_WORKFLOW, JOBS_MONITORING_JOBS_TAB, JOBS_MONITORING_PAGE, JOBS_PAGE, MONITOR_JOBS_TAB, MONITOR_WORKFLOWS_TAB, + NAME_FILTER, PROJECT_FILTER } from '../constants' import { openPopUp } from 'igz-controls/utils/common.util' @@ -64,7 +67,11 @@ export const createJobsMonitorTabContent = (jobs, jobName, isStagingMode) => { ) : '' } else { - return `/projects/${job.project}/${JOBS_PAGE.toLowerCase()}/${MONITOR_JOBS_TAB}/${job.name}${saveAndTransformSearchParams(window.location.search)}` + return `/projects/${job.project}/${JOBS_PAGE.toLowerCase()}/${MONITOR_JOBS_TAB}/${job.name}${saveAndTransformSearchParams( + window.location.search, + true, + [BE_PAGE, FE_PAGE, NAME_FILTER] + )}` } } @@ -443,7 +450,13 @@ export const createJobsMonitoringContent = (jobs, jobName, isStagingMode) => { }/${job.project}/${job.uid}/${tab.toLowerCase()}${window.location.search}` : '' } else { - return `/projects/*/${JOBS_MONITORING_PAGE}/${JOBS_MONITORING_JOBS_TAB}/${job.name}${saveAndTransformSearchParams(window.location.search)}${window.location.search ? '&' : '?'}${`${PROJECT_FILTER}=${job.project}`}` + const savedAndTransformedSearchParams = saveAndTransformSearchParams( + window.location.search, + true, + [BE_PAGE, FE_PAGE, NAME_FILTER] + ) + + return `/projects/*/${JOBS_MONITORING_PAGE}/${JOBS_MONITORING_JOBS_TAB}/${job.name}${savedAndTransformedSearchParams}${savedAndTransformedSearchParams ? '&' : '?'}${`${PROJECT_FILTER}=${job.project}`}` } } @@ -544,7 +557,7 @@ export const createJobsMonitoringContent = (jobs, jobName, isStagingMode) => { }) } -export const createScheduleJobsMonitoringContent = (jobs) => { +export const createScheduleJobsMonitoringContent = jobs => { return jobs.map(job => { const identifierUnique = getJobIdentifier(job, true) const [, projectName, lastRunUid, lastRunIter] = diff --git a/src/utils/filter.util.js b/src/utils/filter.util.js index 34e034dbfb..5ad09559dc 100644 --- a/src/utils/filter.util.js +++ b/src/utils/filter.util.js @@ -20,12 +20,23 @@ such restriction. const SAVED_PARAMS = 'savedParams' -export const getSavedSearchParams = (params) => { - return atob(new URLSearchParams(params)?.get(SAVED_PARAMS) ?? '') ?? params +export const getSavedSearchParams = searchParams => { + return atob(new URLSearchParams(searchParams)?.get(SAVED_PARAMS) ?? '') ?? searchParams } -export const saveAndTransformSearchParams = (params) => { - return params ? `${params}&${SAVED_PARAMS}=${btoa(params)}` : '' +export const saveAndTransformSearchParams = ( + searchParams, + includeSearchParams = false, + excludeParamsNames = [] +) => { + let newSearchParams = '?' + + if (includeSearchParams) { + const filteredNewSearchParams = getFilteredSearchParams(searchParams, excludeParamsNames) + newSearchParams = filteredNewSearchParams ? `${filteredNewSearchParams}&` : newSearchParams + } + + return searchParams ? `${newSearchParams}${SAVED_PARAMS}=${btoa(searchParams)}` : '' } export const getFilteredSearchParams = (searchParams, excludeParamsNames = []) => { diff --git a/src/utils/jobs.util.js b/src/utils/jobs.util.js index 1a6e84ed28..298c9d571b 100644 --- a/src/utils/jobs.util.js +++ b/src/utils/jobs.util.js @@ -42,7 +42,11 @@ import { PAST_24_HOUR_DATE_OPTION, PAST_WEEK_DATE_OPTION } from './datePicker.util' -import { generateTypeFilter, jobsStatuses, workflowsStatuses } from '../components/FilterMenu/filterMenu.settings' +import { + generateTypeFilter, + jobsStatuses, + workflowsStatuses +} from '../components/FilterMenu/filterMenu.settings' export const handleAbortJob = ( abortJob, @@ -208,7 +212,10 @@ export const getJobsFiltersConfig = (jobName, crossProjects) => { [NAME_FILTER]: { label: 'Name:', hidden: Boolean(jobName), initialValue: '' }, [DATES_FILTER]: { label: 'Start time:', - initialValue: getDatePickerFilterValue(datePickerPastOptions, crossProjects ? PAST_24_HOUR_DATE_OPTION : PAST_WEEK_DATE_OPTION) + initialValue: getDatePickerFilterValue( + datePickerPastOptions, + crossProjects ? PAST_24_HOUR_DATE_OPTION : PAST_WEEK_DATE_OPTION + ) }, [PROJECT_FILTER]: { label: 'Project:', initialValue: '', isModal: true }, [STATUS_FILTER]: { label: 'Status:', initialValue: [FILTER_ALL_ITEMS], isModal: true }, @@ -217,12 +224,15 @@ export const getJobsFiltersConfig = (jobName, crossProjects) => { } } -export const getWorkflowsFiltersConfig = (crossProjects) => { +export const getWorkflowsFiltersConfig = crossProjects => { return { [NAME_FILTER]: { label: 'Name:', initialValue: '' }, [DATES_FILTER]: { label: 'Created at:', - initialValue: getDatePickerFilterValue(datePickerPastOptions, crossProjects ? PAST_24_HOUR_DATE_OPTION : PAST_WEEK_DATE_OPTION) + initialValue: getDatePickerFilterValue( + datePickerPastOptions, + crossProjects ? PAST_24_HOUR_DATE_OPTION : PAST_WEEK_DATE_OPTION + ) }, [PROJECT_FILTER]: { label: 'Project:', initialValue: '', isModal: true }, [STATUS_FILTER]: { label: 'Status:', initialValue: [FILTER_ALL_ITEMS], isModal: true }, @@ -230,7 +240,7 @@ export const getWorkflowsFiltersConfig = (crossProjects) => { } } -export const getScheduledFiltersConfig = (crossProjects) => { +export const getScheduledFiltersConfig = crossProjects => { return { [NAME_FILTER]: { label: 'Name:', initialValue: '' }, [DATES_FILTER]: { @@ -264,11 +274,11 @@ export const parseJobsQueryParamsCallback = (paramName, paramValue) => { return paramValue } - export const parseWorkflowsQueryParamsCallback = (paramName, paramValue) => { - if (paramName === STATUS_FILTER) { - const filteredStatuses = paramValue?.split(',').filter(paramStatus => workflowsStatuses.find(status => status.id === paramStatus)) + const filteredStatuses = paramValue + ?.split(',') + .filter(paramStatus => workflowsStatuses.find(status => status.id === paramStatus)) return filteredStatuses?.length ? filteredStatuses : null } @@ -278,8 +288,9 @@ export const parseWorkflowsQueryParamsCallback = (paramName, paramValue) => { export const parseScheduledQueryParamsCallback = (paramName, paramValue) => { if (paramName === TYPE_FILTER) { - return generateTypeFilter(JOBS_MONITORING_SCHEDULED_TAB).find(type => type.id === paramValue)?.id + return generateTypeFilter(JOBS_MONITORING_SCHEDULED_TAB).find(type => type.id === paramValue) + ?.id } return paramValue -} \ No newline at end of file +} diff --git a/src/utils/link-helper.util.js b/src/utils/link-helper.util.js index c879b93e6c..42b9d02797 100644 --- a/src/utils/link-helper.util.js +++ b/src/utils/link-helper.util.js @@ -31,8 +31,7 @@ export const isProjectValid = (navigate, projects, currentProjectName) => { if ( projects.length > 0 && currentProjectName && - !projects - .some(project => project?.metadata?.name === currentProjectName) + !projects.some(project => project?.metadata?.name === currentProjectName) ) { navigate('/projects', { replace: true }) } @@ -42,11 +41,20 @@ export const generateUrlFromRouterPath = link => { return new URL(link, window.location.origin).toString() } -export const getCloseDetailsLink = (location, paramName) => { - return location.pathname - .split('/') - .splice(0, location.pathname.split('/').lastIndexOf(paramName) + 1) - .join('/') + window.location.search +export const getCloseDetailsLink = paramName => { + const link = + window.location.pathname + .split('/') + .splice(0, window.location.pathname.split('/').lastIndexOf(paramName) + 1) + .join('/') + window.location.search + + return generateUrlFromRouterPath(link) +} + +export const getDefaultCloseDetailsLink = (params, page, tab) => { + return `/projects/${params.projectName}/${page.toLowerCase()}${ + params.pageTab ? `/${params.pageTab}` : tab ? `/${tab}` : '' + }${window.location.search}` } export const generateLinkToDetailsPanel = ( diff --git a/src/utils/parseFunctions.js b/src/utils/parseFunctions.js index 8a1765d13d..e27b79754f 100644 --- a/src/utils/parseFunctions.js +++ b/src/utils/parseFunctions.js @@ -22,7 +22,6 @@ import { parseFunction } from './parseFunction' export const parseFunctions = (functions, projectName) => { return chain(functions) - .orderBy('metadata.updated', 'desc') .map(func => parseFunction(func, projectName)) .value() } diff --git a/tests/mockServer/data/run.json b/tests/mockServer/data/run.json index 264ef40abf..3d0e08485c 100644 --- a/tests/mockServer/data/run.json +++ b/tests/mockServer/data/run.json @@ -34935,56 +34935,6 @@ "error": "cannot import from ''" } }, - { - "metadata": { - "project": "default", - "uid": "bafbef8fefd740748d25ef3bb7e42c54" - }, - "status": { - "state": "pending", - "last_update": "2021-08-29T15:21:14.702592+00:00" - } - }, - { - "metadata": { - "project": "default", - "uid": "604f65049df74fbf93f105ebe0f274a2" - }, - "status": { - "state": "pending", - "last_update": "2021-08-29T15:21:14.664433+00:00" - } - }, - { - "metadata": { - "project": "default", - "uid": "a34480f582d74b75bdc42a40a521cc12" - }, - "status": { - "state": "pending", - "last_update": "2021-08-29T15:21:14.641175+00:00" - } - }, - { - "metadata": { - "project": "default", - "uid": "f1613eca3c6a43f2aad5d5a4151e6994" - }, - "status": { - "state": "pending", - "last_update": "2021-08-29T15:21:14.584666+00:00" - } - }, - { - "metadata": { - "project": "default", - "uid": "6a82e1043c56496f9666e6593d7132c7" - }, - "status": { - "state": "pending", - "last_update": "2021-08-29T15:21:14.559559+00:00" - } - }, { "kind": "run", "metadata": { @@ -64609,6 +64559,6 @@ "abort_task_id": "74be3e06-56fb-4bc9-9ece-b77f7f818373", "status_text": "aborted" } - } + } ] } diff --git a/tests/mockServer/data/runs.json b/tests/mockServer/data/runs.json index 455f259722..40b58dd860 100644 --- a/tests/mockServer/data/runs.json +++ b/tests/mockServer/data/runs.json @@ -34944,56 +34944,6 @@ "error": "cannot import from ''" } }, - { - "metadata": { - "project": "default", - "uid": "bafbef8fefd740748d25ef3bb7e42c54" - }, - "status": { - "state": "pending", - "last_update": "2021-08-29T15:21:14.702592+00:00" - } - }, - { - "metadata": { - "project": "default", - "uid": "604f65049df74fbf93f105ebe0f274a2" - }, - "status": { - "state": "pending", - "last_update": "2021-08-29T15:21:14.664433+00:00" - } - }, - { - "metadata": { - "project": "default", - "uid": "a34480f582d74b75bdc42a40a521cc12" - }, - "status": { - "state": "pending", - "last_update": "2021-08-29T15:21:14.641175+00:00" - } - }, - { - "metadata": { - "project": "default", - "uid": "f1613eca3c6a43f2aad5d5a4151e6994" - }, - "status": { - "state": "pending", - "last_update": "2021-08-29T15:21:14.584666+00:00" - } - }, - { - "metadata": { - "project": "default", - "uid": "6a82e1043c56496f9666e6593d7132c7" - }, - "status": { - "state": "pending", - "last_update": "2021-08-29T15:21:14.559559+00:00" - } - }, { "kind": "run", "metadata": { @@ -64496,6 +64446,6 @@ "abort_task_id": "74be3e06-56fb-4bc9-9ece-b77f7f818373", "status_text": "aborted" } - } + } ] } diff --git a/tests/mockServer/mock.js b/tests/mockServer/mock.js index 3f2b44bb04..11b4429dad 100644 --- a/tests/mockServer/mock.js +++ b/tests/mockServer/mock.js @@ -23,22 +23,24 @@ import yaml from 'js-yaml' import fs from 'fs' import crypto from 'node:crypto' import { + chunk, + clamp, cloneDeep, - remove, defaults, - noop, + find, + forEach, get, - random, isArray, + isEmpty, isFunction, - clamp, - find, - set, + isNil, + noop, omit, - forEach, + orderBy, pick, - isNil, - isEmpty + random, + remove, + set } from 'lodash' import mime from 'mime-types' @@ -81,7 +83,7 @@ import { updateSchedules } from './dateSynchronization.js' -//updating values in files with synthetic data +// Updating values in files with synthetic data updateRuns(runs) updatePipelines(pipelines) updatePipelineIDs(pipelineIDs) @@ -181,7 +183,7 @@ const secretKeyTemplate = { secret_keys: [] } -// Mock consts +// Mock constants const mockHome = process.cwd() + '/tests/mockServer' const mlrunIngress = '/mlrun-api-ingress.default-tenant.app.vmdev36.lab.iguazeng.com' const mlrunAPIIngress = `${mlrunIngress}/api/v1` @@ -191,7 +193,7 @@ const iguazioApiUrl = '/platform-api.default-tenant.app.vmdev36.lab.iguazeng.com const port = 30000 const NOT_ALLOWED_SECRET_KEY = 'mlrun.' -// Support function +// Support functions function createTask(projectName, config) { const newTask = cloneDeep(backgroundTaskTemplate) const now = new Date().toISOString() @@ -281,6 +283,30 @@ function getGraphById(targetId) { return foundGraph } +function getPaginationConfig(data, query) { + let paginationQueryConfig = { + page: null, + 'page-size': null, + 'page-token': null + } + let pageData = data + + if (query['page-size'] && query.page) { + const dataPaginated = chunk(data, query['page-size']) + pageData = dataPaginated[query.page - 1] ?? [] + const pageDataIsEmpty = isEmpty(pageData) + const nextPageDataIsEmpty = isEmpty(dataPaginated[query.page]) + + paginationQueryConfig = { + page: pageDataIsEmpty ? null : parseInt(query.page), + 'page-size': pageDataIsEmpty ? null : parseInt(query['page-size']), + 'page-token': nextPageDataIsEmpty || pageDataIsEmpty ? null : '12345' + } + } + + return [pageData, paginationQueryConfig] +} + function makeUID(length) { let result = '' const characters = 'abcdef0123456789' @@ -835,7 +861,9 @@ function getRuns(req, res) { }) } - res.send({ runs: collectedRuns }) + const [paginatedRuns, pagination] = getPaginationConfig(collectedRuns, req.query) + + res.send({ runs: paginatedRuns, pagination }) } function getRun(req, res) { @@ -1280,7 +1308,9 @@ function getArtifacts(req, res) { }) } - res.send({ artifacts: collectedArtifacts }) + const [paginatedArtifacts, pagination] = getPaginationConfig(collectedArtifacts, req.query) + + res.send({ artifacts: paginatedArtifacts, pagination }) } function getProjectsFeatureSets(req, res) { @@ -1574,17 +1604,24 @@ function getFuncs(req, res) { }) } - res.send({ funcs: collectedFuncs }) + collectedFuncs = orderBy(collectedFuncs, 'metadata.updated', 'desc') + const [paginatedFuncs, pagination] = getPaginationConfig(collectedFuncs, req.query) + + res.send({ funcs: paginatedFuncs, pagination }) } function getFunc(req, res) { - const collectedFunc = funcs.funcs + let collectedFuncs = funcs.funcs .filter(func => func.metadata.project === req.params['project']) .filter(func => func.metadata.name === req.params['func']) .filter(func => func.metadata.hash === req.query.hash_key) + if (req.query.tag) { + collectedFuncs = collectedFuncs.filter(func => func.metadata.tag === req.query.tag) + } + let respBody = {} - if (collectedFunc.length === 0) { + if (collectedFuncs.length === 0) { res.statusCode = 404 respBody = { detail: { @@ -1592,7 +1629,7 @@ function getFunc(req, res) { } } } else { - respBody = { func: collectedFunc[0] } + respBody = { func: collectedFuncs[0] } } res.send(respBody) From bccf72dc6ac5aa99ebc1323dace1da3b25978395 Mon Sep 17 00:00:00 2001 From: Ilank <63646693+ilan7empest@users.noreply.github.com> Date: Tue, 3 Dec 2024 16:46:19 +0200 Subject: [PATCH 06/12] Fix [Jobs] When running job of type local, we don't collect logs so "Logs" tab should not be displayed (#2903) --- .../Jobs/MonitorJobs/monitorJobs.util.js | 19 ++++++------------ .../MonitorWorkflows/monitorWorkflows.util.js | 20 ++++--------------- src/components/Jobs/jobs.util.js | 11 ++++++---- src/elements/JobsTable/jobsTable.util.js | 2 +- src/elements/WorkflowsTable/WorkflowsTable.js | 2 +- 5 files changed, 19 insertions(+), 35 deletions(-) diff --git a/src/components/Jobs/MonitorJobs/monitorJobs.util.js b/src/components/Jobs/MonitorJobs/monitorJobs.util.js index 0bedc1acc4..e3750538fe 100644 --- a/src/components/Jobs/MonitorJobs/monitorJobs.util.js +++ b/src/components/Jobs/MonitorJobs/monitorJobs.util.js @@ -20,10 +20,7 @@ such restriction. import React from 'react' import { isNil, isEmpty, debounce } from 'lodash' -import { - FUNCTION_RUN_KINDS, - JOBS_PAGE -} from '../../../constants' +import { FUNCTION_RUN_KINDS, JOBS_PAGE } from '../../../constants' import { JOB_STEADY_STATES, getInfoHeaders, @@ -51,7 +48,7 @@ export const generatePageData = ( return { page: JOBS_PAGE, details: { - menu: getJobsDetailsMenu(selectedJob?.labels), + menu: getJobsDetailsMenu(selectedJob), type: JOBS_PAGE, infoHeaders: getInfoHeaders(!isNil(selectedJob.ui_run)), refreshLogs: handleFetchJobLogs, @@ -90,7 +87,9 @@ export const generateActionsMenu = ( { label: 'Batch re-run', icon: , - hidden: !FUNCTION_RUN_KINDS.includes(job?.ui?.originalContent.metadata.labels?.kind) || isDetailsPopUp, + hidden: + !FUNCTION_RUN_KINDS.includes(job?.ui?.originalContent.metadata.labels?.kind) || + isDetailsPopUp, onClick: handleRerunJob }, { @@ -145,13 +144,7 @@ export const generateActionsMenu = ( } export const fetchInitialJobs = debounce( - ( - filters, - selectedJob, - jobId, - refreshJobs, - jobsAreInitializedRef - ) => { + (filters, selectedJob, jobId, refreshJobs, jobsAreInitializedRef) => { if (isEmpty(selectedJob) && !jobId && !jobsAreInitializedRef.current) { refreshJobs(filters) jobsAreInitializedRef.current = true diff --git a/src/components/Jobs/MonitorWorkflows/monitorWorkflows.util.js b/src/components/Jobs/MonitorWorkflows/monitorWorkflows.util.js index 9e8286802e..e2fa00c235 100644 --- a/src/components/Jobs/MonitorWorkflows/monitorWorkflows.util.js +++ b/src/components/Jobs/MonitorWorkflows/monitorWorkflows.util.js @@ -20,12 +20,7 @@ such restriction. import React from 'react' import { debounce } from 'lodash' -import { - FUNCTIONS_PAGE, - GROUP_BY_NONE, - GROUP_BY_WORKFLOW, - JOBS_PAGE -} from '../../../constants' +import { FUNCTIONS_PAGE, GROUP_BY_NONE, GROUP_BY_WORKFLOW, JOBS_PAGE } from '../../../constants' import { getJobsDetailsMenu, getInfoHeaders, @@ -56,7 +51,7 @@ export const generatePageData = ( handleFetchFunctionLogs, handleFetchJobLogs, handleRemoveFunctionLogs, - selectedJobLabels + selectedJob ) => { return { page: JOBS_PAGE, @@ -64,7 +59,7 @@ export const generatePageData = ( type: !isEveryObjectValueEmpty(selectedFunction) ? FUNCTIONS_PAGE : JOBS_PAGE, menu: !isEveryObjectValueEmpty(selectedFunction) ? functionsDetailsMenu - : getJobsDetailsMenu(selectedJobLabels), + : getJobsDetailsMenu(selectedJob), infoHeaders: !isEveryObjectValueEmpty(selectedFunction) ? functionsInfoHeaders : getInfoHeaders(false), @@ -160,14 +155,7 @@ export const generateActionsMenu = ( } export const fetchInitialWorkflows = debounce( - ( - filters, - params, - getWorkflows, - setFilters, - dispatch, - workflowsAreInitializedRef - ) => { + (filters, params, getWorkflows, setFilters, dispatch, workflowsAreInitializedRef) => { if (!workflowsAreInitializedRef.current) { if (params.workflowId) { dispatch(setFilters({ groupBy: GROUP_BY_NONE })) diff --git a/src/components/Jobs/jobs.util.js b/src/components/Jobs/jobs.util.js index e4f5fd1dd7..4cea72a635 100644 --- a/src/components/Jobs/jobs.util.js +++ b/src/components/Jobs/jobs.util.js @@ -83,7 +83,7 @@ export const actionButtonHeader = 'Batch Run' export const JOB_STEADY_STATES = ['completed', ERROR_STATE, 'aborted', FAILED_STATE] export const JOB_RUNNING_STATES = ['running', 'pending'] -export const getJobsDetailsMenu = (jobLabels = []) => { +export const getJobsDetailsMenu = (job = {}) => { return [ { label: 'overview', @@ -103,12 +103,13 @@ export const getJobsDetailsMenu = (jobLabels = []) => { }, { label: 'logs', - id: 'logs' + id: 'logs', + hidden: isJobKindLocal(job) }, { label: 'pods', id: 'pods', - hidden: arePodsHidden(jobLabels) + hidden: arePodsHidden(job?.labels) } ] } @@ -129,7 +130,9 @@ export const isJobAborting = (currentJob = {}) => { } export const isJobKindDask = (jobLabels = []) => { - return (isObject(jobLabels) ? parseKeyValues(jobLabels) : jobLabels)?.includes(`kind: ${JOB_KIND_DASK}`) + return (isObject(jobLabels) ? parseKeyValues(jobLabels) : jobLabels)?.includes( + `kind: ${JOB_KIND_DASK}` + ) } export const isJobKindLocal = job => diff --git a/src/elements/JobsTable/jobsTable.util.js b/src/elements/JobsTable/jobsTable.util.js index d80fe9c7c3..7874531ab8 100644 --- a/src/elements/JobsTable/jobsTable.util.js +++ b/src/elements/JobsTable/jobsTable.util.js @@ -32,7 +32,7 @@ export const generatePageData = ( return { page: JOBS_PAGE, details: { - menu: getJobsDetailsMenu(selectedJob?.labels), + menu: getJobsDetailsMenu(selectedJob), type: JOBS_PAGE, infoHeaders: getInfoHeaders(!isNil(selectedJob.ui_run), selectedJob), refreshLogs: handleFetchJobLogs, diff --git a/src/elements/WorkflowsTable/WorkflowsTable.js b/src/elements/WorkflowsTable/WorkflowsTable.js index cdffe90d8e..c42156ebaf 100644 --- a/src/elements/WorkflowsTable/WorkflowsTable.js +++ b/src/elements/WorkflowsTable/WorkflowsTable.js @@ -170,7 +170,7 @@ const WorkflowsTable = React.forwardRef( handleFetchFunctionLogs, handleFetchJobLogs, handleRemoveFunctionLogs, - selectedJob?.labels + selectedJob ), [ handleFetchJobLogs, From 40ee476d2176b3a58bfb5210b78ee220e1588574 Mon Sep 17 00:00:00 2001 From: Ilank <63646693+ilan7empest@users.noreply.github.com> Date: Tue, 3 Dec 2024 16:46:33 +0200 Subject: [PATCH 07/12] Fix [Jobs monitoring] "Past 24 hours" time filter is applied after click on "In process" jobs (#2902) --- .../JobsCounters.js | 8 +++-- .../ScheduledJobsCounters.js | 28 +++++++++++---- .../WorkflowsCounters.js | 4 +-- src/utils/generateMonitoringData.js | 34 +++++-------------- 4 files changed, 39 insertions(+), 35 deletions(-) diff --git a/src/elements/ProjectsMonitoringCounters/JobsCounters.js b/src/elements/ProjectsMonitoringCounters/JobsCounters.js index 1f3a9ee3bf..4267a2be59 100644 --- a/src/elements/ProjectsMonitoringCounters/JobsCounters.js +++ b/src/elements/ProjectsMonitoringCounters/JobsCounters.js @@ -61,7 +61,11 @@ const JobsCounters = () => { {projectStore.projectsSummary.loading ? ( ) : ( - + {jobStats.all.counter} )} @@ -73,7 +77,7 @@ const JobsCounters = () => { ) : ( }> - + {counter} diff --git a/src/elements/ProjectsMonitoringCounters/ScheduledJobsCounters.js b/src/elements/ProjectsMonitoringCounters/ScheduledJobsCounters.js index cf851152ab..2f62fe53cf 100644 --- a/src/elements/ProjectsMonitoringCounters/ScheduledJobsCounters.js +++ b/src/elements/ProjectsMonitoringCounters/ScheduledJobsCounters.js @@ -74,10 +74,10 @@ const ScheduledJobsCounters = () => { ) : ( - {scheduledStats.jobs.counter}{' '} + {scheduledStats.jobs.counter} )} @@ -91,16 +91,32 @@ const ScheduledJobsCounters = () => { + {scheduledStats.workflows.counter} + + )} + + + +
Total
+ + {projectStore.projectsSummary.loading ? ( + + ) : ( + - {scheduledStats.workflows.counter}{' '} + {scheduledStats.all.counter} )}
- + ) diff --git a/src/elements/ProjectsMonitoringCounters/WorkflowsCounters.js b/src/elements/ProjectsMonitoringCounters/WorkflowsCounters.js index 8f57e2ab70..b203a31f20 100644 --- a/src/elements/ProjectsMonitoringCounters/WorkflowsCounters.js +++ b/src/elements/ProjectsMonitoringCounters/WorkflowsCounters.js @@ -74,7 +74,7 @@ const WorkflowsCounters = () => { {workflowsStats.all.counter} @@ -87,7 +87,7 @@ const WorkflowsCounters = () => { ) : ( }> - + {counter} diff --git a/src/utils/generateMonitoringData.js b/src/utils/generateMonitoringData.js index e7f3901978..55a219f6b0 100644 --- a/src/utils/generateMonitoringData.js +++ b/src/utils/generateMonitoringData.js @@ -30,11 +30,7 @@ import { ERROR_STATE, FAILED_STATE } from '../constants' -import { - ANY_TIME_DATE_OPTION, - datePickerPastOptions, - getDatePickerFilterValue -} from './datePicker.util' +import { ANY_TIME_DATE_OPTION } from './datePicker.util' export const generateMonitoringStats = (data, navigate, tab) => { const navigateToJobsMonitoringPage = (filters = {}) => { @@ -51,17 +47,10 @@ export const generateMonitoringStats = (data, navigate, tab) => { { counter: data.running, link: () => - navigateToJobsMonitoringPage( - { - [STATUS_FILTER]: ['running', 'pending', 'aborting'] - }, - { - [DATES_FILTER]: getDatePickerFilterValue( - datePickerPastOptions, - ANY_TIME_DATE_OPTION - ) - } - ), + navigateToJobsMonitoringPage({ + [STATUS_FILTER]: ['running', 'pending', 'aborting'], + [DATES_FILTER]: ANY_TIME_DATE_OPTION + }), statusClass: 'running', tooltip: 'Aborting, Pending, Running' }, @@ -89,15 +78,10 @@ export const generateMonitoringStats = (data, navigate, tab) => { { counter: data.running, link: () => - navigateToJobsMonitoringPage( - { [STATUS_FILTER]: ['running'] }, - { - [DATES_FILTER]: getDatePickerFilterValue( - datePickerPastOptions, - ANY_TIME_DATE_OPTION - ) - } - ), + navigateToJobsMonitoringPage({ + [STATUS_FILTER]: ['running'], + [DATES_FILTER]: ANY_TIME_DATE_OPTION + }), statusClass: 'running', tooltip: 'Running' }, From 43f207bb2ee3b40e7a3c89c4e0fdc3c7a561003d Mon Sep 17 00:00:00 2001 From: Ilank <63646693+ilan7empest@users.noreply.github.com> Date: Tue, 3 Dec 2024 16:46:41 +0200 Subject: [PATCH 08/12] Fix [UI] project request is sent 3 times (#2901) --- src/common/Breadcrumbs/Breadcrumbs.js | 23 ++++++++++++++++++- .../BreadcrumbsStep/BreadcrumbsStep.js | 18 ++------------- .../BreadcrumbsDropdown.js | 9 ++++---- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/common/Breadcrumbs/Breadcrumbs.js b/src/common/Breadcrumbs/Breadcrumbs.js index 1b3c118161..86933c50d5 100755 --- a/src/common/Breadcrumbs/Breadcrumbs.js +++ b/src/common/Breadcrumbs/Breadcrumbs.js @@ -17,15 +17,18 @@ 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, { useMemo, useRef, useState } from 'react' +import React, { useEffect, useMemo, useRef, useState } from 'react' import PropTypes from 'prop-types' import { useLocation, useParams } from 'react-router-dom' +import { useDispatch, useSelector } from 'react-redux' import BreadcrumbsStep from './BreadcrumbsStep/BreadcrumbsStep' import { useMode } from '../../hooks/mode.hook' import { generateMlrunScreens, generateTabsList } from './breadcrumbs.util' import { PROJECTS_PAGE_PATH } from '../../constants' +import projectsAction from '../../actions/projects' +import { generateProjectsList } from '../../utils/projects' import './breadcrumbs.scss' @@ -35,9 +38,16 @@ const Breadcrumbs = ({ onClick = () => {} }) => { const [showProjectsList, setShowProjectsList] = useState(false) const { isDemoMode } = useMode() const breadcrumbsRef = useRef() + const dispatch = useDispatch() const params = useParams() const location = useLocation() + const projectStore = useSelector(state => state.projectStore) + + const projectsList = useMemo(() => { + return generateProjectsList(projectStore.projectsNames.data) + }, [projectStore.projectsNames.data]) + const mlrunScreens = useMemo(() => { return generateMlrunScreens(params, isDemoMode) }, [isDemoMode, params]) @@ -72,6 +82,16 @@ const Breadcrumbs = ({ onClick = () => {} }) => { } }, [location.pathname, params.projectName, mlrunScreens, projectTabs]) + useEffect(() => { + if ( + projectsList.length === 0 && + location.pathname !== '/projects' && + !location.pathname.startsWith('/projects/*') + ) { + dispatch(projectsAction.fetchProjects({ format: 'minimal' })) + } + }, [dispatch, location.pathname, projectsList.length]) + return (
+ {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) From 4217d986ac1d6fa549935755d8d271148f0a21ae Mon Sep 17 00:00:00 2001 From: Ilank <63646693+ilan7empest@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:14:53 +0200 Subject: [PATCH 12/12] Fix [Workflows] single-line display issue for pipeline and job errors (#2900) --- .../Details/DetailsHeader/DetailsHeader.js | 100 +++++++++--------- src/components/Details/details.scss | 19 +++- 2 files changed, 68 insertions(+), 51 deletions(-) diff --git a/src/components/Details/DetailsHeader/DetailsHeader.js b/src/components/Details/DetailsHeader/DetailsHeader.js index 998779a172..76564503cf 100644 --- a/src/components/Details/DetailsHeader/DetailsHeader.js +++ b/src/components/Details/DetailsHeader/DetailsHeader.js @@ -169,55 +169,59 @@ const DetailsHeader = ({
{/*In the Workflow page we display both Jobs and Functions items. The function contains `updated` property. The job contains startTime property.*/} - - {Object.keys(selectedItem).length > 0 && - pageData.page === JOBS_PAGE && - !selectedItem?.updated - ? formatDatetime( - selectedItem?.startTime, - stateValue === 'aborted' ? 'N/A' : 'Not yet started' - ) - : selectedItem?.updated - ? formatDatetime(selectedItem?.updated, 'N/A') - : selectedItem?.spec?.model.includes(':') // 'model-key:model-tag' - ? selectedItem.spec.model.replace(/^.*:/, '') // remove key - : selectedItem?.spec?.model - ? selectedItem?.metadata?.uid - : ''} - - {stateValue && stateLabel && ( - }> - - - )} - {selectedItem.ui?.customError?.title && selectedItem.ui?.customError?.message && ( - - } - > - {selectedItem.ui.customError.title} {selectedItem.ui.customError.message} - - )} - {errorMessage && ( - } - > - {errorMessage} - - )} - {!isEmpty(detailsStore.pods.podsPending) && ( - - {`${detailsStore.pods.podsPending.length} of ${detailsStore.pods.podsList.length} pods are pending`} +
+ + {Object.keys(selectedItem).length > 0 && + pageData.page === JOBS_PAGE && + !selectedItem?.updated + ? formatDatetime( + selectedItem?.startTime, + stateValue === 'aborted' ? 'N/A' : 'Not yet started' + ) + : selectedItem?.updated + ? formatDatetime(selectedItem?.updated, 'N/A') + : selectedItem?.spec?.model.includes(':') // 'model-key:model-tag' + ? selectedItem.spec.model.replace(/^.*:/, '') // remove key + : selectedItem?.spec?.model + ? selectedItem?.metadata?.uid + : ''} - )} - {detailsStore.pods.error && ( - Failed to load pods - )} + {stateValue && stateLabel && ( + }> + + + )} +
+
+ {selectedItem.ui?.customError?.title && selectedItem.ui?.customError?.message && ( + + } + > + {selectedItem.ui.customError.title} {selectedItem.ui.customError.message} + + )} + {errorMessage && ( + } + > + {errorMessage} + + )} + {!isEmpty(detailsStore.pods.podsPending) && ( + + {`${detailsStore.pods.podsPending.length} of ${detailsStore.pods.podsList.length} pods are pending`} + + )} + {detailsStore.pods.error && ( + Failed to load pods + )} +
diff --git a/src/components/Details/details.scss b/src/components/Details/details.scss index 25d5c84511..6934209623 100644 --- a/src/components/Details/details.scss +++ b/src/components/Details/details.scss @@ -55,10 +55,15 @@ &__status { color: $topaz; - display: flex; - flex-flow: row wrap; - align-items: center; line-height: 25px; + + &-row { + display: flex; + flex-flow: row wrap; + align-items: center; + flex: 1 0 100%; + margin-bottom: 5px; + } } &__back-btn { @@ -91,6 +96,7 @@ } .state { + display: inline-block; min-width: 18px; i { @@ -99,7 +105,14 @@ } .error-container { + flex: 1; margin-left: 0; + + &:not(:last-child) { + border-right: $secondaryBorder; + margin-right: 1em; + padding-right: 1em; + } } }