Skip to content

Commit

Permalink
Improve quality UX (#8865)
Browse files Browse the repository at this point in the history
Introduced refined design of quality management page
  • Loading branch information
klakhov authored Jan 16, 2025
1 parent d1be6e6 commit 8613254
Show file tree
Hide file tree
Showing 15 changed files with 301 additions and 168 deletions.
4 changes: 4 additions & 0 deletions changelog.d/20250114_123017_klakhov_improve_quality_ux.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Changed

- Improved UX of quality management page: better table layout, file name search, ability to download table as `.csv`
(<https://github.com/cvat-ai/cvat/pull/8865>)
15 changes: 1 addition & 14 deletions cvat-ui/src/components/analytics-page/styles.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2023-2024 CVAT.ai Corporation
// Copyright (C) 2023-2025 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

Expand Down Expand Up @@ -66,19 +66,6 @@
min-width: $grid-unit-size * 47;
}

.cvat-analytics-download-report-button {
padding-left: $grid-unit-size * 2;
padding-right: $grid-unit-size * 2;

a {
color: white;

&:hover {
color: white;
}
}
}

.cvat-analytics-page {
height: 100%;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2023-2024 CVAT.ai Corporation
// Copyright (C) 2023-2025 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

Expand Down Expand Up @@ -361,7 +361,7 @@ function QualityControlPage(): JSX.Element {
if (instance) {
title = (
<Col>
<Title level={4} className='cvat-text-color'>
<Title level={4} className='cvat-text-color cvat-quality-page-header'>
Quality control for
<Link to={`/tasks/${instance.id}`}>{` Task #${instance.id}`}</Link>
</Title>
Expand Down
67 changes: 60 additions & 7 deletions cvat-ui/src/components/quality-control/styles.scss
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Copyright (C) 2024 CVAT.ai Corporation
// Copyright (C) 2024-2025 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

@import 'base';

.cvat-quality-scrollbar {
overflow-y: auto;
overflow: hidden auto;

&::-webkit-scrollbar {
background-color: #fff;
Expand Down Expand Up @@ -37,8 +37,11 @@
}

.cvat-quality-settings-form {
@extend .cvat-quality-scrollbar;

display: block;
position: relative;
height: calc(100vh - $grid-unit-size * 30);

.cvat-quality-settings-save-btn {
position: sticky;
Expand Down Expand Up @@ -67,7 +70,6 @@ $excluded-background: #d9d9d973;
.cvat-frame-allocation-list {
width: 100%;
height: auto;
margin-top: $grid-unit-size * 2;
overflow: hidden;

td.ant-table-column-sort {
Expand Down Expand Up @@ -98,9 +100,16 @@ $excluded-background: #d9d9d973;
}

.cvat-open-frame-button {
text-align: left;

span {
text-overflow: ellipsis;
display: block;
word-wrap: break-word;
word-break: break-word;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
}
}

Expand Down Expand Up @@ -131,14 +140,58 @@ $excluded-background: #d9d9d973;
}

.cvat-frame-allocation-table .ant-table-container {
max-height: calc(100vh - $grid-unit-size * 61);
max-height: calc(100vh - $grid-unit-size * 60);
overflow-y: auto;

.ant-table-tbody .ant-table-cell {
padding: $grid-unit-size * 0.5 $grid-unit-size !important;
}
}

.cvat-task-control-tabs {
.ant-tabs-tabpane {
@extend .cvat-quality-scrollbar;
height: calc(100vh - $grid-unit-size * 30);
}

.ant-table-pagination.ant-pagination {
margin-bottom: 0;
}

height: calc(100vh - $grid-unit-size * 33);
.ant-tag {
margin-inline-end: 0;
}
}

.cvat-quality-control-management-tab-summary {
margin-left: - $grid-unit-size;
margin-right: - $grid-unit-size;

> .ant-col {
padding: 0 $grid-unit-size;
}
}

.cvat-quality-control-management-tab {
height: 100%;
}

.cvat-quality-control-validation-mode-hint {
padding: $grid-unit-size 0;
}

.cvat-quality-page-header {
margin-top: $grid-unit-size * 2;
}

.cvat-quality-table-search-wrapper {
padding-bottom: $grid-unit-size * 0.5;

.ant-input, .ant-input-search-button {
height: $grid-unit-size * 3.5;
}
}

.cvat-quality-download-report-button {
padding-left: $grid-unit-size * 2;
padding-right: 0;
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
// Copyright (C) 2024 CVAT.ai Corporation
// Copyright (C) 2024-2025 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

import React, { useState } from 'react';
import { useHistory } from 'react-router';
import { useSelector } from 'react-redux';
import { CombinedState } from 'reducers';
import { Row, Col } from 'antd/lib/grid';
import Table from 'antd/lib/table';
import Button from 'antd/lib/button';
import Text from 'antd/lib/typography/Text';
import { Key } from 'antd/lib/table/interface';
import Icon, { DeleteOutlined } from '@ant-design/icons';

Expand All @@ -18,15 +16,17 @@ import {
Task, FramesMetaData, TaskValidationLayout, QualitySettings,
} from 'cvat-core-wrapper';
import CVATTooltip from 'components/common/cvat-tooltip';
import { sorter } from 'utils/quality';
import { sorter, tablePaginationPageSize } from 'utils/quality';
import { ValidationMode } from 'components/create-task-page/quality-configuration-form';
import QualityTableHeader from './quality-table-header';

interface Props {
task: Task;
gtJobId: number;
gtJobMeta: FramesMetaData;
validationLayout: TaskValidationLayout;
qualitySettings: QualitySettings;
pageSizeData: { width: number, height: number };
onDeleteFrames: (frames: number[]) => void;
onRestoreFrames: (frames: number[]) => void;
}
Expand All @@ -37,10 +37,12 @@ interface RowData {
active: boolean;
}

function AllocationTable(props: Readonly<Props>): JSX.Element {
const FRAME_NAME_WIDTH_COEF = 0.70;

function AllocationTable(props: Readonly<Props>): JSX.Element | null {
const {
task, gtJobId, gtJobMeta, validationLayout,
onDeleteFrames, onRestoreFrames,
onDeleteFrames, onRestoreFrames, pageSizeData,
} = props;

const history = useHistory();
Expand All @@ -61,12 +63,34 @@ function AllocationTable(props: Readonly<Props>): JSX.Element {
active: !disabledFrames.includes(frame),
}));

const [filteredData, setFilteredData] = useState(data);

const handleSearch = (query: string): void => {
const lowerCaseQuery = query.toLowerCase();
const filtered = data.filter((item) => item.name.toLowerCase().includes(lowerCaseQuery));
setFilteredData(filtered);
};

const handleDownload = () => {
const filename = `allocation-table-task_${task.id}.csv`;
const csvContent = filteredData.map(({ key, ...rest }) => rest);
return { filename, data: csvContent };
};

const { width: pageWidth, height: pageHeight } = pageSizeData;
const frameNameWidth = FRAME_NAME_WIDTH_COEF * pageWidth;
const defaultPageSize = tablePaginationPageSize(pageHeight);

if (!pageWidth || !pageHeight) {
return null;
}

const columns = [
{
title: 'Frame',
dataIndex: 'frame',
key: 'frame',
width: 50,
align: 'center' as const,
sorter: sorter('frame'),
render: (frame: number): JSX.Element => (
<div>
Expand All @@ -87,33 +111,38 @@ function AllocationTable(props: Readonly<Props>): JSX.Element {
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 300,
align: 'center' as const,
sorter: sorter('name'),
render: (name: string, record: RowData) => (
<CVATTooltip title={name}>
<Button
className='cvat-open-frame-button'
type='link'
onClick={(e: React.MouseEvent): void => {
e.preventDefault();
history.push(`/tasks/${task.id}/jobs/${gtJobId}?frame=${record.frame}`);
}}
>
{name}
</Button>
</CVATTooltip>
),
width: frameNameWidth,
render: (name: string, record: RowData) => {
const link = `/tasks/${task.id}/jobs/${gtJobId}?frame=${record.frame}`;
return (
<CVATTooltip title={name}>
<Button
style={{ width: frameNameWidth }}
className='cvat-open-frame-button'
type='link'
onClick={(e: React.MouseEvent): void => {
e.preventDefault();
history.push(link);
}}
href={link}
>
{name}
</Button>
</CVATTooltip>
);
},
},
{
title: 'Actions',
dataIndex: 'active',
key: 'actions',
align: 'center' as const,
width: 20,
filters: [
{ text: 'Active', value: true },
{ text: 'Excluded', value: false },
],
align: 'center' as const,
sorter: sorter('active'),
onFilter: (value: boolean | Key, record: RowData) => record.active === value,
render: (active: boolean, record: RowData): JSX.Element => (
Expand All @@ -135,40 +164,38 @@ function AllocationTable(props: Readonly<Props>): JSX.Element {

return (
<div className='cvat-frame-allocation-list'>
<Row justify='start' align='middle' className='cvat-frame-allocation-actions'>
<Col>
<Text className='cvat-text-color cvat-frame-allocation-header'> Frames </Text>
</Col>
{
<QualityTableHeader
title='Frames'
onSearch={handleSearch}
onDownload={handleDownload}
actions={
selection.selectedRowKeys.length !== 0 ? (
<>
<Col className='cvat-allocation-selection-frame-delete'>
<DeleteOutlined
onClick={() => {
const framesToUpdate = selection.selectedRows
.filter((frameData) => frameData.active)
.map((frameData) => frameData.frame);
onDeleteFrames(framesToUpdate);
setSelection({ selectedRowKeys: [], selectedRows: [] });
}}
/>
</Col>
<Col className='cvat-allocation-selection-frame-restore'>
<Icon
onClick={() => {
const framesToUpdate = selection.selectedRows
.filter((frameData) => !frameData.active)
.map((frameData) => frameData.frame);
onRestoreFrames(framesToUpdate);
setSelection({ selectedRowKeys: [], selectedRows: [] });
}}
component={RestoreIcon}
/>
</Col>
<DeleteOutlined
className='cvat-allocation-selection-frame-delete'
onClick={() => {
const framesToUpdate = selection.selectedRows
.filter((frameData) => frameData.active)
.map((frameData) => frameData.frame);
onDeleteFrames(framesToUpdate);
setSelection({ selectedRowKeys: [], selectedRows: [] });
}}
/>
<Icon
className='cvat-allocation-selection-frame-restore'
onClick={() => {
const framesToUpdate = selection.selectedRows
.filter((frameData) => !frameData.active)
.map((frameData) => frameData.frame);
onRestoreFrames(framesToUpdate);
setSelection({ selectedRowKeys: [], selectedRows: [] });
}}
component={RestoreIcon}
/>
</>
) : null
}
</Row>
/>
<Table
className='cvat-frame-allocation-table'
rowClassName={(rowData) => {
Expand All @@ -178,7 +205,7 @@ function AllocationTable(props: Readonly<Props>): JSX.Element {
return 'cvat-allocation-frame-row';
}}
columns={columns}
dataSource={data}
dataSource={filteredData}
rowSelection={{
selectedRowKeys: selection.selectedRowKeys,
onChange: (selectedRowKeys: Key[], selectedRows: RowData[]) => {
Expand All @@ -190,7 +217,7 @@ function AllocationTable(props: Readonly<Props>): JSX.Element {
},
}}
size='small'
pagination={{ showSizeChanger: true }}
pagination={{ showSizeChanger: true, defaultPageSize }}
/>
</div>
);
Expand Down
Loading

0 comments on commit 8613254

Please sign in to comment.