task-item.tsx 12.0 KB
Newer Older
B
Boris Sekachev 已提交
1
// Copyright (C) 2020-2022 Intel Corporation
2
// Copyright (C) 2022-2023 CVAT.ai Corporation
B
Boris Sekachev 已提交
3 4 5
//
// SPDX-License-Identifier: MIT

6
import React from 'react';
7 8
import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom';
9
import Text from 'antd/lib/typography/Text';
10 11
import { Row, Col } from 'antd/lib/grid';
import Button from 'antd/lib/button';
12
import { LoadingOutlined, MoreOutlined } from '@ant-design/icons';
13 14
import Dropdown from 'antd/lib/dropdown';
import Progress from 'antd/lib/progress';
15
import Badge from 'antd/lib/badge';
16 17
import moment from 'moment';

18
import { Task, RQStatus } from 'cvat-core-wrapper';
19
import ActionsMenuContainer from 'containers/actions-menu/actions-menu';
20
import Preview from 'components/common/preview';
21
import { ActiveInference, PluginComponent } from 'reducers';
22
import AutomaticAnnotationProgress from './automatic-annotation-progress';
23

24
export interface TaskItemProps {
25
    taskInstance: any;
26
    deleted: boolean;
27
    activeInference: ActiveInference | null;
28
    ribbonPlugins: PluginComponent[];
29
    cancelAutoAnnotation(): void;
30
    updateTaskInState(task: Task): void;
31 32
}

33 34
interface State {
    importingState: {
B
Boris Sekachev 已提交
35
        state: RQStatus | null;
36 37 38 39 40 41 42 43 44 45 46 47 48 49
        message: string;
        progress: number;
    } | null;
}

class TaskItemComponent extends React.PureComponent<TaskItemProps & RouteComponentProps, State> {
    #isUnmounted: boolean;

    constructor(props: TaskItemProps & RouteComponentProps) {
        super(props);
        const { taskInstance } = props;
        this.#isUnmounted = false;
        this.state = {
            importingState: taskInstance.size > 0 ? null : {
B
Boris Sekachev 已提交
50
                state: null,
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
                message: 'Request current progress',
                progress: 0,
            },
        };
    }

    public componentDidMount(): void {
        const { taskInstance, updateTaskInState } = this.props;
        const { importingState } = this.state;

        if (importingState !== null) {
            taskInstance.listenToCreate((state: RQStatus, progress: number, message: string) => {
                if (!this.#isUnmounted) {
                    this.setState({
                        importingState: {
                            message,
                            progress: Math.floor(progress * 100),
                            state,
                        },
                    });
                }
            }).then((createdTask: Task) => {
                if (!this.#isUnmounted) {
                    this.setState({ importingState: null });
                    setTimeout(() => {
                        const { taskInstance: currentTaskInstance } = this.props;
                        if (currentTaskInstance.size !== createdTask.size) {
                            // update state only if it was not updated anywhere else
                            // for example in createTaskAsync
                            updateTaskInState(createdTask);
                        }
                    }, 1000);
                }
            });
        }
    }

    public componentWillUnmount(): void {
        this.#isUnmounted = true;
    }

B
Boris Sekachev 已提交
92
    private renderPreview(): JSX.Element {
93
        const { taskInstance } = this.props;
94
        return (
95
            <Col span={4}>
96 97 98 99 100 101 102
                <Preview
                    task={taskInstance}
                    loadingClassName='cvat-task-item-loading-preview'
                    emptyPreviewClassName='cvat-task-item-empty-preview'
                    previewWrapperClassName='cvat-task-item-preview-wrapper'
                    previewClassName='cvat-task-item-preview'
                />
103
            </Col>
B
Boris Sekachev 已提交
104
        );
105 106
    }

B
Boris Sekachev 已提交
107
    private renderDescription(): JSX.Element {
108
        // Task info
109
        const { taskInstance } = this.props;
B
Boris Sekachev 已提交
110 111 112 113
        const { id } = taskInstance;
        const owner = taskInstance.owner ? taskInstance.owner.username : null;
        const updated = moment(taskInstance.updatedDate).fromNow();
        const created = moment(taskInstance.createdDate).format('MMMM Do YYYY');
114 115

        // Get and truncate a task name
B
Boris Sekachev 已提交
116
        const name = `${taskInstance.name.substring(0, 70)}${taskInstance.name.length > 70 ? '...' : ''}`;
117 118

        return (
119
            <Col span={10} className='cvat-task-item-description'>
120
                <Text strong type='secondary' className='cvat-item-task-id'>{`#${id}: `}</Text>
V
Vitaliy Nishukov 已提交
121 122 123
                <Text strong className='cvat-item-task-name'>
                    {name}
                </Text>
B
Boris Sekachev 已提交
124
                <br />
V
Vitaliy Nishukov 已提交
125 126 127 128 129 130
                {owner && (
                    <>
                        <Text type='secondary'>{`Created ${owner ? `by ${owner}` : ''} on ${created}`}</Text>
                        <br />
                    </>
                )}
131
                <Text type='secondary'>{`Last updated ${updated}`}</Text>
132
            </Col>
B
Boris Sekachev 已提交
133
        );
134 135
    }

B
Boris Sekachev 已提交
136
    private renderProgress(): JSX.Element {
V
Vitaliy Nishukov 已提交
137
        const { taskInstance, activeInference, cancelAutoAnnotation } = this.props;
138 139 140 141
        const { importingState } = this.state;

        if (importingState) {
            let textType: 'success' | 'danger' = 'success';
B
Boris Sekachev 已提交
142
            if (!!importingState.state && [RQStatus.FAILED, RQStatus.UNKNOWN].includes(importingState.state)) {
143 144 145 146 147 148 149 150
                textType = 'danger';
            }

            return (
                <Col span={7}>
                    <Row>
                        <Col span={24} className='cvat-task-item-progress-wrapper'>
                            <div>
B
Boris Sekachev 已提交
151 152 153 154
                                <Text
                                    strong
                                    type={[RQStatus.QUEUED, null].includes(importingState.state) ? undefined : textType}
                                >
155
                                    {`\u2022 ${importingState.message || importingState.state}`}
B
Boris Sekachev 已提交
156
                                    { !!importingState.state && [RQStatus.QUEUED, RQStatus.STARTED]
157 158 159 160 161 162 163 164 165 166 167 168 169 170
                                        .includes(importingState.state) && <LoadingOutlined /> }
                                </Text>
                            </div>
                            <Progress
                                percent={importingState.progress}
                                strokeColor='#1890FF'
                                strokeWidth={5}
                                size='small'
                            />
                        </Col>
                    </Row>
                </Col>
            );
        }
171
        // Count number of jobs and performed jobs
172 173
        const numOfJobs = taskInstance.progress.totalJobs;
        const numOfCompleted = taskInstance.progress.completedJobs;
174 175
        const numOfValidation = taskInstance.progress.validationJobs;
        const numOfAnnotation = taskInstance.progress.annotationJobs;
176

177
        // Progress appearance depends on number of jobs
178
        const jobsProgress = ((numOfCompleted + numOfValidation) * 100) / numOfJobs;
179 180

        return (
181
            <Col span={7}>
182
                <Row>
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
                    <Col span={24} className='cvat-task-item-progress-wrapper'>
                        <div>
                            { numOfCompleted > 0 && (
                                <Text strong className='cvat-task-completed-progress'>
                                    {`\u2022 ${numOfCompleted} done `}
                                </Text>
                            )}

                            { numOfValidation > 0 && (
                                <Text strong className='cvat-task-validation-progress'>
                                    {`\u2022 ${numOfValidation} on review `}
                                </Text>
                            )}

                            { numOfAnnotation > 0 && (
                                <Text strong className='cvat-task-annotation-progress'>
                                    {`\u2022 ${numOfAnnotation} annotating `}
                                </Text>
                            )}
                            <Text strong type='secondary'>
                                {`\u2022 ${numOfJobs} total`}
                            </Text>
                        </div>
206
                        <Progress
207 208 209 210
                            percent={jobsProgress}
                            success={{
                                percent: (numOfCompleted * 100) / numOfJobs,
                            }}
211 212 213 214 215
                            strokeColor='#1890FF'
                            showInfo={false}
                            strokeWidth={5}
                            size='small'
                        />
216
                    </Col>
217
                </Row>
218 219 220 221
                <AutomaticAnnotationProgress
                    activeInference={activeInference}
                    cancelAutoAnnotation={cancelAutoAnnotation}
                />
222
            </Col>
B
Boris Sekachev 已提交
223
        );
224 225
    }

B
Boris Sekachev 已提交
226
    private renderNavigation(): JSX.Element {
227
        const { importingState } = this.state;
V
Vitaliy Nishukov 已提交
228
        const { taskInstance, history } = this.props;
B
Boris Sekachev 已提交
229
        const { id } = taskInstance;
230

231 232 233 234
        const onViewAnalytics = (): void => {
            history.push(`/tasks/${taskInstance.id}/analytics`);
        };

235
        return (
236
            <Col span={3}>
B
Boris Sekachev 已提交
237
                <Row justify='end'>
238
                    <Col>
B
Boris Sekachev 已提交
239
                        <Button
240
                            disabled={!!importingState}
241
                            className='cvat-item-open-task-button'
B
Boris Sekachev 已提交
242 243 244
                            type='primary'
                            size='large'
                            ghost
245 246 247
                            href={`/tasks/${id}`}
                            onClick={(e: React.MouseEvent): void => {
                                e.preventDefault();
D
Dmitry Kalinin 已提交
248
                                history.push(`/tasks/${id}`);
249
                            }}
B
Boris Sekachev 已提交
250 251 252
                        >
                            Open
                        </Button>
253 254
                    </Col>
                </Row>
B
Boris Sekachev 已提交
255
                <Row justify='end'>
256 257 258 259 260 261 262 263
                    <Dropdown
                        destroyPopupOnHide
                        overlay={(
                            <ActionsMenuContainer
                                taskInstance={taskInstance}
                                onViewAnalytics={onViewAnalytics}
                            />
                        )}
264
                    >
265 266
                        <Col className='cvat-item-open-task-actions'>
                            <Text className='cvat-text-color'>Actions</Text>
B
Boris Sekachev 已提交
267
                            <MoreOutlined className='cvat-menu-icon' />
268 269
                        </Col>
                    </Dropdown>
270 271
                </Row>
            </Col>
B
Boris Sekachev 已提交
272
        );
273 274
    }

B
Boris Sekachev 已提交
275
    public render(): JSX.Element {
276 277
        const { deleted, ribbonPlugins } = this.props;

278
        const style = {};
B
Boris Sekachev 已提交
279
        if (deleted) {
280 281 282 283
            (style as any).pointerEvents = 'none';
            (style as any).opacity = 0.5;
        }

284 285 286 287
        const ribbonItems = ribbonPlugins
            .filter((plugin) => plugin.data.shouldBeRendered(this.props, this.state))
            .map((plugin) => ({ component: plugin.component, weight: plugin.data.weight }));

288
        return (
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
            <Badge.Ribbon
                style={{ visibility: ribbonItems.length ? 'visible' : 'hidden' }}
                className='cvat-task-item-ribbon'
                placement='start'
                text={(
                    <div>
                        {ribbonItems.sort((item1, item2) => item1.weight - item2.weight)
                            .map((item) => item.component).map((Component, index) => (
                                <Component key={index} targetProps={this.props} targetState={this.state} />
                            ))}
                    </div>
                )}
            >
                <Row className='cvat-tasks-list-item' justify='center' align='top' style={{ ...style }}>
                    {this.renderPreview()}
                    {this.renderDescription()}
                    {this.renderProgress()}
                    {this.renderNavigation()}
                </Row>
            </Badge.Ribbon>
B
Boris Sekachev 已提交
309 310
        );
    }
311
}
312 313

export default withRouter(TaskItemComponent);