React & Antd: Dashboard migration (#892)

* Removed old dashboard
* Getting all users
* Updated changelog
* Reimplemented login decorator
* Implicit host, scheme in docker-compose
* Fixed issue with pagination
* Implicit page size parameter for tasks
* Fixed linkedin icon, added links to tasks in notifications
* Configurable method for check plugin
main
Boris Sekachev 6 years ago committed by Nikita Manovich
parent dc7bfa9b12
commit 171a9202ed

@ -21,6 +21,7 @@ https://github.com/opencv/cvat/issues/750).
### Changed ### Changed
- page_size parameter for all REST API methods - page_size parameter for all REST API methods
- React & Redux & Antd based dashboard
### Deprecated ### Deprecated
- -

@ -15,16 +15,14 @@
const store = require('store'); const store = require('store');
const config = require('./config'); const config = require('./config');
function generateError(errorData, baseMessage) { function generateError(errorData) {
if (errorData.response) { if (errorData.response) {
const message = `${baseMessage}. ` const message = `${errorData.message}. ${JSON.stringify(errorData.response.data) || ''}.`;
+ `${errorData.message}. ${JSON.stringify(errorData.response.data) || ''}.`;
return new ServerError(message, errorData.response.status); return new ServerError(message, errorData.response.status);
} }
// Server is unavailable (no any response) // Server is unavailable (no any response)
const message = `${baseMessage}. ` const message = `${errorData.message}.`; // usually is "Error Network"
+ `${errorData.message}.`; // usually is "Error Network"
return new ServerError(message, 0); return new ServerError(message, 0);
} }
@ -49,7 +47,7 @@
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not get "about" information from the server'); throw generateError(errorData);
} }
return response.data; return response.data;
@ -65,7 +63,7 @@
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not get "share" information from the server'); throw generateError(errorData);
} }
return response.data; return response.data;
@ -82,7 +80,7 @@
}, },
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not send an exception to the server'); throw generateError(errorData);
} }
} }
@ -95,7 +93,7 @@
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not get annotation formats from the server'); throw generateError(errorData);
} }
return response.data; return response.data;
@ -111,7 +109,7 @@
}); });
response = JSON.parse(response.data); response = JSON.parse(response.data);
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not get export formats from the server'); throw generateError(errorData);
} }
return response; return response;
@ -135,7 +133,7 @@
}, },
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, `Could not register '${username}' user on the server`); throw generateError(errorData);
} }
return response.data; return response.data;
@ -157,7 +155,7 @@
}, },
); );
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not login on a server'); throw generateError(errorData);
} }
if (authenticationResponse.headers['set-cookie']) { if (authenticationResponse.headers['set-cookie']) {
@ -178,7 +176,7 @@
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not logout from the server'); throw generateError(errorData);
} }
store.remove('token'); store.remove('token');
@ -206,7 +204,7 @@
...data, ...data,
})).data; })).data;
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not have done the request'); throw generateError(errorData);
} }
} }
@ -215,11 +213,11 @@
let response = null; let response = null;
try { try {
response = await Axios.get(`${backendAPI}/tasks?${filter}`, { response = await Axios.get(`${backendAPI}/tasks?page_size=10&${filter}`, {
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not get tasks from a server'); throw generateError(errorData);
} }
response.data.results.count = response.data.count; response.data.results.count = response.data.count;
@ -237,7 +235,7 @@
}, },
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not save the task on the server'); throw generateError(errorData);
} }
} }
@ -247,7 +245,7 @@
try { try {
await Axios.delete(`${backendAPI}/tasks/${id}`); await Axios.delete(`${backendAPI}/tasks/${id}`);
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, `Could not delete the task ${id} from the server`); throw generateError(errorData);
} }
} }
@ -269,10 +267,7 @@
resolve(url); resolve(url);
} }
} catch (errorData) { } catch (errorData) {
reject(generateError( reject(generateError(errorData));
errorData,
`Failed to export the task ${id} as a dataset`,
));
} }
} }
@ -311,7 +306,7 @@
} }
} catch (errorData) { } catch (errorData) {
reject( reject(
generateError(errorData, 'Could not put task to the server'), generateError(errorData),
); );
} }
} }
@ -340,7 +335,7 @@
}, },
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not put task to the server'); throw generateError(errorData);
} }
onUpdate('The data is being uploaded to the server..'); onUpdate('The data is being uploaded to the server..');
@ -355,7 +350,7 @@
// ignore // ignore
} }
throw generateError(errorData, 'Could not put data to the server'); throw generateError(errorData);
} }
try { try {
@ -378,7 +373,7 @@
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not get jobs from a server'); throw generateError(errorData);
} }
return response.data; return response.data;
@ -395,7 +390,7 @@
}, },
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not save the job on the server'); throw generateError(errorData);
} }
} }
@ -405,7 +400,7 @@
let response = null; let response = null;
try { try {
if (id === null) { if (id === null) {
response = await Axios.get(`${backendAPI}/users`, { response = await Axios.get(`${backendAPI}/users?page_size=all`, {
proxy: config.proxy, proxy: config.proxy,
}); });
} else { } else {
@ -414,7 +409,7 @@
}); });
} }
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not get users from the server'); throw generateError(errorData);
} }
return response.data.results; return response.data.results;
@ -429,7 +424,7 @@
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not get user data from the server'); throw generateError(errorData);
} }
return response.data; return response.data;
@ -466,10 +461,7 @@
responseType: 'blob', responseType: 'blob',
}); });
} catch (errorData) { } catch (errorData) {
throw generateError( throw generateError(errorData);
errorData,
`Could not get frame ${frame} for the task ${tid} from the server`,
);
} }
return response.data; return response.data;
@ -484,10 +476,7 @@
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
throw generateError( throw generateError(errorData);
errorData,
`Could not get frame meta info for the task ${tid} from the server`,
);
} }
return response.data; return response.data;
@ -503,10 +492,7 @@
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
throw generateError( throw generateError(errorData);
errorData,
`Could not get annotations for the ${session} ${id} from the server`,
);
} }
return response.data; return response.data;
@ -534,10 +520,7 @@
}, },
}); });
} catch (errorData) { } catch (errorData) {
throw generateError( throw generateError(errorData);
errorData,
`Could not ${action} annotations for the ${session} ${id} on the server`,
);
} }
return response.data; return response.data;
@ -564,10 +547,7 @@
resolve(); resolve();
} }
} catch (errorData) { } catch (errorData) {
reject(generateError( reject(generateError(errorData));
errorData,
`Could not upload annotations for the ${session} ${id}`,
));
} }
} }
@ -595,10 +575,7 @@
resolve(url); resolve(url);
} }
} catch (errorData) { } catch (errorData) {
reject(generateError( reject(generateError(errorData));
errorData,
`Could not dump annotations for the task ${id} from the server`,
));
} }
} }

@ -551,10 +551,11 @@ function inferModelSuccess(): AnyAction {
return action; return action;
} }
function inferModelFailed(error: any): AnyAction { function inferModelFailed(error: any, taskID: number): AnyAction {
const action = { const action = {
type: ModelsActionTypes.INFER_MODEL_FAILED, type: ModelsActionTypes.INFER_MODEL_FAILED,
payload: { payload: {
taskID,
error, error,
}, },
}; };
@ -599,7 +600,7 @@ export function inferModelAsync(
dispatch(getInferenceStatusAsync([taskInstance.id])); dispatch(getInferenceStatusAsync([taskInstance.id]));
} catch (error) { } catch (error) {
dispatch(inferModelFailed(error)); dispatch(inferModelFailed(error, taskInstance.id));
return; return;
} }

@ -90,10 +90,12 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
return; return;
} }
const array = Array.from(result); const array = Array.from(result)
.filter((task: any) => task.jobs.length > 0);
const previews = []; const previews = [];
const promises = array const promises = array
.map((task): string => (task as any).frames.preview()); .map((task): string => (task as any).frames.preview());
dispatch( dispatch(
getInferenceStatusAsync( getInferenceStatusAsync(
array.map( array.map(
@ -441,23 +443,23 @@ function updateTask(): AnyAction {
return action; return action;
} }
function updateTaskSuccess(taskInstance: any): AnyAction { function updateTaskSuccess(task: any): AnyAction {
const action = { const action = {
type: TasksActionTypes.UPDATE_TASK_SUCCESS, type: TasksActionTypes.UPDATE_TASK_SUCCESS,
payload: { payload: {
taskInstance, task,
}, },
}; };
return action; return action;
} }
function updateTaskFailed(error: any, taskInstance: any): AnyAction { function updateTaskFailed(error: any, task: any): AnyAction {
const action = { const action = {
type: TasksActionTypes.UPDATE_TASK_FAILED, type: TasksActionTypes.UPDATE_TASK_FAILED,
payload: { payload: {
error, error,
taskInstance, task,
}, },
}; };

@ -7,7 +7,6 @@ import {
Alert, Alert,
Button, Button,
Tooltip, Tooltip,
Modal,
message, message,
notification, notification,
} from 'antd'; } from 'antd';
@ -96,7 +95,7 @@ export default class CreateModelContent extends React.PureComponent<Props> {
const status = this.props.modelCreatingStatus const status = this.props.modelCreatingStatus
&& this.props.modelCreatingStatus !== 'CREATED' ? this.props.modelCreatingStatus : ''; && this.props.modelCreatingStatus !== 'CREATED' ? this.props.modelCreatingStatus : '';
const guideLink = 'https://github.com/opencv/cvat/tree/develop/cvat/apps/auto_annotation'; const guideLink = 'https://github.com/opencv/cvat/blob/develop/cvat/apps/auto_annotation/README.md';
return ( return (
<Row type='flex' justify='start' align='middle' className='cvat-create-model-content'> <Row type='flex' justify='start' align='middle' className='cvat-create-model-content'>
<Col span={24}> <Col span={24}>

@ -56,7 +56,11 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
private showMessages() { private showMessages() {
function showMessage(title: string) { function showMessage(title: string) {
notification.info({ notification.info({
message: title, message: (
<div dangerouslySetInnerHTML={{
__html: title,
}}/>
),
duration: null, duration: null,
}); });
} }
@ -81,7 +85,11 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
function showError(title: string, _error: any) { function showError(title: string, _error: any) {
const error = _error.toString(); const error = _error.toString();
notification.error({ notification.error({
message: title, message: (
<div dangerouslySetInnerHTML={{
__html: title,
}}/>
),
duration: null, duration: null,
description: error.length > 200 ? '' : error, description: error.length > 200 ? '' : error,
}); });
@ -104,64 +112,64 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
|| !!models.metaFetching; || !!models.metaFetching;
if (auth.authorized) { if (auth.authorized) {
showError('Could not check authorization on the server', auth.authorized); showError(auth.authorized.message, auth.authorized.reason);
} }
if (auth.login) { if (auth.login) {
showError('Could not login on the server', auth.login); showError(auth.login.message, auth.login.reason);
} }
if (auth.register) { if (auth.register) {
showError('Could not register on the server', auth.register); showError(auth.register.message, auth.register.reason);
} }
if (auth.logout) { if (auth.logout) {
showError('Could not logout on the server', auth.logout); showError(auth.logout.message, auth.logout.reason);
} }
if (tasks.fetching) { if (tasks.fetching) {
showError('Could not fetch tasks from the server', tasks.fetching); showError(tasks.fetching.message, tasks.fetching.reason);
} }
if (tasks.updating) { if (tasks.updating) {
showError('Could not update task on the server', tasks.updating); showError(tasks.updating.message, tasks.updating.reason);
} }
if (tasks.dumping) { if (tasks.dumping) {
showError('Could not dump annotations from the server', tasks.dumping); showError(tasks.dumping.message, tasks.dumping.reason);
} }
if (tasks.loading) { if (tasks.loading) {
showError('Could not upload annotations to the server', tasks.loading); showError(tasks.loading.message, tasks.loading.reason);
} }
if (tasks.exporting) { if (tasks.exporting) {
showError('Could not export task from the server', tasks.exporting); showError(tasks.exporting.message, tasks.exporting.reason);
} }
if (tasks.deleting) { if (tasks.deleting) {
showError('Could not delete task on the server', tasks.deleting); showError(tasks.deleting.message, tasks.deleting.reason);
} }
if (tasks.creating) { if (tasks.creating) {
showError('Could not create task on the server', tasks.creating); showError(tasks.creating.message, tasks.creating.reason);
} }
if (formats.fetching) { if (formats.fetching) {
showError('Could not get annotations and dataset formats from the server', formats.fetching); showError(formats.fetching.message, formats.fetching.reason);
} }
if (users.fetching) { if (users.fetching) {
showError('Could not get users from the server', users.fetching); showError(users.fetching.message, users.fetching.reason);
} }
if (share.fetching) { if (share.fetching) {
showError('Could not get share info from the server', share.fetching); showError(share.fetching.message, share.fetching.reason);
} }
if (models.creating) { if (models.creating) {
showError('Could not create model on the server', models.creating); showError(models.creating.message, models.creating.reason);
} }
if (models.starting) { if (models.starting) {
showError('Could not run model on the server', models.starting); showError(models.starting.message, models.starting.reason);
} }
if (models.fetching) { if (models.fetching) {
showError('Could not get models from the server', models.fetching); showError(models.fetching.message, models.fetching.reason);
} }
if (models.deleting) { if (models.deleting) {
showError('Could not delete model from the server', models.deleting); showError(models.deleting.message, models.deleting.reason);
} }
if (models.metaFetching) { if (models.metaFetching) {
showError('Could not fetch models meta information from the server', models.metaFetching); showError(models.metaFetching.message, models.metaFetching.reason);
} }
if (models.inferenceStatusFetching) { if (models.inferenceStatusFetching) {
showError('Could not fetch inference status from the server', models.inferenceStatusFetching); showError(models.inferenceStatusFetching.message, models.inferenceStatusFetching.reason);
} }
if (shown) { if (shown) {

@ -22,7 +22,7 @@ import {
VKIcon, VKIcon,
RedditIcon, RedditIcon,
ViberIcon, ViberIcon,
LineIcon, LinkedinIcon,
} from 'react-share'; } from 'react-share';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
@ -55,7 +55,7 @@ export default class Feedback extends React.PureComponent<{}, State> {
<br/> <br/>
<Icon type='like'/> <Icon type='like'/>
<Text style={{marginLeft: '10px'}}> <Text style={{marginLeft: '10px'}}>
Left a <a target='_blank' href={feedbackURL}>feedback</a> Leave a <a target='_blank' href={feedbackURL}>feedback</a>
</Text> </Text>
<hr/> <hr/>
<div style={{display: 'flex'}}> <div style={{display: 'flex'}}>
@ -72,7 +72,7 @@ export default class Feedback extends React.PureComponent<{}, State> {
<RedditIcon size={32} round={true} /> <RedditIcon size={32} round={true} />
</RedditShareButton> </RedditShareButton>
<LinkedinShareButton url={githubURL}> <LinkedinShareButton url={githubURL}>
<LineIcon size={32} round={true} /> <LinkedinIcon size={32} round={true} />
</LinkedinShareButton> </LinkedinShareButton>
<TelegramShareButton url={githubURL} title='Computer Vision Annotation Tool'> <TelegramShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<TelegramIcon size={32} round={true} /> <TelegramIcon size={32} round={true} />

@ -32,7 +32,7 @@ class TaskPageComponent extends React.PureComponent<Props> {
this.props.history.replace('/tasks'); this.props.history.replace('/tasks');
} }
if (this.attempts > 1) { if (this.attempts == 2) {
notification.warning({ notification.warning({
message: 'Something wrong with the task. It cannot be fetched from the server', message: 'Something wrong with the task. It cannot be fetched from the server',
}); });

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { import {
Icon,
Select, Select,
} from 'antd'; } from 'antd';
@ -13,13 +14,13 @@ interface Props {
export default function UserSelector(props: Props) { export default function UserSelector(props: Props) {
return ( return (
<Select <Select
defaultValue={props.value ? props.value : '\0'} defaultValue={props.value ? props.value : ''}
size='small' size='small'
showSearch showSearch
className='cvat-user-selector' className='cvat-user-selector'
onChange={props.onChange} onChange={props.onChange}
> >
<Select.Option key='-1' value='\0'>{'\0'}</Select.Option> <Select.Option key='-1' value='—'>{'—'}</Select.Option>
{ props.users.map((user) => { { props.users.map((user) => {
return ( return (
<Select.Option key={user.id} value={user.username}> <Select.Option key={user.id} value={user.username}>

@ -146,39 +146,44 @@ export interface ModelFiles {
json: string | File; json: string | File;
} }
export interface ErrorState {
message: string,
reason: string,
}
export interface NotificationsState { export interface NotificationsState {
errors: { errors: {
auth: { auth: {
authorized: any; authorized: null | ErrorState;
login: any; login: null | ErrorState;
logout: any; logout: null | ErrorState;
register: any; register: null | ErrorState;
}; };
tasks: { tasks: {
fetching: any; fetching: null | ErrorState;
updating: any; updating: null | ErrorState;
dumping: any; dumping: null | ErrorState;
loading: any; loading: null | ErrorState;
exporting: any; exporting: null | ErrorState;
deleting: any; deleting: null | ErrorState;
creating: any; creating: null | ErrorState;
}; };
formats: { formats: {
fetching: any; fetching: null | ErrorState;
}; };
users: { users: {
fetching: any; fetching: null | ErrorState;
}; };
share: { share: {
fetching: any; fetching: null | ErrorState;
}; };
models: { models: {
creating: any; creating: null | ErrorState;
starting: any; starting: null | ErrorState;
deleting: any; deleting: null | ErrorState;
fetching: any; fetching: null | ErrorState;
metaFetching: any; metaFetching: null | ErrorState;
inferenceStatusFetching: any; inferenceStatusFetching: null | ErrorState;
}; };
}; };
messages: { messages: {

@ -64,7 +64,10 @@ export default function (state = defaultState, action: AnyAction): Notifications
...state.errors, ...state.errors,
auth: { auth: {
...state.errors.auth, ...state.errors.auth,
authorized: action.payload.error, authorized: {
message: 'Could not check authorization on the server',
reason: action.payload.error.toString(),
}
}, },
}, },
}; };
@ -76,7 +79,10 @@ export default function (state = defaultState, action: AnyAction): Notifications
...state.errors, ...state.errors,
auth: { auth: {
...state.errors.auth, ...state.errors.auth,
login: action.payload.error, login: {
message: 'Could not login on the server',
reason: action.payload.error.toString(),
}
}, },
}, },
}; };
@ -88,7 +94,10 @@ export default function (state = defaultState, action: AnyAction): Notifications
...state.errors, ...state.errors,
auth: { auth: {
...state.errors.auth, ...state.errors.auth,
logout: action.payload.error, logout: {
message: 'Could not logout from the server',
reason: action.payload.error.toString(),
},
}, },
}, },
}; };
@ -100,19 +109,27 @@ export default function (state = defaultState, action: AnyAction): Notifications
...state.errors, ...state.errors,
auth: { auth: {
...state.errors.auth, ...state.errors.auth,
register: action.payload.error, register: {
message: 'Could not register on the server',
reason: action.payload.error.toString(),
},
}, },
}, },
}; };
} }
case TasksActionTypes.EXPORT_DATASET_FAILED: { case TasksActionTypes.EXPORT_DATASET_FAILED: {
const taskID = action.payload.task.id;
return { return {
...state, ...state,
errors: { errors: {
...state.errors, ...state.errors,
tasks: { tasks: {
...state.errors.tasks, ...state.errors.tasks,
exporting: action.payload.error, exporting: {
message: 'Could not export dataset for the '
+ `<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
reason: action.payload.error.toString(),
},
}, },
}, },
}; };
@ -124,68 +141,92 @@ export default function (state = defaultState, action: AnyAction): Notifications
...state.errors, ...state.errors,
tasks: { tasks: {
...state.errors.tasks, ...state.errors.tasks,
fetching: action.payload.error, fetching: {
message: 'Could not fetch tasks',
reason: action.payload.error.toString(),
},
}, },
}, },
}; };
} }
case TasksActionTypes.LOAD_ANNOTATIONS_FAILED: { case TasksActionTypes.LOAD_ANNOTATIONS_FAILED: {
const taskID = action.payload.task.id;
return { return {
...state, ...state,
errors: { errors: {
...state.errors, ...state.errors,
tasks: { tasks: {
...state.errors.tasks, ...state.errors.tasks,
loading: action.payload.error, loading: {
message: 'Could not upload annotation for the '
+ `<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
reason: action.payload.error.toString(),
},
}, },
}, },
}; };
} }
case TasksActionTypes.LOAD_ANNOTATIONS_SUCCESS: { case TasksActionTypes.LOAD_ANNOTATIONS_SUCCESS: {
const { task } = action.payload; const taskID = action.payload.task.id;
return { return {
...state, ...state,
messages: { messages: {
...state.messages, ...state.messages,
tasks: { tasks: {
...state.messages.tasks, ...state.messages.tasks,
loadingDone: `Annotations have been loaded to the task ${task.id}`, loadingDone: 'Annotations have been loaded to the '
+ `<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
}, },
}, },
}; };
} }
case TasksActionTypes.UPDATE_TASK_FAILED: { case TasksActionTypes.UPDATE_TASK_FAILED: {
const taskID = action.payload.task.id;
return { return {
...state, ...state,
errors: { errors: {
...state.errors, ...state.errors,
tasks: { tasks: {
...state.errors.tasks, ...state.errors.tasks,
updating: action.payload.error, updating: {
message: 'Could not update '
+ `<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
reason: action.payload.error.toString(),
},
}, },
}, },
}; };
} }
case TasksActionTypes.DUMP_ANNOTATIONS_FAILED: { case TasksActionTypes.DUMP_ANNOTATIONS_FAILED: {
const taskID = action.payload.task.id;
return { return {
...state, ...state,
errors: { errors: {
...state.errors, ...state.errors,
tasks: { tasks: {
...state.errors.tasks, ...state.errors.tasks,
dumping: action.payload.error, dumping: {
message: 'Could not dump annotations for the '
+ `<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
reason: action.payload.error.toString(),
},
}, },
}, },
}; };
} }
case TasksActionTypes.DELETE_TASK_FAILED: { case TasksActionTypes.DELETE_TASK_FAILED: {
const { taskID } = action.payload;
return { return {
...state, ...state,
errors: { errors: {
...state.errors, ...state.errors,
tasks: { tasks: {
...state.errors.tasks, ...state.errors.tasks,
deleting: action.payload.error, deleting: {
message: 'Could not delete the '
+ `<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
reason: action.payload.error.toString(),
},
}, },
}, },
}; };
@ -197,7 +238,10 @@ export default function (state = defaultState, action: AnyAction): Notifications
...state.errors, ...state.errors,
tasks: { tasks: {
...state.errors.tasks, ...state.errors.tasks,
creating: action.payload.error, creating: {
message: 'Could not create the task',
reason: action.payload.error.toString(),
},
}, },
}, },
}; };
@ -209,7 +253,10 @@ export default function (state = defaultState, action: AnyAction): Notifications
...state.errors, ...state.errors,
formats: { formats: {
...state.errors.formats, ...state.errors.formats,
fetching: action.payload.error, fetching: {
message: 'Could not get formats from the server',
reason: action.payload.error.toString(),
},
}, },
}, },
}; };
@ -221,7 +268,10 @@ export default function (state = defaultState, action: AnyAction): Notifications
...state.errors, ...state.errors,
users: { users: {
...state.errors.users, ...state.errors.users,
fetching: action.payload.error, fetching: {
message: 'Could not get users from the server',
reason: action.payload.error.toString(),
},
}, },
}, },
}; };
@ -233,7 +283,10 @@ export default function (state = defaultState, action: AnyAction): Notifications
...state.errors, ...state.errors,
share: { share: {
...state.errors.share, ...state.errors.share,
fetching: action.payload.error, fetching: {
message: 'Could not load share data from the server',
reason: action.payload.error.toString(),
},
}, },
}, },
}; };
@ -245,7 +298,10 @@ export default function (state = defaultState, action: AnyAction): Notifications
...state.errors, ...state.errors,
models: { models: {
...state.errors.models, ...state.errors.models,
creating: action.payload.error, creating: {
message: 'Could not create the model',
reason: action.payload.error.toString(),
},
}, },
}, },
}; };
@ -257,20 +313,25 @@ export default function (state = defaultState, action: AnyAction): Notifications
...state.errors, ...state.errors,
models: { models: {
...state.errors.models, ...state.errors.models,
deleting: action.payload.error, deleting: {
message: 'Could not delete the model',
reason: action.payload.error.toString(),
},
}, },
}, },
}; };
} }
case ModelsActionTypes.GET_INFERENCE_STATUS_SUCCESS: { case ModelsActionTypes.GET_INFERENCE_STATUS_SUCCESS: {
if (action.payload.activeInference.status === 'finished') { if (action.payload.activeInference.status === 'finished') {
const taskID = action.payload.taskID;
return { return {
...state, ...state,
messages: { messages: {
...state.messages, ...state.messages,
models: { models: {
...state.messages.models, ...state.messages.models,
inferenceDone: `Automatic annotation finished for the task ${action.payload.taskID}`, inferenceDone: 'Automatic annotation finished for the '
+ `<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
}, },
}, },
}; };
@ -287,19 +348,27 @@ export default function (state = defaultState, action: AnyAction): Notifications
...state.errors, ...state.errors,
models: { models: {
...state.errors.models, ...state.errors.models,
metaFetching: action.payload.error, metaFetching: {
message: 'Could not fetch models meta information',
reason: action.payload.error.toString(),
}
}, },
}, },
}; };
} }
case ModelsActionTypes.GET_INFERENCE_STATUS_FAILED: { case ModelsActionTypes.GET_INFERENCE_STATUS_FAILED: {
const { taskID } = action.payload;
return { return {
...state, ...state,
errors: { errors: {
...state.errors, ...state.errors,
models: { models: {
...state.errors.models, ...state.errors.models,
inferenceStatusFetching: action.payload.error, inferenceStatusFetching: {
message: 'Could not fetch inference status for the '
+ `<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
reason: action.payload.error.toString(),
}
}, },
}, },
}; };
@ -311,19 +380,27 @@ export default function (state = defaultState, action: AnyAction): Notifications
...state.errors, ...state.errors,
models: { models: {
...state.errors.models, ...state.errors.models,
fetching: action.payload.error, fetching: {
message: 'Could not get models from the server',
reason: action.payload.error.toString(),
},
}, },
}, },
}; };
} }
case ModelsActionTypes.INFER_MODEL_FAILED: { case ModelsActionTypes.INFER_MODEL_FAILED: {
const { taskID } = action.payload;
return { return {
...state, ...state,
errors: { errors: {
...state.errors, ...state.errors,
models: { models: {
...state.errors.models, ...state.errors.models,
starting: action.payload.error, starting: {
message: 'Could not infer model for the '
+ `<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
reason: action.payload.error.toString(),
}
}, },
}, },
}; };

@ -379,10 +379,10 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
return { return {
...state, ...state,
current: state.current.map((task): Task => { current: state.current.map((task): Task => {
if (task.instance.id === action.payload.taskInstance.id) { if (task.instance.id === action.payload.task.id) {
return { return {
...task, ...task,
instance: action.payload.taskInstance, instance: action.payload.task,
}; };
} }
@ -394,10 +394,10 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
return { return {
...state, ...state,
current: state.current.map((task): Task => { current: state.current.map((task): Task => {
if (task.instance.id === action.payload.taskInstance.id) { if (task.instance.id === action.payload.task.id) {
return { return {
...task, ...task,
instance: action.payload.taskInstance, instance: action.payload.task,
}; };
} }

@ -7,34 +7,32 @@ const core = getCore();
class PluginChecker { class PluginChecker {
public static async check(plugin: SupportedPlugins): Promise<boolean> { public static async check(plugin: SupportedPlugins): Promise<boolean> {
const serverHost = core.config.backendAPI.slice(0, -7); const serverHost = core.config.backendAPI.slice(0, -7);
const isReachable = async (url: string): Promise<boolean> => { const isReachable = async (url: string, method: string): Promise<boolean> => {
try { try {
await core.server.request(url); await core.server.request(url, {
method,
});
return true; return true;
} catch (error) { } catch (error) {
if (error.code === 404) { return ![0, 404].includes(error.code);
return false;
}
throw error;
} }
}; };
switch (plugin) { switch (plugin) {
case SupportedPlugins.GIT_INTEGRATION: { case SupportedPlugins.GIT_INTEGRATION: {
return isReachable(`${serverHost}/git/repository/meta/get`); return isReachable(`${serverHost}/git/repository/meta/get`, 'OPTIONS');
} }
case SupportedPlugins.AUTO_ANNOTATION: { case SupportedPlugins.AUTO_ANNOTATION: {
return isReachable(`${serverHost}/auto_annotation/meta/get`); return isReachable(`${serverHost}/auto_annotation/meta/get`, 'OPTIONS');
} }
case SupportedPlugins.TF_ANNOTATION: { case SupportedPlugins.TF_ANNOTATION: {
return isReachable(`${serverHost}/tensorflow/annotation/meta/get`); return isReachable(`${serverHost}/tensorflow/annotation/meta/get`, 'OPTIONS');
} }
case SupportedPlugins.TF_SEGMENTATION: { case SupportedPlugins.TF_SEGMENTATION: {
return isReachable(`${serverHost}/tensorflow/segmentation/meta/get`); return isReachable(`${serverHost}/tensorflow/segmentation/meta/get`, 'OPTIONS');
} }
case SupportedPlugins.ANALYTICS: { case SupportedPlugins.ANALYTICS: {
return isReachable(`${serverHost}/analytics/app/kibana`); return isReachable(`${serverHost}/analytics/app/kibana`, 'GET');
} }
default: default:
return false; return false;

@ -1,14 +1,12 @@
# Copyright (C) 2018 Intel Corporation # Copyright (C) 2018-2019 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from functools import wraps from functools import wraps
from urllib.parse import urlparse from django.views.generic import RedirectView
from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth import REDIRECT_FIELD_NAME
from django.shortcuts import resolve_url, reverse
from django.http import JsonResponse from django.http import JsonResponse
from django.contrib.auth.views import redirect_to_login
from django.conf import settings from django.conf import settings
from rest_framework.authentication import TokenAuthentication from rest_framework.authentication import TokenAuthentication
@ -26,19 +24,14 @@ def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME,
request.user = auth[0] request.user = auth[0]
return view_func(request, *args, **kwargs) return view_func(request, *args, **kwargs)
login_url = '{}/login'.format(settings.UI_URL)
if request.method not in redirect_methods: if request.method not in redirect_methods:
return JsonResponse({'login_page_url': reverse('login')}, status=403) return JsonResponse({'login_page_url': login_url}, status=403)
path = request.build_absolute_uri() return RedirectView.as_view(
resolved_login_url = resolve_url(login_url or settings.LOGIN_URL) url=login_url,
# If the login url is the same scheme and net location then just permanent=True,
# use the path as the "next" url. query_string=True
login_scheme, login_netloc = urlparse(resolved_login_url)[:2] )(request)
current_scheme, current_netloc = urlparse(path)[:2]
if ((not login_scheme or login_scheme == current_scheme) and
(not login_netloc or login_netloc == current_netloc)):
path = request.get_full_path()
return redirect_to_login(path, resolved_login_url, redirect_field_name)
return _wrapped_view return _wrapped_view
return decorator(function) if function else decorator return decorator(function) if function else decorator

@ -1,12 +1,6 @@
# Copyright (C) 2018 Intel Corporation # Copyright (C) 2018-2019 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from cvat.settings.base import JS_3RDPARTY, CSS_3RDPARTY
default_app_config = 'cvat.apps.auto_annotation.apps.AutoAnnotationConfig' default_app_config = 'cvat.apps.auto_annotation.apps.AutoAnnotationConfig'
JS_3RDPARTY['dashboard'] = JS_3RDPARTY.get('dashboard', []) + ['auto_annotation/js/dashboardPlugin.js']
CSS_3RDPARTY['dashboard'] = CSS_3RDPARTY.get('dashboard', []) + ['auto_annotation/stylesheet.css']

@ -1,12 +1,11 @@
# Copyright (C) 2018 Intel Corporation # Copyright (C) 2018-2019 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
import json import json
import cv2 import cv2
import os import os
import subprocess
import numpy as np import numpy as np
from cvat.apps.auto_annotation.inference_engine import make_plugin, make_network from cvat.apps.auto_annotation.inference_engine import make_plugin, make_network

@ -1,4 +1,4 @@
# Copyright (C) 2018 Intel Corporation # Copyright (C) 2018-2019 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
@ -9,7 +9,6 @@ import os
import rq import rq
import shutil import shutil
import tempfile import tempfile
import itertools
from django.db import transaction from django.db import transaction
from django.utils import timezone from django.utils import timezone
@ -22,12 +21,11 @@ from cvat.apps.engine.serializers import LabeledDataSerializer
from cvat.apps.engine.annotation import put_task_data, patch_task_data from cvat.apps.engine.annotation import put_task_data, patch_task_data
from .models import AnnotationModel, FrameworkChoice from .models import AnnotationModel, FrameworkChoice
from .model_loader import ModelLoader, load_labelmap from .model_loader import load_labelmap
from .image_loader import ImageLoader from .image_loader import ImageLoader
from .inference import run_inference_engine_annotation from .inference import run_inference_engine_annotation
def _remove_old_file(model_file_field): def _remove_old_file(model_file_field):
if model_file_field and os.path.exists(model_file_field.name): if model_file_field and os.path.exists(model_file_field.name):
os.remove(model_file_field.name) os.remove(model_file_field.name)

@ -1,794 +0,0 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
/* global
showMessage
showOverlay
userConfirm
*/
window.cvatUI = window.cvatUI || {};
const AutoAnnotationServer = {
start(modelId, taskId, data, success, error, progress, check) {
$.ajax({
url: `/auto_annotation/start/${modelId}/${taskId}`,
type: 'POST',
data: JSON.stringify(data),
contentType: 'application/json',
success: (responseData) => {
check(responseData.id, success, error, progress);
},
error: (responseData) => {
const message = `Starting request has been failed. Code: ${responseData.status}. Message: ${responseData.responseText || responseData.statusText}`;
error(message);
},
});
},
update(data, success, error, progress, check, modelId) {
let url = '';
if (modelId === null) {
url = '/auto_annotation/create';
} else {
url = `/auto_annotation/update/${modelId}`;
}
$.ajax({
url,
type: 'POST',
data,
contentType: false,
processData: false,
success: (responseData) => {
check(responseData.id, success, error, progress);
},
error: (responseData) => {
const message = `Creating request has been failed. Code: ${responseData.status}. Message: ${responseData.responseText || responseData.statusText}`;
error(message);
},
});
},
delete(modelId, success, error) {
$.ajax({
url: `/auto_annotation/delete/${modelId}`,
type: 'DELETE',
success,
error: (data) => {
const message = `Deleting request has been failed. Code: ${data.status}. Message: ${data.responseText || data.statusText}`;
error(message);
},
});
},
check(workerId, success, error, progress) {
function updateProgress(data) {
if (data.progress && progress) {
progress(data.progress);
}
}
function checkCallback() {
$.ajax({
url: `/auto_annotation/check/${workerId}`,
type: 'GET',
success: (data) => {
updateProgress(data, progress);
switch (data.status) {
case 'failed':
error(`Checking request has returned the "${data.status}" status. Message: ${data.error}`);
break;
case 'unknown':
error(`Checking request has returned the "${data.status}" status.`);
break;
case 'finished':
success();
break;
default:
setTimeout(checkCallback, 1000);
}
},
error: (data) => {
const message = `Checking request has been failed. Code: ${data.status}. Message: ${data.responseText || data.statusText}`;
error(message);
},
});
}
setTimeout(checkCallback, 1000);
},
meta(tids, success, error) {
$.ajax({
url: '/auto_annotation/meta/get',
type: 'POST',
data: JSON.stringify(tids),
contentType: 'application/json',
success,
error: (data) => {
const message = `Getting meta request has been failed. Code: ${data.status}. Message: ${data.responseText || data.statusText}`;
error(message);
},
});
},
cancel(tid, success, error) {
$.ajax({
url: `/auto_annotation/cancel/${tid}`,
type: 'GET',
success,
error: (data) => {
const message = `Getting meta request has been failed. Code: ${data.status}. Message: ${data.responseText || data.statusText}`;
error(message);
},
});
},
};
class AutoAnnotationModelManagerView {
constructor() {
const html = `<div class="modal hidden" id="${window.cvatUI.autoAnnotation.managerWindowId}">
<div class="modal-content" id="${window.cvatUI.autoAnnotation.managerContentId}">
<div style="float: left; width: 55%; height: 100%;">
<center>
<label class="regular h1"> Uploaded Models </label>
</center>
<div style="overflow: auto; height: 90%; margin-top: 2%;">
<table class="regular modelsTable">
<thead>
<tr>
<th> Name </th>
<th> Upload Date </th>
<th> Actions </th>
</tr>
</thead>
<tbody id="${window.cvatUI.autoAnnotation.managerUploadedModelsId}"> </tbody>
</table>
</div>
</div>
<div class="regular" id="${window.cvatUI.autoAnnotation.uploadContentId}">
<center>
<label class="regular h1" id="${window.cvatUI.autoAnnotation.uploadTitleId}"> Create Model </label>
</center>
<table>
<tr>
<td style="width: 25%"> <label class="regular h3"> Name: </label> </td>
<td> <input type="text" id="${window.cvatUI.autoAnnotation.uploadNameInputId}" class="regular h3" style="width: 100%"> </td>
</tr>
<tr>
<td> <label class="regular h3"> Source: </label> </td>
<td>
<input id="${window.cvatUI.autoAnnotation.uploadLocalSourceId}" type="radio" name="modelSourceType" value="local" checked>
<label for="${window.cvatUI.autoAnnotation.uploadLocalSourceId}" class="regular h3"> Local </label>
<br>
<input id="${window.cvatUI.autoAnnotation.uploadShareSourceId}" type="radio" name="modelSourceType" value="shared">
<label for="${window.cvatUI.autoAnnotation.uploadShareSourceId}" class="regular h3"> Share </label>
</td>
</tr>
<tr id="${window.cvatUI.autoAnnotation.uploadGloballyBlockId}">
<td> <label class="regular h3"> Upload Globally </label> </td>
<td> <input type="checkbox" id="${window.cvatUI.autoAnnotation.uploadGloballyId}"> </td>
</tr>
</table>
<div style="text-align: left;">
<div>
<button id="${window.cvatUI.autoAnnotation.selectFilesButtonId}" class="regular h3"> Select Files </button>
<label id="${window.cvatUI.autoAnnotation.selectedFilesId}" class="regular h3" style="margin-left: 10px"> No Files </label>
<input id="${window.cvatUI.autoAnnotation.localFileSelectorId}" type="file" accept=".bin,.xml,.json,.py" style="display: none" multiple>
</div>
</div>
<div>
<div style="float: right; width: 50%; height: 50px;">
<button class="regular h3" id="${window.cvatUI.autoAnnotation.submitUploadButtonId}"> Submit </button>
<button class="regular h3" id="${window.cvatUI.autoAnnotation.cancelUploadButtonId}"> Cancel </button>
</div>
<div style="float: left; overflow-y: auto; height: 75px; overflow: auto; width: 100%; word-break: break-word;">
<label class="regular h3 selectable" style="float: left;" id="${window.cvatUI.autoAnnotation.uploadMessageId}"> </label>
</div>
</div>
</div>
</div>`;
this.el = $(html);
this.table = this.el.find(`#${window.cvatUI.autoAnnotation.managerUploadedModelsId}`);
this.globallyBlock = this.el.find(`#${window.cvatUI.autoAnnotation.uploadGloballyBlockId}`);
this.uploadTitle = this.el.find(`#${window.cvatUI.autoAnnotation.uploadTitleId}`);
this.uploadNameInput = this.el.find(`#${window.cvatUI.autoAnnotation.uploadNameInputId}`);
this.uploadMessage = this.el.find(`#${window.cvatUI.autoAnnotation.uploadMessageId}`);
this.selectedFilesLabel = this.el.find(`#${window.cvatUI.autoAnnotation.selectedFilesId}`);
this.modelNameInput = this.el.find(`#${window.cvatUI.autoAnnotation.uploadNameInputId}`);
this.localSource = this.el.find(`#${window.cvatUI.autoAnnotation.uploadLocalSourceId}`);
this.shareSource = this.el.find(`#${window.cvatUI.autoAnnotation.uploadShareSourceId}`);
this.cancelButton = this.el.find(`#${window.cvatUI.autoAnnotation.cancelUploadButtonId}`);
this.submitButton = this.el.find(`#${window.cvatUI.autoAnnotation.submitUploadButtonId}`);
this.globallyBox = this.el.find(`#${window.cvatUI.autoAnnotation.uploadGloballyId}`);
this.selectButton = this.el.find(`#${window.cvatUI.autoAnnotation.selectFilesButtonId}`);
this.localSelector = this.el.find(`#${window.cvatUI.autoAnnotation.localFileSelectorId}`);
this.shareSelector = $('#dashboardShareBrowseModal');
this.shareBrowseTree = $('#dashboardShareBrowser');
this.submitShare = $('#dashboardSubmitBrowseServer');
this.id = null;
this.source = this.localSource.prop('checked') ? 'local' : 'shared';
this.files = [];
function filesLabel(source, files) {
const fileLabels = source === 'local' ? [...files].map(el => el.name) : files;
if (fileLabels.length) {
const labelStr = fileLabels.join(', ');
if (labelStr.length > 30) {
return `${labelStr.substr(0, 30)}..`;
}
return labelStr;
}
return 'No Files';
}
function extractFiles(extensions, files, source) {
const extractedFiles = {};
function getExt(file) {
return source === 'local' ? file.name.split('.').pop() : file.split('.').pop();
}
function addFile(file, extention) {
if (extention in files) {
throw Error(`More than one file with the extension .${extention} have been found`);
}
extractedFiles[extention] = file;
}
files.forEach((file) => {
const fileExt = getExt(file);
if (extensions.includes(fileExt)) {
addFile(file, fileExt);
}
});
return extractedFiles;
}
function validateFiles(isUpdate, files, source) {
const extensions = ['xml', 'bin', 'py', 'json'];
const extractedFiles = extractFiles(extensions, files, source);
if (!isUpdate) {
extensions.forEach((extension) => {
if (!(extension in extractedFiles)) {
throw Error(`Please specify a .${extension} file`);
}
});
}
return extractedFiles;
}
this.localSource.on('click', () => {
if (this.source !== 'local') {
this.source = 'local';
this.files = [];
this.selectedFilesLabel.text(filesLabel(this.source, this.files));
}
});
this.shareSource.on('click', () => {
if (this.source !== 'shared') {
this.source = 'shared';
this.files = [];
this.selectedFilesLabel.text(filesLabel(this.source, this.files));
}
});
this.selectButton.on('click', () => {
if (this.source === 'local') {
this.localSelector.click();
} else {
this.shareSelector.appendTo('body');
this.shareBrowseTree.jstree('refresh');
this.shareSelector.removeClass('hidden');
this.shareBrowseTree.jstree({
core: {
async data(obj, callback) {
const directory = obj.id === '#' ? '' : `${obj.id}/`;
let shareFiles = await window.cvat.server.share(directory);
shareFiles = Array.from(shareFiles, (element) => {
const shareFileInfo = {
id: `${directory}${element.name}`,
children: element.type === 'DIR',
text: element.name,
icon: element.type === 'DIR' ? 'jstree-folder' : 'jstree-file',
};
return shareFileInfo;
});
callback.call(this, shareFiles);
},
},
plugins: ['checkbox', 'sort'],
});
}
});
this.submitShare.on('click', () => {
if (!this.el.hasClass('hidden')) {
this.shareSelector.addClass('hidden');
this.files = this.shareBrowseTree.jstree(true).get_selected();
this.selectedFilesLabel.text(filesLabel(this.source, this.files));
}
});
this.localSelector.on('change', (e) => {
this.files = Array.from(e.target.files);
this.selectedFilesLabel.text(filesLabel(this.source, this.files));
});
this.cancelButton.on('click', () => this.el.addClass('hidden'));
this.submitButton.on('click', () => {
try {
this.submitButton.prop('disabled', true);
const name = $.trim(this.modelNameInput.prop('value'));
if (!name.length) {
this.uploadMessage.css('color', 'red');
this.uploadMessage.text('Please specify a model name');
return;
}
let validatedFiles = {};
try {
validatedFiles = validateFiles(this.id !== null, this.files, this.source);
} catch (err) {
this.uploadMessage.css('color', 'red');
this.uploadMessage.text(err);
return;
}
const modelData = new FormData();
modelData.append('name', name);
modelData.append('storage', this.source);
modelData.append('shared', this.globallyBox.prop('checked'));
['xml', 'bin', 'json', 'py'].filter(e => e in validatedFiles).forEach((ext) => {
modelData.append(ext, validatedFiles[ext]);
});
this.uploadMessage.text('');
const overlay = showOverlay('Send request to the server..');
window.cvatUI.autoAnnotation.server.update(modelData, () => {
window.location.reload();
}, (message) => {
overlay.remove();
showMessage(message);
}, (progress) => {
overlay.setMessage(progress);
}, window.cvatUI.autoAnnotation.server.check, this.id);
} finally {
this.submitButton.prop('disabled', false);
}
});
}
reset() {
const setBlocked = () => {
if (window.cvatUI.autoAnnotation.data.admin) {
this.globallyBlock.removeClass('hidden');
} else {
this.globallyBlock.addClass('hidden');
}
};
setBlocked();
this.uploadTitle.text('Create Model');
this.uploadNameInput.prop('value', '');
this.uploadMessage.css('color', '');
this.uploadMessage.text('');
this.selectedFilesLabel.text('No Files');
this.localSource.prop('checked', true);
this.globallyBox.prop('checked', false);
this.table.empty();
this.id = null;
this.source = this.localSource.prop('checked') ? 'local' : 'share';
this.files = [];
const updateButtonClickHandler = (event) => {
this.reset();
this.uploadTitle.text('Update Model');
this.uploadNameInput.prop('value', `${event.data.model.name}`);
this.id = event.data.model.id;
};
const deleteButtonClickHandler = (event) => {
userConfirm(`Do you actually want to delete the "${event.data.model.name}" model. Are you sure?`, () => {
window.cvatUI.autoAnnotation.server.delete(event.data.model.id, () => {
const filtered = window.cvatUI.autoAnnotation.data.models.filter(
item => item !== event.data.model,
);
window.cvatUI.autoAnnotation.data.models = filtered;
this.reset();
}, (message) => {
showMessage(message);
});
});
};
const getModelModifyButtons = (model) => {
if (model.primary) {
return '<td> <label class="h1 regular"> Primary Model </label> </td>';
}
const updateButtonHtml = '<button class="regular h3" style="width: 7em;"> Update </button>';
const deleteButtonHtml = '<button class="regular h3" style="width: 7em; margin-top: 5%;"> Delete </button>';
return $('<td> </td>').append(
$(updateButtonHtml).on('click', { model }, updateButtonClickHandler),
$(deleteButtonHtml).on('click', { model }, deleteButtonClickHandler),
);
};
window.cvatUI.autoAnnotation.data.models.forEach((model) => {
const rowHtml = `<tr>
<td> ${model.name} </td>
<td> ${model.uploadDate} </td>
</tr>`;
this.table.append(
$(rowHtml).append(getModelModifyButtons(model)),
);
});
return this;
}
show() {
this.el.removeClass('hidden');
return this;
}
get element() {
return this.el;
}
}
class AutoAnnotationModelRunnerView {
constructor() {
const html = `<div class="modal hidden" id="${window.cvatUI.autoAnnotation.runnerWindowId}">
<div class="modal-content" id="${window.cvatUI.autoAnnotation.runnerContentId}">
<div style="width: 55%; height: 100%; float: left;">
<center style="height: 10%;">
<label class="regular h1"> Uploaded Models </label>
</center>
<div style="height: 70%; overflow: auto; margin-top: 2%;">
<table class="modelsTable" id="${window.cvatUI.autoAnnotation.runnerUploadedModelsId}"> </table>
</div>
<div>
<input type="checkbox" id="${window.cvatUI.autoAnnotation.removeCurrentAnnotationId}"/>
<label class="regular h3" for="${window.cvatUI.autoAnnotation.removeCurrentAnnotationId}"> Remove current annotation </label>
</div>
</div>
<div style="width: 40%; height: 100%; float: left; margin-left: 3%;">
<center style="height: 10%;">
<label class="regular h1"> Annotation Labels </label>
</center>
<div style="height: 70%; overflow: auto; margin-top: 2%;">
<table class="regular" style="text-align: center; word-break: break-all; width: 100%;">
<thead>
<tr style="width: 100%;">
<th style="width: 45%;"> Task Label </th>
<th style="width: 45%;"> DL Model Label </th>
<th style="width: 10%;"> </th>
</tr>
</thead>
<tbody id="${window.cvatUI.autoAnnotation.annotationLabelsId}">
</tbody>
</table>
</div>
<div style="float:right;">
<button class="regular h3" style="width: 6em;" id="${window.cvatUI.autoAnnotation.submitAnnotationId}"> Start </button>
<button class="regular h3" style="width: 6em;" id="${window.cvatUI.autoAnnotation.cancelAnnotationId}"> Cancel </button>
</div>
</div>
</div>
</div>`;
this.el = $(html);
this.id = null;
this.tid = null;
this.initButton = null;
this.modelsTable = this.el.find(`#${window.cvatUI.autoAnnotation.runnerUploadedModelsId}`);
this.labelsTable = this.el.find(`#${window.cvatUI.autoAnnotation.annotationLabelsId}`);
this.active = null;
this.el.find(`#${window.cvatUI.autoAnnotation.cancelAnnotationId}`).on('click', () => {
this.el.addClass('hidden');
});
this.el.find(`#${window.cvatUI.autoAnnotation.submitAnnotationId}`).on('click', () => {
try {
if (this.id === null) {
throw Error('Please specify a model for an annotation process');
}
const mapping = {};
$('.annotatorMappingRow').each((_, element) => {
const dlModelLabel = $(element).find('.annotatorDlLabelSelector')[0].value;
const taskLabel = $(element).find('.annotatorTaskLabelSelector')[0].value;
if (dlModelLabel in mapping) {
throw Error(`The label "${dlModelLabel}" has been specified twice or more`);
}
mapping[dlModelLabel] = taskLabel;
});
if (!Object.keys(mapping).length) {
throw Error('Labels for an annotation process haven\'t been found');
}
const overlay = showOverlay('Request has been sent');
window.cvatUI.autoAnnotation.server.start(this.id, this.tid, {
reset: $(`#${window.cvatUI.autoAnnotation.removeCurrentAnnotationId}`).prop('checked'),
labels: mapping,
}, () => {
overlay.remove();
this.initButton[0].setupRun();
window.cvatUI.autoAnnotation.runner.hide();
}, (message) => {
overlay.remove();
this.initButton[0].setupRun();
showMessage(message);
}, () => {
window.location.reload();
}, window.cvatUI.autoAnnotation.server.check);
} catch (error) {
showMessage(error);
}
});
}
reset(data, initButton) {
function labelsSelect(labels, elClass) {
const select = $(`<select class="regular h3 ${elClass}" style="width:100%;"> </select>`);
labels.forEach(label => select.append($(`<option value="${label}"> ${label} </option>`)));
select.prop('value', null);
return select;
}
function makeCreator(dlSelect, taskSelect, callback) {
let dlIsFilled = false;
let taskIsFilled = false;
const creator = $('<tr style="margin-bottom: 5px;"> </tr>').append(
$('<td style="width: 45%;"> </td>').append(taskSelect),
$('<td style="width: 45%;"> </td>').append(dlSelect),
);
const onSelectHandler = () => {
$('<td style="width: 10%; position: relative;"> </td>').append(
$('<a class="close"></a>').css('top', '0px').on('click', (e) => {
$(e.target.parentNode.parentNode).remove();
}),
).appendTo(creator);
creator.addClass('annotatorMappingRow');
callback();
};
dlSelect.on('change', (e) => {
if (e.target.value && taskIsFilled) {
dlSelect.off('change');
taskSelect.off('change');
onSelectHandler();
}
dlIsFilled = Boolean(e.target.value);
});
taskSelect.on('change', (e) => {
if (e.target.value && dlIsFilled) {
dlSelect.off('change');
taskSelect.off('change');
onSelectHandler();
}
taskIsFilled = Boolean(e.target.value);
});
return creator;
}
this.id = null;
this.initButton = initButton;
this.tid = data.id;
this.modelsTable.empty();
this.labelsTable.empty();
this.active = null;
const modelItemClickHandler = (event) => {
if (this.active) {
this.active.style.color = '';
}
this.id = event.data.model.id;
this.active = event.target;
this.active.style.color = 'darkblue';
this.labelsTable.empty();
const labels = event.data.data.labels.map(x => x.name);
const intersection = labels.filter(el => event.data.model.labels.indexOf(el) !== -1);
intersection.forEach((label) => {
const dlSelect = labelsSelect(event.data.model.labels, 'annotatorDlLabelSelector');
dlSelect.prop('value', label);
const taskSelect = labelsSelect(labels, 'annotatorTaskLabelSelector');
taskSelect.prop('value', label);
$('<tr class="annotatorMappingRow" style="margin-bottom: 5px;"> </tr>').append(
$('<td style="width: 45%;"> </td>').append(taskSelect),
$('<td style="width: 45%;"> </td>').append(dlSelect),
$('<td style="width: 10%; position: relative;"> </td>').append(
$('<a class="close"></a>').css('top', '0px').on('click', (e) => {
$(e.target.parentNode.parentNode).remove();
}),
),
).appendTo(this.labelsTable);
});
const dlSelect = labelsSelect(event.data.model.labels, 'annotatorDlLabelSelector');
const taskSelect = labelsSelect(labels, 'annotatorTaskLabelSelector');
const callback = () => {
makeCreator(
labelsSelect(event.data.model.labels, 'annotatorDlLabelSelector'),
labelsSelect(labels, 'annotatorTaskLabelSelector'),
callback,
).appendTo(this.labelsTable);
};
makeCreator(dlSelect, taskSelect, callback).appendTo(this.labelsTable);
};
window.cvatUI.autoAnnotation.data.models.forEach((model) => {
this.modelsTable.append(
$(`<tr> <td> <label class="regular h3"> ${model.name} (${model.uploadDate}) </label> </td> </tr>`).on(
'click', { model, data }, modelItemClickHandler,
),
);
});
return this;
}
show() {
this.el.removeClass('hidden');
return this;
}
hide() {
this.el.addClass('hidden');
return this;
}
get element() {
return this.el;
}
}
window.cvatUI.autoAnnotation = {
managerWindowId: 'annotatorManagerWindow',
managerContentId: 'annotatorManagerContent',
managerUploadedModelsId: 'annotatorManagerUploadedModels',
uploadContentId: 'annotatorManagerUploadModel',
uploadTitleId: 'annotatorManagerUploadTitle',
uploadNameInputId: 'annotatorManagerUploadNameInput',
uploadLocalSourceId: 'annotatorManagerUploadLocalSource',
uploadShareSourceId: 'annotatorManagerUploadShareSource',
uploadGloballyId: 'annotatorManagerUploadGlobally',
uploadGloballyBlockId: 'annotatorManagerUploadGloballyblock',
selectFilesButtonId: 'annotatorManagerUploadSelector',
selectedFilesId: 'annotatorManagerUploadSelectedFiles',
localFileSelectorId: 'annotatorManagerUploadLocalSelector',
shareFileSelectorId: 'annotatorManagerUploadShareSelector',
submitUploadButtonId: 'annotatorManagerSubmitUploadButton',
cancelUploadButtonId: 'annotatorManagerCancelUploadButton',
uploadMessageId: 'annotatorUploadStatusMessage',
runnerWindowId: 'annotatorRunnerWindow',
runnerContentId: 'annotatorRunnerContent',
runnerUploadedModelsId: 'annotatorRunnerUploadedModels',
removeCurrentAnnotationId: 'annotatorRunnerRemoveCurrentAnnotationBox',
annotationLabelsId: 'annotatorRunnerAnnotationLabels',
submitAnnotationId: 'annotatorRunnerSubmitAnnotationButton',
cancelAnnotationId: 'annotatorRunnerCancelAnnotationButton',
managerButtonId: 'annotatorManagerButton',
};
window.addEventListener('DOMContentLoaded', () => {
window.cvatUI.autoAnnotation.server = AutoAnnotationServer;
window.cvatUI.autoAnnotation.manager = new AutoAnnotationModelManagerView();
window.cvatUI.autoAnnotation.runner = new AutoAnnotationModelRunnerView();
$('body').append(window.cvatUI.autoAnnotation.manager.element, window.cvatUI.autoAnnotation.runner.element);
$(`<button id="${window.cvatUI.autoAnnotation.managerButtonId}" class="regular h1" style=""> Model Manager</button>`)
.on('click', () => {
const overlay = showOverlay('The manager are being setup..');
window.cvatUI.autoAnnotation.manager.reset().show();
overlay.remove();
}).appendTo('#dashboardManageButtons');
});
window.addEventListener('dashboardReady', (event) => {
const elements = $('.dashboardItem');
const tids = Array.from(elements, el => +el.getAttribute('tid'));
window.cvatUI.autoAnnotation.server.meta(tids, (data) => {
window.cvatUI.autoAnnotation.data = data;
elements.each(function setupDashboardItem() {
const elem = $(this);
const tid = +elem.attr('tid');
const button = $('<button> Run Auto Annotation </button>').addClass('regular dashboardButtonUI');
button[0].setupRun = function setupRun() {
const self = $(this);
const taskInfo = event.detail.filter(task => task.id === tid)[0];
self.text('Run Auto Annotation').off('click').on('click', () => {
window.cvatUI.autoAnnotation.runner.reset(taskInfo, self).show();
});
};
button[0].setupCancel = function setupCancel() {
const self = $(this);
self.off('click').text('Cancel Auto Annotation').on('click', () => {
userConfirm('Process will be canceled. Are you sure?', () => {
window.cvatUI.autoAnnotation.server.cancel(tid, () => {
this.setupRun();
}, (message) => {
showMessage(message);
});
});
});
window.cvatUI.autoAnnotation.server.check(
window.cvatUI.autoAnnotation.data.run[tid].rq_id,
() => {
this.setupRun();
},
(error) => {
button[0].setupRun();
button.text('Annotation has failed');
button.title(error);
},
(progress) => {
button.text(`Cancel Auto Annotation (${progress.toString().slice(0, 4)})%`);
},
);
};
const taskStatus = window.cvatUI.autoAnnotation.data.run[tid];
if (taskStatus && ['queued', 'started'].includes(taskStatus.status)) {
button[0].setupCancel();
} else {
button[0].setupRun();
}
button.appendTo(elem.find('div.dashboardButtonsUI')[0]);
});
}, (error) => {
showMessage(`Cannot get models meta information: ${error}`);
});
});

@ -1,83 +0,0 @@
#annotatorManagerContent, #annotatorRunnerContent {
width: 800px;
height: 300px;
}
#annotatorManagerButton {
padding: 7px;
margin-left: 4px;
}
.modelsTable {
width: 100%;
color:#666;
text-shadow: 1px 1px 0px #fff;
background:#D2D3D4;
border:#ccc 1px solid;
border-radius: 3px;
box-shadow: 0 1px 2px black;
}
.modelsTable th {
border-top: 1px solid #fafafa;
border-bottom: 1px solid #e0e0e0;
background: #ededed;
}
.modelsTable th:first-child {
text-align: left;
padding-left:20px;
}
.modelsTable tr:first-child th:first-child {
border-top-left-radius:3px;
}
.modelsTable tr:first-child th:last-child {
border-top-right-radius:3px;
}
.modelsTable tr {
text-align: center;
padding-left: 20px;
}
.modelsTable td:first-child {
text-align: left;
padding-left: 20px;
border-left: 0;
}
.modelsTable td {
padding: 18px;
border-top: 1px solid #ffffff;
border-bottom:1px solid #e0e0e0;
border-left: 1px solid #e0e0e0;
background: #fafafa;
}
.modelsTable tr.even td {
background: #f6f6f6;
}
.modelsTable tr:last-child td {
border-bottom:0;
}
.modelsTable tr:last-child td:first-child {
border-bottom-left-radius:3px;
}
.modelsTable tr:last-child td:last-child {
border-bottom-right-radius:3px;
}
.modelsTable tr:hover td {
background: #f2f2f2;
}
#annotatorManagerUploadModel {
float: left;
padding-left: 3%;
width: 40%;
}

@ -7,6 +7,7 @@ import json
import os import os
from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
from rest_framework.decorators import api_view
from django.db.models import Q from django.db.models import Q
from rules.contrib.views import permission_required, objectgetter from rules.contrib.views import permission_required, objectgetter
@ -124,6 +125,7 @@ def delete_model(request, mid):
model_manager.delete(mid) model_manager.delete(mid)
return HttpResponse() return HttpResponse()
@api_view(['POST'])
@login_required @login_required
def get_meta_info(request): def get_meta_info(request):
try: try:

@ -1,9 +1,4 @@
# Copyright (C) 2018 Intel Corporation # Copyright (C) 2018-2019 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from cvat.settings.base import JS_3RDPARTY
JS_3RDPARTY['dashboard'] = JS_3RDPARTY.get('dashboard', []) + ['auto_segmentation/js/dashboardPlugin.js']

@ -1,112 +0,0 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
/* global
userConfirm:false
showMessage:false
*/
window.addEventListener('dashboardReady', () => {
function checkProcess(tid, button) {
function checkCallback() {
$.get(`/tensorflow/segmentation/check/task/${tid}`).done((statusData) => {
if (['started', 'queued'].includes(statusData.status)) {
const progress = Math.round(statusData.progress) || '0';
button.text(`Cancel Auto Segmentation (${progress}%)`);
setTimeout(checkCallback, 5000);
} else {
button.text('Run Auto Segmentation');
button.removeClass('tfAnnotationProcess');
button.prop('disabled', false);
if (statusData.status === 'failed') {
const message = `Tensorflow Segmentation failed. Error: ${statusData.stderr}`;
showMessage(message);
} else if (statusData.status !== 'finished') {
const message = `Tensorflow segmentation check request returned status "${statusData.status}"`;
showMessage(message);
}
}
}).fail((errorData) => {
const message = `Can not sent tensorflow segmentation check request. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
});
}
setTimeout(checkCallback, 5000);
}
function runProcess(tid, button) {
$.get(`/tensorflow/segmentation/create/task/${tid}`).done(() => {
showMessage('Process has started');
button.text('Cancel Auto Segmentation (0%)');
button.addClass('tfAnnotationProcess');
checkProcess(tid, button);
}).fail((errorData) => {
const message = `Can not run Auto Segmentation. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
});
}
function cancelProcess(tid, button) {
$.get(`/tensorflow/segmentation/cancel/task/${tid}`).done(() => {
button.prop('disabled', true);
}).fail((errorData) => {
const message = `Can not cancel Auto Segmentation. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
});
}
function setupDashboardItem(item, metaData) {
const tid = +item.attr('tid');
const button = $('<button> Run Auto Segmentation </button>');
button.on('click', () => {
if (button.hasClass('tfAnnotationProcess')) {
userConfirm('The process will be canceled. Continue?', () => {
cancelProcess(tid, button);
});
} else {
userConfirm('The current annotation will be lost. Are you sure?', () => {
runProcess(tid, button);
});
}
});
button.addClass('dashboardTFAnnotationButton regular dashboardButtonUI');
button.appendTo(item.find('div.dashboardButtonsUI'));
if ((tid in metaData) && (metaData[tid].active)) {
button.text('Cancel Auto Segmentation');
button.addClass('tfAnnotationProcess');
checkProcess(tid, button);
}
}
const elements = $('.dashboardItem');
const tids = Array.from(elements, el => +el.getAttribute('tid'));
$.ajax({
type: 'POST',
url: '/tensorflow/segmentation/meta/get',
data: JSON.stringify(tids),
contentType: 'application/json; charset=utf-8',
}).done((metaData) => {
elements.each(function setupDashboardItemWrapper() {
setupDashboardItem($(this), metaData);
});
}).fail((errorData) => {
const message = `Can not get Auto Segmentation meta info. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
});
});

@ -1,9 +1,11 @@
# Copyright (C) 2018 Intel Corporation # Copyright (C) 2018-2019 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
from rest_framework.decorators import api_view
from rules.contrib.views import permission_required, objectgetter from rules.contrib.views import permission_required, objectgetter
from cvat.apps.authentication.decorators import login_required from cvat.apps.authentication.decorators import login_required
from cvat.apps.engine.models import Task as TaskModel from cvat.apps.engine.models import Task as TaskModel
@ -185,9 +187,10 @@ def create_thread(tid, labels_mapping, user):
try: try:
slogger.task[tid].exception('exception was occured during auto segmentation of the task', exc_info=True) slogger.task[tid].exception('exception was occured during auto segmentation of the task', exc_info=True)
except Exception: except Exception:
slogger.glob.exception('exception was occured during auto segmentation of the task {}'.format(tid), exc_into=True) slogger.glob.exception('exception was occured during auto segmentation of the task {}'.format(tid), exc_info=True)
raise ex raise ex
@api_view(['POST'])
@login_required @login_required
def get_meta_info(request): def get_meta_info(request):
try: try:
@ -204,7 +207,7 @@ def get_meta_info(request):
return JsonResponse(result) return JsonResponse(result)
except Exception as ex: except Exception as ex:
slogger.glob.exception('exception was occured during tf meta request', exc_into=True) slogger.glob.exception('exception was occured during tf meta request', exc_info=True)
return HttpResponseBadRequest(str(ex)) return HttpResponseBadRequest(str(ex))

@ -1,8 +0,0 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
from cvat.settings.base import JS_3RDPARTY
JS_3RDPARTY['engine'] = JS_3RDPARTY.get('engine', []) + ['dashboard/js/enginePlugin.js']

@ -1,13 +0,0 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
from django.apps import AppConfig
class DashboardConfig(AppConfig):
name = 'cvat.apps.dashboard'
def ready(self):
# plugin registration
pass

@ -1,5 +0,0 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

@ -1,107 +0,0 @@
/*!
* Generated using the Bootstrap Customizer (https://getbootstrap.com/docs/3.4/customize/)
*/
/*!
* Bootstrap v3.4.1 (https://getbootstrap.com/)
* Copyright 2011-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
.pagination {
display: inline-block;
padding-left: 0;
margin: 20px 0;
border-radius: 4px;
}
.pagination > li {
display: inline;
}
.pagination > li > a,
.pagination > li > span {
position: relative;
float: left;
padding: 6px 12px;
margin-left: -1px;
line-height: 1.42857143;
color: #337ab7;
text-decoration: none;
background-color: #ffffff;
border: 1px solid #dddddd;
}
.pagination > li > a:hover,
.pagination > li > span:hover,
.pagination > li > a:focus,
.pagination > li > span:focus {
z-index: 2;
color: #23527c;
background-color: #eeeeee;
border-color: #dddddd;
}
.pagination > li:first-child > a,
.pagination > li:first-child > span {
margin-left: 0;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.pagination > li:last-child > a,
.pagination > li:last-child > span {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.pagination > .active > a,
.pagination > .active > span,
.pagination > .active > a:hover,
.pagination > .active > span:hover,
.pagination > .active > a:focus,
.pagination > .active > span:focus {
z-index: 3;
color: #ffffff;
cursor: default;
background-color: #337ab7;
border-color: #337ab7;
}
.pagination > .disabled > span,
.pagination > .disabled > span:hover,
.pagination > .disabled > span:focus,
.pagination > .disabled > a,
.pagination > .disabled > a:hover,
.pagination > .disabled > a:focus {
color: #777777;
cursor: not-allowed;
background-color: #ffffff;
border-color: #dddddd;
}
.pagination-lg > li > a,
.pagination-lg > li > span {
padding: 10px 16px;
font-size: 18px;
line-height: 1.3333333;
}
.pagination-lg > li:first-child > a,
.pagination-lg > li:first-child > span {
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
}
.pagination-lg > li:last-child > a,
.pagination-lg > li:last-child > span {
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
.pagination-sm > li > a,
.pagination-sm > li > span {
padding: 5px 10px;
font-size: 12px;
line-height: 1.5;
}
.pagination-sm > li:first-child > a,
.pagination-sm > li:first-child > span {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
.pagination-sm > li:last-child > a,
.pagination-sm > li:last-child > span {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}

@ -1,364 +0,0 @@
/*!
* jQuery pagination plugin v1.4.2
* http://josecebe.github.io/twbs-pagination/
*
* Copyright 2014-2018, Eugene Simakin
* Released under Apache 2.0 license
* http://apache.org/licenses/LICENSE-2.0.html
*/
(function ($, window, document, undefined) {
'use strict';
var old = $.fn.twbsPagination;
// PROTOTYPE AND CONSTRUCTOR
var TwbsPagination = function (element, options) {
this.$element = $(element);
this.options = $.extend({}, $.fn.twbsPagination.defaults, options);
if (this.options.startPage < 1 || this.options.startPage > this.options.totalPages) {
throw new Error('Start page option is incorrect');
}
this.options.totalPages = parseInt(this.options.totalPages);
if (isNaN(this.options.totalPages)) {
throw new Error('Total pages option is not correct!');
}
this.options.visiblePages = parseInt(this.options.visiblePages);
if (isNaN(this.options.visiblePages)) {
throw new Error('Visible pages option is not correct!');
}
if (this.options.beforePageClick instanceof Function) {
this.$element.first().on('beforePage', this.options.beforePageClick);
}
if (this.options.onPageClick instanceof Function) {
this.$element.first().on('page', this.options.onPageClick);
}
// hide if only one page exists
if (this.options.hideOnlyOnePage && this.options.totalPages == 1) {
if (this.options.initiateStartPageClick) {
this.$element.trigger('page', 1);
}
return this;
}
if (this.options.href) {
this.options.startPage = this.getPageFromQueryString();
if (!this.options.startPage) {
this.options.startPage = 1;
}
}
var tagName = (typeof this.$element.prop === 'function') ?
this.$element.prop('tagName') : this.$element.attr('tagName');
if (tagName === 'UL') {
this.$listContainer = this.$element;
} else {
var elements = this.$element;
var $newListContainer = $([]);
elements.each(function(index) {
var $newElem = $("<ul></ul>");
$(this).append($newElem);
$newListContainer.push($newElem[0]);
});
this.$listContainer = $newListContainer;
this.$element = $newListContainer;
}
this.$listContainer.addClass(this.options.paginationClass);
if (this.options.initiateStartPageClick) {
this.show(this.options.startPage);
} else {
this.currentPage = this.options.startPage;
this.render(this.getPages(this.options.startPage));
this.setupEvents();
}
return this;
};
TwbsPagination.prototype = {
constructor: TwbsPagination,
destroy: function () {
this.$element.empty();
this.$element.removeData('twbs-pagination');
this.$element.off('page');
return this;
},
show: function (page) {
if (page < 1 || page > this.options.totalPages) {
throw new Error('Page is incorrect.');
}
this.currentPage = page;
this.$element.trigger('beforePage', page);
var pages = this.getPages(page);
this.render(pages);
this.setupEvents();
this.$element.trigger('page', page);
return pages;
},
enable: function () {
this.show(this.currentPage);
},
disable: function () {
var _this = this;
this.$listContainer.off('click').on('click', 'li', function (evt) {
evt.preventDefault();
});
this.$listContainer.children().each(function () {
var $this = $(this);
if (!$this.hasClass(_this.options.activeClass)) {
$(this).addClass(_this.options.disabledClass);
}
});
},
buildListItems: function (pages) {
var listItems = [];
if (this.options.first) {
listItems.push(this.buildItem('first', 1));
}
if (this.options.prev) {
var prev = pages.currentPage > 1 ? pages.currentPage - 1 : this.options.loop ? this.options.totalPages : 1;
listItems.push(this.buildItem('prev', prev));
}
for (var i = 0; i < pages.numeric.length; i++) {
listItems.push(this.buildItem('page', pages.numeric[i]));
}
if (this.options.next) {
var next = pages.currentPage < this.options.totalPages ? pages.currentPage + 1 : this.options.loop ? 1 : this.options.totalPages;
listItems.push(this.buildItem('next', next));
}
if (this.options.last) {
listItems.push(this.buildItem('last', this.options.totalPages));
}
return listItems;
},
buildItem: function (type, page) {
var $itemContainer = $('<li></li>'),
$itemContent = $('<a></a>'),
itemText = this.options[type] ? this.makeText(this.options[type], page) : page;
$itemContainer.addClass(this.options[type + 'Class']);
$itemContainer.data('page', page);
$itemContainer.data('page-type', type);
$itemContainer.append($itemContent.attr('href', this.makeHref(page)).addClass(this.options.anchorClass).html(itemText));
return $itemContainer;
},
getPages: function (currentPage) {
var pages = [];
var half = Math.floor(this.options.visiblePages / 2);
var start = currentPage - half + 1 - this.options.visiblePages % 2;
var end = currentPage + half;
var visiblePages = this.options.visiblePages;
if (visiblePages > this.options.totalPages) {
visiblePages = this.options.totalPages;
}
// handle boundary case
if (start <= 0) {
start = 1;
end = visiblePages;
}
if (end > this.options.totalPages) {
start = this.options.totalPages - visiblePages + 1;
end = this.options.totalPages;
}
var itPage = start;
while (itPage <= end) {
pages.push(itPage);
itPage++;
}
return {"currentPage": currentPage, "numeric": pages};
},
render: function (pages) {
var _this = this;
this.$listContainer.children().remove();
var items = this.buildListItems(pages);
$.each(items, function(key, item){
_this.$listContainer.append(item);
});
this.$listContainer.children().each(function () {
var $this = $(this),
pageType = $this.data('page-type');
switch (pageType) {
case 'page':
if ($this.data('page') === pages.currentPage) {
$this.addClass(_this.options.activeClass);
}
break;
case 'first':
$this.toggleClass(_this.options.disabledClass, pages.currentPage === 1);
break;
case 'last':
$this.toggleClass(_this.options.disabledClass, pages.currentPage === _this.options.totalPages);
break;
case 'prev':
$this.toggleClass(_this.options.disabledClass, !_this.options.loop && pages.currentPage === 1);
break;
case 'next':
$this.toggleClass(_this.options.disabledClass,
!_this.options.loop && pages.currentPage === _this.options.totalPages);
break;
default:
break;
}
});
},
setupEvents: function () {
var _this = this;
this.$listContainer.off('click').on('click', 'li', function (evt) {
var $this = $(this);
if ($this.hasClass(_this.options.disabledClass) || $this.hasClass(_this.options.activeClass)) {
return false;
}
// Prevent click event if href is not set.
!_this.options.href && evt.preventDefault();
_this.show(parseInt($this.data('page')));
});
},
changeTotalPages: function(totalPages, currentPage) {
this.options.totalPages = totalPages;
return this.show(currentPage);
},
makeHref: function (page) {
return this.options.href ? this.generateQueryString(page) : "#";
},
makeText: function (text, page) {
return text.replace(this.options.pageVariable, page)
.replace(this.options.totalPagesVariable, this.options.totalPages)
},
getPageFromQueryString: function (searchStr) {
var search = this.getSearchString(searchStr),
regex = new RegExp(this.options.pageVariable + '(=([^&#]*)|&|#|$)'),
page = regex.exec(search);
if (!page || !page[2]) {
return null;
}
page = decodeURIComponent(page[2]);
page = parseInt(page);
if (isNaN(page)) {
return null;
}
return page;
},
generateQueryString: function (pageNumber, searchStr) {
var search = this.getSearchString(searchStr),
regex = new RegExp(this.options.pageVariable + '=*[^&#]*');
if (!search) return '';
return '?' + search.replace(regex, this.options.pageVariable + '=' + pageNumber);
},
getSearchString: function (searchStr) {
var search = searchStr || window.location.search;
if (search === '') {
return null;
}
if (search.indexOf('?') === 0) search = search.substr(1);
return search;
},
getCurrentPage: function () {
return this.currentPage;
},
getTotalPages: function () {
return this.options.totalPages;
}
};
// PLUGIN DEFINITION
$.fn.twbsPagination = function (option) {
var args = Array.prototype.slice.call(arguments, 1);
var methodReturn;
var $this = $(this);
var data = $this.data('twbs-pagination');
var options = typeof option === 'object' ? option : {};
if (!data) $this.data('twbs-pagination', (data = new TwbsPagination(this, options) ));
if (typeof option === 'string') methodReturn = data[ option ].apply(data, args);
return ( methodReturn === undefined ) ? $this : methodReturn;
};
$.fn.twbsPagination.defaults = {
totalPages: 1,
startPage: 1,
visiblePages: 5,
initiateStartPageClick: true,
hideOnlyOnePage: false,
href: false,
pageVariable: '{{page}}',
totalPagesVariable: '{{total_pages}}',
page: null,
first: 'First',
prev: 'Previous',
next: 'Next',
last: 'Last',
loop: false,
beforePageClick: null,
onPageClick: null,
paginationClass: 'pagination',
nextClass: 'page-item next',
prevClass: 'page-item prev',
lastClass: 'page-item last',
firstClass: 'page-item first',
pageClass: 'page-item',
activeClass: 'active',
disabledClass: 'disabled',
anchorClass: 'page-link'
};
$.fn.twbsPagination.Constructor = TwbsPagination;
$.fn.twbsPagination.noConflict = function () {
$.fn.twbsPagination = old;
return this;
};
$.fn.twbsPagination.version = "1.4.2";
})(window.jQuery, window, document);

@ -1,797 +0,0 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
/* global
userConfirm:false
LabelsInfo:false
showMessage:false
showOverlay:false
isDefaultFormat:false
*/
class TaskView {
constructor(task, annotationFormats, exportFormats) {
this.init(task);
this._annotationFormats = annotationFormats;
this._exportFormats = exportFormats;
this._UI = null;
}
_disable() {
this._UI.find('*').attr('disabled', true).css('opacity', 0.5);
this._UI.find('.dashboardJobList').empty();
}
async _remove() {
try {
await this._task.delete();
this._disable();
} catch (exception) {
let { message } = exception;
if (exception instanceof window.cvat.exceptions.ServerError) {
message += ` Code: ${exception.code}`;
}
showMessage(message);
}
}
_update() {
$('#dashboardUpdateModal').remove();
const dashboardUpdateModal = $($('#dashboardUpdateTemplate').html()).appendTo('body');
// TODO: Use JSON labels format instead of custom
$('#dashboardOldLabels').prop('value', LabelsInfo.serialize(this._task.labels.map(el => el.toJSON())));
$('#dashboardCancelUpdate').on('click', () => {
dashboardUpdateModal.remove();
});
$('#dashboardSubmitUpdate').on('click', async () => {
let jsonLabels = null;
try {
jsonLabels = LabelsInfo.deserialize($('#dashboardNewLabels').prop('value'));
} catch (exception) {
showMessage(exception);
return;
}
try {
const labels = jsonLabels.map(label => new window.cvat.classes.Label(label));
this._task.labels = labels;
await this._task.save();
showMessage('Task has been successfully updated');
} catch (exception) {
let { message } = exception;
if (exception instanceof window.cvat.exceptions.ServerError) {
message += ` Code: ${exception.code}`;
}
showMessage(message);
}
dashboardUpdateModal.remove();
});
}
_upload(uploadAnnotationButton, format) {
const button = $(uploadAnnotationButton);
$(`<input type="file" accept=".${format.format}">`)
.on('change', async (onChangeEvent) => {
const file = onChangeEvent.target.files[0];
$(onChangeEvent.target).remove();
if (file) {
button.prop('disabled', true);
try {
await this._task.annotations.upload(file, format);
} catch (error) {
showMessage(error.message);
} finally {
button.prop('disabled', false);
}
}
}).click();
}
async _dump(button, format) {
button.disabled = true;
try {
const url = await this._task.annotations.dump(this._task.name, format);
const a = document.createElement('a');
a.href = `${url}`;
document.body.appendChild(a);
a.click();
a.remove();
} catch (error) {
showMessage(error.message);
} finally {
button.disabled = false;
}
}
async _exportDataset(button, formatName) {
button.disabled = true;
try {
const format = this._exportFormats.find((x) => {
return x.name == formatName;
});
if (!format) {
throw `Unknown dataset export format '${formatName}'`;
}
const url = await this._task.annotations.exportDataset(format.tag);
const tempElem = document.createElement('a');
tempElem.href = `${url}`;
document.body.appendChild(tempElem);
tempElem.click();
tempElem.remove();
} catch (error) {
showMessage(error.message);
} finally {
button.disabled = false;
}
}
init(task) {
this._task = task;
}
render(baseURL) {
this._UI = $(`<div tid=${this._task.id} class="dashboardItem"> </div>`).append(
$(`<center class="dashboardTitleWrapper">
<label class="semiBold h1 selectable"> ${this._task.name} </label>
</center>`),
).append(
$(`<center class="dashboardTitleWrapper">
<label class="regular selectable"> ${this._task.status} </label>
</center>`),
).append(
$('<div class="dashboardTaskIntro"> </div>').css({
'background-image': `url("/api/v1/tasks/${this._task.id}/frames/0")`,
}),
);
const buttonsContainer = $('<div class="dashboardButtonsUI"> </div>').appendTo(this._UI);
const downloadButton = $('<select class="regular dashboardButtonUI"'
+ 'style="text-align-last: center;"> Dump Annotation </select>');
$('<option selected disabled> Dump Annotation </option>').appendTo(downloadButton);
const uploadButton = $('<select class="regular dashboardButtonUI"'
+ 'style="text-align-last: center;"> Upload Annotation </select>');
$('<option selected disabled> Upload Annotation </option>').appendTo(uploadButton);
const dumpers = {};
const loaders = {};
for (const format of this._annotationFormats) {
for (const dumper of format.dumpers) {
dumpers[dumper.name] = dumper;
const item = $(`<option>${dumper.name}</li>`);
if (isDefaultFormat(dumper.name, this._task.mode)) {
item.addClass('bold');
}
item.appendTo(downloadButton);
}
for (const loader of format.loaders) {
loaders[loader.name] = loader;
$(`<option>${loader.name}</li>`).appendTo(uploadButton);
}
}
downloadButton.on('change', (e) => {
this._dump(e.target, dumpers[e.target.value]);
downloadButton.prop('value', 'Dump Annotation');
});
uploadButton.on('change', (e) => {
this._upload(e.target, loaders[e.target.value]);
uploadButton.prop('value', 'Upload Annotation');
});
downloadButton.appendTo(buttonsContainer);
uploadButton.appendTo(buttonsContainer);
const exportButton = $('<select class="regular dashboardButtonUI"'
+ 'style="text-align-last: center;"> Export as Dataset </select>');
$('<option selected disabled> Export as Dataset </option>').appendTo(exportButton);
for (const format of this._exportFormats) {
const item = $(`<option>${format.name}</li>`);
if (format.is_default) {
item.addClass('bold');
}
item.appendTo(exportButton);
}
exportButton.on('change', (e) => {
this._exportDataset(e.target, e.target.value);
exportButton.prop('value', 'Export as Dataset');
});
exportButton.appendTo(buttonsContainer)
$('<button class="regular dashboardButtonUI"> Update Task </button>').on('click', () => {
this._update();
}).appendTo(buttonsContainer);
$('<button class="regular dashboardButtonUI"> Delete Task </button>').on('click', () => {
userConfirm('The task will be removed. Are you sure?', () => this._remove());
}).appendTo(buttonsContainer);
if (this._task.bugTracker) {
$('<button class="regular dashboardButtonUI"> Open Bug Tracker </button>').on('click', () => {
window.open.call(window, this._task.bugTracker);
}).appendTo(buttonsContainer);
}
const jobsContainer = $('<table class="dashboardJobList regular">');
for (const job of this._task.jobs) {
const link = `${baseURL}?id=${job.id}`;
jobsContainer.append($(`<tr> <td> <a href="${link}"> ${link} </a> </td> </tr>`));
}
this._UI.append($(`
<div class="dashboardJobsUI">
<center class="dashboardTitleWrapper">
<label class="regular h1"> Jobs </label>
</center>
</div>
`).append(jobsContainer));
if (this._removed) {
this._disable();
}
return this._UI;
}
}
class DashboardView {
constructor(metaData, taskData, annotationFormats, exportFormats) {
this._dashboardList = taskData.results;
this._maxUploadSize = metaData.max_upload_size;
this._maxUploadCount = metaData.max_upload_count;
this._baseURL = metaData.base_url;
this._sharePath = metaData.share_path;
this._params = {};
this._annotationFormats = annotationFormats;
this._exportFormats = exportFormats;
this._setupList();
this._setupTaskSearch();
this._setupCreateDialog();
}
_setupList() {
const dashboardList = $('#dashboardList');
const dashboardPagination = $('#dashboardPagination');
const baseURL = this._baseURL;
const defaults = {
totalPages: 1,
visiblePages: 7,
onPageClick: async (_, page) => {
dashboardPagination.css({
visibility: 'hidden',
});
const overlay = showOverlay('Loading..');
dashboardList.empty();
let tasks = null;
try {
const id = (new URLSearchParams(window.location.search)).get('id');
const filters = Object.assign({}, {
page,
}, this._params);
if (id !== null) {
filters.id = +id;
}
tasks = await window.cvat.tasks.get(filters);
} catch (exception) {
let { message } = exception;
if (exception instanceof window.cvat.exceptions.ServerError) {
message += ` Code: ${exception.code}`;
}
showMessage(message);
return;
} finally {
overlay.remove();
}
let startPage = dashboardPagination.twbsPagination('getCurrentPage');
if (!Number.isInteger(startPage)) {
startPage = 1;
}
dashboardPagination.twbsPagination('destroy');
dashboardPagination.twbsPagination(Object.assign({}, defaults, {
totalPages: Math.max(1, Math.ceil(tasks.count / 10)),
startPage,
initiateStartPageClick: false,
}));
for (const task of tasks) {
const taskView = new TaskView(task,
this._annotationFormats, this._exportFormats);
dashboardList.append(taskView.render(baseURL));
}
dashboardPagination.css({
'margin-left': (window.screen.width - dashboardPagination.width()) / 2,
visibility: 'visible',
});
window.dispatchEvent(new CustomEvent('dashboardReady', {
detail: tasks,
}));
},
};
dashboardPagination.twbsPagination(defaults);
}
_setupTaskSearch() {
const dashboardPagination = $('#dashboardPagination');
const searchInput = $('#dashboardSearchInput');
const searchSubmit = $('#dashboardSearchSubmit');
searchInput.on('keypress', (e) => {
if (e.keyCode !== 13) {
return;
}
this._params = {};
const search = e.target.value.replace(/\s+/g, ' ').replace(/\s*:+\s*/g, ':').trim();
for (const field of ['name', 'mode', 'owner', 'assignee', 'status', 'id']) {
for (let param of search.split(/[\s]+and[\s]+|[\s]+AND[\s]+/)) {
if (param.includes(':')) {
param = param.split(':');
if (param[0] === field && param[1]) {
[, this._params[field]] = param;
}
}
}
}
if ('id' in this._params) {
this._params.id = +this._params.id;
}
if (!Object.keys(this._params).length) {
this._params.search = search;
}
window.history.replaceState(null, null,
`${window.location.origin}${window.location.pathname}`);
dashboardPagination.twbsPagination('show', 1);
});
searchSubmit.on('click', () => {
const e = $.Event('keypress');
e.keyCode = 13;
searchInput.trigger(e);
});
}
_setupCreateDialog() {
const dashboardCreateTaskButton = $('#dashboardCreateTaskButton');
const createModal = $('#dashboardCreateModal');
const nameInput = $('#dashboardNameInput');
const labelsInput = $('#dashboardLabelsInput');
const bugTrackerInput = $('#dashboardBugTrackerInput');
const localSourceRadio = $('#dashboardLocalSource');
const remoteSourceRadio = $('#dashboardRemoteSource');
const shareSourceRadio = $('#dashboardShareSource');
const selectFiles = $('#dashboardSelectFiles');
const filesLabel = $('#dashboardFilesLabel');
const remoteFileInput = $('#dashboardRemoteFileInput');
const localFileSelector = $('#dashboardLocalFileSelector');
const shareFileSelector = $('#dashboardShareBrowseModal');
const shareBrowseTree = $('#dashboardShareBrowser');
const cancelBrowseServer = $('#dashboardCancelBrowseServer');
const submitBrowseServer = $('#dashboardSubmitBrowseServer');
const zOrderBox = $('#dashboardZOrder');
const segmentSizeInput = $('#dashboardSegmentSize');
const customSegmentSize = $('#dashboardCustomSegment');
const overlapSizeInput = $('#dashboardOverlap');
const customOverlapSize = $('#dashboardCustomOverlap');
const imageQualityInput = $('#dashboardImageQuality');
const customCompressQuality = $('#dashboardCustomQuality');
const startFrameInput = $('#dashboardStartFrame');
const customStartFrame = $('#dashboardCustomStart');
const stopFrameInput = $('#dashboardStopFrame');
const customStopFrame = $('#dashboardCustomStop');
const frameFilterInput = $('#dashboardFrameFilter');
const customFrameFilter = $('#dashboardCustomFilter');
const taskMessage = $('#dashboardCreateTaskMessage');
const submitCreate = $('#dashboardSubmitTask');
const cancelCreate = $('#dashboardCancelTask');
let name = nameInput.prop('value');
let labels = labelsInput.prop('value');
let bugTrackerLink = bugTrackerInput.prop('value').trim();
let source = 'local';
let zOrder = false;
let segmentSize = 5000;
let overlapSize = 0;
let compressQuality = 50;
let startFrame = 0;
let stopFrame = 0;
let frameFilter = '';
let files = [];
function updateSelectedFiles() {
switch (files.length) {
case 0:
filesLabel.text('No Files');
break;
case 1:
filesLabel.text(typeof (files[0]) === 'string' ? files[0] : files[0].name);
break;
default:
filesLabel.text(`${files.length} files`);
}
}
function validateName() {
const math = name.match('[a-zA-Z0-9_]+');
return math !== null;
}
function validateLabels() {
try {
const result = LabelsInfo.deserialize(labels);
return result.length;
} catch (error) {
return false;
}
}
function validateBugTracker() {
return !bugTrackerLink || !!bugTrackerLink.match(/^http[s]?/);
}
function validateSegmentSize() {
return (segmentSize >= 100 && segmentSize <= 50000);
}
function validateOverlapSize() {
return (overlapSize >= 0 && overlapSize <= segmentSize - 1);
}
function validateStopFrame(stop, start) {
return !customStopFrame.prop('checked') || stop >= start;
}
dashboardCreateTaskButton.on('click', () => {
$('#dashboardCreateModal').removeClass('hidden');
});
nameInput.on('change', (e) => {
name = e.target.value;
});
bugTrackerInput.on('change', (e) => {
bugTrackerLink = e.target.value.trim();
});
labelsInput.on('change', (e) => {
labels = e.target.value;
});
localSourceRadio.on('click', () => {
if (source === 'local') {
return;
}
if (source === 'remote') {
selectFiles.parent().removeClass('hidden');
remoteFileInput.parent().addClass('hidden');
}
source = 'local';
files = [];
updateSelectedFiles();
});
remoteSourceRadio.on('click', () => {
if (source === 'remote') {
return;
}
source = 'remote';
selectFiles.parent().addClass('hidden');
remoteFileInput.parent().removeClass('hidden');
remoteFileInput.prop('value', '');
files = [];
});
shareSourceRadio.on('click', () => {
if (source === 'share') {
return;
}
if (source === 'remote') {
selectFiles.parent().removeClass('hidden');
remoteFileInput.parent().addClass('hidden');
}
source = 'share';
files = [];
updateSelectedFiles();
});
selectFiles.on('click', () => {
if (source === 'local') {
localFileSelector.click();
} else {
shareBrowseTree.jstree('refresh');
shareFileSelector.removeClass('hidden');
shareBrowseTree.jstree({
core: {
async data(obj, callback) {
const directory = obj.id === '#' ? '' : `${obj.id}/`;
let shareFiles = await window.cvat.server.share(directory);
shareFiles = Array.from(shareFiles, (element) => {
const shareFileInfo = {
id: `${directory}${element.name}`,
children: element.type === 'DIR',
text: element.name,
icon: element.type === 'DIR' ? 'jstree-folder' : 'jstree-file',
};
return shareFileInfo;
});
callback.call(this, shareFiles);
},
},
plugins: ['checkbox', 'sort'],
});
}
});
localFileSelector.on('change', (e) => {
const localFiles = e.target.files;
files = localFiles;
updateSelectedFiles();
});
remoteFileInput.on('change', () => {
const text = remoteFileInput.prop('value');
files = text.split('\n').map(f => f.trim()).filter(f => f.length > 0);
});
cancelBrowseServer.on('click', () => shareFileSelector.addClass('hidden'));
submitBrowseServer.on('click', () => {
if (!createModal.hasClass('hidden')) {
files = Array.from(shareBrowseTree.jstree(true).get_selected());
cancelBrowseServer.click();
updateSelectedFiles();
}
});
zOrderBox.on('click', (e) => {
zOrder = e.target.checked;
});
customSegmentSize.on('change', e => segmentSizeInput.prop('disabled', !e.target.checked));
customOverlapSize.on('change', e => overlapSizeInput.prop('disabled', !e.target.checked));
customCompressQuality.on('change', e => imageQualityInput.prop('disabled', !e.target.checked));
customStartFrame.on('change', e => startFrameInput.prop('disabled', !e.target.checked));
customStopFrame.on('change', e => stopFrameInput.prop('disabled', !e.target.checked));
customFrameFilter.on('change', e => frameFilterInput.prop('disabled', !e.target.checked));
segmentSizeInput.on('change', () => {
const value = Math.clamp(
+segmentSizeInput.prop('value'),
+segmentSizeInput.prop('min'),
+segmentSizeInput.prop('max'),
);
segmentSizeInput.prop('value', value);
segmentSize = value;
});
overlapSizeInput.on('change', () => {
const value = Math.clamp(
+overlapSizeInput.prop('value'),
+overlapSizeInput.prop('min'),
+overlapSizeInput.prop('max'),
);
overlapSizeInput.prop('value', value);
overlapSize = value;
});
imageQualityInput.on('change', () => {
const value = Math.clamp(
+imageQualityInput.prop('value'),
+imageQualityInput.prop('min'),
+imageQualityInput.prop('max'),
);
imageQualityInput.prop('value', value);
compressQuality = value;
});
startFrameInput.on('change', () => {
const value = Math.max(
+startFrameInput.prop('value'),
+startFrameInput.prop('min')
);
startFrameInput.prop('value', value);
startFrame = value;
});
stopFrameInput.on('change', () => {
const value = Math.max(
+stopFrameInput.prop('value'),
+stopFrameInput.prop('min')
);
stopFrameInput.prop('value', value);
stopFrame = value;
});
frameFilterInput.on('change', () => {
frameFilter = frameFilterInput.prop('value');
});
submitCreate.on('click', async () => {
if (!validateName(name)) {
taskMessage.css('color', 'red');
taskMessage.text('Bad task name');
return;
}
if (!validateLabels()) {
taskMessage.css('color', 'red');
taskMessage.text('Bad labels specification');
return;
}
if (!validateSegmentSize()) {
taskMessage.css('color', 'red');
taskMessage.text('Segment size out of range');
return;
}
if (!validateBugTracker()) {
taskMessage.css('color', 'red');
taskMessage.text('Bad bug tracker link');
return;
}
if (!validateOverlapSize()) {
taskMessage.css('color', 'red');
taskMessage.text('Overlap size must be positive and not more then segment size');
return;
}
if (!validateStopFrame(stopFrame, startFrame)) {
taskMessage.css('color', 'red');
taskMessage.text('Stop frame must be greater than or equal to start frame');
return;
}
if (files.length <= 0) {
taskMessage.css('color', 'red');
taskMessage.text('No files specified for the task');
return;
}
if (files.length > window.maxUploadCount && source === 'local') {
taskMessage.css('color', 'red');
taskMessage.text('Too many files were specified. Please use share to upload');
return;
}
if (source === 'local') {
let commonSize = 0;
for (const file of files) {
commonSize += file.size;
}
if (commonSize > window.maxUploadSize) {
taskMessage.css('color', 'red');
taskMessage.text('Too big files size. Please use share to upload');
return;
}
}
const description = {
name,
labels: LabelsInfo.deserialize(labels),
image_quality: compressQuality,
z_order: zOrder,
bug_tracker: bugTrackerLink,
};
if (customSegmentSize.prop('checked')) {
description.segment_size = segmentSize;
}
if (customOverlapSize.prop('checked')) {
description.overlap = overlapSize;
}
if (customStartFrame.prop('checked')) {
description.start_frame = startFrame;
}
if (customStopFrame.prop('checked')) {
description.stop_frame = stopFrame;
}
if (customFrameFilter.prop('checked')) {
description.frame_filter = frameFilter;
}
if (customStartFrame.prop('checked')) {
description.start_frame = startFrame;
}
if (customStopFrame.prop('checked')) {
description.stop_frame = stopFrame;
}
if (customFrameFilter.prop('checked')) {
description.frame_filter = frameFilter;
}
try {
let task = new window.cvat.classes.Task(description);
if (source === 'local') {
task.clientFiles = Array.from(files);
} else if (source === 'share') {
task.serverFiles = Array.from(files);
} else if (source === 'remote') {
task.remoteFiles = Array.from(files);
}
submitCreate.attr('disabled', true);
cancelCreate.attr('disabled', true);
task = await task.save((message) => {
taskMessage.css('color', 'green');
taskMessage.text(message);
});
window.location.reload();
} catch (exception) {
let { message } = exception;
if (exception instanceof window.cvat.exceptions.ServerError) {
message += ` Code: ${exception.code}`;
}
taskMessage.css('color', 'red');
taskMessage.text(message);
submitCreate.attr('disabled', false);
cancelCreate.attr('disabled', false);
}
});
cancelCreate.on('click', () => createModal.addClass('hidden'));
}
}
// DASHBOARD ENTRYPOINT
window.addEventListener('DOMContentLoaded', () => {
window.cvat.config.backendAPI = `${window.location.origin}/api/v1`;
$.when(
// TODO: Use REST API in order to get meta
$.get('/dashboard/meta'),
$.get(`/api/v1/tasks${window.location.search}`),
window.cvat.server.formats(),
window.cvat.server.datasetFormats(),
).then((metaData, taskData, annotationFormats, exportFormats) => {
try {
new DashboardView(metaData[0], taskData[0],
annotationFormats, exportFormats);
} catch (exception) {
$('#content').empty();
const message = `Can not build CVAT dashboard. Exception: ${exception}.`;
showMessage(message);
}
}).fail((errorData) => {
$('#content').empty();
const message = `Can not build CVAT dashboard. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
}).always(() => {
$('#loadingOverlay').remove();
});
});

@ -1,12 +0,0 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
window.addEventListener('DOMContentLoaded', () => {
$('<button class="menuButton semiBold h2"> Open Task </button>').on('click', () => {
const win = window.open(`${window.location.origin}/dashboard/?id=${window.cvat.job.task_id}`, '_blank');
win.focus();
}).prependTo('#engineMenuButtons');
});

@ -1,122 +0,0 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
.dashboardItem {
margin: 5px auto;
width: 1200px;
height: 335px;
background-color: #e7edf5;
border: 1px solid;
border-radius: 5px;
}
.dashboardTaskIntro {
width: 30%;
height: 75%;
float: left;
margin-left: 20px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.dashboardButtonsUI {
margin-top: 1%;
width: 35%;
height: 75%;
float: left;
overflow-y: auto;
}
.dashboardButtonUI {
display: block;
width: 60%;
height: 1.6em;
margin: auto;
margin-top: 0.3em;
font-size: 1em;
}
.dashboardJobsUI {
width: 30%;
height: 75%;
float: left;
text-align: center;
overflow-y: auto;
}
.dashboardJobList {
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
width: 100%;
margin-top: 15px;
}
.dashboardTitleWrapper {
overflow: hidden;
}
#dashboardSearchInput {
padding: 10px;
border: 1px solid grey;
float: left;
width: 76%;
background: #f1f1f1;
outline: 0;
}
#dashboardSearchSubmit {
float: left;
width: 20%;
padding: 10px;
background: #0b7dda;
border: 1px solid grey;
border-left: none;
cursor: pointer;
outline: 0;
}
#dashboardSearchSubmit:hover {
background: #4c92ee;
}
#dashboardSearchSubmit:active {
background: #0b7fff;
}
#dashboardCreateContent {
width: 500px;
display: table;
text-align: center;
}
#dashboardShareBrowser {
width: 100%;
height: 80%;
border: 1px solid black;
border-radius: 5px;
margin-bottom: 10px;
overflow: auto;
}
#dashboardUpdateContent {
width: 450px;
height: 120px;
}
#dashboardOldLabels {
width: 97%;
margin-bottom: 10px;
}
#dashboardNewLabels {
width: 97%;
margin-bottom: 10px;
}

@ -1,223 +0,0 @@
<!--
Copyright (C) 2018 Intel Corporation
SPDX-License-Identifier: MIT
-->
{% extends 'engine/base.html' %}
{% load static %}
{% load pagination_tags %}
{% block head_title %}
CVAT Dashboard
{% endblock %}
{% block head_css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'dashboard/stylesheet.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dashboard/js/3rdparty/jstree/themes/default/style.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dashboard/js/3rdparty/pagination/bootstrap.css' %}">
{% for css_file in css_3rdparty %}
<link rel="stylesheet" type="text/css" href="{% static css_file %}">
{% endfor %}
{% endblock %}
{% block head_js_3rdparty %}
{{ block.super }}
<script type="text/javascript" src="{% static 'dashboard/js/3rdparty/jstree/jstree.min.js' %}"></script>
<script type="text/javascript" src="{% static 'dashboard/js/3rdparty/pagination/jquery.twbsPagination.js' %}"></script>
{% for js_file in js_3rdparty %}
<script type="text/javascript" src="{% static js_file %}" defer></script>
{% endfor %}
{% endblock %}
{% block head_js_cvat %}
{{ block.super }}
<script type="text/javascript" src="{% static 'engine/js/cvat-core.min.js' %}"></script>
<script type="text/javascript" src="{% static 'dashboard/js/dashboard.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/listener.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/labelsInfo.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/shapes.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/annotationParser.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/server.js' %}"></script>
{% endblock %}
{% block content %}
<div id="content">
<div style="width: 100%; display: flex;">
<div style="width: 50%; display: flex;"> </div>
<div style="width: 100%; display: flex;">
<div id="dashboardManageButtons" style="display: flex;">
<button id="dashboardCreateTaskButton" class="regular h1" style="padding: 7px;"> Create New Task </button>
</div>
<div style="width: 300px; display: flex; margin-left: 4px;">
<input type="text" id="dashboardSearchInput" class="regular h3" placeholder="Search.." name="search">
<button id="dashboardSearchSubmit" class="regular h3"> &#x1F50D; </button>
</div>
</div>
<div style="width: 50%; display: flex;"> </div>
</div>
<div id="dashboardList" style="float: left; width: 100%"> </div>
<ul id="dashboardPagination" class="pagination-sm"></ul>
</div>
<div id="dashboardCreateModal" class="modal hidden">
<form id="dashboardCreateContent" class="modal-content" autocomplete="on" onsubmit="return false">
<center>
<label class="semiBold h1"> Task Configuration </label>
</center>
<table style="width: 100%; text-align: left;">
<tr>
<td style="width: 25%"> <label class="regular h2"> Name: </label> </td>
<td> <input type="text" id="dashboardNameInput" class="regular" style="width: 90%"/> </td>
</tr>
<tr>
<td> <label class="regular h2"> Labels: </label> </td>
<td> <input type="text" id="dashboardLabelsInput" class="regular" style="width: 90%" title='
Example:
car @select=color:blue,red,black ~checkbox=parked:true
@text=plate:"" @select=model:volvo,mazda,bmw
~radio=quality:good,bad
Specification:
<prefix>checkbox=id:true/false
<prefix>radio=id:name1,name2,...
<prefix>number=id:min,max,step
<prefix>text=id:"default value"
<prefix>select=id:value1,value2,...
<prefix> can be @ for unique properties and ~ for properties
which can change its value during lifetime of the object.
Default value for all elements is the first value after ":".
For "select" and "radio" types the special value is available: "__undefined__".
Specify the value FIRST if the attribute should be annotated explicitly.
Example: @select=race:__undefined__,skip,asian,black,caucasian,other'/>
</td>
</tr>
<tr>
<td> <label class="regular h2"> Bug Tracker: </label> </td>
<td> <input type="text" id="dashboardBugTrackerInput" class="regular" style="width: 90%", placeholder="Please specify full URL"/> </td>
</tr>
<tr>
<td> <label class="regular h2"> Source: </label> </td>
<td>
<input id="dashboardLocalSource" type="radio" name="sourceType" value="local" checked=true/> <label for="dashboardLocalSource" class="regular h2" for="localSource"> Local </label>
<br> <input id="dashboardRemoteSource" type="radio" name="sourceType" value="remote"/> <label for="dashboardRemoteSource" class="regular h2"> Remote </label>
<br> <input id="dashboardShareSource" type="radio" name="sourceType" value="share"/> <label for="dashboardShareSource" class="regular h2" for="shareSource"> Share </label>
</td>
</tr>
<tr>
<td>
<label class="regular h2"> Z-Order </label>
</td>
<td>
<input type="checkbox" id="dashboardZOrder"/>
</td>
</tr>
<tr>
<td>
<label class="regular h2"> Overlap Size </label>
</td>
<td>
<input type="number" id="dashboardOverlap" class="regular" max="50000" min="0" value="0" disabled=true/>
<input type="checkbox" id="dashboardCustomOverlap" title="Custom overlap size"/>
</td>
</tr>
<tr>
<td>
<label class="regular h2"> Segment Size </label>
</td>
<td>
<input type="number" id="dashboardSegmentSize" class="regular" max="50000" min="100" value="5000" disabled=true/>
<input type="checkbox" id="dashboardCustomSegment" title="Custom segment size"/>
</td>
</tr>
<tr>
<td>
<label class="regular h2"> Image Quality </label>
</td>
<td>
<input type="number" id="dashboardImageQuality" class="regular" style="width: 4.5em;" max="95" min="1" value="50" disabled=true/>
<input type="checkbox" id="dashboardCustomQuality" title="Custom image quality"/>
</td>
</tr>
<tr>
<td>
<label class="regular h2"> Start Frame </label>
</td>
<td>
<input type="number" id="dashboardStartFrame" class="regular" style="width: 4.5em;" min="0" value=0 disabled=true/>
<input type="checkbox" id="dashboardCustomStart" title="Custom start frame"/>
</td>
</tr>
<tr>
<td>
<label class="regular h2"> Stop Frame </label>
</td>
<td>
<input type="number" id="dashboardStopFrame" class="regular" style="width: 4.5em;" min="0" value=0 disabled=true/>
<input type="checkbox" id="dashboardCustomStop" title="Custom stop frame"/>
</td>
</tr>
<tr>
<td>
<label class="regular h2"> Frame Filter </label>
</td>
<td>
<input type="text" id="dashboardFrameFilter" class="regular" style="width: 4.5em;" title="Currently only support 'step=K' filter expression." disabled=true/>
<input type="checkbox" id="dashboardCustomFilter" title="Custom frame filter"/>
</td>
</tr>
</table>
<div style="text-align: left;">
<div>
<button id="dashboardSelectFiles" class="regular h2"> Select Files </button>
<label id="dashboardFilesLabel" class="regular h2" style="margin-left: 10px"> No Files </label>
<input id="dashboardLocalFileSelector" type="file" style="display: none" multiple/>
</div>
<div class="hidden">
<label for="dashboardRemoteFileInput" class="regular h2"> URLs: </label><br>
<textarea id="dashboardRemoteFileInput" rows="5" placeholder="One URL per line" style="width: 100%;"></textarea>
</div>
</div>
<div style="width: 100%; height: 14%; padding-top: 10px;">
<div style="float: right; width: 35%; height: 50px;">
<button id="dashboardCancelTask" class="regular h2"> Cancel </button>
<button id="dashboardSubmitTask" class="regular h2"> Submit </button>
</div>
<div style="float: left; height: 50px; overflow: auto; width: 100%; height: auto; word-break: break-word;">
<label id="dashboardCreateTaskMessage" class="regular h2 selectable" style="float:left;"> </label>
</div>
</div>
</form>
</div>
<div id="dashboardShareBrowseModal" class="modal hidden">
<div style="width: 600px; height: 400px;" class="modal-content noSelect">
<center> <label id="dashboardShareBasePath" class="regular h1"> </label> </center>
<div id="dashboardShareBrowser"> </div>
<center>
<button id="dashboardCancelBrowseServer" class="regular h2" style="margin: 0px 10px"> Cancel </button>
<button id="dashboardSubmitBrowseServer" class="regular h2" style="margin: 0px 10px"> Submit </button>
</center>
</div>
</div>
<template id="dashboardUpdateTemplate">
<div id="dashboardUpdateModal" class="modal">
<div id="dashboardUpdateContent" class="modal-content">
<input id="dashboardOldLabels" type="text" readonly=true class="regular h2">
<input id="dashboardNewLabels" type="text" placeholder="expand the specification here" class="regular h2">
<center>
<button id="dashboardCancelUpdate" class="regular h2"> Cancel </button>
<button id="dashboardSubmitUpdate" class="regular h2"> Update </button>
</center>
</div>
</div>
</template>
{% endblock %}

@ -1,13 +0,0 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
from django.urls import path
from . import views
urlpatterns = [
path('', views.DashboardView),
path('meta', views.DashboardMeta),
]

@ -1,30 +0,0 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
from django.shortcuts import redirect
from django.shortcuts import render
from django.conf import settings
from cvat.apps.authentication.decorators import login_required
from cvat.settings.base import JS_3RDPARTY, CSS_3RDPARTY
import os
@login_required
def DashboardView(request):
return render(request, 'dashboard/dashboard.html', {
'js_3rdparty': JS_3RDPARTY.get('dashboard', []),
'css_3rdparty': CSS_3RDPARTY.get('dashboard', []),
})
@login_required
def DashboardMeta(request):
return JsonResponse({
'max_upload_size': settings.LOCAL_LOAD_MAX_FILES_SIZE,
'max_upload_count': settings.LOCAL_LOAD_MAX_FILES_COUNT,
'base_url': "{0}://{1}/".format(request.scheme, request.get_host()),
'share_path': os.getenv('CVAT_SHARE_URL', default=r'${cvat_root}/share'),
})

@ -1,8 +1,4 @@
# Copyright (C) 2018 Intel Corporation # Copyright (C) 2018-2019 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from cvat.settings.base import JS_3RDPARTY
JS_3RDPARTY['dashboard'] = JS_3RDPARTY.get('dashboard', []) + ['documentation/js/dashboardPlugin.js']

@ -1,11 +0,0 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
window.addEventListener('DOMContentLoaded', () => {
$('<button class="regular h1" style="margin-left: 5px;"> User Guide </button>').on('click', () => {
window.open('/documentation/user_guide.html');
}).appendTo('#dashboardManageButtons');
});

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018 Intel Corporation * Copyright (C) 2018-2019 Intel Corporation
* *
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
@ -384,6 +384,13 @@ function setupMenu(job, task, shapeCollectionModel,
$('#settingsWindow').removeClass('hidden'); $('#settingsWindow').removeClass('hidden');
}); });
$('#openTaskButton').on('click', () => {
const win = window.open(
`${window.UI_URL}/tasks/${window.cvat.job.task_id}`, '_blank'
);
win.focus();
});
$('#settingsButton').attr('title', ` $('#settingsButton').attr('title', `
${shortkeys.open_settings.view_value} - ${shortkeys.open_settings.description}`); ${shortkeys.open_settings.view_value} - ${shortkeys.open_settings.description}`);

@ -1,5 +1,5 @@
<!-- <!--
Copyright (C) 2018 Intel Corporation Copyright (C) 2018-2019 Intel Corporation
SPDX-License-Identifier: MIT SPDX-License-Identifier: MIT
--> -->
@ -34,6 +34,9 @@
{% block head_js_cvat %} {% block head_js_cvat %}
{{ block.super }} {{ block.super }}
<script type="text/javascript">
window.UI_URL = "{{ ui_url }}";
</script>
<script type="text/javascript" src="{% static 'engine/js/logger.js' %}"></script> <script type="text/javascript" src="{% static 'engine/js/logger.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/server.js' %}"></script> <script type="text/javascript" src="{% static 'engine/js/server.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/listener.js' %}"></script> <script type="text/javascript" src="{% static 'engine/js/listener.js' %}"></script>
@ -338,6 +341,7 @@
<option selected disabled> Upload Annotation </option> <option selected disabled> Upload Annotation </option>
</select> </select>
<button id="openTaskButton" class="menuButton semiBold h2"> Open Task </button>
<button id="removeAnnotationButton" class="menuButton semiBold h2"> Remove Annotation </button> <button id="removeAnnotationButton" class="menuButton semiBold h2"> Remove Annotation </button>
<button id="settingsButton" class="menuButton semiBold h2"> Settings </button> <button id="settingsButton" class="menuButton semiBold h2"> Settings </button>
<button id="fullScreenButton" class="menuButton semiBold h2"> Fullscreen Player </button> <button id="fullScreenButton" class="menuButton semiBold h2"> Fullscreen Player </button>

@ -1,5 +1,5 @@
# Copyright (C) 2018 Intel Corporation # Copyright (C) 2018-2019 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
@ -34,6 +34,7 @@ router.register('plugins', views.PluginViewSet)
urlpatterns = [ urlpatterns = [
# Entry point for a client # Entry point for a client
path('', views.dispatch_request), path('', views.dispatch_request),
path('dashboard/', views.dispatch_request),
# documentation for API # documentation for API
path('api/swagger.<slug:format>$', schema_view.without_ui(cache_timeout=0), name='schema-json'), path('api/swagger.<slug:format>$', schema_view.without_ui(cache_timeout=0), name='schema-json'),

@ -1,4 +1,4 @@
# Copyright (C) 2018 Intel Corporation # Copyright (C) 2018-2019 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
@ -11,8 +11,9 @@ import shutil
from datetime import datetime from datetime import datetime
from tempfile import mkstemp from tempfile import mkstemp
from django.http import HttpResponseBadRequest from django.views.generic import RedirectView
from django.shortcuts import redirect, render from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import render
from django.conf import settings from django.conf import settings
from sendfile import sendfile from sendfile import sendfile
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
@ -32,7 +33,6 @@ from django.utils import timezone
from . import annotation, task, models from . import annotation, task, models
from cvat.settings.base import JS_3RDPARTY, CSS_3RDPARTY from cvat.settings.base import JS_3RDPARTY, CSS_3RDPARTY
from cvat.apps.authentication.decorators import login_required from cvat.apps.authentication.decorators import login_required
import logging
from .log import slogger, clogger from .log import slogger, clogger
from cvat.apps.engine.models import StatusChoice, Task, Job, Plugin from cvat.apps.engine.models import StatusChoice, Task, Job, Plugin
from cvat.apps.engine.serializers import (TaskSerializer, UserSerializer, from cvat.apps.engine.serializers import (TaskSerializer, UserSerializer,
@ -53,14 +53,22 @@ import cvat.apps.dataset_manager.task as DatumaroTask
@login_required @login_required
def dispatch_request(request): def dispatch_request(request):
"""An entry point to dispatch legacy requests""" """An entry point to dispatch legacy requests"""
if request.method == 'GET' and 'id' in request.GET: if 'dashboard' in request.path or (request.path == '/' and 'id' not in request.GET):
return RedirectView.as_view(
url=settings.UI_URL,
permanent=True,
query_string=True
)(request)
elif request.method == 'GET' and 'id' in request.GET and request.path == '/':
return render(request, 'engine/annotation.html', { return render(request, 'engine/annotation.html', {
'css_3rdparty': CSS_3RDPARTY.get('engine', []), 'css_3rdparty': CSS_3RDPARTY.get('engine', []),
'js_3rdparty': JS_3RDPARTY.get('engine', []), 'js_3rdparty': JS_3RDPARTY.get('engine', []),
'status_list': [str(i) for i in StatusChoice] 'status_list': [str(i) for i in StatusChoice],
'ui_url': settings.UI_URL
}) })
else: else:
return redirect('/dashboard/') return HttpResponseNotFound()
class ServerViewSet(viewsets.ViewSet): class ServerViewSet(viewsets.ViewSet):
serializer_class = None serializer_class = None

@ -1,7 +1,3 @@
# Copyright (C) 2018 Intel Corporation # Copyright (C) 2018-2019 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from cvat.settings.base import JS_3RDPARTY
JS_3RDPARTY['dashboard'] = JS_3RDPARTY.get('dashboard', []) + ['git/js/dashboardPlugin.js']

@ -1,290 +0,0 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
/* global
showMessage:false
*/
// GIT ENTRYPOINT
window.addEventListener('dashboardReady', () => {
const reposWindowId = 'gitReposWindow';
const closeReposWindowButtonId = 'closeGitReposButton';
const reposURLTextId = 'gitReposURLText';
const reposSyncButtonId = 'gitReposSyncButton';
const labelStatusId = 'gitReposLabelStatus';
const labelMessageId = 'gitReposLabelMessage';
const reposWindowTemplate = `
<div id="${reposWindowId}" class="modal">
<div style="width: 700px; height: auto;" class="modal-content">
<div style="width: 100%; height: 60%; overflow-y: auto;">
<table style="width: 100%;">
<tr>
<td style="width: 20%;">
<label class="regular h2"> Repository URL: </label>
</td>
<td style="width: 80%;" colspan="2">
<input class="regular h2" type="text" style="width: 92%;" id="${reposURLTextId}" readonly/>
</td>
</td>
<tr>
<td style="width: 20%;">
<label class="regular h2"> Status: </label>
</td>
<td style="width: 60%;">
<div>
<label class="regular h2" id="${labelStatusId}"> </label>
<label class="regular h2" id="${labelMessageId}" style="word-break: break-word; user-select: text;"> </label>
</div>
</td>
<td style="width: 20%;">
<button style="width: 70%;" id="${reposSyncButtonId}" class="regular h2"> Sync </button>
</td>
</tr>
</table>
</div>
<center>
<button id="${closeReposWindowButtonId}" class="regular h1" style="margin-top: 15px;"> Close </button>
</center>
</div>
</div>`;
$.get('/git/repository/meta/get').done((gitData) => {
const dashboardItems = $('.dashboardItem');
dashboardItems.each(function setupDashboardItem() {
const tid = +this.getAttribute('tid');
if (tid in gitData) {
if (['sync', 'syncing'].includes(gitData[tid])) {
this.style.background = 'floralwhite';
} else if (gitData[tid] === 'merged') {
this.style.background = 'azure';
} else {
this.style.background = 'mistyrose';
}
$('<button> Git Repository Sync </button>').addClass('regular dashboardButtonUI').on('click', () => {
$(`#${reposWindowId}`).remove();
const gitWindow = $(reposWindowTemplate).appendTo('body');
const closeReposWindowButton = $(`#${closeReposWindowButtonId}`);
const reposSyncButton = $(`#${reposSyncButtonId}`);
const gitLabelMessage = $(`#${labelMessageId}`);
const gitLabelStatus = $(`#${labelStatusId}`);
const reposURLText = $(`#${reposURLTextId}`);
function updateState() {
reposURLText.attr('placeholder', 'Waiting for server response..');
reposURLText.prop('value', '');
gitLabelMessage.css('color', '#cccc00').text('Waiting for server response..');
gitLabelStatus.css('color', '#cccc00').text('\u25cc');
reposSyncButton.attr('disabled', true);
$.get(`/git/repository/get/${tid}`).done((data) => {
reposURLText.attr('placeholder', '');
reposURLText.prop('value', data.url.value);
if (!data.status.value) {
gitLabelStatus.css('color', 'red').text('\u26a0');
gitLabelMessage.css('color', 'red').text(data.status.error);
reposSyncButton.attr('disabled', false);
return;
}
if (data.status.value === '!sync') {
gitLabelStatus.css('color', 'red').text('\u2606');
gitLabelMessage.css('color', 'red').text('Repository is not synchronized');
reposSyncButton.attr('disabled', false);
} else if (data.status.value === 'sync') {
gitLabelStatus.css('color', '#cccc00').text('\u2605');
gitLabelMessage.css('color', 'black').text('Synchronized (merge required)');
} else if (data.status.value === 'merged') {
gitLabelStatus.css('color', 'darkgreen').text('\u2605');
gitLabelMessage.css('color', 'darkgreen').text('Synchronized');
} else if (data.status.value === 'syncing') {
gitLabelMessage.css('color', '#cccc00').text('Synchronization..');
gitLabelStatus.css('color', '#cccc00').text('\u25cc');
} else {
const message = `Got unknown repository status: ${data.status.value}`;
gitLabelStatus.css('color', 'red').text('\u26a0');
gitLabelMessage.css('color', 'red').text(message);
}
}).fail((data) => {
gitWindow.remove();
const message = 'Error occured during get an repos status. '
+ `Code: ${data.status}, text: ${data.responseText || data.statusText}`;
showMessage(message);
});
}
closeReposWindowButton.on('click', () => {
gitWindow.remove();
});
reposSyncButton.on('click', () => {
function badResponse(message) {
try {
showMessage(message);
throw Error(message);
} finally {
gitWindow.remove();
}
}
gitLabelMessage.css('color', '#cccc00').text('Synchronization..');
gitLabelStatus.css('color', '#cccc00').text('\u25cc');
reposSyncButton.attr('disabled', true);
$.get(`/git/repository/push/${tid}`).done((rqData) => {
function checkCallback() {
$.get(`/git/repository/check/${rqData.rq_id}`).done((statusData) => {
if (['queued', 'started'].includes(statusData.status)) {
setTimeout(checkCallback, 1000);
} else if (statusData.status === 'finished') {
updateState();
} else if (statusData.status === 'failed') {
const message = `Can not push to remote repository. Message: ${statusData.stderr}`;
badResponse(message);
} else {
const message = `Check returned status "${statusData.status}".`;
badResponse(message);
}
}).fail((errorData) => {
const message = 'Errors occured during pushing an repos entry. '
+ `Code: ${errorData.status}, text: ${errorData.responseText || errorData.statusText}`;
badResponse(message);
});
}
setTimeout(checkCallback, 1000);
}).fail((errorData) => {
const message = 'Errors occured during pushing an repos entry. '
+ `Code: ${errorData.status}, text: ${errorData.responseText || errorData.statusText}`;
badResponse(message);
});
});
updateState();
}).appendTo($(this).find('div.dashboardButtonsUI')[0]);
}
});
}).fail((errorData) => {
const message = `Can not get repository meta information. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
});
});
window.addEventListener('DOMContentLoaded', () => {
const createURLInputTextId = 'gitCreateURLInputText';
const lfsCheckboxId = 'gitLFSCheckbox';
// Setup the "Create task" dialog
const title = 'Field for a repository URL and a relative path inside the repository. \n'
+ 'Default repository path is `annotation/<dump_file_name>.zip`. \n'
+ 'There are .zip or .xml extenstions are supported.';
const placeh = 'github.com/user/repos [annotation/<dump_file_name>.zip]';
$(`
<tr>
<td> <label class="regular h2"> Dataset Repository: </label> </td>
<td>
<input type="text" id="${createURLInputTextId}" class="regular" style="width: 90%", placeholder="${placeh}" title="${title}"/>
</td>
</tr>
<tr>
<td> <label class="regular h2" checked> Use LFS: </label> </td>
<td> <input type="checkbox" checked id="${lfsCheckboxId}" </td>
</tr>`).insertAfter($('#dashboardBugTrackerInput').parent().parent());
async function cloneRepos(_, createdTask) {
async function wait(rqId) {
return new Promise((resolve, reject) => {
async function checkCallback() {
let response = null;
try {
response = await $.get(`/git/repository/check/${rqId}`);
} catch (errorData) {
const message = `Can not sent a request to clone the repository. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
reject(new Error(message));
}
if (['queued', 'started'].includes(response.status)) {
setTimeout(checkCallback, 1000);
} else if (response.status === 'finished') {
resolve();
} else if (response.status === 'failed') {
let message = 'Repository status check failed. ';
if (response.stderr) {
message += response.stderr;
}
reject(new Error(message));
} else {
const message = `Repository status check returned the status "${response.status}"`;
reject(new Error(message));
}
}
setTimeout(checkCallback, 1000);
});
}
const taskMessage = $('#dashboardCreateTaskMessage');
const path = $(`#${createURLInputTextId}`).prop('value').replace(/\s/g, '');
const lfs = $(`#${lfsCheckboxId}`).prop('checked');
let response = null;
if (path.length) {
taskMessage.text('Git repository is being cloned..');
try {
response = await $.ajax({
url: `/git/repository/create/${createdTask.id}`,
type: 'POST',
data: JSON.stringify({
path,
lfs,
tid: createdTask.id,
}),
contentType: 'application/json',
});
} catch (errorData) {
createdTask.delete();
const message = `Can not send a request to clone the repository. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
throw new Error(message);
}
try {
await wait(response.rq_id);
} catch (exception) {
createdTask.delete();
throw exception;
}
taskMessage.text('Git repository has been cloned..');
}
}
const gitPlugin = {
name: 'Git Plugin',
description: 'Plugin allows you to attach a repository to a task',
cvat: {
classes: {
Task: {
prototype: {
save: {
leave: cloneRepos,
},
},
},
},
},
};
window.cvat.plugins.register(gitPlugin);
});

@ -1,7 +1,3 @@
# Copyright (C) 2018 Intel Corporation # Copyright (C) 2018-2019 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from cvat.settings.base import JS_3RDPARTY
JS_3RDPARTY['dashboard'] = JS_3RDPARTY.get('dashboard', []) + ['log_viewer/js/dashboardPlugin.js']

@ -1,11 +0,0 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
window.addEventListener('DOMContentLoaded', () => {
$('<button class="regular h1" style="margin-left: 5px;"> Analytics </button>').on('click', () => {
window.open('/analytics/app/kibana');
}).appendTo('#dashboardManageButtons');
});

@ -1,9 +1,4 @@
# Copyright (C) 2018 Intel Corporation # Copyright (C) 2018-2019 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from cvat.settings.base import JS_3RDPARTY
JS_3RDPARTY['dashboard'] = JS_3RDPARTY.get('dashboard', []) + ['tf_annotation/js/dashboardPlugin.js']

@ -1,112 +0,0 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
/* global
userConfirm:false
showMessage:false
*/
window.addEventListener('dashboardReady', () => {
function checkProcess(tid, button) {
function checkCallback() {
$.get(`/tensorflow/annotation/check/task/${tid}`).done((statusData) => {
if (['started', 'queued'].includes(statusData.status)) {
const progress = Math.round(statusData.progress) || '0';
button.text(`Cancel TF Annotation (${progress}%)`);
setTimeout(checkCallback, 5000);
} else {
button.text('Run TF Annotation');
button.removeClass('tfAnnotationProcess');
button.prop('disabled', false);
if (statusData.status === 'failed') {
const message = `Tensorflow annotation failed. Error: ${statusData.stderr}`;
showMessage(message);
} else if (statusData.status !== 'finished') {
const message = `Tensorflow annotation check request returned status "${statusData.status}"`;
showMessage(message);
}
}
}).fail((errorData) => {
const message = `Can not sent tensorflow annotation check request. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
});
}
setTimeout(checkCallback, 5000);
}
function runProcess(tid, button) {
$.get(`/tensorflow/annotation/create/task/${tid}`).done(() => {
showMessage('Process has started');
button.text('Cancel TF Annotation (0%)');
button.addClass('tfAnnotationProcess');
checkProcess(tid, button);
}).fail((errorData) => {
const message = `Can not run tf annotation. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
});
}
function cancelProcess(tid, button) {
$.get(`/tensorflow/annotation/cancel/task/${tid}`).done(() => {
button.prop('disabled', true);
}).fail((errorData) => {
const message = `Can not cancel tf annotation. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
});
}
function setupDashboardItem(item, metaData) {
const tid = +item.attr('tid');
const button = $('<button> Run TF Annotation </button>');
button.on('click', () => {
if (button.hasClass('tfAnnotationProcess')) {
userConfirm('The process will be canceled. Continue?', () => {
cancelProcess(tid, button);
});
} else {
userConfirm('The current annotation will be lost. Are you sure?', () => {
runProcess(tid, button);
});
}
});
button.addClass('dashboardTFAnnotationButton regular dashboardButtonUI');
button.appendTo(item.find('div.dashboardButtonsUI'));
if ((tid in metaData) && (metaData[tid].active)) {
button.text('Cancel TF Annotation');
button.addClass('tfAnnotationProcess');
checkProcess(tid, button);
}
}
const elements = $('.dashboardItem');
const tids = Array.from(elements, el => +el.getAttribute('tid'));
$.ajax({
type: 'POST',
url: '/tensorflow/annotation/meta/get',
data: JSON.stringify(tids),
contentType: 'application/json; charset=utf-8',
}).done((metaData) => {
elements.each(function setupDashboardItemWrapper() {
setupDashboardItem($(this), metaData);
});
}).fail((errorData) => {
const message = `Can not get tf annotation meta info. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
});
});

@ -1,21 +1,18 @@
# Copyright (C) 2018 Intel Corporation # Copyright (C) 2018-2019 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest, QueryDict from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
from django.core.exceptions import ObjectDoesNotExist from rest_framework.decorators import api_view
from django.shortcuts import render
from rules.contrib.views import permission_required, objectgetter from rules.contrib.views import permission_required, objectgetter
from cvat.apps.authentication.decorators import login_required from cvat.apps.authentication.decorators import login_required
from cvat.apps.engine.models import Task as TaskModel from cvat.apps.engine.models import Task as TaskModel
from cvat.apps.engine import annotation, task
from cvat.apps.engine.serializers import LabeledDataSerializer from cvat.apps.engine.serializers import LabeledDataSerializer
from cvat.apps.engine.annotation import put_task_data from cvat.apps.engine.annotation import put_task_data
import django_rq import django_rq
import fnmatch import fnmatch
import logging
import json import json
import os import os
import rq import rq
@ -223,9 +220,10 @@ def create_thread(tid, labels_mapping, user):
try: try:
slogger.task[tid].exception('exception was occured during tf annotation of the task', exc_info=True) slogger.task[tid].exception('exception was occured during tf annotation of the task', exc_info=True)
except: except:
slogger.glob.exception('exception was occured during tf annotation of the task {}'.format(tid), exc_into=True) slogger.glob.exception('exception was occured during tf annotation of the task {}'.format(tid), exc_info=True)
raise ex raise ex
@api_view(['POST'])
@login_required @login_required
def get_meta_info(request): def get_meta_info(request):
try: try:
@ -242,7 +240,7 @@ def get_meta_info(request):
return JsonResponse(result) return JsonResponse(result)
except Exception as ex: except Exception as ex:
slogger.glob.exception('exception was occured during tf meta request', exc_into=True) slogger.glob.exception('exception was occured during tf meta request', exc_info=True)
return HttpResponseBadRequest(str(ex)) return HttpResponseBadRequest(str(ex))

@ -1,4 +1,4 @@
# Copyright (C) 2018 Intel Corporation # Copyright (C) 2018-2019 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
@ -30,8 +30,9 @@ INTERNAL_IPS = ['127.0.0.1']
try: try:
sys.path.append(BASE_DIR) sys.path.append(BASE_DIR)
from keys.secret_key import SECRET_KEY from keys.secret_key import SECRET_KEY # pylint: disable=unused-import
except ImportError: except ImportError:
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
keys_dir = os.path.join(BASE_DIR, 'keys') keys_dir = os.path.join(BASE_DIR, 'keys')
if not os.path.isdir(keys_dir): if not os.path.isdir(keys_dir):
@ -91,7 +92,6 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'cvat.apps.engine', 'cvat.apps.engine',
'cvat.apps.dashboard',
'cvat.apps.authentication', 'cvat.apps.authentication',
'cvat.apps.documentation', 'cvat.apps.documentation',
'cvat.apps.git', 'cvat.apps.git',
@ -191,7 +191,10 @@ UI_HOST = os.environ.get('UI_HOST', 'localhost')
UI_PORT = os.environ.get('UI_PORT', '3000') UI_PORT = os.environ.get('UI_PORT', '3000')
CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_CREDENTIALS = True
CSRF_TRUSTED_ORIGINS = [UI_HOST] CSRF_TRUSTED_ORIGINS = [UI_HOST]
UI_URL = '{}://{}:{}'.format(UI_SCHEME, UI_HOST, UI_PORT) UI_URL = '{}://{}'.format(UI_SCHEME, UI_HOST)
if len(UI_URL):
UI_URL += ':{}'.format(UI_PORT)
CORS_ORIGIN_WHITELIST = [UI_URL] CORS_ORIGIN_WHITELIST = [UI_URL]
CORS_REPLACE_HTTPS_REFERER = True CORS_REPLACE_HTTPS_REFERER = True

@ -1,5 +1,5 @@
# Copyright (C) 2018 Intel Corporation # Copyright (C) 2018-2019 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
@ -18,17 +18,14 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from django.apps import apps from django.apps import apps
import os
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('', include('cvat.apps.engine.urls')), path('', include('cvat.apps.engine.urls')),
path('dashboard/', include('cvat.apps.dashboard.urls')),
path('django-rq/', include('django_rq.urls')), path('django-rq/', include('django_rq.urls')),
path('auth/', include('cvat.apps.authentication.urls')), path('auth/', include('cvat.apps.authentication.urls')),
path('documentation/', include('cvat.apps.documentation.urls')), path('documentation/', include('cvat.apps.documentation.urls')),

@ -54,8 +54,11 @@ services:
OPENVINO_TOOLKIT: "no" OPENVINO_TOOLKIT: "no"
environment: environment:
DJANGO_MODWSGI_EXTRA_ARGS: "" DJANGO_MODWSGI_EXTRA_ARGS: ""
UI_SCHEME: http
UI_HOST: localhost
UI_PORT: 7080 UI_PORT: 7080
volumes: volumes:
- cvat_data:/home/django/data - cvat_data:/home/django/data
- cvat_keys:/home/django/keys - cvat_keys:/home/django/keys

Loading…
Cancel
Save