-
+
@@ -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`
+ this.onFilterChange(e)}"
+ value="${status.id}"
+ ?checked="${status.id === this.status}">
+
+ `: 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`
+
this.onFieldChange(e)}"
+ @clear="${e => this.onClear(e)}"
+ @submit="${e => this.onSubmit(e)}">
+ `;
+ }
+
+ 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`
+
this.onSubmit(e)}">
+
+ `;
+ }
+
+ 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`
+
this.onGroupEvent(e, `${this._prefix}UpdatePermissionsModal`)}">
+
+ `,
+ });
+ }
+
+ 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`
+
this.onGroupEvent(e, `${this._prefix}DeleteModal`)}">
+
+ `,
+ });
+ }
+
+ 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 `
+
this.onFieldChange(e)}"
+ @clear="${this.onClear}"
+ @submit="${this.onSubmit}">
+
+ `;
+ }
+
+ 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`
+
+
+ this.onFilterChange("userId", e.detail.value)}">
+
+
+ `: nothing}
+
+ ${~this._config.filter.sections[0].filters.findIndex(field => field.id === "action") ? html`
+
+
+ this.onFilterChange("action", e.detail.value)}">
+
+
+ ` : nothing}
+
+ ${~this._config.filter.sections[0].filters.findIndex(field => field.id === "resource") ? html`
+
+
+ this.onFilterChange("resource", e.detail.value)}">
+
+
+ ` : nothing}
+
+ ${~this._config.filter.sections[0].filters.findIndex(field => field.id === "status") ? html`
+
+
+ this.onFilterChange("status", e.detail.value)}">
+
+
+ ` : 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`
+
this.onFieldChange(e)}"
+ @clear="${this.onClear}"
+ @submit="${this.onSubmit}">
+
+ `;
+ }
+
+ // *** 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.onProjectCreate(e)}">
+ `
+ },
+ };
+
+ 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.onStudyCreate()}">
+
+ `,
+ },
+ };
+
+ 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`
+
this.onStudyEvent(e, `${this._prefix}CreateGroupModal`)}">
+
+ `,
+ });
+ }
+
+ 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`
+
this.onStudyEvent(e, `${this._prefix}UpdateStudyModal`)}">
+
+ `,
+ });
+ }
+
+ 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`
+
this.onStudyEvent(e, `${this._prefix}ManageUsersStudyModal`)}">
+
+ `;
+ }
+ });
+ }
+
+ 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()}
+
this.onFieldChange(e)}"
+ @clear="${e => this.onClear(e)}"
+ @submit="${e => this.onSubmit(e)}">
+
+ `;
+ }
+
+ 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`
+
+ this.onUserGroupChange(e, userId, group.id)}">
+ ${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`
+
this.onFieldChange(e)}"
+ @clear="${e => this.onClear(e)}"
+ @submit="${e => this.onSubmit(e)}">
+ `;
+ }
+
+ 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 `
+
this.onFieldChange(e)}"
+ @submit="${this.onSubmit}"
+ @clear="${this.onClear}">
+
+ `;
+ }
+
+ 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.onUserCreate(e)}">
+ `
+ },
+ };
+
+ 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`
+
this.onUserUpdate(e, `${this._prefix}UpdateDetailsModal`)}">
+
+ `;
+ },
+ });
+ }
+
+ /*
+ // 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`
+
this.onUserUpdate(e, `${this._prefix}ChangePasswordModal`)}">
+
+ `;
+ },
+ });
+ }
+ */
+
+ renderModalPasswordReset() {
+ return html`
+
this.onCloseNotification(e)}">
+
+ `;
+ }
+
+ 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`
+
this.onUserUpdate(e, `${this._prefix}ChangeStatusModal`)}">
+
+ `,
+ });
+ }
+
+ 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`
+
this.onUserUpdate(e, `${this._prefix}ChangeAdminModal`)}">
+
+ `,
+ });
+ }
+
+ 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`
+
this.onFieldChange(e)}"
+ @clear="${this.onClear}"
+ @submit="${this.onSubmit}">
+
+ `;
+ }
+
+ 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 `
+
dataFormFilterChange(e.detail.value)}">
+
+ `,
+ },
+ },
+ ],
+ },
+ ],
+ };
+ }
+
+}
+
+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 `
+
this.onFieldChange(e)}"
+ @clear="${this.onClear}"
+ @submit="${this.onSubmit}">
+
+ `;
+ }
+
+ 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`
this.onFieldChange(e)}"
@clear="${e => this.onClear(e)}"
@@ -172,20 +142,11 @@ export default class ProjectCreate extends LitElement {
}
getDefaultConfig() {
- return Types.dataFormConfig({
- type: "form",
- display: this.displayConfig || this.displayConfigDefault,
+ return {
+ display: this.displayConfig,
sections: [
{
elements: [
- {
- type: "notification",
- text: "Some changes have been done in the form. Not saved, changes will be lost",
- display: {
- visible: () => !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 {