diff --git a/src/common/Download/Download.js b/src/common/Download/Download.js index 9c7e70f2c..4cefa9da6 100644 --- a/src/common/Download/Download.js +++ b/src/common/Download/Download.js @@ -20,6 +20,7 @@ such restriction. import React, { useState, useEffect, useCallback, useRef } from 'react' import PropTypes from 'prop-types' import axios from 'axios' +import classnames from 'classnames' import { useDispatch } from 'react-redux' import { useParams } from 'react-router-dom' @@ -34,10 +35,14 @@ import colors from 'igz-controls/scss/colors.scss' const DEFAULT_FILE_NAME = 'mlrun-file' -const Download = ({ fileName, path, user }) => { +const Download = ({ disabled, fileName, path, user }) => { const [progress, setProgress] = useState(0) const [isDownload, setDownload] = useState(false) const params = useParams() + const downloadContainerClassnames = classnames( + 'download-container', + disabled && 'download-container_disabled' + ) const downloadRef = useRef(null) const dispatch = useDispatch() @@ -149,14 +154,17 @@ const Download = ({ fileName, path, user }) => { }, [downloadCallback, downloadRef]) const handleClick = () => { - if (downloadRef.current?.cancel) { - return downloadRef.current.cancel('cancel') + if (!disabled) { + if (downloadRef.current?.cancel) { + return downloadRef.current.cancel('cancel') + } + + setDownload(!isDownload) } - setDownload(!isDownload) } return ( -
+
{ {!isDownload ? ( - + { setDataInputState(prev => ({ @@ -189,7 +203,7 @@ const TargetPath = ({ const projectItem = dataInputState.projectItem if (dataInputState.inputProjectItemPathEntered && storePathType && projectName && projectItem) { - if (storePathType === 'artifacts' && dataInputState.artifactsReferences.length === 0) { + if (storePathType !== 'feature-vectors' && dataInputState.artifactsReferences.length === 0) { dispatch(fetchArtifact({ project: projectName, artifact: projectItem })) .unwrap() .then(artifacts => { @@ -238,6 +252,10 @@ const TargetPath = ({ setDataInputState ]) + const generatedPathTips = useMemo(() => { + return pathTips(dataInputState.storePathType) + }, [dataInputState.storePathType]) + return ( <> { - const pathType = storePathType === 'feature-vectors' ? 'feature-vector' : 'artifact' +export const pathTips = projectItem => { + const pathType = + projectItem === 'feature-vectors' + ? 'feature-vector' + : projectItem === 'artifacts' + ? 'artifact' + : 'dataset' return { [MLRUN_STORAGE_INPUT_PATH_SCHEME]: `${pathType}s/my-project/my-${pathType}:my-tag" or "${pathType}s/my-project/my-${pathType}@my-uid`, @@ -70,6 +75,10 @@ export const storePathTypes = [ label: 'Artifacts', id: 'artifacts' }, + { + label: 'Datasets', + id: 'datasets' + }, { label: 'Feature vectors', id: 'feature-vectors' @@ -80,11 +89,11 @@ export const handleStoreInputPathChange = (targetPathState, setTargetPathState, const pathItems = value.split('/') const [projectItem, projectItemReference] = getParsedResource(pathItems[2]) const projectItems = - targetPathState[pathItems[0] === 'artifacts' ? 'artifacts' : 'featureVectors'] + targetPathState[pathItems[0] !== 'feature-vectors' ? 'artifacts' : 'featureVectors'] const projectItemIsEntered = projectItems.find(project => project.id === projectItem) const projectItemsReferences = targetPathState[ - pathItems[0] === 'artifacts' ? 'artifactsReferences' : 'featureVectorsReferences' + pathItems[0] !== 'feature-vectors' ? 'artifactsReferences' : 'featureVectorsReferences' ] const projectItemReferenceIsEntered = projectItemsReferences.find( projectItemRef => projectItemRef.id === projectItemReference @@ -206,23 +215,12 @@ export const generateComboboxMatchesList = ( } else if (!inputProjectPathEntered && storePathTypes.some(type => type.id === storePathType)) { return projects.filter(proj => proj.id.startsWith(project)) } else if (!inputProjectItemPathEntered) { - const selectedStorePathType = storePathType - const projectItems = - selectedStorePathType === 'artifacts' - ? artifacts - : selectedStorePathType === 'feature-vectors' - ? featureVectors - : null + const projectItems = storePathType === 'feature-vectors' ? featureVectors : artifacts return projectItems ? projectItems.filter(projItem => projItem.id.startsWith(projectItem)) : [] } else if (!inputProjectItemReferencePathEntered) { - const selectedStorePathType = storePathType const projectItemsReferences = - selectedStorePathType === 'artifacts' - ? artifactsReferences - : selectedStorePathType === 'feature-vectors' - ? featureVectorsReferences - : null + storePathType === 'feature-vectors' ? featureVectorsReferences : artifactsReferences return projectItemsReferences ? projectItemsReferences.filter(projectItem => @@ -234,8 +232,8 @@ export const generateComboboxMatchesList = ( } } -export const generateArtifactsList = artifacts => - artifacts +export const generateArtifactsList = artifacts => { + const generatedArtifacts = artifacts .map(artifact => { const key = artifact.link_iteration ? artifact.link_iteration.db_key : artifact.key ?? '' return { @@ -246,8 +244,11 @@ export const generateArtifactsList = artifacts => .filter(artifact => artifact.label !== '') .sort((prevArtifact, nextArtifact) => prevArtifact.id.localeCompare(nextArtifact.id)) -export const generateArtifactsReferencesList = artifacts => - artifacts + return uniqBy(generatedArtifacts, 'id') +} + +export const generateArtifactsReferencesList = artifacts => { + const generatedArtifacts = artifacts .map(artifact => { const artifactReference = getArtifactReference(artifact) @@ -268,3 +269,6 @@ export const generateArtifactsReferencesList = artifacts => return prevRefTree.localeCompare(nextRefTree) } }) + + return uniqBy(generatedArtifacts, 'id') +} diff --git a/src/components/Datasets/Datasets.js b/src/components/Datasets/Datasets.js index 6557d3251..ae094c88b 100644 --- a/src/components/Datasets/Datasets.js +++ b/src/components/Datasets/Datasets.js @@ -74,6 +74,7 @@ const Datasets = () => { const navigate = useNavigate() const location = useLocation() const dispatch = useDispatch() + const frontendSpec = useSelector(store => store.appStore.frontendSpec) const detailsFormInitialValues = useMemo( () => ({ @@ -185,10 +186,11 @@ const Datasets = () => { setSelectedRowData, filtersStore.iter, filtersStore.tag, - params.projectName + params.projectName, + frontendSpec ) }, - [dispatch, filtersStore.iter, filtersStore.tag, params.projectName] + [dispatch, filtersStore.iter, filtersStore.tag, frontendSpec, params.projectName] ) const handleRemoveRowData = useCallback( @@ -219,10 +221,12 @@ const Datasets = () => { const tableContent = useMemo(() => { return filtersStore.groupBy === GROUP_BY_NAME ? latestItems.map(contentItem => { - return createDatasetsRowData(contentItem, params.projectName, true) + return createDatasetsRowData(contentItem, params.projectName, frontendSpec, true) }) - : datasets.map(contentItem => createDatasetsRowData(contentItem, params.projectName)) - }, [datasets, filtersStore.groupBy, latestItems, params.projectName]) + : datasets.map(contentItem => + createDatasetsRowData(contentItem, params.projectName, frontendSpec) + ) + }, [datasets, filtersStore.groupBy, frontendSpec, latestItems, params.projectName]) useEffect(() => { dispatch(removeDataSet({})) diff --git a/src/components/Datasets/datasets.util.js b/src/components/Datasets/datasets.util.js index 7888d532e..94ee25945 100644 --- a/src/components/Datasets/datasets.util.js +++ b/src/components/Datasets/datasets.util.js @@ -105,7 +105,8 @@ export const fetchDataSetRowData = async ( setSelectedRowData, iter, tag, - projectName + projectName, + frontendSpec ) => { const dataSetIdentifier = getArtifactIdentifier(dataSet) @@ -124,7 +125,9 @@ export const fetchDataSetRowData = async ( return { ...state, [dataSetIdentifier]: { - content: result.map(artifact => createDatasetsRowData(artifact, projectName)), + content: result.map(artifact => + createDatasetsRowData(artifact, projectName, frontendSpec) + ), error: null, loading: false } diff --git a/src/components/FeatureSetsPanel/FeatureSetsPanelDataSource/FeatureSetsPanelDataSource.js b/src/components/FeatureSetsPanel/FeatureSetsPanelDataSource/FeatureSetsPanelDataSource.js index c473253e1..1a2aac80b 100644 --- a/src/components/FeatureSetsPanel/FeatureSetsPanelDataSource/FeatureSetsPanelDataSource.js +++ b/src/components/FeatureSetsPanel/FeatureSetsPanelDataSource/FeatureSetsPanelDataSource.js @@ -25,7 +25,11 @@ import PropTypes from 'prop-types' import FeatureSetsPanelDataSourceView from './FeatureSetsPanelDataSourceView' import featureStoreActions from '../../../actions/featureStore' -import { MLRUN_STORAGE_INPUT_PATH_SCHEME } from '../../../constants' +import { + ARTIFACT_OTHER_TYPE, + DATASET_TYPE, + MLRUN_STORAGE_INPUT_PATH_SCHEME +} from '../../../constants' import { getParsedResource } from '../../../utils/resources' import { CSV, @@ -95,7 +99,18 @@ const FeatureSetsPanelDataSource = ({ useEffect(() => { if (urlProjectItemTypeEntered && urlProjectPathEntered && artifacts.length === 0) { - dispatch(fetchArtifacts({ project: data.url.project })) + dispatch( + fetchArtifacts({ + project: data.url.project, + filters: null, + config: { + params: { + category: + data.url.projectItemType === 'artifacts' ? ARTIFACT_OTHER_TYPE : DATASET_TYPE + } + } + }) + ) .unwrap() .then(artifacts => { if (artifacts?.length > 0) { @@ -106,6 +121,7 @@ const FeatureSetsPanelDataSource = ({ }, [ artifacts.length, data.url.project, + data.url.projectItemType, dispatch, urlProjectItemTypeEntered, urlProjectPathEntered diff --git a/src/components/FeatureSetsPanel/FeatureSetsPanelDataSource/FeatureSetsPanelDataSourceView.js b/src/components/FeatureSetsPanel/FeatureSetsPanelDataSource/FeatureSetsPanelDataSourceView.js index ad7349319..e37286098 100644 --- a/src/components/FeatureSetsPanel/FeatureSetsPanelDataSource/FeatureSetsPanelDataSourceView.js +++ b/src/components/FeatureSetsPanel/FeatureSetsPanelDataSource/FeatureSetsPanelDataSourceView.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 from 'react' +import React, { useMemo } from 'react' import PropTypes from 'prop-types' import cronstrue from 'cronstrue' @@ -29,15 +29,10 @@ import ScheduleFeatureSet from '../ScheduleFeatureSet/ScheduleFeatureSet' import Select from '../../../common/Select/Select' import { Button } from 'igz-controls/components' -import { - comboboxSelectList, - CSV, - kindOptions, - PARQUET -} from './featureSetsPanelDataSource.util' +import { comboboxSelectList, CSV, kindOptions, PARQUET } from './featureSetsPanelDataSource.util' import { MLRUN_STORAGE_INPUT_PATH_SCHEME } from '../../../constants' import { SECONDARY_BUTTON } from 'igz-controls/constants' -import { pathPlaceholders } from '../../../utils/panelPathScheme' +import { pathTips } from '../../../utils/panelPathScheme' import { ReactComponent as Pencil } from 'igz-controls/images/edit.svg' @@ -61,6 +56,10 @@ const FeatureSetsPanelDataSourceView = ({ urlProjectItemTypeEntered, validation }) => { + const generatedPathTips = useMemo(() => { + return pathTips(data.url.projectItemType) + }, [data.url.projectItemType]) + return (
@@ -78,18 +77,14 @@ const FeatureSetsPanelDataSourceView = ({ comboboxClassName="url" hideSearchInput={!urlProjectItemTypeEntered} inputDefaultValue={ - data.url.pathType === MLRUN_STORAGE_INPUT_PATH_SCHEME - ? data.url.projectItemType - : '' + data.url.pathType === MLRUN_STORAGE_INPUT_PATH_SCHEME ? data.url.projectItemType : '' } inputOnChange={path => { handleUrlPathChange(path) }} inputPlaceholder={data.url.placeholder} invalid={!validation.isUrlValid} - invalidText={`Field must be in "${ - pathPlaceholders[data.url.pathType] - }" format`} + invalidText={`Field must be in "${generatedPathTips[data.url.pathType]}" format`} matches={comboboxMatches} maxSuggestedMatches={3} onBlur={handleUrlOnBlur} @@ -109,9 +104,7 @@ const FeatureSetsPanelDataSourceView = ({ className="schedule-tumbler" label={ <> - {data.schedule - ? cronstrue.toString(data.schedule) - : 'Schedule'} + {data.schedule ? cronstrue.toString(data.schedule) : 'Schedule'} } @@ -136,10 +129,7 @@ const FeatureSetsPanelDataSourceView = ({ invalid={!validation.isParseDatesValid} label="Parse Dates" onBlur={event => { - if ( - featureStore.newFeatureSet.spec.source.parse_dates !== - event.target.value - ) { + if (featureStore.newFeatureSet.spec.source.parse_dates !== event.target.value) { setNewFeatureSetDataSourceParseDates(event.target.value) } }} @@ -150,17 +140,12 @@ const FeatureSetsPanelDataSourceView = ({ })) } placeholder="col_name1,col_name2,..." - setInvalid={value => - setValidation(state => ({ ...state, isParseDatesValid: value })) - } + setInvalid={value => setValidation(state => ({ ...state, isParseDatesValid: value }))} type="text" /> )} {data.kind === PARQUET && ( - + )}
diff --git a/src/components/FeatureSetsPanel/FeatureSetsPanelDataSource/featureSetsPanelDataSource.util.js b/src/components/FeatureSetsPanel/FeatureSetsPanelDataSource/featureSetsPanelDataSource.util.js index c55cff1e6..1b77cde8f 100644 --- a/src/components/FeatureSetsPanel/FeatureSetsPanelDataSource/featureSetsPanelDataSource.util.js +++ b/src/components/FeatureSetsPanel/FeatureSetsPanelDataSource/featureSetsPanelDataSource.util.js @@ -76,9 +76,7 @@ export const generateComboboxMatchesList = ( urlProjectItemTypeEntered ) => { if (!urlProjectItemTypeEntered) { - return projectItemsPathTypes.some(type => - type.id.startsWith(url.projectItemType) - ) + return projectItemsPathTypes.some(type => type.id.startsWith(url.projectItemType)) ? projectItemsPathTypes : [] } else if ( @@ -105,20 +103,19 @@ export const projectItemsPathTypes = [ { label: 'Artifacts', id: 'artifacts' + }, + { + label: 'Datasets', + id: 'datasets' } ] -export const isUrlInputValid = ( - pathInputType, - pathInputValue, - dataSourceKind -) => { +export const isUrlInputValid = (pathInputType, pathInputValue, dataSourceKind) => { const regExp = dataSourceKind === CSV - ? /^artifacts\/(.+?)\/(.+?)(#(.+?))?(:(.+?))?(@(.+))?(? 0 && /.*?\/(.*?)/.test(pathInputValue) + ? /^artifacts|datasets\/(.+?)\/(.+?)(#(.+?))?(:(.+?))?(@(.+))?(? 0 && /.*?\/(.*?)/.test(pathInputValue) switch (pathInputType) { case MLRUN_STORAGE_INPUT_PATH_SCHEME: diff --git a/src/components/FeatureSetsPanel/FeatureSetsPanelDataSourceTable/FeatureSetsPanelDataSourceTable.js b/src/components/FeatureSetsPanel/FeatureSetsPanelDataSourceTable/FeatureSetsPanelDataSourceTable.js deleted file mode 100644 index 6cd08cc01..000000000 --- a/src/components/FeatureSetsPanel/FeatureSetsPanelDataSourceTable/FeatureSetsPanelDataSourceTable.js +++ /dev/null @@ -1,19 +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. -*/ diff --git a/src/components/FeatureSetsPanel/FeatureSetsPanelTargetStore/FeatureSetsPanelTargetStore.js b/src/components/FeatureSetsPanel/FeatureSetsPanelTargetStore/FeatureSetsPanelTargetStore.js index 4b6cb7ab9..6f5011eaa 100644 --- a/src/components/FeatureSetsPanel/FeatureSetsPanelTargetStore/FeatureSetsPanelTargetStore.js +++ b/src/components/FeatureSetsPanel/FeatureSetsPanelTargetStore/FeatureSetsPanelTargetStore.js @@ -274,9 +274,11 @@ const FeatureSetsPanelTargetStore = ({ const handleDiscardPathChange = kind => { const currentStoreType = kind === ONLINE ? NOSQL : kind - const currentKind = featureStore.newFeatureSet.spec.targets.find(el => el.kind === currentStoreType) + const currentKind = featureStore.newFeatureSet.spec.targets.find( + el => el.kind === currentStoreType + ) - if (currentKind .path.length > 0) { + if (currentKind.path.length > 0) { setData(state => ({ ...state, [kind]: { @@ -564,57 +566,57 @@ const FeatureSetsPanelTargetStore = ({ } const triggerPartitionCheckbox = (id, kind) => { - if (kind === EXTERNAL_OFFLINE || kind === PARQUET) { - setData(state => { - let path = state[kind].path - - if ( - kind === PARQUET && - !targetsPathEditData.parquet.isEditMode && - !targetsPathEditData.parquet.isModified - ) { - path = generatePath( - frontendSpec.feature_store_data_prefixes, - project, - data[kind].kind, - featureStore.newFeatureSet.metadata.name, - data[kind].partitioned ? PARQUET : '' - ) - } + setData(state => { + let path = state[kind].path + + if ( + kind === PARQUET && + !targetsPathEditData.parquet.isEditMode && + !targetsPathEditData.parquet.isModified + ) { + path = generatePath( + frontendSpec.feature_store_data_prefixes, + project, + data[kind].kind, + featureStore.newFeatureSet.metadata.name, + data[kind].partitioned ? PARQUET : '' + ) + } else if (kind === PARQUET && targetsPathEditData.parquet.isModified) { + path = state[kind].partitioned ? `${path}.parquet` : path.replace(/\.[^.]+$/, '') + } - return data[kind]?.partitioned - ? { - ...state, - [kind]: { - ...dataInitialState[kind], - path, - kind: PARQUET - } + return data[kind]?.partitioned + ? { + ...state, + [kind]: { + ...dataInitialState[kind], + path, + kind: PARQUET } - : { - ...state, - [kind]: { - ...state[kind], - path, - partitioned: state[kind].partitioned === id ? '' : id, - key_bucketing_number: '', - partition_cols: '', - time_partitioning_granularity: 'hour' - } + } + : { + ...state, + [kind]: { + ...state[kind], + path, + partitioned: state[kind].partitioned === id ? '' : id, + key_bucketing_number: '', + partition_cols: '', + time_partitioning_granularity: 'hour' } - }) + } + }) - if (data[kind].partitioned) { - setShowAdvanced(state => ({ ...state, [kind]: false })) - setPartitionRadioButtonsState(state => ({ - ...state, - [kind]: 'districtKeys' - })) - setSelectedPartitionKind(state => ({ - ...state, - [kind]: [...selectedPartitionKindInitialState[kind]] - })) - } + if (data[kind].partitioned) { + setShowAdvanced(state => ({ ...state, [kind]: false })) + setPartitionRadioButtonsState(state => ({ + ...state, + [kind]: 'districtKeys' + })) + setSelectedPartitionKind(state => ({ + ...state, + [kind]: [...selectedPartitionKindInitialState[kind]] + })) } const targets = cloneDeep(featureStore.newFeatureSet.spec.targets).map(targetKind => { diff --git a/src/components/FeatureSetsPanel/FeatureSetsPanelTargetStore/FeatureSetsPanelTargetStoreView.js b/src/components/FeatureSetsPanel/FeatureSetsPanelTargetStore/FeatureSetsPanelTargetStoreView.js index 9c9ab59ab..541151efc 100644 --- a/src/components/FeatureSetsPanel/FeatureSetsPanelTargetStore/FeatureSetsPanelTargetStoreView.js +++ b/src/components/FeatureSetsPanel/FeatureSetsPanelTargetStore/FeatureSetsPanelTargetStoreView.js @@ -34,7 +34,9 @@ import { externalOfflineKindOptions, ONLINE, PARQUET, - checkboxModels + checkboxModels, + isParquetPathValid, + getInvalidParquetPathMessage } from './featureSetsPanelTargetStore.util' import { ReactComponent as Online } from 'igz-controls/images/nosql.svg' @@ -178,14 +180,18 @@ const FeatureSetsPanelTargetStoreView = ({ density="normal" floatingLabel focused={frontendSpecIsNotEmpty} - invalid={!validation.isOfflineTargetPathValid} + invalid={isParquetPathValid( + validation.isOfflineTargetPathValid, + data.parquet + )} + invalidText={getInvalidParquetPathMessage(data.parquet)} label="Path" - onChange={path => + onChange={path => { setData(state => ({ ...state, parquet: { ...state.parquet, path } })) - } + }} placeholder={ 'v3io:///projects/{project}/FeatureStore/{name}/{run_id}/parquet/sets/{name}.parquet' } @@ -201,7 +207,14 @@ const FeatureSetsPanelTargetStoreView = ({ wrapperClassName="offline-path" />
- + )} triggerPartitionCheckbox(id, PARQUET)} selectedId={data.parquet.partitioned} diff --git a/src/components/FeatureSetsPanel/FeatureSetsPanelTargetStore/featureSetsPanelTargetStore.scss b/src/components/FeatureSetsPanel/FeatureSetsPanelTargetStore/featureSetsPanelTargetStore.scss index f75800957..7a23bfa27 100644 --- a/src/components/FeatureSetsPanel/FeatureSetsPanelTargetStore/featureSetsPanelTargetStore.scss +++ b/src/components/FeatureSetsPanel/FeatureSetsPanelTargetStore/featureSetsPanelTargetStore.scss @@ -82,7 +82,7 @@ .partition-fields { width: 100%; - margin-bottom: 10px; + margin: 10px 0; &__checkbox-container { display: flex; diff --git a/src/components/FeatureSetsPanel/FeatureSetsPanelTargetStore/featureSetsPanelTargetStore.util.js b/src/components/FeatureSetsPanel/FeatureSetsPanelTargetStore/featureSetsPanelTargetStore.util.js index 83e2c9fa3..2464a53b9 100644 --- a/src/components/FeatureSetsPanel/FeatureSetsPanelTargetStore/featureSetsPanelTargetStore.util.js +++ b/src/components/FeatureSetsPanel/FeatureSetsPanelTargetStore/featureSetsPanelTargetStore.util.js @@ -218,3 +218,19 @@ export const handlePathChange = ( })) } } + +export const isParquetPathValid = (validation, parquet) => { + return ( + !validation || + Boolean(parquet.partitioned && /\.\w*\s*$/.test(parquet.path)) || + Boolean(!parquet.partitioned && !/\.parquet\s*$|\.pq\s*$/.test(parquet.path)) + ) +} + +export const getInvalidParquetPathMessage = parquet => { + return parquet.partitioned && /\.\w*\s*$/.test(parquet.path) + ? 'The partitioned Parquet target for storey engine must be a directory. (The directory name must not end in .parquet/.pq.)' + : !parquet.partitioned && !/\.parquet\s*$|\.pq\s*$/.test(parquet.path) + ? 'The Parquet target for storey engine file path must have a .parquet/.pq suffix.' + : 'This field is invalid.' +} diff --git a/src/components/Files/Files.js b/src/components/Files/Files.js index 5bbc2d132..1ccf04a10 100644 --- a/src/components/Files/Files.js +++ b/src/components/Files/Files.js @@ -74,6 +74,7 @@ const Files = () => { const dispatch = useDispatch() const filesRef = useRef(null) const pageData = useMemo(() => generatePageData(selectedFile), [selectedFile]) + const frontendSpec = useSelector(store => store.appStore.frontendSpec) const detailsFormInitialValues = useMemo( () => ({ @@ -172,10 +173,11 @@ const Files = () => { dispatch, params.projectName, filtersStore.iter, - filtersStore.tag + filtersStore.tag, + frontendSpec ) }, - [dispatch, filtersStore.iter, filtersStore.tag, params.projectName] + [dispatch, filtersStore.iter, filtersStore.tag, frontendSpec, params.projectName] ) const { latestItems, handleExpandRow } = useGroupContent( @@ -190,10 +192,10 @@ const Files = () => { const tableContent = useMemo(() => { return filtersStore.groupBy === GROUP_BY_NAME ? latestItems.map(contentItem => { - return createFilesRowData(contentItem, params.projectName, true) + return createFilesRowData(contentItem, params.projectName, frontendSpec, true) }) - : files.map(contentItem => createFilesRowData(contentItem, params.projectName)) - }, [files, filtersStore.groupBy, latestItems, params.projectName]) + : files.map(contentItem => createFilesRowData(contentItem, params.projectName, frontendSpec)) + }, [files, filtersStore.groupBy, frontendSpec, latestItems, params.projectName]) const applyDetailsChanges = useCallback( changes => { diff --git a/src/components/Files/files.util.js b/src/components/Files/files.util.js index c7c6bf5e9..b0254498a 100644 --- a/src/components/Files/files.util.js +++ b/src/components/Files/files.util.js @@ -100,7 +100,15 @@ export const filters = [ ] export const actionsMenuHeader = 'Register artifact' -export const fetchFilesRowData = (file, setSelectedRowData, dispatch, projectName, iter, tag) => { +export const fetchFilesRowData = ( + file, + setSelectedRowData, + dispatch, + projectName, + iter, + tag, + frontendSpec +) => { const fileIdentifier = getArtifactIdentifier(file) setSelectedRowData(state => ({ @@ -117,7 +125,9 @@ export const fetchFilesRowData = (file, setSelectedRowData, dispatch, projectNam setSelectedRowData(state => ({ ...state, [fileIdentifier]: { - content: result.map(artifact => createFilesRowData(artifact, projectName)), + content: result.map(artifact => + createFilesRowData(artifact, projectName, frontendSpec) + ), error: null, loading: false } diff --git a/src/components/JobsPanelDataInputs/JobsPanelDataInputs.js b/src/components/JobsPanelDataInputs/JobsPanelDataInputs.js index 00dbe32c2..71c7dee0b 100644 --- a/src/components/JobsPanelDataInputs/JobsPanelDataInputs.js +++ b/src/components/JobsPanelDataInputs/JobsPanelDataInputs.js @@ -42,7 +42,7 @@ import { } from './jobsPanelDataInputs.util' import featureStoreActions from '../../actions/featureStore' import { isEveryObjectValueEmpty } from '../../utils/isEveryObjectValueEmpty' -import { MLRUN_STORAGE_INPUT_PATH_SCHEME } from '../../constants' +import { ARTIFACT_OTHER_TYPE, DATASET_TYPE, MLRUN_STORAGE_INPUT_PATH_SCHEME } from '../../constants' import { getFeatureReference, getParsedResource } from '../../utils/resources' import { generateArtifactsList, @@ -112,10 +112,21 @@ const JobsPanelDataInputs = ({ useEffect(() => { const storePathType = getInputValue('storePathType') const projectName = getInputValue('project') + const projectItem = getInputValue('projectItem') if (inputsState.inputProjectPathEntered && storePathType && projectName) { - if (storePathType === 'artifacts' && inputsState.artifacts.length === 0) { - dispatch(fetchArtifacts({ project: projectName })) + if (storePathType !== 'feature-vectors' && inputsState.artifacts.length === 0) { + dispatch( + fetchArtifacts({ + project: projectName, + filters: null, + config: { + params: { + category: projectItem === 'artifacts' ? ARTIFACT_OTHER_TYPE : DATASET_TYPE + } + } + }) + ) .unwrap() .then(artifacts => { inputsDispatch({ @@ -156,7 +167,7 @@ const JobsPanelDataInputs = ({ const projectItem = getInputValue('projectItem') if (inputsState.inputProjectItemPathEntered && storePathType && projectName && projectItem) { - if (storePathType === 'artifacts' && inputsState.artifactsReferences.length === 0) { + if (storePathType !== 'feature-vectors' && inputsState.artifactsReferences.length === 0) { dispatch(fetchArtifact({ project: projectName, artifact: projectItem })) .unwrap() .then(artifacts => { diff --git a/src/components/JobsPanelDataInputs/jobsPanelDataInputs.util.js b/src/components/JobsPanelDataInputs/jobsPanelDataInputs.util.js index 3203a6692..33f44be0c 100644 --- a/src/components/JobsPanelDataInputs/jobsPanelDataInputs.util.js +++ b/src/components/JobsPanelDataInputs/jobsPanelDataInputs.util.js @@ -50,9 +50,7 @@ export const generateComboboxMatchesList = ( selectedDataInputPath ) => { if (!inputStorePathTypeEntered) { - return storePathTypes.some(type => - type.id.startsWith(newInput.path.storePathType) - ) + return storePathTypes.some(type => type.id.startsWith(newInput.path.storePathType)) ? storePathTypes : [] } else if ( @@ -71,32 +69,24 @@ export const generateComboboxMatchesList = ( } else if (!inputProjectItemPathEntered) { const selectedStorePathType = newInput.path.storePathType || selectedDataInputPath.value.split('/')[0] - const projectItems = - selectedStorePathType === 'artifacts' - ? artifacts - : selectedStorePathType === 'feature-vectors' - ? featureVectors - : null + const projectItems = selectedStorePathType === 'feature-vectors' ? featureVectors : artifacts return projectItems ? projectItems.filter(projectItem => { return isEveryObjectValueEmpty(selectedDataInputPath) ? projectItem.id.startsWith(newInput.path.projectItem) - : projectItem.id.startsWith( - selectedDataInputPath.value.split('/')[2] - ) + : projectItem.id.startsWith(selectedDataInputPath.value.split('/')[2]) }) : [] } else if (!inputProjectItemReferencePathEntered) { const selectedStorePathType = newInput.path.storePathType || selectedDataInputPath.value.split('/')[0] const projectItemsReferences = - selectedStorePathType === 'artifacts' + selectedStorePathType !== 'feature-vectors' ? artifactsReferences : selectedStorePathType === 'feature-vectors' ? featureVectorsReferences : null - return projectItemsReferences ? projectItemsReferences.filter(projectItem => { return isEveryObjectValueEmpty(selectedDataInputPath) @@ -125,8 +115,7 @@ export const handleAddItem = ( newInputUrlPath, setDataInputsValidations ) => { - const isMlRunStorePath = - newItemObj.path.pathType === MLRUN_STORAGE_INPUT_PATH_SCHEME + const isMlRunStorePath = newItemObj.path.pathType === MLRUN_STORAGE_INPUT_PATH_SCHEME let mlRunStorePath = '' if (isMlRunStorePath) { @@ -145,9 +134,7 @@ export const handleAddItem = ( if (newItemObj.name.length === 0 || !pathInputIsValid) { setDataInputsValidations({ - isNameValid: - !isNameNotUnique(newItemObj.name, dataInputs) && - newItemObj.name.length > 0, + isNameValid: !isNameNotUnique(newItemObj.name, dataInputs) && newItemObj.name.length > 0, isPathValid: pathInputIsValid }) } else { @@ -208,15 +195,9 @@ export const handleEdit = ( if (selectedItem.newDataInputName) { delete currentDataObj[selectedItem.name] - currentDataObj[selectedItem.newDataInputName] = joinDataOfArrayOrObject( - selectedItem.path, - '' - ) + currentDataObj[selectedItem.newDataInputName] = joinDataOfArrayOrObject(selectedItem.path, '') } else { - currentDataObj[selectedItem.name] = joinDataOfArrayOrObject( - selectedItem.path, - '' - ) + currentDataObj[selectedItem.name] = joinDataOfArrayOrObject(selectedItem.path, '') } setCurrentPanelData({ ...currentDataObj }) @@ -242,10 +223,7 @@ export const handleEdit = ( }) } -export const resetDataInputsData = ( - inputsDispatch, - setDataInputsValidations -) => { +export const resetDataInputsData = (inputsDispatch, setDataInputsValidations) => { inputsDispatch({ type: inputsActions.REMOVE_NEW_INPUT_DATA }) @@ -287,15 +265,11 @@ export const handleDelete = ( setCurrentPanelData({ ...newInputs }) panelDispatch({ type: setPreviousPanelData, - payload: previousPanelData.filter( - dataItem => dataItem.data.name !== selectedItem.data.name - ) + payload: previousPanelData.filter(dataItem => dataItem.data.name !== selectedItem.data.name) }) panelDispatch({ type: setCurrentTableData, - payload: currentTableData.filter( - dataItem => dataItem.data.name !== selectedItem.data.name - ) + payload: currentTableData.filter(dataItem => dataItem.data.name !== selectedItem.data.name) }) } @@ -342,6 +316,10 @@ export const storePathTypes = [ label: 'Artifacts', id: 'artifacts' }, + { + label: 'Datasets', + id: 'datasets' + }, { label: 'Feature vectors', id: 'feature-vectors' @@ -389,10 +367,7 @@ export const handleInputPathTypeChange = ( export const handleInputPathChange = (inputsDispatch, inputsState, path) => { if (inputsState.newInput.path.pathType === MLRUN_STORAGE_INPUT_PATH_SCHEME) { - if ( - path.length === 0 && - inputsState.newInputDefaultPathProject.length > 0 - ) { + if (path.length === 0 && inputsState.newInputDefaultPathProject.length > 0) { inputsDispatch({ type: inputsActions.SET_NEW_INPUT_DEFAULT_PATH_PROJECT, payload: '' @@ -408,12 +383,7 @@ export const handleInputPathChange = (inputsDispatch, inputsState, path) => { } } -export const handleStoreInputPathChange = ( - isNewInput, - inputsDispatch, - inputsState, - path -) => { +export const handleStoreInputPathChange = (isNewInput, inputsDispatch, inputsState, path) => { const pathItems = path.split('/') const [projectItem, projectItemReference] = getParsedResource(pathItems[2]) @@ -457,29 +427,22 @@ export const handleStoreInputPathChange = ( storePathType: pathItems[0] ?? inputsState.newInput.path.storePathType, project: pathItems[1] ?? inputsState.newInput.path.project, projectItem: projectItem ?? inputsState.newInput.path.projectItem, - projectItemReference: - projectItemReference ?? inputsState.newInput.path.projectItemReference + projectItemReference: projectItemReference ?? inputsState.newInput.path.projectItemReference } }) } const projectItems = - inputsState[pathItems[0] === 'artifacts' ? 'artifacts' : 'featureVectors'] - const projectItemIsEntered = projectItems.find( - project => project.id === projectItem - ) + inputsState[pathItems[0] !== 'feature-vectors' ? 'artifacts' : 'featureVectors'] + const projectItemIsEntered = projectItems.find(project => project.id === projectItem) const projectItemsReferences = inputsState[ - pathItems[0] === 'artifacts' - ? 'artifactsReferences' - : 'featureVectorsReferences' + pathItems[0] !== 'feature-vectors' ? 'artifactsReferences' : 'featureVectorsReferences' ] const projectItemReferenceIsEntered = projectItemsReferences.find( projectItemRef => projectItemRef.id === projectItemReference ) - const isInputStorePathTypeValid = storePathTypes.some(type => - type.id.startsWith(pathItems[0]) - ) + const isInputStorePathTypeValid = storePathTypes.some(type => type.id.startsWith(pathItems[0])) inputsDispatch({ type: inputsActions.SET_INPUT_STORE_PATH_TYPE_ENTERED, @@ -505,7 +468,7 @@ export const isPathInputValid = (pathInputType, pathInputValue) => { case MLRUN_STORAGE_INPUT_PATH_SCHEME: return ( valueIsNotEmpty && - /^(artifacts|feature-vectors)\/(.+?)\/(.+?)(#(.+?))?(:(.+?))?(@(.+))?$/.test( + /^(artifacts|datasets|feature-vectors)\/(.+?)\/(.+?)(#(.+?))?(:(.+?))?(@(.+))?$/.test( pathInputValue ) ) @@ -516,12 +479,3 @@ export const isPathInputValid = (pathInputType, pathInputValue) => { return valueIsNotEmpty } } - -export const pathTips = { - [MLRUN_STORAGE_INPUT_PATH_SCHEME]: - 'artifacts/my-project/my-artifact:my-tag" or "artifacts/my-project/my-artifact@my-uid', - [S3_INPUT_PATH_SCHEME]: 'bucket/path', - [GOOGLE_STORAGE_INPUT_PATH_SCHEME]: 'bucket/path', - [AZURE_STORAGE_INPUT_PATH_SCHEME]: 'container/path', - [V3IO_INPUT_PATH_SCHEME]: 'container-name/file' -} diff --git a/src/components/ModelsPage/Models/Models.js b/src/components/ModelsPage/Models/Models.js index 195b40d89..a14326265 100644 --- a/src/components/ModelsPage/Models/Models.js +++ b/src/components/ModelsPage/Models/Models.js @@ -79,6 +79,7 @@ const Models = ({ fetchModelFeatureVector }) => { const modelsRef = useRef(null) const pageData = useMemo(() => generatePageData(selectedModel), [selectedModel]) const { fetchData, models, setModels, toggleConvertedYaml } = useModelsPage() + const frontendSpec = useSelector(store => store.appStore.frontendSpec) const detailsFormInitialValues = useMemo( () => ({ @@ -171,10 +172,11 @@ const Models = ({ fetchModelFeatureVector }) => { setSelectedRowData, filtersStore.iter, filtersStore.tag, - params.projectName + params.projectName, + frontendSpec ) }, - [dispatch, filtersStore.iter, filtersStore.tag, params.projectName] + [dispatch, filtersStore.iter, filtersStore.tag, frontendSpec, params.projectName] ) const applyDetailsChanges = useCallback( @@ -224,10 +226,12 @@ const Models = ({ fetchModelFeatureVector }) => { const tableContent = useMemo(() => { return filtersStore.groupBy === GROUP_BY_NAME ? latestItems.map(contentItem => { - return createModelsRowData(contentItem, params.projectName, true) + return createModelsRowData(contentItem, params.projectName, frontendSpec, true) }) - : models.map(contentItem => createModelsRowData(contentItem, params.projectName)) - }, [filtersStore.groupBy, latestItems, models, params.projectName]) + : models.map(contentItem => + createModelsRowData(contentItem, params.projectName, frontendSpec) + ) + }, [filtersStore.groupBy, frontendSpec, latestItems, models, params.projectName]) const { sortTable, selectedColumnName, getSortingIcon, sortedTableContent } = useSortTable({ headers: tableContent[0]?.content, diff --git a/src/components/ModelsPage/Models/models.util.js b/src/components/ModelsPage/Models/models.util.js index a3d89456f..b7c7a85a2 100644 --- a/src/components/ModelsPage/Models/models.util.js +++ b/src/components/ModelsPage/Models/models.util.js @@ -80,7 +80,8 @@ export const fetchModelsRowData = async ( setSelectedRowData, iter, tag, - projectName + projectName, + frontendSpec ) => { const modelIdentifier = getArtifactIdentifier(model) @@ -99,7 +100,9 @@ export const fetchModelsRowData = async ( return { ...state, [modelIdentifier]: { - content: result.map(artifact => createModelsRowData(artifact, projectName)), + content: result.map(artifact => + createModelsRowData(artifact, projectName, frontendSpec) + ), error: null, loading: false } diff --git a/src/components/Table/Table.js b/src/components/Table/Table.js index fa37344a0..42ab1e35c 100644 --- a/src/components/Table/Table.js +++ b/src/components/Table/Table.js @@ -68,6 +68,7 @@ const Table = ({ const { isStagingMode } = useMode() const params = useParams() const tableStore = useSelector(store => store.tableStore) + const frontendSpec = useSelector(store => store.appStore.frontendSpec) useEffect(() => { const calculatePanelHeight = () => { @@ -103,6 +104,7 @@ const Table = ({ pageData.page, tableStore.isTablePanelOpen, params, + frontendSpec, isStagingMode, !isEveryObjectValueEmpty(selectedItem) ) @@ -131,7 +133,8 @@ const Table = ({ pageData.mainRowItemsCount, pageData.page, selectedItem, - tableStore.isTablePanelOpen + tableStore.isTablePanelOpen, + frontendSpec ]) return ( diff --git a/src/components/Table/table.scss b/src/components/Table/table.scss index 19c22cd24..1e7eba9d1 100644 --- a/src/components/Table/table.scss +++ b/src/components/Table/table.scss @@ -252,7 +252,14 @@ font-size: 15px; background-color: transparent; border: none; - cursor: pointer; + + &:disabled { + cursor: default; + } + + &:not(:disabled) { + cursor: pointer; + } } .expand-arrow { @@ -318,7 +325,7 @@ &:first-child { position: sticky; top: 50px; - z-index: 1; + z-index: 3; background-color: $white; .table-body__cell { diff --git a/src/constants.js b/src/constants.js index 418f04e17..46eb6bc34 100644 --- a/src/constants.js +++ b/src/constants.js @@ -182,6 +182,8 @@ export const FETCH_FUNCTION_TEMPLATE_SUCCESS = 'FETCH_FUNCTION_TEMPLATE_SUCCESS' export const FUNCTION_TYPE_JOB = 'job' export const FUNCTION_TYPE_LOCAL = 'local' export const FUNCTION_TYPE_SERVING = 'serving' +export const FUNCTION_TYPE_NUCLIO = 'nuclio' +export const FUNCTION_TYPE_REMOTE = 'remote' export const GET_FUNCTION_BEGIN = 'GET_FUNCTION_BEGIN' export const GET_FUNCTION_FAILURE = 'GET_FUNCTION_FAILURE' export const GET_FUNCTION_SUCCESS = 'GET_FUNCTION_SUCCESS' diff --git a/src/elements/EditableDataInputsRow/EditableDataInputsRow.js b/src/elements/EditableDataInputsRow/EditableDataInputsRow.js index e861f4d7a..55cddbc0b 100644 --- a/src/elements/EditableDataInputsRow/EditableDataInputsRow.js +++ b/src/elements/EditableDataInputsRow/EditableDataInputsRow.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, { useCallback, useEffect, useRef, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import PropTypes from 'prop-types' import classNames from 'classnames' @@ -25,18 +25,12 @@ import Combobox from '../../common/Combobox/Combobox' import Input from '../../common/Input/Input' import { Tooltip, TextTooltipTemplate } from 'igz-controls/components' -import { - comboboxSelectList, - pathTips -} from '../../components/JobsPanelDataInputs/jobsPanelDataInputs.util' +import { comboboxSelectList } from '../../components/JobsPanelDataInputs/jobsPanelDataInputs.util' import { inputsActions } from '../../components/JobsPanelDataInputs/jobsPanelDataInputsReducer' import { MLRUN_STORAGE_INPUT_PATH_SCHEME } from '../../constants' -import { - applyEditButtonHandler, - handleEditInputPath -} from './EditableDataInputsRow.utils' +import { applyEditButtonHandler, handleEditInputPath } from './EditableDataInputsRow.utils' import { isNameNotUnique } from '../../components/JobsPanel/jobsPanel.util' -import { pathPlaceholders } from '../../utils/panelPathScheme' +import { pathPlaceholders, pathTips } from '../../utils/panelPathScheme' import { ReactComponent as Checkmark } from 'igz-controls/images/checkmark.svg' @@ -64,24 +58,15 @@ const EditableDataInputsRow = ({ }) const tableRowRef = useRef(null) - const comboboxClassNames = classNames( - 'input-row__item', - requiredField.path && 'required' - ) - const inputNameClassNames = classNames( - 'input', - requiredField.name && 'input_required' - ) + const comboboxClassNames = classNames('input-row__item', requiredField.path && 'required') + const inputNameClassNames = classNames('input', requiredField.name && 'input_required') useEffect(() => { if (selectDefaultValue.label?.length === 0) { setSelectDefaultValue({ id: selectedDataInput.data.path.pathType, label: selectedDataInput.data.path.pathType, - className: `path-type-${selectedDataInput.data.path.pathType?.replace( - /:\/\/.*$/g, - '' - )}` + className: `path-type-${selectedDataInput.data.path.pathType?.replace(/:\/\/.*$/g, '')}` }) } }, [selectDefaultValue.label, selectedDataInput.data.path.pathType]) @@ -143,6 +128,10 @@ const EditableDataInputsRow = ({ }) } + const generatedPathTips = useMemo(() => { + return pathTips(selectedDataInput.data.path.projectItem) + }, [selectedDataInput.data.path.projectItem]) + return (
{selectedDataInput.isDefault ? ( @@ -155,8 +144,7 @@ const EditableDataInputsRow = ({ className={inputNameClassNames} density="dense" invalid={ - inputName !== selectedDataInput.data.name && - isNameNotUnique(inputName, content) + inputName !== selectedDataInput.data.name && isNameNotUnique(inputName, content) } invalidText="Name already exists" onChange={name => { @@ -182,12 +170,11 @@ const EditableDataInputsRow = ({ inputPlaceholder={inputsState.pathPlaceholder} invalid={!isPathValid} invalidText={`Field must be in "${ - pathTips[selectedDataInput.data.path.pathType] + generatedPathTips[selectedDataInput.data.path.pathType] }" format`} matches={comboboxMatchesList} maxSuggestedMatches={ - inputsState.selectedDataInput.data.path.pathType === - MLRUN_STORAGE_INPUT_PATH_SCHEME + inputsState.selectedDataInput.data.path.pathType === MLRUN_STORAGE_INPUT_PATH_SCHEME ? 3 : 2 } @@ -214,8 +201,7 @@ const EditableDataInputsRow = ({ - + +
) } else if (data.type === 'buttonDownload') { return (
- }> +
@@ -148,7 +149,7 @@ const TableCell = ({ } else if (data.type === BUTTON_COPY_URI_CELL_TYPE) { return (
- data.actionHandler(item)}> + data.actionHandler(item)}>
diff --git a/src/reducers/artifactsReducer.js b/src/reducers/artifactsReducer.js index e53d44583..3c0033b0a 100644 --- a/src/reducers/artifactsReducer.js +++ b/src/reducers/artifactsReducer.js @@ -86,9 +86,11 @@ export const fetchArtifact = createAsyncThunk('fetchArtifact', ({ project, artif return filterArtifacts(data.artifacts) }) }) -export const fetchArtifacts = createAsyncThunk('fetchArtifacts', ({ project, filters }) => { - return artifactsApi.getArtifacts(project, filters).then(({ data }) => { - return filterArtifacts(data.artifacts) +export const fetchArtifacts = createAsyncThunk('fetchArtifacts', ({ project, filters, config }) => { + return artifactsApi.getArtifacts(project, filters, config).then(({ data }) => { + const result = parseArtifacts(data.artifacts) + + return generateArtifacts(filterArtifacts(result)) }) }) export const fetchArtifactTags = createAsyncThunk('fetchArtifactTags', ({ project, category }) => { diff --git a/src/reducers/projectReducer.js b/src/reducers/projectReducer.js index 29f05bc88..62e73a127 100644 --- a/src/reducers/projectReducer.js +++ b/src/reducers/projectReducer.js @@ -251,59 +251,7 @@ const projectReducer = (state = initialState, { type, payload }) => { error: null, loading: false, project: { - data: null, - error: null, - loading: false, - dataSets: { - data: null, - error: null, - loading: false - }, - failedJobs: { - data: [], - error: null, - loading: false - }, - featureSets: { - data: null, - error: null, - loading: false - }, - files: { - data: null, - error: null, - loading: false - }, - jobs: { - data: null, - error: null, - loading: false - }, - functions: { - data: null, - error: null, - loading: false - }, - models: { - data: [], - error: null, - loading: false - }, - runningJobs: { - data: [], - error: null, - loading: false - }, - scheduledJobs: { - data: [], - error: null, - loading: false - }, - workflows: { - data: [], - error: null, - loading: false - } + ...initialState.project } } case FETCH_PROJECT_BEGIN: diff --git a/src/utils/createArtifactsContent.js b/src/utils/createArtifactsContent.js index 03d1ebfa1..a6a33cb1a 100644 --- a/src/utils/createArtifactsContent.js +++ b/src/utils/createArtifactsContent.js @@ -43,21 +43,21 @@ import { ReactComponent as SeverityOk } from 'igz-controls/images/severity-ok.sv import { ReactComponent as SeverityWarning } from 'igz-controls/images/severity-warning.svg' import { ReactComponent as SeverityError } from 'igz-controls/images/severity-error.svg' -export const createArtifactsContent = (artifacts, page, pageTab, project) => { +export const createArtifactsContent = (artifacts, page, pageTab, project, frontendSpec) => { return (artifacts.filter(artifact => !artifact.link_iteration) ?? []).map(artifact => { if (page === ARTIFACTS_PAGE) { return createArtifactsRowData(artifact) } else if (page === MODELS_PAGE) { if (pageTab === MODELS_TAB) { - return createModelsRowData(artifact, project) + return createModelsRowData(artifact, project, frontendSpec) } else if (pageTab === MODEL_ENDPOINTS_TAB) { return createModelEndpointsRowData(artifact, project) } } else if (page === FILES_PAGE) { - return createFilesRowData(artifact, project) + return createFilesRowData(artifact, project, frontendSpec) } - return createDatasetsRowData(artifact, project) + return createDatasetsRowData(artifact, project, frontendSpec) }) } @@ -104,8 +104,17 @@ const createArtifactsRowData = artifact => { } } -export const createModelsRowData = (artifact, project, showExpandButton) => { - const iter = isNaN(parseInt(artifact?.iter)) ? '' : ` #${artifact?.iter}` +const getIter = artifact => (isNaN(parseInt(artifact?.iter)) ? '' : ` #${artifact?.iter}`) +const getIsTargetPathValid = (artifact, frontendSpec) => + frontendSpec?.allowed_artifact_path_prefixes_list + ? frontendSpec.allowed_artifact_path_prefixes_list.some(prefix => { + return artifact.target_path?.startsWith?.(prefix) + }) + : false + +export const createModelsRowData = (artifact, project, frontendSpec, showExpandButton) => { + const iter = getIter(artifact) + const isTargetPathValid = getIsTargetPathValid(artifact, frontendSpec) return { data: { @@ -215,14 +224,16 @@ export const createModelsRowData = (artifact, project, showExpandButton) => { headerId: 'popupt', value: '', class: 'table-cell-icon', - type: 'buttonPopout' + type: 'buttonPopout', + disabled: !isTargetPathValid }, { id: `buttonDownload.${artifact.ui.identifierUnique}`, headerId: 'download', value: '', class: 'table-cell-icon', - type: 'buttonDownload' + type: 'buttonDownload', + disabled: !isTargetPathValid }, { id: `buttonCopy.${artifact.ui.identifierUnique}`, @@ -236,8 +247,9 @@ export const createModelsRowData = (artifact, project, showExpandButton) => { } } -export const createFilesRowData = (artifact, project, showExpandButton) => { - const iter = isNaN(parseInt(artifact?.iter)) ? '' : ` #${artifact?.iter}` +export const createFilesRowData = (artifact, project, frontendSpec, showExpandButton) => { + const iter = getIter(artifact) + const isTargetPathValid = getIsTargetPathValid(artifact, frontendSpec) return { data: { @@ -331,14 +343,16 @@ export const createFilesRowData = (artifact, project, showExpandButton) => { headerId: 'popout', value: '', class: 'table-cell-icon', - type: 'buttonPopout' + type: 'buttonPopout', + disabled: !isTargetPathValid }, { id: `buttonDownload.${artifact.ui.identifierUnique}`, headerId: 'download', value: '', class: 'table-cell-icon', - type: 'buttonDownload' + type: 'buttonDownload', + disabled: !isTargetPathValid }, { id: `buttonCopy.${artifact.ui.identifierUnique}`, @@ -374,7 +388,7 @@ export const createModelEndpointsRowData = (artifact, project) => { ? `store://functions/${artifact.spec.function_uri}` : '' const { key: functionName } = parseUri(functionUri) - const averageLatency = artifact.status?.metrics?.latency_avg_1h?.values?.[0]?.[1] + const averageLatency = artifact.status?.metrics?.real_time?.latency_avg_1h?.[0]?.[1] return { data: { @@ -477,8 +491,9 @@ export const createModelEndpointsRowData = (artifact, project) => { } } -export const createDatasetsRowData = (artifact, project, showExpandButton) => { - const iter = isNaN(parseInt(artifact?.iter)) ? '' : ` #${artifact?.iter}` +export const createDatasetsRowData = (artifact, project, frontendSpec, showExpandButton) => { + const iter = getIter(artifact) + const isTargetPathValid = getIsTargetPathValid(artifact, frontendSpec) return { data: { @@ -565,14 +580,16 @@ export const createDatasetsRowData = (artifact, project, showExpandButton) => { headerId: 'popout', value: '', class: 'table-cell-icon', - type: 'buttonPopout' + type: 'buttonPopout', + disabled: !isTargetPathValid }, { id: `buttonDownload.${artifact.ui.identifierUnique}`, headerId: 'download', value: '', class: 'table-cell-icon', - type: 'buttonDownload' + type: 'buttonDownload', + disabled: !isTargetPathValid }, { id: `buttonCopy.${artifact.ui.identifierUnique}`, diff --git a/src/utils/createFunctionsContent.js b/src/utils/createFunctionsContent.js index e7eb8327e..0b4a0e622 100644 --- a/src/utils/createFunctionsContent.js +++ b/src/utils/createFunctionsContent.js @@ -18,7 +18,14 @@ under the Apache 2.0 license is conditioned upon your compliance with such restriction. */ import { formatDatetime } from './datetime' -import { FUNCTIONS_PAGE, MODELS_PAGE, REAL_TIME_PIPELINES_TAB } from '../constants' +import { + FUNCTION_TYPE_NUCLIO, + FUNCTION_TYPE_REMOTE, + FUNCTION_TYPE_SERVING, + FUNCTIONS_PAGE, + MODELS_PAGE, + REAL_TIME_PIPELINES_TAB +} from '../constants' import { generateLinkToDetailsPanel } from './generateLinkToDetailsPanel' const createFunctionsContent = (functions, pageTab, projectName, showExpandButton) => @@ -140,7 +147,12 @@ const createFunctionsContent = (functions, pageTab, projectName, showExpandButto id: `image.${func.ui.identifierUnique}`, headerId: 'image', headerLabel: 'Image', - value: func.image, + value: + func.type === FUNCTION_TYPE_NUCLIO || + func.type === FUNCTION_TYPE_SERVING || + func.type === FUNCTION_TYPE_REMOTE + ? func.container_image + : func.image, class: 'table-cell-1' }, { diff --git a/src/utils/generateTableContent.js b/src/utils/generateTableContent.js index 81f04c99c..c21913c9f 100644 --- a/src/utils/generateTableContent.js +++ b/src/utils/generateTableContent.js @@ -46,6 +46,7 @@ export const generateTableContent = ( page, isTablePanelOpen, params, + frontendSpec, isStagingMode, isSelectedItem ) => { @@ -59,7 +60,7 @@ export const generateTableContent = ( ? createFunctionsContent(group, isSelectedItem, params.pageTab, params.projectName, true) : page === FEATURE_STORE_PAGE ? createFeatureStoreContent(group, params.pageTab, params.projectName, isTablePanelOpen) - : createArtifactsContent(group, page, params.pageTab, params.projectName) + : createArtifactsContent(group, page, params.pageTab, params.projectName, frontendSpec) ) } else if (!isEmpty(content) && (groupFilter === GROUP_BY_NONE || !groupFilter)) { return page === CONSUMER_GROUP_PAGE @@ -70,7 +71,7 @@ export const generateTableContent = ( page === FILES_PAGE || page === DATASETS_PAGE || (page === MODELS_PAGE && params.pageTab !== REAL_TIME_PIPELINES_TAB) - ? createArtifactsContent(content, page, params.pageTab, params.projectName) + ? createArtifactsContent(content, page, params.pageTab, params.projectName, frontendSpec) : page === FEATURE_STORE_PAGE ? createFeatureStoreContent(content, params.pageTab, params.projectName, isTablePanelOpen) : createFunctionsContent(content, isSelectedItem, params) diff --git a/src/utils/panelPathScheme.js b/src/utils/panelPathScheme.js index 4c0a43b8c..f7f58bd90 100644 --- a/src/utils/panelPathScheme.js +++ b/src/utils/panelPathScheme.js @@ -25,6 +25,7 @@ import { S3_INPUT_PATH_SCHEME, V3IO_INPUT_PATH_SCHEME } from '../constants' +import { uniqBy } from 'lodash' export const generateProjectsList = (projectsList, currentProject) => projectsList @@ -40,24 +41,23 @@ export const generateProjectsList = (projectsList, currentProject) => : prevProject.id.localeCompare(nextProject.id) }) -export const generateArtifactsList = artifacts => - artifacts +export const generateArtifactsList = artifacts => { + const generatedArtifacts = artifacts .map(artifact => { - const key = artifact.link_iteration - ? artifact.link_iteration.db_key - : artifact.key ?? '' + const key = artifact.link_iteration ? artifact.link_iteration.db_key : artifact.key ?? '' return { label: key, id: key } }) .filter(artifact => artifact.label !== '') - .sort((prevArtifact, nextArtifact) => - prevArtifact.id.localeCompare(nextArtifact.id) - ) + .sort((prevArtifact, nextArtifact) => prevArtifact.id.localeCompare(nextArtifact.id)) -export const generateArtifactsReferencesList = artifacts => - artifacts + return uniqBy(generatedArtifacts, 'id') +} + +export const generateArtifactsReferencesList = artifacts => { + const generatedArtifacts = artifacts .map(artifact => { const artifactReference = getArtifactReference(artifact) @@ -79,6 +79,9 @@ export const generateArtifactsReferencesList = artifacts => } }) + return uniqBy(generatedArtifacts, 'id') +} + export const pathPlaceholders = { [MLRUN_STORAGE_INPUT_PATH_SCHEME]: 'artifacts/my-project/my-artifact:my-tag', [S3_INPUT_PATH_SCHEME]: 'bucket/path', @@ -86,3 +89,20 @@ export const pathPlaceholders = { [AZURE_STORAGE_INPUT_PATH_SCHEME]: 'container/path', [V3IO_INPUT_PATH_SCHEME]: 'container-name/file' } + +export const pathTips = projectItem => { + const pathType = + projectItem === 'feature-vectors' + ? 'feature-vector' + : projectItem === 'artifacts' + ? 'artifact' + : 'dataset' + + return { + [MLRUN_STORAGE_INPUT_PATH_SCHEME]: `${pathType}s/my-project/my-${pathType}:my-tag" or "${pathType}s/my-project/my-${pathType}@my-uid`, + [S3_INPUT_PATH_SCHEME]: 'bucket/path', + [GOOGLE_STORAGE_INPUT_PATH_SCHEME]: 'bucket/path', + [AZURE_STORAGE_INPUT_PATH_SCHEME]: 'container/path', + [V3IO_INPUT_PATH_SCHEME]: 'container-name/file' + } +} diff --git a/src/utils/parseFunction.js b/src/utils/parseFunction.js index 53658daae..bb6774504 100644 --- a/src/utils/parseFunction.js +++ b/src/utils/parseFunction.js @@ -28,6 +28,7 @@ export const parseFunction = (func, projectName, customState) => { base_spec: func.spec?.base_spec ?? {}, build: func.spec?.build ?? {}, command: func.spec?.command, + container_image: func?.status?.container_image ?? '', default_class: func.spec?.default_class ?? '', default_handler: func.spec?.default_handler ?? '', description: func.spec?.description ?? '', @@ -51,7 +52,7 @@ export const parseFunction = (func, projectName, customState) => { type: func.kind, volume_mounts: func.spec?.volume_mounts ?? [], volumes: func.spec?.volumes ?? [], - updated: new Date(func.metadata?.updated ?? ''), + updated: new Date(func.metadata?.updated ?? '') } item.ui = {