diff --git a/src/common/Input/Input.js b/src/common/Input/Input.js index fcd7b9caf..2357a0a4c 100644 --- a/src/common/Input/Input.js +++ b/src/common/Input/Input.js @@ -231,6 +231,7 @@ const Input = React.forwardRef( {label && (
diff --git a/src/components/ProjectSettings/ProjectSettings.js b/src/components/ProjectSettings/ProjectSettings.js index b9d5d883d..45b08bca9 100644 --- a/src/components/ProjectSettings/ProjectSettings.js +++ b/src/components/ProjectSettings/ProjectSettings.js @@ -17,8 +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, { useCallback, useEffect, useMemo, useReducer, useState } from 'react' -import { connect, useDispatch } from 'react-redux' +import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' import { useLocation, useNavigate, useParams } from 'react-router-dom' import ProjectSettingsGeneral from '../../elements/ProjectSettingsGeneral/ProjectSettingsGeneral' @@ -27,6 +27,9 @@ import ProjectSettingsSecrets from '../../elements/ProjectSettingsSecrets/Projec import Breadcrumbs from '../../common/Breadcrumbs/Breadcrumbs' import ContentMenu from '../../elements/ContentMenu/ContentMenu' +import { Button, ConfirmDialog } from 'igz-controls/components' +import { DANGER_BUTTON, TERTIARY_BUTTON } from 'igz-controls/constants' + import { COMPLETED_STATE, generateMembers, @@ -35,6 +38,8 @@ import { tabs, validTabs } from './projectSettings.util' +import { onDeleteProject } from '../ProjectsPage/projects.util' +import projectsAction from '../../actions/projects' import { initialMembersState, membersActions, @@ -47,14 +52,19 @@ import { showErrorNotification } from '../../utils/notifications.util' import './projectSettings.scss' -const ProjectSettings = ({ frontendSpec }) => { +const ProjectSettings = () => { const [projectMembersIsShown, setProjectMembersIsShown] = useState(false) const [projectOwnerIsShown, setProjectOwnerIsShown] = useState(false) + const [confirmData, setConfirmData] = useState(null) const [membersState, membersDispatch] = useReducer(membersReducer, initialMembersState) const location = useLocation() const navigate = useNavigate() const params = useParams() const dispatch = useDispatch() + const deletingProjectsRef = useRef({}) + const terminatePollRef = useRef(null) + const projectStore = useSelector(state => state.projectStore) + const frontendSpec = useSelector(store => store.appStore.frontendSpec) const projectMembershipIsEnabled = useMemo( () => frontendSpec?.feature_flags?.project_membership === 'enabled', @@ -122,7 +132,7 @@ const ProjectSettings = ({ frontendSpec }) => { [] ) || []) ]) - + membersDispatch({ type: membersActions.SET_ACTIVE_USER, payload: activeUser @@ -208,6 +218,10 @@ const ProjectSettings = ({ frontendSpec }) => { }) }, []) + const fetchMinimalProjects = useCallback(() => { + dispatch(projectsAction.fetchProjects({ format: 'minimal' })) + }, [dispatch]) + useEffect(() => { membersDispatch({ type: membersActions.GET_PROJECT_USERS_DATA_BEGIN @@ -226,43 +240,83 @@ const ProjectSettings = ({ frontendSpec }) => { }, [navigate, params.pageTab, params.projectName]) return ( -
-
- -
-
- + {confirmData && ( + - {params.pageTab === PROJECTS_SETTINGS_MEMBERS_TAB && projectMembersTabIsShown ? ( - - ) : params.pageTab === PROJECTS_SETTINGS_SECRETS_TAB ? ( - - ) : ( - - )} + )} + +
+
+ +
+
+
+ +
+ {projectMembershipIsEnabled && ( +
+
+ {params.pageTab === PROJECTS_SETTINGS_MEMBERS_TAB && projectMembersTabIsShown ? ( + + ) : params.pageTab === PROJECTS_SETTINGS_SECRETS_TAB ? ( + + ) : ( + + )} +
-
+ ) } -export default connect( - ({ appStore }) => ({ - frontendSpec: appStore.frontendSpec - }), - null -)(ProjectSettings) +export default ProjectSettings diff --git a/src/components/ProjectsPage/Projects.js b/src/components/ProjectsPage/Projects.js index a22cfcb2b..a4778cde9 100644 --- a/src/components/ProjectsPage/Projects.js +++ b/src/components/ProjectsPage/Projects.js @@ -28,7 +28,6 @@ import ProjectsView from './ProjectsView' import { generateMonitoringCounters, generateProjectActionsMenu, - handleDeleteProjectError, pollDeletingProjects, projectDeletionKind, projectDeletionWrapperKind, @@ -36,9 +35,10 @@ import { } from './projects.util' import nuclioActions from '../../actions/nuclio' import projectsAction from '../../actions/projects' -import { BG_TASK_RUNNING, isBackgroundTaskRunning } from '../../utils/poll.util' +import { BG_TASK_RUNNING } from '../../utils/poll.util' +import { onDeleteProject } from './projects.util' import { PROJECT_ONLINE_STATUS } from '../../constants' -import { DANGER_BUTTON, FORBIDDEN_ERROR_STATUS_CODE, PRIMARY_BUTTON } from 'igz-controls/constants' +import { FORBIDDEN_ERROR_STATUS_CODE, PRIMARY_BUTTON } from 'igz-controls/constants' import { fetchBackgroundTasks } from '../../reducers/tasksReducer' import { setNotification } from '../../reducers/notificationReducer' import { showErrorNotification } from '../../utils/notifications.util' @@ -135,7 +135,9 @@ const Projects = () => { backgroundTask => backgroundTask.metadata.kind.startsWith( wrapperIsUsed ? projectDeletionWrapperKind : projectDeletionKind - ) && backgroundTask?.status?.state === BG_TASK_RUNNING && deletingProjectsRef.current[backgroundTask.metadata.name] + ) && + backgroundTask?.status?.state === BG_TASK_RUNNING && + deletingProjectsRef.current[backgroundTask.metadata.name] ) .reduce((acc, backgroundTask) => { acc[backgroundTask.metadata.name] = last(backgroundTask.metadata.kind.split('.')) @@ -188,53 +190,6 @@ const Projects = () => { [dispatch, fetchMinimalProjects] ) - const handleDeleteProject = useCallback( - (project, deleteNonEmpty) => { - setConfirmData(null) - - dispatch(projectsAction.deleteProject(project.metadata.name, deleteNonEmpty)) - .then(response => { - if (isBackgroundTaskRunning(response)) { - dispatch( - setNotification({ - status: 200, - id: Math.random(), - message: 'Project deletion in progress' - }) - ) - - const newDeletingProjects = { - ...deletingProjectsRef.current, - [response.data.metadata.name]: last(response.data.metadata.kind.split('.')) - } - - dispatch(projectsAction.setDeletingProjects(newDeletingProjects)) - pollDeletingProjects(terminatePollRef, newDeletingProjects, refreshProjects, dispatch) - } else { - fetchMinimalProjects() - dispatch( - setNotification({ - status: 200, - id: Math.random(), - message: `Project "${project}" was deleted successfully` - }) - ) - } - }) - .catch(error => { - handleDeleteProjectError( - error, - handleDeleteProject, - project, - setConfirmData, - dispatch, - deleteNonEmpty - ) - }) - }, - [dispatch, fetchMinimalProjects, refreshProjects] - ) - const handleUnarchiveProject = useCallback( project => { dispatch( @@ -276,23 +231,6 @@ const Projects = () => { [handleArchiveProject] ) - const onDeleteProject = useCallback( - project => { - setConfirmData({ - item: project, - header: 'Delete project?', - message: `You are trying to delete the project "${project.metadata.name}". Deleted projects cannot be restored`, - btnConfirmLabel: 'Delete', - btnConfirmType: DANGER_BUTTON, - rejectHandler: () => { - setConfirmData(null) - }, - confirmHandler: handleDeleteProject - }) - }, - [handleDeleteProject] - ) - const exportYaml = useCallback( projectMinimal => { if (projectMinimal?.metadata?.name) { @@ -303,7 +241,7 @@ const Projects = () => { FileSaver.saveAs(blob, `${projectMinimal.metadata.name}.yaml`) }) .catch(error => { - showErrorNotification(dispatch, error, '', 'Failed to fetch project\'s YAML', () => + showErrorNotification(dispatch, error, '', "Failed to fetch project's YAML", () => exportYaml(projectMinimal) ) }) @@ -322,7 +260,7 @@ const Projects = () => { .catch(error => { setConvertedYaml('') - showErrorNotification(dispatch, error, '', 'Failed to fetch project\'s YAML', () => + showErrorNotification(dispatch, error, '', "Failed to fetch project's YAML", () => viewYaml(projectMinimal) ) }) @@ -338,6 +276,21 @@ const Projects = () => { [dispatch] ) + const handleOnDeleteProject = useCallback( + project => + onDeleteProject( + project, + setConfirmData, + dispatch, + deletingProjectsRef, + terminatePollRef, + fetchMinimalProjects, + null, + refreshProjects + ), + [dispatch, fetchMinimalProjects, refreshProjects] + ) + useEffect(() => { setActionsMenu( generateProjectActionsMenu( @@ -347,17 +300,17 @@ const Projects = () => { viewYaml, onArchiveProject, handleUnarchiveProject, - onDeleteProject + handleOnDeleteProject ) ) }, [ convertToYaml, + handleOnDeleteProject, projectStore.deletingProjects, exportYaml, handleUnarchiveProject, isDemoMode, onArchiveProject, - onDeleteProject, projectStore.projects, viewYaml ]) diff --git a/src/components/ProjectsPage/projects.util.js b/src/components/ProjectsPage/projects.util.js index e86228610..6bbec1a1a 100644 --- a/src/components/ProjectsPage/projects.util.js +++ b/src/components/ProjectsPage/projects.util.js @@ -18,7 +18,7 @@ under the Apache 2.0 license is conditioned upon your compliance with such restriction. */ import React from 'react' -import { get, omit } from 'lodash' +import { get, omit, last } from 'lodash' import tasksApi from '../../api/tasks-api' import { @@ -26,7 +26,12 @@ import { SERVICE_UNAVAILABLE_ERROR_STATUS_CODE, GATEWAY_TIMEOUT_STATUS_CODE } from 'igz-controls/constants' -import { BG_TASK_FAILED, BG_TASK_SUCCEEDED, pollTask } from '../../utils/poll.util' +import { + BG_TASK_FAILED, + BG_TASK_SUCCEEDED, + isBackgroundTaskRunning, + pollTask +} from '../../utils/poll.util' import { PROJECT_ONLINE_STATUS } from '../../constants' import { DANGER_BUTTON, FORBIDDEN_ERROR_STATUS_CODE } from 'igz-controls/constants' import { setNotification } from '../../reducers/notificationReducer' @@ -138,7 +143,11 @@ export const handleDeleteProjectError = ( project, setConfirmData, dispatch, - deleteNonEmpty + deleteNonEmpty, + deletingProjectsRef, + terminatePollRef, + fetchMinimalProjects, + navigate ) => { if (error.response?.status === 412 && !deleteNonEmpty) { setConfirmData({ @@ -153,7 +162,16 @@ export const handleDeleteProjectError = ( setConfirmData(null) }, confirmHandler: project => { - handleDeleteProject(project, true) + handleDeleteProject( + project, + true, + setConfirmData, + dispatch, + deletingProjectsRef, + terminatePollRef, + fetchMinimalProjects, + navigate + ) } }) } else { @@ -162,7 +180,18 @@ export const handleDeleteProjectError = ( ? `You do not have permission to delete the project ${project.metadata.name} ` : `Failed to delete the project ${project.metadata.name}` - showErrorNotification(dispatch, error, '', customErrorMsg, () => handleDeleteProject(project)) + showErrorNotification(dispatch, error, '', customErrorMsg, () => + handleDeleteProject( + project, + false, + setConfirmData, + dispatch, + deletingProjectsRef, + terminatePollRef, + fetchMinimalProjects, + navigate + ) + ) } } @@ -244,21 +273,117 @@ export const generateMonitoringCounters = (data, dispatch) => { monitoringCounters.jobs.all += project.runs_completed_recent_count || 0 monitoringCounters.jobs.all += project.runs_failed_recent_count || 0 monitoringCounters.jobs.all += project.runs_running_count || 0 + monitoringCounters.jobs.all += + project.runs_completed_recent_count || + 0 + project.runs_failed_recent_count || + 0 + project.runs_running_count || + 0 monitoringCounters.jobs.completed += project.runs_completed_recent_count || 0 monitoringCounters.jobs.failed += project.runs_failed_recent_count || 0 monitoringCounters.jobs.running += project.runs_running_count || 0 monitoringCounters.workflows.all += project.pipelines_completed_recent_count || 0 monitoringCounters.workflows.all += project.pipelines_failed_recent_count || 0 monitoringCounters.workflows.all += project.pipelines_running_count || 0 + monitoringCounters.workflows.all += + project.pipelines_completed_recent_count || + 0 + project.pipelines_failed_recent_count || + 0 + project.pipelines_running_count || + 0 monitoringCounters.workflows.completed += project.pipelines_completed_recent_count || 0 monitoringCounters.workflows.failed += project.pipelines_failed_recent_count || 0 monitoringCounters.workflows.running += project.pipelines_running_count || 0 - monitoringCounters.scheduled.all += project.distinct_scheduled_jobs_pending_count || 0 - monitoringCounters.scheduled.all += project.distinct_scheduled_pipelines_pending_count || 0 + monitoringCounters.scheduled.all += + project.distinct_scheduled_jobs_pending_count || + 0 + project.distinct_scheduled_pipelines_pending_count || + 0 monitoringCounters.scheduled.jobs += project.distinct_scheduled_jobs_pending_count || 0 monitoringCounters.scheduled.workflows += project.distinct_scheduled_pipelines_pending_count || 0 + monitoringCounters.scheduled.workflows += + project.distinct_scheduled_pipelines_pending_count || 0 }) dispatch(projectsAction.setJobsMonitoringData(monitoringCounters)) } + +export const onDeleteProject = (project, setConfirmData, ...args) => { + setConfirmData({ + item: project, + header: 'Delete project?', + message: `You are trying to delete the project "${project.metadata.name}". Deleted projects cannot be restored`, + btnConfirmLabel: 'Delete', + btnConfirmType: DANGER_BUTTON, + rejectHandler: () => { + setConfirmData(null) + }, + confirmHandler: deleteNonEmpty => + handleDeleteProject(project, deleteNonEmpty, setConfirmData, ...args) + }) +} + +export const handleDeleteProject = ( + project, + deleteNonEmpty, + setConfirmData, + dispatch, + deletingProjectsRef, + terminatePollRef, + fetchMinimalProjects, + navigate, + refreshProjects +) => { + setConfirmData && setConfirmData(null) + + dispatch(projectsAction.deleteProject(project.metadata.name, deleteNonEmpty)) + .then(response => { + if (isBackgroundTaskRunning(response)) { + dispatch( + setNotification({ + status: 200, + id: Math.random(), + message: 'Project deletion in progress' + }) + ) + + const newDeletingProjects = { + ...deletingProjectsRef.current, + [response.data.metadata.name]: last(response.data.metadata.kind.split('.')) + } + + dispatch(projectsAction.setDeletingProjects(newDeletingProjects)) + if (refreshProjects) { + pollDeletingProjects(terminatePollRef, newDeletingProjects, refreshProjects, dispatch) + } + + if (navigate) { + navigate('/projects') + } + } else { + fetchMinimalProjects() + dispatch( + setNotification({ + status: 200, + id: Math.random(), + message: `Project "${project.metadata.name}" was deleted successfully` + }) + ) + if (navigate) { + navigate('/projects') + } + } + }) + .catch(error => { + handleDeleteProjectError( + error, + handleDeleteProject, + project, + setConfirmData, + dispatch, + deleteNonEmpty, + deletingProjectsRef, + terminatePollRef, + fetchMinimalProjects, + navigate + ) + }) +} diff --git a/src/elements/ChangeOwnerPopUp/ChangeOwnerPopUp.js b/src/elements/ChangeOwnerPopUp/ChangeOwnerPopUp.js index 7ddeac6fd..7cb8c3930 100644 --- a/src/elements/ChangeOwnerPopUp/ChangeOwnerPopUp.js +++ b/src/elements/ChangeOwnerPopUp/ChangeOwnerPopUp.js @@ -176,6 +176,7 @@ const ChangeOwnerPopUp = ({ changeOwnerCallback, projectId }) => { state.projectStore) + return ( -
{ - if ( - event.target.tagName !== 'A' && - !ref.current.contains(event.target) && - !chipRef.current?.contains(event.target) && - !event.target.closest('#overlay_container') - ) { - navigate(`/projects/${project.metadata.name}/monitor`) - } - }} - ref={cardRef} - > -
-
-
- } - > - {project.metadata.name} - +
+ {Object.values(deletingProjects).includes(project.metadata.name) && } +
{ + if ( + event.target.tagName !== 'A' && + !ref.current.contains(event.target) && + !chipRef.current?.contains(event.target) && + !event.target.closest('#overlay_container') + ) { + navigate(`/projects/${project.metadata.name}/monitor`) + } + }} + ref={cardRef} + > +
+
+
+ } + > + {project.metadata.name} + -
- - Created {getTimeElapsedByDate(project.metadata.created)} +
+ + Created {getTimeElapsedByDate(project.metadata.created)} +
-
-
- Owner: - {project.spec.owner} +
+ Owner: + {project.spec.owner} +
-
-
-
- {project?.spec.description && ( - }> - {project.spec.description} - - )} -
+
+
+ {project?.spec.description && ( + }> + {project.spec.description} + + )} +
-
- +
+ +
-
- {project.metadata.labels && ( -
- Labels: - -
- )} -
+ {project.metadata.labels && ( +
+ Labels: + +
+ )} +
-
- +
+ +
) diff --git a/src/elements/ProjectCard/projectCard.scss b/src/elements/ProjectCard/projectCard.scss index 2424eee2e..29573335d 100644 --- a/src/elements/ProjectCard/projectCard.scss +++ b/src/elements/ProjectCard/projectCard.scss @@ -93,6 +93,10 @@ box-shadow: $previewBoxShadow; } + .loader-wrapper { + position: absolute; + } + .project-data-card__statistics { &-item { flex: 0 0 auto; diff --git a/src/elements/ProjectSettingsGeneral/ProjectSettingsGeneral.js b/src/elements/ProjectSettingsGeneral/ProjectSettingsGeneral.js index 7dfb1aa86..c882e6e3e 100644 --- a/src/elements/ProjectSettingsGeneral/ProjectSettingsGeneral.js +++ b/src/elements/ProjectSettingsGeneral/ProjectSettingsGeneral.js @@ -45,6 +45,7 @@ import { DEFAULT_IMAGE, DESCRIPTION, GOALS, + KEY_CODES, LABELS, LOAD_SOURCE_ON_RUN, NODE_SELECTORS, @@ -58,7 +59,6 @@ import { setFieldState } from 'igz-controls/utils/form.util' import { FORBIDDEN_ERROR_STATUS_CODE } from 'igz-controls/constants' -import { KEY_CODES } from '../../constants' import { getChipOptions } from '../../utils/getChipOptions' import { getErrorMsg } from 'igz-controls/utils/common.util' import { @@ -84,7 +84,8 @@ const ProjectSettingsGeneral = ({ }) => { const [projectIsInitialized, setProjectIsInitialized] = useState(false) const [lastEditedProjectValues, setLastEditedProjectValues] = useState({}) - const formRef = React.useRef( + + const formRef = useRef( createForm({ initialValues: {}, mutators: { ...arrayMutators, setFieldState }, @@ -228,164 +229,174 @@ const ProjectSettingsGeneral = ({ }, []) return ( -
{}}> - {formState => { - formStateRef.current = formState + <> + {(projectStore.loading || projectStore.project.loading) && } - return ( -
- {projectStore.project.loading ? ( - - ) : projectStore.project.error ? ( -
-

{projectStore.project.error.message}

-
- ) : ( -
-
-
- - - -
-
- - - Enter the default path for saving the artifacts within your - projectStore.project. - - Read more - - -
-
- -
-
- -
-
- -
-
- + {}}> + {formState => { + formStateRef.current = formState + + return ( +
+ {projectStore.project.loading ? ( + + ) : projectStore.project.error ? ( +
+

{projectStore.project.error.message}

+
+ ) : ( + <> +
+ Project: {params.projectName || ''}
- {areNodeSelectorsSupported && ( -
-
- Node Selectors - +
+
+ + +
- -
- )} -
-
-
-
-
- Owner: - - {membersState.projectInfo?.owner?.username || - projectStore.project.data?.spec?.owner} +
+ + + Enter the default path for saving the artifacts within your + projectStore.project. + + Read more +
+
+ +
+
+ +
+
+ +
+
+ +
+ {areNodeSelectorsSupported && ( +
+
+ Node Selectors + +
+ +
+ )} +
+
+
+
+
+ Owner: + + {membersState.projectInfo?.owner?.username || + projectStore.project.data?.spec?.owner} + +
+
+ {projectMembershipIsEnabled && projectOwnerIsShown && ( + + )} +
+
+

Parameters

+

+ The parameters enable users to pass key/value to the project context that + can later be used for running jobs & pipelines +

+ +
- {projectMembershipIsEnabled && projectOwnerIsShown && ( - - )} -
-
-

Parameters

-

- The parameters enable users to pass key/value to the project context that can - later be used for running jobs & pipelines -

-
-
-
- )} -
- ) - }} - + + )} +
+ ) + }} + + ) } diff --git a/src/elements/ProjectSettingsGeneral/projectSettingsGeneral.scss b/src/elements/ProjectSettingsGeneral/projectSettingsGeneral.scss index fa75bd39d..f8568e950 100644 --- a/src/elements/ProjectSettingsGeneral/projectSettingsGeneral.scss +++ b/src/elements/ProjectSettingsGeneral/projectSettingsGeneral.scss @@ -69,4 +69,14 @@ .text-area { width: 100%; } + + .delete-project { + &-danger { + svg { + path:first-child { + fill: $amaranth; + } + } + } + } }