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) 2022-2023 CVAT.ai Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
import Tag from 'antd/lib/tag';
|
||||
import Select from 'antd/lib/select';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import { Model } from 'reducers';
|
||||
import CVATTooltip from 'components/common/cvat-tooltip';
|
||||
import { MoreOutlined } from '@ant-design/icons';
|
||||
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 {
|
||||
model: Model;
|
||||
model: MLModel;
|
||||
}
|
||||
|
||||
export default function DeployedModelItem(props: Props): JSX.Element {
|
||||
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 (
|
||||
<Row className='cvat-models-list-item'>
|
||||
<Col span={3}>
|
||||
<Tag color='purple'>{model.framework}</Tag>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<CVATTooltip overlay={model.name}>
|
||||
<Text className='cvat-text-color'>{model.name}</Text>
|
||||
</CVATTooltip>
|
||||
</Col>
|
||||
<Col span={3} offset={1}>
|
||||
<Tag color='orange'>{model.type}</Tag>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<CVATTooltip overlay={model.description}>
|
||||
<Text style={{ whiteSpace: 'normal', height: 'auto' }}>{model.description}</Text>
|
||||
</CVATTooltip>
|
||||
</Col>
|
||||
<Col span={5} offset={1}>
|
||||
<Select showSearch placeholder='Supported labels' style={{ width: '90%' }} value='Supported labels'>
|
||||
{model.labels.map(
|
||||
(label): JSX.Element => (
|
||||
<Select.Option value={label} key={label}>
|
||||
{label}
|
||||
</Select.Option>
|
||||
),
|
||||
<>
|
||||
<Modal
|
||||
className='cvat-model-info-modal'
|
||||
title='Model'
|
||||
visible={isModalShown}
|
||||
onCancel={onCloseModel}
|
||||
footer={null}
|
||||
>
|
||||
<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'
|
||||
/>
|
||||
{icon ? <div className='cvat-model-item-provider-inner'>{icon}</div> : null}
|
||||
<div className='cvat-model-info-container'>
|
||||
<Title level={3}>{model.name}</Title>
|
||||
<Text type='secondary'>{`Added ${created}`}</Text>
|
||||
</div>
|
||||
<Divider />
|
||||
{
|
||||
model.labels?.length ? (
|
||||
<>
|
||||
<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) 2022-2023 CVAT.ai Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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 EmptyListComponent from './empty-list';
|
||||
import FeedbackComponent from '../feedback/feedback';
|
||||
import { Model } from '../../reducers';
|
||||
import { CombinedState } from '../../reducers';
|
||||
import TopBar from './top-bar';
|
||||
|
||||
interface Props {
|
||||
interactors: Model[];
|
||||
detectors: Model[];
|
||||
trackers: Model[];
|
||||
reid: Model[];
|
||||
}
|
||||
function ModelsPageComponent(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const dispatch = useDispatch();
|
||||
const fetching = useSelector((state: CombinedState) => state.models.fetching);
|
||||
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 {
|
||||
interactors, detectors, trackers, reid,
|
||||
} = props;
|
||||
const updatedQuery = { ...query };
|
||||
useEffect(() => {
|
||||
history.replace({
|
||||
search: updateHistoryFromQuery(query),
|
||||
});
|
||||
}, [query]);
|
||||
|
||||
const deployedModels = [...detectors, ...interactors, ...trackers, ...reid];
|
||||
useEffect(() => {
|
||||
dispatch(getModelProvidersAsync());
|
||||
dispatch(getModelsAsync());
|
||||
}, []);
|
||||
|
||||
const content = totalCount ? (
|
||||
<DeployedModelsList />
|
||||
) : <EmptyListComponent />;
|
||||
|
||||
return (
|
||||
<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 />
|
||||
</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