diff --git a/src/Routes/Base/Header/NewButtonSelect.js b/src/Routes/Base/Header/NewButtonSelect.js index b25f80962..352001c1b 100644 --- a/src/Routes/Base/Header/NewButtonSelect.js +++ b/src/Routes/Base/Header/NewButtonSelect.js @@ -4,6 +4,7 @@ import styled from 'styled-components'; import { Button, Dropdown } from 'antd'; import { ReactComponent as IconAddPipeline } from 'images/no-fill/add-pipeline.svg'; import { ReactComponent as IconAddAlgorithm } from 'images/algorithm-icon.svg'; +import { ReactComponent as IconAddMarketplace } from 'images/marketplace.svg'; import { ReactComponent as IconDataSource } from 'images/datasource.svg'; import { LEFT_SIDEBAR_NAMES, @@ -39,6 +40,10 @@ export const topActions = [ name: RIGHT_SIDEBAR_NAMES.ADD_ALGORITHM, component: IconAddAlgorithm, }, + { + name: RIGHT_SIDEBAR_NAMES.ADD_MARKETPLACE, + component: IconAddMarketplace, + }, { name: RIGHT_SIDEBAR_NAMES.ADD_DATASOURCE, component: IconDataSource, @@ -54,6 +59,8 @@ const NewButtonSelect = () => { let page = RIGHT_SIDEBAR_NAMES.ADD_PIPELINE; if (pageName === NEW_ITEM_PAGE.ALGORITHM) { page = RIGHT_SIDEBAR_NAMES.ADD_ALGORITHM; + } else if (pageName === NEW_ITEM_PAGE.MARKETPLACE) { + page = RIGHT_SIDEBAR_NAMES.ADD_MARKETPLACE; } else if (pageName === NEW_ITEM_PAGE.DATASOURCE) { page = RIGHT_SIDEBAR_NAMES.ADD_DATASOURCE; } @@ -75,6 +82,7 @@ const NewButtonSelect = () => { ), icon: , }, + { key: RIGHT_SIDEBAR_NAMES.ADD_ALGORITHM, label: ( diff --git a/src/Routes/Base/SidebarLeft/index.js b/src/Routes/Base/SidebarLeft/index.js index e3cdff90e..6c039c629 100644 --- a/src/Routes/Base/SidebarLeft/index.js +++ b/src/Routes/Base/SidebarLeft/index.js @@ -10,6 +10,7 @@ import { useErrorLogs, useCounters } from 'hooks/graphql'; import { dataCountMock } from 'config'; import { LEFT_SIDEBAR_NAMES, USER_GUIDE } from 'const'; import { ReactComponent as AlgorithmIcon } from 'images/algorithm-icon.svg'; +import { ReactComponent as MarketplaceIcon } from 'images/marketplace.svg'; import { ReactComponent as DataSourceIcon } from 'images/datasource.svg'; import { ReactComponent as JobsIcon } from 'images/jobs-icon.svg'; @@ -56,6 +57,7 @@ const instanceCounterAdapter = obj => ({ [LEFT_SIDEBAR_NAMES.QUEUE]: obj.queue || 0, [LEFT_SIDEBAR_NAMES.PIPELINES]: obj.pipelines, [LEFT_SIDEBAR_NAMES.ALGORITHMS]: obj.algorithms, + [LEFT_SIDEBAR_NAMES.MARKETPLACE]: 3, [LEFT_SIDEBAR_NAMES.DATASOURCES]: obj.dataSources, [LEFT_SIDEBAR_NAMES.WORKERS]: obj.workers, [LEFT_SIDEBAR_NAMES.DRIVERS]: obj.drivers, @@ -72,6 +74,7 @@ const SidebarLeft = () => { const itemsMenu = [ [LEFT_SIDEBAR_NAMES.JOBS, JobsIcon, '/jobs'], [LEFT_SIDEBAR_NAMES.QUEUE, QueueIcon, '/queue'], + [LEFT_SIDEBAR_NAMES.MARKETPLACE, MarketplaceIcon, '/marketplace'], [LEFT_SIDEBAR_NAMES.ALGORITHMS, AlgorithmIcon, '/algorithms'], [LEFT_SIDEBAR_NAMES.PIPELINES, PipelineIcon, '/pipelines'], ]; diff --git a/src/Routes/SidebarRight/AddAlgorithm/schemaMarketplace.js b/src/Routes/SidebarRight/AddAlgorithm/schemaMarketplace.js new file mode 100644 index 000000000..728c28429 --- /dev/null +++ b/src/Routes/SidebarRight/AddAlgorithm/schemaMarketplace.js @@ -0,0 +1,200 @@ +export const memoryTypes = [ + 'Ki', + 'M', + 'Mi', + 'Gi', + 'm', + 'K', + 'G', + 'T', + 'Ti', + 'P', + 'Pi', + 'E', + 'Ei', +]; + +const addAlgorithmSchema = { + ENV_TYPES: { + nodejs: 'Node.js', + python: 'Python', + java: 'Java', + }, + BUILD_TYPES: { + CODE: { + DIVIDERS: { + BUILD: `Build Configuration`, + }, + DRAGGER: { + field: 'code.dragger', + }, + ENTRY_POINT: { + field: 'code.entryPoint', + label: 'Entry Point', + placeholder: 'Insert Entry Point', + message: 'Entry Point is required', + }, + ENVIRONMENT: { + field: 'code.env', + placeholder: 'Pick Environment', + label: 'Environment', + message: 'Environment is required', + }, + label: 'Code', + field: 'code', + BASE_IMAGE: { + field: 'code.baseImage', + label: 'Base Image', + placeholder: '(Optional) Docker Image Name', + }, + }, + GIT: { + DIVIDERS: { + BUILD: `Build Configuration`, + GIT: `Git Configuration`, + ADVANCED: `Git Advance Configuration`, + }, + BASE_IMAGE: { + field: 'gitRepository.baseImage', + label: 'Base Image', + placeholder: '(Optional) Docker Image Name', + }, + BRANCH: { + field: 'gitRepository.branchName', + label: 'Branch', + placeholder: '(Optional) Branch', + }, + COMMIT: { + field: 'gitRepository.commit', + ID: { + field: 'gitRepository.commit.id', + label: 'Commit ID', + placeholder: '(Optional) Commit ID', + }, + label: 'Commit Details', + MESSAGE: { + field: 'gitRepository.commit.message', + label: 'Message', + placeholder: '(Optional) Enter Commit Message', + }, + TIMESTAMP: { + field: 'gitRepository.commit.timestamp', + label: 'Time Stamp', + placeholder: '(Optional) Enter Commit Time Stamp', + }, + }, + field: 'gitRepository', + GIT_KIND: { + field: 'gitRepository.gitKind', + label: 'Git Host', + placeholder: '(Optional) Git Host', + types: ['github', 'gitlab'], + }, + label: 'Git', + TAG: { + field: 'gitRepository.tag', + label: 'Tag', + placeholder: '(Optional) Tag', + }, + TOKEN: { + field: 'gitRepository.token', + label: 'Token', + placeholder: '(Optional) Token', + }, + URL: { + field: 'gitRepository.url', + label: 'URL', + addOns: { + before: ['https://', 'http://'], + after: '.git', + }, + placeholder: 'Enter Git Repository URL', + message: 'GIT URL required', + }, + ENTRY_POINT: { + field: 'gitRepository.entryPoint', + label: 'Entry Point', + placeholder: 'Insert Entry Point', + message: 'Entry Point is required', + }, + ENVIRONMENT: { + field: 'gitRepository.env', + placeholder: 'Pick Environment', + label: 'Environment', + message: 'Environment is required', + }, + }, + IMAGE: { + ALGORITHM_IMAGE: { + field: 'image.algorithmImage', + label: 'Algorithm Image', + placeholder: 'Insert URL', + message: 'Image URL required', + }, + label: 'Image', + field: 'image', + }, + }, + MAIN: { + field: 'main', + CPU: { + field: 'main.cpu', + label: 'CPU Usage', + }, + DIVIDER: { + ADVANCED: 'Advanced', + RESOURCES: 'Resources', + }, + GPU: { + field: 'main.gpu', + label: 'GPU Usage', + }, + MEMORY: { + field: 'main.mem', + label: 'Memory Usage', + types: memoryTypes, + }, + NAME: { + field: 'main.name', + label: 'Algorithm Name', + placeholder: 'Insert Algorithm Name', + message: + 'Lower cased letters and numbers are only allowed in Algorithm Name.', + }, + DESCRIPTION: { + field: 'main.description', + label: 'Description', + placeholder: 'Algorithm Description', + }, + SIDECAR: { + field: 'main.sideCars', + label: 'Side Car', + }, + OPTIONS: { + field: 'main.options', + label: 'Options', + placeholder: '(Optional) Enable Options', + types: ['binary', 'opengl'], + }, + WORKERS: { + field: 'main.minHotWorkers', + label: 'Min Hot Workers', + }, + RESERVE_MEMORY: { + field: 'main.reservedMemory', + label: 'Reserved Memory', + types: memoryTypes, + tooltip: + "Reserved memory for HKube's operations, such as in-memory cache. Higher values speed up data retrieval but leave less memory for the algorithms. Lower values slow down data retrieval but leave more memory for the algorithms.", + }, + WORKER_ENV: { + field: 'main.workerEnv', + label: 'Worker Env', + }, + ALGORITEM_ENV: { + field: 'main.algorithmEnv', + label: 'Algorithm Env', + }, + }, +}; +export default addAlgorithmSchema; diff --git a/src/Routes/SidebarRight/Content.react.js b/src/Routes/SidebarRight/Content.react.js index a33f8bb39..0ba779848 100644 --- a/src/Routes/SidebarRight/Content.react.js +++ b/src/Routes/SidebarRight/Content.react.js @@ -26,7 +26,15 @@ const rightSidebarContent = { ), }, - + [RIGHT_SIDEBAR_NAMES.ADD_MARKETPLACE]: { + width: DRAWER_SIZE.ADD_MARKETPLACE, + title: RIGHT_SIDEBAR_NAMES.ADD_MARKETPLACE, + description: ( + <> + Marketplace descriptor to be added to the store. + + ), + }, [RIGHT_SIDEBAR_NAMES.ADD_DATASOURCE]: { width: DRAWER_SIZE.ADD_DATASOURCE, title: RIGHT_SIDEBAR_NAMES.ADD_DATASOURCE, diff --git a/src/Routes/SidebarRight/Drawer.js b/src/Routes/SidebarRight/Drawer.js index b20c6947b..ab2e9cfa3 100644 --- a/src/Routes/SidebarRight/Drawer.js +++ b/src/Routes/SidebarRight/Drawer.js @@ -25,6 +25,7 @@ const operationSelector = { [RIGHT_SIDEBAR_NAMES.DRIVERS]: DriversTable, [RIGHT_SIDEBAR_NAMES.ADD_PIPELINE]: AddPipeline, [RIGHT_SIDEBAR_NAMES.ADD_ALGORITHM]: AddAlgorithm, + [RIGHT_SIDEBAR_NAMES.ADD_MARKETPLACE]: AddAlgorithm, [RIGHT_SIDEBAR_NAMES.RUN_RAW_PIPELINE]: RunRawPipeline, [RIGHT_SIDEBAR_NAMES.ADD_DATASOURCE]: AddDataSource, [RIGHT_SIDEBAR_NAMES.ERROR_LOGS]: ErrorLogsTable, @@ -39,6 +40,7 @@ const titleSelector = { [RIGHT_SIDEBAR_NAMES.DRIVERS]: DRAWER_TITLES.DRIVERS, [RIGHT_SIDEBAR_NAMES.ADD_PIPELINE]: DRAWER_TITLES.ADD_PIPELINE, [RIGHT_SIDEBAR_NAMES.ADD_ALGORITHM]: DRAWER_TITLES.ADD_ALGORITHM, + [RIGHT_SIDEBAR_NAMES.ADD_MARKETPLACE]: DRAWER_TITLES.ADD_MARKETPLACE, [RIGHT_SIDEBAR_NAMES.RUN_RAW_PIPELINE]: DRAWER_TITLES.RUN_RAW_PIPELINE, [RIGHT_SIDEBAR_NAMES.ADD_DATASOURCE]: DRAWER_TITLES.ADD_DATASOURCE, [RIGHT_SIDEBAR_NAMES.ERROR_LOGS]: DRAWER_TITLES.ERROR_LOGS, diff --git a/src/Routes/SidebarRight/schema.js b/src/Routes/SidebarRight/schema.js index 2f3e72830..847e1e690 100644 --- a/src/Routes/SidebarRight/schema.js +++ b/src/Routes/SidebarRight/schema.js @@ -21,6 +21,10 @@ export const topActions = [ name: RIGHT_SIDEBAR_NAMES.ADD_ALGORITHM, component: IconAddAlgorithm, }, + { + name: RIGHT_SIDEBAR_NAMES.ADD_MARKETPLACE, + component: IconAddAlgorithm, + }, { name: RIGHT_SIDEBAR_NAMES.ADD_DATASOURCE, component: IconDataSource, diff --git a/src/Routes/Tables/Algorithms/index.js b/src/Routes/Tables/Algorithms/index.js index 826881df3..4b4831afd 100644 --- a/src/Routes/Tables/Algorithms/index.js +++ b/src/Routes/Tables/Algorithms/index.js @@ -60,26 +60,19 @@ const AlgorithmsTable = () => { const getList = useMemo(() => { const filterValue = instanceFilter.algorithms.qAlgorithmName; + let list = query.data?.algorithms?.list || []; - if (filterValue != null && query.data?.algorithms?.list) { - const filterAlgorithm = query.data?.algorithms?.list.filter(item => - item.name.includes(filterValue) - ); - return [...filterAlgorithm]; + if (filterValue) { + list = list.filter(item => item.name.includes(filterValue)); } + // no marketplace + list = list.filter(item => !item.name.toLowerCase().startsWith('mark')); - return ( - (query.data && - query.data.algorithms && - query.data.algorithms.list && - [...query.data.algorithms.list].sort((x, y) => { - if (x.unscheduledReason && !y.unscheduledReason) return -1; - if (!x.unscheduledReason && y.unscheduledReason) return 1; - - return x.modified < y.modified ? 1 : -1; - })) || - [] - ); + return [...list].sort((x, y) => { + if (x.unscheduledReason && !y.unscheduledReason) return -1; + if (!x.unscheduledReason && y.unscheduledReason) return 1; + return x.modified < y.modified ? 1 : -1; + }); }, [instanceFilter.algorithms.qAlgorithmName, query.data]); // if have keycloak remove avatar from columns job diff --git a/src/Routes/Tables/Marketplaces/AlgorithmActions.react.js b/src/Routes/Tables/Marketplaces/AlgorithmActions.react.js new file mode 100644 index 000000000..29b7ad491 --- /dev/null +++ b/src/Routes/Tables/Marketplaces/AlgorithmActions.react.js @@ -0,0 +1,211 @@ +import React, { useCallback, useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import KeycloakServices from 'keycloak/keycloakServices'; +import { selectors } from 'reducers'; +import { useSelector } from 'react-redux'; +import { + BugOutlined, + DeleteOutlined, + EditOutlined, + InfoCircleOutlined, + PlayCircleOutlined, +} from '@ant-design/icons'; +import { keycloakRoles } from '@hkube/consts'; +import { Button, Modal, Popover, Typography, Tooltip } from 'antd'; +import { useActions } from 'hooks'; +import RunForm from './RunForm'; +import usePath from './usePath'; + +const NOTVIEW = false; +const deleteConfirmAction = action => { + Modal.confirm({ + title: 'Deleting Algorithm', + content: ( + <> + Deleting algorithm will{' '} + delete all related pipelines + and stop all executions. + + ), + okText: 'Confirm', + okType: 'danger', + cancelText: 'Cancel', + onOk() { + action(); + }, + }); +}; + +const overlayStyle = { width: `50ch` }; + +const AlgorithmActions = ({ record }) => { + const { keycloakEnable } = useSelector(selectors.connection); + + const isRoleEdit = keycloakEnable + ? KeycloakServices.getUserRoles(keycloakRoles.API_EDIT) + : true; + const isRoleDelete = keycloakEnable + ? KeycloakServices.getUserRoles(keycloakRoles.API_DELETE) + : true; + + const isRoleRunOrStop = keycloakEnable + ? KeycloakServices.getUserRoles(keycloakRoles.API_EXECUTE) + : true; + + const [openPopupRun, setOpenPopupRun] = useState(false); + const [openPopupRunDebug, setOpenPopupRunDebug] = useState(false); + const { goTo } = usePath(); + const { builds, ...algorithm } = record; + const { name } = algorithm; + + const { /* applyAlgorithm , */ deleteAlgorithm, runAlgorithm } = useActions(); + const container = useRef(); + + // const [inputs, setInputs] = useState(EMPTY_INITIAL); + + const setPopupContainer = useCallback(() => container.current, []); + + // const onSubmit = useCallback( + // value => { + // const formData = new FormData(); + // formData.append('payload', value); + // applyAlgorithm(formData); + // }, + // [applyAlgorithm] + // ); + + const onEdit = useCallback( + () => goTo.edit({ nextAlgorithmId: name }), + [goTo, name] + ); + + const onClickDelete = useCallback( + () => deleteConfirmAction(() => deleteAlgorithm(name)), + [deleteAlgorithm, name] + ); + + const onRun = useCallback( + input => runAlgorithm({ name, input }), + [runAlgorithm, name] + ); + + const onDebug = useCallback( + input => runAlgorithm({ name, input, debug: true }), + [runAlgorithm, name] + ); + + const popOverContentRun = ; + const popOverContentDebug = ; + + const onMoreInfo = useCallback( + () => goTo.overview({ nextAlgorithmId: name }), + [goTo, name] + ); + + const stopPropagation = useCallback(e => { + e.stopPropagation(); + }, []); + + const clickOnRunAlg = useCallback(() => { + onRun(); + setOpenPopupRun(false); + }, [onRun]); + + const handleOpenChange = useCallback(newOpen => { + setOpenPopupRun(newOpen); + }, []); + + const clickOnRunDebug = useCallback(() => { + onDebug(); + setOpenPopupRunDebug(false); + }, [onDebug]); + + const handleOpenChangeDebug = useCallback(newOpen => { + setOpenPopupRunDebug(newOpen); + }, []); + + return ( +
+ + {NOTVIEW && isRoleRunOrStop ? ( + +
+ ); +}; + +AlgorithmActions.propTypes = { + // TODO: detail the props + // eslint-disable-next-line + record: PropTypes.object.isRequired, +}; + +export default React.memo(AlgorithmActions); diff --git a/src/Routes/Tables/Marketplaces/AlgorithmBuildStats.react.js b/src/Routes/Tables/Marketplaces/AlgorithmBuildStats.react.js new file mode 100644 index 000000000..6d2f9f0fb --- /dev/null +++ b/src/Routes/Tables/Marketplaces/AlgorithmBuildStats.react.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Tag } from 'antd'; + +import { FlexBox } from 'components/common'; +import { StatusTag } from 'components/StatusTag'; + +const AlgorithmBuildStats = ({ builds = [] }) => ( + + {builds.total === 0 ? ( + + No Builds + + ) : ( + Object.entries(builds).map( + ([status, count]) => + status !== 'total' && + count > 0 && ( + + + + ) + ) + )} + +); + +AlgorithmBuildStats.propTypes = { + builds: PropTypes.shape({ + status: PropTypes.string, + total: PropTypes.number, + }), +}; + +export default React.memo(AlgorithmBuildStats); diff --git a/src/Routes/Tables/Marketplaces/AlgorithmsQueryTable.js b/src/Routes/Tables/Marketplaces/AlgorithmsQueryTable.js new file mode 100644 index 000000000..202b33b90 --- /dev/null +++ b/src/Routes/Tables/Marketplaces/AlgorithmsQueryTable.js @@ -0,0 +1,102 @@ +import React, { useEffect, useMemo } from 'react'; + +import { Form } from 'antd'; +import { useLocation } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import qs from 'qs'; +import { useReactiveVar } from '@apollo/client'; +import { instanceFiltersVar } from 'cache'; +import { FiltersForms } from 'styles'; +import AutoCompleteFloatingLabelInput from 'components/common/FiltersInput/AutoCompleteFloatingLabelInput'; + +const AlgorithmsQueryTable = ({ + onSubmit = () => {}, + algorithmsList = undefined, +}) => { + const urlParams = useLocation(); + const instanceFilters = useReactiveVar(instanceFiltersVar); + + const [form] = Form.useForm(); + + const SubmitForm = values => { + const algorithms = { + qAlgorithmName: values || null, + }; + + instanceFiltersVar({ ...instanceFiltersVar(), algorithms }); + + form.submit(); + }; + + useEffect(() => { + if (instanceFilters.algorithms.qAlgorithmName === null) { + const qAlgorithmNameValue = form.getFieldValue('qAlgorithmName'); + if (qAlgorithmNameValue !== '') { + form.resetFields(); + } + + setTimeout(() => { + SubmitForm(null); + }, 100); + } + }, [form, instanceFilters.algorithms.qAlgorithmName]); + + useEffect(() => { + const paramsUrl = qs.parse(urlParams.search, { + ignoreQueryPrefix: true, + allowDots: true, + skipNulls: true, + }); + + const algorithmName = + paramsUrl.qAlgorithmName || instanceFilters.algorithms.qAlgorithmName; + + if (algorithmName) { + form.setFieldValue('qAlgorithmName', algorithmName); + setTimeout(() => { + SubmitForm(algorithmName); + }, 500); + } + }, []); + + const onFinish = values => { + onSubmit(values); + }; + + const algorithmOptions = useMemo( + () => + algorithmsList + .filter(item => item.name.toLowerCase().startsWith('mark')) + ?.map(algorithm => ({ + value: algorithm.name, + label: algorithm.name, + })), + [algorithmsList] + ); + + return ( + + + + + + ); +}; + +AlgorithmsQueryTable.propTypes = { + onSubmit: PropTypes.func, +}; + +AlgorithmsQueryTable.propTypes = { + onSubmit: PropTypes.func, + algorithmsList: PropTypes.arrayOf(PropTypes.object), +}; + +export default React.memo(AlgorithmsQueryTable); diff --git a/src/Routes/Tables/Marketplaces/EditDrawer.js b/src/Routes/Tables/Marketplaces/EditDrawer.js new file mode 100644 index 000000000..0849e10d9 --- /dev/null +++ b/src/Routes/Tables/Marketplaces/EditDrawer.js @@ -0,0 +1,46 @@ +import React, { useMemo } from 'react'; +import { TabDrawerText, TabDrawer } from 'styles'; +import { stringify } from 'utils'; +import { DRAWER_SIZE } from 'const'; +import Drawer from 'components/Drawer'; +import useToggle from 'hooks/useToggle'; +import MissingIdError from 'components/MissingIdError'; + +import AddAlgorithm from '../../SidebarRight/AddAlgorithm'; +import usePath from './usePath'; +import useActiveAlgorithm from './useActiveAlgorithm'; +import { DRAWER_TITLES } from '../../../const'; + +const EditDrawer = () => { + const { goTo } = usePath(); + const { activeAlgorithm, algorithmId } = useActiveAlgorithm(); + const { setOff, isOn } = useToggle(true); + + const algorithmValue = useMemo(() => { + const { builds, ...rest } = activeAlgorithm || {}; + return stringify(rest); + }, [activeAlgorithm]); + + return ( + + <> + + {DRAWER_TITLES.MARKETPLACE_EDIT} + + {activeAlgorithm ? ( + + ) : ( + + )} + + + ); +}; + +export default React.memo(EditDrawer); diff --git a/src/Routes/Tables/Marketplaces/LastModified.js b/src/Routes/Tables/Marketplaces/LastModified.js new file mode 100644 index 000000000..0654aa454 --- /dev/null +++ b/src/Routes/Tables/Marketplaces/LastModified.js @@ -0,0 +1,53 @@ +import React from 'react'; +import { UserOutlined } from '@ant-design/icons'; +import { selectors } from 'reducers'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import { Badge, Tag } from 'antd'; +import Moment from 'react-moment'; +import { getColorByName } from 'utils'; + +const LastModified = ({ modified, auditTrail }) => { + const { keycloakEnable } = useSelector(selectors.connection); + + const userName = Array.isArray(auditTrail) ? auditTrail[0]?.user : undefined; + const dateNode = ( + + {+modified} + + ); + return keycloakEnable ? ( + + ) + } + size="small" + color={userName && getColorByName(userName)} + title={userName && userName} + offset={[-7, 0]}> + {dateNode} + + ) : ( + dateNode + ); +}; + +LastModified.propTypes = { + modified: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + .isRequired, + auditTrail: PropTypes.array, +}; + +export default LastModified; diff --git a/src/Routes/Tables/Marketplaces/OverviewDrawer.js b/src/Routes/Tables/Marketplaces/OverviewDrawer.js new file mode 100644 index 000000000..10eb42ddc --- /dev/null +++ b/src/Routes/Tables/Marketplaces/OverviewDrawer.js @@ -0,0 +1,44 @@ +import React from 'react'; +import { TabDrawerText, TabDrawer } from 'styles'; +import { Button } from 'antd'; +import Drawer from 'components/Drawer'; +import { DRAWER_SIZE } from 'const'; +import useToggle from 'hooks/useToggle'; +import MissingIdError from 'components/MissingIdError'; +import AlgorithmsTabs from './Tabs'; +import usePath from './usePath'; +import useActiveAlgorithm from './useActiveAlgorithm'; +import { DRAWER_TITLES } from '../../../const'; + +const OverviewDrawer = () => { + const { goTo } = usePath(); + const { setOff, isOn } = useToggle(true); + const { activeAlgorithm, algorithmId } = useActiveAlgorithm(); + + return ( + + Edit + + }> + <> + + {DRAWER_TITLES.ALGORITHM_INFO} + + {activeAlgorithm ? ( + + ) : ( + + )} + + + ); +}; + +export default React.memo(OverviewDrawer); diff --git a/src/Routes/Tables/Marketplaces/RunForm.js b/src/Routes/Tables/Marketplaces/RunForm.js new file mode 100644 index 000000000..d593937a7 --- /dev/null +++ b/src/Routes/Tables/Marketplaces/RunForm.js @@ -0,0 +1,125 @@ +import React, { useState, useCallback } from 'react'; +import PropTypes from 'prop-types'; +import { PlusOutlined } from '@ant-design/icons'; +import { Form, Button, Card, Space, Typography } from 'antd'; +import styled from 'styled-components'; +import RawInputField from 'components/InputField'; +import { tryParse } from 'utils'; +import useIds from '../../SidebarRight/AddPipeline/Steps/Nodes/useIds'; + +const ButtonGroupCenter = styled(Button.Group)` + display: flex; + justify-content: center; + margin-top: 5px; +`; + +const InputField = ({ onRemove, idx, ...antFields }) => { + const [isValid, setIsValid] = useState(true); + const [value, setValue] = useState(); + const hasRemove = !!onRemove; + const onInputChange = useCallback( + ({ target: { value: src } }) => { + setValue(src); + const onFail = () => setIsValid(src === ''); + const onSuccess = ({ parsed }) => { + antFields.onChange(parsed); + setIsValid(true); + }; + tryParse({ src, onSuccess, onFail }); + }, + [antFields] + ); + + return ( + onRemove(idx)} + value={value} + onChange={onInputChange} + placeholder='ex. {"key": "value"} Or [1,"2",true]]' + /> + ); +}; +InputField.propTypes = { + onRemove: PropTypes.func.isRequired, + idx: PropTypes.number.isRequired, +}; + +const InputsCollection = () => { + const [ids, appendKey, dropKey] = useIds([0]); + return ( + <> + {ids.map(id => ( + + 1 ? dropKey : null} idx={id} /> + + ))} + + + + + ); +}; + +/** + * @param {{ + * onSubmit: () => {}; + * onRum: {}; + * } & import('antd/lib/form').FormComponentProps} props + */ +const AlgorithmRun = ({ onRun, buttonTitle }) => { + const [form] = Form.useForm(); + + const handleRun = useCallback(() => { + const values = form.getFieldsValue(); + + const _values = Object.values(values.inputs)?.filter( + item => item !== undefined + ); + if (_values?.length > 0) { + onRun(_values); + } else { + onRun(); + } + }, [form, onRun]); + + return ( +
+ + + Algorithm input + + + + + +
+ ); +}; + +AlgorithmRun.propTypes = { + onRun: PropTypes.func.isRequired, + + buttonTitle: PropTypes.string.isRequired, +}; + +export default AlgorithmRun; diff --git a/src/Routes/Tables/Marketplaces/Tabs/AlgorithmsTabs.stories.js b/src/Routes/Tables/Marketplaces/Tabs/AlgorithmsTabs.stories.js new file mode 100644 index 000000000..11adec98a --- /dev/null +++ b/src/Routes/Tables/Marketplaces/Tabs/AlgorithmsTabs.stories.js @@ -0,0 +1,14 @@ +import React from 'react'; + +import { SB_SECTIONS } from 'const'; +import AlgorithmsTabs from '.'; + +export default { + title: `${SB_SECTIONS.TABLES.ALGORITHMS}/Tabs`, +}; + +export const Default = () => ( + +); diff --git a/src/Routes/Tables/Marketplaces/Tabs/Builds/getColumns.js b/src/Routes/Tables/Marketplaces/Tabs/Builds/getColumns.js new file mode 100644 index 000000000..8a4bd4295 --- /dev/null +++ b/src/Routes/Tables/Marketplaces/Tabs/Builds/getColumns.js @@ -0,0 +1,126 @@ +import React from 'react'; +import Moment from 'react-moment'; +import { CloseOutlined, RedoOutlined } from '@ant-design/icons'; +import { Button, Progress } from 'antd'; +import { pipelineStatuses as PIPELINE_STATUS } from '@hkube/consts'; +import Ellipsis from 'components/common/Ellipsis.react'; +import humanizeDuration from 'humanize-duration'; +import { COLOR_TASK_STATUS } from 'styles/colors'; +import BaseTag from 'components/BaseTag'; +import { sorter } from 'utils/stringHelper'; + +const BuildId = buildId => ( + +); +const StartTime = startTime => ( + {+startTime} +); + +const Status = status => {status}; + +const RenderProgress = (_, record) => { + const { status, progress } = record; + const failed = status === PIPELINE_STATUS.FAILED; + const progressBar = (progress && parseInt(progress, 10)) || 0; + return ( + + ); +}; + +const sortByBuildId = (a, b) => sorter(a.buildId, b.buildId); +const sortByEnv = (a, b) => sorter(a.env, b.env); +const sortByImageTag = (a, b) => sorter(a.imageTag, b.imageTag); +const sortByStartTime = (a, b) => sorter(a.startTime, b.startTime); +const sortByRunningTime = (a, b) => sorter(a.endTime, b.endTime); +const sortByStatus = (a, b) => sorter(a.status, b.status); + +const getColumns = ({ cancelBuild, rerunBuild, currentTime }) => [ + { + title: 'Build Id', + dataIndex: ['buildId'], + key: 'buildId', + sorter: sortByBuildId, + render: BuildId, + }, + { + title: 'Env', + dataIndex: ['env'], + key: 'env', + sorter: sortByEnv, + }, + { + title: 'Image Tag', + dataIndex: ['imageTag'], + key: 'imageTag', + sorter: sortByImageTag, + }, + { + title: 'Start Time', + dataIndex: ['startTime'], + key: 'startTime', + sorter: sortByStartTime, + render: StartTime, + }, + { + title: 'Running time', + dataIndex: ['timeTook'], + key: 'timeTook', + sorter: sortByRunningTime, + render: (_, { startTime, endTime }) => ( + + {humanizeDuration( + endTime ? endTime - startTime : currentTime - startTime, + { + maxDecimalPoints: 2, + } + )} + + ), + }, + { + title: 'Status', + key: 'status', + dataIndex: ['status'], + sorter: sortByStatus, + render: Status, + }, + { + title: 'Progress', + dataIndex: ['Progress'], + key: 'progress', + width: '20%', + render: RenderProgress, + }, + { + title: 'Actions', + key: 'stop', + render: (_, record) => { + const { status } = record; + const failed = status === PIPELINE_STATUS.FAILED; + const showCancel = + !failed && + ![PIPELINE_STATUS.COMPLETED, PIPELINE_STATUS.STOPPED].includes(status); + return showCancel ? ( + + } + /> + + ); + const expandIcon = ({ expanded, onExpand, record }) => + expanded ? ( + onExpand(record, e)} /> + ) : ( + onExpand(record, e)} /> + ); + return ( + record.buildId} + columns={getColumns({ cancelBuild, rerunBuild, currentTime })} + dataSource={builds} + expandable={{ + defaultExpandedRowKeys: isOpenFirstLog ? [builds[0].buildId] : [], + expandedRowRender, + expandIcon, + }} + /> + ); +}; + +Builds.propTypes = { + // TODO: detail the props + // eslint-disable-next-line + builds: PropTypes.array, + isOpenFirstLog: PropTypes.bool, +}; + +export default React.memo(Builds); diff --git a/src/Routes/Tables/Marketplaces/Tabs/index.js b/src/Routes/Tables/Marketplaces/Tabs/index.js new file mode 100644 index 000000000..093db9e83 --- /dev/null +++ b/src/Routes/Tables/Marketplaces/Tabs/index.js @@ -0,0 +1,256 @@ +import React, { + useCallback, + useEffect, + useState, + useMemo, + useRef, +} from 'react'; +import { CheckOutlined, RedoOutlined } from '@ant-design/icons'; +import { ReactComponent as IconCompare } from 'images/compare.svg'; +import { Button, Checkbox, Modal } from 'antd'; +import Text from 'antd/lib/typography/Text'; +import { + Card, + JsonSwitch, + MdEditor, + Tabs, + JsonDiffTable, +} from 'components/common'; +import { useReadme, useVersions } from 'hooks'; +import PropTypes from 'prop-types'; +import { OVERVIEW_TABS as TABS } from 'const'; +import { VersionsTable, AuditTrailTable } from 'components'; +import AlgorithmBuildsTable from './Builds'; +import usePath from '../usePath'; + +const AlgorithmsTabs = ({ algorithm }) => { + const isFirstRender = useRef(true); + const { tabKey: activeKey, goTo } = usePath(); + const setActiveKey = useCallback( + tab => goTo.overview({ nextTabKey: tab }), + [goTo] + ); + + const [versionsCompare, setVersionsCompare] = useState([]); + const [readme, setReadme] = useState(); + const [isCompareVisible, setCompareVisible] = useState(false); + const [compareJsonPair, setCompareJsonPair] = useState({ + json1: null, + json2: null, + }); + + const [isBuildFirstFail] = useState( + algorithm?.builds?.length > 0 && algorithm?.builds[0]?.status === 'failed' + ); + + const { asyncFetch, post } = useReadme(useReadme.TYPES.ALGORITHM); + + const confirmPopupForceVersion = ( + resError, + name, + version, + applyVersionCallback + ) => { + let isForce = false; + Modal.confirm({ + title: 'WARNING : Version not upgrade', + content: ( + <> +
+ {resError.error.message} +
+ { + isForce = e.target.checked; + }}> + Stop running algorithms. + + + ), + okText: 'Try again', + okType: 'danger', + cancelText: 'Cancel', + onCancel() {}, + onOk() { + setTimeout(() => { + applyVersionCallback({ name, version, force: isForce }); + }, 100); + }, + }); + }; + + const { dataSource, onApply, onDelete, onSaveAs, fetch } = useVersions({ + nameId: algorithm.name, + confirmPopupForceVersion, + isFetch: true, + urlRestData: 'algorithms', + }); + + const onApplyApplyMarkdown = useCallback(() => { + post({ name: algorithm.name, readme }); + }, [post, algorithm, readme]); + + useEffect(() => { + if (activeKey !== TABS.DESCRIPTION) return; + asyncFetch({ name: algorithm.name }).then(setReadme); + }, [asyncFetch, algorithm, activeKey]); + + const CompareJson = () => { + if (versionsCompare.length !== 2) { + Modal.warning({ + title: 'Please select exactly 2 versions to compare', + }); + return; + } + + let json1 = dataSource.find(item => item.version === versionsCompare[0]); + let json2 = dataSource.find(item => item.version === versionsCompare[1]); + + if (!json1 || !json2) { + Modal.error({ + title: 'Error finding selected versions', + content: 'Could not locate both selected versions in dataSource.', + }); + return; + } + + if (json1?.algorithm?.modified > json2?.algorithm?.modified) { + [json1, json2] = [json2, json1]; + } + + setCompareJsonPair({ json1, json2 }); + setCompareVisible(true); + }; + + const extra = + activeKey === TABS.DESCRIPTION ? ( + false && ( + + ) + ) : activeKey === TABS.VERSIONS ? ( + <> + {' '} + + + ) : null; + + useEffect(() => { + if (isFirstRender.current && isBuildFirstFail) { + isFirstRender.current = false; + setActiveKey(TABS.BUILDS); + } + }, [isBuildFirstFail, setActiveKey]); + + const TabsItemsJson = useMemo( + () => [ + { + label: TABS.VERSIONS, + key: TABS.VERSIONS, + children: ( + + ), + }, + { + label: TABS.BUILDS, + key: TABS.BUILDS, + children: ( + + ), + }, + + { + label: TABS.INFO, + key: TABS.INFO, + forceRender: true, + children: , + }, + { + label: TABS.VERSIONSTIMETABLE, + key: TABS.VERSIONSTIMETABLE, + children: , + }, + { + label: TABS.DESCRIPTION, + key: TABS.DESCRIPTION, + children: , + }, + ], + [ + activeKey, + algorithm, + dataSource, + isBuildFirstFail, + isFirstRender, + onApply, + onDelete, + onSaveAs, + readme, + ] + ); + + return ( + <> + + + + + setCompareVisible(false)} + footer={[ + , + ]} + width={900}> +
+ {compareJsonPair.json1 && compareJsonPair.json2 && ( + + )} +
+
+ + ); +}; + +AlgorithmsTabs.propTypes = { + algorithm: PropTypes.shape({ + name: PropTypes.string.isRequired, + builds: PropTypes.arrayOf(PropTypes.object), + version: PropTypes.string.isRequired, + auditTrail: PropTypes.arrayOf(PropTypes.object), + }).isRequired, +}; + +export default React.memo(AlgorithmsTabs); diff --git a/src/Routes/Tables/Marketplaces/columns.js b/src/Routes/Tables/Marketplaces/columns.js new file mode 100644 index 000000000..db7d43798 --- /dev/null +++ b/src/Routes/Tables/Marketplaces/columns.js @@ -0,0 +1,141 @@ +import React from 'react'; +import { WarningOutlined, SettingOutlined } from '@ant-design/icons'; +import { Tag, Tooltip, Typography } from 'antd'; +import { sorter } from 'utils/stringHelper'; +import { copyToClipboard } from 'utils'; +import { errorsCode } from '@hkube/consts'; +import AuditTrailAvatar from '../../../components/AuditTrailAvatar'; +import AlgorithmActions from './AlgorithmActions.react'; +/* + +import AlgorithmBuildStats from './AlgorithmBuildStats.react'; +import LastModified from './LastModified'; +*/ + +// eslint-disable-next-line react/prop-types, no-unused-vars +const HotWorkers = ({ minHotWorkers }) => {minHotWorkers}; +const Memory = mem => {mem || 'No Memory Specified'}; +const Cpu = cpu => {cpu || 'No CPU Assigned'}; + +const Image = algorithmImage => + algorithmImage ? ( + + copyToClipboard(algorithmImage)}> + {algorithmImage} + + + ) : ( + No Image + ); + +const Name = (name, record) => + record?.unscheduledReason ? ( + + {name} + + ) : ( +
+ + copyToClipboard(name)}> + {name} + + + + {record?.errors?.includes(errorsCode.NOT_LAST_VERSION_ALGORITHM) && ( + + + + )} + {record?.options?.devMode && ( + + {' '} + + )} + +
+ ); + +// const BuildStats = builds => ; +const renderAction = (_, record) => ; + +const sortByName = (a, b) => sorter(a.name, b.name); +const filterByImage = (value, record) => record.algorithmImage.includes(value); +const sortByImage = (a, b) => sorter(a.algorithmImage, b.algorithmImage); +const sortByMinHotWorkers = (a, b) => sorter(a.minHotWorkers, b.minHotWorkers); +// const sortByLastModified = (a, b) => sorter(a.modified, b.modified); + +export default [ + { + title: ``, + dataIndex: [`auditTrail`], + key: `auditTrail`, + width: `2%`, + render: auditTrail => , + }, + { + width: '12%', + title: 'App Name', + dataIndex: ['name'], + key: 'name', + sorter: sortByName, + render: Name, + }, + { + width: '40%', + title: 'App Image', + dataIndex: ['algorithmImage'], + key: 'algorithmImage', + onFilter: filterByImage, + sorter: sortByImage, + render: Image, + }, + /* { + width: '7%', + title: 'Builds Stats', + dataIndex: ['buildStats'], + key: 'builds', + render: BuildStats, + }, */ + { + width: '5%', + title: 'CPU', + dataIndex: ['cpu'], + key: 'cpu', + render: Cpu, + }, + { + width: '5%', + title: 'Mem', + dataIndex: ['mem'], + key: 'mem', + render: Memory, + }, + { + width: '7%', + title: 'Min Hot Workers', + key: 'minHotWorkers', + sorter: sortByMinHotWorkers, + render: HotWorkers, + }, + /* { + width: '10%', + title: 'Last modified', + // dataIndex: ['modified'], + key: 'modified', + sorter: sortByLastModified, + render: record => ( + + ), + }, */ + { + width: '14%', + title: 'Action', + dataIndex: ['action'], + key: 'action', + render: renderAction, + }, +]; diff --git a/src/Routes/Tables/Marketplaces/index.js b/src/Routes/Tables/Marketplaces/index.js new file mode 100644 index 000000000..5e93fb821 --- /dev/null +++ b/src/Routes/Tables/Marketplaces/index.js @@ -0,0 +1,131 @@ +import React, { useMemo } from 'react'; +import { selectors } from 'reducers'; +import { useSelector } from 'react-redux'; +import { SkeletonLoader } from 'components/common'; +import { Route, Routes } from 'react-router-dom'; +import { Table } from 'components'; +import { usePolling } from 'hooks'; +import { useQuery, useReactiveVar } from '@apollo/client'; +import { Space, Empty } from 'antd'; +import { instanceFiltersVar } from 'cache'; // algorithmsListVar +import { ALGORITHMS_QUERY } from 'graphql/queries'; +import styled from 'styled-components'; +import OverviewDrawer from './OverviewDrawer'; +// import usePath from './usePath'; +import EditDrawer from './EditDrawer'; +import algorithmColumns from './columns'; +import AlgorithmsQueryTable from './AlgorithmsQueryTable'; + +const rowKey = ({ name }) => `algorithm-${name}`; +const TableAlgorithms = styled(Table)` + .ant-table-body { + min-height: 75vh; + } +`; +const MarketplacesTable = () => { + // const { goTo } = usePath(); + /* const onRow = ({ name }) => ({ + onDoubleClick: () => goTo.overview({ nextAlgorithmId: name }), + }); */ + + // const algorithmsList = useReactiveVar(algorithmsListVar); + const instanceFilter = useReactiveVar(instanceFiltersVar); + const { keycloakEnable } = useSelector(selectors.connection); + + const query = useQuery(ALGORITHMS_QUERY); + usePolling(query, 3000); + + /* const onSubmitFilter = useCallback( + values => { + if (!query.loading) { + if (values?.qAlgorithmName) { + const filterAlgorithm = query.data?.algorithms?.list.filter(item => + item.name.includes(values.qAlgorithmName) + ); + const list = [...filterAlgorithm]; + algorithmsListVar(list); + } else { + const list = [...query.data?.algorithms?.list]; + + algorithmsListVar( + list.sort((x, y) => (x.modified < y.modified ? 1 : -1)) + ); + } + } + }, + [query.data?.algorithms?.list] + ); + */ + const onSubmitFilter = () => {}; + + const getList = useMemo(() => { + const filterValue = instanceFilter.algorithms.qAlgorithmName; + + let list = query.data?.algorithms?.list || []; + + if (filterValue) { + list = list.filter(item => item.name.includes(filterValue)); + } + + list = list.filter(item => item.name.toLowerCase().startsWith('mark')); + + return [...list].sort((x, y) => { + if (x.unscheduledReason && !y.unscheduledReason) return -1; + if (!x.unscheduledReason && y.unscheduledReason) return 1; + return x.modified < y.modified ? 1 : -1; + }); + }, [instanceFilter.algorithms.qAlgorithmName, query.data]); + + // if have keycloak remove avatar from columns job + const algorithmColumnsView = useMemo(() => { + if (!keycloakEnable) { + return algorithmColumns.slice(1); + } + return algorithmColumns; + }, [keycloakEnable]); + + if (query.loading && query.data === undefined) return ; + if (query.error) return `Error! ${query.error.message}`; + + return ( + <> + + + + No results match your search criteria} + /> + ), + }} + /> + + + } + /> + } /> + + + ); +}; + +export default React.memo(MarketplacesTable); diff --git a/src/Routes/Tables/Marketplaces/useActiveAlgorithm.js b/src/Routes/Tables/Marketplaces/useActiveAlgorithm.js new file mode 100644 index 000000000..db4486c12 --- /dev/null +++ b/src/Routes/Tables/Marketplaces/useActiveAlgorithm.js @@ -0,0 +1,39 @@ +import { useMemo } from 'react'; +import { + ALGORITHM_BY_NAME_QUERY, + ALGORITHM_BUILDS_FRAGMENTS, +} from 'graphql/queries'; +import { useQuery } from '@apollo/client'; +import { usePolling } from 'hooks'; +import usePath from './usePath'; + +export default () => { + const { algorithmId } = usePath(); + + const queryAlgorithmsByName = useQuery(ALGORITHM_BY_NAME_QUERY, { + variables: { + name: algorithmId, + }, + }); + usePolling(queryAlgorithmsByName, 3000); + + const algorithmBase = queryAlgorithmsByName.data?.algorithmsByName; + const queryAlgorithmBuilds = useQuery(ALGORITHM_BUILDS_FRAGMENTS, { + variables: { + algorithmName: algorithmId, + }, + }); + usePolling(queryAlgorithmBuilds, 3000); + + const algorithmBuild = queryAlgorithmBuilds.data?.algorithmBuilds; + + const activeAlgorithm = useMemo( + () => (algorithmBase ? { ...algorithmBase, builds: algorithmBuild } : null), + [algorithmBase, algorithmBuild] + ); + + return { + activeAlgorithm, + algorithmId, + }; +}; diff --git a/src/Routes/Tables/Marketplaces/usePath.js b/src/Routes/Tables/Marketplaces/usePath.js new file mode 100644 index 000000000..6ed2de133 --- /dev/null +++ b/src/Routes/Tables/Marketplaces/usePath.js @@ -0,0 +1,49 @@ +import { useMemo } from 'react'; +import { useNavigate, useLocation, useParams } from 'react-router'; +import { OVERVIEW_TABS } from 'const'; + +export default () => { + const { algorithmId, tabKey } = useParams(); + + const navigate = useNavigate(); + const location = useLocation(); + const paths = useMemo( + () => ({ + root: '/marketplace', + overview: ({ + nextAlgorithmId = algorithmId, + nextTabKey = OVERVIEW_TABS.VERSIONS, + } = {}) => `/marketplace/${nextAlgorithmId}/overview/${nextTabKey}`, + edit: ({ nextAlgorithmId = algorithmId } = {}) => + `/marketplace/${nextAlgorithmId}/edit`, + }), + [algorithmId] + ); + + const goTo = useMemo( + () => ({ + root: () => navigate({ pathname: paths.root, search: location.search }), + overview: ({ + nextAlgorithmId = algorithmId, + nextTabKey = OVERVIEW_TABS.VERSIONS, + } = {}) => + navigate({ + pathname: paths.overview({ nextAlgorithmId, nextTabKey }), + search: location.search, + }), + edit: ({ nextAlgorithmId = algorithmId } = {}) => + navigate({ + pathname: paths.edit({ nextAlgorithmId }), + search: location.search, + }), + }), + [navigate, paths, algorithmId, location] + ); + + return { + algorithmId, + goTo, + paths, + tabKey, + }; +}; diff --git a/src/Routes/Tables/index.js b/src/Routes/Tables/index.js index e26c36655..8f7e904de 100644 --- a/src/Routes/Tables/index.js +++ b/src/Routes/Tables/index.js @@ -5,6 +5,7 @@ import { useReactiveVar } from '@apollo/client'; import Jobs from './Jobs'; import QueueOrderJobsV2 from './QueueOrderJobsV2'; import AlgorithmsTable from './Algorithms'; +import MarketplacesTable from './Marketplaces'; import DriversTable from './Drivers'; import PipelinesTable from './Pipelines'; import WorkersTable from './Workers'; @@ -45,6 +46,10 @@ const Body = () => { + }> + + + } /> } /> diff --git a/src/const/drawer-info.js b/src/const/drawer-info.js index 9c01093eb..c85427783 100644 --- a/src/const/drawer-info.js +++ b/src/const/drawer-info.js @@ -3,6 +3,7 @@ export const DRAWER_TITLES = { DRIVERS: 'Drivers', ADD_PIPELINE: 'Pipeline Wizard', ADD_ALGORITHM: 'Algorithm Wizard', + ADD_MARKETPLACE: 'Marketplace Wizard', ADD_DATASOURCE: ' Datasource wizard', EDIT_DATASOURCE: ' Datasource editor', RUN_RAW_PIPELINE: 'Run Pipeline', @@ -15,6 +16,7 @@ export const DRAWER_TITLES = { ALGORITHM_INFO: 'Algorithm Overview', PIPELINE_INFO: 'Pipeline Overview', ALGORITHM_EDIT: ' Algorithm editor', + MARKETPLACE_EDIT: ' Marketplace editor', EDIT_PIPELINE: ' Pipeline editor', PIPELINE_EXECUTE: 'Run Pipeline', TF_RUN: 'TF Run', diff --git a/src/const/drawer-size.js b/src/const/drawer-size.js index 404fec41d..fdf63ef0b 100644 --- a/src/const/drawer-size.js +++ b/src/const/drawer-size.js @@ -1,6 +1,7 @@ export default { ADD_PIPELINE: '85vw', ADD_ALGORITHM: '160ch', + ADD_MARKETPLACE: '160ch', ADD_DATASOURCE: '40vw', EDIT_DATASOURCE: '50vw', RUN_RAW_PIPELINE: '40vw', diff --git a/src/const/sidebar-names.js b/src/const/sidebar-names.js index 29591691a..11bd10c95 100644 --- a/src/const/sidebar-names.js +++ b/src/const/sidebar-names.js @@ -1,11 +1,13 @@ export const NEW_ITEM_PAGE = { PIPELINE: 'pipelines', ALGORITHM: 'algorithms', + MARKETPLACE: 'marketplace', DATASOURCE: 'datasources', }; export const NEW_ITEM = { pipelines: 'Pipeline', algorithms: 'Algorithm', + marketplace: '', datasources: 'Datasource', }; @@ -14,6 +16,7 @@ export const LEFT_SIDEBAR_NAMES = { QUEUE: 'queue', PIPELINES: 'pipelines', ALGORITHMS: 'algorithms', + MARKETPLACE: 'marketplace', WORKERS: 'workers', DRIVERS: 'drivers', DATASOURCES: 'datasources', @@ -22,6 +25,7 @@ export const LEFT_SIDEBAR_NAMES = { export const RIGHT_SIDEBAR_NAMES = { ADD_PIPELINE: 'add-pipeline', ADD_ALGORITHM: 'add-algorithm', + ADD_MARKETPLACE: 'add-marketplace', ADD_DATASOURCE: 'add-datasource', RUN_RAW_PIPELINE: 'run-raw-pipeline', diff --git a/src/images/marketplace.svg b/src/images/marketplace.svg new file mode 100644 index 000000000..8841dfa5a --- /dev/null +++ b/src/images/marketplace.svg @@ -0,0 +1 @@ +