Batch of fixes (#2031)

main
Dmitry Kalinin 6 years ago committed by GitHub
parent 9c4e717ddb
commit 582e23bf88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Issue loading openvino models for semi-automatic and automatic annotation (<https://github.com/opencv/cvat/pull/1996>) - Issue loading openvino models for semi-automatic and automatic annotation (<https://github.com/opencv/cvat/pull/1996>)
- Basic functions of CVAT works without activated nuclio dashboard - Basic functions of CVAT works without activated nuclio dashboard
- Fixed error with creating task with labels with the same name (<https://github.com/opencv/cvat/pull/2031>)
### Security ### Security
- -

@ -1,6 +1,6 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.7.1", "version": "1.7.2",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

@ -1,6 +1,6 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.7.1", "version": "1.7.2",
"description": "CVAT single-page application", "description": "CVAT single-page application",
"main": "src/index.tsx", "main": "src/index.tsx",
"scripts": { "scripts": {

@ -344,10 +344,12 @@ function createTask(): AnyAction {
return action; return action;
} }
function createTaskSuccess(): AnyAction { function createTaskSuccess(taskId: number): AnyAction {
const action = { const action = {
type: TasksActionTypes.CREATE_TASK_SUCCESS, type: TasksActionTypes.CREATE_TASK_SUCCESS,
payload: {}, payload: {
taskId,
},
}; };
return action; return action;
@ -433,10 +435,10 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
dispatch(createTask()); dispatch(createTask());
try { try {
await taskInstance.save((status: string): void => { const savedTask = await taskInstance.save((status: string): void => {
dispatch(createTaskUpdateStatus(status)); dispatch(createTaskUpdateStatus(status));
}); });
dispatch(createTaskSuccess()); dispatch(createTaskSuccess(savedTask.id));
} catch (error) { } catch (error) {
dispatch(createTaskFailed(error)); dispatch(createTaskFailed(error));
} }

@ -3,6 +3,8 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React from 'react';
import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import Alert from 'antd/lib/alert'; import Alert from 'antd/lib/alert';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
@ -26,6 +28,7 @@ export interface CreateTaskData {
interface Props { interface Props {
onCreate: (data: CreateTaskData) => void; onCreate: (data: CreateTaskData) => void;
status: string; status: string;
taskId: number | null;
installedGit: boolean; installedGit: boolean;
} }
@ -48,22 +51,31 @@ const defaultState = {
}, },
}; };
export default class CreateTaskContent extends React.PureComponent<Props, State> { class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps, State> {
private basicConfigurationComponent: any; private basicConfigurationComponent: any;
private advancedConfigurationComponent: any; private advancedConfigurationComponent: any;
private fileManagerContainer: any; private fileManagerContainer: any;
public constructor(props: Props) { public constructor(props: Props & RouteComponentProps) {
super(props); super(props);
this.state = { ...defaultState }; this.state = { ...defaultState };
} }
public componentDidUpdate(prevProps: Props): void { public componentDidUpdate(prevProps: Props): void {
const { status } = this.props; const { status, history, taskId } = this.props;
if (status === 'CREATED' && prevProps.status !== 'CREATED') { if (status === 'CREATED' && prevProps.status !== 'CREATED') {
const btn = (
<Button
onClick={() => history.push(`/tasks/${taskId}`)}
>
Open task
</Button>
);
notification.info({ notification.info({
message: 'The task has been created', message: 'The task has been created',
btn,
}); });
this.basicConfigurationComponent.resetFields(); this.basicConfigurationComponent.resetFields();
@ -252,3 +264,5 @@ export default class CreateTaskContent extends React.PureComponent<Props, State>
); );
} }
} }
export default withRouter(CreateTaskContent);

@ -16,6 +16,7 @@ interface Props {
onCreate: (data: CreateTaskData) => void; onCreate: (data: CreateTaskData) => void;
status: string; status: string;
error: string; error: string;
taskId: number | null;
installedGit: boolean; installedGit: boolean;
} }
@ -23,6 +24,7 @@ export default function CreateTaskPage(props: Props): JSX.Element {
const { const {
error, error,
status, status,
taskId,
onCreate, onCreate,
installedGit, installedGit,
} = props; } = props;
@ -66,6 +68,7 @@ export default function CreateTaskPage(props: Props): JSX.Element {
<Col md={20} lg={16} xl={14} xxl={9}> <Col md={20} lg={16} xl={14} xxl={9}>
<Text className='cvat-title'>Create a new task</Text> <Text className='cvat-title'>Create a new task</Text>
<CreateTaskContent <CreateTaskContent
taskId={taskId}
status={status} status={status}
onCreate={onCreate} onCreate={onCreate}
installedGit={installedGit} installedGit={installedGit}

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import './styles.scss'; import './styles.scss';
import React from 'react'; import React, { MouseEvent } from 'react';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
@ -174,8 +174,12 @@ function HeaderContainer(props: Props): JSX.Element {
className='cvat-header-button' className='cvat-header-button'
type='link' type='link'
value='tasks' value='tasks'
href='/tasks?page=1'
onClick={ onClick={
(): void => props.history.push('/tasks?page=1') (event: React.MouseEvent): void => {
event.preventDefault();
props.history.push('/tasks?page=1');
}
} }
> >
Tasks Tasks
@ -184,8 +188,12 @@ function HeaderContainer(props: Props): JSX.Element {
className='cvat-header-button' className='cvat-header-button'
type='link' type='link'
value='models' value='models'
href='/models'
onClick={ onClick={
(): void => props.history.push('/models') (event: React.MouseEvent): void => {
event.preventDefault();
props.history.push('/models');
}
} }
> >
Models Models
@ -195,8 +203,10 @@ function HeaderContainer(props: Props): JSX.Element {
<Button <Button
className='cvat-header-button' className='cvat-header-button'
type='link' type='link'
href={`${serverHost}/analytics/app/kibana`}
onClick={ onClick={
(): void => { (event: React.MouseEvent): void => {
event.preventDefault();
// false positive // false positive
// eslint-disable-next-line // eslint-disable-next-line
window.open(`${serverHost}/analytics/app/kibana`, '_blank'); window.open(`${serverHost}/analytics/app/kibana`, '_blank');
@ -211,8 +221,10 @@ function HeaderContainer(props: Props): JSX.Element {
<Button <Button
className='cvat-header-button' className='cvat-header-button'
type='link' type='link'
href={GITHUB_URL}
onClick={ onClick={
(): void => { (event: React.MouseEvent): void => {
event.preventDefault();
// false positive // false positive
// eslint-disable-next-line security/detect-non-literal-fs-filename // eslint-disable-next-line security/detect-non-literal-fs-filename
window.open(GITHUB_URL, '_blank'); window.open(GITHUB_URL, '_blank');
@ -225,8 +237,10 @@ function HeaderContainer(props: Props): JSX.Element {
<Button <Button
className='cvat-header-button' className='cvat-header-button'
type='link' type='link'
href={`${serverHost}/documentation/user_guide.html`}
onClick={ onClick={
(): void => { (event: React.MouseEvent): void => {
event.preventDefault();
// false positive // false positive
// eslint-disable-next-line // eslint-disable-next-line
window.open(`${serverHost}/documentation/user_guide.html`, '_blank') window.open(`${serverHost}/documentation/user_guide.html`, '_blank')

@ -8,14 +8,30 @@ import LabelForm from './label-form';
import { Label } from './common'; import { Label } from './common';
interface Props { interface Props {
labelNames: string[];
onCreate: (label: Label | null) => void; onCreate: (label: Label | null) => void;
} }
export default function ConstructorCreator(props: Props): JSX.Element { function compareProps(prevProps: Props, nextProps: Props): boolean {
const { onCreate } = props; if (prevProps.onCreate !== nextProps.onCreate) {
return false;
}
if (!(prevProps.labelNames.length === nextProps.labelNames.length
&& prevProps.labelNames.map((value, index) => value === nextProps.labelNames[index])
.reduce((prevValue, curValue) => prevValue && curValue, true)
)) {
return false;
}
return true;
}
function ConstructorCreator(props: Props): JSX.Element {
const { onCreate, labelNames } = props;
return ( return (
<div className='cvat-label-constructor-creator'> <div className='cvat-label-constructor-creator'>
<LabelForm label={null} onSubmit={onCreate} /> <LabelForm label={null} onSubmit={onCreate} labelNames={labelNames} />
</div> </div>
); );
} }
export default React.memo(ConstructorCreator, compareProps);

@ -32,6 +32,7 @@ export enum AttributeType {
type Props = FormComponentProps & { type Props = FormComponentProps & {
label: Label | null; label: Label | null;
labelNames?: string[];
onSubmit: (label: Label | null) => void; onSubmit: (label: Label | null) => void;
}; };
@ -384,6 +385,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
const { const {
label, label,
form, form,
labelNames,
} = this.props; } = this.props;
const value = label ? label.name : ''; const value = label ? label.name : '';
const locked = label ? label.id >= 0 : false; const locked = label ? label.id >= 0 : false;
@ -399,6 +401,13 @@ class LabelForm extends React.PureComponent<Props, {}> {
}, { }, {
pattern: patterns.validateAttributeName.pattern, pattern: patterns.validateAttributeName.pattern,
message: patterns.validateAttributeName.message, message: patterns.validateAttributeName.message,
}, {
validator:
async (_rule: any, labelName: string, callback: Function) => {
if (labelNames && labelNames.includes(labelName)) {
callback('Label name must be unique for the task');
}
},
}], }],
})(<Input disabled={locked} placeholder='Label name' />)} })(<Input disabled={locked} placeholder='Label name' />)}
</Form.Item> </Form.Item>

@ -221,6 +221,7 @@ export default class LabelsEditor
} }
public render(): JSX.Element { public render(): JSX.Element {
const { labels } = this.props;
const { const {
savedLabels, savedLabels,
unsavedLabels, unsavedLabels,
@ -319,6 +320,7 @@ export default class LabelsEditor
constructorMode === ConstructorMode.CREATE constructorMode === ConstructorMode.CREATE
&& ( && (
<ConstructorCreator <ConstructorCreator
labelNames={labels.map((l) => l.name)}
onCreate={this.handleCreate} onCreate={this.handleCreate}
/> />
) )

@ -28,6 +28,10 @@ class RawViewer extends React.PureComponent<Props> {
if (!Array.isArray(parsed)) { if (!Array.isArray(parsed)) {
callback('Field is expected to be a JSON array'); callback('Field is expected to be a JSON array');
} }
const labelNames = parsed.map((label: Label) => label.name);
if (new Set(labelNames).size !== labelNames.length) {
callback('Label names must be unique for the task');
}
for (const label of parsed) { for (const label of parsed) {
try { try {

@ -10,6 +10,7 @@ import { CreateTaskData } from 'components/create-task-page/create-task-content'
import { createTaskAsync } from 'actions/tasks-actions'; import { createTaskAsync } from 'actions/tasks-actions';
interface StateToProps { interface StateToProps {
taskId: number | null;
status: string; status: string;
error: string; error: string;
installedGit: boolean; installedGit: boolean;

@ -60,6 +60,7 @@ export interface TasksState {
[tid: number]: boolean; // deleted (deleting if in dictionary) [tid: number]: boolean; // deleted (deleting if in dictionary)
}; };
creates: { creates: {
taskId: number | null;
status: string; status: string;
error: string; error: string;
}; };

@ -31,6 +31,7 @@ const defaultState: TasksState = {
loads: {}, loads: {},
deletes: {}, deletes: {},
creates: { creates: {
taskId: null,
status: '', status: '',
error: '', error: '',
}, },
@ -238,6 +239,7 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
activities: { activities: {
...state.activities, ...state.activities,
creates: { creates: {
taskId: null,
status: '', status: '',
error: '', error: '',
}, },
@ -259,12 +261,14 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
}; };
} }
case TasksActionTypes.CREATE_TASK_SUCCESS: { case TasksActionTypes.CREATE_TASK_SUCCESS: {
const { taskId } = action.payload;
return { return {
...state, ...state,
activities: { activities: {
...state.activities, ...state.activities,
creates: { creates: {
...state.activities.creates, ...state.activities.creates,
taskId,
status: 'CREATED', status: 'CREATED',
}, },
}, },

@ -37,6 +37,7 @@ class AttributeSerializer(serializers.ModelSerializer):
class LabelSerializer(serializers.ModelSerializer): class LabelSerializer(serializers.ModelSerializer):
attributes = AttributeSerializer(many=True, source='attributespec_set', attributes = AttributeSerializer(many=True, source='attributespec_set',
default=[]) default=[])
class Meta: class Meta:
model = models.Label model = models.Label
fields = ('id', 'name', 'attributes') fields = ('id', 'name', 'attributes')
@ -305,6 +306,15 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer):
instance.save() instance.save()
return instance return instance
def validate_labels(self, value):
if not value:
raise serializers.ValidationError('Label set must not be empty')
label_names = [label['name'] for label in value]
if len(label_names) != len(set(label_names)):
raise serializers.ValidationError('All label names must be unique for the task')
return value
class ProjectSerializer(serializers.ModelSerializer): class ProjectSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.Project model = models.Project

@ -44,7 +44,7 @@ Cypress.Commands.add('createAnnotationTask', (taksName='New annotation task',
cy.get('input[type="file"]').attachFile(image, { subjectType: 'drag-n-drop' }); cy.get('input[type="file"]').attachFile(image, { subjectType: 'drag-n-drop' });
cy.contains('button', 'Submit').click() cy.contains('button', 'Submit').click()
cy.contains('The task has been created', {timeout: '8000'}) cy.contains('The task has been created', {timeout: '8000'})
cy.get('button[value="tasks"]').click() cy.get('[value="tasks"]').click()
cy.url().should('include', '/tasks?page=') cy.url().should('include', '/tasks?page=')
}) })

Loading…
Cancel
Save