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 ? (
+
+ }
+ onClick={() => clickOnRunAlg()}
+ />
+
+ ) : (
+ NOTVIEW && (
+
+ }
+ disabled={!isRoleRunOrStop}
+ />
+
+ )
+ )}
+ {NOTVIEW && isRoleRunOrStop ? (
+
+ } onClick={() => clickOnRunDebug()} />
+
+ ) : (
+ NOTVIEW && (
+
+ } disabled={!isRoleRunOrStop} />
+
+ )
+ )}
+
+ }
+ onClick={onEdit}
+ disabled={!isRoleEdit}
+ />
+
+ {NOTVIEW && (
+
+ }
+ onClick={onClickDelete}
+ disabled={!isRoleDelete}
+ />
+
+ )}
+
+ } onClick={onMoreInfo} />
+
+
+
+ );
+};
+
+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} />
+
+ ))}
+
+ }
+ size="small"
+ type="primary"
+ onClick={appendKey}>
+ Add Input
+
+
+ >
+ );
+};
+
+/**
+ * @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 (
+
+ );
+};
+
+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 ? (
+ }
+ onClick={() => cancelBuild(record.buildId)}
+ />
+ ) : (
+ }
+ onClick={() => rerunBuild(record.buildId)}
+ />
+ );
+ },
+ },
+];
+
+export default getColumns;
diff --git a/src/Routes/Tables/Marketplaces/Tabs/Builds/index.js b/src/Routes/Tables/Marketplaces/Tabs/Builds/index.js
new file mode 100644
index 000000000..f6d2bf6bb
--- /dev/null
+++ b/src/Routes/Tables/Marketplaces/Tabs/Builds/index.js
@@ -0,0 +1,127 @@
+import { Empty, Button } from 'antd';
+import { Table } from 'components';
+import { Card, JsonView, LogsViewer, Tabs } from 'components/common';
+import { useActions } from 'hooks';
+import PropTypes from 'prop-types';
+import React, { useEffect, useState, useCallback } from 'react';
+import styled from 'styled-components';
+import {
+ DownOutlined,
+ RightOutlined,
+ DownloadOutlined,
+} from '@ant-design/icons';
+import getColumns from './getColumns';
+
+const IDs = {
+ LOGS: 'Logs',
+ INFO: 'Information',
+};
+
+const CardOverflow = styled(Card)`
+ max-height: 60vh;
+ min-height: 20em;
+ padding-bottom: 20px;
+`;
+
+const downloadLogsAsText = async (buildIdStr, textContent) => {
+ const blob = new Blob(textContent, { type: 'text/plain' });
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ const currentDate = new Date().toISOString();
+ link.download = `logs_BuildId_${buildIdStr}_Date_${currentDate}.txt`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+};
+
+const Builds = ({ builds = [], isOpenFirstLog = false }) => {
+ const { cancelBuild, rerunBuild } = useActions();
+ const [currentTime, setCurrentTime] = useState(Date.now());
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setCurrentTime(Date.now());
+ }, 1000);
+ return () => clearInterval(intervalId);
+ }, [setCurrentTime]);
+
+ const TabsItemsJson = useCallback(
+ record => [
+ {
+ label: IDs.LOGS,
+ key: IDs.LOGS,
+ children: (
+
+ {record.result && record.result.data ? (
+
+ ) : (
+
+ )}
+
+ ),
+ },
+ {
+ label: IDs.INFO,
+ key: IDs.INFO,
+ children: (
+
+
+
+ ),
+ },
+ ],
+ []
+ );
+
+ const expandedRowRender = record => (
+
+
+ downloadLogsAsText(
+ record.buildId,
+ record.result.data.split('\r\n\n')
+ )
+ }>
+
+
+ }
+ />
+
+ );
+ 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 && (
+ }>
+ Apply Markdown
+
+ )
+ ) : activeKey === TABS.VERSIONS ? (
+ <>
+ }>
+ Refresh
+ {' '}
+ }>
+ Compare
+
+ >
+ ) : 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 @@
+