diff --git a/.babelrc b/.babelrc deleted file mode 100644 index f8e757fe90..0000000000 --- a/.babelrc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "plugins": [ - "@babel/plugin-transform-runtime" - ], - "presets": [[ - "@babel/preset-env", - { - "useBuiltIns": "usage", - "targets": ">1%, not dead, not ie 11", - "corejs": 3 - } - ]] -} diff --git a/README.md b/README.md index bd5328a2ad..4f6fd1e963 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,11 @@ # Overview JSorolla is a JavaScript library for biological and genomic data visualization. -### Documentation -You can find JSorolla documentation and tutorials at: http://docs.opencb.org/display/jsorolla/JSorolla+Home. - ### Issue Tracking -You can report bugs or request new features at [GitHub issue tracking](https://github.com/opencb/jsorolla/issues). +Found a bug or have an idea for a new feature? Let us know at https://zettagenomics.com/academic/ ### Release Notes and Roadmap -Releases notes are available at [GitHub releases](https://github.com/opencb/jsorolla/releases). - -Roadmap is available at [GitHub milestones](https://github.com/opencb/jsorolla/milestones). You can report bugs or request new features at [GitHub issue tracking](https://github.com/opencb/jsorolla/issues). - -### Versioning -JSorolla is versioned following the rules from [Semantic versioning](http://semver.org/). +Releases notes are available at [[GitHub releases](https://github.com/opencb/jsorolla/releases).](https://zettagenomics.com/release-notes/) ### Maintainers The main developers and maintainers are: diff --git a/cypress/e2e/iva/genome-browser.cy.js b/cypress/e2e/iva/genome-browser.cy.js index 71e2f9f054..015f2465a0 100644 --- a/cypress/e2e/iva/genome-browser.cy.js +++ b/cypress/e2e/iva/genome-browser.cy.js @@ -97,7 +97,7 @@ context("GenomeBrowser", () => { cy.get("@karyotype") .find(`div[data-cy="gb-karyotype-toggle"]`) - .trigger("click"); + .trigger("click", {force: true}); cy.get("@karyotypeContent") .invoke("css", "display") @@ -763,6 +763,17 @@ context("GenomeBrowser", () => { .find("polyline") .should("exist"); }); + + it("should display a tooltip when hovering the coverage", () => { + // eslint-disable-next-line cypress/no-force + cy.get("@coverage") + .find(`rect[data-cy="gb-coverage-tooltip-mask"]`) + .trigger("mouseenter", {force: true}); + + cy.get("@coverage") + .find(`text[data-cy="gb-coverage-tooltip-text"]`) + .should("not.have.css", "display", "none"); + }); }); context("alignments", () => { diff --git a/cypress/e2e/iva/variant-browser-grid.cy.js b/cypress/e2e/iva/variant-browser-grid.cy.js index ac7f4a6e8d..25e0066ab9 100644 --- a/cypress/e2e/iva/variant-browser-grid.cy.js +++ b/cypress/e2e/iva/variant-browser-grid.cy.js @@ -271,6 +271,38 @@ context("Variant Browser Grid", () => { }); }); + context("clinical info", () => { + context("cosmic column", () => { + const columnIndex = 18; + it("should display an 'x' icon if no cosmic information is available", () => { + cy.get("@variantBrowser") + .find(`tbody > tr[data-uniqueid="14:91649938:A:G"] > td`) + .eq(columnIndex) + .find("i") + .should("have.class", "fa-times"); + }); + + it("should display the number of entries and total trait associations", () => { + cy.get("@variantBrowser") + .find("tbody tr:first > td") + .eq(columnIndex) + .should("contain.text", "1 entry (1)"); + }); + + it("should display a tooltip with a link to cosmic", () => { + cy.get("@variantBrowser") + .find("tbody tr:first > td") + .eq(columnIndex) + .find("a") + .trigger("mouseover"); + + cy.get(`div[class="qtip-content"]`) + .find(`a[href^="https://cancer.sanger.ac.uk/cosmic/search?q="]`) + .should("exist"); + }); + }); + }); + context("actions", () => { const variant = "14:91649858:C:T"; beforeEach(() => { diff --git a/cypress/e2e/iva/variant-interpreter-grid-cancer-cnv.cy.js b/cypress/e2e/iva/variant-interpreter-grid-cancer-cnv.cy.js index 981229dfaa..d719156266 100644 --- a/cypress/e2e/iva/variant-interpreter-grid-cancer-cnv.cy.js +++ b/cypress/e2e/iva/variant-interpreter-grid-cancer-cnv.cy.js @@ -168,11 +168,9 @@ context("Variant Interpreter Grid Cancer CNV", () => { it("should display Cohort Stats (Population Frequencies) tooltip", () => { cy.get("tbody tr:first > td") - .eq(10) - .within(() => { - cy.get("a") - .trigger("mouseover"); - }); + .eq(13) + .find("a") + .trigger("mouseover"); cy.get(".qtip-content") .should("be.visible"); }); diff --git a/cypress/e2e/iva/variant-interpreter-grid-cancer.cy.js b/cypress/e2e/iva/variant-interpreter-grid-cancer.cy.js index de196e0fe9..ba7159e103 100644 --- a/cypress/e2e/iva/variant-interpreter-grid-cancer.cy.js +++ b/cypress/e2e/iva/variant-interpreter-grid-cancer.cy.js @@ -208,23 +208,20 @@ context("Variant Interpreter Grid Cancer", () => { }); }); - it("should display cohort stats (population frequencies) tooltip", () => { + it("should display cohort stats tooltip", () => { cy.get("tbody tr:first > td") - .eq(10) - .within(() => { - cy.get("a").trigger("mouseover"); - }); + .eq(13) + .find("a") + .trigger("mouseover"); cy.get(".qtip-content") .should("be.visible"); }); - it("should reference population frequencies tooltip", () => { + it("should display reference population frequencies tooltip", () => { cy.get("tbody tr:first > td") - .eq(11) - .within(() => { - cy.get("a") - .trigger("mouseover"); - }); + .eq(14) + .find("a") + .trigger("mouseover"); cy.get(".qtip-content") .should("be.visible"); }); diff --git a/cypress/e2e/iva/variant-interpreter-grid-germline.cy.js b/cypress/e2e/iva/variant-interpreter-grid-germline.cy.js index e2b4ff34ee..210841ffad 100644 --- a/cypress/e2e/iva/variant-interpreter-grid-germline.cy.js +++ b/cypress/e2e/iva/variant-interpreter-grid-germline.cy.js @@ -168,51 +168,23 @@ context("Variant Interpreter Grid Germiline", () => { }); }); - it("should display cohort stats (Population Frequencies) tooltip", () => { + it("should display cohort stats tooltip", () => { cy.get("tbody tr:first > td") - .eq(9) - .within(() => { - cy.get("a") - .trigger("mouseover"); - }); + .eq(12) + .find("a") + .trigger("mouseover"); cy.get(".qtip-content") .should("be.visible"); }); it("should display reference population frequencies tooltip", () => { cy.get("tbody tr:first > td") - .eq(10) - .within(() => { - cy.get("a") - .trigger("mouseover"); - }); - cy.get(".qtip-content") - .should("be.visible"); - }); - - it("should display ACMG Prediction (Classification) tooltip", () => { - cy.get("tbody tr:first > td") - .eq(16) - .within(() => { - cy.get("a") - .trigger("mouseover"); - }); - cy.get(".qtip-content") - .should("be.visible"); - }); - - it("should display OMIM Prediction (Classification) tooltip", () => { - UtilsTest.changePage(browserInterpreterGrid,2); - - cy.get("tbody tr:nth-child(6) > td:nth-child(15)") - .within(() => { - cy.get("a") - .trigger("mouseover"); - }); + .eq(13) + .find("a") + .trigger("mouseover"); cy.get(".qtip-content") .should("be.visible"); }); - }); context("Helpers", () => { diff --git a/cypress/support/utils-test.js b/cypress/support/utils-test.js index 0a8d30d36e..df652cce4b 100644 --- a/cypress/support/utils-test.js +++ b/cypress/support/utils-test.js @@ -14,24 +14,11 @@ * limitations under the License. */ -import UtilsNew from "../../src/core/utils-new.js"; -import JSZip from "jszip"; import {TIMEOUT} from "./constants.js"; export default class UtilsTest { - - static getFileJson = async (path, filename ) => { - try { - const zipFiles = await JSZip.loadAsync(UtilsNew.importBinaryFile(path)); - const content = await zipFiles.file(filename).async("string"); - return JSON.parse(content); - } catch (err) { - console.error("File not exist", err); - } - } - static getByDataTest = (selector, tag, ...args) => cy.get(`div[data-testid='${selector}'] ${tag ?? ""}`, ...args); static setInput = (selectors, val) => { diff --git a/dependency-graph.sh b/dependency-graph.sh deleted file mode 100755 index dfa8762ea2..0000000000 --- a/dependency-graph.sh +++ /dev/null @@ -1,23 +0,0 @@ -exclude=\ -"node_modules|"\ -"./deprecated|"\ -"src/genome-browser|"\ -"src/core|"\ -"src/core/src/core/utils.js|"\ -"src/core/utilsNew.js|"\ -"src/core/clients|"\ -"src/core/visualisation|"\ -"src/webcomponents/loading-spinner.js|"\ -"src/webcomponents/NotificationUtils.js|"\ -"src/webcomponents/PolymerUtils.js|"\ -"src/webcomponents/opencga/clinical/obsolete|"\ -"src/webcomponents/variant/deprecated|"\ -"src/webcomponents/Notification.js|"\ -"src/webcomponents/commons/filters/deprecated|"\ -"src/webcomponents/commons" - -depcruise "src/webcomponents/**/*.js" -x "^($exclude)" --output-type dot | dot -T svg > dependency.svg -depcruise "src/sites/iva/**/*.js" -x "^($exclude)" --output-type json > dependency.json - -# depcruise "lib/jsorolla/src/core/webcomponents/**/*.js" -x "^($exclude)" --output-type dot | dot -Gsplines=ortho -Grankdir=TD -T svg > dependency.svg -# depcruise "lib/jsorolla/src/core/webcomponents/**/*.js" -x "^($exclude)" --output-type ddot | dot -Gsplines=ortho -T svg > dependency.svg diff --git a/docker/iva-app/Dockerfile b/docker/iva-app/Dockerfile index 6768e4fc9f..6923534ff7 100644 --- a/docker/iva-app/Dockerfile +++ b/docker/iva-app/Dockerfile @@ -1,10 +1,8 @@ FROM httpd:2.4-bullseye -## Custom httpd.conf file to update exposed port from 80 to 8080 -COPY ./docker/iva-app/custom-httpd.conf /usr/local/apache2/conf/httpd.conf ## To run the docker use: ## docker build -f ./docker/iva-app/Dockerfile -t iva-httpd . -## docker run --name jsorolla -p 8888:8080 opencb/iva-app +## docker run --name jsorolla -p 8888:80 opencb/iva-app ## Then open: http://localhost:8888/iva o http://localhost:8888/api LABEL org.label-schema.vendor="OpenCB" \ @@ -17,27 +15,23 @@ LABEL org.label-schema.vendor="OpenCB" \ ## Update and create iva user RUN apt-get update && apt-get -y upgrade && \ apt-get install -y vim jq && \ - rm -rf /var/lib/apt/lists/* && \ - chown -R www-data /usr/local/apache2/logs/ + rm -rf /var/lib/apt/lists/* ## Allow to build different images by passing the path to the SITE ARG SITE=src/sites ## Copy files ## IVA -COPY --chown=www-data ./build/iva /usr/local/apache2/htdocs/iva -COPY --chown=www-data ./${SITE}/iva/conf /usr/local/apache2/htdocs/iva/conf/ -COPY --chown=www-data ./${SITE}/iva/img /usr/local/apache2/htdocs/iva/img/ +COPY ./build/iva /usr/local/apache2/htdocs/iva +COPY ./${SITE}/iva/conf /usr/local/apache2/htdocs/iva/conf/ +COPY ./${SITE}/iva/img /usr/local/apache2/htdocs/iva/img/ RUN true ## API -COPY --chown=www-data ./build/api /usr/local/apache2/htdocs/api -COPY --chown=www-data ./${SITE}/api/conf /usr/local/apache2/htdocs/api/conf/ -COPY --chown=www-data ./${SITE}/api/img /usr/local/apache2/htdocs/api/img/ - -## Run Docker images as non root -USER www-data +COPY ./build/api /usr/local/apache2/htdocs/api +COPY ./${SITE}/api/conf /usr/local/apache2/htdocs/api/conf/ +COPY ./${SITE}/api/img /usr/local/apache2/htdocs/api/img/ ## Genome Maps (Coming soon :-) ) #COPY ./build/genome-maps /usr/local/apache2/htdocs/genome-maps diff --git a/package.json b/package.json index cd9dacb10f..444aefe678 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsorolla", - "version": "3.3.0-dev", + "version": "4.0.0-dev", "description": "JSorolla is a JavaScript bioinformatic library for data analysis and genomic visualization", "repository": { "type": "git", @@ -13,88 +13,46 @@ }, "scripts": { "serve": "npm run serve:dev", - "serve:dev": "vite", - "serve:build": "vite preview", + "serve:dev": "webpack serve", + "serve:build": "cross-env PORT=4000 node ./scripts/server.js", "clean": "rm -rf build", - "build": "npm run clean && rollup -c && npm run package", + "build": "npm run clean && cross-env NODE_ENV=production webpack build", "test:open": "cypress open", "test:prepare": "./prepare-test.sh", "test:run": "cypress run --spec cypress/e2e/commons/data-form.cy.js,cypress/e2e/iva/**/*.cy.js", - "browser-support": "npx browserslist", - "build-deprecated": "npm run clean && npm run dist && npx webpack && npm run package && npm run build-genome-browser", - "build-genome-browser-demo": "for i in `ls src/genome-browser/demo`; do cat src/genome-browser/demo/$i | sed 's/..\\/..\\/..\\/node_modules/vendors/' | sed 's/..\\/..\\/..\\/dist/dist/' | sed 's/..\\/webcomponent\\//webcomponent\\//'> build/genome-browser/$i; done", - "build-new-genome-browser-demo": "npx webpack --config webpack_genome.config.js", - "build-genome-browser": "mkdir -p build/genome-browser/vendors && npm run build-genome-browser-demo && cp -r dist src/genome-browser/webcomponent build/genome-browser && cp -r node_modules/jquery node_modules/qtip2 node_modules/urijs node_modules/cookies-js node_modules/crypto-js node_modules/underscore node_modules/backbone node_modules/@fortawesome/fontawesome-free node_modules/@webcomponents node_modules/@polymer build/genome-browser/vendors", - "webcomponents": "mkdir -p dist/js/webcomponents && cp -r src/webcomponents dist/js/", - "core": "mkdir -p dist/js/core && cd src/core && cat *.js bioinfo/*js cache/*.js clients/*.js clients/cellbase/*js data-source/*.js visualisation/*.js data-adapter/feature-adapter.js data-adapter/cellbase-adapter.js data-adapter/opencga-adapter.js data-adapter/feature-template-adapter.js widgets/*.js ../webcomponents/*.js ../webcomponents/opencga/*.js ../webcomponents/commons/*.js ../webcomponents/variant/*.js > ../../dist/js/core/core.js && cd ../.. && node_modules/uglify-js/bin/uglifyjs dist/js/core/core.js -o dist/js/core/core.min.js && npm run webcomponents", - "genome-browser": "mkdir -p dist/js/genome-browser && cd src/genome-browser && cp -r webcomponent ../../dist/js/genome-browser && cat *.js renderers/renderer.js renderers/*-renderer.js tracks/feature-track.js tracks/gene-track.js tracks/alignment-track.js tracks/variant-track.js tracks/tracklist-panel.js > ../../dist/js/genome-browser/genome-browser.js && cd ../.. && node_modules/uglify-js/bin/uglifyjs dist/js/genome-browser/genome-browser.js -o dist/js/genome-browser/genome-browser.min.js", - "dist": "npm run clean && mkdir -p dist/js && npm install && npm run core && npm run genome-browser && npm run styles", - "serve-deprecated": "wds --watch --open src/sites/iva --node-resolve", "package": "mv build iva-$npm_package_version && tar zcvf iva-$npm_package_version.tar.gz iva-$npm_package_version && mv iva-$npm_package_version build", - "eslint": "eslint -c .eslintrc.json . --ignore-pattern '/web_modules/'", - "lint:js": "lint-staged", - "graph": "./dependency-graph.sh" + "eslint": "eslint -c .eslintrc.json . --ignore-pattern '/web_modules/'" }, "license": "Apache-2.0", "dependencies": { "@eonasdan/tempus-dominus": "^6.7.11", "@fortawesome/fontawesome-free": "^5.11.2", - "@highlightjs/cdn-assets": "^10.6.0", - "@polymer/polymer": "2.6.1", - "@popperjs/core": "^2.11.7", - "@svgdotjs/svg.js": "^3.0.16", - "@vaadin/router": "^1.7.2", - "animate.css": "^3.5.2", "backbone": "~1.3.3", "bootstrap": "^5.3.3", - "bootstrap-3-typeahead": "^4.0.2", - "bootstrap-colorpicker": "2.3.6", - "bootstrap-select": "1.14.0-beta3", "bootstrap-table": "1.21.2", - "bootstrap-treeview": "git@github.com:jonmiles/bootstrap-treeview.git#develop", - "bootstrap-validator": "~0.11.9", "clipboard": "^2.0.6", "cookies-js": "^1.2.3", - "countup.js": "^2.0.8", - "crypto-js": "~3.1.9-1", - "cytoscape": "~2.5.4", - "file-saver": "~1.3.2", - "highcharts": "^8.0.4", + "highcharts": "^11.4.8", "html-to-pdfmake": "^2.4.23", - "jquery": "~2.2.4", - "jquery.json-viewer": "^1.4.0", - "jszip": "^3.10.1", + "jquery": "^3.7.1", "jwt-decode": "^2.2.0", "lit": "^2.7.4", "lodash": "^4.17.19", "moment": "^2.15.1", - "pako": "~0.2.8", "pdfmake": "^0.2.7", "qtip2": "~3.0.3", "select2": "^4.1.0-rc.0", "select2-bootstrap-5-theme": "^1.3.0", - "sweetalert2": "^9.13.1", - "urijs": "~1.19.10", "vanilla-jsoneditor": "^0.7.11" }, "devDependencies": { - "@babel/core": "^7.15.5", - "@babel/plugin-transform-runtime": "^7.9.6", - "@babel/preset-env": "^7.15.4", - "@babel/runtime": "^7.15.4", "@cypress/grep": "^3.1.5", - "@rollup/plugin-babel": "^5.3.0", - "@rollup/plugin-node-resolve": "^13.0.4", - "@rollup/plugin-replace": "^4.0.0", - "@rollup/plugin-terser": "^0.4.3", - "@web/rollup-plugin-html": "^1.10.1", - "babel-eslint": "^10.0.3", - "core-js": "^3.17.2", - "crisper": "~2.1.1", - "cypress": "^12.4.0", + "copy-webpack-plugin": "^12.0.2", + "cross-env": "^7.0.3", + "css-loader": "^7.1.2", + "cypress": "^13.15.1", "cypress-mochawesome-reporter": "^3.2.3", "cypress-wait-until": "^1.7.1", - "dependency-cruiser": "^10.0.7", "eslint": "^7.29.0", "eslint-config-google": "^0.14.0", "eslint-plugin-cypress": "^2.10.3", @@ -102,24 +60,13 @@ "eslint-plugin-lit": "^1.4.1", "eslint-plugin-sort-class-members": "^1.11.0", "eslint-plugin-wc": "^1.3.0", - "file-loader": "^5.0.2", - "grunt": "^1.4.0", - "grunt-shell": "^3.0.1", - "highlight.js": "^10.6.0", + "html-webpack-plugin": "^5.6.2", "lint-staged": "^11.0.0", - "postcss": "^8.4.24", - "regenerator-runtime": "^0.13.3", - "rollup": "^2.56.3", - "rollup-plugin-cleaner": "^1.0.0", - "rollup-plugin-copy": "^3.4.0", - "rollup-plugin-delete": "^2.0.0", - "rollup-plugin-eslint": "^7.0.0", - "rollup-plugin-minify-html-literals": "^1.2.6", - "uglify-es": "~3.1.9", - "uglify-js": "^3.14.2", - "uglifycss": "0.0.29", - "vite": "^5.0.2", - "xmldom": "0.1.27" + "mime-types": "^2.1.35", + "mini-css-extract-plugin": "^2.9.1", + "webpack": "^5.96.1", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^5.1.0" }, "browserslist": [ "chrome > 79", diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index 1bed1b7037..0000000000 --- a/rollup.config.js +++ /dev/null @@ -1,250 +0,0 @@ -import html from "@web/rollup-plugin-html"; -import copy from "rollup-plugin-copy"; -import resolve from "@rollup/plugin-node-resolve"; -import replace from "@rollup/plugin-replace"; -import minifyHTML from "rollup-plugin-minify-html-literals"; -import fs from "fs"; -import path from "path"; -import del from "rollup-plugin-delete"; -import {babel} from "@rollup/plugin-babel"; -import terser from "@rollup/plugin-terser"; -import {execSync} from "child_process"; -import pkg from "./package.json"; - -// eslint-disable-next-line no-undef -const env = process.env || {}; -// eslint-disable-next-line no-undef -const buildPath = path.join(__dirname, "build"); -// eslint-disable-next-line no-undef -const sitesPath = path.join(__dirname, "src/sites"); -const patternConfig = /(config|settings|constants|tools)/gi; -const internalCss = /(global|magic-check|style|toggle-switch|genome-browser)/gi; - -// Get target sites to build -const getSitesToBuild = () => { - // Josemi 2023-09-12 NOTE: test-app has been included by default in build process - const sites = ["iva", "api", "test-app"]; - // Check if we need to include the test-app site in the sites to build list - // This can be enabled using the '--include-test-app' flag when running 'npm run build' command - // if (env.npm_config_include_test_app) { - // sites.push("test-app"); - // } - return sites; -}; - -const revision = () => { - try { - const jsorollaBranch = execSync("git rev-parse --abbrev-ref HEAD").toString(); - const jsorollaSha1 = execSync("git rev-parse HEAD").toString(); - return ` - Jsorolla Version: ${pkg.version} | Git: ${jsorollaBranch.trim()} - ${jsorollaSha1.trim()} - Build generated on: ${new Date()}\n `; - } catch (error) { - console.error(` - Status: ${error.status} - ${error.stderr.toString()} - `); - } -}; - -const isConfig = name => { - return name.match(patternConfig) !== null; -}; - -const isInternalCss = name => { - return name.match(internalCss) !== null; -}; - -const getCustomSitePath = (name, from, folder) => { - // NOTE: custom sites are not allowed for 'test-app' - if (env.npm_config_custom_site && name.toUpperCase() !== "TEST-APP") { - return `${from}custom-sites/${env.npm_config_custom_site}/${name}/${folder}`; - } - return folder; // Default path configuration -}; - -const getExtensionsPath = name => { - // NOTE: extensions are only enabled at this moment for IVA - if (env.npm_extensions && name.toUpperCase() === "IVA") { - // We need to make sure that the extensions file exists - // eslint-disable-next-line no-undef - const extensionsPath = path.join(__dirname, "extensions", "build", "extensions.js"); - if (fs.existsSync(extensionsPath)) { - return "../../../extensions/build"; - } - } - return "extensions"; -}; - -const transformHtmlContent = (html, name) => { - const annihilator = /[\s\S]*?/mg; - const parsedName = name.replace(/-/g, "_").toUpperCase(); - const configRegex = new RegExp(`{{ ${parsedName}_CONFIG_PATH }}`, "g"); - const extensionsRegex = new RegExp(`{{ ${parsedName}_EXTENSIONS_PATH }}`, "g"); - - return html - .replace("[build-signature]", revision()) - .replace(annihilator, "") - .replace(configRegex, getCustomSitePath(name, "../../../", "conf")) - .replace(extensionsRegex, getExtensionsPath(name)); -}; - -const getSiteContent = name => { - const content = fs.readFileSync(path.join(sitesPath, name, "index.html"), "utf8"); - return { - name: "index.html", - html: transformHtmlContent(content, name), - }; -}; - -const getCopyTargets = site => { - const targets = [ - { - src: `./src/sites/${site}/img`, - dest: `${buildPath}/${site}/`, - }, - { - src: "./styles/img", - dest: `${buildPath}/${site}/`, - }, - { - src: "./styles/fonts", - dest: `${buildPath}/${site}/`, - }, - { - src: "./src/genome-browser/img", - dest: `${buildPath}/${site}/`, - }, - { - src: "./node_modules/@fortawesome/fontawesome-free/webfonts/*.woff2", - dest: `${buildPath}/${site}/vendors/webfonts`, - }, - ]; - if (env.npm_config_custom_site) { - targets.push({ - src: getCustomSitePath(site, "", "img"), - dest: `${buildPath}/${site}/` - }); - } - return targets; -}; - -export default getSitesToBuild().map(site => ({ - plugins: [ - del({ - targets: `build/${site}`, - }), - replace({ - preventAssignment: true, - values: { - "process.env.VERSION": JSON.stringify(pkg.version), - }, - }), - html({ - rootDir: `${sitesPath}/${site}/`, - input: getSiteContent(site), - }), - resolve(), - minifyHTML(), - babel({ - exclude: "node_modules/**", - babelHelpers: "runtime", - presets: ["@babel/preset-env"], - plugins: [ - "@babel/plugin-transform-runtime", - ] - }), - terser({ - ecma: 2020, - module: true, - warnings: true - }), - copy({ - targets: getCopyTargets(site), - }), - ], - moduleContext: id => { - // Avoid this Error: https://rollupjs.org/guide/en/#error-this-is-undefined - /* - * In order to match native module behaviour, Rollup sets `this` - * as `undefined` at the top level of modules. Rollup also outputs - * a warning if a module tries to access `this` at the top level. - * The following modules use `this` at the top level and expect it - * to be the global `window` object, so we tell Rollup to set - *`this = window` for these modules. - */ - const thisAsWindowForModules = [ - "node_modules/countup.js/dist/countUp.min.js" - ]; - - if (thisAsWindowForModules.some(id_ => id.trimRight().endsWith(id_))) { - return "window"; - } - }, - output: { - dir: `${buildPath}/${site}`, - minifyInternalExports: false, - manualChunks: id => { - // Extract opencga client mock - if (id.includes("test-app/clients/opencga-client-mock") || id.includes("api-mock")) { - return "lib/opencga-client-mock.min"; - } - // Extract cellbase client mock - if (id.includes("test-app/clients/cellbase-client-mock")) { - return "lib/cellbase-client-mock.min"; - } - // Josemi 2023-09-19 NOTE: 'opencga-catalog-utils' is included inside the 'clients/opencga' folder - // We need to make sure this file is not included in the opencga client bundle - if (id.includes("clients/opencga") && !id.includes("opencga-catalog-utils")) { - return "lib/opencga-client.min"; - } - if (id.includes("node_modules")) { - return "vendors/js/vendors"; - } - if (id.includes("src/webcomponents")) { - return "lib/webcomponents.min"; - } - if (id.includes("src/core")) { - return "lib/core.min"; - } - }, - chunkFileNames: () => { - return "[name]-[hash].js"; // configuration of manualChunks about name format and folder. - }, - entryFileNames: entryInfo => { - // Check for extensions entry --> save into 'extensions' folder - if (entryInfo.name === "extensions") { - return "extensions/[name].js"; - } - return "lib/[name].js"; - }, - assetFileNames: assetInfo => { - // if (assetInfo.name.includes("genome-browser.config")) { - // return "genome-maps/conf/[name][extname]"; - // } - - if (isConfig(assetInfo.name)) { - return "conf/[name][extname]"; - } - - if (isInternalCss(assetInfo.name)) { - return "css/[name]-[hash][extname]"; - } - - // if (assetInfo.name.includes("extensions")) { - // return "extensions/[name][extname]"; - // } - - if (assetInfo.name.endsWith(".js") && !isConfig(assetInfo.name)) { - return "vendors/js/[name]-[hash][extname]"; - } - - if (assetInfo.name.endsWith(".css")) { - return "vendors/css/[name]-[hash][extname]"; - } - - return "vendors/[name]-[hash][extname]"; - }, - }, - -})); diff --git a/scripts/server.js b/scripts/server.js new file mode 100644 index 0000000000..6f5c8bd498 --- /dev/null +++ b/scripts/server.js @@ -0,0 +1,50 @@ +/* eslint-disable no-undef */ +const fs = require("node:fs"); +const http = require("node:http"); +const path = require("node:path"); +const mime = require("mime-types"); + +const PORT = process.env.PORT || 4000; + +// send the provided file as a response +const sendFile = (request, response, filePath) => { + fs.realpath(filePath, "utf8", (error, realFilePath) => { + if (!error && realFilePath === filePath) { + const pathIsDirectory = fs.statSync(realFilePath).isDirectory(); + // 1. File exists and is not a directory + if (!pathIsDirectory) { + response.writeHead(200, { + "Content-Type": mime.lookup(path.basename(realFilePath)), + }); + return fs.createReadStream(realFilePath).pipe(response); + } + // 2. Path is a directory: redirect to the same url but adding the trailing '/' + response.writeHead(302, {location: request.url + "/"}); + return response.end(); + } + // 3. Path does not exist: send a 404 message + response.writeHead(404); + response.end("Not found."); + }); +}; + +const server = http.createServer((request, response) => { + // Capture all requests to 'test-data' folder + if (request.url.includes("test-data/")) { + // Note: test-data folder is always on the root of the jsorolla project + const testDataPath = path.join("../test-data", request.url.replace(/^[\w/\-_]*test-data\//, "")); + return sendFile(request, response, path.resolve(__dirname, testDataPath)); + } + // If request does not contain 'test-data', try to find the file inside 'build' folder + // We will make sure that if request is a folder, the file 'index.html' file is served + const paths = [process.cwd(), "build", request.url]; + if (request.url.endsWith("/")) { + paths.push("index.html"); + } + return sendFile(request, response, path.join(...paths)); +}); + +// Launch server +server.listen(PORT); + +console.log(`Server running at http://127.0.0.1:${PORT}/`); diff --git a/scripts/webpack-plugin-html-assets-fix.js b/scripts/webpack-plugin-html-assets-fix.js new file mode 100644 index 0000000000..1a42f11cef --- /dev/null +++ b/scripts/webpack-plugin-html-assets-fix.js @@ -0,0 +1,26 @@ +/* eslint-disable no-undef */ +/* eslint-disable no-param-reassign */ +const HtmlWebpackPlugin = require("html-webpack-plugin"); + +class WebpackPluginHtmlAssetsFix { + + static PLUGIN_NAME = "WebpackPluginHtmlAssetsFix"; + + apply(compiler) { + compiler.hooks.compilation.tap(WebpackPluginHtmlAssetsFix.PLUGIN_NAME, compilation => { + const compilationHooks = HtmlWebpackPlugin.getCompilationHooks(compilation); + compilationHooks.beforeAssetTagGeneration.tapAsync(WebpackPluginHtmlAssetsFix.PLUGIN_NAME, (data, callback) => { + const chunk = data.plugin.userOptions.chunks[0]; + ["js", "css"].forEach(type => { + data.assets[type] = (data.assets[type] || []) + .filter(file => !!file) + .map(file => file.replace("../" + chunk, ".")); + }); + return callback(null, data); + }); + }); + } + +} + +module.exports = WebpackPluginHtmlAssetsFix; diff --git a/scripts/webpack-plugin-html-build-info.js b/scripts/webpack-plugin-html-build-info.js new file mode 100644 index 0000000000..38772c17c4 --- /dev/null +++ b/scripts/webpack-plugin-html-build-info.js @@ -0,0 +1,36 @@ +/* eslint-disable no-undef */ +/* eslint-disable no-param-reassign */ +const HtmlWebpackPlugin = require("html-webpack-plugin"); + +class WebpackPluginHtmlBuildInfo { + + static PLUGIN_NAME = "WebpackPluginHtmlBuildInfo"; + + constructor(options) { + this.options = options; + } + + apply(compiler) { + compiler.hooks.compilation.tap(WebpackPluginHtmlBuildInfo.PLUGIN_NAME, compilation => { + const compilationHooks = HtmlWebpackPlugin.getCompilationHooks(compilation); + compilationHooks.beforeEmit.tapAsync(WebpackPluginHtmlBuildInfo.PLUGIN_NAME, (data, callback) => { + const lines = data.html.split("\n"); + // find the index where the begins + const headStartIndex = lines.findIndex(line => { + return line.trim().startsWith(""); + }); + if (headStartIndex > -1) { + lines.splice(headStartIndex + 1, 0, ` -->`); + lines.splice(headStartIndex + 1, 0, ` Build generated on: ${this.options.date}`); + lines.splice(headStartIndex + 1, 0, ` ${this.options.name} Version: ${this.options.version} | Git: ${this.options.branch} - ${this.options.commit}`); + lines.splice(headStartIndex + 1, 0, ` "SAMPLES" + // "VIEW_CLINICAL_ANALYSIS" --> "CLINICAL_ANALYSIS" + const resource = permission.split("_").slice(1).join("_"); + + // VALIDATION + if (!study || !userId || !permission || !resource) { + console.error(`No valid parameters, study: ${study}, user: ${userId}, permission: ${permission}, catalogEntity: ${resource}`); return false; } - // Check if user is a Study admin, belongs to @admins group - const admins = study.groups.find(group => group.id === "@admins"); - if (admins.userIds.includes(user)) { - return true; + const permissionLevel = {}; + permissionLevel["NONE"] = 1; + if (permission !== "EXECUTE_JOBS") { + permissionLevel[`VIEW_${resource}`] = 2; + permissionLevel[`WRITE_${resource}`] = 3; + permissionLevel[`DELETE_${resource}`] = 4; } else { - // Check if user is in acl - const aclUserIds = study.groups - .filter(group => group.userIds.includes(user)) - .map(group => group.id); - aclUserIds.push(user); - for (const aclId of aclUserIds) { - // Find the permissions for this user - const userPermissions = study?.acl - ?.find(acl => acl.member === user)?.groups - ?.find(group => group.id === aclId)?.permissions || []; - if (Array.isArray(permissions)) { - for (const permission of permissions) { - if (userPermissions?.includes(permission)) { - return true; - } - } + permissionLevel[permission] = 2; + } + + const getPermissionLevel = permissionList => { + const levels = permissionList + .map(p => permissionLevel[p]) + .filter(p => typeof p === "number"); + return levels.length > 0 ? Math.max(...levels) : 0; + }; + + const getEffectivePermission = (userPermission, groupPermissions) => { + // It is possible to simplify permissions. + if (!simplifyPermissions) { + // First, find permission level at user level + const userPermissionLevel = getPermissionLevel(userPermission); + if (userPermissionLevel) { + // If the permission level at user level is greater than 0, return this permission level because it has priority over groups. + return userPermissionLevel; } else { - if (userPermissions?.includes(permissions)) { - return true; - } + // Check permission level at groups level. No hierarchy defined here. Example: + // If a user belongs to two groups: + // - groupA - Has permission VIEW_SAMPLES + // - groupB - Has permission WRITE_SAMPLES + // The dominant permission will be the highest, i.e. WRITE_SAMPLES + return Math.max(0, ...groupPermissions.map(g => getPermissionLevel(g))); } + } else { + // If "simplifyPermissions = true" permissions become more flexible. + // As long as the user has the necessary permission at the user or group level it'll be able to perform the action. + // I.e., there's no hierarchy where user-level permissions override group-level ones + groupPermissions.push(userPermission); + return Math.max(0, ...groupPermissions.map(g => getPermissionLevel(g))); } + }; + + // ALGORITHM + // 1. If userId is the installation admin grant permission + if (userId === "opencga") { + return true; } - return false; + // 2. If userId is a Study admin, belongs to @admins group. Grant permission + const admins = study.groups.find(group => group.id === "@admins"); + if (admins.userIds.includes(userId)) { + return true; + } + // 3. Permissions for member + const userPermissionsStudy = study?.acl + ?.find(acl => acl.member === userId) + ?.permissions || []; + + // 4. Permissions for groups where the member belongs to + const groupIds = study.groups + .filter(group => group.userIds.includes(userId)) + .map(group => group.id); + + const groupPermissions = groupIds.map(groupId => study?.acl + ?.find(acl => acl.member === userId)?.groups + ?.find(group => group.id === groupId)?.permissions || []); + + // If the effective permission retrieved is greater or equal than the permission level requested, grant permission. + // If not, deny permission + return getEffectivePermission(userPermissionsStudy, groupPermissions) >= permissionLevel[permission]; } // Check if the user has the right the permissions in the study. @@ -95,17 +148,25 @@ export default class OpencgaCatalogUtils { console.error(`No valid parameters, study: ${study}, user: ${userLogged}`); return false; } - // Check if user is the Study owner - const studyOwner = study.fqn.split("@")[0]; - if (userLogged === studyOwner) { + const admins = study.groups.find(group => group.id === "@admins"); + return !!admins.userIds.includes(userLogged); + } + + // Check if the provided user is admin in the organization + static isOrganizationAdmin(organization, userId) { + if (!organization || !userId) { + return false; + } + // 1. Check if user is the organization admin + if (organization?.owner === userId) { return true; } else { - // Check if user is a Study admin, belongs to @admins group - const admins = study.groups.find(group => group.id === "@admins"); - if (admins.userIds.includes(userLogged)) { + // Check if user is an admin of the organization + if (organization?.admins?.includes?.(userId)) { return true; } } + // Other case, user is not admin of the organization return false; } diff --git a/src/core/clients/opencga/opencga-client.js b/src/core/clients/opencga/opencga-client.js index c94b0e2083..bb7cd4c831 100644 --- a/src/core/clients/opencga/opencga-client.js +++ b/src/core/clients/opencga/opencga-client.js @@ -220,7 +220,7 @@ export class OpenCGAClient { if (!this.clients.has("organization")) { this.clients.set("organization", new Organization(this._config)); } - return this.clients.get("organizaton"); + return this.clients.get("organization"); } /* @@ -399,9 +399,13 @@ export class OpenCGAClient { console.error(e); } - + // Save projects session.projects = session.user.projects; + // Fetch organization info + const organizationResponse = await this.organization() + .info(session.user.organization); + session.organization = organizationResponse.responses[0].results[0]; // Fetch authorised Projects and Studies console.log("Fetching projects and studies"); diff --git a/src/core/clients/opencga/opencga-parent-class.js b/src/core/clients/opencga/opencga-parent-class.js index 14e74f6eec..bf24d279eb 100644 --- a/src/core/clients/opencga/opencga-parent-class.js +++ b/src/core/clients/opencga/opencga-parent-class.js @@ -33,11 +33,14 @@ export default class OpenCGAParentClass { _options.token = sid; } } - + // CAUTION Vero 2024-05-10: We believe this bit of code is useless. Temporarily commented out. + // In users endpoint, we cannot find GET method where the path param {user/users} should be autocompleted. + // When needed, they should be explicitly set. // If category == users and userId is not given, we try to set it - if (category1 === "users" && (ids1 === undefined || ids1 === null || ids1 === "")) { - ids1 = this._getUserId(); - } + // if (category1 === "users" && (ids1 === undefined || ids1 === null || ids1 === "")) { + // ids1 = this._getUserId(); + // } + let url = this._createRestUrl(host, version, category1, ids1, category2, ids2, action); // if (method === "GET") { url = this._addQueryParams(url, _params); @@ -58,13 +61,6 @@ export default class OpenCGAParentClass { } _post(category1, ids1, category2, ids2, action, body, params = {}, options = {}) { - // Clear and Revert actions do not need a body, but needs a params - if (category2 === "interpretation" && (action === "clear" || action === "revert")) { - // eslint-disable-next-line no-param-reassign - params = body; - // eslint-disable-next-line no-param-reassign - body = {}; - } const host = this._config.host; const version = this._config.version; // const rpc = this._config.mode; diff --git a/src/core/clients/reactome/reactome-client.js b/src/core/clients/reactome/reactome-client.js deleted file mode 100644 index 829f0c4133..0000000000 --- a/src/core/clients/reactome/reactome-client.js +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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 RestClient from "../rest-client.js"; - -export class ReactomeClient { - - constructor() { - this.host = "https://reactome.org"; - } - - /* - * Client factory functions - */ - contentServiceClient() { - if (typeof this._contentService === "undefined") { - this._contentService = new ContentService(this.host + "/ContentService"); - } - return this._contentService; - } -} - -export class ContentService { - - constructor(host) { - this.host = host; - } - - /* - * Client factory functions - */ - mappingClient() { - if (typeof this._mapping === "undefined") { - this._mapping = new MappingClient(this.host + "/data/mapping"); - } - return this._mapping; - } - -} - -// parent class -export class ReactomeParentClass { - - constructor(host) { - this.host = host; - } - - // post(category, ids, action, params, body, options) { - // return this.extendedPost(category, ids, null, null, action, params, body, options); - // } - // - // extendedPost(category1, ids1, category2, ids2, action, params, body, options) { - // let _options = options; - // if (typeof _options === "undefined") { - // _options = {}; - // } - // _options.method = "POST"; - // let _params = params; - // - // if (typeof _params === "undefined") { - // _params = {}; - // } - // _params.body = body; - // return this.extendedGet(category1, ids1, category2, ids2, action, _params, _options); - // } - - get(category, ids, action, params, options) { - return this.extendedGet(category, ids, null, null, action, params, options); - } - - extendedGet(category1, ids1, category2, ids2, action, params, options) { - // we store the options from the parameter or from the default values in config - const host = this.host; - const rpc = "rest"; - let method = "GET"; - let _options = options; - if (typeof _options === "undefined") { - _options = {}; - } - - if (_options.hasOwnProperty("method")) { - method = _options.method; - } - - let _params = params; - - if (_params === undefined || _params === null || _params === "") { - _params = {}; - } - - if (rpc.toLowerCase() === "rest") { - let url = this._createRestUrl(host, category1, ids1, category2, ids2, action); - url = this._addQueryParams(url, _params); - if (method === "POST") { - _options.data = _params.body; - } - return RestClient.call(url, _options); - } - } - - _createRestUrl(host, category1, ids1, category2, ids2, action) { - let url = host + "/" + category1 + "/"; - - // Some web services do not need IDs - if (typeof ids1 !== "undefined" && ids1 !== null) { - url += `${ids1}/`; - } - - // Some web services do not need a second category - if (typeof category2 !== "undefined" && category2 !== null) { - url += `${category2}/`; - } - - // Some web services do not need the second category of ids - if (typeof ids2 !== "undefined" && ids2 !== null && ids2 !== "") { - url += `${ids2}/`; - } - - // Some web services do not have action - if (typeof action !== "undefined" && action !== null && action !== "") { - url += action; - } - - return url; - } - - _addQueryParams(url, params) { - // We add the query params formatted in URL - const queryParamsUrl = this._createQueryParam(params); - let _url = url; - if (typeof queryParamsUrl !== "undefined" && queryParamsUrl !== null && queryParamsUrl !== "") { - _url += `?${queryParamsUrl}`; - } - return _url; - } - - _createQueryParam(params) { - // Do not remove the sort! we need to sort the array to ensure that the key of the cache will be correct - let keyArray = _.keys(params); - let keyValueArray = []; - for (let i in keyArray) { - // Whatever it is inside body will be sent hidden via POST - if (keyArray[i] !== "body") { - keyValueArray.push(`${keyArray[i]}=${encodeURIComponent(params[keyArray[i]])}`); - } - } - return keyValueArray.join("&"); - } - -} - -export class MappingClient extends ReactomeParentClass { - - constructor(host) { - super(host); - } - - // The lower level pathways where an identifier can be mapped to - pathways(resource, identifier, params) { - return this.get(resource, identifier, "pathways", params); - } - -} diff --git a/src/core/clients/test/test.html b/src/core/clients/test/test.html deleted file mode 100644 index 793c449579..0000000000 --- a/src/core/clients/test/test.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - Cache tests - - - - - - - - - - - - - - - - - - - - - diff --git a/src/core/clients/test/tests.js b/src/core/clients/test/tests.js deleted file mode 100644 index c7c42f87c9..0000000000 --- a/src/core/clients/test/tests.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2015 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. - */ - -/** - * Created with IntelliJ IDEA. - * User: imedina - * Date: 10/9/13 - * Time: 6:00 PM - * To change this template use File | Settings | File Templates. - */ - -test("OpenCGAClient", function() { - ok( 1 == 1, "Passed!" ); -}); - -test("Adding and Getting", function() { - ok( 1 == 1, "Passed!" ); -}); diff --git a/src/core/utils-new.js b/src/core/utils-new.js index 9215532b05..1e3b20d637 100644 --- a/src/core/utils-new.js +++ b/src/core/utils-new.js @@ -265,7 +265,7 @@ export default class UtilsNew { } static initTooltip(scope) { - $("a[tooltip-title], span[tooltip-title]", scope).each(function () { + $("a[tooltip-title], span[tooltip-title], table[tooltip-title], td[tooltip-title]", scope).each(function () { $(this).qtip({ content: { title: $(this).attr("tooltip-title"), @@ -304,32 +304,10 @@ export default class UtilsNew { return document.createRange().createContextualFragment(`${html}`); } - static jobStatusFormatter(status, appendDescription = false) { - const description = appendDescription && status.description ? `
${status.description}` : ""; - // FIXME remove this backward-compatibility check in next v2.3 - const statusId = status.id || status.name; - switch (statusId) { - case "PENDING": - case "QUEUED": - return ` ${statusId}${description}`; - case "RUNNING": - return ` ${statusId}${description}`; - case "DONE": - return ` ${statusId}${description}`; - case "ERROR": - return ` ${statusId}${description}`; - case "UNKNOWN": - return ` ${statusId}${description}`; - case "ABORTED": - return ` ${statusId}${description}`; - case "DELETED": - return ` ${statusId}${description}`; - } - return "-"; - } - // Capitalizes the first letter of a string and lowercase the rest. - static capitalize = ([first, ...rest]) => first.toUpperCase() + rest.join("").toLowerCase(); + static capitalize([first, ...rest]) { + return first.toUpperCase() + rest.join("").toLowerCase(); + } /* * This function creates a table (rows and columns) a given Object or array of Objects using the fields provided. @@ -1099,4 +1077,14 @@ export default class UtilsNew { .filter(item => !!item); } + // Group elements in array by the value in the given key + static groupBy(array, key) { + return array.reduce((result, currentValue) => { + const objectValue = UtilsNew.getObjectValue(currentValue, key); + // eslint-disable-next-line no-param-reassign + (result[objectValue] = result[objectValue] || []).push(currentValue); + return result; + }, {}); + } + } diff --git a/src/exclude b/src/exclude deleted file mode 100644 index 8f0fff205d..0000000000 --- a/src/exclude +++ /dev/null @@ -1 +0,0 @@ -import "./core/clients/test/tests.js"; diff --git a/src/genome-browser/genome-browser.js b/src/genome-browser/genome-browser.js index 4ae1893fb1..2015a73a19 100644 --- a/src/genome-browser/genome-browser.js +++ b/src/genome-browser/genome-browser.js @@ -68,7 +68,7 @@ export default class GenomeBrowser { // Generate GB template const template = UtilsNew.renderHTML(`
-
+
diff --git a/src/webcomponents/clinical/interpretation/clinical-interpretation-update.js b/src/webcomponents/clinical/interpretation/clinical-interpretation-update.js index 19e25ac386..3a2e001956 100644 --- a/src/webcomponents/clinical/interpretation/clinical-interpretation-update.js +++ b/src/webcomponents/clinical/interpretation/clinical-interpretation-update.js @@ -149,6 +149,11 @@ export default class ClinicalInterpretationUpdate extends LitElement { disabled: true, }, }, + { + title: "Interpretation Name", + field: "name", + type: "input-text", + }, { title: "Assigned To", field: "analyst.id", @@ -166,7 +171,7 @@ export default class ClinicalInterpretationUpdate extends LitElement { return html` { // CAUTION: check if the panelLock condition is the same as clinical-analysis-update.js - const panelLock = !!this.clinicalAnalysis?.panelLock; + const panelLock = !!this.clinicalAnalysis?.panelLocked; const panelList = panelLock ? this.clinicalAnalysis?.panels : this.opencgaSession.study?.panels; const handlePanelsFilterChange = e => { const panelList = (e.detail?.value?.split(",") || []) diff --git a/src/webcomponents/cohort/cohort-browser.js b/src/webcomponents/cohort/cohort-browser.js index b5aad11135..c3e2442497 100644 --- a/src/webcomponents/cohort/cohort-browser.js +++ b/src/webcomponents/cohort/cohort-browser.js @@ -15,7 +15,7 @@ */ -import {LitElement, html} from "lit"; +import {LitElement, html, nothing} from "lit"; import UtilsNew from "../../core/utils-new.js"; import "../commons/opencga-browser.js"; import "./cohort-grid.js"; @@ -125,15 +125,18 @@ export default class CohortBrowser extends LitElement { .config="${params.config.filter.result.grid}" .eventNotifyName="${params.eventNotifyName}" .active="${true}" - @selectrow="${e => params.onClickRow(e, "cohort")}" - @cohortUpdate="${e => params.onComponentUpdate(e, "cohort")}" + @selectrow="${e => params.onClickRow(e)}" + @cohortUpdate="${e => params.onComponentUpdate(e)}" @settingsUpdate="${() => this.onSettingsUpdate()}"> - - ` + ${params?.detail ? html` + + + ` : nothing} + `, }, { id: "facet-tab", diff --git a/src/webcomponents/cohort/cohort-create.js b/src/webcomponents/cohort/cohort-create.js index 5f93b0a0f0..5969b812d5 100644 --- a/src/webcomponents/cohort/cohort-create.js +++ b/src/webcomponents/cohort/cohort-create.js @@ -204,14 +204,6 @@ export default class CohortCreate extends LitElement { placeholder: "Add an ID", } }, - { - title: "Name", - field: "status.name", - type: "input-text", - display: { - placeholder: "Add source name" - } - }, { title: "Description", field: "status.description", diff --git a/src/webcomponents/cohort/cohort-grid.js b/src/webcomponents/cohort/cohort-grid.js index 441f39d74b..913390c391 100644 --- a/src/webcomponents/cohort/cohort-grid.js +++ b/src/webcomponents/cohort/cohort-grid.js @@ -22,8 +22,9 @@ import CatalogGridFormatter from "../commons/catalog-grid-formatter.js"; import PolymerUtils from "../PolymerUtils.js"; import "../commons/opencb-grid-toolbar.js"; import NotificationUtils from "../commons/utils/notification-utils.js"; -import ModalUtils from "../commons/modal/modal-utils"; -import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-utils"; +import ModalUtils from "../commons/modal/modal-utils.js"; +import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-utils.js"; +import WebUtils from "../commons/utils/web-utils.js"; export default class CohortGrid extends LitElement { @@ -78,7 +79,6 @@ export default class CohortGrid extends LitElement { super.update(changedProperties); } - updated(changedProperties) { if (changedProperties.size > 0 && this.active) { this.renderTable(); @@ -145,6 +145,8 @@ export default class CohortGrid extends LitElement { // ` // } }; + + this.permissionID = WebUtils.getPermissionID(this.toolbarConfig.resource, "WRITE"); } renderTable() { @@ -343,27 +345,34 @@ export default class CohortGrid extends LitElement { id: "actions", title: "Actions", field: "actions", - formatter: () => ` - - `, align: "center", + formatter: () => { + const hasWritePermission = OpencgaCatalogUtils.getStudyEffectivePermission( + this.opencgaSession.study, + this.opencgaSession.user.id, + this.permissionID, + this.opencgaSession?.organization?.configuration?.optimizations?.simplifyPermissions); + return ` + + `; + }, events: { "click a": this.onActionClick.bind(this), }, @@ -413,7 +422,7 @@ export default class CohortGrid extends LitElement { _.id, _.samples ? _.samples.map(_ => `${_.id}`).join(",") : "", _.creationDate ? CatalogGridFormatter.dateFormatter(_.creationDate) : "-", - _.status.name, + _.status.id, _.type ].join("\t"))]; UtilsNew.downloadData(dataString, "cohort_" + this.opencgaSession.study.id + ".tsv", "text/plain"); diff --git a/src/webcomponents/cohort/cohort-update.js b/src/webcomponents/cohort/cohort-update.js index fb4522054e..f16613bca1 100644 --- a/src/webcomponents/cohort/cohort-update.js +++ b/src/webcomponents/cohort/cohort-update.js @@ -156,14 +156,6 @@ export default class CohortUpdate extends LitElement { placeholder: "Add an ID", } }, - { - title: "Name", - field: "status.name", - type: "input-text", - display: { - placeholder: "Add source name" - } - }, { title: "Description", field: "status.description", diff --git a/src/webcomponents/cohort/cohort-view.js b/src/webcomponents/cohort/cohort-view.js index 8a49ddc4e7..3154dbd91d 100644 --- a/src/webcomponents/cohort/cohort-view.js +++ b/src/webcomponents/cohort/cohort-view.js @@ -239,7 +239,7 @@ export default class CohortView extends LitElement { title: "Status", type: "complex", display: { - template: "${internal.status.name} (${internal.status.date})", + template: "${internal.status.id} (${internal.status.date})", format: { "internal.status.date": date => UtilsNew.dateFormatter(date), } diff --git a/src/webcomponents/commons/analysis/analysis-utils.js b/src/webcomponents/commons/analysis/analysis-utils.js index d5f1e3a1be..5d24d7ad6a 100644 --- a/src/webcomponents/commons/analysis/analysis-utils.js +++ b/src/webcomponents/commons/analysis/analysis-utils.js @@ -3,6 +3,7 @@ import NotificationUtils from "../utils/notification-utils"; import UtilsNew from "../../../core/utils-new"; import "../filters/feature-filter.js"; import "../filters/disease-panel-filter.js"; +import LitUtils from "../utils/lit-utils"; export default class AnalysisUtils { @@ -14,7 +15,7 @@ export default class AnalysisUtils { // } static submit(id, promise, context) { - promise + return promise .then(response => { console.log(response); NotificationUtils.dispatch(context, NotificationUtils.NOTIFY_SUCCESS, { @@ -22,7 +23,10 @@ export default class AnalysisUtils { message: `${id} has been launched successfully`, }); // Call to analysis onClear() method - context.onClear(); + if (typeof context.onClear === "function") { + context.onClear(); + } + return response; }) .catch(response => { console.log(response); @@ -114,13 +118,14 @@ export default class AnalysisUtils { static getAnalysisConfiguration(id, title, description, paramSections, check, config = {}) { return { id: id, - icon: config.icon, + icon: config.icon || "", title: config.title || title, description: config.description || description, display: { // defaultLayout: "vertical" - ...config.display + ...config?.display }, + buttons: config?.buttons || {}, sections: [ { display: {}, @@ -139,6 +144,9 @@ export default class AnalysisUtils { ...paramSections, { title: "Job Info", + display: { + visible: config.isJob !== undefined ? config.isJob : true, + }, elements: [ { title: "Job ID", diff --git a/src/webcomponents/commons/analysis/opencga-analysis-tool-form.js b/src/webcomponents/commons/analysis/opencga-analysis-tool-form.js index b52131aeb5..5931632b0a 100644 --- a/src/webcomponents/commons/analysis/opencga-analysis-tool-form.js +++ b/src/webcomponents/commons/analysis/opencga-analysis-tool-form.js @@ -101,7 +101,11 @@ export default class OpencgaAnalysisToolForm extends LitElement { async updated(changedProperties) { if (changedProperties.has("opencgaSession")) { this.params.study = this.opencgaSession.study.fqn; - this.runnable = OpencgaCatalogUtils.checkPermissions(this.opencgaSession.study, this.opencgaSession.user.id, "EXECUTE_JOBS"); + this.runnable = OpencgaCatalogUtils.getStudyEffectivePermission( + this.opencgaSession.study, + this.opencgaSession.user.id, + "EXECUTE_JOBS", + this.opencgaSession?.organization?.configuration?.optimizations?.simplifyPermissions); this.requestUpdate(); await this.updateComplete; // await this.updateComplete; diff --git a/src/webcomponents/commons/catalog-grid-formatter.js b/src/webcomponents/commons/catalog-grid-formatter.js index 5de4d88496..019df35779 100644 --- a/src/webcomponents/commons/catalog-grid-formatter.js +++ b/src/webcomponents/commons/catalog-grid-formatter.js @@ -19,6 +19,16 @@ import BioinfoUtils from "../../core/bioinfo/bioinfo-utils.js"; export default class CatalogGridFormatter { + static userStatusFormatter(status, config) { + const _config = config || []; + const currentStatus = status.id || status.name || "UNDEFINED"; // Get current status + const displayCurrentStatus = _config.find(status => status.id === currentStatus); + return ` + + ${displayCurrentStatus.displayLabel} + + `; + } static sexFormatter(value, row) { let sexHtml = `${UtilsNew.isEmpty(row?.sex) ? "Not specified" : row.sex.id || row.sex}`; if (row?.karyotypicSex && row.karyotypicSex !== "UNKNOWN") { diff --git a/src/webcomponents/commons/filters/catalog-search-autocomplete.js b/src/webcomponents/commons/filters/catalog-search-autocomplete.js index f087965bdd..d2ac72c217 100644 --- a/src/webcomponents/commons/filters/catalog-search-autocomplete.js +++ b/src/webcomponents/commons/filters/catalog-search-autocomplete.js @@ -80,8 +80,9 @@ export default class CatalogSearchAutocomplete extends LitElement { this.RESOURCES = { "PROJECT": { searchField: "id", - placeholder: "project...", - client: this.opencgaSession.opencgaClient.projects(), + placeholder: "Project...", + // client: this.opencgaSession.opencgaClient.projects(), + fetch: ({study, ...params}) => this.opencgaSession.opencgaClient.projects().search(params), fields: item => ({ "name": item.id, }), @@ -90,24 +91,25 @@ export default class CatalogSearchAutocomplete extends LitElement { } }, "STUDY": { - searchField: "id", - placeholder: "study...", - client: this.opencgaSession.opencgaClient.studies(), + searchField: "fqn", + placeholder: "Study...", + // client: this.opencgaSession.opencgaClient.studies(), + fetch: ({study, ...params}) => this.opencgaSession.opencgaClient.studies().search(this.opencgaSession.project.id, params), fields: item => ({ "name": item.id, }), query: { - project: this.opencgaSession.project.id, - include: "id,name" + include: "id,name,fqn" } }, "SAMPLE": { searchField: "id", placeholder: "HG01879, HG01880, HG01881...", - client: this.opencgaSession.opencgaClient.samples(), + // client: this.opencgaSession.opencgaClient.samples(), + fetch: filters => this.opencgaSession.opencgaClient.samples().search(filters), fields: item => ({ "name": item.id, - "Individual ID": item?.individualId + "Individual ID": item?.individualId, }), query: { include: "id,individualId" @@ -116,7 +118,8 @@ export default class CatalogSearchAutocomplete extends LitElement { "INDIVIDUAL": { searchField: "id", placeholder: "Start typing", - client: this.opencgaSession.opencgaClient.individuals(), + // client: this.opencgaSession.opencgaClient.individuals(), + fetch: filters => this.opencgaSession.opencgaClient.individuals().search(filters), fields: item => ({ "name": item.id }), @@ -127,7 +130,8 @@ export default class CatalogSearchAutocomplete extends LitElement { "FAMILY": { searchField: "id", placeholder: "Start typing", - client: this.opencgaSession.opencgaClient.families(), + // client: this.opencgaSession.opencgaClient.families(), + fetch: filters => this.opencgaSession.opencgaClient.families().search(filters), fields: item => ({ "name": item.id }), @@ -139,7 +143,8 @@ export default class CatalogSearchAutocomplete extends LitElement { "CLINICAL_ANALYSIS": { searchField: "id", placeholder: "Start typing", - client: this.opencgaSession.opencgaClient.clinical(), + // client: this.opencgaSession.opencgaClient.clinical(), + fetch: filters => this.opencgaSession.opencgaClient.clinical().search(filters), fields: item => ({ "name": item.id, "Proband Id": item?.proband?.id @@ -151,7 +156,8 @@ export default class CatalogSearchAutocomplete extends LitElement { "DISEASE_PANEL": { searchField: "id", placeholder: "Start typing", - client: this.opencgaSession.opencgaClient.panels(), + // client: this.opencgaSession.opencgaClient.panels(), + fetch: filters => this.opencgaSession.opencgaClient.panels().search(filters), fields: item => ({ "name": item.id, }), @@ -162,7 +168,8 @@ export default class CatalogSearchAutocomplete extends LitElement { "JOB": { searchField: "id", placeholder: "Start typing", - client: this.opencgaSession.opencgaClient.jobs(), + // client: this.opencgaSession.opencgaClient.jobs(), + fetch: filters => this.opencgaSession.opencgaClient.jobs().search(filters), fields: item => ({ "name": item.id, }), @@ -173,7 +180,8 @@ export default class CatalogSearchAutocomplete extends LitElement { "COHORT": { searchField: "id", placeholder: "Start typing", - client: this.opencgaSession.opencgaClient.cohorts(), + // client: this.opencgaSession.opencgaClient.cohorts(), + fetch: filters => this.opencgaSession.opencgaClient.cohorts().search(filters), fields: item => ({ "name": item.id }), @@ -184,7 +192,8 @@ export default class CatalogSearchAutocomplete extends LitElement { "FILE": { searchField: "name", placeholder: "eg. samples.tsv, phenotypes.vcf...", - client: this.opencgaSession.opencgaClient.files(), + // client: this.opencgaSession.opencgaClient.files(), + fetch: filters => this.opencgaSession.opencgaClient.files().search(filters), fields: item => ({ name: item.name, Format: item.format ?? "N/A", @@ -198,7 +207,8 @@ export default class CatalogSearchAutocomplete extends LitElement { "DIRECTORY": { searchField: "path", placeholder: "eg. /data/platinum-grch38...", - client: this.opencgaSession.opencgaClient.files(), + // client: this.opencgaSession.opencgaClient.files(), + fetch: filters => this.opencgaSession.opencgaClient.files().search(filters), fields: item => ({ name: item.name, path: `/${item.path.replace(`/${item.name}`, "")}` @@ -207,7 +217,31 @@ export default class CatalogSearchAutocomplete extends LitElement { type: "DIRECTORY", include: "id,path", } - } + }, + "NOTE_ORGANIZATION": { + searchField: "id", + placeholder: "Start typing", + // eslint-disable-next-line no-unused-vars + fetch: ({study, ...params}) => this.opencgaSession.opencgaClient.organization().searchNotes(params), + fields: item => ({ + "name": item.id, + }), + query: { + include: "id", + scope: "ORGANIZATION", + }, + }, + "NOTE_STUDY": { + searchField: "id", + placeholder: "Start typing", + fetch: ({study, ...params}) => this.opencgaSession.opencgaClient.studies().searchNotes(study, params), + fields: item => ({ + "name": item.id, + }), + query: { + include: "id", + }, + }, }; this._config = this.getDefaultConfig(); } @@ -252,7 +286,7 @@ export default class CatalogSearchAutocomplete extends LitElement { ...attr, }; - this.RESOURCES[this.resource].client.search(filters) + this.RESOURCES[this.resource].fetch(filters) .then(response => success(response)) .catch(error => failure(error)); }, diff --git a/src/webcomponents/commons/filters/clinical-annotation-filter.js b/src/webcomponents/commons/filters/clinical-annotation-filter.js index 4b9ec5325b..c41d19ef0a 100644 --- a/src/webcomponents/commons/filters/clinical-annotation-filter.js +++ b/src/webcomponents/commons/filters/clinical-annotation-filter.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import {LitElement, html} from "lit"; +import {html, LitElement} from "lit"; import "../../commons/forms/select-field-filter.js"; import "../../commons/forms/checkbox-field-filter.js"; @@ -24,7 +24,7 @@ export default class ClinicalAnnotationFilter extends LitElement { super(); // Set status and init private properties - this._init(); + this.#init(); } createRenderRoot() { @@ -48,7 +48,7 @@ export default class ClinicalAnnotationFilter extends LitElement { }; } - _init() { + #init() { this.query = {}; } @@ -98,7 +98,7 @@ export default class ClinicalAnnotationFilter extends LitElement { render() { return html` -
+
@@ -112,7 +112,7 @@ export default class ClinicalAnnotationFilter extends LitElement {
-
+
@@ -126,7 +126,7 @@ export default class ClinicalAnnotationFilter extends LitElement {
-
+
diff --git a/src/webcomponents/commons/filters/disease-panel-filter.js b/src/webcomponents/commons/filters/disease-panel-filter.js index b20e5d05ba..3a7a3fbc9f 100644 --- a/src/webcomponents/commons/filters/disease-panel-filter.js +++ b/src/webcomponents/commons/filters/disease-panel-filter.js @@ -48,7 +48,7 @@ export default class DiseasePanelFilter extends LitElement { super(); // Set status and init private properties - this._init(); + this.#init(); } createRenderRoot() { @@ -97,7 +97,7 @@ export default class DiseasePanelFilter extends LitElement { }; } - _init() { + #init() { this.query = {}; this.genes = []; // this.panelFeatureType = ""; @@ -203,7 +203,7 @@ export default class DiseasePanelFilter extends LitElement { return html`
-
+
${this.showExtendedFilters ? html`