New models UI (#5635)
parent
91b36ce393
commit
3775bc2557
@ -0,0 +1,48 @@
|
|||||||
|
// Copyright (C) 2022-2023 CVAT.ai Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import './styles.scss';
|
||||||
|
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Row, Col } from 'antd/lib/grid';
|
||||||
|
import Text from 'antd/lib/typography/Text';
|
||||||
|
import Spin from 'antd/lib/spin';
|
||||||
|
import { CombinedState } from 'reducers';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { getModelProvidersAsync } from 'actions/models-actions';
|
||||||
|
import ModelForm from './model-form';
|
||||||
|
|
||||||
|
function CreateModelPage(): JSX.Element {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const fetching = useSelector((state: CombinedState) => state.models.providers.fetching);
|
||||||
|
const providers = useSelector((state: CombinedState) => state.models.providers.list);
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(getModelProvidersAsync());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='cvat-create-model-page'>
|
||||||
|
<Row justify='center' align='middle'>
|
||||||
|
<Col>
|
||||||
|
<Text className='cvat-title'>Add a model</Text>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{
|
||||||
|
fetching ? (
|
||||||
|
<div className='cvat-empty-webhooks-list'>
|
||||||
|
<Spin size='large' className='cvat-spinner' />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Row justify='center' align='top'>
|
||||||
|
<Col md={20} lg={16} xl={14} xxl={9}>
|
||||||
|
<ModelForm providers={providers} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(CreateModelPage);
|
||||||
@ -0,0 +1,162 @@
|
|||||||
|
// Copyright (C) 2023 CVAT.ai Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import './styles.scss';
|
||||||
|
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { Store } from 'antd/lib/form/interface';
|
||||||
|
import { Row, Col } from 'antd/lib/grid';
|
||||||
|
import Form from 'antd/lib/form';
|
||||||
|
import Button from 'antd/lib/button';
|
||||||
|
import Select from 'antd/lib/select';
|
||||||
|
import notification from 'antd/lib/notification';
|
||||||
|
import Input from 'antd/lib/input/Input';
|
||||||
|
|
||||||
|
import { CombinedState } from 'reducers';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createModelAsync } from 'actions/models-actions';
|
||||||
|
import { ModelProvider } from 'cvat-core-wrapper';
|
||||||
|
import ModelProviderIcon from 'components/models-page/model-provider-icon';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
providers: ModelProvider[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProviderFormItems(providerAttributes: Record<string, string>): JSX.Element {
|
||||||
|
delete providerAttributes.url;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
Object.entries(providerAttributes).map(([key, text]) => (
|
||||||
|
<Form.Item
|
||||||
|
key={key}
|
||||||
|
name={key}
|
||||||
|
label={text}
|
||||||
|
rules={[{ required: true, message: `Please, specify ${text}` }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModelForm(props: Props): JSX.Element {
|
||||||
|
const { providers } = props;
|
||||||
|
const providerList = providers.map((provider) => ({
|
||||||
|
value: provider.name,
|
||||||
|
text: provider.name.charAt(0).toUpperCase() + provider.name.slice(1),
|
||||||
|
}));
|
||||||
|
const providerMap = Object.fromEntries(providers.map((provider) => [provider.name, provider.attributes]));
|
||||||
|
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const history = useHistory();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const fetching = useSelector((state: CombinedState) => state.models.fetching);
|
||||||
|
const [currentProviderForm, setCurrentProviderForm] = useState<JSX.Element | null>(null);
|
||||||
|
const onChangeProviderValue = useCallback((provider: string) => {
|
||||||
|
setCurrentProviderForm(createProviderFormItems(providerMap[provider]));
|
||||||
|
const emptiedKeys: Record<string, string | null> = { ...providerMap[provider] };
|
||||||
|
Object.keys(providerMap[provider]).forEach((k) => { emptiedKeys[k] = null; });
|
||||||
|
form.setFieldsValue(emptiedKeys);
|
||||||
|
}, []);
|
||||||
|
const [providerTouched, setProviderTouched] = useState(false);
|
||||||
|
const [currentUrlEmpty, setCurrentUrlEmpty] = useState(true);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const values: Store = await form.validateFields();
|
||||||
|
await dispatch(createModelAsync(values));
|
||||||
|
form.resetFields();
|
||||||
|
setCurrentProviderForm(null);
|
||||||
|
setProviderTouched(false);
|
||||||
|
setCurrentUrlEmpty(true);
|
||||||
|
notification.info({
|
||||||
|
message: 'Model has been successfully created',
|
||||||
|
className: 'cvat-notification-create-model-success',
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (e) {}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row className='cvat-create-model-form-wrapper'>
|
||||||
|
<Col span={24}>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout='vertical'
|
||||||
|
>
|
||||||
|
<Col>
|
||||||
|
<Form.Item
|
||||||
|
name='url'
|
||||||
|
label='Model URL'
|
||||||
|
rules={[{ required: true, message: 'Please, specify Model URL' }]}
|
||||||
|
>
|
||||||
|
<Input onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { value } = event.target;
|
||||||
|
const guessedProvider = providers.find((provider) => value.includes(provider.name));
|
||||||
|
if (guessedProvider && !providerTouched) {
|
||||||
|
form.setFieldsValue({ provider: guessedProvider.name });
|
||||||
|
setCurrentProviderForm(createProviderFormItems(providerMap[guessedProvider.name]));
|
||||||
|
}
|
||||||
|
setCurrentUrlEmpty(!value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
{
|
||||||
|
!currentUrlEmpty && (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label='Provider'
|
||||||
|
name='provider'
|
||||||
|
rules={[{ required: true, message: 'Please, specify model provider' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
virtual={false}
|
||||||
|
onChange={onChangeProviderValue}
|
||||||
|
className='cvat-select-model-provider'
|
||||||
|
onSelect={() => { setProviderTouched(true); }}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
providerList.map(({ value, text }) => (
|
||||||
|
<Select.Option value={value} key={value}>
|
||||||
|
<div className='cvat-model-provider-icon'>
|
||||||
|
<ModelProviderIcon providerName={value} />
|
||||||
|
<span className='cvat-cloud-storage-select-provider'>
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Select.Option>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
{currentProviderForm}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Form>
|
||||||
|
</Col>
|
||||||
|
<Col span={24} className='cvat-create-models-actions'>
|
||||||
|
<Row justify='end'>
|
||||||
|
<Col>
|
||||||
|
<Button onClick={() => history.goBack()}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
<Col offset={1}>
|
||||||
|
<Button type='primary' onClick={handleSubmit} loading={fetching} disabled={currentUrlEmpty}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(ModelForm);
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
// Copyright (C) 2022-2023 CVAT.ai Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
@import '../../base.scss';
|
||||||
|
|
||||||
|
.cvat-create-model-page {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding-top: $grid-unit-size * 5;
|
||||||
|
|
||||||
|
.cvat-title {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvat-create-model-form-wrapper {
|
||||||
|
margin-top: $grid-unit-size * 3;
|
||||||
|
height: auto;
|
||||||
|
border: 1px solid $border-color-1;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: $grid-unit-size * 3;
|
||||||
|
background: $background-color-1;
|
||||||
|
text-align: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvat-create-models-actions {
|
||||||
|
margin-top: $grid-unit-size * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvat-model-provider-icon {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-top: 3px;
|
||||||
|
margin-right: $grid-unit-size;
|
||||||
|
width: $grid-unit-size * 2;
|
||||||
|
height: $grid-unit-size * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvat-select-model-provider {
|
||||||
|
img {
|
||||||
|
margin-top: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,51 +1,159 @@
|
|||||||
// Copyright (C) 2020-2022 Intel Corporation
|
// Copyright (C) 2020-2022 Intel Corporation
|
||||||
|
// Copyright (C) 2022-2023 CVAT.ai Corporation
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { Row, Col } from 'antd/lib/grid';
|
import { Row, Col } from 'antd/lib/grid';
|
||||||
import Tag from 'antd/lib/tag';
|
import Tag from 'antd/lib/tag';
|
||||||
import Select from 'antd/lib/select';
|
|
||||||
import Text from 'antd/lib/typography/Text';
|
import Text from 'antd/lib/typography/Text';
|
||||||
import { Model } from 'reducers';
|
import { MoreOutlined } from '@ant-design/icons';
|
||||||
import CVATTooltip from 'components/common/cvat-tooltip';
|
import Modal from 'antd/lib/modal';
|
||||||
|
import { MLModel, ModelProviders } from 'cvat-core-wrapper';
|
||||||
|
import Title from 'antd/lib/typography/Title';
|
||||||
|
import Meta from 'antd/lib/card/Meta';
|
||||||
|
import Preview from 'components/common/preview';
|
||||||
|
import moment from 'moment';
|
||||||
|
import Divider from 'antd/lib/divider';
|
||||||
|
import Card from 'antd/lib/card';
|
||||||
|
import Dropdown from 'antd/lib/dropdown';
|
||||||
|
import Button from 'antd/lib/button';
|
||||||
|
import ModelActionsMenuComponent from './models-action-menu';
|
||||||
|
import ModelProviderIcon from './model-provider-icon';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
model: Model;
|
model: MLModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DeployedModelItem(props: Props): JSX.Element {
|
export default function DeployedModelItem(props: Props): JSX.Element {
|
||||||
const { model } = props;
|
const { model } = props;
|
||||||
|
const { provider } = model;
|
||||||
|
const [isRemoved, setIsRemoved] = useState(false);
|
||||||
|
const [isModalShown, setIsModalShown] = useState(false);
|
||||||
|
|
||||||
|
const onOpenModel = () => {
|
||||||
|
setIsModalShown(true);
|
||||||
|
};
|
||||||
|
const onCloseModel = () => {
|
||||||
|
setIsModalShown(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = useCallback(() => {
|
||||||
|
setIsRemoved(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const created = moment(model.createdDate).fromNow();
|
||||||
|
const icon = <ModelProviderIcon providerName={provider} />;
|
||||||
return (
|
return (
|
||||||
<Row className='cvat-models-list-item'>
|
<>
|
||||||
<Col span={3}>
|
<Modal
|
||||||
<Tag color='purple'>{model.framework}</Tag>
|
className='cvat-model-info-modal'
|
||||||
</Col>
|
title='Model'
|
||||||
<Col span={3}>
|
visible={isModalShown}
|
||||||
<CVATTooltip overlay={model.name}>
|
onCancel={onCloseModel}
|
||||||
<Text className='cvat-text-color'>{model.name}</Text>
|
footer={null}
|
||||||
</CVATTooltip>
|
>
|
||||||
</Col>
|
<Preview
|
||||||
<Col span={3} offset={1}>
|
model={model}
|
||||||
<Tag color='orange'>{model.type}</Tag>
|
loadingClassName='cvat-model-item-loading-preview'
|
||||||
</Col>
|
emptyPreviewClassName='cvat-model-item-empty-preview'
|
||||||
<Col span={8}>
|
previewWrapperClassName='cvat-models-item-card-preview-wrapper'
|
||||||
<CVATTooltip overlay={model.description}>
|
previewClassName='cvat-models-item-card-preview'
|
||||||
<Text style={{ whiteSpace: 'normal', height: 'auto' }}>{model.description}</Text>
|
/>
|
||||||
</CVATTooltip>
|
{icon ? <div className='cvat-model-item-provider-inner'>{icon}</div> : null}
|
||||||
</Col>
|
<div className='cvat-model-info-container'>
|
||||||
<Col span={5} offset={1}>
|
<Title level={3}>{model.name}</Title>
|
||||||
<Select showSearch placeholder='Supported labels' style={{ width: '90%' }} value='Supported labels'>
|
<Text type='secondary'>{`Added ${created}`}</Text>
|
||||||
{model.labels.map(
|
</div>
|
||||||
(label): JSX.Element => (
|
<Divider />
|
||||||
<Select.Option value={label} key={label}>
|
{
|
||||||
{label}
|
model.labels?.length ? (
|
||||||
</Select.Option>
|
<>
|
||||||
),
|
<div className='cvat-model-info-container'>
|
||||||
|
<Text className='cvat-model-info-modal-labels-title'>Labels:</Text>
|
||||||
|
</div>
|
||||||
|
<div className='cvat-model-info-container cvat-model-info-modal-labels-list'>
|
||||||
|
{model.labels.map((label) => <Tag key={label}>{label}</Tag>)}
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
</>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
<Row justify='space-between' className='cvat-model-info-container'>
|
||||||
|
<Col span={15}>
|
||||||
|
<Row>
|
||||||
|
<Col span={8}>
|
||||||
|
<Text strong>Provider</Text>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Text strong>Type</Text>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col span={8}>
|
||||||
|
{model.provider}
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
{model.kind}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
{model.owner && (
|
||||||
|
<Col>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<Text strong>Owner</Text>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
{model.owner}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
</Modal>
|
||||||
|
<Card
|
||||||
|
cover={(
|
||||||
|
<Preview
|
||||||
|
model={model}
|
||||||
|
loadingClassName='cvat-model-item-loading-preview'
|
||||||
|
emptyPreviewClassName='cvat-model-item-empty-preview'
|
||||||
|
previewWrapperClassName='cvat-models-item-card-preview-wrapper'
|
||||||
|
previewClassName='cvat-models-item-card-preview'
|
||||||
|
onClick={onOpenModel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
size='small'
|
||||||
|
className={`cvat-models-item-card ${isRemoved ? 'cvat-models-item-card-removed' : ''} `}
|
||||||
|
>
|
||||||
|
<Meta
|
||||||
|
title={(
|
||||||
|
<span onClick={onOpenModel} className='cvat-models-item-title' aria-hidden>
|
||||||
|
{model.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
description={(
|
||||||
|
<div className='cvat-models-item-description'>
|
||||||
|
<Row onClick={onOpenModel} className='cvat-models-item-text-description'>
|
||||||
|
{model.owner && (<Text strong>{model.owner}</Text>)}
|
||||||
|
<Text type='secondary'>{` Added ${created}`}</Text>
|
||||||
|
</Row>
|
||||||
|
{
|
||||||
|
model.provider !== ModelProviders.CVAT && (
|
||||||
|
<Dropdown overlay={<ModelActionsMenuComponent model={model} onDelete={onDelete} />}>
|
||||||
|
<Button type='link' size='large' icon={<MoreOutlined />} />
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Select>
|
/>
|
||||||
</Col>
|
{
|
||||||
</Row>
|
icon ? <div className='cvat-model-item-provider'>{icon}</div> : null
|
||||||
|
}
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (C) 2023 CVAT.ai Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { ModelProvider } from 'cvat-core-wrapper';
|
||||||
|
import { CombinedState } from 'reducers';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
providerName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ModelProviderIcon(props: Props): JSX.Element | null {
|
||||||
|
const { providerName } = props;
|
||||||
|
const providers = useSelector((state: CombinedState) => state.models.providers.list);
|
||||||
|
|
||||||
|
let icon: JSX.Element | null = null;
|
||||||
|
const providerInstance = providers.find((_provider: ModelProvider) => _provider.name === providerName);
|
||||||
|
if (providerInstance) {
|
||||||
|
icon = (
|
||||||
|
<img
|
||||||
|
src={`data:image/svg+xml;utf8,${encodeURIComponent(providerInstance.icon)}`}
|
||||||
|
alt={providerName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
// Copyright (C) 2023 CVAT.ai Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import Modal from 'antd/lib/modal';
|
||||||
|
import Menu from 'antd/lib/menu';
|
||||||
|
import { MLModel, ModelProviders } from 'cvat-core-wrapper';
|
||||||
|
import { deleteModelAsync } from 'actions/models-actions';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
model: MLModel;
|
||||||
|
onDelete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ModelActionsMenuComponent(props: Props): JSX.Element {
|
||||||
|
const { model, onDelete } = props;
|
||||||
|
const { provider } = model;
|
||||||
|
const cvatProvider = provider === ModelProviders.CVAT;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const onDeleteModel = useCallback((): void => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: `The model ${model.name} will be deleted`,
|
||||||
|
content: 'You will not be able to use it anymore. Continue?',
|
||||||
|
className: 'cvat-modal-confirm-remove-model',
|
||||||
|
onOk: () => {
|
||||||
|
dispatch(deleteModelAsync(model));
|
||||||
|
onDelete();
|
||||||
|
},
|
||||||
|
okButtonProps: {
|
||||||
|
type: 'primary',
|
||||||
|
danger: true,
|
||||||
|
},
|
||||||
|
okText: 'Delete',
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onOpenUrl = useCallback((): void => {
|
||||||
|
window.open(model.url, '_blank');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu selectable={false} className='cvat-project-actions-menu'>
|
||||||
|
{
|
||||||
|
!cvatProvider && (
|
||||||
|
<Menu.Item key='open' onClick={onOpenUrl}>
|
||||||
|
Open model URL
|
||||||
|
</Menu.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!cvatProvider && (
|
||||||
|
<>
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Item key='delete' onClick={onDeleteModel}>
|
||||||
|
Delete
|
||||||
|
</Menu.Item>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
// Copyright (C) 2022-2023 CVAT.ai Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { Config } from 'react-awesome-query-builder';
|
||||||
|
|
||||||
|
export const config: Partial<Config> = {
|
||||||
|
fields: {
|
||||||
|
description: {
|
||||||
|
label: 'Description',
|
||||||
|
type: 'text',
|
||||||
|
valueSources: ['value'],
|
||||||
|
operators: ['like'],
|
||||||
|
},
|
||||||
|
target_url: {
|
||||||
|
label: 'Target URL',
|
||||||
|
type: 'text',
|
||||||
|
valueSources: ['value'],
|
||||||
|
operators: ['like'],
|
||||||
|
},
|
||||||
|
owner: {
|
||||||
|
label: 'Owner',
|
||||||
|
type: 'text',
|
||||||
|
valueSources: ['value'],
|
||||||
|
operators: ['equal'],
|
||||||
|
},
|
||||||
|
updated_date: {
|
||||||
|
label: 'Last updated',
|
||||||
|
type: 'datetime',
|
||||||
|
operators: ['between', 'greater', 'greater_or_equal', 'less', 'less_or_equal'],
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
label: 'Type',
|
||||||
|
type: 'select',
|
||||||
|
valueSources: ['value'],
|
||||||
|
fieldSettings: {
|
||||||
|
listValues: [
|
||||||
|
{ value: 'organization', title: 'Organization' },
|
||||||
|
{ value: 'project', title: 'Project' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
label: 'ID',
|
||||||
|
type: 'number',
|
||||||
|
operators: ['equal', 'between', 'greater', 'greater_or_equal', 'less', 'less_or_equal'],
|
||||||
|
fieldSettings: { min: 0 },
|
||||||
|
valueSources: ['value'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const localStorageRecentCapacity = 10;
|
||||||
|
export const localStorageRecentKeyword = 'recentlyAppliedWebhooksFilters';
|
||||||
|
export const predefinedFilterValues = {};
|
||||||
@ -1,33 +1,91 @@
|
|||||||
// Copyright (C) 2020-2022 Intel Corporation
|
// Copyright (C) 2020-2022 Intel Corporation
|
||||||
|
// Copyright (C) 2022-2023 CVAT.ai Corporation
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import './styles.scss';
|
import './styles.scss';
|
||||||
import React from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { getModelProvidersAsync, getModelsAsync } from 'actions/models-actions';
|
||||||
|
import { updateHistoryFromQuery } from 'components/resource-sorting-filtering';
|
||||||
|
import Spin from 'antd/lib/spin';
|
||||||
|
|
||||||
import DeployedModelsList from './deployed-models-list';
|
import DeployedModelsList from './deployed-models-list';
|
||||||
import EmptyListComponent from './empty-list';
|
import EmptyListComponent from './empty-list';
|
||||||
import FeedbackComponent from '../feedback/feedback';
|
import FeedbackComponent from '../feedback/feedback';
|
||||||
import { Model } from '../../reducers';
|
import { CombinedState } from '../../reducers';
|
||||||
|
import TopBar from './top-bar';
|
||||||
|
|
||||||
interface Props {
|
function ModelsPageComponent(): JSX.Element {
|
||||||
interactors: Model[];
|
const history = useHistory();
|
||||||
detectors: Model[];
|
const dispatch = useDispatch();
|
||||||
trackers: Model[];
|
const fetching = useSelector((state: CombinedState) => state.models.fetching);
|
||||||
reid: Model[];
|
const query = useSelector((state: CombinedState) => state.models.query);
|
||||||
}
|
const totalCount = useSelector((state: CombinedState) => state.models.totalCount);
|
||||||
|
|
||||||
|
const onCreateModel = useCallback(() => {
|
||||||
|
history.push('/models/create');
|
||||||
|
}, []);
|
||||||
|
|
||||||
export default function ModelsPageComponent(props: Props): JSX.Element {
|
const updatedQuery = { ...query };
|
||||||
const {
|
useEffect(() => {
|
||||||
interactors, detectors, trackers, reid,
|
history.replace({
|
||||||
} = props;
|
search: updateHistoryFromQuery(query),
|
||||||
|
});
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
const deployedModels = [...detectors, ...interactors, ...trackers, ...reid];
|
useEffect(() => {
|
||||||
|
dispatch(getModelProvidersAsync());
|
||||||
|
dispatch(getModelsAsync());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const content = totalCount ? (
|
||||||
|
<DeployedModelsList />
|
||||||
|
) : <EmptyListComponent />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='cvat-models-page'>
|
<div className='cvat-models-page'>
|
||||||
{deployedModels.length ? <DeployedModelsList models={deployedModels} /> : <EmptyListComponent />}
|
<TopBar
|
||||||
|
disabled
|
||||||
|
query={updatedQuery}
|
||||||
|
onCreateModel={onCreateModel}
|
||||||
|
onApplySearch={(search: string | null) => {
|
||||||
|
dispatch(
|
||||||
|
getModelsAsync({
|
||||||
|
...query,
|
||||||
|
search,
|
||||||
|
page: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onApplyFilter={(filter: string | null) => {
|
||||||
|
dispatch(
|
||||||
|
getModelsAsync({
|
||||||
|
...query,
|
||||||
|
filter,
|
||||||
|
page: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onApplySorting={(sorting: string | null) => {
|
||||||
|
dispatch(
|
||||||
|
getModelsAsync({
|
||||||
|
...query,
|
||||||
|
sort: sorting,
|
||||||
|
page: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{ fetching ? (
|
||||||
|
<div className='cvat-empty-models-list'>
|
||||||
|
<Spin size='large' className='cvat-spinner' />
|
||||||
|
</div>
|
||||||
|
) : content }
|
||||||
<FeedbackComponent />
|
<FeedbackComponent />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default React.memo(ModelsPageComponent);
|
||||||
|
|||||||
@ -0,0 +1,89 @@
|
|||||||
|
// Copyright (C) 2022-2023 CVAT.ai Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Row, Col } from 'antd/lib/grid';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import Button from 'antd/lib/button';
|
||||||
|
import { Input } from 'antd';
|
||||||
|
import { SortingComponent, ResourceFilterHOC, defaultVisibility } from 'components/resource-sorting-filtering';
|
||||||
|
import { ModelsQuery } from 'reducers';
|
||||||
|
import {
|
||||||
|
localStorageRecentKeyword, localStorageRecentCapacity, config,
|
||||||
|
} from './models-filter-configuration';
|
||||||
|
|
||||||
|
const FilteringComponent = ResourceFilterHOC(
|
||||||
|
config, localStorageRecentKeyword, localStorageRecentCapacity,
|
||||||
|
);
|
||||||
|
|
||||||
|
interface VisibleTopBarProps {
|
||||||
|
onApplyFilter(filter: string | null): void;
|
||||||
|
onApplySorting(sorting: string | null): void;
|
||||||
|
onApplySearch(search: string | null): void;
|
||||||
|
query: ModelsQuery;
|
||||||
|
onCreateModel(): void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TopBarComponent(props: VisibleTopBarProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
query, onApplyFilter, onApplySorting, onApplySearch, onCreateModel, disabled,
|
||||||
|
} = props;
|
||||||
|
const [visibility, setVisibility] = useState(defaultVisibility);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row className='cvat-models-page-top-bar' justify='center' align='middle'>
|
||||||
|
<Col md={22} lg={18} xl={16} xxl={16}>
|
||||||
|
<div className='cvat-models-page-filters-wrapper'>
|
||||||
|
<Input.Search
|
||||||
|
disabled={disabled}
|
||||||
|
enterButton
|
||||||
|
onSearch={(phrase: string) => {
|
||||||
|
onApplySearch(phrase);
|
||||||
|
}}
|
||||||
|
defaultValue={query.search || ''}
|
||||||
|
className='cvat-webhooks-page-search-bar'
|
||||||
|
placeholder='Search ...'
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<SortingComponent
|
||||||
|
disabled={disabled}
|
||||||
|
visible={visibility.sorting}
|
||||||
|
onVisibleChange={(visible: boolean) => (
|
||||||
|
setVisibility({ ...defaultVisibility, sorting: visible })
|
||||||
|
)}
|
||||||
|
defaultFields={query.sort?.split(',') || ['-ID']}
|
||||||
|
sortingFields={['ID', 'Target URL', 'Owner', 'Description', 'Type', 'Updated date']}
|
||||||
|
onApplySorting={onApplySorting}
|
||||||
|
/>
|
||||||
|
<FilteringComponent
|
||||||
|
disabled={disabled}
|
||||||
|
value={query.filter}
|
||||||
|
predefinedVisible={visibility.predefined}
|
||||||
|
builderVisible={visibility.builder}
|
||||||
|
recentVisible={visibility.recent}
|
||||||
|
onPredefinedVisibleChange={(visible: boolean) => (
|
||||||
|
setVisibility({ ...defaultVisibility, predefined: visible })
|
||||||
|
)}
|
||||||
|
onBuilderVisibleChange={(visible: boolean) => (
|
||||||
|
setVisibility({ ...defaultVisibility, builder: visible })
|
||||||
|
)}
|
||||||
|
onRecentVisibleChange={(visible: boolean) => (
|
||||||
|
setVisibility({
|
||||||
|
...defaultVisibility,
|
||||||
|
builder: visibility.builder,
|
||||||
|
recent: visible,
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
onApplyFilter={onApplyFilter}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='cvat-models-add-wrapper'>
|
||||||
|
<Button onClick={onCreateModel} type='primary' className='cvat-create-model' icon={<PlusOutlined />} />
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,31 +0,0 @@
|
|||||||
// Copyright (C) 2020-2022 Intel Corporation
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import ModelsPageComponent from 'components/models-page/models-page';
|
|
||||||
import { Model, CombinedState } from 'reducers';
|
|
||||||
|
|
||||||
interface StateToProps {
|
|
||||||
interactors: Model[];
|
|
||||||
detectors: Model[];
|
|
||||||
trackers: Model[];
|
|
||||||
reid: Model[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapStateToProps(state: CombinedState): StateToProps {
|
|
||||||
const { models } = state;
|
|
||||||
const {
|
|
||||||
interactors, detectors, trackers, reid,
|
|
||||||
} = models;
|
|
||||||
|
|
||||||
return {
|
|
||||||
interactors,
|
|
||||||
detectors,
|
|
||||||
trackers,
|
|
||||||
reid,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, {})(ModelsPageComponent);
|
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
// Copyright (C) 2023 CVAT.ai Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { Indexable } from 'reducers';
|
||||||
|
|
||||||
|
export function filterNull<Type>(obj: Type): Type {
|
||||||
|
const filteredObject = { ...obj };
|
||||||
|
if (filteredObject) {
|
||||||
|
for (const key of Object.keys(filteredObject)) {
|
||||||
|
if ((filteredObject as Indexable)[key] === null) {
|
||||||
|
delete (filteredObject as Indexable)[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredObject;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue