diff --git a/package.json b/package.json index fffced7c7..b82241181 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.3", + "iguazio.dashboard-react-controls": "2.2.5", "is-wsl": "^1.1.0", "js-base64": "^2.5.2", "js-yaml": "^4.1.0", @@ -128,7 +128,7 @@ "body-parser": "^1.19.0", "case-sensitive-paths-webpack-plugin": "^2.4.0", "chai": "^4.3.4", - "chromedriver": "^128.0.0", + "chromedriver": "^130.0.0", "css-loader": "^6.5.1", "cucumber-html-reporter": "^5.3.0", "eslint": "^8.57.0", diff --git a/src/App.js b/src/App.js index 2da37c887..7cfb5afa6 100755 --- a/src/App.js +++ b/src/App.js @@ -59,7 +59,7 @@ import 'igz-controls/scss/common.scss' import './scss/main.scss' import { createPortal } from 'react-dom' -import Notification from './common/Notification/Notification' +import Notifications from './common/Notifications/Notifications' const Page = lazyRetry(() => import('./layout/Page/Page')) const Datasets = lazyRetry(() => import('./components/Datasets/Datasets')) @@ -298,7 +298,7 @@ const App = () => { }> - {createPortal(, document.getElementById('overlay_container'))} + {createPortal(, document.getElementById('overlay_container'))} ) diff --git a/src/actions/featureStore.js b/src/actions/featureStore.js index 8afa79e37..f306a6a30 100644 --- a/src/actions/featureStore.js +++ b/src/actions/featureStore.js @@ -66,7 +66,9 @@ import { SET_NEW_FEATURE_SET_TARGET, SET_NEW_FEATURE_SET_VERSION, START_FEATURE_SET_INGEST_BEGIN, - START_FEATURE_SET_INGEST_SUCCESS + START_FEATURE_SET_INGEST_SUCCESS, + FETCH_FEATURE_SET_BEGIN, + FETCH_FEATURE_SET_FAILURE } from '../constants' import { CONFLICT_ERROR_STATUS_CODE, FORBIDDEN_ERROR_STATUS_CODE } from 'igz-controls/constants' import { parseFeatureVectors } from '../utils/parseFeatureVectors' @@ -204,14 +206,14 @@ const featureStoreActions = { type: FETCH_FEATURE_SETS_SUCCESS, payload: featureSets }), - fetchFeatureSet: (project, featureSet, tag) => dispatch => { + fetchExpandedFeatureSet: (project, featureSet, tag) => dispatch => { return featureStoreApi - .getFeatureSet(project, featureSet, tag) + .getExpandedFeatureSet(project, featureSet, tag) .then(response => { const generatedFeatureSets = parseFeatureSets(response.data?.feature_sets) dispatch( - featureStoreActions.fetchFeatureSetSuccess({ + featureStoreActions.fetchExpandedFeatureSetSuccess({ [getFeatureSetIdentifier(generatedFeatureSets[0])]: generatedFeatureSets }) ) @@ -222,10 +224,35 @@ const featureStoreActions = { throw error }) }, - fetchFeatureSetSuccess: featureSets => ({ + fetchExpandedFeatureSetSuccess: featureSets => ({ type: FETCH_FEATURE_SET_SUCCESS, payload: featureSets }), + fetchFeatureSet: (project, featureSet, tag) => dispatch => { + dispatch(featureStoreActions.fetchFeatureSetBegin()) + + return featureStoreApi + .getFeatureSet(project, featureSet, tag) + .then(response => { + dispatch(featureStoreActions.fetchFeatureSetSuccess()) + + return parseFeatureSets(response.data?.feature_sets)[0] + }) + .catch(error => { + dispatch(featureStoreActions.fetchFeatureSetFailure(error.message)) + throw error + }) + }, + fetchFeatureSetBegin: () => ({ + type: FETCH_FEATURE_SET_BEGIN + }), + fetchFeatureSetFailure: error => ({ + type: FETCH_FEATURE_SET_FAILURE, + payload: error + }), + fetchFeatureSetSuccess: () => ({ + type: FETCH_FEATURE_SET_SUCCESS + }), fetchFeatureVector: (project, featureVector, tag) => dispatch => { return featureStoreApi .getFeatureVector(project, featureVector, tag) diff --git a/src/actions/projects.js b/src/actions/projects.js index 53cda6384..fe2d4c759 100644 --- a/src/actions/projects.js +++ b/src/actions/projects.js @@ -580,23 +580,25 @@ const projectsAction = { return summaryData }) .catch(err => { - if (!firstServerErrorTimestamp) { - firstServerErrorTimestamp = new Date() + if (mlrunUnhealthyErrors.includes(err.response?.status)) { + if (!firstServerErrorTimestamp) { + firstServerErrorTimestamp = new Date() - dispatch(projectsAction.setMlrunUnhealthyRetrying(true)) - } + dispatch(projectsAction.setMlrunUnhealthyRetrying(true)) + } - const threeMinutesPassed = (new Date() - firstServerErrorTimestamp) / 1000 > 180 + const threeMinutesPassed = (new Date() - firstServerErrorTimestamp) / 1000 > 180 - if (mlrunUnhealthyErrors.includes(err.response?.status) && !threeMinutesPassed) { - setTimeout(() => { - dispatch(projectsAction.fetchProjectsSummary(signal, refresh)) - }, 3000) - } + if (!threeMinutesPassed) { + setTimeout(() => { + dispatch(projectsAction.fetchProjectsSummary(signal, refresh)) + }, 3000) + } - if (threeMinutesPassed) { - dispatch(projectsAction.setMlrunIsUnhealthy(true)) - dispatch(projectsAction.setMlrunUnhealthyRetrying(true)) + if (threeMinutesPassed) { + dispatch(projectsAction.setMlrunIsUnhealthy(true)) + dispatch(projectsAction.setMlrunUnhealthyRetrying(true)) + } } dispatch(projectsAction.fetchProjectsSummaryFailure(err)) diff --git a/src/api/featureStore-api.js b/src/api/featureStore-api.js index f8e612b14..de103f3ba 100644 --- a/src/api/featureStore-api.js +++ b/src/api/featureStore-api.js @@ -27,7 +27,7 @@ import { } from '../constants' const fetchFeatureStoreContent = (path, filters, config = {}, withLatestTag, apiV2) => { - const params = {} + const params = { ...config.params } const httpClient = apiV2 ? mainHttpClientV2 : mainHttpClient if (filters?.labels) { @@ -66,7 +66,8 @@ const featureStoreApi = { mainHttpClient.post(`/projects/${data.metadata.project}/feature-vectors`, data), deleteFeatureVector: (project, featureVector) => mainHttpClient.delete(`/projects/${project}/feature-vectors/${featureVector}`), - fetchFeatureSetsTags: (project, config) => mainHttpClient.get(`/projects/${project}/feature-sets/*/tags`, config), + fetchFeatureSetsTags: (project, config) => + mainHttpClient.get(`/projects/${project}/feature-sets/*/tags`, config), fetchFeatureVectorsTags: (project, config) => mainHttpClient.get(`/projects/${project}/feature-vectors/*/tags`, config), getEntity: (project, entity) => @@ -75,9 +76,10 @@ const featureStoreApi = { }), getEntities: (project, filters, config) => fetchFeatureStoreContent(`/projects/${project}/entities`, filters, config ?? {}, true, true), - getFeatureSet: (project, featureSet, tag) => { + getExpandedFeatureSet: (project, featureSet, tag) => { const params = { - name: featureSet + name: featureSet, + format: 'minimal' } if (tag !== TAG_FILTER_ALL_ITEMS) { @@ -88,6 +90,13 @@ const featureStoreApi = { params }) }, + getFeatureSet: (project, featureSet, tag) => { + const params = { name: featureSet, tag } + + return mainHttpClient.get(`/projects/${project}/feature-sets`, { + params + }) + }, getFeatureSets: (project, filters, config) => { return fetchFeatureStoreContent( `/projects/${project}/${FEATURE_SETS_TAB}`, @@ -125,7 +134,13 @@ const featureStoreApi = { params: { name: feature } }), getFeatures: (project, filters, config) => - fetchFeatureStoreContent(`/projects/${project}/${FEATURES_TAB}`, filters, config ?? {}, true, true), + fetchFeatureStoreContent( + `/projects/${project}/${FEATURES_TAB}`, + filters, + config ?? {}, + true, + true + ), startIngest: (project, featureSet, reference, data) => mainHttpClient.post( `/projects/${project}/feature-sets/${featureSet}/references/${reference}/ingest`, diff --git a/src/api/workflow-api.js b/src/api/workflow-api.js index 18418b4af..8153c8c56 100644 --- a/src/api/workflow-api.js +++ b/src/api/workflow-api.js @@ -108,6 +108,9 @@ const workflowsApi = { } return mainHttpClient.get(`/projects/${project}/pipelines`, newConfig) + }, + rerunWorkflow: (project, workflowId) => { + return mainHttpClient.post(`projects/${project}/pipelines/${workflowId}/retry`) } } diff --git a/src/common/Accordion/Accordion.js b/src/common/Accordion/Accordion.js index 94e9b6203..06b74df33 100644 --- a/src/common/Accordion/Accordion.js +++ b/src/common/Accordion/Accordion.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, { useState, useEffect, useCallback } from 'react' +import React, { useState, useEffect, useCallback, useRef } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' @@ -35,7 +35,7 @@ const Accordion = ({ openByDefault = false }) => { const [open, setOpen] = useState(openByDefault) - const accordionRef = React.createRef() + const accordionRef = useRef() const handleOnBlur = useCallback( event => { diff --git a/src/common/Breadcrumbs/Breadcrumbs.js b/src/common/Breadcrumbs/Breadcrumbs.js index cfd3bf9c6..1b3c11816 100755 --- a/src/common/Breadcrumbs/Breadcrumbs.js +++ b/src/common/Breadcrumbs/Breadcrumbs.js @@ -17,37 +17,26 @@ 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, useCallback, useState, useMemo, useRef } from 'react' +import React, { useMemo, useRef, useState } from 'react' import PropTypes from 'prop-types' -import { useLocation, useParams, Link } from 'react-router-dom' -import classnames from 'classnames' -import { useDispatch, useSelector } from 'react-redux' +import { useLocation, useParams } from 'react-router-dom' -import BreadcrumbsDropdown from '../../elements/BreadcrumbsDropdown/BreadcrumbsDropdown' -import { RoundedIcon } from 'igz-controls/components' +import BreadcrumbsStep from './BreadcrumbsStep/BreadcrumbsStep' import { useMode } from '../../hooks/mode.hook' import { generateMlrunScreens, generateTabsList } from './breadcrumbs.util' -import { generateProjectsList } from '../../utils/projects' -import { scrollToElement } from '../../utils/scroll.util' -import projectsAction from '../../actions/projects' import { PROJECTS_PAGE_PATH } from '../../constants' -import { ReactComponent as ArrowIcon } from 'igz-controls/images/arrow.svg' - -import './breadcrums.scss' +import './breadcrumbs.scss' const Breadcrumbs = ({ onClick = () => {} }) => { + const [searchValue, setSearchValue] = useState('') 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() - const projectStore = useSelector(state => state.projectStore) - const dispatch = useDispatch() const mlrunScreens = useMemo(() => { return generateMlrunScreens(params, isDemoMode) @@ -56,11 +45,7 @@ const Breadcrumbs = ({ onClick = () => {} }) => { return generateTabsList() }, []) - const projectsList = useMemo(() => { - return generateProjectsList(projectStore.projectsNames.data) - }, [projectStore.projectsNames.data]) - - const urlItems = useMemo(() => { + const urlParts = useMemo(() => { if (params.projectName) { const [projects, projectName, screenName] = location.pathname.split('/').slice(1, 4) const screen = mlrunScreens.find(screen => screen.id === screenName) @@ -87,161 +72,28 @@ const Breadcrumbs = ({ onClick = () => {} }) => { } }, [location.pathname, params.projectName, mlrunScreens, projectTabs]) - const handleCloseDropdown = useCallback( - event => { - if (breadcrumbsRef.current && !breadcrumbsRef.current.contains(event.target)) { - const [activeSeparator] = document.getElementsByClassName('breadcrumbs__separator_active') - - if (activeSeparator) { - activeSeparator.classList.remove('breadcrumbs__separator_active') - } - - if (showScreensList) setShowScreensList(false) - - if (showProjectsList) setShowProjectsList(false) - } - - setSearchValue('') - }, - [breadcrumbsRef, showProjectsList, showScreensList] - ) - - const scrollProjectOptionToView = useCallback(() => { - scrollToElement(projectListRef, `#${params.projectName}`, searchValue) - }, [params.projectName, searchValue]) - - useEffect(() => { - if (showProjectsList && projectListRef.current) { - scrollProjectOptionToView() - } - }, [showProjectsList, scrollProjectOptionToView]) - - useEffect(() => { - window.addEventListener('click', handleCloseDropdown) - - return () => { - window.removeEventListener('click', handleCloseDropdown) - } - }, [handleCloseDropdown]) - - useEffect(() => { - if (projectsList.length === 0 && location.pathname !== '/projects') { - dispatch(projectsAction.fetchProjects({ format: 'minimal' })) - } - }, [dispatch, location.pathname, projectsList.length]) - - const handleSeparatorClick = (nextItem, separatorRef) => { - const nextItemIsScreen = Boolean(mlrunScreens.find(screen => screen.label === nextItem)) - - if (nextItemIsScreen || nextItem === params.projectName) { - const [activeSeparator] = document.getElementsByClassName('breadcrumbs__separator_active') - - if ( - activeSeparator && - !separatorRef.current.classList.contains('breadcrumbs__separator_active') - ) { - activeSeparator.classList.remove('breadcrumbs__separator_active') - } - - if (nextItemIsScreen) { - setShowScreensList(state => !state) - - if (showProjectsList) { - setShowProjectsList(false) - } - } - - if (nextItem === params.projectName) { - setShowProjectsList(state => !state) - - if (showScreensList) { - setShowScreensList(false) - } - } - - separatorRef.current.classList.toggle('breadcrumbs__separator_active') - } - } - - const handleSelectDropdownItem = separatorRef => { - if (showProjectsList) setShowProjectsList(false) - - if (showScreensList) setShowScreensList(false) - - separatorRef.current.classList.remove('breadcrumbs__separator_active') - } - return ( diff --git a/src/common/Breadcrumbs/BreadcrumbsStep/BreadcrumbsStep.js b/src/common/Breadcrumbs/BreadcrumbsStep/BreadcrumbsStep.js new file mode 100644 index 000000000..96d959855 --- /dev/null +++ b/src/common/Breadcrumbs/BreadcrumbsStep/BreadcrumbsStep.js @@ -0,0 +1,235 @@ +/* +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, useRef } from 'react' +import { Link, useLocation } from 'react-router-dom' +import { useDispatch, useSelector } from 'react-redux' +import classnames from 'classnames' + +import BreadcrumbsDropdown from '../../../elements/BreadcrumbsDropdown/BreadcrumbsDropdown' +import { RoundedIcon } from 'igz-controls/components' + +import { scrollToElement } from '../../../utils/scroll.util' +import projectsAction from '../../../actions/projects' +import { generateProjectsList } from '../../../utils/projects' + +import { ReactComponent as ArrowIcon } from 'igz-controls/images/arrow.svg' + +import './breadcrumbsStep.scss' + +const BreadcrumbsStep = React.forwardRef( + ( + { + index, + mlrunScreens, + onClick, + params, + searchValue, + setSearchValue, + setShowProjectsList, + setShowScreensList, + showProjectsList, + showScreensList, + urlPart, + urlParts + }, + ref + ) => { + const projectListRef = useRef() + const separatorRef = useRef() + const dispatch = useDispatch() + const location = useLocation() + const projectStore = useSelector(state => state.projectStore) + + const isParam = useMemo(() => Object.values(params ?? {}).includes(urlPart), [urlPart, params]) + const label = useMemo( + () => (isParam ? urlPart : urlPart.charAt(0).toUpperCase() + urlPart.slice(1)), + [urlPart, isParam] + ) + const to = useMemo( + () => `/${urlParts.pathItems.slice(0, index + 1).join('/')}`, + [index, urlParts.pathItems] + ) + const isLastStep = useMemo( + () => index === urlParts.pathItems.length - 1, + [index, urlParts.pathItems.length] + ) + const projectsList = useMemo(() => { + return generateProjectsList(projectStore.projectsNames.data) + }, [projectStore.projectsNames.data]) + + const separatorClassNames = classnames( + 'breadcrumbs__separator', + ((urlParts.pathItems[index + 1] === urlParts.screen?.id && !isParam) || + urlParts.pathItems[index + 1] === params.projectName) && + 'breadcrumbs__separator_tumbler' + ) + + const handleSelectDropdownItem = separatorRef => { + if (showProjectsList) setShowProjectsList(false) + + if (showScreensList) setShowScreensList(false) + + separatorRef.current.classList.remove('breadcrumbs__separator_active') + } + + const handleCloseDropdown = useCallback( + event => { + if (ref.current && !ref.current.contains(event.target)) { + const [activeSeparator] = document.getElementsByClassName('breadcrumbs__separator_active') + + if (activeSeparator) { + activeSeparator.classList.remove('breadcrumbs__separator_active') + } + + if (showScreensList) setShowScreensList(false) + + if (showProjectsList) setShowProjectsList(false) + } + + setSearchValue('') + }, + [ + ref, + setSearchValue, + setShowProjectsList, + setShowScreensList, + showProjectsList, + showScreensList + ] + ) + + const scrollProjectOptionToView = useCallback(() => { + scrollToElement(projectListRef, `#${params.projectName}`, searchValue) + }, [params.projectName, projectListRef, searchValue]) + + useEffect(() => { + if (showProjectsList && projectListRef.current) { + scrollProjectOptionToView() + } + }, [showProjectsList, scrollProjectOptionToView, projectListRef]) + + useEffect(() => { + window.addEventListener('click', handleCloseDropdown) + + return () => { + window.removeEventListener('click', handleCloseDropdown) + } + }, [handleCloseDropdown]) + + useEffect(() => { + if (projectsList.length === 0 && location.pathname !== '/projects') { + dispatch(projectsAction.fetchProjects({ format: 'minimal' })) + } + }, [dispatch, location.pathname, projectsList.length]) + + const handleSeparatorClick = (nextItem, separatorRef) => { + const nextItemIsScreen = Boolean(mlrunScreens.find(screen => screen.label === nextItem)) + + if (nextItemIsScreen || nextItem === params.projectName) { + const [activeSeparator] = document.getElementsByClassName('breadcrumbs__separator_active') + + if ( + activeSeparator && + !separatorRef.current.classList.contains('breadcrumbs__separator_active') + ) { + activeSeparator.classList.remove('breadcrumbs__separator_active') + } + + if (nextItemIsScreen) { + setShowScreensList(state => !state) + + if (showProjectsList) { + setShowProjectsList(false) + } + } + + if (nextItem === params.projectName) { + setShowProjectsList(state => !state) + + if (showScreensList) { + setShowScreensList(false) + } + } + + separatorRef.current.classList.toggle('breadcrumbs__separator_active') + } + } + + return ( + <> + {isLastStep ? ( +
  • + {label} +
  • + ) : ( + [ +
  • + + {label} + +
  • , +
  • + handleSeparatorClick(urlParts.pathItems[index + 1], separatorRef)} + > + + + {showScreensList && urlParts.pathItems[index + 1] === urlParts.screen?.label && ( + handleSelectDropdownItem(separatorRef)} + selectedItem={urlParts.screen?.id} + searchValue={searchValue} + setSearchValue={setSearchValue} + /> + )} + {showProjectsList && urlParts.pathItems[index + 1] === params.projectName && ( + <> + handleSelectDropdownItem(separatorRef)} + ref={projectListRef} + screen={urlParts.screen?.id} + selectedItem={params.projectName} + searchValue={searchValue} + setSearchValue={setSearchValue} + tab={urlParts.tab?.id} + withSearch + /> + + )} +
  • + ] + )} + + ) + } +) + +export default BreadcrumbsStep diff --git a/src/common/Breadcrumbs/BreadcrumbsStep/breadcrumbsStep.scss b/src/common/Breadcrumbs/BreadcrumbsStep/breadcrumbsStep.scss new file mode 100644 index 000000000..ab9cea791 --- /dev/null +++ b/src/common/Breadcrumbs/BreadcrumbsStep/breadcrumbsStep.scss @@ -0,0 +1,30 @@ +@import '~igz-controls/scss/colors'; + +.breadcrumbs__item { + position: relative; + max-width: 700px; + color: $topaz; + + &:last-child { + color: $mulledWine; + font-weight: 500; + font-size: 20px; + line-height: 23px; + } +} + +.breadcrumbs__separator { + display: flex; + margin: 0 8px; + color: $topaz; + transition: transform 0.2s linear; + user-select: none; + + &_tumbler { + cursor: pointer; + } + + &_active { + transform: rotate(90deg); + } +} diff --git a/src/common/Breadcrumbs/breadcrumbs.scss b/src/common/Breadcrumbs/breadcrumbs.scss new file mode 100644 index 000000000..0f4a1bc1f --- /dev/null +++ b/src/common/Breadcrumbs/breadcrumbs.scss @@ -0,0 +1,20 @@ +@import '~igz-controls/scss/mixins'; +@import '~igz-controls/scss/colors'; + +.breadcrumbs { + @include resetSpaces; + + font-size: 20px; + line-height: 23px; +} + +.breadcrumbs__list { + @include resetSpaces; + + display: flex; + flex-wrap: wrap; + align-items: center; + font-weight: 400; + letter-spacing: 0.00938em; + list-style-type: none; +} diff --git a/src/common/Breadcrumbs/breadcrums.scss b/src/common/Breadcrumbs/breadcrums.scss deleted file mode 100644 index 8409b5b31..000000000 --- a/src/common/Breadcrumbs/breadcrums.scss +++ /dev/null @@ -1,49 +0,0 @@ -@import '~igz-controls/scss/mixins'; -@import '~igz-controls/scss/colors'; - -.breadcrumbs { - @include resetSpaces; - - font-size: 20px; - line-height: 23px; - - &__list { - @include resetSpaces; - - display: flex; - flex-wrap: wrap; - align-items: center; - font-weight: 400; - letter-spacing: 0.00938em; - list-style-type: none; - } - - &__item { - position: relative; - max-width: 700px; - color: $topaz; - - &:last-child { - color: $mulledWine; - font-weight: 500; - font-size: 20px; - line-height: 23px; - } - } - - &__separator { - display: flex; - margin: 0 8px; - color: $topaz; - transition: transform 0.2s linear; - user-select: none; - - &_tumbler { - cursor: pointer; - } - - &_active { - transform: rotate(90deg); - } - } -} diff --git a/src/common/Chart/MlChart.js b/src/common/Chart/MlChart.js index 4575fbbfa..265d24d1d 100644 --- a/src/common/Chart/MlChart.js +++ b/src/common/Chart/MlChart.js @@ -27,10 +27,10 @@ import './mlChart.scss' Chart.register(...registerables) -const MlChart = ({ config }) => { +const MlChart = ({ config, showLoader = true }) => { const canvasRef = useRef() const [isLoading, setIsLoading] = useState(true) - const canvasClassNames = classnames(isLoading && 'hidden') + const canvasClassNames = classnames(showLoader && isLoading && 'hidden') useLayoutEffect(() => { const ctx = canvasRef.current.getContext('2d') @@ -53,7 +53,6 @@ const MlChart = ({ config }) => { }) } } - const mlChartInstance = new Chart(ctx, { ...chartConfig, options: { @@ -61,7 +60,7 @@ const MlChart = ({ config }) => { animation: { ...chartConfig.options.animation, onComplete: () => { - setIsLoading(false) + showLoader && setIsLoading(false) if (chartConfig?.options?.animation?.onComplete) { chartConfig.options.animation.onComplete() @@ -72,13 +71,13 @@ const MlChart = ({ config }) => { }) return () => { - mlChartInstance.destroy() + mlChartInstance?.destroy() } - }, [isLoading, config]) + }, [config, showLoader]) return (
    - {isLoading && } + {showLoader && isLoading && }
    ) diff --git a/src/common/ChipForm/ChipForm.js b/src/common/ChipForm/ChipForm.js index 5bed632c4..c5878e66f 100644 --- a/src/common/ChipForm/ChipForm.js +++ b/src/common/ChipForm/ChipForm.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, { useState, useCallback, useEffect, useLayoutEffect, useMemo } from 'react' +import React, { useState, useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' import { isEmpty } from 'lodash' @@ -55,9 +55,9 @@ const ChipForm = React.forwardRef( const minWidthInput = 25 const minWidthValueInput = 35 - const refInputKey = React.createRef() - const refInputValue = React.createRef() - const refInputContainer = React.createRef() + const refInputKey = useRef() + const refInputValue = useRef() + const refInputContainer = useRef() const labelKeyClassName = classnames( className, diff --git a/src/common/NameFilter/NameFilter.js b/src/common/NameFilter/NameFilter.js index cd7b6a975..49282edbf 100644 --- a/src/common/NameFilter/NameFilter.js +++ b/src/common/NameFilter/NameFilter.js @@ -28,7 +28,7 @@ import { ReactComponent as SearchIcon } from 'igz-controls/images/search.svg' import './nameFilter.scss' -const NameFilter = ({ applyChanges, filterMenuName= '' }) => { +const NameFilter = ({ applyChanges, filterMenuName = '' }) => { const { input } = useField(NAME_FILTER) const dispatch = useDispatch() @@ -36,7 +36,9 @@ const NameFilter = ({ applyChanges, filterMenuName= '' }) => { if (event.keyCode === KEY_CODES.ENTER) { applyChanges(event.target.value) if (filterMenuName) { - dispatch(setFiltersValues({ name: filterMenuName, value: { [NAME_FILTER]: event.target.value } })) + dispatch( + setFiltersValues({ name: filterMenuName, value: { [NAME_FILTER]: event.target.value } }) + ) } else { dispatch(setFilters({ [NAME_FILTER]: event.target.value })) } @@ -54,7 +56,7 @@ const NameFilter = ({ applyChanges, filterMenuName= '' }) => { } return ( -
    +
    { - const dispatch = useDispatch() - const notificationStore = useSelector(store => store.notificationStore) - - const defaultStyle = { - transform: 'translateY(130px)', - opacity: 0 - } - - const duration = 500 - - const handleRemoveNotification = itemId => { - dispatch(removeNotification(itemId)) - } - - const handleRetry = item => { - handleRemoveNotification(item.id) - item.retry(item) - } - - return ( -
    - - {notificationStore.notification.map(item => { - const nodeRef = React.createRef() - const isSuccessResponse = inRange(item.status, 200, 300) - const transitionStyles = { - entered: { - transform: 'translateY(0)', - opacity: 1, - transition: `transform ${duration}ms ease-in-out, opacity ${duration}ms ease-in-out` - }, - exiting: { - transform: 'translateY(130px)', - opacity: 0, - transition: `transform ${duration}ms ease-in-out, opacity ${duration}ms ease-in-out` - } - } - - return ( - { - setTimeout(() => { - handleRemoveNotification(item.id) - }, 10000) - }} - > - {state => ( - - )} - - ) - })} - - -
    - ) -} - -export default Notification diff --git a/src/common/Notification/NotificationView.js b/src/common/Notification/NotificationView.js deleted file mode 100644 index d96aebac7..000000000 --- a/src/common/Notification/NotificationView.js +++ /dev/null @@ -1,78 +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 from 'react' -import PropTypes from 'prop-types' - -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, - handleRemoveNotification, - retry, - transitionStyles -}) => { - return ( -
    -
    - -
    -
    - {isSuccessResponse ? : } -
    -
    - {item.message} - {!isSuccessResponse && item.retry && ( -
    { - retry(item) - }} - > - RETRY -
    - )} -
    -
    - ) -} - -NotificationView.propTypes = { - item: PropTypes.shape({}).isRequired, - isSuccessResponse: PropTypes.bool.isRequired, - handleRemoveNotification: PropTypes.func.isRequired, - retry: PropTypes.func.isRequired, - transitionStyles: PropTypes.shape({}).isRequired -} - -export default NotificationView diff --git a/src/common/Notification/notificationView.scss b/src/common/Notification/notificationView.scss deleted file mode 100644 index 470461f0c..000000000 --- a/src/common/Notification/notificationView.scss +++ /dev/null @@ -1,66 +0,0 @@ -@import '~igz-controls/scss/colors'; -@import '~igz-controls/scss/shadows'; - -.notifications-wrapper { - position: absolute; - right: 0; - bottom: 0; - display: flex; - flex: 1 1; - flex-direction: column; - - .notification { - position: relative; - z-index: 1000; - align-self: flex-end; - margin-right: 24px; - margin-bottom: 10px; - padding: 15px; - color: $white; - background-color: $darkPurple; - border-radius: 5px; - box-shadow: $tooltipShadow; - opacity: 0; - - &__body { - display: flex; - align-items: center; - - &__button-retry { - margin-left: 15px; - color: $portage; - cursor: pointer; - } - - &__icon { - .icon-success { - width: 14px; - height: 14px; - } - - .icon-alert { - width: 4px; - height: 14px; - } - } - - &__status { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - margin-right: 5px; - border-radius: 50%; - } - - &__icon-alert { - background-color: $burntSienna; - } - - &__icon-success { - background-color: $brightTurquoise; - } - } - } -} diff --git a/src/common/Notifications/Notification.js b/src/common/Notifications/Notification.js new file mode 100644 index 000000000..abb409382 --- /dev/null +++ b/src/common/Notifications/Notification.js @@ -0,0 +1,127 @@ +/* +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, { useMemo, useRef } from 'react' +import { useDispatch } from 'react-redux' +import { Transition } from 'react-transition-group' +import { inRange } from 'lodash' +import PropTypes from 'prop-types' + +import { removeNotification } from '../../reducers/notificationReducer' + +import { NOTIFICATION_DURATION } from '../../constants' + +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 './notification.scss' + +const Notification = ({ notification, ...rest }) => { + // rest is required for Transition + const dispatch = useDispatch() + const nodeRef = useRef() + + const defaultStyle = { + transform: 'translateY(130px)', + opacity: 0 + } + const transitionStyles = { + entered: { + transform: 'translateY(0)', + opacity: 1, + transition: `transform ${NOTIFICATION_DURATION}ms ease-in-out, opacity ${NOTIFICATION_DURATION}ms ease-in-out` + }, + exiting: { + transform: 'translateY(130px)', + opacity: 0, + transition: `transform ${NOTIFICATION_DURATION}ms ease-in-out, opacity ${NOTIFICATION_DURATION}ms ease-in-out` + } + } + + const isSuccessResponse = useMemo( + () => inRange(notification.status, 200, 300), + [notification.status] + ) + const handleRemoveNotification = itemId => { + dispatch(removeNotification(itemId)) + } + const handleRetry = item => { + handleRemoveNotification(item.id) + item.retry(item) + } + + return ( + { + setTimeout(() => { + handleRemoveNotification(notification.id) + }, 10000) + }} + {...rest} + > + {state => ( +
    +
    + +
    +
    + {isSuccessResponse ? : } +
    +
    + {notification.message} + {!isSuccessResponse && notification.retry && ( +
    { + handleRetry(notification) + }} + > + RETRY +
    + )} +
    +
    + )} +
    + ) +} + +Notification.prototype = { + notification: PropTypes.object.isRequired +} + +export default Notification diff --git a/src/common/Notifications/Notifications.js b/src/common/Notifications/Notifications.js new file mode 100644 index 000000000..c190a37f8 --- /dev/null +++ b/src/common/Notifications/Notifications.js @@ -0,0 +1,44 @@ +/* +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 from 'react' +import { TransitionGroup } from 'react-transition-group' +import { useSelector } from 'react-redux' + +import Notification from './Notification' +import DownloadContainer from '../Download/DownloadContainer' + +import './notifications.scss' + +const Notifications = () => { + const notificationStore = useSelector(store => store.notificationStore) + + return ( +
    + + {notificationStore.notification.map(notification => ( + + ))} + + +
    + ) +} + +export default Notifications diff --git a/src/common/Notifications/notification.scss b/src/common/Notifications/notification.scss new file mode 100644 index 000000000..f319a0acd --- /dev/null +++ b/src/common/Notifications/notification.scss @@ -0,0 +1,57 @@ +@import '~igz-controls/scss/colors'; +@import '~igz-controls/scss/shadows'; + +.notification { + position: relative; + z-index: 1000; + align-self: flex-end; + margin-right: 24px; + margin-bottom: 10px; + padding: 15px; + color: $white; + background-color: $darkPurple; + border-radius: 5px; + box-shadow: $tooltipShadow; + opacity: 0; + + &__body { + display: flex; + align-items: center; + + &__button-retry { + margin-left: 15px; + color: $portage; + cursor: pointer; + } + + &__icon { + .icon-success { + width: 14px; + height: 14px; + } + + .icon-alert { + width: 4px; + height: 14px; + } + } + + &__status { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + margin-right: 5px; + border-radius: 50%; + } + + &__icon-alert { + background-color: $burntSienna; + } + + &__icon-success { + background-color: $brightTurquoise; + } + } +} diff --git a/src/common/Notifications/notifications.scss b/src/common/Notifications/notifications.scss new file mode 100644 index 000000000..ebdda09b4 --- /dev/null +++ b/src/common/Notifications/notifications.scss @@ -0,0 +1,8 @@ +.notifications-wrapper { + position: absolute; + right: 0; + bottom: 0; + display: flex; + flex: 1 1; + flex-direction: column; +} diff --git a/src/common/TabsSlider/TabsSlider.js b/src/common/TabsSlider/TabsSlider.js index 2e4c82194..9d58c74ae 100644 --- a/src/common/TabsSlider/TabsSlider.js +++ b/src/common/TabsSlider/TabsSlider.js @@ -25,7 +25,7 @@ import classnames from 'classnames' import { Tip } from 'igz-controls/components' import { SLIDER_STYLE_1, SLIDER_STYLE_2, SLIDER_TABS } from '../../types' -import { generateUrlFromRouterPath } from '../../utils/link-helper.util' +import { generateUrlFromRouterPath } from '../../utils/link-helper.util' import { ReactComponent as Arrow } from 'igz-controls/images/arrow.svg' @@ -117,8 +117,8 @@ const TabsSlider = ({ } else if ( tabsRef.current?.scrollWidth < tabsWrapperRef.current?.offsetWidth / menuOffsetHalfWidth + - selectedTabNode?.offsetLeft + - selectedTabNode?.offsetWidth + selectedTabNode?.offsetLeft + + selectedTabNode?.offsetWidth ) { setScrolledWidth(tabsRef.current?.scrollWidth - tabsWrapperRef.current?.offsetWidth) setRightArrowDisabled(true) @@ -189,7 +189,9 @@ const TabsSlider = ({ onSelectTab(tab)} key={tab.id} > diff --git a/src/components/ActionBar/ActionBar.js b/src/components/ActionBar/ActionBar.js index 6408559c9..b4079b0d7 100644 --- a/src/components/ActionBar/ActionBar.js +++ b/src/components/ActionBar/ActionBar.js @@ -76,7 +76,7 @@ const ActionBar = ({ }) => { const [autoRefresh, setAutoRefresh] = useState(autoRefreshIsEnabled) const filtersStore = useSelector(store => store.filtersStore) - const filterMenu = useSelector(store => store.filtersStore[FILTER_MENU][filterMenuName].values) + const filterMenu = useSelector(store => store.filtersStore[FILTER_MENU][filterMenuName]?.values ?? {}) const filterMenuModal = useSelector( store => store.filtersStore[FILTER_MENU_MODAL][filterMenuName] ) @@ -204,7 +204,7 @@ const ActionBar = ({ ) handleRefresh({ ...formState.values, - ...filtersStore.filterMenuModal[filterMenuName].values + ...filtersStore.filterMenuModal[filterMenuName]?.values ?? {} }) } }, @@ -283,13 +283,13 @@ const ActionBar = ({ - applyChanges({ ...formState.values, name: value }, filterMenuModal.values) + applyChanges({ ...formState.values, name: value }, filterMenuModal?.values ?? {}) } />
    )} {DATES_FILTER in filterMenu && !filtersConfig[DATES_FILTER].hidden && ( -
    +
    {({ input }) => { return ( @@ -312,18 +312,18 @@ const ActionBar = ({
    )} + {filterMenuModal && ( + applyChanges(formState.values, filterMenuModal)} + filterMenuName={filterMenuName} + initialValues={filterMenuModalInitialState} + restartFormTrigger={`${tab}`} + values={filterMenuModal.values} + > + {children} + + )}
    - {filterMenuModal && ( - applyChanges(formState.values, filterMenuModal)} - filterMenuName={filterMenuName} - initialValues={filterMenuModalInitialState} - restartFormTrigger={`${tab}`} - values={filterMenuModal.values} - > - {children} - - )} {(withRefreshButton || !isEmpty(actionButtons)) && (
    {actionButtons.map( @@ -332,6 +332,7 @@ const ActionBar = ({ !actionButton.hidden && (actionButton.template || (