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