diff --git a/src/core/clients/opencga/api/Job.js b/src/core/clients/opencga/api/Job.js index bb88f18dd7..ffc0b066fd 100644 --- a/src/core/clients/opencga/api/Job.js +++ b/src/core/clients/opencga/api/Job.js @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. * WARNING: AUTOGENERATED CODE - * + * * This code was generated by a tool. - * + * * Manual changes to this file may cause unexpected behavior in your application. - * Manual changes to this file will be overwritten if the code is regenerated. + * Manual changes to this file will be overwritten if the code is regenerated. * **/ @@ -241,4 +241,4 @@ export default class Job extends OpenCGAParentClass { return this._get("jobs", job, "log", null, "tail", params); } -} \ No newline at end of file +} diff --git a/src/core/clients/opencga/api/Organization.js b/src/core/clients/opencga/api/Organization.js index 09b7942924..37cbe70cb8 100644 --- a/src/core/clients/opencga/api/Organization.js +++ b/src/core/clients/opencga/api/Organization.js @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. * WARNING: AUTOGENERATED CODE - * + * * This code was generated by a tool. - * + * * Manual changes to this file may cause unexpected behavior in your application. - * Manual changes to this file will be overwritten if the code is regenerated. + * Manual changes to this file will be overwritten if the code is regenerated. * **/ @@ -180,4 +180,4 @@ export default class Organization extends OpenCGAParentClass { return this._post("organizations", organization, null, null, "update", data, params); } -} \ No newline at end of file +} diff --git a/src/core/clients/opencga/api/Study.js b/src/core/clients/opencga/api/Study.js index bbe53e93df..c638ea47b8 100644 --- a/src/core/clients/opencga/api/Study.js +++ b/src/core/clients/opencga/api/Study.js @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. * WARNING: AUTOGENERATED CODE - * + * * This code was generated by a tool. - * + * * Manual changes to this file may cause unexpected behavior in your application. - * Manual changes to this file will be overwritten if the code is regenerated. + * Manual changes to this file will be overwritten if the code is regenerated. * **/ @@ -342,4 +342,4 @@ export default class Study extends OpenCGAParentClass { return this._post("studies", study, "variableSets", variableSet, "variables/update", data, params); } -} \ No newline at end of file +} diff --git a/src/core/clients/opencga/opencga-catalog-utils.js b/src/core/clients/opencga/opencga-catalog-utils.js index 2645c01ba1..745d55275e 100644 --- a/src/core/clients/opencga/opencga-catalog-utils.js +++ b/src/core/clients/opencga/opencga-catalog-utils.js @@ -89,26 +89,34 @@ export default class OpencgaCatalogUtils { return false; } - // Check if the user has the right the permissions in the study. - static isAdmin(study, userLogged) { - if (!study || !userLogged) { - console.error(`No valid parameters, study: ${study}, user: ${userLogged}`); + // Check if the provided user is admin in the organization + static isOrganizationAdmin(organization, userId) { + if (!organization || !userId) { return false; } - // Check if user is the Study owner - const studyOwner = study.fqn.split("@")[0]; - if (userLogged === studyOwner) { + // 1. Check if user is the organization admin + if (organization?.owner === userId) { return true; } else { - // Check if user is a Study admin, belongs to @admins group - const admins = study.groups.find(group => group.id === "@admins"); - if (admins.userIds.includes(userLogged)) { + // Check if user is an admin of the organization + if (organization?.admins?.includes?.(userId)) { return true; } } + // Other case, user is not admin of the organization return false; } + // Check if the user has the right the permissions in the study. + static isAdmin(study, userLogged) { + if (!study || !userLogged) { + console.error(`No valid parameters, study: ${study}, user: ${userLogged}`); + return false; + } + const admins = study.groups.find(group => group.id === "@admins"); + return !!admins.userIds.includes(userLogged); + } + // Check if the provided user is admin in the organization static isOrganizationAdmin(organization, userId) { if (!organization || !userId) { diff --git a/src/core/clients/opencga/opencga-parent-class.js b/src/core/clients/opencga/opencga-parent-class.js index ca670dc10c..bf24d279eb 100644 --- a/src/core/clients/opencga/opencga-parent-class.js +++ b/src/core/clients/opencga/opencga-parent-class.js @@ -33,11 +33,14 @@ export default class OpenCGAParentClass { _options.token = sid; } } - + // CAUTION Vero 2024-05-10: We believe this bit of code is useless. Temporarily commented out. + // In users endpoint, we cannot find GET method where the path param {user/users} should be autocompleted. + // When needed, they should be explicitly set. // If category == users and userId is not given, we try to set it - if (category1 === "users" && (ids1 === undefined || ids1 === null || ids1 === "")) { - ids1 = this._getUserId(); - } + // if (category1 === "users" && (ids1 === undefined || ids1 === null || ids1 === "")) { + // ids1 = this._getUserId(); + // } + let url = this._createRestUrl(host, version, category1, ids1, category2, ids2, action); // if (method === "GET") { url = this._addQueryParams(url, _params); diff --git a/src/sites/iva/conf/config.js b/src/sites/iva/conf/config.js index 07ddb1fc19..82dcb03b17 100644 --- a/src/sites/iva/conf/config.js +++ b/src/sites/iva/conf/config.js @@ -25,8 +25,8 @@ const hosts = [ url: "https://demo.app.zettagenomics.com/opencga" }, { - id: "xeta-110e", - url: "https://test.app.zettagenomics.com/xeta-110e/opencga" + id: "reference", + url: "https://test.app.zettagenomics.com/reference/opencga" }, ]; @@ -762,9 +762,18 @@ const SUITE = {
` }, menu: [ + { + id: "organization-admin", + name: "Organizations Admin", + fa_icon: "fas fa-file-invoice", + icon: "img/tools/icons/variant_browser.svg", + description: "", + visibility: "public", + featured: true, + }, { id: "study-admin", - name: "Study admin", + name: "Study Admin", fa_icon: "fas fa-file-invoice", icon: "img/tools/icons/variant_browser.svg", description: "", @@ -782,7 +791,7 @@ const SUITE = { // }, { id: "study-admin-iva", - name: "IVA configuration", + name: "IVA Configuration", fa_icon: "fas fa-file-invoice", icon: "img/tools/icons/variant_browser.svg", description: "", @@ -798,24 +807,33 @@ const SUITE = { // visibility: "public", // featured: false, // }, + // { + // id: "study-variant-admin", + // name: "Study Variant Admin", + // fa_icon: "fas fa-file-invoice", + // icon: "img/tools/icons/variant_browser.svg", + // description: "", + // visibility: "public", + // featured: true, + // }, { - id: "study-variant-admin", - name: "Study Variant Admin", - fa_icon: "fas fa-file-invoice", - icon: "img/tools/icons/variant_browser.svg", - description: "", - visibility: "public", - featured: true, - }, - { - id: "projects-admin", - name: "Project Manager", + id: "operations-admin", + name: "Operations Admin", fa_icon: "fas fa-file-invoice", icon: "img/tools/icons/variant_browser.svg", description: "", visibility: "public", featured: true, }, + // { + // id: "projects-admin", + // name: "Project Manager", + // fa_icon: "fas fa-file-invoice", + // icon: "img/tools/icons/variant_browser.svg", + // description: "", + // visibility: "public", + // featured: true, + // }, ], fileExplorer: { visibility: "private" diff --git a/src/sites/iva/iva-app.js b/src/sites/iva/iva-app.js index c57ceef876..a25a0aea52 100644 --- a/src/sites/iva/iva-app.js +++ b/src/sites/iva/iva-app.js @@ -77,11 +77,13 @@ import "../../webcomponents/clinical/clinical-analysis-create.js"; import "../../webcomponents/file/file-manager.js"; import "../../webcomponents/job/job-monitor.js"; import "../../webcomponents/loading-spinner.js"; +import "../../webcomponents/organization/admin/organization-admin.js"; import "../../webcomponents/project/projects-admin.js"; import "../../webcomponents/study/admin/study-admin.js"; import "../../webcomponents/study/admin/study-admin-iva.js"; import "../../webcomponents/study/admin/catalog-admin.js"; import "../../webcomponents/study/admin/variant/study-variant-admin.js"; +import "../../webcomponents/study/admin/variant/operations-admin.js"; import "../../webcomponents/user/user-profile.js"; import "../../webcomponents/api/rest-api.js"; import "../../webcomponents/note/note-browser.js"; @@ -224,13 +226,15 @@ class IvaApp extends LitElement { "diseasePanelUpdate", "clinicalAnalysis", // Admin + "organization-admin", "study-admin", "study-admin-iva", // "catalog-admin", + "operations-admin", "study-variant-admin", "opencga-admin", "variants-admin", - "projects-admin", + // "projects-admin", // REST-API "rest-api", // note @@ -1135,6 +1139,10 @@ class IvaApp extends LitElement { // } onSessionUpdateRequest() { + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { + title: "Refresh Session: Session Update Request", + message: "Session updated correctly", + }); this._createOpenCGASession(); } @@ -1979,13 +1987,15 @@ class IvaApp extends LitElement { ` : nothing} - ${this.config.enabledComponents["projects-admin"] ? html` - -
- +
+ - +
` : nothing} @@ -1998,7 +2008,7 @@ class IvaApp extends LitElement {
` : nothing} - ${this.config.enabledComponents["opencga-admin"] ? html` + ${this.config.enabledComponents["projects-admin"] ? html`
@@ -2023,6 +2032,7 @@ class IvaApp extends LitElement { ${this.config.enabledComponents["study-admin-iva"] ? html`
@@ -2030,13 +2040,14 @@ class IvaApp extends LitElement {
` : nothing} - ${this.config.enabledComponents["study-variant-admin"] ? html` + ${this.config.enabledComponents["operations-admin"] ? html`
- - +
` : nothing} diff --git a/src/webcomponents/api/opencga-rest-input.js b/src/webcomponents/api/opencga-rest-input.js index cc76f36e2f..296a819e9f 100644 --- a/src/webcomponents/api/opencga-rest-input.js +++ b/src/webcomponents/api/opencga-rest-input.js @@ -408,7 +408,8 @@ export default class OpencgaRestInput extends LitElement { #postEndpoint(url, isForm) { // Add Study - url += "study=" + encodeURIComponent(this.opencgaSession.study.fqn); + // Fixme: not all endpoints require study. E.g. POST /users/password/ + url += "study=" + encodeURIComponent(this.opencgaSession?.study?.fqn); // Replace PATH params this.endpoint.parameters diff --git a/src/webcomponents/cohort/cohort-grid.js b/src/webcomponents/cohort/cohort-grid.js index f9ac6808ea..6e33852c9c 100644 --- a/src/webcomponents/cohort/cohort-grid.js +++ b/src/webcomponents/cohort/cohort-grid.js @@ -346,6 +346,7 @@ export default class CohortGrid extends LitElement { id: "actions", title: "Actions", field: "actions", + align: "center", formatter: () => ` `, - align: "center", events: { "click a": this.onActionClick.bind(this), }, diff --git a/src/webcomponents/commons/catalog-grid-formatter.js b/src/webcomponents/commons/catalog-grid-formatter.js index 5de4d88496..019df35779 100644 --- a/src/webcomponents/commons/catalog-grid-formatter.js +++ b/src/webcomponents/commons/catalog-grid-formatter.js @@ -19,6 +19,16 @@ import BioinfoUtils from "../../core/bioinfo/bioinfo-utils.js"; export default class CatalogGridFormatter { + static userStatusFormatter(status, config) { + const _config = config || []; + const currentStatus = status.id || status.name || "UNDEFINED"; // Get current status + const displayCurrentStatus = _config.find(status => status.id === currentStatus); + return ` + + ${displayCurrentStatus.displayLabel} + + `; + } static sexFormatter(value, row) { let sexHtml = `${UtilsNew.isEmpty(row?.sex) ? "Not specified" : row.sex.id || row.sex}`; if (row?.karyotypicSex && row.karyotypicSex !== "UNKNOWN") { diff --git a/src/webcomponents/commons/filters/catalog-search-autocomplete.js b/src/webcomponents/commons/filters/catalog-search-autocomplete.js index 559c46428f..c8ce16fb62 100644 --- a/src/webcomponents/commons/filters/catalog-search-autocomplete.js +++ b/src/webcomponents/commons/filters/catalog-search-autocomplete.js @@ -82,7 +82,7 @@ export default class CatalogSearchAutocomplete extends LitElement { searchField: "id", placeholder: "project...", // client: this.opencgaSession.opencgaClient.projects(), - fetch: filters => this.opencgaSession.opencgaClient.projects().search(filters), + fetch: ({study, ...params}) => this.opencgaSession.opencgaClient.projects().search(params), fields: item => ({ "name": item.id, }), @@ -94,7 +94,7 @@ export default class CatalogSearchAutocomplete extends LitElement { searchField: "id", placeholder: "study...", // client: this.opencgaSession.opencgaClient.studies(), - fetch: filters => this.opencgaSession.opencgaClient.studies().search(filters), + fetch: ({study, ...params}) => this.opencgaSession.opencgaClient.studies().search(params), fields: item => ({ "name": item.id, }), @@ -286,6 +286,7 @@ export default class CatalogSearchAutocomplete extends LitElement { ...this.query || this.RESOURCES[this.resource].query, ...attr, }; + this.RESOURCES[this.resource].fetch(filters) .then(response => success(response)) .catch(error => failure(error)); diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js index 1d073699aa..09516c5ab6 100644 --- a/src/webcomponents/commons/forms/data-form.js +++ b/src/webcomponents/commons/forms/data-form.js @@ -816,7 +816,7 @@ export default class DataForm extends LitElement { ` : nothing} ${hasErrorMessages ? html`
-
+
@@ -837,7 +837,7 @@ export default class DataForm extends LitElement { const content = html`
${element.display?.icon ? html` - + ` : nothing} ${value || ""}
@@ -1352,7 +1352,8 @@ export default class DataForm extends LitElement { content = this._createImageElement(elem); break; case "custom": - content = elem.display?.render(this.getValue(elem.field, row)); + // content = elem.display?.render(this.getValue(elem.field, row)); + content = elem.display?.render(this.getValue(elem.field, row), value => this.onFilterChange(elem, value), this.updateParams, this.data, row); break; default: content = this.getValue(elem.field, row, this._getDefaultValue(element, section), elem.display); diff --git a/src/webcomponents/commons/layouts/custom-vertical-navbar.js b/src/webcomponents/commons/layouts/custom-vertical-navbar.js index aa88e9c141..cc837dfc53 100644 --- a/src/webcomponents/commons/layouts/custom-vertical-navbar.js +++ b/src/webcomponents/commons/layouts/custom-vertical-navbar.js @@ -35,6 +35,9 @@ export default class CustomVerticalNavBar extends LitElement { // --- PROPERTIES --- static get properties() { return { + organization: { + type: Object, + }, studyId: { type: String, }, @@ -80,7 +83,7 @@ export default class CustomVerticalNavBar extends LitElement { // --- LIT LIFECYCLE --- update(changedProperties) { - if (changedProperties.has("studyId") || changedProperties.has("opencgaSession")) { + if (changedProperties.has("studyId")) { this.studyIdObserver(); } if (changedProperties.has("activeMenuItem")) { @@ -99,11 +102,13 @@ export default class CustomVerticalNavBar extends LitElement { // --- OBSERVERS --- studyIdObserver() { - for (const project of this.opencgaSession?.projects) { - for (const study of project.studies) { - if (study.id === this.studyId || study.fqn === this.studyId) { - this.study = study; - break; + if (this.studyId && this.opencgaSession) { + for (const project of this.opencgaSession?.projects) { + for (const study of project.studies) { + if (study.id === this.studyId || study.fqn === this.studyId) { + this.study = study; + break; + } } } } @@ -301,7 +306,11 @@ export default class CustomVerticalNavBar extends LitElement {
- ${subItem.render(this.opencgaSession, this.study)} + ${ + (this.organization) ? + subItem.render(this.opencgaSession, this.organization) : + subItem.render(this.opencgaSession, this.study) + }
`) @@ -339,9 +348,7 @@ export default class CustomVerticalNavBar extends LitElement { // --- DEFAULT CONFIG --- getDefaultConfig() {} - } customElements.define("custom-vertical-navbar", CustomVerticalNavBar); - diff --git a/src/webcomponents/commons/opencb-grid-toolbar.js b/src/webcomponents/commons/opencb-grid-toolbar.js index a62a03a916..ed718950db 100644 --- a/src/webcomponents/commons/opencb-grid-toolbar.js +++ b/src/webcomponents/commons/opencb-grid-toolbar.js @@ -95,7 +95,9 @@ export default class OpencbGridToolbar extends LitElement { const action = e.currentTarget.dataset.action; switch (action) { case "create": - ModalUtils.show(`${this._prefix}CreateModal`); + this._config.create?.modalId ? + ModalUtils.show(this._config.create.modalId) : + ModalUtils.show(`${this._prefix}CreateModal`); break; case "export": ModalUtils.show(`${this._prefix}ExportModal`); @@ -114,6 +116,8 @@ export default class OpencbGridToolbar extends LitElement { rightButtons.push(rightButton.render()); } } + // Button create text + const buttonCreateText = this._settings?.buttonCreateText || "New..."; // Check 'Create' permissions let isCreateDisabled = false; @@ -154,7 +158,7 @@ export default class OpencbGridToolbar extends LitElement { ${isCreateDisabled ? html ` ` : html ` @@ -162,7 +166,7 @@ export default class OpencbGridToolbar extends LitElement { ${this._settings?.downloading === true ? html` ` : nothing} - New ... + ${buttonCreateText} `}
@@ -193,7 +197,7 @@ export default class OpencbGridToolbar extends LitElement { ${(this._config?.create && (this._settings.showCreate || this._settings.showNew) && OpencgaCatalogUtils.checkPermissions(this.opencgaSession?.study, this.opencgaSession?.user?.id, this.permissionID)) ? - ModalUtils.create(this, `${this._prefix}CreateModal`, this._config.create) : + ModalUtils.create(this, this._config.create?.modalId || `${this._prefix}CreateModal`, this._config.create) : nothing} ${this._settings?.showExport && this._config?.export ? ModalUtils.create(this, `${this._prefix}ExportModal`, this._config.export) : nothing} diff --git a/src/webcomponents/commons/utils/web-utils.js b/src/webcomponents/commons/utils/web-utils.js index 6c6bb74bff..1656f2941a 100644 --- a/src/webcomponents/commons/utils/web-utils.js +++ b/src/webcomponents/commons/utils/web-utils.js @@ -49,6 +49,10 @@ export default class WebUtils { "JOB": "JOBS", "FILE": "FILES", "CLINICAL_ANALYSIS": "CLINICAL_ANALYSIS", + "PROJECT": "PROJECTS", + "STUDY": "STUDIES", + "USER": "USERS", + "NOTE": "NOTE", }; return (resource && mapResourcePermissionId[resource] && mode) ? `${mode.toUpperCase()}_${mapResourcePermissionId[resource]}` : ""; } diff --git a/src/webcomponents/organization/admin/filters/user-status-filter.js b/src/webcomponents/organization/admin/filters/user-status-filter.js new file mode 100644 index 0000000000..0d2086c7aa --- /dev/null +++ b/src/webcomponents/organization/admin/filters/user-status-filter.js @@ -0,0 +1,85 @@ +/* + * Copyright 2015-2016 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {LitElement, html, nothing} from "lit"; +import "../../../commons/forms/select-field-filter.js"; +import UtilsNew from "../../../../core/utils-new.js"; +import LitUtils from "../../../commons/utils/lit-utils"; + +export default class UserStatusFilter extends LitElement { + + constructor() { + super(); + + // Set status and init private properties + this._init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + status: { + type: Object, + }, + config: { + type: Array + }, + disabled: { + type: Boolean + }, + }; + } + + _init() { + this.disabled = false; + } + + onFilterChange(e) { + LitUtils.dispatchCustomEvent(this, "filterChange", e.currentTarget.value); + } + + render() { + debugger + return html` +
+ ${this.config.map(status => html` + ${status.isSelectable ? html` + + + `: nothing} + `)} +
+ `; + } + +} + +customElements.define("user-status-filter", UserStatusFilter); diff --git a/src/webcomponents/organization/admin/group-admin-browser.js b/src/webcomponents/organization/admin/group-admin-browser.js new file mode 100644 index 0000000000..f2b9fce96f --- /dev/null +++ b/src/webcomponents/organization/admin/group-admin-browser.js @@ -0,0 +1,271 @@ +/** + * Copyright 2015-2024 OpenCB * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * The group admin browser component has two key responsibilities: + * 1. Retrieving groups per study + * 2. Rendering the graphic filters if enabled and grid. + * Accepted properties are: + * - A single study + * - An organization with multiple projects/studies + */ + +import {LitElement, html} from "lit"; +import LitUtils from "../../commons/utils/lit-utils"; +import UtilsNew from "../../../core/utils-new.js"; +import "./group-admin-grid.js"; + +export default class GroupAdminBrowser extends LitElement { + + /* ----------------------------------------------------------------------------------------------------------------- + CONSTRUCTOR AND PROPERTIES + ----------------------------------------------------------------------------------------------------------------- */ + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + studyId: { + type: String, + }, + study: { + type: Object, + }, + organizationId: { + type: String, + }, + organization: { + type: Object, + }, + opencgaSession: { + type: Object, + }, + // QUESTION: pending to decide if we allow browser settings here. + settings: { + type: Object, + }, + }; + } + + /* ----------------------------------------------------------------------------------------------------------------- + PRIVATE METHODS + ----------------------------------------------------------------------------------------------------------------- */ + #init() { + this.COMPONENT_ID = "groups-admin-browser"; + this._groups = []; + this._studies = []; + this._study = {}; + this._config = this.getDefaultConfig(); + this.isLoading = false; + } + + #setLoading(value) { + this.isLoading = value; + this.requestUpdate(); + } + + #prepareGroups(groups, project, study) { + /* + if (this.study) { + this._groups = []; + this._studies = [this.opencgaSession.study]; + this.study.groups?.forEach(group => { + const newGroup = { + studyId: this.study.id, + fqn: this.study.fqn, + group: group, + isGroupProtected: !!(group.id === "@admins" || group.id === "@members"), + }; + this._groups.push(newGroup); + }); + } + */ + // Get all study groups + const p = project || this.opencgaSession.project; + const s = study || this.opencgaSession.study; + + this._studies.push({ + projectId: p.id, + fqn: s.fqn, + name: s.alias, + }); + this._groups = groups.map(group => ({ + ...group, + fqn: s.fqn, + studyId: s.id, + projectId: p.id, + isProtected: !!(group.id === "@admins" || group.id === "@members"), + })); + } + + /* ----------------------------------------------------------------------------------------------------------------- + LIT LIFE-CYCLE + ----------------------------------------------------------------------------------------------------------------- */ + update(changedProperties) { + if (changedProperties.has("organizationId")) { + this.organizationIdObserver(); + } + if (changedProperties.has("organization")) { + this.organizationObserver(); + } + if (changedProperties.has("studyId")) { + this.studyIdObserver(); + } + if (changedProperties.has("study")) { + this.studyObserver(); + } + if (changedProperties.has("settings")) { + this.settingsObserver(); + } + super.update(changedProperties); + } + + /* ----------------------------------------------------------------------------------------------------------------- + OBSERVERS + ----------------------------------------------------------------------------------------------------------------- */ + organizationObserver() { + // Get all organization groups + if (this.organization) { + this._groups = []; + this._studies = []; + this.organization?.projects?.forEach(project => { + project.studies?.forEach(study => { + this.#prepareGroups(study.groups, project, study); + }); + }); + } + } + + organizationIdObserver() { + if (this.organizationId && this.opencgaSession) { + let error; + this.#setLoading(true); + this.opencgaSession.opencgaClient.organization() + .info(this.organizationId) + .then(response => { + this.organization = UtilsNew.objectClone(response.responses[0].results[0]); + }) + .catch(reason => { + this.organization = {}; + error = reason; + console.error(reason); + }) + .finally(() => { + this._config = this.getDefaultConfig(); + LitUtils.dispatchCustomEvent(this, "organizationInfo", this.organization, {}, error); + this.#setLoading(false); + }); + } + } + + studyObserver() { + if (this.study) { + let error; + this.#setLoading(true); + this.opencgaSession.opencgaClient.studies() + .groups(this.study.fqn) + .then(response => { + const groups = response.responses[0].results; + this.#prepareGroups(groups); + }) + .catch(reason => { + error = reason; + console.error(reason); + }) + .finally(() => { + LitUtils.dispatchCustomEvent(this, "studyChange", this.study, {}, error); + this.#setLoading(false); + }); + } + } + + studyIdObserver() { + if (this.studyId && this.opencgaSession) { + let error; + this.#setLoading(true); + this.opencgaSession.opencgaClient.studies() + .info(this.studyId) + .then(response => { + this.study = UtilsNew.objectClone(response.responses[0].results[0]); + }) + .catch(reason => { + error = reason; + console.error(reason); + }) + .finally(() => { + LitUtils.dispatchCustomEvent(this, "studyChange", this.study, {}, error); + this.#setLoading(false); + }); + } + } + + settingsObserver() { + this._config = { + ...this.getDefaultConfig(), + ...this.settings, + }; + } + + /* ----------------------------------------------------------------------------------------------------------------- + RENDER + ----------------------------------------------------------------------------------------------------------------- */ + renderFilterGraphics() { + if (this._config.showGraphicFilters) { + return html ` + + `; + } + } + + render() { + if (!this.opencgaSession) { + return html`
Not valid session
`; + } + + if (this._groups.length > 0 && this._studies.length > 0) { + return html ` + + ${this.renderFilterGraphics()} + + + + `; + } + } + + /* ----------------------------------------------------------------------------------------------------------------- + DEFAULT CONFIG + ----------------------------------------------------------------------------------------------------------------- */ + getDefaultConfig() { + return { + showGraphicFilters: false, + }; + } + +} + +customElements.define("group-admin-browser", GroupAdminBrowser); diff --git a/src/webcomponents/organization/admin/group-admin-create.js b/src/webcomponents/organization/admin/group-admin-create.js new file mode 100644 index 0000000000..8dbff9f051 --- /dev/null +++ b/src/webcomponents/organization/admin/group-admin-create.js @@ -0,0 +1,249 @@ +/** + * Copyright 2015-2024 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {LitElement, html} from "lit"; +import LitUtils from "../../commons/utils/lit-utils.js"; +import NotificationUtils from "../../commons/utils/notification-utils.js"; +import UtilsNew from "../../../core/utils-new"; + +export default class GroupAdminCreate extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + studyFqn: { + type: String, + }, + studies: { + type: Array, + }, + opencgaSession: { + type: Object, + }, + displayConfig: { + type: Object, + }, + }; + } + + #init() { + this.isLoading = false; + this.displayConfigDefault = { + style: "margin: 10px", + titleWidth: 3, + defaultLayout: "horizontal", + buttonOkText: "Create" + }; + this._config = this.getDefaultConfig(); + } + + #initOriginalObjects() { + this.group = {}; + this.allowedValues = []; + if (this.studies && this.opencgaSession && Array.isArray(this.studies)) { + if (this.studies.length === 1) { + this.group.listStudies = [this.studies[0]]; + } else { + // 1. Prepare structure for displaying studies per project in dropdown + const projects = this.studies.reduce((acc, {fqn, name, projectId}) => { + const study = {fqn, name}; + const item = acc.find(y => y.projectId === projectId); + (item) ? item.studies.push(study) : + acc.push({projectId: projectId, studies: [study]}); + return acc; + }, []); + // 2. Fill allowed values + this.allowedValues = projects + .filter(({studies}) => studies.length > 0) + .map(({projectId, studies}) => ({ + name: `Project '${projectId}'`, + fields: studies.map(({fqn, name}) => ({id: fqn, name})) + })); + } + } + this._config = this.getDefaultConfig(); + } + + #setLoading(value) { + this.isLoading = value; + this.requestUpdate(); + } + + update(changedProperties) { + if (changedProperties.has("studyFqn") || + changedProperties.has("opencgaSession")) { + this.studyFqnObserver(); + } + if (changedProperties.has("studies") || + changedProperties.has("opencgaSession")) { + this.#initOriginalObjects(); + } + if (changedProperties.has("displayConfig")) { + this.displayConfig = { + ...this.displayConfigDefault, + ...this.displayConfig + }; + this._config = this.getDefaultConfig(); + } + super.update(changedProperties); + } + + studyFqnObserver() { + if (this.studyFqn && this.opencgaSession) { + let error; + this.#setLoading(true); + this.opencgaSession.opencgaClient.studies() + .info(this.studyFqn) + .then(response => { + this.study = UtilsNew.objectClone(response.responses[0].results[0]); + this.studies = [this.study]; + }) + .catch(reason => { + error = reason; + console.error(reason); + }) + .finally(() => { + LitUtils.dispatchCustomEvent(this, "studyChange", this.study, {}, error); + this.#setLoading(false); + }); + } + } + + onFieldChange(e, field) { + const param = field || e.detail.param; + // 1. Update group id + if (param === "id") { + // QUESTION 20240325 Vero: verify group name starts with @? + this.group.id = e.detail.data.id; + } + // 2. Update the list of studies + if (param === "fqn") { + this.group.listStudies = e.detail.value?.length > 0 ? e.detail.value?.split(",") : []; + } + this.requestUpdate(); + } + + onClear() { + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, { + title: "Clear group", + message: "Are you sure to clear?", + ok: () => { + this.#initOriginalObjects(); + this.requestUpdate(); + }, + }); + } + + onSubmit() { + const params = { + includeResult: true, + action: "ADD", + }; + this.#setLoading(true); + const groupPromises = (this.group.listStudies || []) + .map(study => { + let error; + return this.opencgaSession.opencgaClient.studies() + .updateGroups(study.fqn, {id: this.group.id}, params) + .then(() => { + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { + title: `Group Create`, + message: `Group ${this.group.id} in study ${study.fqn} CREATED successfully`, + }); + }) + .catch(reason => { + error = reason; + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason); + }) + .finally(() => { + LitUtils.dispatchCustomEvent(this, "groupCreate", {}, { + group: this.group, + studyFqn: study, + }, error); + }); + }); + + Promise.all(groupPromises) + .finally(() => { + this.#setLoading(false); + this.#initOriginalObjects(); + LitUtils.dispatchCustomEvent(this, "studyUpdateRequest", {}); + }); + } + + render() { + if (this.isLoading) { + return html``; + } + + return html` + + `; + } + + getDefaultConfig() { + return { + display: this.displayConfig || this.displayConfigDefault, + sections: [ + { + // title: "General Information", + elements: [ + { + title: "Group ID", + field: "id", + type: "input-text", + required: true, + display: { + placeholder: "Add a short ID...", + helpMessage: `The group ID must start with the character '@' [e.g.'@myNewGroup'].`, + }, + }, + { + title: "Study", + field: "fqn", + type: "select", + multiple: true, + all: true, + required: true, + allowedValues: this.allowedValues, + display: { + visible: !!this.allowedValues?.length, + placeholder: "Select study or studies..." + }, + }, + ], + }, + ], + }; + } + +} + +customElements.define("group-admin-create", GroupAdminCreate); diff --git a/src/webcomponents/organization/admin/group-admin-delete.js b/src/webcomponents/organization/admin/group-admin-delete.js new file mode 100644 index 0000000000..9083eb7124 --- /dev/null +++ b/src/webcomponents/organization/admin/group-admin-delete.js @@ -0,0 +1,160 @@ +/** + * Copyright 2015-2024 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {LitElement, html} from "lit"; +import LitUtils from "../../commons/utils/lit-utils.js"; +import NotificationUtils from "../../commons/utils/notification-utils.js"; + +export default class GroupAdminDelete extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + group: { + type: Object, + }, + active: { + type: Boolean, + }, + opencgaSession: { + type: Object, + }, + displayConfig: { + type: Object, + }, + }; + } + + #init() { + this.isLoading = false; + this.displayConfigDefault = { + style: "margin: 10px", + titleWidth: 3, + defaultLayout: "horizontal", + buttonOkText: "Delete", + }; + this._config = this.getDefaultConfig(); + } + + #setLoading(value) { + this.isLoading = value; + this.requestUpdate(); + } + + update(changedProperties) { + if (changedProperties.has("displayConfig")) { + this.displayConfig = { + ...this.displayConfigDefault, + ...this.displayConfig, + }; + this._config = this.getDefaultConfig(); + } + super.update(changedProperties); + } + + onSubmit() { + this.#setLoading(true); + let error; + return this.opencgaSession.opencgaClient.studies() + .updateGroups(this.group.fqn, {id: this.group.id}, {action: "REMOVE"}) + .then(() => { + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { + title: `Group Delete`, + message: `Group ${this.group.id} in study ${this.group.fqn} deleted successfully`, + }); + LitUtils.dispatchCustomEvent(this, "groupDelete", {}); + LitUtils.dispatchCustomEvent(this, "studyUpdateRequest", {}); + }) + .catch(reason => { + error = reason; + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason); + }) + .finally(() => { + this.#setLoading(false); + }); + } + + render() { + if (this.isLoading) { + return html``; + } + + return html` + + + `; + } + + getDefaultConfig() { + return { + display: this.displayConfig || this.displayConfigDefault, + sections: [ + { + // title: `Are you sure you want to remove group '${this.group?.id}'?`, + elements: [ + { + type: "notification", + text: "The following users could have unexpected permissions if you remove this group", + display: { + visible: true, + icon: "fas fa-exclamation-triangle", + notificationType: "error", + }, + }, + { + // name: "UserIds", + field: "users", + type: "list", + display: { + separator: " ", + contentLayout: "bullets", + transform: users => users.length ? + users.map(user => ({userId: user.id})) : + [{userId: "This group does not have users"}], + template: "${userId}", + // FIXME: why is not working? + // className: { + // "userId": "badge badge-pill badge-primary", + // }, + // style: { + // "userId": { + // "color": "white", + // "background-color": "blue" + // }, + // } + }, + }, + ], + }, + ], + }; + } + +} + +customElements.define("group-admin-delete", GroupAdminDelete); diff --git a/src/webcomponents/organization/admin/group-admin-grid.js b/src/webcomponents/organization/admin/group-admin-grid.js new file mode 100644 index 0000000000..e63fdb0880 --- /dev/null +++ b/src/webcomponents/organization/admin/group-admin-grid.js @@ -0,0 +1,413 @@ +/** + * Copyright 2015-2024 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {LitElement, html, nothing} from "lit"; +import GridCommons from "../../commons/grid-commons.js"; +import UtilsNew from "../../../core/utils-new.js"; +import ModalUtils from "../../commons/modal/modal-utils.js"; +import CatalogGridFormatter from "../../commons/catalog-grid-formatter.js"; +import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils.js"; +import "./group-admin-create.js"; +import "./group-admin-permissions-update.js"; +import "./group-admin-delete.js"; + +export default class GroupAdminGrid extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + toolId: { + type: String, + }, + opencgaSession: { + type: Object, + }, + groups: { + type: Array, + }, + studies: { + type: Object, + }, + config: { + type: Object, + }, + active: { + type: Boolean, + }, + }; + } + + #init() { + this.COMPONENT_ID = "group-grid"; + this._prefix = UtilsNew.randomString(8); + this.gridId = this._prefix + this.COMPONENT_ID; + this.active = true; + this._config = this.getDefaultConfig(); + this.action = ""; + this.studyFqn = ""; + } + + update(changedProperties) { + if (changedProperties.has("opencgaSession") || + changedProperties.has("groups") || + changedProperties.has("toolId") || + changedProperties.has("studies") || + changedProperties.has("config")) { + this.propertyObserver(); + } + super.update(changedProperties); + } + + updated(changedProperties) { + if (changedProperties.size > 0 && this.active) { + if (this.groups?.length > 0) { + this.renderLocalTable(); + } + } + } + + propertyObserver() { + // With each property change we must be updated config and create the columns again. No extra checks are needed. + this._config = { + ...this.getDefaultConfig(), + ...this.config, + }; + + this.gridCommons = new GridCommons(this.gridId, this, this._config); + + // Config for the grid toolbar + this.toolbarSetting = { + ...this._config, + }; + + this.toolbarConfig = { + toolId: this.toolId, + resource: "GROUPS", + columns: this._getDefaultColumns(), + create: { + display: { + modalTitle: "Group Create", + modalDraggable: true, + modalCyDataName: "modal-create", + modalSize: "modal-lg" + }, + render: () => html ` + + + `, + }, + }; + + this.permissions = { + "organization": () => OpencgaCatalogUtils.isOrganizationAdmin(this.organization, this.opencgaSession.user.id) ? "" : "disabled", + "study": () => OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id) ? "" : "disabled", + }; + + this.modals = { + /* + "edit-details": { + label: "Edit Details", + icon: "fas fa-edit", + modalId: `${this._prefix}UpdateDetailsModal`, + render: () => this.renderModalDetailsUpdate(), + permission: OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id) || "disabled", + }, + */ + "edit-permissions": { + label: "Edit Permissions", + icon: "far fa-edit", + modalId: `${this._prefix}UpdatePermissionsModal`, + render: () => this.renderModalPermissionsUpdate(), + permission: this.permissions["study"](), + divider: true, + }, + "delete": { + label: "Delete Group", + icon: "far fa-trash-alt ", + color: "text-danger", + modalId: `${this._prefix}DeleteModal`, + render: () => this.renderModalDelete(), + permission: this.permissions["study"](), + }, + }; + } + + renderLocalTable() { + this.table = $("#" + this.gridId); + this.table.bootstrapTable("destroy"); + this.table.bootstrapTable({ + theadClasses: "table-light", + buttonsClass: "light", + columns: this._getDefaultColumns(), + sidePagination: "server", + // JoseMi Note 2024-01-18: we have added the ajax function for local variants also to support executing + // async calls when getting additional data from columns extensions. + ajax: params => { + const tableOptions = $(this.table).bootstrapTable("getOptions"); + const limit = params.data.limit || tableOptions.pageSize; + const skip = params.data.offset || 0; + const rows = this.groups.slice(skip, skip + limit); + + // Get data for extensions + this.gridCommons.prepareDataForExtensions(this.COMPONENT_ID, this.opencgaSession, null, rows) + .then(() => params.success(rows)) + .catch(error => params.error(error)); + }, + // JoseMi Note 2024-01-18: we use this method to tell bootstrap-table how many rows we have in our data + responseHandler: response => { + return { + total: this.groups.length, + rows: response, + }; + }, + iconsPrefix: GridCommons.GRID_ICONS_PREFIX, + icons: GridCommons.GRID_ICONS, + + // Set table properties, these are read from config property + uniqueId: "id", + pagination: this._config.pagination, + pageSize: this._config.pageSize, + pageList: this._config.pageList, + detailView: this._config.detailView, + loadingTemplate: () => GridCommons.loadingFormatter(), + }); + } + + _getDefaultColumns() { + this._columns = [ + { + title: "Group ID", + field: "id", + visible: this.gridCommons.isColumnVisible("group.id"), + formatter: (value, row) => this.groupIdFormatter(value, row), + }, + { + title: "Study ID", + field: "studyId", + visible: this.gridCommons.isColumnVisible("studyId") + }, + { + title: "Project ID", + field: "projectId", + visible: this.gridCommons.isColumnVisible("projectId") + }, + { + title: "No.Users", + field: "users", + formatter: (value, row) => this.groupNoUsersFormatter(value, row), + }, + { + title: "Users IDs", + field: "users", + formatter: (value, row) => this.groupUsersFormatter(value, row), + }, + ]; + + if (this._config.annotations?.length > 0) { + this.gridCommons.addColumnsFromAnnotations(this._columns, CatalogGridFormatter.customAnnotationFormatter, this._config); + } + + if (this.opencgaSession && this._config.showActions) { + this._columns.push({ + id: "actions", + title: "Actions", + field: "actions", + align: "center", + formatter: (value, row) => ` + + `, + events: { + "click ul>li>a": (e, value, row) => this.onActionClick(e, value, row), + }, + }); + } + + this._columns = this.gridCommons.addColumnsFromExtensions(this._columns, this.COMPONENT_ID); + return this._columns; + } + + // *** FORMATTERS *** + groupIdFormatter(value, row) { + return row.isProtected ? ` +
+ ${value} + PROTECTED +
+ ` : ` +
${value}
+ `; + } + + groupNoUsersFormatter(value) { + return ` + ${value.length} + `; + } + + groupUsersFormatter(value) { + return value.map(user => ` + ${user.id} + `); + } + + // *** EVENTS *** + async onActionClick(e, value, row) { + this.action = e.currentTarget.dataset.action; + this.groupId = row.id; + this.group = this.groups.find(g=> g.id === this.groupId); + this.studyFqn = row.fqn; + this.requestUpdate(); + await this.updateComplete; + ModalUtils.show(this.modals[this.action]["modalId"]); + } + + onGroupEvent(e, id) { + this.studyFqn = e.detail.studyFqn; + ModalUtils.close(id); + } + + // *** RENDER METHODS *** + renderModalPermissionsUpdate() { + return ModalUtils.create(this, `${this._prefix}UpdatePermissionsModal`, { + display: { + modalTitle: `Permissions Update: Group ${this.groupId} in Study ${this.studyFqn}`, + modalDraggable: true, + modalCyDataName: "modal-update", + modalSize: "modal-lg" + }, + render: active => html` + + + `, + }); + } + + renderModalDelete() { + return ModalUtils.create(this, `${this._prefix}DeleteModal`, { + display: { + // modalTitle: `Group Delete: ${this.group?.id} in study ${this.studyFqn}`, + modalTitle: `Are you sure you want to remove group '${this.groupId}' in study '${this.studyFqn}'?`, + modalDraggable: true, + modalCyDataName: "modal-update", + modalSize: "modal-lg" + }, + render: active => html` + + + `, + }); + } + + renderToolbar() { + if (this._config.showToolbar) { + // @groupCreate="${e => this.onGroupEvent(e, `${this._prefix}Modal`)}" + return html ` + + + `; + } + } + + render() { + return html` + + ${this.renderToolbar()} + +
+
+
+ + ${this.action ? this.modals[this.action]["render"](): nothing} + `; + } + + // *** DEFAULT CONFIG *** + getDefaultConfig() { + return { + pagination: true, + pageSize: 10, + pageList: [5, 10, 25], + multiSelection: false, + showSelectCheckbox: false, + + showToolbar: true, + showActions: true, + + showCreate: true, + showExport: false, + showSettings: false, + exportTabs: ["download", "link", "code"], + }; + } + +} + +customElements.define("group-admin-grid", GroupAdminGrid); diff --git a/src/webcomponents/organization/admin/group-admin-permissions-update.js b/src/webcomponents/organization/admin/group-admin-permissions-update.js new file mode 100644 index 0000000000..f985aef79c --- /dev/null +++ b/src/webcomponents/organization/admin/group-admin-permissions-update.js @@ -0,0 +1,346 @@ +/** + * Copyright 2015-2024 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {html, LitElement} from "lit"; +import UtilsNew from "../../../core/utils-new.js"; +import LitUtils from "../../commons/utils/lit-utils.js"; +import FormUtils from "../../commons/forms/form-utils"; +import NotificationUtils from "../../commons/utils/notification-utils"; + +export default class GroupAdminPermissionsUpdate extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + groupId: { + type: String + }, + studyFqn: { + type: String, + }, + active: { + type: Boolean, + }, + opencgaSession: { + type: Object + }, + displayConfig: { + type: Object + }, + }; + } + + #init() { + this.displayConfig = {}; + this.updatedFields = {}; + + this.permissions = {}; // Original permissions + this._permissions = {}; // Updated permissions + + this.templates = { + "custom": { + + }, + "analyst": { + descriptionShort: "Full READ and WRITE (not DELETE) permissions", + description: `The member (user or group) will be given full READ and WRITE (not DELETE) permissions + for all the entries related to the study. These users will be able to view and do modifications on + all the data that is related to the study.`, + permissions: [ + "" + ], + }, + "view_only": { + descriptionShort: "Full READ permissions", + description: "The member (user or group) will be given full READ permissions.", + permissions: [ + "VIEW_SAMPLES", + "VIEW_SAMPLE_ANNOTATIONS", + "VIEW_AGGREGATED_VARIANTS", + "VIEW_SAMPLE_VARIANTS", + "VIEW_INDIVIDUALS", + "VIEW_INDIVIDUAL_ANNOTATIONS", + "VIEW_FAMILIES", + "VIEW_FAMILY_ANNOTATIONS", + "VIEW_COHORTS", + "VIEW_COHORT_ANNOTATIONS", + "VIEW_FILES", + "VIEW_FILE_HEADER", + "VIEW_FILE_CONTENT", + "DOWNLOAD_FILES", + "VIEW_JOBS", + "EXECUTE_JOBS", + "VIEW_PANELS", + "VIEW_CLINICAL_ANALYSIS", + ], + }, + }; + + this.displayConfigDefault = { + style: "margin: 10px", + defaultLayout: "horizontal", + labelAlign: "right", + labelWidth: 3, + buttonOkText: "Update", + }; + this._config = this.getDefaultConfig(); + } + + #setLoading(value) { + this.isLoading = value; + this.requestUpdate(); + } + + #initConfigNotification() { + this._config.notification = { + title: "", + text: "Some changes have been done in the form. Not saved changes will be lost", + type: "notification", + display: { + visible: () => { + return UtilsNew.isNotEmpty(this.updatedFields); + }, + notificationType: "warning", + }, + }; + } + + #initPermissions() { + // 1. Group contains params: (a) id: e.g. "@admins", (b) userIds: e.g. ["test"] + // this.group = this._study.groups.find(group => group.id === this.groupId); + // 2. In the update form, we need to manage as well the permissions of this group. + // Retrieve ACL permissions. Check if this study group has acl + // const groupPermissions = this._study?.acl + // ?.find(acl => acl.member === this.opencgaSession.user.id)?.groups + // ?.find(group => group.id === this.group.id)?.permissions || []; + // 3. Add current permissions and template key to the object group + // this.group = { + // permissions: groupPermissions, + // template: "", + // }; + // this.initOriginalObjects(); + this.permissions = { + default: UtilsNew.objectClone(this.permissions.acl[0].permissions), + custom: UtilsNew.objectClone(this.permissions.acl[0].permissions), + templates: this.templates.keys(), + }; + this._permissions = UtilsNew.objectClone(this.permissions), + this.updatedFields = {}; + } + + initOriginalObjects() { + this._permissions = UtilsNew.objectClone(this.permissions); + this.updatedFields = {}; + } + + update(changedProperties) { + if ((changedProperties.has("groupId") || (changedProperties.has("studyId")) && this.active)) { + this.groupIdObserver(); + } + if (changedProperties.has("displayConfig")) { + this._config = this.getDefaultConfig(); + if (!this._config?.notification) { + // this.#initConfigNotification(); + } + } + super.update(changedProperties); + } + + groupIdObserver() { + if (this.groupId && this.studyId && this.opencgaSession) { + const params = { + member: this.groupId, + }; + let error; + this.#setLoading(true); + this.opencgaSession.opencgaClient.studies() + .acl(this.studyId, params) + .then(response => { + this.#initPermissions(UtilsNew.objectClone(response.responses[0].results[0])); + }) + .catch(reason => { + error = reason; + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason); + }) + .finally(() => { + LitUtils.dispatchCustomEvent(this, "groupAclInfo", this.study, {}, error); + this.#setLoading(false); + }); + } + } + + // Uncomment to post-process data-form manipulation + // onFieldChange(e) { + // debugger + // this.updatedFields = e.detail?.updatedFields || {}; + // this.requestUpdate(); + // } + + onFieldChange(e) { + const param = e.detail.param; + this.updatedFields = FormUtils.getUpdatedFields( + this.group, + this.updatedFields, + param, + e.detail.value, + e.detail.action); + if (param === "template") { + this._group.template = e.detail.value; + } + this.requestUpdate(); + } + + onClear() { + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, { + title: "Discard changes", + message: "Are you sure you want to discard the changes made?", + ok: () => { + this.initOriginalObjects(); + this.requestUpdate(); + // We need to dispatch a component clear event + LitUtils.dispatchCustomEvent(this, "groupClear", null, { + group: this._group, + }); + }, + }); + } + + onSubmit() { + const paramsAction = { + action: "SET" + }; + const studyAclParams = { + study: this.studyId, + template: this._group.template, + // permissions: this._group.permissions, + }; + let error; + this.#setLoading(true); + this.opencgaSession.opencgaClient.studies() + .updateAcl(this.groupId, paramsAction, studyAclParams) + .then(response => { + this.group = UtilsNew.objectClone(response.responses[0].results[0]); + this.updatedFields = {}; + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { + title: `Group Update`, + message: `Group ${this.group.id} updated correctly`, + }); + }) + .catch(reason => { + error = reason; + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason); + }) + .finally(() => { + LitUtils.dispatchCustomEvent(this, "groupUpdate", { + group: this.group, + studyFqn: this.studyFqn, + }, error); + this.#setLoading(false); + }); + } + + render() { + return html ` + + + `; + } + + getDefaultConfig() { + return { + icon: "fas fa-edit", + buttons: { + clearText: "Discard Changes", + okText: "Update", + }, + display: this.displayConfig, + sections: [ + { + title: "Permissions", + display: { + titleVisible: false, + }, + elements: [ + { + title: "Templates", + field: "templates", + type: "toggle-buttons", + allowedValues: Object.keys(this.templates).map(name => name.toUpperCase()), + }, + // TODO: Implement customised permissions for the group + /* + { + title: "Permissions", + field: "permissions", + type: "", + }, + */ + ], + }, + /* + { + title: "Users", + elements: [ + { + field: "users", + type: "custom", + display: { + layout: "vertical", + defaultLayout: "vertical", + width: 12, + style: "padding-left: 0px", + render: family => { + if (family && family.members) { + const individualGridConfig = { + showSelectCheckbox: false, + showToolbar: false + }; + return html` + + + `; + } + }, + }, + } + ], + }, + */ + ], + }; + } + +} + +customElements.define("group-admin-permissions-update", GroupAdminPermissionsUpdate); diff --git a/src/webcomponents/organization/admin/organization-admin-audit.js b/src/webcomponents/organization/admin/organization-admin-audit.js new file mode 100644 index 0000000000..ea55c26218 --- /dev/null +++ b/src/webcomponents/organization/admin/organization-admin-audit.js @@ -0,0 +1,410 @@ +/** + * Copyright 2015-2019 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {html, LitElement, nothing} from "lit"; +import UtilsNew from "../../../core/utils-new.js"; +import GridCommons from "../../commons/grid-commons.js"; +import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils.js"; +import {guardPage} from "../../commons/html-utils.js"; + +export default class OrganizationAdminAudit extends LitElement { + + constructor() { + super(); + + // Set status and init private properties + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + opencgaSession: { + type: Object + }, + studyId: { + type: String + }, + study: { + type: Object + }, + query: { + type: Object + }, + config: { + type: Object + } + }; + } + + #init() { + this._prefix = UtilsNew.randomString(8); + + this._filters = []; + this.query = {}; + this.sortedUserIds = []; + this.gridId = this._prefix + "AuditBrowserGrid"; + this.actionValues = ["SEARCH", "LINK", "INFO", "CREATE"]; + this.resourceTypeValues = ["AUDIT", "USER", "PROJECT", "STUDY", "FILE", "SAMPLE", "JOB", "INDIVIDUAL", "COHORT", "DISEASE_PANEL", + "FAMILY", "CLINICAL_ANALYSIS", "INTERPRETATION", "VARIANT", "ALIGNMENT", "CLINICAL", "EXPRESSION", "FUNCTIONAL"]; + this.statusTypeValues = ["SUCCESS", "ERROR"]; + } + + connectedCallback() { + super.connectedCallback(); + + this._config = {...this.getDefaultConfig(), ...this.config}; + this.gridCommons = new GridCommons(this.gridId, this, this._config); + } + + update(changedProperties) { + if (changedProperties.has("studyId")) { + for (const project of this.opencgaSession.projects) { + for (const study of project.studies) { + if (study.id === this.studyId || study.fqn === this.studyId) { + this.study = {...study}; + break; + } + } + } + } + + if (changedProperties.has("study")) { + this.studyObserver(); + } + if (changedProperties.has("query")) { + this.propertyObserver(); + } + super.update(changedProperties); + } + + async studyObserver() { + this.groupsMap = new Map(); + try { + const resp = await this.opencgaSession.opencgaClient.studies() + .groups(this.study.fqn); + const groups = resp.responses[0].results; + if (groups[0].users) { + for (const group of groups) { + this.groupsMap.set(group.id, group.users); + } + } else { + for (const group of response.responses[0].results) { + this.groupsMap.set(group.id, group.userIds.map(u => { + return {id: u, name: u}; + })); + } + } + this.users = this.groupsMap.get("@members"); + this.sortedUserIds = [...this.groupsMap.get("@members").map(user => user.id).sort()]; + // With the requestUpdate, work to get users for the filter + this.requestUpdate(); + } catch (err) { + console.log("An error occurred fetching users: ", err); + } + this.renderRemoteTable(); + } + + propertyObserver() { + this.renderRemoteTable(); + } + + renderRemoteTable() { + if (this.opencgaSession?.opencgaClient && this.study) { + this.table = $("#" + this.gridId); + this.table.bootstrapTable("destroy"); + this.table.bootstrapTable({ + theadClasses: "table-light", + buttonsClass: "light", + columns: this._getDefaultColumns(), + method: "get", + sidePagination: "server", + iconsPrefix: GridCommons.GRID_ICONS_PREFIX, + icons: GridCommons.GRID_ICONS, + uniqueId: "id", + // Table properties + pagination: this._config.pagination, + pageSize: this._config.pageSize, + pageList: this._config.pageList, + // paginationVAlign: "both", + formatShowingRows: this.gridCommons.formatShowingRows, + showExport: this._config.showExport, + detailView: this._config.detailView, + detailFormatter: this.detailFormatter, + gridContext: this, + // formatLoadingMessage: () => "
", + loadingTemplate: () => GridCommons.loadingFormatter(), + ajax: params => { + const query = { + study: this.study.fqn, + limit: params.data.limit, + skip: params.data.offset || 0, + count: !this.table.bootstrapTable("getOptions").pageNumber || this.table.bootstrapTable("getOptions").pageNumber === 1, + ...this.query + }; + // Store the current filters + // this.lastFilters = {..._filters}; + this.opencgaSession.opencgaClient.studies().searchAudit(this.study.fqn, query) + .then(res => { + params.success(res); + }) + .catch(e => { + console.error(e); + params.error(e); + }); + }, + responseHandler: response => { + const result = this.gridCommons.responseHandler(response, $(this.table).bootstrapTable("getOptions")); + return result.response; + }, + onClickRow: (row, selectedElement, field) => this.gridCommons.onClickRow(row.id, row, selectedElement), + onDblClickRow: (row, element, field) => { + // We detail view is active we expand the row automatically. + // FIXME: Note that we use a CSS class way of knowing if the row is expand or collapse, this is not ideal but works. + if (this._config.detailView) { + if (element[0].innerHTML.includes("fa-plus")) { + this.table.bootstrapTable("expandRow", element[0].dataset.index); + } else { + this.table.bootstrapTable("collapseRow", element[0].dataset.index); + } + } + }, + onLoadSuccess: data => { + this.gridCommons.onLoadSuccess(data, 1); + }, + onLoadError: (e, restResponse) => this.gridCommons.onLoadError(e, restResponse), + onPostBody: data => { + // Add tooltips? + } + }); + } + } + + detailFormatter(index, row) { + return ` +
+

Action Params

+
${JSON.stringify(row.params, null, 2)}
+
+ `; + } + + _getDefaultColumns() { + return [ + { + title: "Audit Record ID", + field: "id", + }, + { + title: "User ID", + field: "userId", + }, + { + title: "Study ID", + field: "studyId", + }, + { + title: "Action", + field: "action" + }, + { + title: "Resource Type", + field: "resource" + }, + { + title: "Resource ID", + field: "resourceId", + }, + { + title: "Date", + field: "date", + formatter: value => value ? UtilsNew.dateFormatter(UtilsNew.getDatetime(value)) : "NA" + }, + { + title: "Status", + field: "status.name", + }, + ]; + } + + onFilterChange(key, value) { + if (value && value !== "") { + this.query = {...this.query, ...{[key]: value}}; + } else { + delete this.query[key]; + this.query = {...this.query}; + } + } + + clear(e) { + this.query = {}; + } + + render() { + if (!OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id)) { + return guardPage("No permission to view this page"); + } + + return html` +
+
+ + ${~this._config.filter.sections[0].filters.findIndex(field => field.id === "userId") ? html` + +
+ + +
+ `: nothing} + + ${~this._config.filter.sections[0].filters.findIndex(field => field.id === "action") ? html` + +
+ + +
+ ` : nothing} + + ${~this._config.filter.sections[0].filters.findIndex(field => field.id === "resource") ? html` + +
+ + +
+ ` : nothing} + + ${~this._config.filter.sections[0].filters.findIndex(field => field.id === "status") ? html` + +
+ + +
+ ` : nothing} + +
+ +
+
+
+ +
+
+
+ + + + `; + } + + getDefaultConfig() { + return { + filter: { + sections: [ + { + title: "", + filters: [ + {id: "userId"}, + {id: "resource"}, + {id: "action"}, + {id: "status"}, + ] + } + ], + }, + pagination: true, + pageSize: 10, + pageList: [10, 25, 50], + showExport: false, + detailView: true, + multiSelection: false, + showSelectCheckbox: true, + showToolbar: true, + showActions: true, + }; + } + + +} + +customElements.define("organization-admin-audit", OrganizationAdminAudit); diff --git a/src/webcomponents/organization/admin/organization-admin-detail.js b/src/webcomponents/organization/admin/organization-admin-detail.js new file mode 100644 index 0000000000..9e7a9fe9e9 --- /dev/null +++ b/src/webcomponents/organization/admin/organization-admin-detail.js @@ -0,0 +1,293 @@ +/** + * Copyright 2015-2024 OpenCB * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import {LitElement, html, nothing} from "lit"; +import UtilsNew from "../../../core/utils-new.js"; +import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils.js"; +import ModalUtils from "../../commons/modal/modal-utils.js"; +import "./organization-admin-update.js"; + +export default class OrganizationAdminDetail extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + organization: { + type: Object, + }, + opencgaSession: { + type: Object, + }, + displayConfig: { + type: Object + }, + }; + } + + #init() { + this.COMPONENT_ID = "organization-admin-detail"; + this._prefix = UtilsNew.randomString(8); + this.gridId = this._prefix + this.COMPONENT_ID; + + this.updatedFields = {}; + this.isLoading = false; + + this.displayConfigDefault = { + buttonsVisible: false, + collapsable: true, + titleVisible: false, + titleWidth: 2, + defaultValue: "-", + pdf: false, + }; + this._config = this.getDefaultConfig(); + } + + update(changedProperties) { + if (changedProperties.has("opencgaSession") || + changedProperties.has("organization") || + changedProperties.has("displayConfig")) { + this.propertyObserver(); + } + + super.update(changedProperties); + } + + propertyObserver() { + // With each property change we must be updated config and create the columns again. No extra checks are needed. + this.displayConfig = { + ...this.displayConfigDefault, + ...this.displayConfig + }; + + this.modals = { + /* + "change-owner": { + label: "Change Owner", + icon: "fas fa-user-shield", + modalId: `${this._prefix}AddAdminOrganizationModal`, + render: () => this.renderChangeOwnerOrganization(), + permission: OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id) || "disabled", + }, + */ + "organization-update": { + label: "Edit Organization (coming soon...)", + icon: "far fa-edit", + modalId: `${this._prefix}UpdateOrganizationModal`, + render: () => this.renderOrganizationUpdate(), + permission: OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id) || "disabled", + }, + }; + } + + // *** EVENTS *** + async onActionClick(e) { + this.action = e.currentTarget.dataset.action; + this.requestUpdate(); + await this.updateComplete; + ModalUtils.show(this.modals[this.action]["modalId"]); + } + + // *** RENDER *** + /* + renderChangeOwnerOrganization() { + return ModalUtils.create(this, `${this._prefix}UpdateOrganizationModal`, { + display: { + modalTitle: `Update Organization: ${this.organization.id}`, + modalDraggable: true, + modalCyDataName: "modal-organization-owner-update", + modalSize: "modal-lg" + }, + render: () => { + return html` + + + `; + }, + }); + } + */ + + renderOrganizationUpdate() { + return ModalUtils.create(this, `${this._prefix}UpdateOrganizationModal`, { + display: { + modalTitle: `Update Organization: ${this.organization.id}`, + modalDraggable: true, + modalCyDataName: "modal-organization-update", + modalSize: "modal-lg" + }, + render: () => { + return html` + + + `; + }, + }); + } + + renderOrganizationToolbar() { + return html ` +
+ ${ + Object.keys(this.modals).map(modalKey => { + const modal = this.modals[modalKey]; + const color = modal.permission !== "disabled" ? modal.color : ""; + return html` + +
+
+ +
+
+ `; + }) + } +
+ `; + } + + render() { + if (this.organization) { + return html` + + ${this.renderOrganizationToolbar()} + + + + + ${this.action ? this.modals[this.action]["render"](): nothing} + `; + } + } + + // *** CONFIG *** + getDefaultConfig() { + return { + title: "Organization", + type: "tabs", + icon: "", + display: this.displayConfig || this.displayConfigDefault, + sections: [ + { + title: "Organization View", + elements: [ + { + title: "Organization ID", + type: "complex", + display: { + template: "${id} (UUID: ${uuid})", + style: { + id: { + "font-weight": "bold", + } + }, + }, + }, + { + title: "Organization Name", + field: "name" + }, + { + title: "Owner", + field: "name" + }, + { + title: "Admins", + field: "admins", + type: "list", + display: { + defaultValue: "The organization does not have admins yet.", + contentLayout: "bullets", + }, + }, + { + title: "Creation Date", + field: "creationDate", + display: { + format: date => UtilsNew.dateFormatter(date), + }, + }, + { + title: "Modification Date", + field: "modificationDate", + display: { + format: date => UtilsNew.dateFormatter(date), + }, + }, + ], + }, + { + title: "Authentication Origins", + elements: [ + { + title: "Authentication origins", + field: "configuration.authenticationOrigins", + type: "table", + display: { + columns: [ + { + id: "id", + title: "ID", + field: "id", + formatter: () => {}, + }, + { + id: "host", + title: "Host", + field: "host", + formatter: () => {}, + }, + { + id: "type", + title: "Type", + field: "type", + formatter: () => {}, + }, + ], + }, + }, + ] + }, + ], + }; + } + +} + +customElements.define("organization-admin-detail", OrganizationAdminDetail); diff --git a/src/webcomponents/organization/admin/organization-admin-update.js b/src/webcomponents/organization/admin/organization-admin-update.js new file mode 100644 index 0000000000..efc036d4c5 --- /dev/null +++ b/src/webcomponents/organization/admin/organization-admin-update.js @@ -0,0 +1,340 @@ +/** + * Copyright 2015-2024 OpenCB * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import {LitElement, html} from "lit"; +import UtilsNew from "../../../core/utils-new.js"; +import LitUtils from "../../commons/utils/lit-utils.js"; +import FormUtils from "../../commons/forms/form-utils.js"; +import NotificationUtils from "../../commons/utils/notification-utils.js"; + +export default class OrganizationAdminUpdate extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + organization: { + type: Object, + }, + opencgaSession: { + type: Object, + }, + displayConfig: { + type: Object + }, + }; + } + + #init() { + this._organization = {}; + + this.updatedFields = {}; + this.isLoading = false; + this.displayConfig = {}; + this.displayConfigDefault = { + style: "margin: 10px", + defaultLayout: "horizontal", + labelAlign: "right", + buttonOkText: "Update", + buttonOkDisabled: true, + }; + this._config = this.getDefaultConfig(); + } + + #setLoading(value) { + this.isLoading = value; + this.requestUpdate(); + } + + #initConfigNotification() { + this._config.notification = { + title: "", + text: "Some changes have been done in the form. Not saved changes will be lost", + type: "notification", + display: { + visible: () => { + return UtilsNew.isNotEmpty(this.updatedFields); + }, + notificationType: "warning", + }, + }; + } + + #initOriginalObjects() { + this._organization = UtilsNew.objectClone(this.organization); + // const {configuration, ...rest} = UtilsNew.objectClone(this.organization); + // this._organization = { + // ...UtilsNew.objectClone(configuration), + // // token: { + // // algorithm: "", + // // secretKey: "", + // // expiration: 0 + // // }, + // // defaultUserExpirationDate: "", + // }; + this.updatedFields = {}; + this.displayConfigObserver(); + } + + update(changedProperties) { + if (changedProperties.has("opencgaSession") || + changedProperties.has("organization")) { + this.#initOriginalObjects(); + } + + if (changedProperties.has("displayConfig")) { + this.displayConfigObserver(); + } + + super.update(changedProperties); + } + + displayConfigObserver() { + this.displayConfig = { + ...this.displayConfigDefault, + ...this.displayConfig, + }; + this._config = this.getDefaultConfig(); + if (!this._config?.notification) { + this.#initConfigNotification(); + } + } + + onFieldChange(e, field) { + const param = field || e.detail.param; + this.updatedFields = FormUtils.getUpdatedFields( + this.organization, + this.updatedFields, + param, + e.detail.value, + e.detail.action); + + this._config.display.buttonOkDisabled = UtilsNew.isEmpty(this.updatedFields); + this._config = {...this._config}; + this.requestUpdate(); + } + + onClear() { + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, { + title: "Discard changes", + message: "Are you sure you want to discard the changes made?", + ok: () => { + this.#initOriginalObjects(); + // We need to dispatch a component clear event + LitUtils.dispatchCustomEvent(this, "organizationClear", null, { + organization: this._organization, + }); + }, + }); + } + + onSubmit() { + const params = { + includeResult: true, + authenticationOriginsAction: "SET", + }; + + let updateParams = FormUtils.getUpdateParams(this._organization, this.updatedFields, this.updateCustomisation); + const {configuration, ...rest} = UtilsNew.objectClone(updateParams); + delete updateParams.configuration; + updateParams = { + ...configuration, + ...updateParams, + }; + let error; + this.#setLoading(true); + this.opencgaSession.opencgaClient.organization() + .updateConfiguration(this.organization.id, updateParams, params) + .then(() => { + this._config = this.getDefaultConfig(); + this.updatedFields = {}; + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { + title: `Organization Configuration Update`, + message: `Organization ${this.organization.id} updated correctly`, + }); + LitUtils.dispatchCustomEvent(this, "sessionUpdateRequest", this._organization, {}, error); + LitUtils.dispatchCustomEvent(this, "organizationUpdate", this._organization, {}, error); + }) + .catch(reason => { + error = reason; + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason); + }) + .finally(() => { + this.#setLoading(false); + }); + } + + render() { + if (this.isLoading) { + return html``; + } + + return html` + + + `; + } + + // *** CONFIG *** + getDefaultConfig() { + return { + display: this.displayConfig, + sections: [ + // CAUTION 20240731 Vero: The update of this datapoint is not included in the endpoint organization().updateConfiuration(). + // Since currently the interface is just displaying a button for updating the configuration, + // and for consistency conceived to use a single endpoint per modal, it needs to be discussed with CTO or line-manager + // where to place this endpoint it in IVA (if needed). + /* + { + title: "General Information", + elements: [ + { + title: "Organization Name", + field: "name", + type: "input-text", + }, + ], + }, + */ + { + title: "Token", + elements: [ + { + title: "Token", + field: "token", + type: "object", + elements: [ + { + title: "Algorithm", + field: "token.algorithm", + type: "input-text", + display: { + placeholder: "Change the algorithm...", + helpMessage: "The default algorithm is HS256.", + }, + }, + { + title: "Secret Key", + field: "token.secretKey", + type: "input-text", + display: { + placeholder: "Change the secret key...", + helpMessage: "", + }, + }, + { + title: "Expiration", + field: "token.expiration", + type: "input-num", + allowedValues: [0], + display: { + placeholder: "Change the expiration time...", + helpMessage: "The expiration time is configured in seconds. The default expiration time is 3600s.", + }, + }, + ], + } + ], + }, + { + title: "Configurations", + elements: [ + { + title: "Optimizations", + field: "configuration.optimizations", + type: "object", + elements: [ + { + title: "Simplify Permissions", + field: "configuration.optimizations.simplifyPermissions", + type: "checkbox", + }, + ], + }, + { + title: "Default User Expiration Date", + field: "defaultUserExpirationDate", + type: "input-date", + display: { + placeholder: "Change the default user expiration date" + }, + }, + { + title: "Authentication Origins", + field: "configuration.authenticationOrigins", + type: "object-list", + display: { + style: "border-left: 2px solid #0c2f4c; padding-left: 12px; margin-bottom:24px", + collapsedUpdate: true, + showAddItemListButton: true, + showAddBatchListButton: true, + showResetListButton: true, + view: data => html` +
${data?.id} - ${data?.host}
+ `, + }, + elements: [ + { + title: "ID", + field: "configuration.authenticationOrigins[].id", + type: "input-text", + display: { + placeholder: "Add an ID...", + }, + }, + { + title: "Type", + field: "configuration.authenticationOrigins[].type", + type: "select", + allowedValues: ["OPENCGA", "LDAP", "AzureAD", "SSO"], + display: { + placeholder: "Select a type...", + }, + }, + { + title: "Host", + field: "configuration.authenticationOrigins[].host", + type: "input-text", + display: { + placeholder: "Add a Host...", + }, + }, + ], + }, + ], + }, + ], + }; + } + +} + +customElements.define("organization-admin-update", OrganizationAdminUpdate); diff --git a/src/webcomponents/organization/admin/organization-admin.js b/src/webcomponents/organization/admin/organization-admin.js new file mode 100644 index 0000000000..6de0b4f0a5 --- /dev/null +++ b/src/webcomponents/organization/admin/organization-admin.js @@ -0,0 +1,198 @@ +/** + * Copyright 2015-2024 OpenCB * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {LitElement, html} from "lit"; +import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils.js"; +import "./group-admin-browser.js"; +import "./user-admin-browser.js"; +import "../../project/projects-admin.js"; +import "./project-admin-browser.js"; +import "./organization-admin-detail.js"; + +export default class OrganizationAdmin extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + opencgaSession: { + type: Object + }, + }; + } + + #init() { + this._config = this.getDefaultConfig(); + this._activeMenuItem = ""; + } + + // --- RENDER METHOD --- + render() { + if (this.opencgaSession?.organization) { + if (!OpencgaCatalogUtils.isOrganizationAdmin(this.opencgaSession.organization, this.opencgaSession.user.id)) { + return html ` +
+

Restricted access

+

The page you are trying to access has restricted access.

+

Please refer to your system administrator.

+
+ `; + } + return html ` + + + + `; + } + } + + getDefaultConfig() { + const menu = [ + { + id: "general", + name: "General", + description: "", + icon: "", + featured: "", + visibility: "private", + submenu: [ + // TODO + { + id: "dashboard", + name: "Dashboard (Coming soon)", + icon: "fas fa-vial", + visibility: "private", + render: () => html``, + }, + // TODO + { + id: "audit", + name: "Audit (Coming soon)", + type: "category", + icon: "fas fa-vial", + visibility: "private", + render: () => html``, + }, + ], + }, + { + id: "manage", + name: "Manage", + description: "", + icon: "", + featured: "", // true | false + visibility: "private", + submenu: [ + /* Vero Note: Maintained for future use in Organization Admin + { + id: "groups", + name: "Groups", + icon: "fas fa-vial", + visibility: "private", + render: (opencgaSession, organization) => html` + + + `, + }, + */ + { + id: "users", + name: "Users", + icon: "fas fa-users", + visibility: "private", + render: (opencgaSession, organization) => html` + + + `, + }, + { + id: "studies", + name: "Projects/Studies", + icon: "fas fa-project-diagram", + visibility: "private", + render: (opencgaSession, organization) => { + return html` + + + `; + }, + }, + ], + }, + { + id: "configure", + name: "Configure", + description: "", + icon: "", + featured: "", + visibility: "private", + submenu: [ + { + id: "settings", + name: "Organization", + icon: "fas fa-sitemap", + visibility: "private", + render: (opencgaSession, organization) => { + return html` + + + `; + }, + }, + /* + { + id: "optimization", + name: "Optimizations", + icon: "fas fa-vial", + visibility: "private", + render: (opencgaSession, study) => html``, + }, + */ + ], + }, + ]; + + return { + name: "Organization Admin", + logo: "", + icon: "", + visibility: "", + menu: menu, + }; + } + +} + +customElements.define("organization-admin", OrganizationAdmin); diff --git a/src/webcomponents/organization/admin/project-admin-browser.js b/src/webcomponents/organization/admin/project-admin-browser.js new file mode 100644 index 0000000000..abcc654c48 --- /dev/null +++ b/src/webcomponents/organization/admin/project-admin-browser.js @@ -0,0 +1,258 @@ +/** + * Copyright 2015-2024 OpenCB * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import {LitElement, html, nothing} from "lit"; +import UtilsNew from "../../../core/utils-new.js"; +import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils.js"; +import ModalUtils from "../../commons/modal/modal-utils.js"; +import "../../project/project-create.js"; +import "../../project/project-update.js"; +import "./study-admin-grid.js"; + +export default class ProjectAdminBrowser extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + organization: { + type: Object, + }, + opencgaSession: { + type: Object, + }, + config: { + type: Object + }, + }; + } + + #init() { + this.COMPONENT_ID = "project-admin-browser"; + this._prefix = UtilsNew.randomString(8); + this.gridId = this._prefix + this.COMPONENT_ID; + this.projects = []; + this._config = this.getDefaultConfig(); + } + + update(changedProperties) { + if (changedProperties.has("opencgaSession") || + changedProperties.has("organization") || + changedProperties.has("config")) { + this.propertyObserver(); + } + super.update(changedProperties); + } + + propertyObserver() { + // With each property change we must be updated config and create the columns again. No extra checks are needed. + this._config = { + ...this.getDefaultConfig(), + ...this.config, + }; + + // Config for the grid toolbar + this.toolbarSetting = { + ...this._config, + }; + + this.toolbarConfig = { + toolId: this.toolId, + resource: "PROJECT", + create: { + display: { + modalTitle: "Project Create", + modalDraggable: true, + modalCyDataName: "modal-create", + modalSize: "modal-lg" + // disabled: true, + // disabledTooltip: "...", + }, + modalId: `${this._prefix}CreateProjectModal`, + render: () => html ` + + ` + }, + }; + + this.modals = { + "project-update": { + label: "Edit Project", + icon: "fas fa-edit", + modalId: `${this._prefix}UpdateProjectModal`, + render: () => this.renderProjectUpdate(), + permission: OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id) || "disabled", + }, + }; + } + + // *** EVENTS *** + async onActionClick(e, project) { + this.action = e.currentTarget.dataset.action; + this.projectId = project.id; + this.requestUpdate(); + await this.updateComplete; + ModalUtils.show(this.modals[this.action]["modalId"]); + } + + onProjectCreate() { + // Close modal + ModalUtils.close(this.toolbarConfig.create.modalId); + } + + // *** RENDER *** + renderProjectUpdate() { + return ModalUtils.create(this, `${this._prefix}UpdateProjectModal`, { + display: { + modalTitle: `Update Project: Project ${this.projectId} in organization ${this.organization.id}`, + modalDraggable: true, + modalCyDataName: "modal-project-update", + modalSize: "modal-lg" + }, + // @projectUpdate="${e => this.onProjectUpdate(e, `${this._prefix}UpdateDetailsModal`)}" + render: () => { + return html` + + + `; + }, + }); + } + + renderProjectsToolbar() { + if (this._config.showToolbar) { + return html ` + + + `; + } + } + + renderProject(project) { + return html ` +
+ +
+ +
+ +

+
+ ${project.name || project.id} +
+
+ [ ${project.fqn} ] +
+

+ +
+ ${ + Object.keys(this.modals).map(modalKey => { + const modal = this.modals[modalKey]; + return html` + + `; + }) + } +
+
+ +
+
+ ${project.organism?.scientificName.toUpperCase() || "-"} (${project.organism?.assembly || "-"}) +
+
+ Cellbase: ${project.cellbase?.version || "-"} +
+
+ Data Release: ${project.cellbase?.dataRelease || "-"} +
+ +
+ +
+ ${project.description} +
+
+ +
+ + +
+ + ${this.action ? this.modals[this.action]["render"](): nothing} +
+ `; + } + + render() { + return html` + + ${this.renderProjectsToolbar()} + + ${this.organization.projects.map(project => this.renderProject(project))} + `; + } + + // *** CONFIG *** + getDefaultConfig() { + return { + showToolbar: true, + showExport: false, + showSettings: false, + showCreate: true, + buttonCreateText: "New Project...", + showGraphicFilters: false, + showProjectToolbar: true, + }; + } + +} + +customElements.define("project-admin-browser", ProjectAdminBrowser); diff --git a/src/webcomponents/organization/admin/study-admin-grid.js b/src/webcomponents/organization/admin/study-admin-grid.js new file mode 100644 index 0000000000..51a53f4e5d --- /dev/null +++ b/src/webcomponents/organization/admin/study-admin-grid.js @@ -0,0 +1,476 @@ +/** + * Copyright 2015-2024 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {LitElement, html, nothing} from "lit"; +import GridCommons from "../../commons/grid-commons.js"; +import CatalogGridFormatter from "../../commons/catalog-grid-formatter.js"; +import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils.js"; +import ModalUtils from "../../commons/modal/modal-utils.js"; +import UtilsNew from "../../../core/utils-new.js"; + +import "../../study/admin/study-create.js"; +import "../../study/admin/study-update.js"; +import "./study-users-manage.js"; + +export default class StudyAdminGrid extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + toolId: { + type: String, + }, + project: { + type: Object, + }, + organization: { + type: Object, + }, + opencgaSession: { + type: Object + }, + active: { + type: Boolean + }, + config: { + type: Object + }, + }; + } + + #init() { + this.COMPONENT_ID = "study-grid"; + this._prefix = UtilsNew.randomString(8); + this.gridId = this._prefix + this.COMPONENT_ID; + this.active = true; + this._config = this.getDefaultConfig(); + this._action = ""; + this.displayConfigDefault = { + header: { + horizontalAlign: "center", + verticalAlign: "bottom", + }, + }; + } + + // --- LIFE-CYCLE METHODS + update(changedProperties) { + if (changedProperties.has("opencgaSession") || + changedProperties.has("toolId") || + changedProperties.has("project") || + changedProperties.has("config")) { + this.propertyObserver(); + } + super.update(changedProperties); + } + + updated(changedProperties) { + if (changedProperties.size > 0 && this.active) { + this.renderRemoteTable(); + } + } + + propertyObserver() { + // With each property change we must be updated config and create the columns again. No extra checks are needed. + this._config = { + ...this.getDefaultConfig(), + ...this.config, + }; + + this.gridCommons = new GridCommons(this.gridId, this, this._config); + + // Config for the grid toolbar + this.toolbarSetting = { + ...this._config, + }; + + this.toolbarConfig = { + toolId: this.toolId, + resource: "STUDY", + columns: this._getDefaultColumns(), + create: { + display: { + modalTitle: "Study Create", + modalDraggable: true, + modalCyDataName: "modal-study-create", + modalSize: "modal-lg" + }, + modalId: `${this._prefix}CreateStudyModal`, + render: () => html ` + + + `, + }, + }; + + this.permissions = { + "organization": () => OpencgaCatalogUtils.isOrganizationAdmin(this.organization, this.opencgaSession.user.id) ? "" : "disabled", + "study": () => OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id) ? "" : "disabled", + }; + + this.modals = { + "edit-study": { + label: "Edit Study", + icon: "fas fa-edit", + modalId: `${this._prefix}UpdateStudyModal`, + render: () => this.renderStudyUpdate(), + permission: this.permissions["organization"](), + divider: true, + }, + "create-group": { + label: "Create Group", + icon: "fas fa-edit", + modalId: `${this._prefix}CreateGroupModal`, + render: () => this.renderGroupCreate(), + permission: this.permissions["organization"](), + divider: false, + }, + "manage-users": { + label: "Manage Organization Users in Study", + icon: "fas fa-user-plus", + modalId: `${this._prefix}ManageUsersStudyModal`, + render: () => this.renderManageUsersStudy(), + permission: this.permissions["organization"](), + divider: true, + }, + "delete": { + label: "Delete Study", + icon: "fas fa-trash-alt ", + // color: "text-danger", + // modalId: `${this._prefix}DeleteModal`, + // render: () => this.renderModalPasswordReset(), + permission: "disabled", // Caution: Not possible to delete studies for now. + }, + }; + } + + // *** PRIVATE METHODS *** + renderRemoteTable() { + if (this.opencgaSession?.opencgaClient && this.project.id) { + this._columns = this._getDefaultColumns(); + this.table = $("#" + this.gridId); + this.table.bootstrapTable("destroy"); + this.table.bootstrapTable({ + theadClasses: "table-light", + buttonsClass: "light", + columns: this._columns, + method: "get", + sidePagination: "server", + iconsPrefix: GridCommons.GRID_ICONS_PREFIX, + icons: GridCommons.GRID_ICONS, + uniqueId: "id", + // Table properties + pagination: this._config.pagination, + pageSize: this._config.pageSize, + pageList: this._config.pageList, + detailView: !!this.detailFormatter, + loadingTemplate: () => GridCommons.loadingFormatter(), + ajax: params => { + let result = null; + this.filters = { + limit: params.data.limit, + skip: params.data.offset || 0, + count: !this.table.bootstrapTable("getOptions").pageNumber || this.table.bootstrapTable("getOptions").pageNumber === 1, + }; + + // Store the current filters + this.opencgaSession.opencgaClient.projects() + .studies(this.project.id, this.filters) + .then(response => { + result = response; + return response; + }) + .then(() => { + // Prepare data for columns extensions + const rows = result.responses?.[0]?.results || []; + return this.gridCommons.prepareDataForExtensions(this.COMPONENT_ID, this.opencgaSession, this.filters, rows); + }) + .then(() => params.success(result)) + .catch(error => { + console.error(error); + params.error(error); + }); + }, + responseHandler: response => { + const result = this.gridCommons.responseHandler(response, $(this.table).bootstrapTable("getOptions")); + return result.response; + }, + onClickRow: (row, selectedElement) => this.gridCommons.onClickRow(row.id, row, selectedElement), + onLoadSuccess: () => UtilsNew.initTooltip(this), + onLoadError: (e, restResponse) => this.gridCommons.onLoadError(e, restResponse), + }); + } + } + + _getDefaultColumns() { + this._columns = [ + { + title: "Study ID", + field: "id", + visible: this.gridCommons.isColumnVisible("id") + }, + { + title: "Study Fqn", + field: "fqn", + visible: this.gridCommons.isColumnVisible("fqn") + }, + { + title: "Name", + field: "name", + visible: this.gridCommons.isColumnVisible("name") + }, + { + title: "Groups", + field: "groups", + formatter: (groups, row) => this.groupsFormatter(groups, row), + visible: this.gridCommons.isColumnVisible("modificationDate") + }, + { + title: "Modification / Creation Dates", + field: "dates", + halign: this.displayConfigDefault.header.horizontalAlign, + valign: "middle", + formatter: (value, row) => this.datesFormatter(value, row), + }, + ]; + + if (this._config.annotations?.length > 0) { + this.gridCommons.addColumnsFromAnnotations(this._columns, CatalogGridFormatter.customAnnotationFormatter, this._config); + } + + if (this.opencgaSession && this._config.showActions) { + this._columns.push({ + id: "actions", + title: "Actions", + field: "actions", + align: "center", + formatter: () => ` + + `, + events: { + "click ul>li>a": (e, value, row) => this.onActionClick(e, value, row), + }, + }); + } + + this._columns = this.gridCommons.addColumnsFromExtensions(this._columns, this.COMPONENT_ID); + return this._columns; + } + + // *** FORMATTERS *** + groupsFormatter(groups) { + const groupsBadges = groups.map(group => ` +
+
${group.id} [${group.userIds.length}]
+ ${group.userIds.join(", ")} +
+ `); + + const maxShow = 3; + const badgesShow = groupsBadges.splice(0, maxShow); + return ` +
+ ${badgesShow.join("")} + ${groupsBadges.length > 0 ? ` + + ... View all groups (${groupsBadges.length}) + + ` : ""} +
+ `; + } + + datesFormatter(value, study) { + return ` +
${CatalogGridFormatter.dateFormatter(study.modificationDate, study)}
+
${CatalogGridFormatter.dateFormatter(study.creationDate, study)}
+ `; + } + + // *** EVENTS *** + async onActionClick(e, value, row) { + this._action = e.currentTarget.dataset.action; + this.studyId = row.id; + this.studyFqn = row.fqn; + if (this._action === "manage-users") { + // Manage organization users: (a) add/remove from study, (b) set/unset as study admins + this.groups = row.groups.filter(group => ["@members", "@admins"].includes(group.id)); + } + this.requestUpdate(); + await this.updateComplete; + ModalUtils.show(this.modals[this._action]["modalId"]); + } + + onStudyEvent(e, id) { + this._action = ""; + ModalUtils.close(id); + } + + onStudyCreate() { + // Close modal + ModalUtils.close(this.toolbarConfig.create.modalId); + } + + // *** RENDER METHODS *** + renderGroupCreate() { + return ModalUtils.create(this, `${this._prefix}CreateGroupModal`, { + display: { + modalTitle: `Group Create in Study: ${this.studyId}`, + modalDraggable: true, + modalCyDataName: "modal-group-create", + modalSize: "modal-lg" + }, + render: () => html` + + + `, + }); + } + + renderStudyUpdate() { + return ModalUtils.create(this, `${this._prefix}UpdateStudyModal`, { + display: { + modalTitle: `Update Study: ${this.studyId}`, + modalDraggable: true, + modalCyDataName: "modal-study-update", + modalSize: "modal-lg" + }, + render: () => html` + + + `, + }); + } + + renderManageUsersStudy() { + return ModalUtils.create(this, `${this._prefix}ManageUsersStudyModal`, { + display: { + modalTitle: `Manage Organization Users in Study: ${this.studyId}`, + modalDraggable: true, + modalCyDataName: "modal-users-study-update", + modalSize: "modal-lg" + }, + render: () => { + return html` + + + `; + } + }); + } + + renderToolbar() { + if (this._config.showToolbar) { + return html ` + + + `; + } + } + + render() { + return html` + + ${this.renderToolbar()} + +
+
+
+ + ${this._action ? this.modals[this._action]["render"](): nothing} + `; + } + + // *** DEFAULT CONFIG *** + getDefaultConfig() { + return { + // Settings + pagination: true, + pageSize: 10, + pageList: [5, 10, 25], + pageInfoShort: true, + multiSelection: false, + showSelectCheckbox: false, + + showToolbar: true, + showActions: true, + + buttonCreateText: "New Study...", + showCreate: true, + showExport: false, + showSettings: false, + exportTabs: ["download", "link", "code"], + }; + } + +} + +customElements.define("study-admin-grid", StudyAdminGrid); diff --git a/src/webcomponents/organization/admin/study-users-manage.js b/src/webcomponents/organization/admin/study-users-manage.js new file mode 100644 index 0000000000..8492a50490 --- /dev/null +++ b/src/webcomponents/organization/admin/study-users-manage.js @@ -0,0 +1,379 @@ +/** + * Copyright 2015-2023 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {html, LitElement, nothing} from "lit"; +import UtilsNew from "../../../core/utils-new.js"; +import NotificationUtils from "../../commons/utils/notification-utils.js"; +import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils.js"; +import FormUtils from "../../commons/forms/form-utils.js"; +import LitUtils from "../../commons/utils/lit-utils.js"; + +export default class StudyUsersManage extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + studyFqn: { + type: String, + }, + groups: { + type: Array, + }, + opencgaSession: { + type: Object, + }, + displayConfig: { + type: Object, + }, + }; + } + + #init() { + this.updateParams = {}; + this.isLoading = false; + this.displayConfig = {}; + this.displayConfigDefault = { + style: "margin: 10px", + defaultLayout: "horizontal", + labelAlign: "right", + labelWidth: 3, + buttonOkText: "Update", + }; + } + + #initOriginalObjects() { + // 1. Data in data-form + this.userRole = { + "org-owner": { + displayName: "OWNER", + check: userId => this.opencgaSession.organization.owner === userId, + }, + "org-admin": { + displayName: "ADMIN", + check: userId => this.opencgaSession.organization.admins.includes(userId), + }, + "study-admin": { + displayName: "STUDY ADMIN", + check: userId => this.groups.find(group => group.id === "@admins").userIds.includes(userId), + }, + }; + // Original object + this.component = { + selectedGroups: this.groups.map(group => group.id).join(",") || "", + selectedUsers: this.users?.map(user => user.id) || [], + }; + // Modified object + this._component = UtilsNew.objectClone(this.component); + + // 2. Query variables + this._userGroupUpdates = []; + + // 3. Display + this.forceDisable = []; + this._config = this.getDefaultConfig(); + } + + #setLoading(value) { + this.isLoading = value; + this.requestUpdate(); + } + + // --- LIT LIFE CYCLE + update(changedProperties) { + if (changedProperties.has("studyFqn") || + changedProperties.has("groups") || + changedProperties.has("opencgaSession")) { + this.propertyObserver(); + } + if (changedProperties.has("displayConfig")) { + this.displayConfig = { + ...this.displayConfigDefault, + ...this.displayConfig, + }; + } + super.update(changedProperties); + } + + // --- OBSERVERS --- + async propertyObserver() { + if (this.opencgaSession?.organization?.id && this.groups && this.studyFqn) { + const filters = { + organization: this.opencgaSession.organization.id, + include: "id", + count: true, + limit: 1, + }; + this.#setLoading(true); + try { + const responseNoUsers = await this.opencgaSession.opencgaClient.users().search(filters); + const noUsers = responseNoUsers.responses[0].numTotalResults; + if (noUsers > 0) { + filters.limit = noUsers; + const responseUsers = await this.opencgaSession.opencgaClient.users().search(filters); + this.users = UtilsNew.objectClone(responseUsers.responses[0].result); + this.#initOriginalObjects(); + } + } catch (error) { + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, error); + } + this.#setLoading(false); + } + } + + // --- EVENTS --- + onFieldChange(e) { + const param = e.detail.param; + this.updatedFields = FormUtils.getUpdatedFields(this.component, this.updatedFields, param, e.detail.value, e.detail.action); + this._config = this.getDefaultConfig(); + this.requestUpdate(); + } + + onClear() { + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, { + title: "Discard changes", + message: "Are you sure you want to discard the changes made?", + ok: () => { + this.#initOriginalObjects(); + this.requestUpdate(); + }, + }); + } + + onSubmit() { + // 1. Create promises with updates + this.#setLoading(true); + const _userGroupPromises = this._userGroupUpdates + .map(update => { + let error; + const params= { + includeResult: true, + action: update.isChecked ? "ADD" : "REMOVE", + }; + const data = { + users: [update.userId], + }; + return this.opencgaSession.opencgaClient.studies() + .updateGroupsUsers(this.studyFqn, update.groupId, data, params) + .then(() => { + const studyId = this.studyFqn.split(":").pop(); + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { + title: `User in Group Update`, + message: ` + ${update.userId} ${update.isChecked ? "ADDED to" : "REMOVED from"} + ${update.groupId} in study ${studyId} correctly. + `, + }); + }) + .catch(reason => { + error = reason; + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason); + }) + .finally(() => { + LitUtils.dispatchCustomEvent(this, "userGroupUpdate", {}, { + user: update.userId, + group: update.groupId, + }, error); + }); + + }); + // 2. Execute all changes and refresh session + Promise.all(_userGroupPromises) + .finally(() => { + this.#setLoading(false); + LitUtils.dispatchCustomEvent(this, "studyUpdateRequest", {}); + LitUtils.dispatchCustomEvent(this, "studyUpdate", this.study, {}); + }); + } + + onUserGroupChange(e, userId, groupId) { + const pos = this._findChangePosition(userId, groupId); + if (pos >= 0) { + // Remove change from this._userGroupUpdates if the change has been undone. + this._userGroupUpdates.splice(pos, 1); + } else { + // Create an object with the params needed + this._userGroupUpdates.push({ + isChecked: e.currentTarget.checked, + userId: userId, + groupId: groupId, + }); + } + // If a user is member not admin: + // - If removed from member => disable admin + // - If added to admin => disable member + // - And enable them back if the change is undone + // If a user is member and admin: + // - If removed from admin and added back => do not disable member + // If a user is not member and not admin: + // - If added to member and removed back => do not disable admin + // To avoid undetermined user permissions on submit if contradictory changes are submitted. + if (groupId === "@admins" && this.groups.find(group => group.id === "@members" && group.userIds.includes(userId))) { + this.forceDisable[`${userId}.@members`] = (e.currentTarget.checked && pos === -1); + } + if (groupId === "@members" && this.groups.find(group => group.id === "@admins" && !group.userIds.includes(userId))) { + this.forceDisable[`${userId}.@admins`] = (!e.currentTarget.checked && pos === -1); + } + + this._config = {...this._config}; + this.requestUpdate(); + } + + // Double-check if the user is undoing a previous change on a specific user / group. + _findChangePosition(userId, groupId) { + // pos will equal -1 if a previous changes has been undone + return this._userGroupUpdates.findIndex(update => update.userId === userId && update.groupId === groupId); + } + + _findCurrentValue(userId, groupId) { + // Check if this user has been added/removed from this group + const change = this._userGroupUpdates.find(update => update.userId === userId && update.groupId === groupId); + if (change) { + // 2. If added/removed, return value + return change.isChecked; + } else { + // 2. If not, check if the user was initially on this group + const group = this.groups.find(group => group.id === groupId); + return group.userIds.includes(userId); + } + } + + renderStyle() { + // Note 20240724 Vero: This css class enables vertical scroll on tbody + return html ` + + `; + } + + // --- RENDER --- + render() { + if (!this.component) { + return nothing; + } + + return html` + ${this.renderStyle()} + + + `; + } + + getDefaultConfig() { + const sections = [ + { + display: { + descriptionClassName: "d-block text-secondary", + visible: data => data?.selectedGroups !== "" && data?.selectedUsers?.length > 0, + }, + elements: [ + { + // title: "Table", + field: "selectedUsers", + type: "table", + display: { + className: "study-users-manage-table", + width: 12, + columns: [ + { + id: "id", + title: "User Id", + type: "custom", + display: { + render: (value, update, params, data, userId) => { + const role = Object.values(this.userRole) + .find(role => role.check(userId)) || {}; + return html` +
+ ${role?.displayName ? html` +
${userId}
+
${role.displayName}
+ ` : html` +
${userId}
+ `} +
+ `; + } + }, + }, + ...this.groups?.map(group => ({ + id: group.id, + title: group.id === "@members" ? "Study Member" : "Study Admin", + type: "custom", + display: { + helpMessage: "", + render: (checked, dataFormFilterChange, updateParams, data, userId) => { + const currentValue = this._findCurrentValue(userId, group.id); + const changePosition = this._findChangePosition(userId, group.id); + // If the user is organization admin or owner, + // the checkboxes in members and admins need to be disabled + const userIsOrganizationAdmin = OpencgaCatalogUtils.isOrganizationAdmin(this.opencgaSession.organization, userId); + return html` +
+ + ${changePosition >= 0 ? html`*` : ""} +
+ `; + } + }, + })), + ], + }, + }, + ], + }, + ]; + return { + id: "", + display: this.displayConfig, + sections: sections, + }; + } + +} + +customElements.define("study-users-manage", StudyUsersManage); + diff --git a/src/webcomponents/organization/admin/user-admin-admins-change.js b/src/webcomponents/organization/admin/user-admin-admins-change.js new file mode 100644 index 0000000000..ecb84ce4bc --- /dev/null +++ b/src/webcomponents/organization/admin/user-admin-admins-change.js @@ -0,0 +1,183 @@ +/** + * Copyright 2015-2024 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {html, LitElement} from "lit"; +import NotificationUtils from "../../commons/utils/notification-utils.js"; +import LitUtils from "../../commons/utils/lit-utils.js"; +import UtilsNew from "../../../core/utils-new.js"; +import "./filters/user-status-filter.js"; + +export default class UserAdminAdminsChange extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + userId: { + type: String + }, + action: { + type: String + }, + organization: { + type: Object, + }, + opencgaSession: { + type: Object + }, + displayConfig: { + type: Object + }, + }; + } + + #init() { + this.userId = ""; + this.displayTitle = ""; + this.displayText = ""; + this.displayConfigDefault = { + style: "margin: 10px", + titleWidth: 3, + titleStyle: "color: var(--main-bg-color);margin-bottom:16px;font-weight:bold;", + defaultLayout: "horizontal", + buttonOkText: "Confirm", + buttonClearText: "", + }; + } + + #setLoading(value) { + this.isLoading = value; + this.requestUpdate(); + } + + update(changedProperties) { + if (changedProperties.has("userId") || changedProperties.has("action")) { + this.propertyObserver(); + } + if (changedProperties.has("displayConfig")) { + this._config = this.getDefaultConfig(); + } + super.update(changedProperties); + } + + propertyObserver() { + if (this.action && this.userId) { + this.displayMessages = { + "REMOVE": { + title: `Remove user '${this.userId}' from the list of organization users.`, + text: `The user '${this.userId}' will be REMOVED from the list of organization administrators. + As a result, they will no longer be able to perform specific actions such as creating or editing users, + modifying organization information, or creating projects and studies.`, + successText: `The '${this.userId}' has been successfully REMOVED from the list of organization administrators`, + }, + "ADD": { + title: `Add user '${this.userId}' to the list of organization users.`, + text: `The user '${this.userId}' will be ADDED to the list of organization administrators. + As a result, they be able to perform specific actions such as creating or editing users, + modifying organization information, or creating projects and studies.`, + successText: `The '${this.userId}' has been successfully ADDED to the list of organization administrators`, + }, + }; + this.displayTitle = this.displayMessages[this.action].title; + this.displayText = this.displayMessages[this.action].text; + this.displaySuccessText = this.displayMessages[this.action].successText; + } + } + + onSubmit() { + const params = { + includeResult: true, + adminsAction: this.action, + }; + let admins = this.organization.admins; + this.action === "ADD" ? admins.push(this.userId) : admins = [this.userId]; // REMOVE userId from array of admins + + const updateParams = { + admins: admins, + }; + this.#setLoading(true); + this.opencgaSession.opencgaClient.organization() + .update(this.organization.id, updateParams, params) + .then(response => { + this.user = UtilsNew.objectClone(response.responses[0].results[0]); + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { + title: `Organization Admin`, + message: this.displaySuccessText, + }); + LitUtils.dispatchCustomEvent(this, "userUpdate", this.user, {}); + LitUtils.dispatchCustomEvent(this, "sessionUpdateRequest", this._study, {}); + }) + .catch(reason => { + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason); + LitUtils.dispatchCustomEvent(this, "userUpdateFailed", this.user, {}, reason); + }) + .finally(() => { + this.#setLoading(false); + }); + } + + render() { + if (this.isLoading) { + return html``; + } + + return html` + + + `; + } + + getDefaultConfig() { + return { + icon: "fas fa-edit", + buttons: { + okText: "Confirm", + }, + display: this.displayConfig || this.displayConfigDefault, + sections: [ + { + // title: this.displayTitle, + elements: [ + { + type: "notification", + text: this.displayText, + display: { + visible: true, + icon: "fas fa-exclamation-triangle", + notificationType: "warning", + }, + }, + ], + }, + ], + }; + } + +} + +customElements.define("user-admin-admins-change", UserAdminAdminsChange); diff --git a/src/webcomponents/organization/admin/user-admin-browser.js b/src/webcomponents/organization/admin/user-admin-browser.js new file mode 100644 index 0000000000..9803bfa211 --- /dev/null +++ b/src/webcomponents/organization/admin/user-admin-browser.js @@ -0,0 +1,137 @@ +/** + * Copyright 2015-2024 OpenCB * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import {LitElement, html} from "lit"; +import LitUtils from "../../commons/utils/lit-utils.js"; +import UtilsNew from "../../../core/utils-new.js"; +import "./user-admin-grid.js"; + +export default class UserAdminBrowser extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + studyId: { + type: String, + }, + study: { + type: Object, + }, + organization: { + type: Object, + }, + opencgaSession: { + type: Object, + }, + // QUESTION: pending to decide if we allow browser settings here. + settings: { + type: Object, + }, + }; + } + + #init() { + this.COMPONENT_ID = "user-admin-browser"; + this.users = []; + this._config = this.getDefaultConfig(); + this.isLoading = false; + } + + #setLoading(value) { + this.isLoading = value; + this.requestUpdate(); + } + + update(changedProperties) { + if (changedProperties.has("settings")) { + this.settingsObserver(); + } + super.update(changedProperties); + } + + settingsObserver() { + this._config = { + ...this.getDefaultConfig(), + ...this.settings, + }; + } + + // Vero 09072024 Note: Maintained for future use of this component in the Study Admin + studyIdObserver() { + if (this.studyId && this.opencgaSession) { + let error; + this.#setLoading(true); + this.opencgaSession.opencgaClient.studies() + .info(this.studyId) + .then(response => { + this._study = UtilsNew.objectClone(response.responses[0].results[0]); + }) + .catch(reason => { + this._study = {}; + error = reason; + console.error(reason); + }) + .finally(() => { + this._config = this.getDefaultConfig(); + LitUtils.dispatchCustomEvent(this, "studyChange", this.study, {}, error); + this.#setLoading(false); + }); + } else { + this._study = {}; + } + } + + renderFilterGraphics() { + if (this._config.showGraphicFilters) { + return html ` + + `; + } + } + + render() { + return html ` + + ${this.renderFilterGraphics()} + + + + `; + } + + getDefaultConfig() { + return { + showGraphicFilters: false, + }; + } + +} + +customElements.define("user-admin-browser", UserAdminBrowser); diff --git a/src/webcomponents/organization/admin/user-admin-create.js b/src/webcomponents/organization/admin/user-admin-create.js new file mode 100644 index 0000000000..eb7221d57c --- /dev/null +++ b/src/webcomponents/organization/admin/user-admin-create.js @@ -0,0 +1,225 @@ +/** + * Copyright 2015-2024 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {LitElement, html} from "lit"; +import LitUtils from "../../commons/utils/lit-utils.js"; +import NotificationUtils from "../../commons/utils/notification-utils.js"; +import UtilsNew from "../../../core/utils-new.js"; + +export default class UserAdminCreate extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + organization: { + type: Object, + }, + opencgaSession: { + type: Object + }, + displayConfig: { + type: Object + }, + }; + } + + #init() { + this._user = {}; + this.isLoading = false; + this.displayConfigDefault = { + style: "margin: 10px", + titleWidth: 3, + defaultLayout: "horizontal", + buttonOkText: "Create" + }; + this._config = this.getDefaultConfig(); + } + + #initOriginalObjects() { + this._user = {}; + this._config = this.getDefaultConfig(); + } + + #setLoading(value) { + this.isLoading = value; + this.requestUpdate(); + } + + update(changedProperties) { + if (changedProperties.has("displayConfig")) { + this.displayConfig = { + ...this.displayConfigDefault, + ...this.displayConfig + }; + } + if (changedProperties.has("opencgaSession")) { + this.opencgaSessionObserver(); + } + super.update(changedProperties); + } + + opencgaSessionObserver() { + this.#initOriginalObjects(); + } + + onFieldChange(e, field) { + this._user = {...e.detail.data}; // force to refresh the object-list + this.requestUpdate(); + } + + onClear() { + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, { + title: "Clear user", + message: "Are you sure to clear?", + ok: () => { + this._user = {}; + this._config = this.getDefaultConfig(); + this.requestUpdate(); + }, + }); + } + + onSubmit() { + // Prepare object to be submitted + this._user.organization = this.organization.id; + delete this._user.confirmPassword; + + this.#setLoading(true); + this.opencgaSession.opencgaClient.users() + .create(this._user) + .then(response => { + const newUser = UtilsNew.objectClone(response.responses[0].results[0]); + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { + title: `User Create`, + message: `User ${newUser.id} created in organization ${this.organization.id} successfully`, + }); + LitUtils.dispatchCustomEvent(this, "userCreate", newUser, {}); + LitUtils.dispatchCustomEvent(this, "sessionUpdateRequest", {}, {}); + }) + .catch(reason => { + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason); + }) + .finally(() => { + this.#setLoading(false); + this.#initOriginalObjects(); + }); + } + + render() { + if (this.isLoading) { + return html``; + } + + return html` + + `; + } + + getDefaultConfig() { + return { + display: this.displayConfig || this.displayConfigDefault, + sections: [ + { + elements: [ + { + title: "User ID", + field: "id", + type: "input-text", + required: true, + display: { + placeholder: "Add a short ID...", + }, + }, + { + title: "User Name", + field: "name", + type: "input-text", + required: true, + display: { + placeholder: "Add the user name...", + }, + }, + { + title: "User Email", + field: "email", + type: "input-text", + required: true, + display: { + placeholder: "Add the user email...", + }, + }, + { + title: "User Password", + field: "password", + type: "input-password", + required: true, + validation: { + validate: (value, user) => !!user.password, + message: "The user password can not be empty.", + }, + display: { + helpMessage: ` + Type a strong password of a minimum length of 8 characters, combining at least: + 1 upper-case letter, 1 lower-case letter, 1 digit, and 1 special character. + `, + }, + }, + { + title: "Confirm user password", + field: "confirmPassword", + type: "input-password", + required: true, + defaultValue: "", + validation: { + validate: (value, user) => { + return !!user.confirmPassword && user.confirmPassword === user.password; + }, + message: "The user passwords do not match.", + }, + }, + { + title: "Change password", + field: "requiredAction", + type: "toggle-switch", + required: false, + display: { + disabled: true, + helpMessage: "Coming soon: Required user action for changing password.", + }, + }, + ], + }, + ], + }; + } + +} + +customElements.define("user-admin-create", UserAdminCreate); diff --git a/src/webcomponents/organization/admin/user-admin-details-update.js b/src/webcomponents/organization/admin/user-admin-details-update.js new file mode 100644 index 0000000000..a7e0c4dd9f --- /dev/null +++ b/src/webcomponents/organization/admin/user-admin-details-update.js @@ -0,0 +1,257 @@ +/** + * Copyright 2015-2024 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {html, LitElement} from "lit"; +import UtilsNew from "../../../core/utils-new.js"; +import LitUtils from "../../commons/utils/lit-utils.js"; +import FormUtils from "../../commons/forms/form-utils.js"; +import NotificationUtils from "../../commons/utils/notification-utils.js"; + +export default class UserAdminDetailsUpdate extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + userId: { + type: String + }, + organization: { + type: Object, + }, + opencgaSession: { + type: Object + }, + displayConfig: { + type: Object + }, + }; + } + + #init() { + this.user = {}; // Original object + this._user = {}; // Updated object + this.userId = ""; + this.displayConfig = {}; + this.updatedFields = {}; + // Some of the fields modeled for USER cannot be updated at all or updated through the endpoint used in this component. + // They need to be removed from the object. + this.updateCustomisation = [ + "internal.status", + "internal.registrationDate", + "internal.lastModified", + "internal.account.password", + "internal.account.failedAttempts", + "internal.account.authentication", + ]; + this._config = this.getDefaultConfig(); + } + + #setLoading(value) { + this.isLoading = value; + this.requestUpdate(); + } + + #initConfigNotification() { + this._config.notification = { + title: "", + text: "Some changes have been done in the form. Not saved changes will be lost", + type: "notification", + display: { + visible: () => { + return UtilsNew.isNotEmpty(this.updatedFields); + }, + notificationType: "warning", + }, + }; + } + + #initOriginalObjects() { + this._user = UtilsNew.objectClone(this.user); + this.updatedFields = {}; + this.requestUpdate(); + } + + update(changedProperties) { + if (changedProperties.has("userId")) { + this.userIdObserver(); + } + if (changedProperties.has("displayConfig")) { + this._config = this.getDefaultConfig(); + if (!this._config?.notification) { + this.#initConfigNotification(); + } + } + super.update(changedProperties); + } + + userIdObserver() { + if (this.userId && this.opencgaSession) { + const params = { + organization: this.organization.id, + }; + let error; + this.#setLoading(true); + this.opencgaSession.opencgaClient.users() + .info(this.userId, params) + .then(response => { + this.user = UtilsNew.objectClone(response.responses[0].results[0]); + this.#initOriginalObjects(); + }) + .catch(reason => { + error = reason; + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason); + }) + .finally(() => { + LitUtils.dispatchCustomEvent(this, "userInfo", this.user, {}, error); + this.#setLoading(false); + }); + } + } + + onFieldChange(e, field) { + const param = field || e.detail.param; + this.updatedFields = FormUtils.getUpdatedFields( + this.user, + this.updatedFields, + param, + e.detail.value, + e.detail.action); + + this.requestUpdate(); + } + + onClear() { + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, { + title: "Discard changes", + message: "Are you sure you want to discard the changes made?", + ok: () => { + this.#initOriginalObjects(); + this.requestUpdate(); + // We need to dispatch a component clear event + LitUtils.dispatchCustomEvent(this, "userClear", null, { + user: this.user, + }); + }, + }); + } + + onSubmit() { + const params = { + includeResult: true, + }; + debugger + const updateParams = FormUtils.getUpdateParams(this._user, this.updatedFields, this.updateCustomisation); + + this.#setLoading(true); + this.opencgaSession.opencgaClient.organization() + .updateUser(this.userId, updateParams, params) + .then(response => { + this.user = UtilsNew.objectClone(response.responses[0].results[0]); + this.#initOriginalObjects(); + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { + title: `User Details Update`, + message: `User ${this.userId} updated correctly`, + }); + LitUtils.dispatchCustomEvent(this, "userUpdate", this.user, {}); + }) + .catch(error => { + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, error); + LitUtils.dispatchCustomEvent(this, "userUpdateFailed", this.user, {}, error); + }) + .finally(() => { + this.#setLoading(false); + }); + + } + + render() { + return html ` + + + `; + } + + getDefaultConfig() { + return { + icon: "fas fa-edit", + buttons: { + clearText: "Discard Changes", + okText: "Update", + }, + display: this.displayConfig, + sections: [ + { + title: "Details", + display: { + titleVisible: false, + }, + elements: [ + { + title: "User Name", + field: "name", + type: "input-text", + display: { + helpMessage: "Edit the user name...", + }, + }, + { + title: "User email", + field: "email", + type: "input-text", + display: { + helpMessage: "Edit the user email...", + }, + }, + ], + }, + { + title: "Account", + display: { + titleVisible: false, + }, + elements: [ + { + title: "Expiration Date", + field: "internal.account.expirationDate", + type: "input-date", + display: { + format: date => UtilsNew.dateFormatter(date) + }, + }, + ], + }, + ], + }; + } + +} + +customElements.define("user-admin-details-update", UserAdminDetailsUpdate); diff --git a/src/webcomponents/organization/admin/user-admin-grid.js b/src/webcomponents/organization/admin/user-admin-grid.js new file mode 100644 index 0000000000..f998cd36a9 --- /dev/null +++ b/src/webcomponents/organization/admin/user-admin-grid.js @@ -0,0 +1,630 @@ +/** + * Copyright 2015-2024 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {LitElement, html, nothing} from "lit"; +import GridCommons from "../../commons/grid-commons.js"; +import CatalogGridFormatter from "../../commons/catalog-grid-formatter.js"; +import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils.js"; +import ModalUtils from "../../commons/modal/modal-utils.js"; +import UtilsNew from "../../../core/utils-new.js"; +import "./user-admin-create.js"; +import "./user-admin-details-update.js"; +import "./user-admin-password-reset.js"; +import "./user-admin-status-update.js"; +import "./user-admin-admins-change.js"; +// import "./user-admin-password-change.js"; + +export default class UserAdminGrid extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + toolId: { + type: String, + }, + opencgaSession: { + type: Object + }, + organization: { + type: Object, + }, + users: { + type: Array + }, + active: { + type: Boolean + }, + config: { + type: Object + }, + }; + } + + #init() { + this.COMPONENT_ID = "user-grid"; + this._prefix = UtilsNew.randomString(8); + this.gridId = this._prefix + this.COMPONENT_ID; + this.active = true; + this._config = this.getDefaultConfig(); + this.action = ""; + this.displayConfigDefault = { + header: { + horizontalAlign: "center", + verticalAlign: "bottom", + }, + }; + } + + // --- LIFE-CYCLE METHODS + update(changedProperties) { + if (changedProperties.has("opencgaSession") || + changedProperties.has("toolId") || + changedProperties.has("organization") || + changedProperties.has("config")) { + this.propertyObserver(); + } + super.update(changedProperties); + } + + updated(changedProperties) { + if (changedProperties.size > 0 && this.active) { + this.renderRemoteTable(); + } + } + + propertyObserver() { + // With each property change we must be updated config and create the columns again. No extra checks are needed. + this._config = { + ...this.getDefaultConfig(), + ...this.config, + }; + + this.gridCommons = new GridCommons(this.gridId, this, this._config); + + // Config for the grid toolbar + this.toolbarSetting = { + ...this._config, + }; + + this.toolbarConfig = { + toolId: this.toolId, + resource: "USER", + columns: this._getDefaultColumns(), + create: { + display: { + modalTitle: "User Create", + modalDraggable: true, + modalCyDataName: "modal-create", + modalSize: "modal-lg" + // disabled: true, + // disabledTooltip: "...", + }, + modalId: `${this._prefix}CreateUserModal`, + render: () => html ` + + ` + }, + }; + + this.permissions = { + "organization": () => OpencgaCatalogUtils.isOrganizationAdmin(this.organization, this.opencgaSession.user.id) ? "" : "disabled", + "study": () => OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id) ? "" : "disabled", + }; + + this.modals = { + "edit-details": { + label: "Edit Details", + icon: "fas fa-edit", + modalId: `${this._prefix}UpdateDetailsModal`, + render: () => this.renderModalDetailsUpdate(), + permission: this.permissions["organization"](), + divider: true, + }, + // ToDo 20240529 Vero: Nacho/Pedro to discuss: + // - Organization admin/owner can change usr pwd without entering current pwd + /* + "change-password": { + label: "Change Password", + icon: "fas fa-edit", + modalId: `${this._prefix}ChangePasswordModal`, + render: () => this.renderModalPasswordUpdate(), + permission: OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id) || "disabled", + }, + */ + "reset-password": { + label: "Reset Password", + icon: "fas fa-key", + modalId: `${this._prefix}ResetPasswordModal`, + render: () => this.renderModalPasswordReset(), + permission: this.permissions["organization"](), + divider: true, + }, + "change-status": { + label: "Change Status", + icon: "fas fa-sign-in-alt", + modalId: `${this._prefix}ChangeStatusModal`, + render: () => this.renderModalStatusUpdate(), + permission: this.permissions["organization"](), + }, + "change-admin": { + labelAdd: "Add as Admin", + labelRemove: "Remove as Admin", + iconAdd: "fas fa-user-plus", + iconRemove: "fas fa-user-minus", + modalId: `${this._prefix}ChangeAdminModal`, + render: action => this.renderModalAdminChange(action), + permission: this.permissions["organization"](), + divider: true, + }, + "delete": { + label: "Delete User", + icon: "fas fa-trash-alt ", + color: "text-danger", + // modalId: `${this._prefix}DeleteUserModal`, + // render: () => this.renderModalDeleteUser(), + permission: "disabled", // CAUTION: Not possible to delete users for now + }, + }; + } + + // *** PRIVATE METHODS *** + renderRemoteTable() { + if (this.opencgaSession?.opencgaClient && this.organization.id) { + this._columns = this._getDefaultColumns(); + this.table = $("#" + this.gridId); + this.table.bootstrapTable("destroy"); + this.table.bootstrapTable({ + theadClasses: "table-light", + buttonsClass: "light", + columns: this._columns, + method: "get", + sidePagination: "server", + iconsPrefix: GridCommons.GRID_ICONS_PREFIX, + icons: GridCommons.GRID_ICONS, + uniqueId: "id", + // Table properties + pagination: this._config.pagination, + pageSize: this._config.pageSize, + pageList: this._config.pageList, + paginationVAlign: "both", + // formatShowingRows: this.gridCommons.formatShowingRows, + detailView: !!this.detailFormatter, + loadingTemplate: () => GridCommons.loadingFormatter(), + ajax: params => { + let result = null; + this.filters = { + organization: this.organization.id, + limit: params.data.limit, + skip: params.data.offset || 0, + count: !this.table.bootstrapTable("getOptions").pageNumber || this.table.bootstrapTable("getOptions").pageNumber === 1, + }; + + // Store the current filters + this.opencgaSession.opencgaClient.users() + .search(this.filters) + .then(response => { + result = response; + return response; + }) + .then(() => { + // Prepare data for columns extensions + const rows = result.responses?.[0]?.results || []; + return this.gridCommons.prepareDataForExtensions(this.COMPONENT_ID, this.opencgaSession, this.filters, rows); + }) + .then(() => params.success(result)) + .catch(error => { + console.error(error); + params.error(error); + }); + }, + responseHandler: response => { + const result = this.gridCommons.responseHandler(response, $(this.table).bootstrapTable("getOptions")); + return result.response; + }, + onClickRow: (row, selectedElement) => this.gridCommons.onClickRow(row.id, row, selectedElement), + onLoadSuccess: data => this.gridCommons.onLoadSuccess(data, 1), + onLoadError: (e, restResponse) => this.gridCommons.onLoadError(e, restResponse), + }); + } + } + + _getDefaultColumns() { + this._columns = [ + { + id: "id", + title: "User ID", + field: "id", + visible: this.gridCommons.isColumnVisible("id"), + formatter: (value, row) => this.userIdFormatter(value, row), + }, + { + id: "name", + title: "Name", + field: "name", + visible: this.gridCommons.isColumnVisible("name"), + }, + { + id: "email", + title: "Email", + field: "email", + visible: this.gridCommons.isColumnVisible("email"), + }, + { + id: "authentication", + title: "Authentication", + field: "internal.account.authentication.id", + visible: this.gridCommons.isColumnVisible("authentication") + }, + { + id: "failedAttempts", + title: "Failed Attempts", + field: "internal.account.failedAttempts", + visible: this.gridCommons.isColumnVisible("failedAttempts"), + }, + { + id: "status", + title: "Status", + field: "internal.status", + formatter: value => CatalogGridFormatter.userStatusFormatter(value, this._config.userStatus), + visible: this.gridCommons.isColumnVisible("status") + }, + { + id: "lastModifiedDate", + title: "Last Modified Date", + field: "internal.lastModified", + formatter: (value, row) => UtilsNew.dateFormatter(row.internal.lastModified), + visible: this.gridCommons.isColumnVisible("lastModifiedDate") + }, + { + id: "dates", + title: "Expiration / Creation Dates", + field: "dates", + halign: this.displayConfigDefault.header.horizontalAlign, + valign: "middle", + formatter: (value, row) => this.datesFormatter(value, row), + visible: this.gridCommons.isColumnVisible("dates") + }, + ]; + + if (this._config.annotations?.length > 0) { + this.gridCommons.addColumnsFromAnnotations(this._columns, CatalogGridFormatter.customAnnotationFormatter, this._config); + } + + if (this.opencgaSession && this._config.showActions) { + this._columns.push({ + id: "actions", + title: "Actions", + field: "actions", + align: "center", + formatter: (value, row) => ` + + `, + events: { + "click ul>li>a": (e, value, row) => this.onActionClick(e, value, row), + }, + }); + } + + this._columns = this.gridCommons.addColumnsFromExtensions(this._columns, this.COMPONENT_ID); + return this._columns; + } + + userIdFormatter(value, user) { + // Note 20240620 vero: Viz and change owner will be implemented in following release + // const organizationOwner = this.organization.owner; + return this.organization.admins.includes(user.id) ? ` +
+ + ${value} +
+ ` : value; + } + + datesFormatter(value, user) { + const expirationDateString = UtilsNew.dateFormatter(user.internal.account.expirationDate); + const expirationDate = new Date(expirationDateString); + const currentDate = new Date(); + let expirationDateClass = null; + if (currentDate > expirationDate) { + expirationDateClass = "text-danger"; + } + return ` +
${expirationDateString}
+
${UtilsNew.dateFormatter(user.creationDate)}
+ `; + } + + // *** EVENTS *** + async onActionClick(e, value, row) { + this.action = e.currentTarget.dataset.action; + this.userId = row.id; + this.adminAction = e.currentTarget.dataset.admin ?? ""; + this.requestUpdate(); + await this.updateComplete; + // NOTE 20240804 Vero: Since reset password does not need inputs, it has been decided that it should be + // a notification instead of the regular update modal. Therefore, in this case, a modal is not created and + // should not be shown. + if (this.action !== "reset-password") { + ModalUtils.show(this.modals[this.action]["modalId"]); + } + } + + onUserUpdate(e, id) { + ModalUtils.close(id); + this.renderRemoteTable(); + } + + onUserCreate() { + // Close modal + ModalUtils.close(this.toolbarConfig.create.modalId); + } + + onCloseNotification() { + this.userId = null; + this.action = ""; + this.requestUpdate(); + } + + // *** RENDER METHODS *** + renderModalDetailsUpdate() { + return ModalUtils.create(this, `${this._prefix}UpdateDetailsModal`, { + display: { + modalTitle: `Update Details: User ${this.userId} in organization ${this.organization.id}`, + modalDraggable: true, + modalCyDataName: "modal-details-update", + modalSize: "modal-lg" + }, + render: () => { + return html` + + + `; + }, + }); + } + + /* + // Caution 20240616 Vero: Uncomment this code when endpoint fixed in OpenCGA for admin/owner change usr pwd + renderModalPasswordUpdate() { + return ModalUtils.create(this, `${this._prefix}ChangePasswordModal`, { + display: { + modalTitle: `Change Password: User ${this.userId} in organization ${this.organization.id}`, + modalDraggable: true, + modalCyDataName: "modal-password-change", + modalSize: "modal-lg" + }, + render: () => { + return html` + + + `; + }, + }); + } + */ + + renderModalPasswordReset() { + return html` + + + `; + } + + renderModalStatusUpdate() { + return ModalUtils.create(this, `${this._prefix}ChangeStatusModal`, { + display: { + modalTitle: `Update Status: User '${this.userId}' in organization '${this.organization.id}'`, + modalDraggable: true, + modalCyDataName: "modal-user-admin-status-update", + modalSize: "modal-lg" + }, + render: () => html` + + + `, + }); + } + + renderModalAdminChange(action) { + return ModalUtils.create(this, `${this._prefix}ChangeAdminModal`, { + display: { + modalTitle: `Update Organization Admins: User ${this.userId} in organization ${this.organization.id}`, + modalDraggable: true, + modalCyDataName: "modal-user-admin-admin-set", + modalSize: "modal-lg" + }, + render: () => html` + + + `, + }); + } + + renderToolbar() { + if (this._config.showToolbar) { + return html ` + + + `; + } + } + + render() { + return html` + + ${this.renderToolbar()} + +
+
+
+ + ${this.action ? this.modals[this.action]["render"](this.adminAction || null) : nothing} + `; + } + + // *** DEFAULT CONFIG *** + getDefaultConfig() { + return { + // Settings + pagination: true, + pageSize: 10, + pageList: [5, 10, 25], + multiSelection: false, + showSelectCheckbox: false, + + showToolbar: true, + showActions: true, + + showCreate: true, + showExport: false, + showSettings: false, + exportTabs: ["download", "link", "code"], + // Config + userStatus: [ + { + id: "READY", + displayLabel: "ACTIVE", // Fixme: ACTIVE | ENABLED | READY? + displayColor: "#16A83C", + displayOutline: "btn-outline-success", + description: "The user can login", + isSelectable: true, // Choice selectable by org admin/owner + isEnabled: true, // Choice visible by org admin/owner + }, + { + id: "SUSPENDED", + displayLabel: "SUSPENDED", + displayColor: "#E1351E", + displayOutline: "btn-outline-danger", + description: "The user can not login into the system", + isSelectable: true, + isEnabled: true, + }, + { + id: "BANNED", + displayLabel: "BANNED", + displayColor: "#E17F1E", + description: "User locked for more than allowed login attempts. The admin/owner can enable the user back.", + isSelectable: false, + isEnabled: true, + }, + { + id: "UNDEFINED", + displayLabel: "-", + displayColor: "#E1E2E5", + description: "The user status is unknown", + isSelectable: false, + isEnabled: false, + }, + ], + + }; + } + +} + +customElements.define("user-admin-grid", UserAdminGrid); diff --git a/src/webcomponents/organization/admin/user-admin-password-change.js b/src/webcomponents/organization/admin/user-admin-password-change.js new file mode 100644 index 0000000000..25fdd3fbc9 --- /dev/null +++ b/src/webcomponents/organization/admin/user-admin-password-change.js @@ -0,0 +1,218 @@ +/** + * Copyright 2015-2024 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {html, LitElement} from "lit"; +import UtilsNew from "../../../core/utils-new.js"; +import LitUtils from "../../commons/utils/lit-utils.js"; +import NotificationUtils from "../../commons/utils/notification-utils.js"; +import "../../user/user-password-change.js"; + +export default class UserAdminPasswordChange extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + userId: { + type: String + }, + organization: { + type: Object, + }, + opencgaSession: { + type: Object + }, + displayConfig: { + type: Object + }, + }; + } + + #init() { + this.user = {}; // Original object + this._user = {}; // Updated object + this.userId = ""; + this.displayConfig = {}; + this.updatedFields = {}; + + this._config = this.getDefaultConfig(); + } + + #setLoading(value) { + this.isLoading = value; + this.requestUpdate(); + } + + #initConfigNotification() { + this._config.notification = { + title: "", + text: "Some changes have been done in the form. Not saved changes will be lost", + type: "notification", + display: { + visible: () => { + return UtilsNew.isNotEmpty(this.updatedFields); + }, + notificationType: "warning", + }, + }; + } + + #initOriginalObjects() { + this._user = UtilsNew.objectClone(this.user); + this.updatedFields = {}; + } + + update(changedProperties) { + if (changedProperties.has("userId")) { + this.userIdObserver(); + } + if (changedProperties.has("displayConfig")) { + this._config = this.getDefaultConfig(); + if (!this._config?.notification) { + this.#initConfigNotification(); + } + } + super.update(changedProperties); + } + + userIdObserver() { + if (this.userId && this.opencgaSession) { + const params = { + organization: this.organization.id, + }; + let error; + this.#setLoading(true); + this.opencgaSession.opencgaClient.users() + .info(this.userId, params) + .then(response => { + this.user = UtilsNew.objectClone(response.responses[0].results[0]); + this.#initOriginalObjects(); + }) + .catch(reason => { + error = reason; + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason); + }) + .finally(() => { + LitUtils.dispatchCustomEvent(this, "userInfo", this.user, {}, error); + this.#setLoading(false); + }); + } + } + + render() { + return html ` + + + `; + } + + getDefaultConfig() { + return { + title: "", + showTitle: false, + items: [ + { + id: "change-password", + name: "Change Password", + active: true, + render: (user, active, opencgaSession) => { + return html` + + + `; + }, + }, + ], + }; + /* + return { + icon: "fas fa-edit", + buttons: { + clearText: "Discard Changes", + okText: "Update", + }, + display: this.displayConfig, + sections: [ + { + title: "Change Password", + elements: [ + { + type: "custom", + display: { + render: () => { + debugger + return html` + + + `; + }, + }, + }, + /* + { + title: "Reset password", + field: "pwdReset", + type: "toggle-switch", + display: { + disabled: true, + helpMessage: "Coming soon: Force user to reset the password", + }, + }, + { + title: "Expires in", + field: "pwdExpiration", + type: "input-text", + display: { + disabled: true, + helpMessage: "Coming soon: Enable password expiration", + }, + }, + */ + /* + ], + }, + ], + }; + */ + } + +} + +customElements.define("user-admin-password-change", UserAdminPasswordChange); diff --git a/src/webcomponents/organization/admin/user-admin-password-reset.js b/src/webcomponents/organization/admin/user-admin-password-reset.js new file mode 100644 index 0000000000..6e99e8e280 --- /dev/null +++ b/src/webcomponents/organization/admin/user-admin-password-reset.js @@ -0,0 +1,132 @@ +/** + * Copyright 2015-2024 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {html, LitElement} from "lit"; +import UtilsNew from "../../../core/utils-new.js"; +import LitUtils from "../../commons/utils/lit-utils.js"; +import NotificationUtils from "../../commons/utils/notification-utils.js"; +import "../../user/user-password-reset.js"; + +export default class UserAdminPasswordReset extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + userId: { + type: String + }, + opencgaSession: { + type: Object + }, + }; + } + + #init() { + this._user = null; + this._config = this.getDefaultConfig(); + } + + #setLoading(value) { + this.isLoading = value; + this.requestUpdate(); + } + + update(changedProperties) { + if (changedProperties.has("userId") || changedProperties.has("opencgaSession")) { + this.userIdObserver(); + } + super.update(changedProperties); + } + + userIdObserver() { + if (this.opencgaSession && this.userId) { + this.opencgaSession.opencgaClient.users() + .info(this.userId, { + organization: this.opencgaSession.organization.id, + }) + .then(response => { + this._user = UtilsNew.objectClone(response.responses[0].results[0]); + this.requestUpdate(); + }) + .catch(response => { + console.error(response); + }); + } + } + + onSubmit() { + let error; + this.#setLoading(true); + // Reset password + this.opencgaSession.opencgaClient.users() + .resetPassword(this._user.id) + .then(() => { + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { + title: `User Reset Password`, + message: `User ${this._user.id} password reset correctly`, + }); + LitUtils.dispatchCustomEvent(this, "closeNotification", this._user.id, {}, error); + this._user = null; + }) + .catch(reason => { + error = reason; + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason); + }) + .finally(() => { + this.#setLoading(false); + }); + } + + render() { + if (this.isLoading) { + return html``; + } + + return NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, { + title: `Reset Password: User ${this._user.id} in organization ${this.opencgaSession.organization.id}`, + message: ` + Are you sure you want to reset ${this._user.id}'s password? +

+ The user ${this._user.id} will receive an email with a temporary password in the following + email address: + ${this._user.email}. + `, + ok: () => { + this.onSubmit(); + }, + cancel: () => { + this._user = null; + LitUtils.dispatchCustomEvent(this, "closeNotification", null); + }, + }); + } + + getDefaultConfig() { + return {}; + } + +} + +customElements.define("user-admin-password-reset", UserAdminPasswordReset); diff --git a/src/webcomponents/organization/admin/user-admin-status-update.js b/src/webcomponents/organization/admin/user-admin-status-update.js new file mode 100644 index 0000000000..d16b214db2 --- /dev/null +++ b/src/webcomponents/organization/admin/user-admin-status-update.js @@ -0,0 +1,284 @@ +/** + * Copyright 2015-2024 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {html, LitElement} from "lit"; +import UtilsNew from "../../../core/utils-new.js"; +import LitUtils from "../../commons/utils/lit-utils.js"; +import NotificationUtils from "../../commons/utils/notification-utils.js"; +import CatalogGridFormatter from "../../commons/catalog-grid-formatter.js"; +import FormUtils from "../../commons/forms/form-utils.js"; +import "./filters/user-status-filter.js"; + +export default class UserAdminStatusUpdate extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + userId: { + type: String, + }, + organization: { + type: Object, + }, + opencgaSession: { + type: Object, + }, + displayConfig: { + type: Object, + }, + }; + } + + #init() { + this.user = {}; // Original object + this._user = {}; // Updated object + this.userId = ""; + this.updatedFields = {}; + this.displayConfig = {}; + this.displayConfigDefault = { + style: "margin: 10px", + defaultLayout: "horizontal", + labelAlign: "right", + buttonOkText: "Update Status", + buttonOkDisabled: true, + }; + } + + #setLoading(value) { + this.isLoading = value; + this.requestUpdate(); + } + + #initConfigNotification() { + this._config.notification = { + title: "", + text: "Some changes have been done in the form. Not saved changes will be lost", + type: "notification", + display: { + visible: () => UtilsNew.isNotEmpty(this.updatedFields), + notificationType: "warning", + }, + }; + } + + #initOriginalObjects() { + this._user = UtilsNew.objectClone(this.user); + this.updatedFields = {}; + this.displayConfigObserver(); + } + + update(changedProperties) { + if (changedProperties.has("userId")) { + this.userIdObserver(); + } + if (changedProperties.has("displayConfig")) { + this.displayConfigObserver(); + } + super.update(changedProperties); + } + + userIdObserver() { + if (this.userId && this.opencgaSession) { + const params = { + organization: this.organization.id, + }; + let error; + this.#setLoading(true); + this.opencgaSession.opencgaClient.users() + .info(this.userId, params) + .then(response => { + this.user = UtilsNew.objectClone(response.responses[0].results[0]); + this.#initOriginalObjects(); + }) + .catch(reason => { + error = reason; + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason); + }) + .finally(() => { + LitUtils.dispatchCustomEvent(this, "userInfo", this.user, {}, error); + this.#setLoading(false); + }); + } + } + + displayConfigObserver() { + this.displayConfig = { + ...this.displayConfigDefault, + ...this.displayConfig, + }; + this._config = this.getDefaultConfig(); + if (!this._config?.notification) { + this.#initConfigNotification(); + } + } + + onFieldChange(e) { + const param = e.detail.param; + this.updatedFields = FormUtils.getUpdatedFields( + this.user, + this.updatedFields, + param, + e.detail.value, + e.detail.action); + + this._config.display.buttonOkDisabled = UtilsNew.isEmpty(this.updatedFields); + this._config = {...this._config}; + this.requestUpdate(); + } + + onClear() { + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, { + title: "Discard changes", + message: "Are you sure you want to discard the changes made?", + ok: () => { + this.#initOriginalObjects(); + // We need to dispatch a component clear event + LitUtils.dispatchCustomEvent(this, "userClear", null, { + user: this.user, + }); + }, + }); + } + + onSubmit() { + let error; + const params = { + includeResult: true, + }; + const updateParams = FormUtils.getUpdateParams(this._user, this.updatedFields); + + this.#setLoading(true); + this.opencgaSession.opencgaClient.organization() + .userUpdateStatus(this.user.id, updateParams.internal.status.id, params) + .then(response => { + this.user = UtilsNew.objectClone(response.responses[0].results[0]); + this.updatedFields = {}; + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { + title: `User Status Update`, + message: `User ${this.user.id} status has been updated correctly`, + }); + LitUtils.dispatchCustomEvent(this, "userUpdate", this.user, {}, error); + }) + .catch(reason => { + error = reason; + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason); + }) + .finally(() => { + this.#setLoading(false); + }); + } + + render() { + if (this.isLoading) { + return html``; + } + + if (!this.user.internal?.status?.id) { + return html ` +
+ + The ${this.user.id} does not have a status ID. +
+ `; + } + + return html` + + + `; + } + + getDefaultConfig() { + return { + display: this.displayConfig, + sections: [ + { + title: "Current Status", + elements: [ + { + title: "User ID", + field: "id", + type: "input-text", + display: { + disabled: true, + } + }, + { + title: "Status", + type: "complex", + display: { + containerClassName: "d-flex align-items-center", + template: "${internal.status}", + format: { + // FIXME: Displaying original data this.user.internal.status. Should not be a complex element + "internal.status": () => CatalogGridFormatter.userStatusFormatter(this.user?.internal?.status, this._config.display?.userStatus), + }, + }, + }, + ], + }, + { + title: "Change Status", + description: "You can change the status of the user here", + display: { + // titleHeader: "h4", + // titleClassName: "d-block text-secondary" + // titleStyle: "", + // descriptionClassName: "d-block text-secondary", + // descriptionStyle: "", + // visible: () => + }, + elements: [ + { + title: "New Status", + field: "internal.status.id", + type: "custom", + display: { + render: (status, dataFormFilterChange) => html ` + + + `, + }, + }, + ], + }, + ], + }; + } + +} + +customElements.define("user-admin-status-update", UserAdminStatusUpdate); diff --git a/src/webcomponents/organization/admin/user-admin-update.js b/src/webcomponents/organization/admin/user-admin-update.js new file mode 100644 index 0000000000..ebd8d6236e --- /dev/null +++ b/src/webcomponents/organization/admin/user-admin-update.js @@ -0,0 +1,344 @@ +/** + * Copyright 2015-2024 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {html, LitElement} from "lit"; +import UtilsNew from "../../../core/utils-new.js"; +import LitUtils from "../../commons/utils/lit-utils.js"; +import FormUtils from "../../commons/forms/form-utils.js"; +import NotificationUtils from "../../commons/utils/notification-utils.js"; +import "../../user/user-password-change.js"; +import "../../user/user-password-reset.js"; + +export default class UserAdminUpdate extends LitElement { + + constructor() { + super(); + + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + userId: { + type: String + }, + organization: { + type: Object, + }, + studyId: { + type: String, + }, + active: { + type: Boolean, + }, + opencgaSession: { + type: Object + }, + displayConfig: { + type: Object + }, + }; + } + + #init() { + this._user = {}; + this.userId = ""; + this.studyId = ""; + this.displayConfig = {}; + this.updatedFields = {}; + + this._config = this.getDefaultConfig(); + } + + #setLoading(value) { + this.isLoading = value; + this.requestUpdate(); + } + + #initConfigNotification() { + this._config.notification = { + title: "", + text: "Some changes have been done in the form. Not saved changes will be lost", + type: "notification", + display: { + visible: () => { + return UtilsNew.isNotEmpty(this.updatedFields); + }, + notificationType: "warning", + }, + }; + } + + #initUser() { + // 1. Group contains params: (a) id: e.g. "@admins", (b) userIds: e.g. ["test"] + this._user = UtilsNew.objectClone(this.user); + // 2. In the update form, we need to manage as well the permissions of this group. + // Retrieve ACL permissions. Check if this study group has acl + // CAUTION: study does not have acl? + // const groupPermissions = this._study?.acl + // ?.find(acl => acl.member === this.opencgaSession.user.id)?.groups + // ?.find(group => group.id === this.group.id)?.permissions || []; + // // 3. Add current permissions and template key to the object group + // this.group = { + // ...this.group, + // permissions: groupPermissions, + // template: "", // Fixme: not sure how to retrieve template + // }; + this.initOriginalObjects(); + } + + #initOriginalObjects() { + this._user = UtilsNew.objectClone(this.user); + this.updatedFields = {}; + } + + update(changedProperties) { + if ((changedProperties.has("userId") && this.active)) { + this.userIdObserver(); + } + if (changedProperties.has("displayConfig")) { + this._config = this.getDefaultConfig(); + if (!this._config?.notification) { + this.#initConfigNotification(); + } + } + super.update(changedProperties); + } + + userIdObserver() { + if (this.userId && this.opencgaSession) { + const params = { + organization: this.organization.id, + }; + let error; + this.#setLoading(true); + this.opencgaSession.opencgaClient.users() + .info(this.userId, params) + .then(response => { + this.user = UtilsNew.objectClone(response.responses[0].results[0]); + this.#initOriginalObjects(); + }) + .catch(reason => { + error = reason; + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason); + }) + .finally(() => { + LitUtils.dispatchCustomEvent(this, "userInfo", this.study, {}, error); + this.#setLoading(false); + }); + } + } + + // Uncomment to post-process data-form manipulation + // onFieldChange(e) { + // debugger + // this.updatedFields = e.detail?.updatedFields || {}; + // this.requestUpdate(); + // } + + onFieldChange(e) { + const param = e.detail.param; + this.updatedFields = FormUtils.getUpdatedFields( + this.group, + this.updatedFields, + param, + e.detail.value, + e.detail.action); + if (param === "template") { + this._group.template = e.detail.value; + } + this.requestUpdate(); + } + + onClear() { + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, { + title: "Discard changes", + message: "Are you sure you want to discard the changes made?", + ok: () => { + this.initOriginalObjects(); + this.requestUpdate(); + // We need to dispatch a component clear event + LitUtils.dispatchCustomEvent(this, "groupClear", null, { + group: this._group, + }); + }, + }); + } + + onSubmit() { + const paramsAction = { + action: "SET" + }; + const studyAclParams = { + study: this.studyId, + template: this._group.template, + // permissions: this._group.permissions, + }; + let error; + this.#setLoading(true); + this.opencgaSession.opencgaClient.studies() + .updateAcl(this.groupId, paramsAction, studyAclParams) + .then(response => { + this.group = UtilsNew.objectClone(response.responses[0].results[0]); + this.updatedFields = {}; + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { + title: `User Update`, + message: `User ${this.userId} updated correctly`, + }); + }) + .catch(reason => { + error = reason; + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason); + }) + .finally(() => { + LitUtils.dispatchCustomEvent(this, "updateUser", this.group, {}, error); + this.#setLoading(false); + }); + } + + render() { + return html ` + + + `; + } + + getDefaultConfig() { + return { + icon: "fas fa-edit", + buttons: { + clearText: "Discard Changes", + okText: "Update", + }, + display: this.displayConfig, + sections: [ + { + title: "Details", + elements: [ + { + title: "User Name", + field: "name", + type: "input-text", + display: { + helpMessage: "Edit the user name...", + }, + }, + { + title: "User email", + field: "email", + type: "input-text", + display: { + helpMessage: "Edit the user email...", + }, + }, + { + title: "Enable user", + field: "enabled", + type: "toggle-switch", + display: { + disabled: true, + helpMessage: "Coming soon: Enable/Disable a user in an organization", + }, + }, + + ], + }, + { + title: "Credentials", + elements: [ + { + title: "Set password", + type: "custom", + display: { + render: (data, active, opencgaSession) => html` + + + `, + }, + }, + { + title: "Reset password", + type: "custom", + display: { + render: (data, active, opencgaSession) => html` + + + `, + }, + }, + /* + { + title: "Reset password", + field: "pwdReset", + type: "toggle-switch", + display: { + disabled: true, + helpMessage: "Coming soon: Force user to reset the password", + }, + }, + { + title: "Expires in", + field: "pwdExpiration", + type: "input-text", + display: { + disabled: true, + helpMessage: "Coming soon: Enable password expiration", + }, + }, + */ + ], + }, + { + title: "Permissions", + elements: [ + { + title: "Templates", + field: "template", + type: "toggle-buttons", + allowedValues: ["analyst", "view_only"], + }, + // TODO: Implement customised permissions for the group + // { + // title: "Permissions", + // field: "permissions", + // type: "toggle-buttons", + // }, + ], + }, + // { + // title: "Users", + // elements: [ + // ], + // }, + ], + }; + } + +} + +customElements.define("user-admin-update", UserAdminUpdate); diff --git a/src/webcomponents/project/project-create.js b/src/webcomponents/project/project-create.js index 0a3715db54..1c806947e2 100644 --- a/src/webcomponents/project/project-create.js +++ b/src/webcomponents/project/project-create.js @@ -16,10 +16,8 @@ import {LitElement, html} from "lit"; import LitUtils from "../commons/utils/lit-utils.js"; -import FormUtils from "../commons/forms/form-utils.js"; import NotificationUtils from "../commons/utils/notification-utils.js"; import UtilsNew from "../../core/utils-new.js"; -import Types from "../commons/types.js"; export default class ProjectCreate extends LitElement { @@ -53,7 +51,6 @@ export default class ProjectCreate extends LitElement { defaultLayout: "horizontal", buttonOkText: "Create" }; - this._config = this.getDefaultConfig(); } #initOriginalObject() { @@ -64,7 +61,7 @@ export default class ProjectCreate extends LitElement { }, cellbase: { url: "https://ws.zettagenomics.com/cellbase", - version: "v5.1" + version: "v5.8" } }; this._project = UtilsNew.objectClone(this.project); @@ -86,72 +83,45 @@ export default class ProjectCreate extends LitElement { super.update(changedProperties); } - onFieldChange(e, field) { - const param = field || e.detail.param; - switch (param) { - case "id": - case "name": - case "description": - case "organism.scientificName": - case "organism.assembly": - case "cellbase": - this.project = { - ...FormUtils.createObject( - this.project, - param, - e.detail.value - ) - }; - } + onFieldChange(e) { + this._project = {...e.detail.data}; // force to refresh the object-list this.requestUpdate(); } onClear() { - const resetForm = () => { - this.#initOriginalObject(); - this._config = this.getDefaultConfig(); - this.requestUpdate(); - }; - if (!this.displayConfig?.modal) { - NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, { - title: "Clear project", - message: "Are you sure to clear?", - ok: () => { - resetForm(); - }, - }); - } else { - LitUtils.dispatchCustomEvent(this, "clearProject"); - resetForm(); - } + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, { + title: "Clear project", + message: "Are you sure to clear?", + ok: () => { + this.#initOriginalObject(); + this._config = this.getDefaultConfig(); + this.requestUpdate(); + }, + }); } onSubmit() { const params = { - includeResult: true + includeResult: true, }; - let project, error; + let error; this.#setLoading(true); this.opencgaSession.opencgaClient.projects() - .create(this.project, params) - .then(response => { + .create(this._project, params) + .then(() => { this.#initOriginalObject(); - this._config = this.getDefaultConfig(); - - project = response.responses[0].results[0]; NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { title: "Project Create", message: "New project created correctly" }); - LitUtils.dispatchCustomEvent(this, "sessionUpdateRequest"); + LitUtils.dispatchCustomEvent(this, "projectCreate", {}, {}); + LitUtils.dispatchCustomEvent(this, "sessionUpdateRequest", {}, {}); }) .catch(reason => { - project = this.project; error = reason; NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, error); }) .finally(() => { - LitUtils.dispatchCustomEvent(this, "projectCreate", project, {}, error); this.#setLoading(false); }); } @@ -163,7 +133,7 @@ export default class ProjectCreate extends LitElement { return html` !UtilsNew.objectCompare(this.project, this._project), - notificationType: "warning", - }, - }, { name: "Project ID", field: "id", @@ -239,8 +200,12 @@ export default class ProjectCreate extends LitElement { title: "Version", field: "cellbase.version", type: "select", - allowedValues: ["v5.0", "v5.1"], - defaultValue: "v5.1", + // FIXME Vero 20240712: Waiting for Nacho's advise + // Can they be queried? In cellbase more versions are responding: + // 5.0, 5.1, 5.2, 5.3, 5.4, 5.5, 5.8 + // https://ws.zettagenomics.com/cellbase/webservices/#!/Gene/getInfo_1 + allowedValues: ["v5.0", "v5.1", "v5.2", "v5.8"], + defaultValue: "v5.8", display: { // placeholder: "Add version" } @@ -259,7 +224,7 @@ export default class ProjectCreate extends LitElement { ] } ] - }); + }; } } diff --git a/src/webcomponents/project/project-update.js b/src/webcomponents/project/project-update.js index fcae974962..6356384293 100644 --- a/src/webcomponents/project/project-update.js +++ b/src/webcomponents/project/project-update.js @@ -80,7 +80,10 @@ export default class ProjectUpdate extends LitElement { this.projectIdObserver(); } if (changedProperties.has("displayConfig")) { - this.displayConfig = {...this.displayConfigDefault, ...this.displayConfig}; + this.displayConfig = { + ...this.displayConfigDefault, + ...this.displayConfig + }; this._config = this.getDefaultConfig(); } super.update(changedProperties); @@ -165,7 +168,7 @@ export default class ProjectUpdate extends LitElement { const params = { includeResult: true }; - let project, error; + let error; this.#setLoading(true); this.opencgaSession.opencgaClient.projects() .update(this.project?.fqn, this.updateParams, params) @@ -177,15 +180,14 @@ export default class ProjectUpdate extends LitElement { title: "Project Update", message: "Project updated correctly" }); - LitUtils.dispatchCustomEvent(this, "sessionUpdateRequest"); + LitUtils.dispatchCustomEvent(this, "sessionUpdateRequest", this._project, {}, error); }) .catch(reason => { - project = this.project; error = reason; NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, error); }) .finally(() => { - LitUtils.dispatchCustomEvent(this, "projectUpdate", this.project, {}, error); + // LitUtils.dispatchCustomEvent(this, "projectUpdate", project, {}, error); this.#setLoading(false); }); } diff --git a/src/webcomponents/project/projects-admin.js b/src/webcomponents/project/projects-admin.js index 8f55543463..58a30c4031 100644 --- a/src/webcomponents/project/projects-admin.js +++ b/src/webcomponents/project/projects-admin.js @@ -20,7 +20,7 @@ import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-util import {guardPage} from "../commons/html-utils.js"; import "../commons/tool-header.js"; import "../study/study-form.js"; -import "../study/study-create.js"; +import "../study/admin/study-create.js"; import "./project-create.js"; import "./project-update.js"; @@ -218,7 +218,7 @@ export default class ProjectsAdmin extends LitElement { let content; switch (id) { case "createProject": - content=html` + content = html` { + this.organization = UtilsNew.objectClone(response.responses[0].results[0]); + }) + .catch(reason => { + // this.organization = {}; + error = reason; + console.error(reason); + }) + .finally(() => { + LitUtils.dispatchCustomEvent(this, "organizationInfo", this.organization, {}, error); + this.#setLoading(false); + }); + } } studyIdObserver() { @@ -102,9 +131,27 @@ export default class StudyAdminIva extends LitElement { } } + opencgaSessionObserver() { + this.study = this.opencgaSession.study; + this._config = this.getDefaultConfig(); + } + // --- RENDER METHOD --- render() { - return html` + if (this.opencgaSession.study && this.organization) { + if (!OpencgaCatalogUtils.isOrganizationAdmin(this.organization, this.opencgaSession.user.id) && + !OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id)) { + return html` + +
+

Restricted access

+

The page you are trying to access has restricted access.

+

Please refer to your system administrator.

+
+ `; + } + + return html` `; + } } getDefaultConfig() { diff --git a/src/webcomponents/study/admin/study-admin-permissions.js b/src/webcomponents/study/admin/study-admin-permissions.js index 5cd5f4110d..087500d182 100644 --- a/src/webcomponents/study/admin/study-admin-permissions.js +++ b/src/webcomponents/study/admin/study-admin-permissions.js @@ -70,11 +70,11 @@ export default class StudyAdminPermissions extends LitElement { active: true, render: (study, active, opencgaSession) => { return html` - - `; + `; } }, { diff --git a/src/webcomponents/study/admin/study-admin-users.js b/src/webcomponents/study/admin/study-admin-users.js index 4a84774401..8ea61f04ea 100644 --- a/src/webcomponents/study/admin/study-admin-users.js +++ b/src/webcomponents/study/admin/study-admin-users.js @@ -99,9 +99,11 @@ export default class StudyAdminUsers extends LitElement { studyObserver() { if (this.study) { + // FIXME!!!!!: admin/owner organization. Ask Pedro again this.owner = this.study.fqn.split("@")[0]; this.groupsMap = new Map(); - this.opencgaSession.opencgaClient.studies().groups(this.study.fqn) + this.opencgaSession.opencgaClient.studies() + .groups(this.study.fqn) .then(response => { const groups = response.responses[0].results; // Remove in OpenCGA 2.1 @@ -157,14 +159,36 @@ export default class StudyAdminUsers extends LitElement { } groupFormatter(value, row) { - // const isOwner = this.field.owner === this.field.loggedUser; - const isOwner = OpencgaCatalogUtils.checkUserAccountView(this.field.owner, this.field.loggedUser); + // NOTE: + // - Only organization owner/admin can manage study admins + // - Study admins can add/remove users to the study and groups (except group admins) + // - Regular users (no org owner/admin, no study admin) can not access this site + const isOrganizationAdmin = OpencgaCatalogUtils.isOrganizationAdmin(this.field.opencgaSession.organization, row.id); + const isLoggedUserOrganizationAdmin = OpencgaCatalogUtils.isOrganizationAdmin(this.field.opencgaSession.organization, this.field.loggedUser); + const isStudyAdmin = OpencgaCatalogUtils.isAdmin(this.field.study, row.id); + const isLoggedUserStudyAdmin = OpencgaCatalogUtils.isAdmin(this.field.study, this.field.loggedUser); const checked = this.field.groupsMap?.get(this.field.groupId).findIndex(e => e.id === row.id) !== -1; if (this.field.groupId === "@admins") { - return ``; + if (isLoggedUserOrganizationAdmin) { + // return ``; + return ``; + } else { + return ``; + } } else { - return ``; + // return ``; + return ``; } + // Todo: the checked attribute is not working. Investigate why and fix + // return ` + //
+ // + //
+ // `; } async onCheck(e, value, row, group, context) { @@ -189,9 +213,8 @@ export default class StudyAdminUsers extends LitElement { NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { message: messageAlert, }); - // this.notifyStudyUpdateRequest(); LitUtils.dispatchCustomEvent(this, "studyUpdateRequest", this.study.fqn); - this.requestUpdate(); + // this.requestUpdate(); } catch (error) { // console.error("Message error: ", error); NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, error); @@ -212,7 +235,10 @@ export default class StudyAdminUsers extends LitElement { groupId: group, groupsMap: this.groupsMap, owner: this.owner, - loggedUser: this.opencgaSession?.user?.id + loggedUser: this.opencgaSession?.user?.id, + // Terrible two hacks for having access to the organization + opencgaSession: this.opencgaSession, + study: this.study, }, rowspan: 1, colspan: 1, @@ -331,20 +357,18 @@ export default class StudyAdminUsers extends LitElement { console.log("User already exists in the study"); return; } + const params = { + study: this.study.fqn, + template: "", + permissions: "", + }; - this.opencgaSession.opencgaClient.studies().updateUsers(this.study.fqn, "@members", {users: [this.addUserId]}, {action: "ADD"}) - .then(res => { + this.opencgaSession.opencgaClient.studies() + // .updateUsers(this.study.fqn, "@members", {users: [this.addUserId]}, {action: "ADD"}) + .updateAcl(this.addUserId, {action: "ADD"}, params) + .then(() => { this.addUserId = ""; - this.requestUpdate(); - - this.dispatchEvent(new CustomEvent("studyUpdateRequest", { - detail: { - value: this.study.fqn - }, - bubbles: true, - composed: true - })); - + LitUtils.dispatchCustomEvent(this, "studyUpdateRequest", this.study.fqn); NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { message: "User added.", }); @@ -370,20 +394,31 @@ export default class StudyAdminUsers extends LitElement { onRemoveUserTest(e, row) { NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, { title: `Remove user '${row.id}'`, - message: `Are you sure you want to remove user '${row.id}'?`, + message: `Are you sure you want to remove user '${row.id}' from the study ${this.study.fqn}?`, display: { okButtonText: "Yes, remove user", }, ok: () => { - this.opencgaSession.opencgaClient.studies().updateUsers(this.study.fqn, "@members", {users: [row.id]}, {action: "REMOVE"}) + // this.opencgaSession.opencgaClient.studies() + // .updateUsers(this.study.fqn, "@members", {users: [row.id]}, {action: "REMOVE"}) + const params= { + includeResult: true, + action: "REMOVE", + }; + const data = { + users: [row.id], + }; + this.opencgaSession.opencgaClient.studies() + .updateGroupsUsers(this.study.fqn, "@members", data, params) .then(() => { - this.requestUpdate(); LitUtils.dispatchCustomEvent(this, "studyUpdateRequest", this.study.fqn); - this.requestUpdate(); - NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { message: "User removed", }); + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { + title: `User in Group Update`, + message: `User ${row.id} REMOVED from @members in study ${this.study.id} correctly`, + }); }).catch(err => { console.error(err); }); @@ -400,16 +435,8 @@ export default class StudyAdminUsers extends LitElement { this.opencgaSession.opencgaClient.studies().updateUsers(this.study.fqn, "@members", {users: userIds}, {action: "REMOVE"}) .then(res => { this.removeUserSet = new Set(); - this.requestUpdate(); - - this.dispatchEvent(new CustomEvent("studyUpdateRequest", { - detail: { - value: this.study.fqn - }, - bubbles: true, - composed: true - })); - + // this.requestUpdate(); + LitUtils.dispatchCustomEvent(this, "studyUpdateRequest", this.study.fqn); NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { message: "User removed", }); @@ -433,8 +460,7 @@ export default class StudyAdminUsers extends LitElement { this.opencgaSession.opencgaClient.studies().updateGroups(this.study.fqn, {id: this.addGroupId}, {action: "ADD"}) .then(res => { this.addGroupId = ""; - this.requestUpdate(); - // this.notifyStudyUpdateRequest(); + // this.requestUpdate(); LitUtils.dispatchCustomEvent(this, "studyUpdateRequest", this.study.fqn); NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { @@ -489,20 +515,8 @@ export default class StudyAdminUsers extends LitElement { NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_INFO, { message: messageAlert, }); - - // this.notifyStudyUpdateRequest(); LitUtils.dispatchCustomEvent(this, "studyUpdateRequest", this.study.fqn); - this.requestUpdate(); - } - - notifyStudyUpdateRequest() { - this.dispatchEvent(new CustomEvent("studyUpdateRequest", { - detail: { - value: this.study.fqn - }, - bubbles: true, - composed: true - })); + // this.requestUpdate(); } render() { @@ -540,9 +554,16 @@ export default class StudyAdminUsers extends LitElement {