diff --git a/package.json b/package.json index 21369d0a5..3b69495a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mlrun.ui", - "version": "1.4.0", + "version": "1.6.0", "private": true, "homepage": "/mlrun", "dependencies": { @@ -19,11 +19,12 @@ "dagre": "^0.8.5", "dotenv": "^10.0.0", "dotenv-expand": "^5.1.0", + "file-saver": "^2.0.5", "final-form": "^4.20.7", "final-form-arrays": "^3.0.2", "fs-extra": "^10.0.0", "identity-obj-proxy": "^3.0.0", - "iguazio.dashboard-react-controls": "1.3.0", + "iguazio.dashboard-react-controls": "1.5.1", "is-wsl": "^1.1.0", "js-base64": "^2.5.2", "js-yaml": "^3.13.1", @@ -41,13 +42,13 @@ "react-final-form": "^6.5.9", "react-final-form-arrays": "^3.1.3", "react-final-form-listeners": "^1.0.3", - "react-flow-renderer": "^8.6.0", "react-modal-promise": "^1.0.2", "react-redux": "^7.1.3", "react-refresh": "^0.11.0", "react-router-dom": "6.2.2", "react-text-mask": "^5.4.3", "react-transition-group": "^4.3.0", + "reactflow": "^11.8.3", "redux": "^4.0.5", "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0", @@ -130,7 +131,7 @@ "body-parser": "^1.19.0", "case-sensitive-paths-webpack-plugin": "^2.4.0", "chai": "^4.3.4", - "chromedriver": "^113.0.0", + "chromedriver": "^117.0.0", "crypto": "^1.0.1", "css-loader": "^6.5.1", "cucumber-html-reporter": "^5.3.0", diff --git a/src/App.js b/src/App.js index 1f3edfa6e..edd4acc47 100755 --- a/src/App.js +++ b/src/App.js @@ -39,10 +39,13 @@ import { MONITOR_WORKFLOWS_TAB, PIPELINE_SUB_PAGE, PROJECTS_SETTINGS_GENERAL_TAB, + PROJECT_MONITOR, + PROJECT_QUICK_ACTIONS_PAGE, REAL_TIME_PIPELINES_TAB, SCHEDULE_TAB } from './constants' +import 'reactflow/dist/style.css' import 'igz-controls/scss/common.scss' import './scss/main.scss' @@ -115,17 +118,25 @@ const App = () => { > } /> - } /> - } /> + } /> + } + /> + {!isNuclioModeDisabled && ( } > } /> - } /> + } /> )} + } + /> } diff --git a/src/actions/details.js b/src/actions/details.js index 81d9e9cb3..9e2d7c4bb 100644 --- a/src/actions/details.js +++ b/src/actions/details.js @@ -94,11 +94,11 @@ const detailsActions = { type: FETCH_MODEL_FEATURE_VECTOR_SUCCESS, payload: featureSets }), - fetchJobPods: (project, uid) => dispatch => { + fetchJobPods: (project, uid, kind) => dispatch => { dispatch(detailsActions.fetchPodsBegin()) return detailsApi - .getJobPods(project) + .getJobPods(project, uid, kind) .then(({ data }) => { let podsData = generatePods(project, uid, data) diff --git a/src/actions/functions.js b/src/actions/functions.js index 946413126..42cbe89cd 100644 --- a/src/actions/functions.js +++ b/src/actions/functions.js @@ -27,6 +27,10 @@ import { FETCH_FUNCTION_TEMPLATE_BEGIN, FETCH_FUNCTION_TEMPLATE_FAILURE, FETCH_FUNCTION_TEMPLATE_SUCCESS, + FETCH_HUB_FUNCTION_TEMPLATE_BEGIN, + FETCH_HUB_FUNCTION_TEMPLATE_FAILURE, + FETCH_HUB_FUNCTION_TEMPLATE_SUCCESS, + FETCH_HUB_FUNCTIONS_BEGIN, REMOVE_FUNCTION_TEMPLATE, SET_FUNCTIONS_TEMPLATES, SET_LOADING, @@ -39,6 +43,7 @@ import { SET_NEW_FUNCTION_IMAGE, SET_NEW_FUNCTION_BASE_IMAGE, SET_NEW_FUNCTION_COMMANDS, + SET_NEW_FUNCTION_REQUIREMENTS, SET_NEW_FUNCTION_VOLUME_MOUNTS, SET_NEW_FUNCTION_VOLUMES, SET_NEW_FUNCTION_RESOURCES, @@ -71,12 +76,16 @@ import { GET_FUNCTION_FAILURE, GET_FUNCTION_BEGIN, REMOVE_FUNCTION, + REMOVE_HUB_FUNCTIONS, SET_NEW_FUNCTION_FORCE_BUILD, SET_NEW_FUNCTION_PREEMTION_MODE, - SET_NEW_FUNCTION_PRIORITY_CLASS_NAME + SET_NEW_FUNCTION_PRIORITY_CLASS_NAME, + FETCH_FUNCTIONS_TEMPLATES_FAILURE, + FETCH_HUB_FUNCTIONS_FAILURE, + SET_HUB_FUNCTIONS } from '../constants' import { FORBIDDEN_ERROR_STATUS_CODE } from 'igz-controls/constants' -import { generateCategories } from '../utils/generateTemplatesCategories' +import { generateCategories, generateHubCategories } from '../utils/generateTemplatesCategories' import { setNotification } from '../reducers/notificationReducer' const functionsActions = { @@ -97,8 +106,6 @@ const functionsActions = { : error.message dispatch(functionsActions.createNewFunctionFailure(message)) - - throw error }) }, createNewFunctionBegin: () => ({ @@ -161,20 +168,63 @@ const functionsActions = { fetchFunctionLogsSuccess: () => ({ type: FETCH_FUNCTION_LOGS_SUCCESS }), - fetchFunctions: (project, filters) => dispatch => { - dispatch(functionsActions.fetchFunctionsBegin()) + fetchFunctionTemplate: path => dispatch => { + dispatch(functionsActions.fetchFunctionTemplateBegin()) return functionsApi - .getFunctions(project, filters) - .then(({ data }) => { - dispatch(functionsActions.fetchFunctionsSuccess(data.funcs)) + .getFunctionTemplate(path) + .then(response => { + let parsedData = yaml.safeLoad(response.data) + const templates = { + name: parsedData.metadata.name, + functions: parsedData.spec.entry_point ? [] : [parsedData] + } - return data.funcs + dispatch(functionsActions.fetchFunctionTemplateSuccess(templates)) + + return templates }) - .catch(err => { - dispatch(functionsActions.fetchFunctionsFailure(err.message)) + .catch(error => { + const errorMsg = get(error, 'response.data.detail', "Function's template failed to load") + + dispatch(functionsActions.fetchFunctionTemplateFailure(error)) + dispatch( + setNotification({ + status: error.response?.status || 400, + id: Math.random(), + message: errorMsg, + error + }) + ) }) }, + fetchFunctionTemplateSuccess: selectFunction => ({ + type: FETCH_FUNCTION_TEMPLATE_SUCCESS, + payload: selectFunction + }), + fetchFunctionTemplateBegin: () => ({ + type: FETCH_FUNCTION_TEMPLATE_BEGIN + }), + fetchFunctionTemplateFailure: err => ({ + type: FETCH_FUNCTION_TEMPLATE_FAILURE, + payload: err + }), + fetchFunctions: + (project, filters, withoutLoader = false) => + dispatch => { + dispatch(functionsActions.fetchFunctionsBegin(withoutLoader)) + + return functionsApi + .getFunctions(project, filters) + .then(({ data }) => { + dispatch(functionsActions.fetchFunctionsSuccess(data.funcs)) + + return data.funcs + }) + .catch(err => { + dispatch(functionsActions.fetchFunctionsFailure(err.message)) + }) + }, fetchFunctionsBegin: () => ({ type: FETCH_FUNCTIONS_BEGIN }), @@ -196,28 +246,62 @@ const functionsActions = { return templatesData }) - .catch(error => dispatch(functionsActions.fetchJobLogsFailure(error))) + .catch(error => { + dispatch(functionsActions.fetchFunctionsTemplatesFailure(error)) + }) }, - fetchFunctionTemplate: path => dispatch => { - dispatch(functionsActions.fetchFunctionTemplateBegin()) + fetchFunctionsTemplatesFailure: err => ({ + type: FETCH_FUNCTIONS_TEMPLATES_FAILURE, + payload: err + }), + fetchHubFunction: hubFunctionName => dispatch => { + dispatch(functionsActions.fetchHubFunctionTemplateBegin()) return functionsApi - .getFunctionTemplate(path) + .getHubFunction(hubFunctionName) .then(response => { - let parsedData = yaml.safeLoad(response.data) - const templates = { - name: parsedData.metadata.name, - functions: parsedData.spec.entry_point ? [] : [parsedData] - } + dispatch(functionsActions.fetchHubFunctionTemplateSuccess()) + return response.data + }) + .catch(error => { + const errorMsg = get(error, 'response.data.detail', 'The function failed to load') + dispatch(functionsActions.fetchHubFunctionTemplateFailure(error)) - dispatch(functionsActions.fetchFunctionTemplateSuccess(templates)) + dispatch( + setNotification({ + status: error.response?.status || 400, + id: Math.random(), + message: errorMsg, + error + }) + ) + }) + }, + fetchHubFunctionTemplateSuccess: () => ({ + type: FETCH_HUB_FUNCTION_TEMPLATE_SUCCESS + }), + fetchHubFunctionTemplateBegin: () => ({ + type: FETCH_HUB_FUNCTION_TEMPLATE_BEGIN + }), + fetchHubFunctionTemplateFailure: err => ({ + type: FETCH_HUB_FUNCTION_TEMPLATE_FAILURE, + payload: err + }), + fetchHubFunctions: () => dispatch => { + dispatch(functionsActions.fetchHubFunctionsBegin()) - return templates + return functionsApi + .getHubFunctions() + .then(({ data: functionTemplates }) => { + const templatesData = generateHubCategories(functionTemplates.catalog) + + dispatch(functionsActions.setHubFunctions(templatesData)) + + return templatesData }) .catch(error => { - const errorMsg = get(error, 'response.data.detail', "Function's template failed to load") + const errorMsg = get(error, 'response.data.detail', 'Functions failed to load') - dispatch(functionsActions.fetchFunctionTemplateFailure(error)) dispatch( setNotification({ status: error.response?.status || 400, @@ -226,26 +310,21 @@ const functionsActions = { error }) ) - - throw error }) }, - fetchFunctionTemplateSuccess: selectFunction => ({ - type: FETCH_FUNCTION_TEMPLATE_SUCCESS, - payload: selectFunction - }), - fetchFunctionTemplateBegin: () => ({ - type: FETCH_FUNCTION_TEMPLATE_BEGIN + + fetchHubFunctionsBegin: () => ({ + type: FETCH_HUB_FUNCTIONS_BEGIN }), - fetchFunctionTemplateFailure: err => ({ - type: FETCH_FUNCTION_TEMPLATE_FAILURE, + fetchHubFunctionsFailure: err => ({ + type: FETCH_HUB_FUNCTIONS_FAILURE, payload: err }), - getFunction: (project, name, hash) => dispatch => { + getFunction: (project, name, hash, tag) => dispatch => { dispatch(functionsActions.getFunctionBegin()) return functionsApi - .getFunction(project, name, hash) + .getFunction(project, name, hash, tag) .then(result => { dispatch(functionsActions.getFunctionSuccess(result.data.func)) @@ -270,6 +349,9 @@ const functionsActions = { removeFunction: () => ({ type: REMOVE_FUNCTION }), + removeHubFunctions: () => ({ + type: REMOVE_HUB_FUNCTIONS + }), removeFunctionTemplate: () => ({ type: REMOVE_FUNCTION_TEMPLATE }), @@ -286,6 +368,10 @@ const functionsActions = { type: SET_FUNCTIONS_TEMPLATES, payload }), + setHubFunctions: payload => ({ + type: SET_HUB_FUNCTIONS, + payload + }), setLoading: loading => ({ type: SET_LOADING, payload: loading @@ -306,6 +392,10 @@ const functionsActions = { type: SET_NEW_FUNCTION_COMMANDS, payload: commands }), + setNewFunctionRequirements: requirements => ({ + type: SET_NEW_FUNCTION_REQUIREMENTS, + payload: requirements + }), setNewFunctionDefaultClass: default_class => ({ type: SET_NEW_FUNCTION_DEFAULT_CLASS, payload: default_class diff --git a/src/actions/jobs.js b/src/actions/jobs.js index c2a5d6f14..9059722e9 100644 --- a/src/actions/jobs.js +++ b/src/actions/jobs.js @@ -43,8 +43,6 @@ import { FETCH_JOB_LOGS_FAILURE, FETCH_JOB_LOGS_SUCCESS, FETCH_JOB_SUCCESS, - FETCH_SCHEDULED_JOB_ACCESS_KEY_BEGIN, - FETCH_SCHEDULED_JOB_ACCESS_KEY_END, REMOVE_JOB, REMOVE_JOB_ERROR, REMOVE_JOB_FUNCTION, @@ -70,7 +68,10 @@ import { SET_NEW_JOB_VOLUMES, SET_NEW_JOB_VOLUME_MOUNTS, SET_TUNING_STRATEGY, - SET_URL + SET_URL, + DELETE_JOB_BEGIN, + DELETE_JOB_FAILURE, + DELETE_JOB_SUCCESS } from '../constants' import { getNewJobErrorMsg } from '../components/JobWizard/JobWizard.util' import { setNotification } from '../reducers/notificationReducer' @@ -97,16 +98,38 @@ const jobsActions = { abortJobSuccess: () => ({ type: ABORT_JOB_SUCCESS }), + deleteJob: (project, job) => dispatch => { + dispatch(jobsActions.deleteJobBegin()) + + return jobsApi + .deleteJob(project, job.uid) + .then(() => dispatch(jobsActions.deleteJobSuccess())) + .catch(error => { + dispatch(jobsActions.deleteJobFailure(error.message)) + + throw error + }) + }, + deleteJobBegin: () => ({ + type: DELETE_JOB_BEGIN + }), + deleteJobFailure: error => ({ + type: DELETE_JOB_FAILURE, + payload: error + }), + deleteJobSuccess: () => ({ + type: DELETE_JOB_SUCCESS + }), editJob: (postData, project) => () => jobsApi.editJob(postData, project), editJobFailure: error => ({ type: EDIT_JOB_FAILURE, payload: error }), - fetchAllJobRuns: (project, filters, jobName) => dispatch => { + fetchAllJobRuns: (project, filters, jobName, cancelToken) => dispatch => { dispatch(jobsActions.fetchAllJobRunsBegin()) return jobsApi - .getAllJobRuns(project, jobName, filters) + .getAllJobRuns(project, jobName, filters, cancelToken) .then(({ data }) => { dispatch(jobsActions.fetchAllJobRunsSuccess(data.runs || [])) @@ -175,8 +198,6 @@ const jobsActions = { error }) ) - - throw error }) }, fetchJobFunctionBegin: () => ({ @@ -274,27 +295,6 @@ const jobsActions = { type: FETCH_JOBS_SUCCESS, payload: jobsList }), - fetchScheduledJobAccessKey: (projectName, jobName) => dispatch => { - dispatch(jobsActions.fetchScheduledJobAccessKeyBegin()) - - return jobsApi - .getScheduledJobAccessKey(projectName, jobName) - .then(result => { - dispatch(jobsActions.fetchScheduledJobAccessKeyEnd()) - - return result - }) - .catch(error => { - dispatch(jobsActions.fetchScheduledJobAccessKeyEnd()) - throw error - }) - }, - fetchScheduledJobAccessKeyBegin: () => ({ - type: FETCH_SCHEDULED_JOB_ACCESS_KEY_BEGIN - }), - fetchScheduledJobAccessKeyEnd: () => ({ - type: FETCH_SCHEDULED_JOB_ACCESS_KEY_END - }), handleRunScheduledJob: (postData, project, job) => () => jobsApi.runScheduledJob(postData, project, job), removeJob: () => ({ diff --git a/src/actions/nuclio.js b/src/actions/nuclio.js index 189383c1c..aa82863f8 100644 --- a/src/actions/nuclio.js +++ b/src/actions/nuclio.js @@ -84,8 +84,6 @@ const nuclioActions = { }) .catch(error => { dispatch(nuclioActions.fetchNuclioFunctionsFailure(error.message)) - - throw error.message }) }, fetchNuclioFunctionsBegin: () => ({ diff --git a/src/actions/projects.js b/src/actions/projects.js index 4cdc7a95c..19f13c893 100644 --- a/src/actions/projects.js +++ b/src/actions/projects.js @@ -322,7 +322,7 @@ const projectsAction = { type: FETCH_PROJECT_FUNCTIONS_SUCCESS, payload: functions }), - fetchProjectJobs: project => dispatch => { + fetchProjectJobs: (project, startTimeFrom) => dispatch => { dispatch(projectsAction.fetchProjectJobsBegin()) const params = { @@ -331,7 +331,8 @@ const projectsAction = { 'partition-sort-by': 'updated', 'rows-per-partition': '5', 'max-partitions': '5', - iter: 'false' + iter: 'false', + start_time_from: startTimeFrom } return projectsApi @@ -494,7 +495,11 @@ const projectsAction = { .getProjects(params) .then(response => { dispatch(projectsAction.fetchProjectsSuccess(response.data.projects)) - dispatch(projectsAction.fetchProjectsNamesSuccess(response.data.projects.map(project => project.metadata.name))) + dispatch( + projectsAction.fetchProjectsNamesSuccess( + response.data.projects.map(project => project.metadata.name) + ) + ) return response.data.projects }) diff --git a/src/api/details-api.js b/src/api/details-api.js index 8dce0f5cc..41900808d 100644 --- a/src/api/details-api.js +++ b/src/api/details-api.js @@ -19,17 +19,22 @@ such restriction. */ import { mainHttpClient } from '../httpClient' +import { JOB_KIND_JOB } from '../constants' + const detailsApi = { - getJobPods: project => - mainHttpClient.get(`/projects/${project}/runtime-resources?group-by=job`), + getJobPods: (project, uid, kind) => { + const params = { + 'group-by': JOB_KIND_JOB, + kind, + 'label-selector': `mlrun/uid=${uid}` + } + + return mainHttpClient.get(`/projects/${project}/runtime-resources`, { params }) + }, getModelEndpoint: (project, uid) => - mainHttpClient.get( - `/projects/${project}/model-endpoints/${uid}?feature_analysis=true` - ), + mainHttpClient.get(`/projects/${project}/model-endpoints/${uid}?feature_analysis=true`), getModelFeatureVector: (project, name, reference) => - mainHttpClient.get( - `/projects/${project}/feature-vectors/${name}/references/${reference}` - ) + mainHttpClient.get(`/projects/${project}/feature-vectors/${name}/references/${reference}`) } export default detailsApi diff --git a/src/api/functions-api.js b/src/api/functions-api.js index 057cc101f..c123ae2ee 100644 --- a/src/api/functions-api.js +++ b/src/api/functions-api.js @@ -43,10 +43,16 @@ const functionsApi = { return mainHttpClient.get(`/projects/${project}/functions`, { params }) }, - getFunction: (project, functionName, hash) => { + getFunction: (project, functionName, hash, tag) => { const params = {} - if (hash) params.hash_key = hash + if (hash) { + params.hash_key = hash + } + + if (tag) { + params.tag = tag + } return mainHttpClient.get(`/projects/${project}/functions/${functionName}`, { params }) }, @@ -64,7 +70,18 @@ const functionsApi = { return mainHttpClient.get('/build/status', { params }) }, - getFunctionTemplate: path => functionTemplatesHttpClient.get(path), + getHubFunction: hubFunctionName => + mainHttpClient.get(`/hub/sources/default/items/${hubFunctionName}`), + getHubFunctions: () => mainHttpClient.get('/hub/sources/default/items'), + getFunctionTemplate: path => { + if (path.startsWith('http')) { + return mainHttpClient.get('/hub/sources/default/item-object', { + params: { url: path } + }) + } else { + return functionTemplatesHttpClient.get(path) + } + }, getFunctionTemplatesCatalog: () => functionTemplatesHttpClient.get('catalog.json') } diff --git a/src/api/jobs-api.js b/src/api/jobs-api.js index 975c69158..5eae4e50f 100644 --- a/src/api/jobs-api.js +++ b/src/api/jobs-api.js @@ -66,6 +66,9 @@ const jobsApi = { { params } ) }, + deleteJob: (project, jobUid) => { + return mainHttpClient.delete(`/projects/${project}/runs/${jobUid}`) + }, editJob: (postData, project) => mainHttpClient.put( `/projects/${project}/schedules/${postData.scheduled_object.task.metadata.name}`, @@ -94,14 +97,20 @@ const jobsApi = { return mainHttpClient.get(`/runs?${jobListQuery}`, { params }) }, - getAllJobRuns: (project, jobName, filters) => { - const params = { - project, - name: jobName, - ...generateRequestParams(filters) + getAllJobRuns: (project, jobName, filters, cancelToken) => { + const config = { + params: { + project, + name: jobName, + ...generateRequestParams(filters) + } } - return mainHttpClient.get('/runs', { params }) + if (cancelToken) { + config.cancelToken = cancelToken + } + + return mainHttpClient.get('/runs', config) }, getJob: (project, jobId, iter) => { const params = {} @@ -116,8 +125,6 @@ const jobsApi = { fetch(`${mainBaseUrl}/log/${project}/${id}`, { method: 'get' }), - getScheduledJobAccessKey: (project, job) => - mainHttpClient.get(`/projects/${project}/schedules/${job}?include-credentials=true`), getScheduledJobs: (project, filters) => { const params = { include_last_run: 'yes' diff --git a/src/common/ActionsMenu/ActionsMenu.js b/src/common/ActionsMenu/ActionsMenu.js index cc0902ca2..388de5286 100644 --- a/src/common/ActionsMenu/ActionsMenu.js +++ b/src/common/ActionsMenu/ActionsMenu.js @@ -32,16 +32,19 @@ import { ReactComponent as ActionMenuIcon } from 'igz-controls/images/elipsis.sv import './actionsMenu.scss' -const ActionsMenu = ({ dataItem, menu, time }) => { +const ActionsMenu = ({ dataItem, menu, time, withQuickActions }) => { const [isShowMenu, setIsShowMenu] = useState(false) const [isIconDisplayed, setIsIconDisplayed] = useState(false) const [actionMenu, setActionMenu] = useState(menu) const [renderMenu, setRenderMenu] = useState(false) const actionMenuRef = useRef() const dropDownMenuRef = useRef() + const mainActionsWrapperRef = useRef() + const actionMenuBtnRef = useRef() const actionMenuClassNames = classnames( 'actions-menu__container', + withQuickActions && 'actions-menu__container_extended', isShowMenu && 'actions-menu__container-active' ) const dropDownMenuClassNames = classnames('actions-menu__body', isShowMenu && 'show') @@ -55,26 +58,26 @@ const ActionsMenu = ({ dataItem, menu, time }) => { }, [dataItem, menu]) useEffect(() => { - setIsIconDisplayed(actionMenu.some(menuItem => menuItem.icon)) + setIsIconDisplayed(actionMenu[0].some(menuItem => menuItem.icon)) }, [actionMenu]) const showActionsList = () => { setIsShowMenu(show => !show) - const actionMenuRect = actionMenuRef.current.getBoundingClientRect() + const actionMenuBtnRect = actionMenuBtnRef.current.getBoundingClientRect() const dropDownMenuRect = dropDownMenuRef.current.getBoundingClientRect() if ( - actionMenuRect.top + actionMenuRect.height + offset + dropDownMenuRect.height >= + actionMenuBtnRect.top + actionMenuBtnRect.height + offset + dropDownMenuRect.height >= window.innerHeight ) { - dropDownMenuRef.current.style.top = `${actionMenuRect.top - dropDownMenuRect.height}px` + dropDownMenuRef.current.style.top = `${actionMenuBtnRect.top - dropDownMenuRect.height}px` dropDownMenuRef.current.style.left = `${ - actionMenuRect.left - dropDownMenuRect.width + offset + actionMenuBtnRect.left - dropDownMenuRect.width + offset }px` } else { - dropDownMenuRef.current.style.top = `${actionMenuRect.bottom}px` + dropDownMenuRef.current.style.top = `${actionMenuBtnRect.bottom}px` dropDownMenuRef.current.style.left = `${ - actionMenuRect.left - (dropDownMenuRect.width - offset) + actionMenuBtnRect.left - (dropDownMenuRect.width - offset) }px` } } @@ -88,8 +91,13 @@ const ActionsMenu = ({ dataItem, menu, time }) => { } } - const handleMouseOver = () => { - setRenderMenu(true) + const handleMouseOver = event => { + if (mainActionsWrapperRef.current?.contains(event.target)) { + setRenderMenu(false) + setIsShowMenu(false) + } else { + setRenderMenu(true) + } if (idTimeout) clearTimeout(idTimeout) } @@ -113,10 +121,23 @@ const ActionsMenu = ({ dataItem, menu, time }) => { onMouseOver={handleMouseOver} ref={actionMenuRef} > - + {withQuickActions && ( +
+ {actionMenu[1].map(mainAction => ( + mainAction.onClick(dataItem)} + tooltipText={mainAction.label} + key={mainAction.label} + > + {mainAction.icon} + + ))} +
+ )} + - {renderMenu && createPortal(
{ }} ref={dropDownMenuRef} > - {actionMenu.map( + {actionMenu[0].map( (menuItem, idx) => !menuItem.hidden && ( { ActionsMenu.defaultProps = { dataItem: {}, - time: 100 + time: 100, + withQuickActions: false } ActionsMenu.propTypes = { dataItem: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.string]), menu: ACTIONS_MENU.isRequired, - time: PropTypes.number + time: PropTypes.number, + withQuickActions: PropTypes.bool } export default ActionsMenu diff --git a/src/common/ActionsMenu/actionsMenu.scss b/src/common/ActionsMenu/actionsMenu.scss index 73c6bbf9d..b37b36a69 100644 --- a/src/common/ActionsMenu/actionsMenu.scss +++ b/src/common/ActionsMenu/actionsMenu.scss @@ -7,14 +7,41 @@ position: relative; display: none; + &_extended { + position: absolute; + right: 0; + display: none; + align-items: center; + justify-content: center; + background-color: $ghostWhite; + + &:before { + content: ''; + width: 30px; + height: 100%; + position: absolute; + display: block; + left: -30px; + background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(245,247,255,1) 100%); + } + } + &-active { - display: block; + display: flex; } + + } + + &__main-actions-wrapper { + display: flex; + align-items: center; + justify-content: center; } &__body { position: fixed; - width: 150px; + min-width: 150px; + max-width: 250px; background: $white; border: $primaryBorder; border-radius: $mainBorderRadius; diff --git a/src/common/Breadcrumbs/Breadcrumbs.js b/src/common/Breadcrumbs/Breadcrumbs.js index 73eac5fdf..cb4f02d3b 100755 --- a/src/common/Breadcrumbs/Breadcrumbs.js +++ b/src/common/Breadcrumbs/Breadcrumbs.js @@ -38,8 +38,10 @@ import './breadcrums.scss' const Breadcrumbs = ({ onClick, projectStore, fetchProjectsNames }) => { const [showScreensList, setShowScreensList] = useState(false) const [showProjectsList, setShowProjectsList] = useState(false) + const [searchValue, setSearchValue] = useState('') const { isDemoMode } = useMode() const breadcrumbsRef = useRef() + const projectListRef = useRef() const params = useParams() const location = useLocation() @@ -81,10 +83,34 @@ const Breadcrumbs = ({ onClick, projectStore, fetchProjectsNames }) => { if (showProjectsList) setShowProjectsList(false) } + + setSearchValue('') }, [breadcrumbsRef, showProjectsList, showScreensList] ) + const scrollProjectOptionToView = useCallback(() => { + const selectedOptionEl = projectListRef.current.querySelector(`#${params.projectName}`) + + searchValue + ? projectListRef.current.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) + : setTimeout(() => { + selectedOptionEl.scrollIntoView( + { + behavior: 'smooth', + block: 'center' + }, + 0 + ) + }) + }, [params.projectName, searchValue]) + + useEffect(() => { + if (showProjectsList && projectListRef.current) { + scrollProjectOptionToView() + } + }, [showProjectsList, scrollProjectOptionToView]) + useEffect(() => { window.addEventListener('click', handleCloseDropdown) @@ -188,6 +214,8 @@ const Breadcrumbs = ({ onClick, projectStore, fetchProjectsNames }) => { list={projectScreens} onClick={() => handleSelectDropdownItem(separatorRef)} selectedItem={urlItems.screen?.id} + searchValue={searchValue} + setSearchValue={setSearchValue} /> )} {showProjectsList && urlItems.pathItems[i + 1] === params.projectName && ( @@ -196,8 +224,11 @@ const Breadcrumbs = ({ onClick, projectStore, fetchProjectsNames }) => { link={to} list={projectsList} onClick={() => handleSelectDropdownItem(separatorRef)} + ref={projectListRef} screen={urlItems.screen?.id} selectedItem={params.projectName} + searchValue={searchValue} + setSearchValue={setSearchValue} tab={urlItems.tab?.id} withSearch /> diff --git a/src/common/Breadcrumbs/breadcrumbs.util.js b/src/common/Breadcrumbs/breadcrumbs.util.js index bda0def0b..90c8f9fd1 100644 --- a/src/common/Breadcrumbs/breadcrumbs.util.js +++ b/src/common/Breadcrumbs/breadcrumbs.util.js @@ -25,6 +25,8 @@ import { MONITOR_JOBS_TAB, MODELS_TAB, MONITOR_WORKFLOWS_TAB, + PROJECT_MONITOR, + PROJECT_QUICK_ACTIONS_PAGE, SCHEDULE_TAB, REAL_TIME_PIPELINES_TAB } from '../../constants' @@ -32,14 +34,18 @@ import { generateNuclioLink } from '../../utils' export const generateProjectScreens = params => [ { - label: 'Project Monitoring', - id: 'monitor' + label: 'Project monitoring', + id: PROJECT_MONITOR + }, + { + label: 'Quick actions', + id: PROJECT_QUICK_ACTIONS_PAGE }, { label: 'Feature Store', id: 'feature-store' }, { label: 'Datasets', id: 'datasets' }, { label: 'Artifacts', id: 'files' }, { label: 'Models', id: 'models' }, - { label: 'Jobs', id: 'jobs' }, + { label: 'Jobs and workflows', id: 'jobs' }, { label: 'ML functions', id: 'functions' }, { label: 'Real-time functions', diff --git a/src/common/Chart/MlChart.js b/src/common/Chart/MlChart.js index 2a326ada3..4575fbbfa 100644 --- a/src/common/Chart/MlChart.js +++ b/src/common/Chart/MlChart.js @@ -34,17 +34,37 @@ const MlChart = ({ config }) => { useLayoutEffect(() => { const ctx = canvasRef.current.getContext('2d') - const mlChartInstance = new Chart(ctx, { + const pythonInfinity = 'e+308' + const chartConfig = { ...config, + data: { + ...config.data, + labels: config.data.labels.map(label => { + const labelStr = String(label) + if (labelStr.includes(pythonInfinity)) { + if (labelStr.includes('-')) { + return `${labelStr.replace(/^([-]).*/, '$1∞')}` + } + + return '∞' + } + + return label + }) + } + } + + const mlChartInstance = new Chart(ctx, { + ...chartConfig, options: { - ...config.options, + ...chartConfig.options, animation: { - ...config.options.animation, + ...chartConfig.options.animation, onComplete: () => { setIsLoading(false) - if (config?.options?.animation?.onComplete) { - config.options.animation.onComplete() + if (chartConfig?.options?.animation?.onComplete) { + chartConfig.options.animation.onComplete() } } } diff --git a/src/common/Chart/mlChart.scss b/src/common/Chart/mlChart.scss index 06141fd6a..d71a33d2f 100644 --- a/src/common/Chart/mlChart.scss +++ b/src/common/Chart/mlChart.scss @@ -10,6 +10,8 @@ canvas.hidden { position: absolute; + top: 0; + left: 0; width: 0; height: 0; opacity: 0; diff --git a/src/common/ChipCell/ChipCell.js b/src/common/ChipCell/ChipCell.js index fd9c2ed02..50769d3a1 100644 --- a/src/common/ChipCell/ChipCell.js +++ b/src/common/ChipCell/ChipCell.js @@ -25,7 +25,7 @@ import ChipCellView from './ChipCellView' import { CHIP_OPTIONS } from '../../types' import { CLICK, TAB, TAB_SHIFT } from 'igz-controls/constants' import { cutChips } from '../../utils/cutChips' -import { useChipCell } from 'igz-controls/hooks/useChipCell.hook' +import { useChipCell } from 'igz-controls/hooks' const ChipCell = ({ addChip, diff --git a/src/common/ChipCell/HiddenChipsBlock/HiddenChipsBlock.js b/src/common/ChipCell/HiddenChipsBlock/HiddenChipsBlock.js index 78ebb569a..e36109cdd 100644 --- a/src/common/ChipCell/HiddenChipsBlock/HiddenChipsBlock.js +++ b/src/common/ChipCell/HiddenChipsBlock/HiddenChipsBlock.js @@ -25,7 +25,7 @@ import Chip from '../../Chip/Chip' import ChipTooltip from '../ChipTooltip/ChipTooltip' import { CHIP_OPTIONS, CHIPS } from '../../../types' -import { useHiddenChipsBlock } from 'igz-controls/hooks/useHiddenChipsBlock.hook' +import { useHiddenChipsBlock } from 'igz-controls/hooks' const HiddenChipsBlock = React.forwardRef( ( diff --git a/src/common/Combobox/combobox.scss b/src/common/Combobox/combobox.scss index 1e3d5c55c..b0214e285 100644 --- a/src/common/Combobox/combobox.scss +++ b/src/common/Combobox/combobox.scss @@ -78,7 +78,6 @@ } &-select { - position: relative; display: flex; max-width: 95px; @@ -88,9 +87,10 @@ &__body { position: absolute; - top: 25px; + top: 100%; z-index: 5; display: none; + margin-top: 5px; max-width: 220px; background-color: $white; box-shadow: $previewBoxShadow; diff --git a/src/common/FormTagFilter/FormTagFilter.js b/src/common/FormTagFilter/FormTagFilter.js index 212eaca81..2ecf08d13 100644 --- a/src/common/FormTagFilter/FormTagFilter.js +++ b/src/common/FormTagFilter/FormTagFilter.js @@ -17,13 +17,14 @@ 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, useRef, useEffect, useCallback, useMemo } from 'react' +import React, { useState, useRef, useEffect, useCallback, useMemo, useLayoutEffect } from 'react' import PropTypes from 'prop-types' import { Field, useField } from 'react-final-form' import { useSelector } from 'react-redux' import { isEqual } from 'lodash' +import classnames from 'classnames' -import TagFilterDropdown from '../TagFilter/TagFilterDropdown' +import { PopUpDialog } from 'igz-controls/components' import { tagFilterOptions } from '../../components/FilterMenu/filterMenu.settings' import { TAG_FILTER_LATEST } from '../../constants' @@ -38,7 +39,15 @@ const FormTagFilter = ({ label, name }) => { const [tagFilter, setTagFilter] = useState(input.value) const [tagOptions, setTagOptions] = useState(tagFilterOptions) const tagFilterRef = useRef() + const dropdownRef = useRef() const filtersStore = useSelector(store => store.filtersStore) + const [dropdownWidth, setDropdownWidth] = useState(200) + + useLayoutEffect(() => { + if (tagFilterRef?.current) { + setDropdownWidth(tagFilterRef?.current.getBoundingClientRect().width) + } + }, []) const options = useMemo(() => { let newTagOptions = tagFilterOptions @@ -92,7 +101,10 @@ const FormTagFilter = ({ label, name }) => { event => { const elementPath = event.path ?? event.composedPath?.() - if (!elementPath.includes(tagFilterRef.current)) { + if ( + !elementPath.includes(tagFilterRef.current) && + !dropdownRef.current.contains(event.target) + ) { if (tagFilter.length <= 0) { input.onChange(TAG_FILTER_LATEST) setTagFilter(tagFilterOptions.find(tag => tag.id === TAG_FILTER_LATEST).label) @@ -161,17 +173,40 @@ const FormTagFilter = ({ label, name }) => { } }} /> -
+
{isDropDownMenuOpen && ( - + + {tagOptions.map(tag => { + const dropdownItemClassName = classnames( + 'form-tag-filter__dropdown-item', + tagFilter.length !== 0 && + tagFilter === tag.id && + 'form-tag-filter__dropdown-item_selected' + ) + + return ( +
handleSelectFilter(event, tag)} + > + {tag.label} +
+ ) + })} +
)}
)} diff --git a/src/common/FormTagFilter/formTagFilters.scss b/src/common/FormTagFilter/formTagFilters.scss index 20367fa60..c0c83e2fa 100644 --- a/src/common/FormTagFilter/formTagFilters.scss +++ b/src/common/FormTagFilter/formTagFilters.scss @@ -35,10 +35,7 @@ } } - .tag-filter__dropdown { - position: absolute; - top: calc(100% + 5px); - z-index: 3; + &__dropdown { width: 100%; max-height: 400px; overflow-y: auto; @@ -47,6 +44,10 @@ border-radius: $mainBorderRadius; box-shadow: $tooltipShadow; + .pop-up-dialog { + padding: 0; + } + &-button { display: flex; flex: 1; diff --git a/src/common/Input/input.scss b/src/common/Input/input.scss index c94425ed4..a08008ae7 100644 --- a/src/common/Input/input.scss +++ b/src/common/Input/input.scss @@ -5,6 +5,7 @@ .input { @import '../../scss/main.scss'; + @include fieldWrapperOld; &::placeholder { @@ -143,6 +144,7 @@ &.active-label { top: 9px; + height: auto; font-weight: 700; font-size: 10px; line-height: 12px; @@ -154,13 +156,14 @@ } &-floating { - display: flex; - align-items: center; position: absolute; - top: 15px; + top: unset; left: 16px; + display: flex; + align-items: center; + height: 100%; color: $topaz; - transition: 300ms ease all; + transition: 300ms linear all; } &_disabled { diff --git a/src/common/Notification/Notification.js b/src/common/Notification/Notification.js index 294b34b36..699c5c36d 100644 --- a/src/common/Notification/Notification.js +++ b/src/common/Notification/Notification.js @@ -33,16 +33,20 @@ const Notification = () => { const defaultStyle = { position: 'fixed', right: '24px', - bottom: '-100px', + bottom: '-115px', opacity: 0, zIndex: '1000' } - const heightNotification = 60 - const offsetNotification = 60 + const heightNotification = 65 + const offsetNotification = 65 const duration = 500 + const handleRemoveNotification = itemId => { + dispatch(removeNotification(itemId)) + } + const handleRetry = item => { - dispatch(removeNotification(item.id)) + handleRemoveNotification(item.id) item.retry(item) } @@ -73,8 +77,8 @@ const Notification = () => { classNames="notification_download" onEntered={() => { setTimeout(() => { - dispatch(removeNotification(item.id)) - }, 4000) + handleRemoveNotification(item.id) + }, 10000) }} > {state => ( @@ -86,6 +90,7 @@ const Notification = () => { }} key={item.id} isSuccessResponse={isSuccessResponse} + handleRemoveNotification={handleRemoveNotification} retry={handleRetry} /> )} diff --git a/src/common/Notification/NotificationView.js b/src/common/Notification/NotificationView.js index 78b7b831f..ec19444f9 100644 --- a/src/common/Notification/NotificationView.js +++ b/src/common/Notification/NotificationView.js @@ -21,15 +21,28 @@ import React from 'react' import PropTypes from 'prop-types' import { createPortal } from 'react-dom' +import { ReactComponent as CloseIcon } from 'igz-controls/images/close.svg' import { ReactComponent as SuccessDone } from 'igz-controls/images/success_done.svg' import { ReactComponent as UnsuccessAlert } from 'igz-controls/images/unsuccess_alert.svg' import './notificationView.scss' -const NotificationView = ({ item, isSuccessResponse, retry, transitionStyles }) => { +const NotificationView = ({ + item, + isSuccessResponse, + handleRemoveNotification, + retry, + transitionStyles +}) => { return createPortal(
+
* { + fill: $white; + } + } + } + &_icon { .success_icon { width: 14px; diff --git a/src/common/ProjectDetailsHeader/ProjectDetailsHeader.js b/src/common/ProjectDetailsHeader/ProjectDetailsHeader.js index 1841acaf2..874b1e49d 100644 --- a/src/common/ProjectDetailsHeader/ProjectDetailsHeader.js +++ b/src/common/ProjectDetailsHeader/ProjectDetailsHeader.js @@ -21,7 +21,7 @@ import React from 'react' import { Link, useLocation } from 'react-router-dom' import { getDateAndTimeByFormat } from '../../utils/' -import { PROJECT_MONITOR } from '../../constants' +import { PROJECT_MONITOR, PROJECT_QUICK_ACTIONS_PAGE } from '../../constants' import './ProjectDetailsHeader.scss' @@ -57,8 +57,8 @@ const ProjectDetailsHeader = ({ projectData, projectName }) => {
  • {location.pathname.includes(PROJECT_MONITOR) ? ( - - Project Home + + Project Quick Actions ) : ( diff --git a/src/common/RangeInput/RangeInput.js b/src/common/RangeInput/RangeInput.js index f402b8fd5..1430c0f58 100644 --- a/src/common/RangeInput/RangeInput.js +++ b/src/common/RangeInput/RangeInput.js @@ -122,8 +122,10 @@ const RangeInput = ({ min={min} max={max} onChange={value => { - setInputValue(Number(value)) - onChange(Number(value)) + const targetValue = value.length === 0 ? '' : Number(value) + + setInputValue(targetValue) + onChange(targetValue) }} tip={tip} required={required} diff --git a/src/common/ReactFlow/MlReactFlow.js b/src/common/ReactFlow/MlReactFlow.js index 46148c72b..d0d6aeff9 100644 --- a/src/common/ReactFlow/MlReactFlow.js +++ b/src/common/ReactFlow/MlReactFlow.js @@ -19,7 +19,7 @@ such restriction. */ import React, { useState, useEffect, useCallback } from 'react' import PropTypes from 'prop-types' -import ReactFlow, { ReactFlowProvider } from 'react-flow-renderer' +import ReactFlow, { ReactFlowProvider } from 'reactflow' import MlReactFlowNode from './MlReactFlowNode' import MlReactFlowEdge from './MlReactFlowEdge' @@ -36,7 +36,7 @@ const nodeTypes = { [ML_NODE]: MlReactFlowNode } -const MlReactFlow = ({ alignTriggerItem, elements, onElementClick }) => { +const MlReactFlow = ({ alignTriggerItem, edges, nodes, onNodeClick }) => { const domChangeHandler = () => { const edgesWrapper = document.querySelector('.react-flow__edges > g') const selectedEdges = edgesWrapper.getElementsByClassName('selected') @@ -51,17 +51,20 @@ const MlReactFlow = ({ alignTriggerItem, elements, onElementClick }) => { const handleFitGraphView = useCallback(() => { setTimeout(() => { reactFlowInstance.fitView() - const { position, zoom } = reactFlowInstance.toObject() - reactFlowInstance.setTransform({ x: position[0], y: 50, zoom: zoom }) + const { + viewport: { x, zoom } + } = reactFlowInstance.toObject() + + reactFlowInstance.setViewport({ x, y: 50, zoom: zoom }) }, 20) }, [reactFlowInstance]) useEffect(() => { - if (reactFlowInstance && !initialGraphViewGenerated && elements.length > 0) { + if (reactFlowInstance && !initialGraphViewGenerated && nodes.length > 0) { setInitialGraphViewGenerated(true) } - }, [elements.length, initialGraphViewGenerated, reactFlowInstance]) + }, [nodes.length, initialGraphViewGenerated, reactFlowInstance]) useEffect(() => { if (reactFlowInstance && initialGraphViewGenerated) { @@ -77,7 +80,7 @@ const MlReactFlow = ({ alignTriggerItem, elements, onElementClick }) => { } }, [observer]) - const onLoad = reactFlowInstance => { + const onInit = reactFlowInstance => { const edgesWrapper = document.querySelector('.react-flow__nodes') if (edgesWrapper) { @@ -94,14 +97,16 @@ const MlReactFlow = ({ alignTriggerItem, elements, onElementClick }) => { @@ -110,13 +115,14 @@ const MlReactFlow = ({ alignTriggerItem, elements, onElementClick }) => { MlReactFlow.defaultProps = { alignTriggerItem: '', - onElementClick: () => {} + onNodeClick: () => {} } MlReactFlow.propTypes = { alignTriggerItem: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - elements: PropTypes.arrayOf(PropTypes.shape({})).isRequired, - onElementClick: PropTypes.func + edges: PropTypes.arrayOf(PropTypes.shape({})).isRequired, + nodes: PropTypes.arrayOf(PropTypes.shape({})).isRequired, + onNodeClick: PropTypes.func } export default React.memo(MlReactFlow) diff --git a/src/common/ReactFlow/MlReactFlowEdge.js b/src/common/ReactFlow/MlReactFlowEdge.js index 5d7195a49..86133645f 100644 --- a/src/common/ReactFlow/MlReactFlowEdge.js +++ b/src/common/ReactFlow/MlReactFlowEdge.js @@ -17,13 +17,9 @@ 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 } from 'react' +import React, { useMemo, useCallback } from 'react' import PropTypes from 'prop-types' -import { - useStoreState, - getBezierPath, - getSmoothStepPath -} from 'react-flow-renderer' +import { BaseEdge, useNodes, getBezierPath, getSmoothStepPath } from 'reactflow' import { getEdgeParams, getMarkerEnd } from './mlReactFlow.util' import { @@ -47,26 +43,19 @@ const MlReactFlowEdge = ({ targetX, targetY }) => { - const nodes = useStoreState(state => state.nodes) - const markerEnd = getMarkerEnd(arrowHeadType, markerEndId, id) - - const sourceNode = useMemo(() => nodes.find(n => n.id === source), [ - source, - nodes - ]) - const targetNode = useMemo(() => nodes.find(n => n.id === target), [ - target, - nodes - ]) + const nodes = useNodes() + const markerEnd = useMemo( + () => getMarkerEnd(arrowHeadType, markerEndId, id), + [arrowHeadType, id, markerEndId] + ) + const sourceNode = useMemo(() => nodes.find(n => n.id === source), [source, nodes]) + const targetNode = useMemo(() => nodes.find(n => n.id === target), [target, nodes]) - const getPath = () => { - let d = null + const getPath = useCallback(() => { + let d = [] if (data.subType === FLOATING_EDGE) { - const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams( - sourceNode, - targetNode - ) + const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode) d = getBezierPath({ sourceX: sx, @@ -76,10 +65,7 @@ const MlReactFlowEdge = ({ targetX: tx, targetY: ty }) - } else if ( - data.subType === STEP_EDGE || - data.subType === SMOOTH_STEP_EDGE - ) { + } else if (data.subType === STEP_EDGE || data.subType === SMOOTH_STEP_EDGE) { d = getSmoothStepPath({ sourceX, sourceY, @@ -99,7 +85,9 @@ const MlReactFlowEdge = ({ } return d - } + }, [data.subType, sourceNode, sourceX, sourceY, targetNode, targetX, targetY]) + + const path = useMemo(() => getPath(), [getPath]) if (!sourceNode || !targetNode) { return null @@ -144,16 +132,7 @@ const MlReactFlowEdge = ({ /> - - - + ) } diff --git a/src/common/ReactFlow/MlReactFlowNode.js b/src/common/ReactFlow/MlReactFlowNode.js index c08f79247..774b0d743 100644 --- a/src/common/ReactFlow/MlReactFlowNode.js +++ b/src/common/ReactFlow/MlReactFlowNode.js @@ -19,17 +19,11 @@ such restriction. */ import React from 'react' import PropTypes from 'prop-types' -import { Handle } from 'react-flow-renderer' +import { Handle } from 'reactflow' import { Tooltip, TextTooltipTemplate } from 'igz-controls/components' -import { - ERROR_NODE, - INPUT_NODE, - OUTPUT_NODE, - PRIMARY_NODE, - SECONDARY_NODE -} from '../../constants' +import { ERROR_NODE, INPUT_NODE, OUTPUT_NODE, PRIMARY_NODE, SECONDARY_NODE } from '../../constants' const MlReactFlowNode = ({ data, isConnectable }) => { return ( @@ -46,15 +40,10 @@ const MlReactFlowNode = ({ data, isConnectable }) => { />
    -