@ -0,0 +1,30 @@
|
||||
http:
|
||||
routers:
|
||||
kibana:
|
||||
entryPoints:
|
||||
- web
|
||||
middlewares:
|
||||
- analytics-auth
|
||||
- strip-prefix
|
||||
service: kibana
|
||||
rule: Host(`{{ env "CVAT_HOST" }}`) && PathPrefix(`/analytics`)
|
||||
|
||||
middlewares:
|
||||
analytics-auth:
|
||||
forwardauth:
|
||||
address: http://cvat:8080/analytics
|
||||
authRequestHeaders:
|
||||
- "Cookie"
|
||||
- "Authorization"
|
||||
|
||||
strip-prefix:
|
||||
stripprefix:
|
||||
prefixes:
|
||||
- /analytics
|
||||
|
||||
services:
|
||||
kibana:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: http://{{ env "DJANGO_LOG_VIEWER_HOST" }}:{{ env "DJANGO_LOG_VIEWER_PORT" }}
|
||||
passHostHeader: false
|
||||
@ -1,8 +1,11 @@
|
||||
// Copyright (C) 2019-2021 Intel Corporation
|
||||
// Copyright (C) 2019-2022 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
module.exports = {
|
||||
backendAPI: '/api/v1',
|
||||
backendAPI: '/api',
|
||||
proxy: false,
|
||||
organizationID: null,
|
||||
origin: '',
|
||||
uploadChunkSize: 100,
|
||||
};
|
||||
|
||||
@ -0,0 +1,378 @@
|
||||
// Copyright (C) 2021-2022 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, {
|
||||
filter: JSON.stringify({
|
||||
and: [{
|
||||
'==': [{ var: '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,397 +0,0 @@
|
||||
// Copyright (C) 2020 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Review;
|
||||
@ -1,7 +1,17 @@
|
||||
server {
|
||||
root /usr/share/nginx/html;
|
||||
# Any route that doesn't have a file extension (e.g. /devices)
|
||||
|
||||
location / {
|
||||
# Any route that doesn't exist on the server (e.g. /devices)
|
||||
try_files $uri $uri/ /index.html;
|
||||
add_header Cache-Control: "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma: "no-cache";
|
||||
add_header Expires: 0;
|
||||
}
|
||||
|
||||
location /assets {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public";
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { createAction, ActionUnion, ThunkAction } from 'utils/redux';
|
||||
import { CombinedState } from 'reducers/interfaces';
|
||||
import { getProjectsAsync } from './projects-actions';
|
||||
|
||||
export enum ImportActionTypes {
|
||||
OPEN_IMPORT_MODAL = 'OPEN_IMPORT_MODAL',
|
||||
CLOSE_IMPORT_MODAL = 'CLOSE_IMPORT_MODAL',
|
||||
IMPORT_DATASET = 'IMPORT_DATASET',
|
||||
IMPORT_DATASET_SUCCESS = 'IMPORT_DATASET_SUCCESS',
|
||||
IMPORT_DATASET_FAILED = 'IMPORT_DATASET_FAILED',
|
||||
IMPORT_DATASET_UPDATE_STATUS = 'IMPORT_DATASET_UPDATE_STATUS',
|
||||
}
|
||||
|
||||
export const importActions = {
|
||||
openImportModal: (instance: any) => createAction(ImportActionTypes.OPEN_IMPORT_MODAL, { instance }),
|
||||
closeImportModal: () => createAction(ImportActionTypes.CLOSE_IMPORT_MODAL),
|
||||
importDataset: (projectId: number) => (
|
||||
createAction(ImportActionTypes.IMPORT_DATASET, { id: projectId })
|
||||
),
|
||||
importDatasetSuccess: () => (
|
||||
createAction(ImportActionTypes.IMPORT_DATASET_SUCCESS)
|
||||
),
|
||||
importDatasetFailed: (instance: any, error: any) => (
|
||||
createAction(ImportActionTypes.IMPORT_DATASET_FAILED, {
|
||||
instance,
|
||||
error,
|
||||
})
|
||||
),
|
||||
importDatasetUpdateStatus: (progress: number, status: string) => (
|
||||
createAction(ImportActionTypes.IMPORT_DATASET_UPDATE_STATUS, { progress, status })
|
||||
),
|
||||
};
|
||||
|
||||
export const importDatasetAsync = (instance: any, format: string, file: File): ThunkAction => (
|
||||
async (dispatch, getState) => {
|
||||
try {
|
||||
const state: CombinedState = getState();
|
||||
if (state.import.importingId !== null) {
|
||||
throw Error('Only one importing of dataset allowed at the same time');
|
||||
}
|
||||
dispatch(importActions.importDataset(instance.id));
|
||||
await instance.annotations.importDataset(format, file, (message: string, progress: number) => (
|
||||
dispatch(importActions.importDatasetUpdateStatus(progress * 100, message))
|
||||
));
|
||||
} catch (error) {
|
||||
dispatch(importActions.importDatasetFailed(instance, error));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(importActions.importDatasetSuccess());
|
||||
dispatch(getProjectsAsync({ id: instance.id }));
|
||||
}
|
||||
);
|
||||
|
||||
export type ImportActions = ActionUnion<typeof importActions>;
|
||||
@ -0,0 +1,47 @@
|
||||
// Copyright (C) 2022 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
|
||||
import getCore from 'cvat-core-wrapper';
|
||||
import { JobsQuery } from 'reducers/interfaces';
|
||||
|
||||
const cvat = getCore();
|
||||
|
||||
export enum JobsActionTypes {
|
||||
GET_JOBS = 'GET_JOBS',
|
||||
GET_JOBS_SUCCESS = 'GET_JOBS_SUCCESS',
|
||||
GET_JOBS_FAILED = 'GET_JOBS_FAILED',
|
||||
}
|
||||
|
||||
interface JobsList extends Array<any> {
|
||||
count: number;
|
||||
}
|
||||
|
||||
const jobsActions = {
|
||||
getJobs: (query: Partial<JobsQuery>) => createAction(JobsActionTypes.GET_JOBS, { query }),
|
||||
getJobsSuccess: (jobs: JobsList, previews: string[]) => (
|
||||
createAction(JobsActionTypes.GET_JOBS_SUCCESS, { jobs, previews })
|
||||
),
|
||||
getJobsFailed: (error: any) => createAction(JobsActionTypes.GET_JOBS_FAILED, { error }),
|
||||
};
|
||||
|
||||
export type JobsActions = ActionUnion<typeof jobsActions>;
|
||||
|
||||
export const getJobsAsync = (query: JobsQuery): ThunkAction => async (dispatch) => {
|
||||
try {
|
||||
// Remove all keys with null values from the query
|
||||
const filteredQuery: Partial<JobsQuery> = { ...query };
|
||||
if (filteredQuery.page === null) delete filteredQuery.page;
|
||||
if (filteredQuery.filter === null) delete filteredQuery.filter;
|
||||
if (filteredQuery.sort === null) delete filteredQuery.sort;
|
||||
if (filteredQuery.search === null) delete filteredQuery.search;
|
||||
|
||||
dispatch(jobsActions.getJobs(filteredQuery));
|
||||
const jobs = await cvat.jobs.get(filteredQuery);
|
||||
const previewPromises = jobs.map((job: any) => (job as any).frames.preview().catch(() => ''));
|
||||
dispatch(jobsActions.getJobsSuccess(jobs, await Promise.all(previewPromises)));
|
||||
} catch (error) {
|
||||
dispatch(jobsActions.getJobsFailed(error));
|
||||
}
|
||||
};
|
||||
@ -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 |
@ -0,0 +1,22 @@
|
||||
<!-- Downloaded from: https://www.svgrepo.com/svg/253277/vector-ellipse, CC0 License -->
|
||||
<svg width="40" height="40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M503.47,231.292h-19.628c-22.657-62.638-105.58-108.515-210.691-116.371V94.806c0-4.709-3.822-8.53-8.53-8.53h-51.182
|
||||
c-4.709,0-8.53,3.822-8.53,8.53v21.138C87.138,128.714,0,193.264,0,269.678c0,86.046,111.014,156.046,247.465,156.046
|
||||
c127.776,0,221.943-50.389,237.766-126.189h18.238c4.709,0,8.53-3.822,8.53-8.53v-51.182
|
||||
C512,235.113,508.187,231.292,503.47,231.292z M221.968,103.337h34.121v34.121h-34.121V103.337z M247.465,408.663
|
||||
c-127.051,0-230.405-62.348-230.405-138.985c0-67.236,79.742-124.355,187.847-136.571v12.881c0,4.709,3.822,8.53,8.53,8.53h51.182
|
||||
c4.709,0,8.53-3.822,8.53-8.53v-13.964c94.508,7.328,169.336,46.072,192.556,99.268h-13.41c-4.709,0-8.53,3.822-8.53,8.53v51.182
|
||||
c0,4.709,3.822,8.53,8.53,8.53h15.235C451.025,364.246,362.821,408.663,247.465,408.663z M494.939,282.474h-34.121v-34.121h34.121
|
||||
V282.474z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M256.09,256.883h-8.53v-8.53c0-4.709-3.813-8.53-8.53-8.53c-4.709,0-8.53,3.822-8.53,8.53v8.53h-8.53
|
||||
c-4.709,0-8.53,3.822-8.53,8.53c0,4.709,3.822,8.53,8.53,8.53h8.53v8.53c0,4.709,3.822,8.53,8.53,8.53s8.53-3.822,8.53-8.53v-8.53
|
||||
h8.53c4.709,0,8.53-3.822,8.53-8.53C264.62,260.704,260.798,256.883,256.09,256.883z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -1 +0,0 @@
|
||||
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" stroke="#000" stroke-width="2" fill="none"><path d="M3 9h34v22H3z"/><path d="M33.626 16.983h-2.571v-5.538h-3.858v5.538h-2.571l4.5 6.462z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 235 B |
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- The icon received from: https://github.com/gilbarbara/logos -->
|
||||
<!-- License: CC0-1.0 License -->
|
||||
<svg width="1em" height="1em" viewBox="0 0 256 206" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<path d="M170.2517,56.8186 L192.5047,34.5656 L193.9877,25.1956 C153.4367,-11.6774 88.9757,-7.4964 52.4207,33.9196 C42.2667,45.4226 34.7337,59.7636 30.7167,74.5726 L38.6867,73.4496 L83.1917,66.1106 L86.6277,62.5966 C106.4247,40.8546 139.8977,37.9296 162.7557,56.4286 L170.2517,56.8186 Z" fill="#EA4335"></path>
|
||||
<path d="M224.2048,73.9182 C219.0898,55.0822 208.5888,38.1492 193.9878,25.1962 L162.7558,56.4282 C175.9438,67.2042 183.4568,83.4382 183.1348,100.4652 L183.1348,106.0092 C198.4858,106.0092 210.9318,118.4542 210.9318,133.8052 C210.9318,149.1572 198.4858,161.2902 183.1348,161.2902 L127.4638,161.2902 L121.9978,167.2242 L121.9978,200.5642 L127.4638,205.7952 L183.1348,205.7952 C223.0648,206.1062 255.6868,174.3012 255.9978,134.3712 C256.1858,110.1682 244.2528,87.4782 224.2048,73.9182" fill="#4285F4"></path>
|
||||
<path d="M71.8704,205.7957 L127.4634,205.7957 L127.4634,161.2897 L71.8704,161.2897 C67.9094,161.2887 64.0734,160.4377 60.4714,158.7917 L52.5844,161.2117 L30.1754,183.4647 L28.2234,191.0387 C40.7904,200.5277 56.1234,205.8637 71.8704,205.7957" fill="#34A853"></path>
|
||||
<path d="M71.8704,61.4255 C31.9394,61.6635 -0.2366,94.2275 0.0014,134.1575 C0.1344,156.4555 10.5484,177.4455 28.2234,191.0385 L60.4714,158.7915 C46.4804,152.4705 40.2634,136.0055 46.5844,122.0155 C52.9044,108.0255 69.3704,101.8085 83.3594,108.1285 C89.5244,110.9135 94.4614,115.8515 97.2464,122.0155 L129.4944,89.7685 C115.7734,71.8315 94.4534,61.3445 71.8704,61.4255" fill="#FBBC05"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@ -1 +0,0 @@
|
||||
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M20.5 11c2.475 0 4.5-2.025 4.5-4.5S22.975 2 20.5 2A4.513 4.513 0 0 0 16 6.5c0 2.475 2.025 4.5 4.5 4.5zm0 4.5A4.513 4.513 0 0 0 16 20c0 2.475 2.025 4.5 4.5 4.5S25 22.475 25 20s-2.025-4.5-4.5-4.5zm0 13.5a4.513 4.513 0 0 0-4.5 4.5c0 2.475 2.025 4.5 4.5 4.5s4.5-2.025 4.5-4.5-2.025-4.5-4.5-4.5z" fill="#000" fill-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 403 B |
@ -1 +0,0 @@
|
||||
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M20 28.656c-4.267 0-7.72-3.325-8.038-7.54l-5.9-4.591c-.777.98-1.49 2.016-2.066 3.149a1.844 1.844 0 0 0 0 1.653C7.046 27.32 13.085 31.375 20 31.375c1.514 0 2.974-.227 4.381-.593l-2.919-2.274a8.053 8.053 0 0 1-1.462.148zm17.652 3.291l-6.218-4.84a18.74 18.74 0 0 0 4.57-5.78 1.844 1.844 0 0 0 0-1.654C32.954 13.68 26.914 9.625 20 9.625a17.24 17.24 0 0 0-8.287 2.135L4.557 6.191a.896.896 0 0 0-1.263.16L2.189 7.78a.91.91 0 0 0 .159 1.272l33.095 25.756a.896.896 0 0 0 1.263-.16l1.105-1.43a.91.91 0 0 0-.159-1.272zm-10.334-8.043l-2.21-1.72a5.38 5.38 0 0 0-1.812-6.027 5.3 5.3 0 0 0-4.72-.88c.34.463.523 1.023.524 1.598a2.659 2.659 0 0 1-.087.566l-4.14-3.222A7.972 7.972 0 0 1 20 12.344a8.067 8.067 0 0 1 5.729 2.387 8.18 8.18 0 0 1 2.37 5.769c0 1.225-.297 2.367-.781 3.405z" fill="#000" fill-rule="nonzero"/></svg>
|
||||
|
Before Width: | Height: | Size: 880 B |
@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" height="1em" width="1em" viewBox="64 64 896 896" style="transform: scale(1.5)">
|
||||
<g style="transform: scale(25)">
|
||||
<g transform="translate(3 7)" fill-rule="evenodd">
|
||||
<rect x="5.75" y="4.5" width="22.5" height="15.75" rx="2.25"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 359 B |
@ -1 +0,0 @@
|
||||
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M20.5 3c4.885 0 8.857 3.812 8.857 8.5 0 4.688-3.972 8.5-8.857 8.5s-8.857-3.812-8.857-8.5c0-4.688 3.972-8.5 8.857-8.5zM36 34.45c0 1.408-1.384 2.55-3.1 2.55H8.1C6.384 37 5 35.858 5 34.45V31.9c0-4.223 4.166-7.65 9.3-7.65h.692a14.802 14.802 0 0 0 11.016 0h.692c5.134 0 9.3 3.427 9.3 7.65v2.55z" fill="#000" fill-rule="nonzero"/></svg>
|
||||
|
Before Width: | Height: | Size: 402 B |
@ -0,0 +1,14 @@
|
||||
<svg width="120" height="28" viewBox="0 -3 120 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M117.045 0.566335H116.582V0.264099H117.898V0.566335H117.435V1.97881H117.044V0.566335H117.045Z" fill="black"/>
|
||||
<path d="M118.188 0.26413H118.7L119.101 1.33112L119.496 0.26413H120V1.97884H119.614V0.77498L119.139 1.97884H119.012L118.535 0.77498V1.97884H118.187V0.26413H118.188Z" fill="black"/>
|
||||
<path d="M23.0686 17.0748V24.4231H20.4009V6.05237H23.0686V7.81389C23.9997 6.58052 25.5852 5.80051 27.2714 5.80051C30.568 5.80051 33.5375 8.31711 33.5375 12.3434C33.5375 16.3447 30.4673 18.8862 27.2963 18.8862C25.5598 18.8867 23.9748 18.2324 23.0686 17.0748ZM30.8698 12.3184C30.8698 10.0283 29.1332 8.2418 26.8939 8.2418C24.6037 8.2418 22.918 10.0791 22.918 12.3184C22.918 14.5832 24.6042 16.4205 26.8939 16.4205C29.1337 16.4205 30.8698 14.6086 30.8698 12.3184Z" fill="black"/>
|
||||
<path d="M34.778 12.3943C34.778 8.69466 37.6217 5.80103 41.3208 5.80103C45.0453 5.80103 47.7884 8.49368 47.7884 12.2182V13.275H37.3704C37.7225 15.2126 39.2326 16.4963 41.4221 16.4963C43.1332 16.4963 44.4419 15.5901 45.0962 14.2316L47.3106 15.465C46.2034 17.5287 44.2149 18.8878 41.4221 18.8878C37.4457 18.8867 34.778 16.043 34.778 12.3943ZM37.496 10.9848H45.0458C44.6434 9.17294 43.2589 8.16651 41.3213 8.16651C39.4585 8.16651 37.9993 9.32406 37.496 10.9848Z" fill="black"/>
|
||||
<path d="M49.409 6.05237H52.0767V7.68821C52.9325 6.53066 54.2916 5.80051 55.9524 5.80051C59.1233 5.80051 61.0609 7.86426 61.0609 11.2866V18.6349H58.3428V11.5135C58.3428 9.44973 57.3109 8.14106 55.2975 8.14106C53.4856 8.14106 52.1016 9.5001 52.1016 11.7145V18.6349H49.409V6.05237Z" fill="black"/>
|
||||
<path d="M60.3419 0.264099H63.1856L68.1435 14.5832L73.1009 0.264099H75.9447L69.5275 18.6349H66.7595L60.3419 0.264099Z" fill="black"/>
|
||||
<path d="M76.9639 0.264099H79.7069V18.6349H76.9639V0.264099Z" fill="black"/>
|
||||
<path d="M82.2078 0.264099H85.3034L94.6147 14.1558V0.264099H97.3323V18.6349H94.4382L84.9508 4.51729V18.6349H82.2078V0.264099Z" fill="black"/>
|
||||
<path d="M12.9117 7.96652C12.6675 8.46617 12.1663 8.81369 11.5633 8.81369C10.7172 8.81369 10.0583 8.12629 10.0583 7.28572C10.0583 6.6782 10.4048 6.15208 10.9151 5.9048C10.4587 5.70738 9.95395 5.59747 9.41817 5.59747C7.302 5.59747 5.65497 7.32999 5.65497 9.4319C5.65497 11.5338 7.30251 13.2516 9.41817 13.2516C11.5481 13.2516 13.1956 11.5333 13.1956 9.4319C13.1951 8.91393 13.0938 8.41936 12.9117 7.96652Z" fill="black"/>
|
||||
<path d="M0 9.437C0 4.22775 4.20281 0 9.41206 0C14.6468 0 18.8491 4.22775 18.8491 9.437C18.8491 14.6462 14.6462 18.874 9.41206 18.874C4.20281 18.874 0 14.6462 0 9.437ZM16.106 9.437C16.106 5.71247 13.187 2.64228 9.41206 2.64228C5.66261 2.64228 2.74302 5.71247 2.74302 9.437C2.74302 13.1615 5.6621 16.2063 9.41206 16.2063C13.187 16.2068 16.106 13.1615 16.106 9.437Z" fill="black"/>
|
||||
<path d="M111.862 7.97924C111.618 8.4789 111.117 8.82642 110.514 8.82642C109.668 8.82642 109.008 8.13901 109.008 7.29845C109.008 6.69092 109.355 6.16481 109.865 5.91752C109.409 5.7201 108.904 5.6102 108.368 5.6102C106.252 5.6102 104.605 7.34272 104.605 9.44463C104.605 11.5465 106.252 13.2648 108.368 13.2648C110.498 13.2648 112.145 11.5465 112.145 9.44463C112.145 8.92666 112.044 8.43158 111.862 7.97924Z" fill="black"/>
|
||||
<path d="M98.9503 9.44972C98.9503 4.24047 103.153 0.0127258 108.362 0.0127258C113.597 0.0127258 117.799 4.24047 117.799 9.44972C117.799 14.659 113.596 18.8867 108.362 18.8867C103.153 18.8867 98.9503 14.659 98.9503 9.44972ZM115.056 9.44972C115.056 5.72519 112.137 2.655 108.362 2.655C104.613 2.655 101.693 5.72519 101.693 9.44972C101.693 13.1743 104.612 16.219 108.362 16.219C112.137 16.219 115.056 13.1743 115.056 9.44972Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@ -1 +0,0 @@
|
||||
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M19.258 20.746l.049 13.82a.7.7 0 0 0 .697.698.687.687 0 0 0 .692-.693l-.048-13.924 13.924.049a.687.687 0 0 0 .692-.692.7.7 0 0 0-.698-.698l-13.82-.048-.048-13.83a.7.7 0 0 0-.697-.697.69.69 0 0 0-.692.693l.049 13.933-13.934-.048a.69.69 0 0 0-.692.692.7.7 0 0 0 .697.697l13.83.048z" stroke="#000" stroke-width="2" fill="#000" fill-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 423 B |
@ -1 +0,0 @@
|
||||
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M22.147 2C23.169 2 24 2.831 24 3.853h0v3.368c0 .542.298.988.798 1.195a1.257 1.257 0 0 0 1.41-.28h0l2.38-2.381c.701-.702 1.922-.7 2.622 0h0l3.035 3.035c.35.35.543.816.543 1.311s-.193.961-.543 1.311h0l-2.381 2.38c-.382.383-.487.91-.28 1.41.207.5.653.798 1.195.798h3.368c1.022 0 1.853.831 1.853 1.853h0v4.294A1.855 1.855 0 0 1 36.147 24h0-3.368c-.542 0-.989.298-1.196.798a1.26 1.26 0 0 0 .28 1.41h0l2.382 2.38c.35.35.542.816.542 1.31 0 .496-.192.962-.542 1.312h0l-3.036 3.035c-.7.7-1.92.702-2.622 0h0l-2.38-2.381a1.257 1.257 0 0 0-1.41-.28c-.5.207-.798.653-.798 1.195h0v3.368A1.855 1.855 0 0 1 22.146 38h0-4.293A1.855 1.855 0 0 1 16 36.147h0v-3.368c0-.542-.298-.988-.798-1.195a1.258 1.258 0 0 0-1.41.28h0l-2.38 2.381c-.701.702-1.921.701-2.622 0h0L5.755 31.21a1.844 1.844 0 0 1-.543-1.311c0-.495.193-.961.543-1.311h0l2.381-2.38c.382-.383.487-.91.28-1.41A1.257 1.257 0 0 0 7.221 24h0-3.368A1.855 1.855 0 0 1 2 22.146h0v-4.293C2 16.831 2.831 16 3.853 16h3.368c.542 0 .988-.298 1.195-.798a1.26 1.26 0 0 0-.28-1.41h0l-2.381-2.38a1.843 1.843 0 0 1-.543-1.31c0-.496.193-.962.543-1.312h0L8.79 5.755c.7-.7 1.92-.702 2.622 0h0l2.38 2.38c.383.382.911.49 1.41.281.5-.207.798-.653.798-1.195h0V3.853C16 2.831 16.831 2 17.853 2h0zm-.001 1.333h-4.293a.52.52 0 0 0-.52.52h0v3.368a2.584 2.584 0 0 1-1.621 2.426 2.586 2.586 0 0 1-2.863-.569h0l-2.38-2.38a.52.52 0 0 0-.736 0h0L6.697 9.732a.52.52 0 0 0 0 .736h0l2.382 2.38c.766.766.984 1.863.569 2.863a2.586 2.586 0 0 1-2.427 1.621h0-3.368a.52.52 0 0 0-.52.52h0v4.294c0 .286.234.52.52.52h3.368c1.083 0 2.013.621 2.427 1.62.415 1 .196 2.098-.57 2.863h0l-2.38 2.38a.52.52 0 0 0 0 .737h0l3.035 3.035a.52.52 0 0 0 .736 0h0l2.38-2.381a2.59 2.59 0 0 1 2.863-.569 2.586 2.586 0 0 1 1.621 2.427h0v3.368c0 .286.234.52.52.52h4.294a.52.52 0 0 0 .52-.52h0v-3.368c0-1.083.621-2.013 1.621-2.427 1-.413 2.097-.197 2.863.57h0l2.38 2.38a.52.52 0 0 0 .736 0h0l3.036-3.035a.52.52 0 0 0 0-.736h0l-2.382-2.38a2.585 2.585 0 0 1-.569-2.863 2.586 2.586 0 0 1 2.427-1.621h3.368a.52.52 0 0 0 .52-.52h0v-4.294a.52.52 0 0 0-.52-.519h0-3.368a2.586 2.586 0 0 1-2.427-1.621c-.415-1-.196-2.098.57-2.863h0l2.38-2.38a.52.52 0 0 0 0-.737h0l-3.035-3.035a.52.52 0 0 0-.736 0h0l-2.38 2.38a2.585 2.585 0 0 1-2.863.57 2.586 2.586 0 0 1-1.621-2.427h0V3.853a.52.52 0 0 0-.521-.52h0zM20 14c3.309 0 6 2.691 6 6s-2.691 6-6 6-6-2.691-6-6 2.691-6 6-6zm0 1.333A4.673 4.673 0 0 0 15.333 20 4.673 4.673 0 0 0 20 24.667 4.673 4.673 0 0 0 24.667 20 4.673 4.673 0 0 0 20 15.333z" stroke="#000" fill="#000" fill-rule="nonzero"/></svg>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
@ -1 +0,0 @@
|
||||
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" stroke="#000" stroke-width="2" fill="none"><path d="M3 9h34v22H3z"/><path d="M28.5 9H37v22h-8.5z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 195 B |