IAM: Open Policy Agent integration (#3788)
Co-authored-by: Boris Sekachev <boris.sekachev@intel.com> Co-authored-by: Dmitry Kruchinin <dmitryx.kruchinin@intel.com>main
parent
5281e7938c
commit
4708b5ecf8
@ -0,0 +1,372 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
const { checkObjectType, isEnum } = require('./common');
|
||||
const config = require('./config');
|
||||
const { MembershipRole } = require('./enums');
|
||||
const { ArgumentError, ServerError } = require('./exceptions');
|
||||
const PluginRegistry = require('./plugins');
|
||||
const serverProxy = require('./server-proxy');
|
||||
const User = require('./user');
|
||||
|
||||
/**
|
||||
* Class representing an organization
|
||||
* @memberof module:API.cvat.classes
|
||||
*/
|
||||
class Organization {
|
||||
/**
|
||||
* @param {object} initialData - Object which is used for initialization
|
||||
* <br> It must contains keys:
|
||||
* <br> <li style="margin-left: 10px;"> slug
|
||||
|
||||
* <br> It can contains keys:
|
||||
* <br> <li style="margin-left: 10px;"> name
|
||||
* <br> <li style="margin-left: 10px;"> description
|
||||
* <br> <li style="margin-left: 10px;"> owner
|
||||
* <br> <li style="margin-left: 10px;"> created_date
|
||||
* <br> <li style="margin-left: 10px;"> updated_date
|
||||
* <br> <li style="margin-left: 10px;"> contact
|
||||
*/
|
||||
constructor(initialData) {
|
||||
const data = {
|
||||
id: undefined,
|
||||
slug: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
created_date: undefined,
|
||||
updated_date: undefined,
|
||||
owner: undefined,
|
||||
contact: undefined,
|
||||
};
|
||||
|
||||
for (const prop of Object.keys(data)) {
|
||||
if (prop in initialData) {
|
||||
data[prop] = initialData[prop];
|
||||
}
|
||||
}
|
||||
|
||||
if (data.owner) data.owner = new User(data.owner);
|
||||
|
||||
checkObjectType('slug', data.slug, 'string');
|
||||
if (typeof data.name !== 'undefined') {
|
||||
checkObjectType('name', data.name, 'string');
|
||||
}
|
||||
|
||||
if (typeof data.description !== 'undefined') {
|
||||
checkObjectType('description', data.description, 'string');
|
||||
}
|
||||
|
||||
if (typeof data.id !== 'undefined') {
|
||||
checkObjectType('id', data.id, 'number');
|
||||
}
|
||||
|
||||
if (typeof data.contact !== 'undefined') {
|
||||
checkObjectType('contact', data.contact, 'object');
|
||||
for (const prop in data.contact) {
|
||||
if (typeof data.contact[prop] !== 'string') {
|
||||
throw ArgumentError(`Contact fields must be strings, tried to set ${typeof data.contact[prop]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof data.owner !== 'undefined' && data.owner !== null) {
|
||||
checkObjectType('owner', data.owner, null, User);
|
||||
}
|
||||
|
||||
Object.defineProperties(this, {
|
||||
id: {
|
||||
get: () => data.id,
|
||||
},
|
||||
slug: {
|
||||
get: () => data.slug,
|
||||
},
|
||||
name: {
|
||||
get: () => data.name,
|
||||
set: (name) => {
|
||||
if (typeof name !== 'string') {
|
||||
throw ArgumentError(`Name property must be a string, tried to set ${typeof description}`);
|
||||
}
|
||||
data.name = name;
|
||||
},
|
||||
},
|
||||
description: {
|
||||
get: () => data.description,
|
||||
set: (description) => {
|
||||
if (typeof description !== 'string') {
|
||||
throw ArgumentError(
|
||||
`Description property must be a string, tried to set ${typeof description}`,
|
||||
);
|
||||
}
|
||||
data.description = description;
|
||||
},
|
||||
},
|
||||
contact: {
|
||||
get: () => ({ ...data.contact }),
|
||||
set: (contact) => {
|
||||
if (typeof contact !== 'object') {
|
||||
throw ArgumentError(`Contact property must be an object, tried to set ${typeof contact}`);
|
||||
}
|
||||
for (const prop in contact) {
|
||||
if (typeof contact[prop] !== 'string') {
|
||||
throw ArgumentError(`Contact fields must be strings, tried to set ${typeof contact[prop]}`);
|
||||
}
|
||||
}
|
||||
data.contact = { ...contact };
|
||||
},
|
||||
},
|
||||
owner: {
|
||||
get: () => data.owner,
|
||||
},
|
||||
createdDate: {
|
||||
get: () => data.created_date,
|
||||
},
|
||||
updatedDate: {
|
||||
get: () => data.updated_date,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Method updates organization data if it was created before, or creates a new organization
|
||||
* @method save
|
||||
* @returns {module:API.cvat.classes.Organization}
|
||||
* @memberof module:API.cvat.classes.Organization
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async save() {
|
||||
const result = await PluginRegistry.apiWrapper.call(this, Organization.prototype.save);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method returns paginatable list of organization members
|
||||
* @method save
|
||||
* @returns {module:API.cvat.classes.Organization}
|
||||
* @param page page number
|
||||
* @param page_size number of results per page
|
||||
* @memberof module:API.cvat.classes.Organization
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
*/
|
||||
async members(page = 1, page_size = 10) {
|
||||
const result = await PluginRegistry.apiWrapper.call(
|
||||
this,
|
||||
Organization.prototype.members,
|
||||
this.slug,
|
||||
page,
|
||||
page_size,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method removes the organization
|
||||
* @method remove
|
||||
* @returns {module:API.cvat.classes.Organization}
|
||||
* @memberof module:API.cvat.classes.Organization
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async remove() {
|
||||
const result = await PluginRegistry.apiWrapper.call(this, Organization.prototype.remove);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method invites new members by email
|
||||
* @method invite
|
||||
* @returns {module:API.cvat.classes.Organization}
|
||||
* @param {string} email
|
||||
* @param {string} role
|
||||
* @memberof module:API.cvat.classes.Organization
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async invite(email, role) {
|
||||
const result = await PluginRegistry.apiWrapper.call(this, Organization.prototype.invite, email, role);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method allows a user to get out from an organization
|
||||
* The difference between deleteMembership is that membershipId is unknown in this case
|
||||
* @method leave
|
||||
* @returns {module:API.cvat.classes.Organization}
|
||||
* @memberof module:API.cvat.classes.Organization
|
||||
* @param {module:API.cvat.classes.User} user
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async leave(user) {
|
||||
const result = await PluginRegistry.apiWrapper.call(this, Organization.prototype.leave, user);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method allows to change a membership role
|
||||
* @method updateMembership
|
||||
* @returns {module:API.cvat.classes.Organization}
|
||||
* @param {number} membershipId
|
||||
* @param {string} role
|
||||
* @memberof module:API.cvat.classes.Organization
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async updateMembership(membershipId, role) {
|
||||
const result = await PluginRegistry.apiWrapper.call(
|
||||
this,
|
||||
Organization.prototype.updateMembership,
|
||||
membershipId,
|
||||
role,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method allows to kick a user from an organization
|
||||
* @method deleteMembership
|
||||
* @returns {module:API.cvat.classes.Organization}
|
||||
* @param {number} membershipId
|
||||
* @memberof module:API.cvat.classes.Organization
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async deleteMembership(membershipId) {
|
||||
const result = await PluginRegistry.apiWrapper.call(
|
||||
this,
|
||||
Organization.prototype.deleteMembership,
|
||||
membershipId,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Organization.prototype.save.implementation = async function () {
|
||||
if (typeof this.id === 'number') {
|
||||
const organizationData = {
|
||||
name: this.name || this.slug,
|
||||
description: this.description,
|
||||
contact: this.contact,
|
||||
};
|
||||
|
||||
const result = await serverProxy.organizations.update(this.id, organizationData);
|
||||
return new Organization(result);
|
||||
}
|
||||
|
||||
const organizationData = {
|
||||
slug: this.slug,
|
||||
name: this.name || this.slug,
|
||||
description: this.description,
|
||||
contact: this.contact,
|
||||
};
|
||||
|
||||
const result = await serverProxy.organizations.create(organizationData);
|
||||
return new Organization(result);
|
||||
};
|
||||
|
||||
Organization.prototype.members.implementation = async function (orgSlug, page, pageSize) {
|
||||
checkObjectType('orgSlug', orgSlug, 'string');
|
||||
checkObjectType('page', page, 'number');
|
||||
checkObjectType('pageSize', pageSize, 'number');
|
||||
|
||||
const result = await serverProxy.organizations.members(orgSlug, page, pageSize);
|
||||
await Promise.all(
|
||||
result.results.map((membership) => {
|
||||
const { invitation } = membership;
|
||||
membership.user = new User(membership.user);
|
||||
if (invitation) {
|
||||
return serverProxy.organizations
|
||||
.invitation(invitation)
|
||||
.then((invitationData) => {
|
||||
membership.invitation = invitationData;
|
||||
})
|
||||
.catch(() => {
|
||||
membership.invitation = null;
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}),
|
||||
);
|
||||
|
||||
result.results.count = result.count;
|
||||
return result.results;
|
||||
};
|
||||
|
||||
Organization.prototype.remove.implementation = async function () {
|
||||
if (typeof this.id === 'number') {
|
||||
await serverProxy.organizations.delete(this.id);
|
||||
config.organizationID = null;
|
||||
}
|
||||
};
|
||||
|
||||
Organization.prototype.invite.implementation = async function (email, role) {
|
||||
checkObjectType('email', email, 'string');
|
||||
if (!isEnum.bind(MembershipRole)(role)) {
|
||||
throw new ArgumentError(`Role must be one of: ${Object.values(MembershipRole).toString()}`);
|
||||
}
|
||||
|
||||
if (typeof this.id === 'number') {
|
||||
await serverProxy.organizations.invite(this.id, { email, role });
|
||||
}
|
||||
};
|
||||
|
||||
Organization.prototype.updateMembership.implementation = async function (membershipId, role) {
|
||||
checkObjectType('membershipId', membershipId, 'number');
|
||||
if (!isEnum.bind(MembershipRole)(role)) {
|
||||
throw new ArgumentError(`Role must be one of: ${Object.values(MembershipRole).toString()}`);
|
||||
}
|
||||
|
||||
if (typeof this.id === 'number') {
|
||||
await serverProxy.organizations.updateMembership(membershipId, { role });
|
||||
}
|
||||
};
|
||||
|
||||
Organization.prototype.deleteMembership.implementation = async function (membershipId) {
|
||||
checkObjectType('membershipId', membershipId, 'number');
|
||||
if (typeof this.id === 'number') {
|
||||
await serverProxy.organizations.deleteMembership(membershipId);
|
||||
}
|
||||
};
|
||||
|
||||
Organization.prototype.leave.implementation = async function (user) {
|
||||
checkObjectType('user', user, null, User);
|
||||
if (typeof this.id === 'number') {
|
||||
const result = await serverProxy.organizations.members(this.slug, 1, 10, { user: user.id });
|
||||
const [membership] = result.results;
|
||||
if (!membership) {
|
||||
throw new ServerError(`Could not find membership for user ${user.username} in organization ${this.slug}`);
|
||||
}
|
||||
await serverProxy.organizations.deleteMembership(membership.id);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Organization;
|
||||
@ -1,405 +0,0 @@
|
||||
// Copyright (C) 2020-2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
const store = require('store');
|
||||
|
||||
const PluginRegistry = require('./plugins');
|
||||
const Issue = require('./issue');
|
||||
const User = require('./user');
|
||||
const { ArgumentError, DataError } = require('./exceptions');
|
||||
const { ReviewStatus } = require('./enums');
|
||||
const { negativeIDGenerator } = require('./common');
|
||||
const serverProxy = require('./server-proxy');
|
||||
|
||||
/**
|
||||
* Class representing a single review
|
||||
* @memberof module:API.cvat.classes
|
||||
* @hideconstructor
|
||||
*/
|
||||
class Review {
|
||||
constructor(initialData) {
|
||||
const data = {
|
||||
id: undefined,
|
||||
job: undefined,
|
||||
issue_set: [],
|
||||
estimated_quality: undefined,
|
||||
status: undefined,
|
||||
reviewer: undefined,
|
||||
assignee: undefined,
|
||||
reviewed_frames: undefined,
|
||||
reviewed_states: undefined,
|
||||
};
|
||||
|
||||
for (const property in data) {
|
||||
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
|
||||
data[property] = initialData[property];
|
||||
}
|
||||
}
|
||||
|
||||
if (data.reviewer && !(data.reviewer instanceof User)) data.reviewer = new User(data.reviewer);
|
||||
if (data.assignee && !(data.assignee instanceof User)) data.assignee = new User(data.assignee);
|
||||
|
||||
data.reviewed_frames = Array.isArray(data.reviewed_frames) ? new Set(data.reviewed_frames) : new Set();
|
||||
data.reviewed_states = Array.isArray(data.reviewed_states) ? new Set(data.reviewed_states) : new Set();
|
||||
if (data.issue_set) {
|
||||
data.issue_set = data.issue_set.map((issue) => new Issue(issue));
|
||||
}
|
||||
|
||||
if (typeof data.id === 'undefined') {
|
||||
data.id = negativeIDGenerator();
|
||||
}
|
||||
|
||||
Object.defineProperties(
|
||||
this,
|
||||
Object.freeze({
|
||||
/**
|
||||
* @name id
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
id: {
|
||||
get: () => data.id,
|
||||
},
|
||||
/**
|
||||
* An identifier of a job the review is attached to
|
||||
* @name job
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
job: {
|
||||
get: () => data.job,
|
||||
},
|
||||
/**
|
||||
* List of attached issues
|
||||
* @name issues
|
||||
* @type {number[]}
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @instance
|
||||
* @readonly
|
||||
*/
|
||||
issues: {
|
||||
get: () => data.issue_set.filter((issue) => !issue.removed),
|
||||
},
|
||||
/**
|
||||
* Estimated quality of the review
|
||||
* @name estimatedQuality
|
||||
* @type {number}
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @instance
|
||||
* @readonly
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
*/
|
||||
estimatedQuality: {
|
||||
get: () => data.estimated_quality,
|
||||
set: (value) => {
|
||||
if (typeof value !== 'number' || value < 0 || value > 5) {
|
||||
throw new ArgumentError(`Value must be a number in range [0, 5]. Got ${value}`);
|
||||
}
|
||||
data.estimated_quality = value;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* @name status
|
||||
* @type {module:API.cvat.enums.ReviewStatus}
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @instance
|
||||
* @readonly
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
*/
|
||||
status: {
|
||||
get: () => data.status,
|
||||
set: (status) => {
|
||||
const type = ReviewStatus;
|
||||
let valueInEnum = false;
|
||||
for (const value in type) {
|
||||
if (type[value] === status) {
|
||||
valueInEnum = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!valueInEnum) {
|
||||
throw new ArgumentError(
|
||||
'Value must be a value from the enumeration cvat.enums.ReviewStatus',
|
||||
);
|
||||
}
|
||||
|
||||
data.status = status;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* An instance of a user who has done the review
|
||||
* @name reviewer
|
||||
* @type {module:API.cvat.classes.User}
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @readonly
|
||||
* @instance
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
*/
|
||||
reviewer: {
|
||||
get: () => data.reviewer,
|
||||
set: (reviewer) => {
|
||||
if (!(reviewer instanceof User)) {
|
||||
throw new ArgumentError(`Reviewer must be an instance of the User class. Got ${reviewer}`);
|
||||
}
|
||||
|
||||
data.reviewer = reviewer;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* An instance of a user who was assigned for annotation before the review
|
||||
* @name assignee
|
||||
* @type {module:API.cvat.classes.User}
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
assignee: {
|
||||
get: () => data.assignee,
|
||||
},
|
||||
/**
|
||||
* A set of frames that have been visited during review
|
||||
* @name reviewedFrames
|
||||
* @type {number[]}
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
reviewedFrames: {
|
||||
get: () => Array.from(data.reviewed_frames),
|
||||
},
|
||||
/**
|
||||
* A set of reviewed states (server IDs combined with frames)
|
||||
* @name reviewedFrames
|
||||
* @type {string[]}
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
reviewedStates: {
|
||||
get: () => Array.from(data.reviewed_states),
|
||||
},
|
||||
__internal: {
|
||||
get: () => data,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method appends a frame to a set of reviewed frames
|
||||
* Reviewed frames are saved only in local storage
|
||||
* @method reviewFrame
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @param {number} frame
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async reviewFrame(frame) {
|
||||
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.reviewFrame, frame);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method appends a frame to a set of reviewed frames
|
||||
* Reviewed states are saved only in local storage. They are used to automatic annotations quality assessment
|
||||
* @method reviewStates
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @param {string[]} stateIDs
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async reviewStates(stateIDs) {
|
||||
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.reviewStates, stateIDs);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} IssueData
|
||||
* @property {number} frame
|
||||
* @property {number[]} position
|
||||
* @property {number} owner
|
||||
* @property {CommentData[]} comment_set
|
||||
* @global
|
||||
*/
|
||||
/**
|
||||
* Method adds a new issue to the review
|
||||
* @method openIssue
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @param {IssueData} data
|
||||
* @returns {module:API.cvat.classes.Issue}
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async openIssue(data) {
|
||||
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.openIssue, data);
|
||||
return result;
|
||||
}
|
||||
|
||||
async deleteIssue(issueId) {
|
||||
await PluginRegistry.apiWrapper.call(this, Review.prototype.deleteIssue, issueId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method submits local review to the server
|
||||
* @method submit
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.DataError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async submit() {
|
||||
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.submit);
|
||||
return result;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
const { issues, reviewedFrames, reviewedStates } = this;
|
||||
const data = {
|
||||
job: this.job,
|
||||
issue_set: issues.map((issue) => issue.serialize()),
|
||||
reviewed_frames: Array.from(reviewedFrames),
|
||||
reviewed_states: Array.from(reviewedStates),
|
||||
};
|
||||
|
||||
if (this.id > 0) {
|
||||
data.id = this.id;
|
||||
}
|
||||
if (typeof this.estimatedQuality !== 'undefined') {
|
||||
data.estimated_quality = this.estimatedQuality;
|
||||
}
|
||||
if (typeof this.status !== 'undefined') {
|
||||
data.status = this.status;
|
||||
}
|
||||
if (this.reviewer) {
|
||||
data.reviewer = this.reviewer.toJSON();
|
||||
}
|
||||
if (this.assignee) {
|
||||
data.reviewer = this.assignee.toJSON();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const data = this.serialize();
|
||||
const {
|
||||
reviewer,
|
||||
assignee,
|
||||
reviewed_frames: reviewedFrames,
|
||||
reviewed_states: reviewedStates,
|
||||
...updated
|
||||
} = data;
|
||||
|
||||
return {
|
||||
...updated,
|
||||
issue_set: this.issues.map((issue) => issue.toJSON()),
|
||||
reviewer_id: reviewer ? reviewer.id : undefined,
|
||||
assignee_id: assignee ? assignee.id : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async toLocalStorage() {
|
||||
const data = this.serialize();
|
||||
store.set(`job-${this.job}-review`, JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
Review.prototype.reviewFrame.implementation = function (frame) {
|
||||
if (!Number.isInteger(frame)) {
|
||||
throw new ArgumentError(`The argument "frame" is expected to be an integer. Got ${frame}`);
|
||||
}
|
||||
this.__internal.reviewed_frames.add(frame);
|
||||
};
|
||||
|
||||
Review.prototype.reviewStates.implementation = function (stateIDs) {
|
||||
if (!Array.isArray(stateIDs) || stateIDs.some((stateID) => typeof stateID !== 'string')) {
|
||||
throw new ArgumentError(`The argument "stateIDs" is expected to be an array of string. Got ${stateIDs}`);
|
||||
}
|
||||
|
||||
stateIDs.forEach((stateID) => this.__internal.reviewed_states.add(stateID));
|
||||
};
|
||||
|
||||
Review.prototype.openIssue.implementation = async function (data) {
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
throw new ArgumentError(`The argument "data" must be a not null object. Got ${data}`);
|
||||
}
|
||||
|
||||
if (typeof data.frame !== 'number') {
|
||||
throw new ArgumentError(`Issue frame must be a number. Got ${data.frame}`);
|
||||
}
|
||||
|
||||
if (!(data.owner instanceof User)) {
|
||||
throw new ArgumentError(`Issue owner must be a User instance. Got ${data.owner}`);
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.position) || data.position.some((coord) => typeof coord !== 'number')) {
|
||||
throw new ArgumentError(`Issue position must be an array of numbers. Got ${data.position}`);
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.comment_set)) {
|
||||
throw new ArgumentError(`Issue comment set must be an array. Got ${data.comment_set}`);
|
||||
}
|
||||
|
||||
const copied = {
|
||||
frame: data.frame,
|
||||
position: Issue.hull(data.position),
|
||||
owner: data.owner,
|
||||
comment_set: [],
|
||||
};
|
||||
|
||||
const issue = new Issue(copied);
|
||||
|
||||
for (const comment of data.comment_set) {
|
||||
await issue.comment.implementation.call(issue, comment);
|
||||
}
|
||||
|
||||
this.__internal.issue_set.push(issue);
|
||||
return issue;
|
||||
};
|
||||
|
||||
Review.prototype.submit.implementation = async function () {
|
||||
if (typeof this.estimatedQuality === 'undefined') {
|
||||
throw new DataError('Estimated quality is expected to be a number. Got "undefined"');
|
||||
}
|
||||
|
||||
if (typeof this.status === 'undefined') {
|
||||
throw new DataError('Review status is expected to be a string. Got "undefined"');
|
||||
}
|
||||
|
||||
if (this.id < 0) {
|
||||
const data = this.toJSON();
|
||||
|
||||
const response = await serverProxy.jobs.reviews.create(data);
|
||||
store.remove(`job-${this.job}-review`);
|
||||
this.__internal.id = response.id;
|
||||
this.__internal.issue_set = response.issue_set.map((issue) => new Issue(issue));
|
||||
this.__internal.estimated_quality = response.estimated_quality;
|
||||
this.__internal.status = response.status;
|
||||
|
||||
if (response.reviewer) this.__internal.reviewer = new User(response.reviewer);
|
||||
if (response.assignee) this.__internal.assignee = new User(response.assignee);
|
||||
}
|
||||
};
|
||||
|
||||
Review.prototype.deleteIssue.implementation = function (issueId) {
|
||||
this.__internal.issue_set = this.__internal.issue_set.filter((issue) => issue.id !== issueId);
|
||||
};
|
||||
|
||||
module.exports = Review;
|
||||
@ -0,0 +1,266 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { Store } from 'antd/lib/form/interface';
|
||||
import { User } from 'components/task-page/user-selector';
|
||||
import getCore from 'cvat-core-wrapper';
|
||||
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
|
||||
|
||||
const core = getCore();
|
||||
|
||||
export enum OrganizationActionsTypes {
|
||||
GET_ORGANIZATIONS = 'GET_ORGANIZATIONS',
|
||||
GET_ORGANIZATIONS_SUCCESS = 'GET_ORGANIZATIONS_SUCCESS',
|
||||
GET_ORGANIZATIONS_FAILED = 'GET_ORGANIZATIONS_FAILED',
|
||||
ACTIVATE_ORGANIZATION_SUCCESS = 'ACTIVATE_ORGANIZATION_SUCCESS',
|
||||
ACTIVATE_ORGANIZATION_FAILED = 'ACTIVATE_ORGANIZATION_FAILED',
|
||||
CREATE_ORGANIZATION = 'CREATE_ORGANIZATION',
|
||||
CREATE_ORGANIZATION_SUCCESS = 'CREATE_ORGANIZATION_SUCCESS',
|
||||
CREATE_ORGANIZATION_FAILED = 'CREATE_ORGANIZATION_FAILED',
|
||||
UPDATE_ORGANIZATION = 'UPDATE_ORGANIZATION',
|
||||
UPDATE_ORGANIZATION_SUCCESS = 'UPDATE_ORGANIZATION_SUCCESS',
|
||||
UPDATE_ORGANIZATION_FAILED = 'UPDATE_ORGANIZATION_FAILED',
|
||||
REMOVE_ORGANIZATION = 'REMOVE_ORGANIZATION',
|
||||
REMOVE_ORGANIZATION_SUCCESS = 'REMOVE_ORGANIZATION_SUCCESS',
|
||||
REMOVE_ORGANIZATION_FAILED = 'REMOVE_ORGANIZATION_FAILED',
|
||||
INVITE_ORGANIZATION_MEMBERS = 'INVITE_ORGANIZATION_MEMBERS',
|
||||
INVITE_ORGANIZATION_MEMBERS_FAILED = 'INVITE_ORGANIZATION_MEMBERS_FAILED',
|
||||
INVITE_ORGANIZATION_MEMBERS_DONE = 'INVITE_ORGANIZATION_MEMBERS_DONE',
|
||||
INVITE_ORGANIZATION_MEMBER_SUCCESS = 'INVITE_ORGANIZATION_MEMBER_SUCCESS',
|
||||
INVITE_ORGANIZATION_MEMBER_FAILED = 'INVITE_ORGANIZATION_MEMBER_FAILED',
|
||||
LEAVE_ORGANIZATION = 'LEAVE_ORGANIZATION',
|
||||
LEAVE_ORGANIZATION_SUCCESS = 'LEAVE_ORGANIZATION_SUCCESS',
|
||||
LEAVE_ORGANIZATION_FAILED = 'LEAVE_ORGANIZATION_FAILED',
|
||||
REMOVE_ORGANIZATION_MEMBER = 'REMOVE_ORGANIZATION_MEMBERS',
|
||||
REMOVE_ORGANIZATION_MEMBER_SUCCESS = 'REMOVE_ORGANIZATION_MEMBER_SUCCESS',
|
||||
REMOVE_ORGANIZATION_MEMBER_FAILED = 'REMOVE_ORGANIZATION_MEMBER_FAILED',
|
||||
UPDATE_ORGANIZATION_MEMBER = 'UPDATE_ORGANIZATION_MEMBER',
|
||||
UPDATE_ORGANIZATION_MEMBER_SUCCESS = 'UPDATE_ORGANIZATION_MEMBER_SUCCESS',
|
||||
UPDATE_ORGANIZATION_MEMBER_FAILED = 'UPDATE_ORGANIZATION_MEMBER_FAILED',
|
||||
}
|
||||
|
||||
const organizationActions = {
|
||||
getOrganizations: () => createAction(OrganizationActionsTypes.GET_ORGANIZATIONS),
|
||||
getOrganizationsSuccess: (list: any[]) => createAction(
|
||||
OrganizationActionsTypes.GET_ORGANIZATIONS_SUCCESS, { list },
|
||||
),
|
||||
getOrganizationsFailed: (error: any) => createAction(OrganizationActionsTypes.GET_ORGANIZATIONS_FAILED, { error }),
|
||||
createOrganization: () => createAction(OrganizationActionsTypes.CREATE_ORGANIZATION),
|
||||
createOrganizationSuccess: (organization: any) => createAction(
|
||||
OrganizationActionsTypes.CREATE_ORGANIZATION_SUCCESS, { organization },
|
||||
),
|
||||
createOrganizationFailed: (slug: string, error: any) => createAction(
|
||||
OrganizationActionsTypes.CREATE_ORGANIZATION_FAILED, { slug, error },
|
||||
),
|
||||
updateOrganization: () => createAction(OrganizationActionsTypes.UPDATE_ORGANIZATION),
|
||||
updateOrganizationSuccess: (organization: any) => createAction(
|
||||
OrganizationActionsTypes.UPDATE_ORGANIZATION_SUCCESS, { organization },
|
||||
),
|
||||
updateOrganizationFailed: (slug: string, error: any) => createAction(
|
||||
OrganizationActionsTypes.UPDATE_ORGANIZATION_FAILED, { slug, error },
|
||||
),
|
||||
activateOrganizationSuccess: (organization: any | null) => createAction(
|
||||
OrganizationActionsTypes.ACTIVATE_ORGANIZATION_SUCCESS, { organization },
|
||||
),
|
||||
activateOrganizationFailed: (error: any, slug: string | null) => createAction(
|
||||
OrganizationActionsTypes.ACTIVATE_ORGANIZATION_FAILED, { slug, error },
|
||||
),
|
||||
removeOrganization: () => createAction(OrganizationActionsTypes.REMOVE_ORGANIZATION),
|
||||
removeOrganizationSuccess: (slug: string) => createAction(
|
||||
OrganizationActionsTypes.REMOVE_ORGANIZATION_SUCCESS, { slug },
|
||||
),
|
||||
removeOrganizationFailed: (error: any, slug: string) => createAction(
|
||||
OrganizationActionsTypes.REMOVE_ORGANIZATION_FAILED, { error, slug },
|
||||
),
|
||||
inviteOrganizationMembers: () => createAction(OrganizationActionsTypes.INVITE_ORGANIZATION_MEMBERS),
|
||||
inviteOrganizationMembersFailed: (error: any) => createAction(
|
||||
OrganizationActionsTypes.INVITE_ORGANIZATION_MEMBERS_FAILED, { error },
|
||||
),
|
||||
inviteOrganizationMembersDone: () => createAction(OrganizationActionsTypes.INVITE_ORGANIZATION_MEMBERS_DONE),
|
||||
inviteOrganizationMemberSuccess: (email: string) => createAction(
|
||||
OrganizationActionsTypes.INVITE_ORGANIZATION_MEMBER_SUCCESS, { email },
|
||||
),
|
||||
inviteOrganizationMemberFailed: (email: string, error: any) => createAction(
|
||||
OrganizationActionsTypes.INVITE_ORGANIZATION_MEMBER_FAILED, { email, error },
|
||||
),
|
||||
leaveOrganization: () => createAction(OrganizationActionsTypes.LEAVE_ORGANIZATION),
|
||||
leaveOrganizationSuccess: () => createAction(OrganizationActionsTypes.LEAVE_ORGANIZATION_SUCCESS),
|
||||
leaveOrganizationFailed: (error: any) => createAction(
|
||||
OrganizationActionsTypes.LEAVE_ORGANIZATION_FAILED, { error },
|
||||
),
|
||||
removeOrganizationMember: () => createAction(OrganizationActionsTypes.REMOVE_ORGANIZATION_MEMBER),
|
||||
removeOrganizationMemberSuccess: () => createAction(OrganizationActionsTypes.REMOVE_ORGANIZATION_MEMBER_SUCCESS),
|
||||
removeOrganizationMemberFailed: (username: string, error: any) => createAction(
|
||||
OrganizationActionsTypes.REMOVE_ORGANIZATION_MEMBER_FAILED, { username, error },
|
||||
),
|
||||
updateOrganizationMember: () => createAction(OrganizationActionsTypes.UPDATE_ORGANIZATION_MEMBER),
|
||||
updateOrganizationMemberSuccess: () => createAction(OrganizationActionsTypes.UPDATE_ORGANIZATION_MEMBER_SUCCESS),
|
||||
updateOrganizationMemberFailed: (username: string, role: string, error: any) => createAction(
|
||||
OrganizationActionsTypes.UPDATE_ORGANIZATION_MEMBER_FAILED, { username, role, error },
|
||||
),
|
||||
};
|
||||
|
||||
export function getOrganizationsAsync(): ThunkAction {
|
||||
return async function (dispatch) {
|
||||
dispatch(organizationActions.getOrganizations());
|
||||
|
||||
try {
|
||||
const organizations = await core.organizations.get();
|
||||
let currentOrganization = null;
|
||||
|
||||
try {
|
||||
// this action is dispatched after user is authentificated
|
||||
// need to configure organization at cvat-core immediately to get relevant data
|
||||
const curSlug = localStorage.getItem('currentOrganization');
|
||||
if (curSlug) {
|
||||
currentOrganization =
|
||||
organizations.find((organization: any) => organization.slug === curSlug) || null;
|
||||
if (currentOrganization) {
|
||||
await core.organizations.activate(currentOrganization);
|
||||
} else {
|
||||
// not valid anymore (for example when organization
|
||||
// does not exist anymore, or the user has been kicked from it)
|
||||
localStorage.removeItem('currentOrganization');
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(organizationActions.activateOrganizationSuccess(currentOrganization));
|
||||
} catch (error) {
|
||||
dispatch(
|
||||
organizationActions.activateOrganizationFailed(error, localStorage.getItem('currentOrganization')),
|
||||
);
|
||||
} finally {
|
||||
dispatch(organizationActions.getOrganizationsSuccess(organizations));
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch(organizationActions.getOrganizationsFailed(error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createOrganizationAsync(
|
||||
organizationData: Store,
|
||||
onCreateSuccess?: (createdSlug: string) => void,
|
||||
): ThunkAction {
|
||||
return async function (dispatch) {
|
||||
const { slug } = organizationData;
|
||||
const organization = new core.classes.Organization(organizationData);
|
||||
dispatch(organizationActions.createOrganization());
|
||||
|
||||
try {
|
||||
const createdOrganization = await organization.save();
|
||||
dispatch(organizationActions.createOrganizationSuccess(createdOrganization));
|
||||
if (onCreateSuccess) onCreateSuccess(createdOrganization.slug);
|
||||
} catch (error) {
|
||||
dispatch(organizationActions.createOrganizationFailed(slug, error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function updateOrganizationAsync(organization: any): ThunkAction {
|
||||
return async function (dispatch) {
|
||||
dispatch(organizationActions.updateOrganization());
|
||||
|
||||
try {
|
||||
const updatedOrganization = await organization.save();
|
||||
dispatch(organizationActions.updateOrganizationSuccess(updatedOrganization));
|
||||
} catch (error) {
|
||||
dispatch(organizationActions.updateOrganizationFailed(organization.slug, error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function removeOrganizationAsync(organization: any): ThunkAction {
|
||||
return async function (dispatch) {
|
||||
try {
|
||||
await organization.remove();
|
||||
localStorage.removeItem('currentOrganization');
|
||||
dispatch(organizationActions.removeOrganizationSuccess(organization.slug));
|
||||
} catch (error) {
|
||||
dispatch(organizationActions.removeOrganizationFailed(error, organization.slug));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function inviteOrganizationMembersAsync(
|
||||
organization: any,
|
||||
members: { email: string; role: string }[],
|
||||
onFinish: () => void,
|
||||
): ThunkAction {
|
||||
return async function (dispatch) {
|
||||
dispatch(organizationActions.inviteOrganizationMembers());
|
||||
try {
|
||||
for (let i = 0; i < members.length; i++) {
|
||||
const { email, role } = members[i];
|
||||
organization
|
||||
.invite(email, role)
|
||||
.then(() => {
|
||||
dispatch(organizationActions.inviteOrganizationMemberSuccess(email));
|
||||
})
|
||||
.catch((error: any) => {
|
||||
dispatch(organizationActions.inviteOrganizationMemberFailed(email, error));
|
||||
})
|
||||
.finally(() => {
|
||||
if (i === members.length - 1) {
|
||||
dispatch(organizationActions.inviteOrganizationMembersDone());
|
||||
onFinish();
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch(organizationActions.inviteOrganizationMembersFailed(error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function leaveOrganizationAsync(organization: any): ThunkAction {
|
||||
return async function (dispatch, getState) {
|
||||
const { user } = getState().auth;
|
||||
dispatch(organizationActions.leaveOrganization());
|
||||
try {
|
||||
await organization.leave(user);
|
||||
dispatch(organizationActions.leaveOrganizationSuccess());
|
||||
localStorage.removeItem('currentOrganization');
|
||||
} catch (error) {
|
||||
dispatch(organizationActions.leaveOrganizationFailed(error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function removeOrganizationMemberAsync(
|
||||
organization: any,
|
||||
{ user, id }: { user: User; id: number },
|
||||
onFinish: () => void,
|
||||
): ThunkAction {
|
||||
return async function (dispatch) {
|
||||
dispatch(organizationActions.removeOrganizationMember());
|
||||
try {
|
||||
await organization.deleteMembership(id);
|
||||
dispatch(organizationActions.removeOrganizationMemberSuccess());
|
||||
onFinish();
|
||||
} catch (error) {
|
||||
dispatch(organizationActions.removeOrganizationMemberFailed(user.username, error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function updateOrganizationMemberAsync(
|
||||
organization: any,
|
||||
{ user, id }: { user: User; id: number },
|
||||
role: string,
|
||||
onFinish: () => void,
|
||||
): ThunkAction {
|
||||
return async function (dispatch) {
|
||||
dispatch(organizationActions.updateOrganizationMember());
|
||||
try {
|
||||
await organization.updateMembership(id, role);
|
||||
dispatch(organizationActions.updateOrganizationMemberSuccess());
|
||||
onFinish();
|
||||
} catch (error) {
|
||||
dispatch(organizationActions.updateOrganizationMemberFailed(user.username, role, error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type OrganizationActions = ActionUnion<typeof organizationActions>;
|
||||
@ -1 +0,0 @@
|
||||
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M25.9 23c-1.73 0-2.561 1-5.4 1-2.839 0-3.664-1-5.4-1-4.472 0-8.1 3.762-8.1 8.4V33c0 1.656 1.296 3 2.893 3h21.214C32.704 36 34 34.656 34 33v-1.6c0-4.637-3.628-8.4-8.1-8.4zm5.207 10H9.893v-1.6c0-2.975 2.338-5.4 5.207-5.4.88 0 2.308 1 5.4 1 3.116 0 4.514-1 5.4-1 2.869 0 5.207 2.425 5.207 5.4V33zM20.5 22c4.791 0 8.679-4.031 8.679-9S25.29 4 20.5 4s-8.679 4.031-8.679 9 3.888 9 8.679 9zm0-15c3.188 0 5.786 2.694 5.786 6s-2.598 6-5.786 6c-3.188 0-5.786-2.694-5.786-6s2.598-6 5.786-6z" fill="#000" fill-rule="nonzero"/></svg>
|
||||
|
Before Width: | Height: | Size: 591 B |
@ -1,64 +0,0 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { AnyAction } from 'redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useHistory } from 'react-router';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import Title from 'antd/lib/typography/Title';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
|
||||
import UserSelector, { User } from 'components/task-page/user-selector';
|
||||
import { CombinedState, TaskStatus } from 'reducers/interfaces';
|
||||
import { switchRequestReviewDialog } from 'actions/annotation-actions';
|
||||
import { updateJobAsync } from 'actions/tasks-actions';
|
||||
|
||||
export default function RequestReviewModal(): JSX.Element | null {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
const isVisible = useSelector((state: CombinedState): boolean => state.annotation.requestReviewDialogVisible);
|
||||
const job = useSelector((state: CombinedState): any => state.annotation.job.instance);
|
||||
const [reviewer, setReviewer] = useState<User | null>(job.reviewer ? job.reviewer : null);
|
||||
const close = (): AnyAction => dispatch(switchRequestReviewDialog(false));
|
||||
const submitAnnotations = (): void => {
|
||||
job.reviewer = reviewer;
|
||||
job.status = TaskStatus.REVIEW;
|
||||
dispatch(updateJobAsync(job));
|
||||
history.push(`/tasks/${job.task.id}`);
|
||||
};
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className='cvat-request-review-dialog'
|
||||
visible={isVisible}
|
||||
destroyOnClose
|
||||
onCancel={close}
|
||||
onOk={submitAnnotations}
|
||||
okText='Submit'
|
||||
>
|
||||
<Row justify='start'>
|
||||
<Col>
|
||||
<Title level={4}>Assign a user who is responsible for review</Title>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align='middle' justify='start'>
|
||||
<Col>
|
||||
<Text type='secondary'>Reviewer: </Text>
|
||||
</Col>
|
||||
<Col offset={1}>
|
||||
<UserSelector value={reviewer} onSelect={setReviewer} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row justify='start'>
|
||||
<Text type='secondary'>You might not be able to change the job after this action. Continue?</Text>
|
||||
</Row>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -1,149 +0,0 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { AnyAction } from 'redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import Title from 'antd/lib/typography/Title';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import Radio, { RadioChangeEvent } from 'antd/lib/radio';
|
||||
import RadioButton from 'antd/lib/radio/radioButton';
|
||||
import Description from 'antd/lib/descriptions';
|
||||
import Rate from 'antd/lib/rate';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
|
||||
import UserSelector, { User } from 'components/task-page/user-selector';
|
||||
import { CombinedState, ReviewStatus } from 'reducers/interfaces';
|
||||
import { switchSubmitReviewDialog } from 'actions/annotation-actions';
|
||||
import { submitReviewAsync } from 'actions/review-actions';
|
||||
import { clamp } from 'utils/math';
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
function computeEstimatedQuality(reviewedStates: number, openedIssues: number): number {
|
||||
if (reviewedStates === 0 && openedIssues === 0) {
|
||||
return 5; // corner case
|
||||
}
|
||||
|
||||
const K = 2; // means how many reviewed states are equivalent to one issue
|
||||
const quality = reviewedStates / (reviewedStates + K * openedIssues);
|
||||
return clamp(+(5 * quality).toPrecision(2), 0, 5);
|
||||
}
|
||||
|
||||
export default function SubmitReviewModal(): JSX.Element | null {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
const isVisible = useSelector((state: CombinedState): boolean => state.annotation.submitReviewDialogVisible);
|
||||
const job = useSelector((state: CombinedState): any => state.annotation.job.instance);
|
||||
const activeReview = useSelector((state: CombinedState): any => state.review.activeReview);
|
||||
const reviewIsBeingSubmitted = useSelector((state: CombinedState): any => state.review.fetching.reviewId);
|
||||
const numberOfIssues = useSelector((state: CombinedState): any => state.review.issues.length);
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const numberOfNewIssues = activeReview ? activeReview.issues.length : 0;
|
||||
const reviewedFrames = activeReview ? activeReview.reviewedFrames.length : 0;
|
||||
const reviewedStates = activeReview ? activeReview.reviewedStates.length : 0;
|
||||
|
||||
const [reviewer, setReviewer] = useState<User | null>(job.reviewer ? job.reviewer : null);
|
||||
const [reviewStatus, setReviewStatus] = useState<string>(ReviewStatus.ACCEPTED);
|
||||
const [estimatedQuality, setEstimatedQuality] = useState<number>(0);
|
||||
|
||||
const close = (): AnyAction => dispatch(switchSubmitReviewDialog(false));
|
||||
const submitReview = (): void => {
|
||||
activeReview.estimatedQuality = estimatedQuality;
|
||||
activeReview.status = reviewStatus;
|
||||
if (reviewStatus === ReviewStatus.REVIEW_FURTHER) {
|
||||
activeReview.reviewer = reviewer;
|
||||
}
|
||||
dispatch(submitReviewAsync(activeReview));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setEstimatedQuality(computeEstimatedQuality(reviewedStates, numberOfNewIssues));
|
||||
}, [reviewedStates, numberOfNewIssues]);
|
||||
useEffect(() => {
|
||||
if (!isSubmitting && activeReview && activeReview.id === reviewIsBeingSubmitted) {
|
||||
setIsSubmitting(true);
|
||||
} else if (isSubmitting && reviewIsBeingSubmitted === null) {
|
||||
setIsSubmitting(false);
|
||||
close();
|
||||
history.push(`/tasks/${job.task.id}`);
|
||||
}
|
||||
}, [reviewIsBeingSubmitted, activeReview]);
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className='cvat-submit-review-dialog'
|
||||
visible={isVisible}
|
||||
destroyOnClose
|
||||
confirmLoading={isSubmitting}
|
||||
onOk={submitReview}
|
||||
onCancel={close}
|
||||
okText='Submit'
|
||||
width={650}
|
||||
>
|
||||
<Row justify='start'>
|
||||
<Col>
|
||||
<Title level={4}>Submitting your review</Title>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row justify='start'>
|
||||
<Col span={12}>
|
||||
<Description title='Review summary' layout='horizontal' column={1} size='small' bordered>
|
||||
<Description.Item label='Estimated quality: '>{estimatedQuality}</Description.Item>
|
||||
<Description.Item label='Issues: '>
|
||||
<Text>{numberOfIssues}</Text>
|
||||
{!!numberOfNewIssues && <Text strong>{` (+${numberOfNewIssues})`}</Text>}
|
||||
</Description.Item>
|
||||
<Description.Item label='Reviewed frames '>{reviewedFrames}</Description.Item>
|
||||
<Description.Item label='Reviewed objects: '>{reviewedStates}</Description.Item>
|
||||
</Description>
|
||||
</Col>
|
||||
<Col span={11} offset={1}>
|
||||
<Row>
|
||||
<Col>
|
||||
<Radio.Group
|
||||
value={reviewStatus}
|
||||
onChange={(event: RadioChangeEvent) => {
|
||||
if (typeof event.target.value !== 'undefined') {
|
||||
setReviewStatus(event.target.value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RadioButton value={ReviewStatus.ACCEPTED}>Accept</RadioButton>
|
||||
<RadioButton value={ReviewStatus.REVIEW_FURTHER}>Review next</RadioButton>
|
||||
<RadioButton value={ReviewStatus.REJECTED}>Reject</RadioButton>
|
||||
</Radio.Group>
|
||||
{reviewStatus === ReviewStatus.REVIEW_FURTHER && (
|
||||
<Row align='middle' justify='start'>
|
||||
<Col span={7}>
|
||||
<Text type='secondary'>Reviewer: </Text>
|
||||
</Col>
|
||||
<Col span={16} offset={1}>
|
||||
<UserSelector value={reviewer} onSelect={setReviewer} />
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row justify='center' align='middle'>
|
||||
<Col>
|
||||
<Rate
|
||||
value={Math.round(estimatedQuality)}
|
||||
onChange={(value: number | undefined) => {
|
||||
if (typeof value !== 'undefined') {
|
||||
setEstimatedQuality(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Form from 'antd/lib/form';
|
||||
import Input from 'antd/lib/input';
|
||||
import Button from 'antd/lib/button';
|
||||
import Space from 'antd/lib/space';
|
||||
import { Store } from 'antd/lib/form/interface';
|
||||
import { useForm } from 'antd/lib/form/Form';
|
||||
import notification from 'antd/lib/notification';
|
||||
|
||||
import { createOrganizationAsync } from 'actions/organization-actions';
|
||||
import validationPatterns from 'utils/validation-patterns';
|
||||
import { CombinedState } from 'reducers/interfaces';
|
||||
|
||||
function CreateOrganizationForm(): JSX.Element {
|
||||
const [form] = useForm<Store>();
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
const creating = useSelector((state: CombinedState) => state.organizations.creating);
|
||||
const MAX_SLUG_LEN = 16;
|
||||
const MAX_NAME_LEN = 64;
|
||||
|
||||
const onFinish = (values: Store): void => {
|
||||
const {
|
||||
phoneNumber, location, email, ...rest
|
||||
} = values;
|
||||
|
||||
rest.contact = {
|
||||
...(phoneNumber ? { phoneNumber } : {}),
|
||||
...(email ? { email } : {}),
|
||||
...(location ? { location } : {}),
|
||||
};
|
||||
|
||||
dispatch(
|
||||
createOrganizationAsync(rest, (createdSlug: string): void => {
|
||||
form.resetFields();
|
||||
notification.info({ message: `Organization ${createdSlug} has been successfully created` });
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
autoComplete='off'
|
||||
onFinish={onFinish}
|
||||
className='cvat-create-organization-form'
|
||||
layout='vertical'
|
||||
>
|
||||
<Form.Item
|
||||
hasFeedback
|
||||
name='slug'
|
||||
label='Short name'
|
||||
rules={[
|
||||
{ required: true, message: 'Short name is a required field' },
|
||||
{ max: MAX_SLUG_LEN, message: `Short name must not exceed ${MAX_SLUG_LEN} characters` },
|
||||
{ ...validationPatterns.validateOrganizationSlug },
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
hasFeedback
|
||||
name='name'
|
||||
label='Full name'
|
||||
rules={[{ max: MAX_NAME_LEN, message: `Full name must not exceed ${MAX_NAME_LEN} characters` }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item hasFeedback name='description' label='Description'>
|
||||
<Input.TextArea rows={3} />
|
||||
</Form.Item>
|
||||
<Form.Item hasFeedback name='email' label='Email' rules={[{ type: 'email', message: 'The input is not a valid E-mail' }]}>
|
||||
<Input autoComplete='email' placeholder='support@organization.com' />
|
||||
</Form.Item>
|
||||
<Form.Item hasFeedback name='phoneNumber' label='Phone number' rules={[{ ...validationPatterns.validatePhoneNumber }]}>
|
||||
<Input autoComplete='phoneNumber' placeholder='+44 5555 555555' />
|
||||
</Form.Item>
|
||||
<Form.Item hasFeedback name='location' label='Location'>
|
||||
<Input autoComplete='location' placeholder='Country, State/Province, Address, Postal code' />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space className='cvat-create-organization-form-buttons-block' align='end'>
|
||||
<Button onClick={() => history.goBack()}>Cancel</Button>
|
||||
<Button loading={creating} disabled={creating} htmlType='submit' type='primary'>
|
||||
Submit
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(CreateOrganizationForm);
|
||||
@ -0,0 +1,23 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import './styles.scss';
|
||||
import React from 'react';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
|
||||
import CreateOrganizationForm from './create-organization-form';
|
||||
|
||||
function CreateOrganizationComponent(): JSX.Element {
|
||||
return (
|
||||
<Row justify='center' align='top' className='cvat-create-organization-page'>
|
||||
<Col md={20} lg={16} xl={14} xxl={9}>
|
||||
<Text className='cvat-title'>Create a new organization</Text>
|
||||
<CreateOrganizationForm />
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(CreateOrganizationComponent);
|
||||
@ -0,0 +1,52 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@import '../../base.scss';
|
||||
|
||||
.cvat-create-organization-page {
|
||||
text-align: center;
|
||||
padding-top: $grid-unit-size * 5;
|
||||
overflow-y: auto;
|
||||
height: 90%;
|
||||
width: 100%;
|
||||
|
||||
> div:first-child {
|
||||
> span {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-create-organization-form {
|
||||
text-align: initial;
|
||||
margin-top: $grid-unit-size * 2;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid $border-color-1;
|
||||
border-radius: 3px;
|
||||
padding: $grid-unit-size * 2;
|
||||
background: $background-color-1;
|
||||
|
||||
> div:not(first-child) {
|
||||
margin-top: $grid-unit-size;
|
||||
}
|
||||
|
||||
.cvat-create-organization-form-buttons-block {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.cvat-create-organization-form-contact-block {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
> .ant-space-item:first-child {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-create-organization-form-add-contact-block {
|
||||
align-items: baseline;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import Select from 'antd/lib/select';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
import moment from 'moment';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import Modal from 'antd/lib/modal';
|
||||
|
||||
export interface Props {
|
||||
membershipInstance: any;
|
||||
onRemoveMembership(): void;
|
||||
onUpdateMembershipRole(role: string): void;
|
||||
}
|
||||
|
||||
function MemberItem(props: Props): JSX.Element {
|
||||
const {
|
||||
membershipInstance, onRemoveMembership, onUpdateMembershipRole,
|
||||
} = props;
|
||||
const {
|
||||
user, joined_date: joinedDate, role, invitation,
|
||||
} = membershipInstance;
|
||||
const { username, firstName, lastName } = user;
|
||||
|
||||
return (
|
||||
<Row className='cvat-organization-member-item' justify='space-between'>
|
||||
<Col span={5} className='cvat-organization-member-item-username'>
|
||||
<Text strong>{username}</Text>
|
||||
</Col>
|
||||
<Col span={6} className='cvat-organization-member-item-name'>
|
||||
<Text strong>{`${firstName || ''} ${lastName || ''}`}</Text>
|
||||
</Col>
|
||||
<Col span={8} className='cvat-organization-member-item-dates'>
|
||||
{invitation ? (
|
||||
<Text type='secondary'>
|
||||
{`Invited ${moment(invitation.created_date).fromNow()} ${invitation.owner ? `by ${invitation.owner.username}` : ''}`}
|
||||
</Text>
|
||||
) : null}
|
||||
{joinedDate ? <Text type='secondary'>{`Joined ${moment(joinedDate).fromNow()}`}</Text> : null}
|
||||
</Col>
|
||||
<Col span={3} className='cvat-organization-member-item-role'>
|
||||
<Select
|
||||
onChange={(_role: string) => {
|
||||
onUpdateMembershipRole(_role);
|
||||
}}
|
||||
value={role}
|
||||
disabled={role === 'owner'}
|
||||
>
|
||||
{role === 'owner' ? (
|
||||
<Select.Option value='owner'>Owner</Select.Option>
|
||||
) : (
|
||||
<>
|
||||
<Select.Option value='worker'>Worker</Select.Option>
|
||||
<Select.Option value='supervisor'>Supervisor</Select.Option>
|
||||
<Select.Option value='maintainer'>Maintainer</Select.Option>
|
||||
</>
|
||||
)}
|
||||
</Select>
|
||||
</Col>
|
||||
<Col span={1} className='cvat-organization-member-item-remove'>
|
||||
{role !== 'owner' ? (
|
||||
<CloseOutlined
|
||||
onClick={() => {
|
||||
Modal.confirm({
|
||||
title: `You are removing "${username}" from this organization`,
|
||||
content: 'The person will not have access to the organization data anymore. Continue?',
|
||||
okText: 'Yes, remove',
|
||||
okButtonProps: {
|
||||
danger: true,
|
||||
},
|
||||
onOk: () => {
|
||||
onRemoveMembership();
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(MemberItem);
|
||||
@ -0,0 +1,83 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import Pagination from 'antd/lib/pagination';
|
||||
import Spin from 'antd/lib/spin';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { CombinedState } from 'reducers/interfaces';
|
||||
import { removeOrganizationMemberAsync, updateOrganizationMemberAsync } from 'actions/organization-actions';
|
||||
import MemberItem from './member-item';
|
||||
|
||||
export interface Props {
|
||||
organizationInstance: any;
|
||||
userInstance: any;
|
||||
fetching: boolean;
|
||||
pageSize: number;
|
||||
pageNumber: number;
|
||||
members: any[];
|
||||
setPageNumber: (pageNumber: number) => void;
|
||||
setPageSize: (pageSize: number) => void;
|
||||
fetchMembers: () => void;
|
||||
}
|
||||
|
||||
function MembersList(props: Props): JSX.Element {
|
||||
const {
|
||||
organizationInstance, fetching, members, pageSize, pageNumber, fetchMembers, setPageNumber, setPageSize,
|
||||
} = props;
|
||||
const dispatch = useDispatch();
|
||||
const inviting = useSelector((state: CombinedState) => state.organizations.inviting);
|
||||
const updatingMember = useSelector((state: CombinedState) => state.organizations.updatingMember);
|
||||
const removingMember = useSelector((state: CombinedState) => state.organizations.removingMember);
|
||||
|
||||
return fetching || inviting || updatingMember || removingMember ? (
|
||||
<Spin className='cvat-spinner' />
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
{members.map(
|
||||
(member: any): JSX.Element => (
|
||||
<MemberItem
|
||||
key={member.user.id}
|
||||
membershipInstance={member}
|
||||
onRemoveMembership={() => {
|
||||
dispatch(
|
||||
removeOrganizationMemberAsync(organizationInstance, member, () => {
|
||||
fetchMembers();
|
||||
}),
|
||||
);
|
||||
}}
|
||||
onUpdateMembershipRole={(role: string) => {
|
||||
dispatch(
|
||||
updateOrganizationMemberAsync(organizationInstance, member, role, () => {
|
||||
fetchMembers();
|
||||
}),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<div className='cvat-organization-members-pagination-block'>
|
||||
<Pagination
|
||||
total={members.length ? (members as any).count : 0}
|
||||
onShowSizeChange={(current: number, newShowSize: number) => {
|
||||
setPageNumber(current);
|
||||
setPageSize(newShowSize);
|
||||
}}
|
||||
onChange={(current: number) => {
|
||||
setPageNumber(current);
|
||||
}}
|
||||
current={pageNumber}
|
||||
pageSize={pageSize}
|
||||
showSizeChanger
|
||||
showQuickJumper
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(MembersList);
|
||||
@ -0,0 +1,86 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import './styles.scss';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Empty from 'antd/lib/empty';
|
||||
import Spin from 'antd/lib/spin';
|
||||
|
||||
import { CombinedState } from 'reducers/interfaces';
|
||||
import TopBarComponent from './top-bar';
|
||||
import MembersList from './members-list';
|
||||
|
||||
function fetchMembers(
|
||||
organizationInstance: any,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
setMembers: (members: any[]) => void,
|
||||
setFetching: (fetching: boolean) => void,
|
||||
): void {
|
||||
setFetching(true);
|
||||
organizationInstance
|
||||
.members(page, pageSize)
|
||||
.then((_members: any[]) => {
|
||||
setMembers(_members);
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
setFetching(false);
|
||||
});
|
||||
}
|
||||
|
||||
function OrganizationPage(): JSX.Element | null {
|
||||
const organization = useSelector((state: CombinedState) => state.organizations.current);
|
||||
const fetching = useSelector((state: CombinedState) => state.organizations.fetching);
|
||||
const updating = useSelector((state: CombinedState) => state.organizations.updating);
|
||||
const user = useSelector((state: CombinedState) => state.auth.user);
|
||||
const [membersFetching, setMembersFetching] = useState<boolean>(true);
|
||||
const [members, setMembers] = useState<any[]>([]);
|
||||
const [pageNumber, setPageNumber] = useState<number>(1);
|
||||
const [pageSize, setPageSize] = useState<number>(10);
|
||||
|
||||
useEffect(() => {
|
||||
if (organization) {
|
||||
fetchMembers(organization, pageNumber, pageSize, setMembers, setMembersFetching);
|
||||
}
|
||||
}, [pageSize, pageNumber, organization]);
|
||||
|
||||
if (fetching || updating) {
|
||||
return <Spin className='cvat-spinner' />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='cvat-organization-page'>
|
||||
{!organization ? (
|
||||
<Empty description='You are not in an organization' />
|
||||
) : (
|
||||
<>
|
||||
<TopBarComponent
|
||||
organizationInstance={organization}
|
||||
userInstance={user}
|
||||
fetchMembers={() => fetchMembers(
|
||||
organization, pageNumber, pageSize, setMembers, setMembersFetching,
|
||||
)}
|
||||
/>
|
||||
<MembersList
|
||||
fetching={membersFetching}
|
||||
members={members}
|
||||
organizationInstance={organization}
|
||||
userInstance={user}
|
||||
pageSize={pageSize}
|
||||
pageNumber={pageNumber}
|
||||
setPageNumber={setPageNumber}
|
||||
setPageSize={setPageSize}
|
||||
fetchMembers={() => fetchMembers(
|
||||
organization, pageNumber, pageSize, setMembers, setMembersFetching,
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(OrganizationPage);
|
||||
@ -0,0 +1,150 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@import 'base.scss';
|
||||
|
||||
.cvat-organization-page {
|
||||
height: 100%;
|
||||
padding-top: $grid-unit-size * 2;
|
||||
width: $grid-unit-size * 120;
|
||||
margin: 0 auto;
|
||||
|
||||
.ant-empty {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
> div:nth-child(1) {
|
||||
align-items: flex-end;
|
||||
|
||||
.cvat-organization-top-bar-buttons-block {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.cvat-organization-top-bar-descriptions {
|
||||
> div {
|
||||
> button {
|
||||
margin-top: $grid-unit-size;
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
> span:nth-child(3) {
|
||||
max-height: 7em;
|
||||
display: grid;
|
||||
overflow: auto;
|
||||
margin-bottom: $grid-unit-size;
|
||||
}
|
||||
|
||||
> span:not(.cvat-title),
|
||||
div {
|
||||
font-size: 12px;
|
||||
|
||||
span.anticon {
|
||||
margin-right: $grid-unit-size;
|
||||
}
|
||||
|
||||
span.anticon[aria-label=edit] {
|
||||
margin-left: $grid-unit-size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-organization-top-bar-contacts {
|
||||
button {
|
||||
margin-top: $grid-unit-size;
|
||||
float: right;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
> span,
|
||||
div {
|
||||
font-size: 12px;
|
||||
|
||||
span.anticon {
|
||||
margin-right: $grid-unit-size;
|
||||
}
|
||||
|
||||
span.anticon[aria-label=edit] {
|
||||
margin-left: $grid-unit-size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> div:nth-child(2) {
|
||||
overflow: auto;
|
||||
height: auto;
|
||||
max-height: 60%;
|
||||
margin-top: $grid-unit-size;
|
||||
}
|
||||
|
||||
@media screen and (min-height: 900px) {
|
||||
> div:nth-child(2) {
|
||||
max-height: 65%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-height: 1080px) {
|
||||
> div:nth-child(2) {
|
||||
max-height: 70%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-organization-member-item {
|
||||
border: 1px solid $border-color-1;
|
||||
border-radius: 3px;
|
||||
padding: $grid-unit-size;
|
||||
background: $background-color-1;
|
||||
margin-top: $grid-unit-size;
|
||||
align-items: center;
|
||||
|
||||
> .cvat-organization-member-item-username,
|
||||
.cvat-organization-member-item-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
> .cvat-organization-member-item-dates {
|
||||
font-size: 12px;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
> .cvat-organization-member-item-remove {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
> .cvat-organization-member-item-role {
|
||||
> .ant-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-organization-members-pagination-block {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.cvat-remove-organization-submit {
|
||||
> input {
|
||||
margin-top: $grid-unit-size;
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-organization-invitation-field {
|
||||
align-items: baseline;
|
||||
}
|
||||
@ -0,0 +1,341 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import moment from 'moment';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import Button from 'antd/lib/button';
|
||||
import Space from 'antd/lib/space';
|
||||
import Input from 'antd/lib/input';
|
||||
import Form from 'antd/lib/form';
|
||||
import Select from 'antd/lib/select';
|
||||
import { useForm } from 'antd/lib/form/Form';
|
||||
import { Store } from 'antd/lib/form/interface';
|
||||
import {
|
||||
CloseOutlined, EditTwoTone, EnvironmentOutlined, MailOutlined, PhoneOutlined, PlusCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
import {
|
||||
inviteOrganizationMembersAsync,
|
||||
leaveOrganizationAsync,
|
||||
removeOrganizationAsync,
|
||||
updateOrganizationAsync,
|
||||
} from 'actions/organization-actions';
|
||||
|
||||
export interface Props {
|
||||
organizationInstance: any;
|
||||
userInstance: any;
|
||||
fetchMembers: () => void;
|
||||
}
|
||||
|
||||
function OrganizationTopBar(props: Props): JSX.Element {
|
||||
const { organizationInstance, userInstance, fetchMembers } = props;
|
||||
const {
|
||||
owner, createdDate, description, updatedDate, slug, name, contact,
|
||||
} = organizationInstance;
|
||||
const { id: userID } = userInstance;
|
||||
const [form] = useForm();
|
||||
const descriptionEditingRef = useRef<HTMLDivElement>(null);
|
||||
const [visibleInviteModal, setVisibleInviteModal] = useState<boolean>(false);
|
||||
const [editingDescription, setEditingDescription] = useState<boolean>(false);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (event: MouseEvent): void => {
|
||||
const divElement = descriptionEditingRef.current;
|
||||
if (editingDescription && divElement && !event.composedPath().includes(divElement)) {
|
||||
setEditingDescription(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('mousedown', listener);
|
||||
return () => {
|
||||
window.removeEventListener('mousedown', listener);
|
||||
};
|
||||
});
|
||||
|
||||
let organizationName = name;
|
||||
let organizationDescription = description;
|
||||
let organizationContacts = contact;
|
||||
return (
|
||||
<>
|
||||
<Row justify='space-between'>
|
||||
<Col span={24}>
|
||||
<div className='cvat-organization-top-bar-descriptions'>
|
||||
<Text>
|
||||
<Text className='cvat-title'>{`Organization: ${slug} `}</Text>
|
||||
</Text>
|
||||
<Text
|
||||
editable={{
|
||||
onChange: (value: string) => {
|
||||
organizationName = value;
|
||||
},
|
||||
onEnd: () => {
|
||||
organizationInstance.name = organizationName;
|
||||
dispatch(updateOrganizationAsync(organizationInstance));
|
||||
},
|
||||
}}
|
||||
type='secondary'
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
{!editingDescription ? (
|
||||
<span style={{ display: 'grid' }}>
|
||||
{(description || 'Add description').split('\n').map((val: string, idx: number) => (
|
||||
<Text key={idx} type='secondary'>
|
||||
{val}
|
||||
{idx === 0 ? <EditTwoTone onClick={() => setEditingDescription(true)} /> : null}
|
||||
</Text>
|
||||
))}
|
||||
</span>
|
||||
) : (
|
||||
<div ref={descriptionEditingRef}>
|
||||
<Input.TextArea
|
||||
defaultValue={description}
|
||||
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
organizationDescription = event.target.value;
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
onClick={() => {
|
||||
if (organizationDescription !== description) {
|
||||
organizationInstance.description = organizationDescription;
|
||||
dispatch(updateOrganizationAsync(organizationInstance));
|
||||
}
|
||||
setEditingDescription(false);
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className='cvat-organization-top-bar-contacts'>
|
||||
<div>
|
||||
<PhoneOutlined />
|
||||
{ !contact.phoneNumber ? <Text type='secondary'>Add phone number</Text> : null }
|
||||
<Text
|
||||
type='secondary'
|
||||
editable={{
|
||||
onChange: (value: string) => {
|
||||
organizationContacts = {
|
||||
...organizationInstance.contact, phoneNumber: value,
|
||||
};
|
||||
},
|
||||
onEnd: () => {
|
||||
organizationInstance.contact = organizationContacts;
|
||||
dispatch(updateOrganizationAsync(organizationInstance));
|
||||
},
|
||||
}}
|
||||
>
|
||||
{contact.phoneNumber}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<MailOutlined />
|
||||
{ !contact.email ? <Text type='secondary'>Add email</Text> : null }
|
||||
<Text
|
||||
type='secondary'
|
||||
editable={{
|
||||
onChange: (value: string) => {
|
||||
organizationContacts = {
|
||||
...organizationInstance.contact, email: value,
|
||||
};
|
||||
},
|
||||
onEnd: () => {
|
||||
organizationInstance.contact = organizationContacts;
|
||||
dispatch(updateOrganizationAsync(organizationInstance));
|
||||
},
|
||||
}}
|
||||
>
|
||||
{contact.email}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<EnvironmentOutlined />
|
||||
{ !contact.location ? <Text type='secondary'>Add location</Text> : null }
|
||||
<Text
|
||||
type='secondary'
|
||||
editable={{
|
||||
onChange: (value: string) => {
|
||||
organizationContacts = {
|
||||
...organizationInstance.contact, location: value,
|
||||
};
|
||||
},
|
||||
onEnd: () => {
|
||||
organizationInstance.contact = organizationContacts;
|
||||
dispatch(updateOrganizationAsync(organizationInstance));
|
||||
},
|
||||
}}
|
||||
>
|
||||
{contact.location}
|
||||
</Text>
|
||||
</div>
|
||||
<Text type='secondary'>{`Created ${moment(createdDate).format('MMMM Do YYYY')}`}</Text>
|
||||
<Text type='secondary'>{`Updated ${moment(updatedDate).fromNow()}`}</Text>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12} className='cvat-organization-top-bar-buttons-block'>
|
||||
<Space align='end'>
|
||||
{!(owner && userID === owner.id) ? (
|
||||
<Button
|
||||
type='primary'
|
||||
danger
|
||||
onClick={() => {
|
||||
Modal.confirm({
|
||||
onOk: () => {
|
||||
dispatch(leaveOrganizationAsync(organizationInstance));
|
||||
},
|
||||
content: (
|
||||
<>
|
||||
<Text>Please, confirm leaving the organization</Text>
|
||||
<Text strong>{` ${organizationInstance.slug}`}</Text>
|
||||
<Text>. You will not have access to the organization data anymore</Text>
|
||||
</>
|
||||
),
|
||||
okText: 'Leave',
|
||||
okButtonProps: {
|
||||
danger: true,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Leave organization
|
||||
</Button>
|
||||
) : null}
|
||||
{owner && userID === owner.id ? (
|
||||
<Button
|
||||
type='primary'
|
||||
danger
|
||||
onClick={() => {
|
||||
const modal = Modal.confirm({
|
||||
onOk: () => {
|
||||
dispatch(removeOrganizationAsync(organizationInstance));
|
||||
},
|
||||
content: (
|
||||
<div className='cvat-remove-organization-submit'>
|
||||
<Text type='warning'>
|
||||
To remove the organization, enter its short name below
|
||||
</Text>
|
||||
<Input
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
modal.update({
|
||||
okButtonProps: {
|
||||
disabled:
|
||||
event.target.value !== organizationInstance.slug,
|
||||
danger: true,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
okButtonProps: {
|
||||
disabled: true,
|
||||
danger: true,
|
||||
},
|
||||
okText: 'Remove',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Remove organization
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={() => setVisibleInviteModal(true)}
|
||||
icon={<PlusCircleOutlined />}
|
||||
>
|
||||
Invite members
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
<Modal
|
||||
visible={visibleInviteModal}
|
||||
onCancel={() => {
|
||||
setVisibleInviteModal(false);
|
||||
form.resetFields(['users']);
|
||||
}}
|
||||
destroyOnClose
|
||||
onOk={() => {
|
||||
form.submit();
|
||||
}}
|
||||
>
|
||||
<Form
|
||||
initialValues={{
|
||||
users: [{ email: '', role: 'worker' }],
|
||||
}}
|
||||
onFinish={(values: Store) => {
|
||||
dispatch(
|
||||
inviteOrganizationMembersAsync(organizationInstance, values.users, () => {
|
||||
fetchMembers();
|
||||
}),
|
||||
);
|
||||
setVisibleInviteModal(false);
|
||||
form.resetFields(['users']);
|
||||
}}
|
||||
layout='vertical'
|
||||
form={form}
|
||||
>
|
||||
<Text>Invitation list: </Text>
|
||||
<Form.List name='users'>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map((field: any, index: number) => (
|
||||
<Row className='cvat-organization-invitation-field' key={field.key}>
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
hasFeedback
|
||||
name={[field.name, 'email']}
|
||||
fieldKey={[field.fieldKey, 'email']}
|
||||
rules={[
|
||||
{ required: true, message: 'This field is required' },
|
||||
{ type: 'email', message: 'The input is not a valid email' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder='Enter an email address' />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={10} offset={1}>
|
||||
<Form.Item
|
||||
name={[field.name, 'role']}
|
||||
fieldKey={[field.fieldKey, 'role']}
|
||||
initialValue='worker'
|
||||
rules={[{ required: true, message: 'This field is required' }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value='worker'>Worker</Select.Option>
|
||||
<Select.Option value='supervisor'>Supervisor</Select.Option>
|
||||
<Select.Option value='maintainer'>Maintainer</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={1} offset={1}>
|
||||
{index > 0 ? <CloseOutlined onClick={() => remove(field.name)} /> : null}
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button icon={<PlusCircleOutlined />} onClick={() => add()}>
|
||||
Invite more
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(OrganizationTopBar);
|
||||
@ -1,81 +0,0 @@
|
||||
// Copyright (C) 2020-2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { CombinedState } from 'reducers/interfaces';
|
||||
import { showStatistics } from 'actions/annotation-actions';
|
||||
import StatisticsModalComponent from 'components/annotation-page/top-bar/statistics-modal';
|
||||
|
||||
interface StateToProps {
|
||||
visible: boolean;
|
||||
collecting: boolean;
|
||||
data: any;
|
||||
jobInstance: any;
|
||||
jobStatus: string;
|
||||
savingJobStatus: boolean;
|
||||
}
|
||||
|
||||
interface DispatchToProps {
|
||||
closeStatistics(): void;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: CombinedState): StateToProps {
|
||||
const {
|
||||
annotation: {
|
||||
statistics: { visible, collecting, data },
|
||||
job: {
|
||||
saving: savingJobStatus,
|
||||
instance: { status: jobStatus },
|
||||
instance: jobInstance,
|
||||
},
|
||||
},
|
||||
} = state;
|
||||
|
||||
return {
|
||||
visible,
|
||||
collecting,
|
||||
data,
|
||||
jobInstance,
|
||||
jobStatus,
|
||||
savingJobStatus,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||
return {
|
||||
closeStatistics(): void {
|
||||
dispatch(showStatistics(false));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type Props = StateToProps & DispatchToProps;
|
||||
|
||||
class StatisticsModalContainer extends React.PureComponent<Props> {
|
||||
public render(): JSX.Element {
|
||||
const {
|
||||
jobInstance, visible, collecting, data, closeStatistics, jobStatus, savingJobStatus,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<StatisticsModalComponent
|
||||
jobInstance={jobInstance}
|
||||
collecting={collecting}
|
||||
data={data}
|
||||
visible={visible}
|
||||
jobStatus={jobStatus}
|
||||
bugTracker={jobInstance.task.bugTracker}
|
||||
startFrame={jobInstance.startFrame}
|
||||
stopFrame={jobInstance.stopFrame}
|
||||
assignee={jobInstance.assignee ? jobInstance.assignee.username : 'Nobody'}
|
||||
reviewer={jobInstance.reviewer ? jobInstance.reviewer.username : 'Nobody'}
|
||||
savingJobStatus={savingJobStatus}
|
||||
closeStatistics={closeStatistics}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StatisticsModalContainer);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue