From 008ff183b46a63ec79589291149f0f9c6852b63a Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Thu, 9 Feb 2023 09:35:05 -0800 Subject: [PATCH 01/40] Develop no poetry (#477) * Set instance size to xxxlarge and log level to verbose during app deploy. * Only install schematic from pip, never use develop branch. * configureApp() will fail if the app does not exist yet. Put after deployApp() in case of first deployment. * try setting upload to FALSE and then configure it. The configApp() fails after deployApp() * if app exists, configure then deploy. Otherwise, deploy then configure. * change appName to testingXXX so it creates a new app instance and tests if the rsconnect configuration logic works. * Logic worked, go back to testing1 name --- .github/workflows/shinyapps_deploy.yml | 33 ++++++++++++-------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/.github/workflows/shinyapps_deploy.yml b/.github/workflows/shinyapps_deploy.yml index 7304da8e..95caabaf 100644 --- a/.github/workflows/shinyapps_deploy.yml +++ b/.github/workflows/shinyapps_deploy.yml @@ -48,23 +48,10 @@ jobs: run: | # has to activate each bash step source .venv/bin/activate - # use 'poetry' to install schematic dev schematic - # If commit is tagged for release or in main branch, install schematic from pypi - if [[ $GITHUB_REF_NAME == v*.*.* ]] || [[ $GITHUB_REF_NAME == main ]]; then - echo Installing pypi version of schematic - git clone --single-branch --branch main https://github.com/Sage-Bionetworks/schematic.git - cp schematic_config.yml schematic/config.yml - cd schematic - pip3 install schematicpy - else - pip3 install poetry - echo Installing develop branch of schematic from github - git clone --single-branch --branch develop https://github.com/Sage-Bionetworks/schematic.git - cp schematic_config.yml schematic/config.yml - cd schematic - poetry build - pip3 install dist/schematicpy-1.0.0-py3-none-any.whl - fi + echo Installing pypi version of schematic + git clone --single-branch --branch main https://github.com/Sage-Bionetworks/schematic.git + cp schematic_config.yml schematic/config.yml + pip3 install schematicpy - name: Set Configurations for Schematic shell: bash @@ -131,4 +118,14 @@ jobs: finally=close(configFileConn) ) rsconnect::setAccountInfo(rsConnectUser, rsConnectToken, rsConnectSecret) - rsconnect::deployApp(appName = appName) + # Get app names. If app exists, configure then deploy. Otherwise + # deploy then configure + apps <- rsconnect::applications()$name + if (appName %in% apps) { + rsconnect::configureApp(appName = appName, size = "xxxlarge", logLevel = "verbose") + rsconnect::deployApp(appName = appName) + } else { + rsconnect::deployApp(appName = appName) + rsconnect::configureApp(appName = appName, size = "xxxlarge", logLevel = "verbose") + } + From 74d6b9104e60f47f3b00dd394b4e4a5be0f8b1b9 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Wed, 22 Mar 2023 11:56:37 -0700 Subject: [PATCH 02/40] coerce warning to character so strsplit() does not fail. (#520) --- functions/validationResult.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/validationResult.R b/functions/validationResult.R index 92e6c31d..7db26368 100644 --- a/functions/validationResult.R +++ b/functions/validationResult.R @@ -121,7 +121,7 @@ validationResult <- function(anno.res, template, manifest = NULL, dashboard = FA # similar warnings in the same column should be concatenated from backend, like "['value1', 'value2', ...]" # extract the single quoted values from the warning string if (!is.null(warn[[4]])) { - warn_values <- gsub("^[^']*'|'\\],?$", "", strsplit(warn[[4]], "'(?=,)", perl = TRUE)[[1]]) + warn_values <- gsub("^[^']*'|'\\],?$", "", strsplit(as.character(warn[[4]]), "'(?=,)", perl = TRUE)[[1]]) } else { # if matchExactOne (set) warning exist, highlight entire column warn_values <- "ht_entire_column" From 967fcb0affcdee80328e1d8329f239fe56179d0e Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Thu, 23 Mar 2023 14:43:09 -0700 Subject: [PATCH 03/40] Beta schematic rest api (#516) * hone in where python is failing 2 * debug python venv path * use newest reticulate * update all packages * hone in where python is failing 3 * hone in where python is failing 4 * hone in where python is failing 4 * hone in where python is failing 5 * hone in where python is failing 5 * Revert "hone in where python is failing 5" Revert changes trying to use python3.10. It's not possible for shinyapps.io at the moment. This reverts commit 1d7c5664d7e0236f413d17b6b3f60729e58131bf. * Revert "hone in where python is failing 5" This reverts commit a833ea9797ed241ef5ed158ca0b5596c04499678. * Revert "hone in where python is failing 4" This reverts commit 34bfab8972c334464a4f5342508f11ebaeb3bbd2. * Revert "hone in where python is failing 4" This reverts commit 6766939c9109f4bb440de415b2a550f8acb783e3. * Revert "hone in where python is failing 3" This reverts commit 5cd8c7dfb613a5a36b3d0fa3eae552cc6ccaf97d. * Revert "update all packages" This reverts commit 08bd7db28a6a50965043b61c2f47d3ce6ba77c81. * Revert "use newest reticulate" This reverts commit 49d835de484fc7a12a43639fd1629f41540fca8b. * Revert "debug python venv path" This reverts commit c9e16b5cae819cd0b4e72108980497a011770cb6. * Revert "hone in where python is failing 2" This reverts commit 35cdfe0047dfaf84c55b5862c7a9074252b8ce93. * Revert "hone in where python is failing." This reverts commit 221ecfa990c5920d6b5da02b1bd1db1bc3227775. * Revert "Test py commands in global.R" This reverts commit 388b183843ebe8e1f464d2aaf34e48b80bba03e6. * Revert "use pip" This reverts commit 058c698a02ac5449bb60f29503603c375f4ee9dc. * Revert "check env var in shiny log" This reverts commit fad46ec7c100c938ae2d0a0d3b1bb4d35bf935d2. * Revert "Remove blank line" This reverts commit 68bc652005733bdaadde1f5f1972fe81d4994eba. * Revert "don't use poetry shell" This reverts commit c6bad6ad6560798c1cd4c4acead16d10bea37247. * Revert "read venv in schematic set up func" This reverts commit 79cd7f4713dc40537ee957d279189c623f4dda53. * Revert "Try not using a virtual env" This reverts commit 3c5ae28358991dfd0bba79b7fffa15892dad9b49. * Revert "Try hard coding python path" This reverts commit 1a2c087e2654affe598b0dc5e32c41a8ab43617b. * Revert "set reticulate python env in workflow" This reverts commit 76753bbeb897f7d97e75de22e93b65012a8ececd. * Revert "Try setting python path in deployApp()" This reverts commit 287a921462fd7b02d9dbd1741ad420a12325a5b9. * Revert "apt-get remove instead of uninstall" This reverts commit 66be0bbfd1c575647dd7570a4b50806af8cc224b. * Revert "Don't load reticulate by default." This reverts commit 67d14ef984ec8f42e69ce936500638674290153b. * Revert "Uninstall python3.8" This reverts commit 8d6ae515b8140fc9c27d11d375aef84a1cb7a6c0. * Revert "Add reticulate python env var to deployment workflow" This reverts commit 661490bf30bdfe059702718f2ebc16725d58d3ee. * Revert "Remove reticulate python from global.R" This reverts commit 6cb177f5568ff4248a279933fc29cdc10e45b648. * Revert "Add asset view back to waiter." This reverts commit 79d3e75c2061d481689033dbd38b295e1446bcc6. * Revert "Don't use renv to avoid png error" This reverts commit 27ab92c3e360e225c19c8761baf96ccd02682d7d. * Revert "Explicitly use python from virtualenv" This reverts commit 7e97c3f2fd33bf9382fa7213166a5fd10169b5c6. * Revert "Use renv" This reverts commit 15c484514ea6506229d343fed2b3da0500c54ec4. * Revert "Add renv" This reverts commit 81c4a0a1200b3111728c0cb4c8def1239fe37589. * Revert "Add color theme rds files." This reverts commit 38e90d6fb921060bb7c2f56863744d95ebe3b3a6. * Revert "Add missing }" This reverts commit 924af32b16c07014d62b5c948d7ac5d63418bbda. * Revert "Use python3.10 to run config_schema.py" This reverts commit 1e82bf69019de1e9c57773a01560b381285504aa. * Revert "Don't read template menu config file in global.R" This reverts commit bf6b67a19fd5890bdeaf4b7c92bb4d703586d161. * Revert "Actuallly write updated .Renviron this time" This reverts commit 10547266e6409392490fbf7604da9b538a6d9771. * Revert "Add DCA_APP_URL env var." This reverts commit e9299fc15ab47a4cf87e9a49b89353aa19b26e1e. * Revert "Use jammy ubuntu repo" This reverts commit 28e533ee89a786fc692659de87be6b8c657eff72. * Revert "Remove config generation in workflow. The app does that." This reverts commit d72d62aff6bf010b11fffa8ae43cc319a059e291. * Revert "Fix double quites" This reverts commit 1b8daf9c01ffc8bf8ace631a37e96b14e66f6c34. * Revert "Update package repo." This reverts commit 6aadb4b77f1edb282967b58c3b06b4aea2672be2. * Revert "Add missing quote." This reverts commit 70ae71fecdeed05bb7a1b2552046094d1be0f27c. * Revert "Use rocker/rstudio:latest to get most recent ubuntu (jimmy) and install python3.10 correctly" This reverts commit 7e36fce44554c5acdadb8e128e21c59060be808b. * Revert "Use python 3.10 explicitly." This reverts commit 118145bbf703bebdb1ec46b890695f90d04a4557. * Revert "Print out python version to debug" This reverts commit 49b4fbf1aeb73c3e8cd507e2fe17ed947b2d9c8c. * Revert "Add python venv" This reverts commit 71bafb22d3bdfa3100b43296dc62881160009df9. * Revert "Try setting DEBIAN_FRONTEND env var to disable keyboard prompt during apt-get update. Move into apt get command again" This reverts commit 915d9708159a06f6373bf767b84c0d0a3040efcc. * Revert "Try setting DEBIAN_FRONTEND env var to disable keyboard prompt during apt-get update. Move into apt get command" This reverts commit a4fd44039982f2652a7b9de54dba84c2d246d317. * Revert "Try setting DEBIAN_FRONTEND env var to disable keyboard prompt during apt-get update." This reverts commit c0244683341633569c0e4ecbeb9f38c07af82d29. * Revert "Add -y flag to apt-get update so interactive text doesn't hinder workflow." This reverts commit 01264d656258dcd8f8ad95d11bb5a335876cd056. * Revert "Use python 3.10 because schematic doesn't support 3.11" This reverts commit 35dac985f11f7dd50d83043eabe45f115373d616. * Revert "Use python 3.11 because schematic doesn't support 3.8" This reverts commit 24dc62f94ffcc4254fc0e5dbf606d9ad29fbd103. * Revert changes related to debugging python. Shinyapps.io requires python 3.8. * Revert "Use python 3.11 because schematic doesn't support 3.8" Stick with python 3.8 because shinyapps.io doesn't support other versions. This commit undoes all of the tweaking related to updating and debugging python 3.10. This reverts commit 24dc62f94ffcc4254fc0e5dbf606d9ad29fbd103. * Add color theme rds files. * Update asset view text in waiter after selecting asset view. * Add offline mode. If can't access synapse OAuth redirect. Go to landing page with no access token. * Add try() to redirect, if it fails, go to landing page without an access token. * Add offline mode to first and second tabs. Use dca_schematic_api env to be reticulate, rest, or offline. * use dca_schematic_api set to offline to go into ui without an access token. * use dca_schematic_api var to determine if running in offline mode. * Add a template dropdown for offline use. * Add offline mode to manifest generation tab. * Set instance size and log level during deployment. * Don't ever install develop schematic * Remove single quotes from echo. * remove APP_URL env var * Add appUrl to Renviron. * Add missing quote * Add mock config when not using python * add dashboard to ui * source dashboard functions * add dashboard to server * Make waiter screen default color slightly transparent. * Update include config file * Revert make waiter transparent by default. It was too transparent. * Add get_component_requirements for reticulate * Use reticulate wrapper for get_component requirements * Get syn_store object from synapse_driver * Allow manifest name to have component * Write manifest name with component * Hide dropdown menu from first page. * Remove parallel processing from validating data in loop. For some reason, it will not run properly when schematic encounters errors. But lapply() works fine. * Read in reactive config file outside of reticulate config file update. * dashboardFuns can use schematic via reticulate or rest api * dashboard can use schematic via reticulate or rest api * Add rest api arguments to dashboard * Read manifest basename from schematic_config.yml * Move reticulate/rest switch inside of get_schema_nodes(). Add () to synapse_user_profile. * Merge develop-add-rest-api into schematic-rest-api. This adds reticulate and REST api access to schematic. * Remove browser() from server.R * Add use_schema_label and manifest_record_type to manifest/validate. * Add waiter until data type menu updates * Simplify logic in manifest download for excel sheet * Expand downloadHandler content function to download the manifest from schematic. This allows users to skip downloading manifests if they don't need it. * Expose use_schema_label in model/submit to user. This can be toggled with DCA_SCHEMATIC_SUBMIT_USE_SCHEMA_LABEL env var. * Update contributors (#488) * If a server error is encountered with manifest/validate, return a list with an error in a similar format to other schematic validation errors. That way validationResult() can handle it as a proper error. (#490) * Develop offline mode (#491) * turn off dashboard in offline mode for now * fix logic for offline manifest to show up. * Add offline mode for manifest download. It will download a mock data csv. Add an offline option for manifest validation as well. * Add another download handler for downloading a corrected mock offline manifest * Add logic to download a corrected mock manifest in offline mode. * Develop update logo (#492) * Update default logo to transparent background * Update sage logo in waiter to transparent background * Add sage logo with transparent background * Remove extra heart from footer. * Remove synapse ID from asset view labels and waiter screens. (#493) * fix grammar (#494) * Develop toggle db (#495) * Add UI toggle for dashboard based on DCA_COMPLIANCE_DASHBOARD env var. * use example config.json * For validationresult in dashboard, concatenate the error messsage * Change branch in install_github() * Update waiter color to dcc color scheme. * When validation fails, remove option to download manifest as google sheet. * Add gihub PAT to renviron to avoid API error on deployment. * Dev beta fix templates (#502) * Remove project dirs in www/ since their relevant files are in the directory itself and in dca_themes/ * Update htan config to use file/record type * Move template config.json files into www subdirectory. * Update htan config so it can be read by jsonlite::fromJSON() * Add other assay to default template config. * Add other assay to example template config. * Read template config file and synID mapping env var, DCA_TEMPLATE_MENU_CONFIG, as a variable in global.R * Clean up template config code. Ensure the template dropdown menu updates to match the selected asset view. * Update config path in global.R * Update path of default template in server.R * Test deploying to shinyapps.io by moving R/ files into functions/ during deployment. * add covr and rex to fix release error * Don't use Renviron when installing packages. * try renv * add png and xml libs * add rsconnect * Don't source renv files. * Beta shinyapps deploy (#504) * show files to see if R/ directory is still present. * change devel branch in deployment * remove NAMESPACE and DESCRIPTION * install packages manually to avoid shinypop error. * add packages missing from rsconnect deploy error. * Add missing env vars to deployment script * Add template menu config env var to deployment workflow * Change url to testing1 * Add data model url env var to deployment. * Use single quotes * Fix typo to use .Renviron instead of .Rprofile. * Download and save data model before writing schematic config. * Revert listing files for debugging. * use download.file() instead of system(curl...) * Change branch in shinyapps deploy * Only use one DCC because shinyapps.io workers share the same reticulate environment. So different sessions can only use the same schematic config. * Add 'Step X' to sidebar tabs to guide users. * Revert add step X to sidebar. * Beta schematic rest api (#507) * Set up test instance for fork * Move parse_env_vars() and update_logo() to utils.R * Add test to parse_env_vars that checks if it errors out on empty string. * source files at top of global.R * Revert change to appName * Add naming logic back to deployment workflow * Beta schematic rest api (#508) * Set up test instance for fork * Move parse_env_vars() and update_logo() to utils.R * Add test to parse_env_vars that checks if it errors out on empty string. * source files at top of global.R * Revert change to appName * Add naming logic back to deployment workflow * Make manifest_url() reactive so link shows up correctly. * Beta submit body (#513) * Add 'Step X' to sidebar tabs to guide users. * Revert add step X to sidebar. * Update submit and validate to upload data in the request body, not header. * Copy R/schematic_rest_api.R to functions/ so it works on shinyappps.io * Update validate and submit calls to use a file name instead of passing a json object. * Add renv::isolate to Dockerfile * source renv activate on startup. * Simplify renv install and use jammy repo * Use updated shiny-base image that has libglpk-dev to avoid networkD3 error installing from binary. * Try renv and setting the repo to focal binaries. * Add renv::isolate inside of rsconnect deploy step * Only do deployApp() to test where deployment fails * try avoiding cache * use checkout v3 * use ubuntulatest and jammy repo. * Use focal once again. * update renv.lock * Update renv lock from ubuntu * Back to install-pkgs script * fix spacing * ubuntu 20.04 * checkout v3 and pandoc v2 * Don't source renv/activate.R * Remove R package install script to use renv * Update renv.lock for R4.1.3 * Use renv in workflow. restore and isolate in deploy step * Remove repo from Dockerfile because it's in the renv.lock file. * Use shinydashboardplus from github, not cran * need to source activate.R in Rprofile * Update renv activate to v0.17 * Update base image for stringi lib. * Update shiny-base image to fix stringi error * Update base image to afwillia/shiny-base:release-1.2 * Use same versioon of R as the AWS deployment. * Test removing renv calls from deployApp step * confirmed removing renv from deployApps crashes. Add renv::restore() back. * Install rsconnect in deployapps step * Remove rsconnect, remotes, and packrat from renv.lock because these are specific to shinyapps.io deployment workflow. * Add updated renv/activate.R and .gitignore * Revert changes to renv/activate.R because this breaks the shinyapps.io deployment * use sagebionetworks/shiny-base and install extra libs in DCA dockerfile * Try installing from source to avoid stringri error on jammy * Add rstudio jammy repo to dockerfile to speed up package installation. * Test overriding renv repo in workkflow to jammy binary * Still uses ubuntu20 cache, despite running on ubuntu22. What happens if we bypass_cache. * Add 'with' in setup-renv * Don't indent 'with' in setup-renv * shinydashboardPlus dependency fastmap fails to install because there is no 'GLIBCXX_3.4.29'. Abandon this quest and use previously functional setup. * Try updating glibc and avoid shinydashboardplus and fastmap error * Updating glibc-source did not work. Go back to existing workflow. * Update node for security patch * Remove default node then install from nodesource * Update renv gitignore with defaults from v0.17 * Beta dcc config (#515) * Add config file for DCC settings * Use dcc config file instead of env vars. * Use dcc config file in ui instead of env vars. * Use dcc config file in server instead of env vars. * Add theme colors to dcc config file * Change theme switching to use dcc config file instead of rds files. * Remove rds theme files. * rename csbc template file to mc2 * Make waiter background transparent. * Update waiter background to be slightly transparent * Update waiter landing text from ux feedback * Change waiter text and add things may take a minute. * welcome user by name not synapse username. * Remove env variables from shinyapps.io workflow that are in dcc_config.csv * Update staging app to use the schematic rest api service. * Don't install schematic with rest api deploy * Remove data model config from workflow since those files are hard coded. * Set host instead of port in Renviron. * Update README for new configuration options with environment variables. * Renviron instead of Rprofile. * Format files for github rendering * clean up readme for github formatting * Fixes FDS-73. An issue where validation was failing. * move shinyapps_deploy documentation to docs/ * Update link to shinyapps.io deployment docs * Update link to OAuth client setup docs * Update shinyapps.io deployment for the REST/reticulate version of DCA. * Fix extra characters in code chunks. * Add error checking for DCA_SCHEMATIC_API_TYPE env var * Update workflow to conditionally install python if using reticulate * Remove space between $ and {. * fix variable name in if statement * add shell: bash to venv activate * Write .Renviron after checking out repo * checkout repo first. * update system then checkout * try using setup-python for caching * Don't use setup-py yet * try caching venv * put IFs into one line * update python cache * try using setup-python with pip not pipenv * fix typo * set env path * update cache dir * update cache dir 2 * set hash files in key * update cache key * create cache key from release 23.1.1 schematic poetry lock * download with wget * fix expression * use double & * only cache venv.zip * move if statements * don't use pip cache dir * add pip cache dir * add pip cache dir 2 * add pip cache dir 4 * add pip download cache dir * print out pip version * move schematic install * Try installing schematicpy outside of venv for caching * upgrade pip * use default pip * create data model config * update testing1 instance with schematic rest api * hotfix when validationResult returns a warning to highlight particular columns, if the vector is length 0 then DTable quietly breaks. This results in the submit button not showing up. To address this, check the length of cells to highlight. If it's 0, skip highlighting. Need to determine why validationResult is not returning the correct cells to highlight. * add error checking to model_submit * write manifest to temp dir * Add error handling and table_manipulation arguments to model_submit. * Update tests for model_submit * Update model_submit arguments to use dcc_config and add arguments. * Redploy app with reticulate to staging instance. * Add restrict rules to validate * update validate tests * Update README * Don't cache python installation, it doesn't speed things up much. * Install schematicpy in its own step --- .Rbuildignore | 2 + .Rprofile | 1 + .github/schematic_config.yml | 42 + .github/workflows/docker_build.yml | 51 + .github/workflows/shinyapps_deploy.yml | 124 +- .github/workflows/test_schematic_api.yml | 93 ++ .gitignore | 2 +- DESCRIPTION | 11 + Dockerfile | 21 + NAMESPACE | 16 + R/schematic_rest_api.R | 306 ++++ R/synapse_rest_api.R | 100 ++ README.md | 118 +- dca_startup.sh | 8 + dcc_config.csv | 5 + .../shinyapps_deploy.md | 18 +- functions/dashboardFuns.R | 153 +- functions/dcWaiter.R | 22 +- functions/schematic_rest_api.R | 290 ++++ functions/schematic_reticulate.R | 105 ++ functions/synapse_rest_api.R | 101 ++ functions/utils.R | 38 + functions/validationResult.R | 20 +- global.R | 90 +- .../HTAN-Biospecimen-Tier-1-2-fail.csv | 2 + .../HTAN-Biospecimen-Tier-1-2-pass.csv | 2 + install-pkgs.R | 49 - man/get_asset_view_table.Rd | 23 + man/manifest_generate.Rd | 36 + man/manifest_populate.Rd | 29 + man/manifest_validate.Rd | 29 + man/model_component_requirements.Rd | 26 + man/model_submit.Rd | 37 + man/storage_dataset_files.Rd | 33 + man/storage_project_datasets.Rd | 27 + man/storage_projects.Rd | 24 + man/synapse_access.Rd | 25 + man/synapse_get.Rd | 22 + man/synapse_is_certified.Rd | 26 + man/synapse_user_profile.Rd | 28 + modules/DTable.R | 1 + modules/dashboard/dashboard.R | 34 +- .../selectedDatatypeTab/dbCheckList.R | 82 + modules/dashboard/shared/setTabTitle.R | 15 + renv.lock | 1412 +++++++++++++++++ renv/.gitignore | 7 + renv/activate.R | 1020 ++++++++++++ schematic_config.yml | 52 +- server.R | 615 +++++-- tests/testthat.R | 3 + tests/testthat/test_schematic_rest_api.R | 136 ++ tests/testthat/test_synapse_rest_api.R | 36 + tests/testthat/test_utils.R | 7 + ui.R | 134 +- www/img/INCLUDE DCC Logo-01.png | Bin 0 -> 185531 bytes www/img/Logo_Sage_Logomark.png | Bin 0 -> 4873 bytes www/img/cckp_logo.png | Bin 0 -> 14950 bytes www/scss/basic/_button.scss | 6 +- www/scss/basic/_message.scss | 30 +- www/scss/basic/_variables.scss | 17 +- www/scss/modules/_checkList.scss | 2 +- www/scss/modules/_collapsibleTree.scss | 21 +- www/scss/modules/_dbRater.scss | 15 +- www/scss/modules/_dbStatsBox.scss | 2 +- www/scss/section/_header.scss | 2 +- www/scss/section/dashboard/_dashboard.scss | 28 +- .../dashboard/_selectedDatatypeTab.scss | 16 +- .../dashboard/_selectedProjectTab.scss | 13 - www/template_config/config.json | 31 + www/template_config/config_offline.json | 16 + www/template_config/example_config.json | 31 + www/template_config/htan_config.json | 306 ++++ www/template_config/include_config.json | 12 + www/template_config/mc2_config.json | 131 ++ 74 files changed, 5790 insertions(+), 598 deletions(-) create mode 100644 .Rbuildignore create mode 100644 .github/schematic_config.yml create mode 100644 .github/workflows/docker_build.yml create mode 100644 .github/workflows/test_schematic_api.yml create mode 100644 DESCRIPTION create mode 100644 Dockerfile create mode 100644 NAMESPACE create mode 100644 R/schematic_rest_api.R create mode 100644 R/synapse_rest_api.R create mode 100755 dca_startup.sh create mode 100644 dcc_config.csv rename shinyapps_deploy.md => docs/shinyapps_deploy.md (61%) create mode 100644 functions/schematic_rest_api.R create mode 100644 functions/schematic_reticulate.R create mode 100644 functions/synapse_rest_api.R create mode 100644 inst/testdata/HTAN-Biospecimen-Tier-1-2-fail.csv create mode 100644 inst/testdata/HTAN-Biospecimen-Tier-1-2-pass.csv delete mode 100644 install-pkgs.R create mode 100644 man/get_asset_view_table.Rd create mode 100644 man/manifest_generate.Rd create mode 100644 man/manifest_populate.Rd create mode 100644 man/manifest_validate.Rd create mode 100644 man/model_component_requirements.Rd create mode 100644 man/model_submit.Rd create mode 100644 man/storage_dataset_files.Rd create mode 100644 man/storage_project_datasets.Rd create mode 100644 man/storage_projects.Rd create mode 100644 man/synapse_access.Rd create mode 100644 man/synapse_get.Rd create mode 100644 man/synapse_is_certified.Rd create mode 100644 man/synapse_user_profile.Rd create mode 100644 modules/dashboard/selectedDatatypeTab/dbCheckList.R create mode 100644 modules/dashboard/shared/setTabTitle.R create mode 100644 renv.lock create mode 100644 renv/.gitignore create mode 100644 renv/activate.R create mode 100644 tests/testthat.R create mode 100644 tests/testthat/test_schematic_rest_api.R create mode 100644 tests/testthat/test_synapse_rest_api.R create mode 100644 tests/testthat/test_utils.R create mode 100644 www/img/INCLUDE DCC Logo-01.png create mode 100644 www/img/Logo_Sage_Logomark.png create mode 100644 www/img/cckp_logo.png create mode 100644 www/template_config/config.json create mode 100644 www/template_config/config_offline.json create mode 100644 www/template_config/example_config.json create mode 100644 www/template_config/htan_config.json create mode 100644 www/template_config/include_config.json create mode 100644 www/template_config/mc2_config.json diff --git a/.Rbuildignore b/.Rbuildignore new file mode 100644 index 00000000..0e9fd074 --- /dev/null +++ b/.Rbuildignore @@ -0,0 +1,2 @@ +^renv$ +^renv\.lock$ diff --git a/.Rprofile b/.Rprofile index 8fe7c4b1..974262f3 100644 --- a/.Rprofile +++ b/.Rprofile @@ -1,3 +1,4 @@ +source("renv/activate.R") .First <- function() { options( repos = c( diff --git a/.github/schematic_config.yml b/.github/schematic_config.yml new file mode 100644 index 00000000..01f46044 --- /dev/null +++ b/.github/schematic_config.yml @@ -0,0 +1,42 @@ +# During the github workflow to auto deploy the app +# This config file will be used to overwrite the config.yml in the schematic folder +# +# Please modify the configuration values based on your project + +# Do not change the 'definitions' section unless you know what you're doing +definitions: + synapse_config: '.synapseConfig' + creds_path: 'credentials.json' + token_pickle: 'token.pickle' + service_acct_creds: 'schematic_service_account_creds.json' + +synapse: + master_fileview: 'syn20446927' # fileview of project with datasets on Synapse + manifest_folder: 'manifests' # manifests will be downloaded to this folder + manifest_filename: 'synapse_storage_manifest.csv' # name of the manifest file in the project dataset + token_creds: 'syn23643259' # synapse ID of credentials.json file + service_acct_creds: 'syn25171627' # synapse ID of service_account_creds.json file + +manifest: + title: 'Patient Manifest' # title of metadata manifest file + data_type: 'Patient' # component or data type from the data model + +model: + input: + download_url: 'https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld' # url to download JSON-LD data model + location: 'data-models/HTAN.model.jsonld' # path to JSON-LD data model + file_type: 'local' # only type "local" is supported currently + +style: + google_manifest: + req_bg_color: + red: 0.9215 + green: 0.9725 + blue: 0.9803 + opt_bg_color: + red: 1.0 + green: 1.0 + blue: 0.9019 + master_template_id: '1LYS5qE4nV9jzcYw5sXwCza25slDfRA1CIg3cs-hCdpU' + strict_validation: true + diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml new file mode 100644 index 00000000..54728c7b --- /dev/null +++ b/.github/workflows/docker_build.yml @@ -0,0 +1,51 @@ +# Taken from https://sagebionetworks.jira.com/wiki/spaces/IT/pages/2721251378/GitHub+workflow+for+Docker+container+build+and+registry+deployment +# Workflow derived from https://github.com/r-lib/actions/tree/master/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help + +name: Create and publish a Docker image + +on: + push: + tags: + - '*beta*' + +env: + REGISTRY: ghcr.io + IMAGE_PATH: ghcr.io/${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.IMAGE_PATH }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{raw}} + + - name: Build and push Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + diff --git a/.github/workflows/shinyapps_deploy.yml b/.github/workflows/shinyapps_deploy.yml index 95caabaf..6fabe65f 100644 --- a/.github/workflows/shinyapps_deploy.yml +++ b/.github/workflows/shinyapps_deploy.yml @@ -1,6 +1,3 @@ -# Workflow derived from https://github.com/r-lib/actions/tree/master/examples -# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help - name: shiny-deploy on: @@ -8,67 +5,84 @@ on: branches: - main - develop* + - develop-* + - beta-schematic-rest-api tags: - v[0-9]+.[0-9]+.[0-9]+ paths-ignore: - '.github/ISSUE_TEMPLATE/**' - '**/*.md' - '**/.gitignore' - jobs: shiny-deploy: runs-on: ubuntu-latest - # This image seems to be based on rocker/r-ver which in turn is based on debian container: rocker/rstudio:4.1.2 env: - # This should not be necessary for installing from public repo's however remotes::install_github() fails without it. - GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - + GITHUB_PAT: ${{ github.GITHUB_TOKEN }} + DCA_SCHEMATIC_API_TYPE: reticulate + DCA_API_HOST: "https://schematic.api.sagebionetworks.org" + DCA_API_PORT: "" steps: - name: Install System Dependencies run: | sudo apt-get update - sudo apt-get install -y pip python3.8-venv libcurl4-openssl-dev - - uses: actions/checkout@v2 - - uses: r-lib/actions/setup-pandoc@v1 + sudo apt-get install -y libcurl4-openssl-dev libpng-dev libxml2-dev + + - uses: actions/checkout@v3 - - name: Create and Activate Python Virtual Environment + - uses: r-lib/actions/setup-pandoc@v2 + + - if: ${{env.DCA_SCHEMATIC_API_TYPE == 'reticulate' }} + name: Install python + run: | + sudo apt-get install -y pip python3.8-venv + + - if: ${{env.DCA_SCHEMATIC_API_TYPE == 'reticulate' }} + name: Set up virtual environment + # When shinyapps.io supports python 3.9+ split this into two steps + # to install schematic from pypi or github shell: bash run: | python3 -m venv .venv chmod 755 .venv/bin/activate source .venv/bin/activate - - name: Install R Packages Dependencies - run: | - R -f install-pkgs.R - - - name: Install Schematic + - if: ${{env.DCA_SCHEMATIC_API_TYPE == 'reticulate' }} + name: Install schematicpy shell: bash run: | - # has to activate each bash step source .venv/bin/activate - echo Installing pypi version of schematic - git clone --single-branch --branch main https://github.com/Sage-Bionetworks/schematic.git - cp schematic_config.yml schematic/config.yml - pip3 install schematicpy - + pip install schematicpy==23.1.1 + +# https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md#caching-packages +# For the future, look at poetry install +# - uses: actions/checkout@v3 +# - name: Install poetry +# run: pipx install poetry +# - uses: actions/setup-python@v4 +# with: +# python-version: '3.9' +# cache: 'poetry' +# - run: poetry install +# - run: poetry run pytest +# - name: Set Configurations for Schematic + # write out configuration files using github secrets + if: ${{env.DCA_SCHEMATIC_API_TYPE == 'reticulate' }} shell: bash run: | - # write out configuration files using github secrets echo "${{ secrets.SCHEMATIC_SYNAPSE_CONFIG }}" > .synapseConfig - echo "${{ secrets.SCHEMATIC_CREDS_PATH }}" > credentials.json - echo "${{ secrets.SCHEMATIC_TOKEN_PICKLE }}" | base64 -d > token.pickle - name: Save service account credentials for Schematic + if: ${{env.DCA_SCHEMATIC_API_TYPE == 'reticulate' }} id: create-json uses: jsdaniell/create-json@1.1.2 with: name: 'schematic_service_account_creds.json' json: ${{ secrets.SCHEMATIC_SERVICE_ACCT_CREDS }} - - - name: Set Configurations for Data Model + + - if: ${{env.DCA_SCHEMATIC_API_TYPE == 'reticulate' }} + name: Set Configurations for Data Model shell: bash run: | source .venv/bin/activate @@ -77,26 +91,51 @@ jobs: -c schematic_config.yml \ --service_repo 'Sage-Bionetworks/schematic' \ --overwrite - - - name: zip virtual env + + - if: ${{env.DCA_SCHEMATIC_API_TYPE == 'reticulate' }} + name: zip virtual env shell: bash # ShinyApps has a limit of 7000 files, far exceeded by the many Python dependencies # that this app' has. As a workaround we zip the virtual environment and later # unzip it in 'global.R' run: | zip -rm .venv.zip .venv + + - uses: r-lib/actions/setup-renv@v2 + + - name: shinyapps.io R pkg hack + # Delete files that makes shinyapps.io treat this like a golem app + shell: bash + run: | + cp R/* functions/ + rm -r R/ DESCRIPTION NAMESPACE + + - name: Write R environmental variables + shell: bash + run: | + echo 'DCA_CLIENT_ID="${{ secrets.OAUTH_CLIENT_ID }}"' >> .Renviron + echo 'DCA_CLIENT_SECRET="${{ secrets.OAUTH_CLIENT_SECRET }}"' >> .Renviron + + echo 'DCA_SCHEMATIC_API_TYPE="${{ env.DCA_SCHEMATIC_API_TYPE }}"' >> .Renviron + echo 'DCA_API_PORT="${{ env.DCA_API_PORT }}"' >> .Renviron + echo 'DCA_API_HOST="${{ env.DCA_API_HOST }}"' >> .Renviron + + echo 'GITHUB_PAT="${{ secrets.GITHUB_TOKEN }}"' >> .Renviron - name: Authorize and deploy app shell: Rscript {0} run: | - # if there is a tag, 'refName' will be tag name + renv::restore() + install.packages("rsconnect") + repo <- Sys.getenv("GITHUB_REPOSITORY") appName <- strsplit(repo, "/")[[1]][2] refName <- Sys.getenv("GITHUB_REF_NAME") + # if tag is v*.*.*, deploy to prod, if main to staging, otherwise to test if (grepl("v[0-9]+.[0-9]+.[0-9]+", refName)) { message("Deploying release version of app") - } else if (refName == "main") { + } else if (refName %in% c("main", "beta-schematic-rest-api")) { appName <- paste(appName, "staging", sep = "-") message("Deploying staging version of app") } else { @@ -104,22 +143,20 @@ jobs: message("Deploying testing version of app") } message(sprintf("Deploying to %s instance.", appName)) + rsConnectUser <- "${{ secrets.RSCONNECT_USER }}" rsConnectToken <- "${{ secrets.RSCONNECT_TOKEN }}" rsConnectSecret <- "${{ secrets.RSCONNECT_SECRET }}" - # create config file - config <- "CLIENT_ID: ${{ secrets.OAUTH_CLIENT_ID }}" - config <- c(config, "CLIENT_SECRET: ${{ secrets.OAUTH_CLIENT_SECRET }}") - appUrl <- sprintf("https://%s.shinyapps.io/%s", rsConnectUser, appName) - config <- c(config, sprintf("APP_URL: %s", appUrl)) - configFileConn <- file("oauth_config.yml") - tryCatch( - writeLines(config, configFileConn), - finally=close(configFileConn) - ) rsconnect::setAccountInfo(rsConnectUser, rsConnectToken, rsConnectSecret) + + # Set app URL for OAuth + appUrl <- sprintf("https://%s.shinyapps.io/%s", rsConnectUser, appName) + renviron <- readLines(".Renviron") + renviron <- c(renviron, sprintf("DCA_APP_URL=%s", appUrl)) + writeLines(renviron, ".Renviron") + # Get app names. If app exists, configure then deploy. Otherwise - # deploy then configure + # deploy then configure. apps <- rsconnect::applications()$name if (appName %in% apps) { rsconnect::configureApp(appName = appName, size = "xxxlarge", logLevel = "verbose") @@ -128,4 +165,3 @@ jobs: rsconnect::deployApp(appName = appName) rsconnect::configureApp(appName = appName, size = "xxxlarge", logLevel = "verbose") } - diff --git a/.github/workflows/test_schematic_api.yml b/.github/workflows/test_schematic_api.yml new file mode 100644 index 00000000..b331cd1b --- /dev/null +++ b/.github/workflows/test_schematic_api.yml @@ -0,0 +1,93 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/master/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help + +# This workflow creates an instance of data curator app with schematic, then +# creates a flask server running schematic to test data curator's use of +# schematic's REST API endpoints. +name: test-schematic-api + +on: + push: + branches: + - develop + paths-ignore: + - .github/ISSUE_TEMPLATE/** + +jobs: + test-schematic-rest-api: + runs-on: ubuntu-latest + # This image seems to be based on rocker/r-ver which in turn is based on debian + container: rocker/rstudio + env: + # This should not be necessary for installing from public repo's however remotes::install_github() fails without it. + GITHUB_PAT: ${{ secrets.REPO_PAT }} + + steps: + - name: Install System Dependencies + run: | + sudo apt-get update + sudo apt-get install -y pip python3.8-venv libcurl4-openssl-dev + + - uses: actions/checkout@v2 + + - uses: r-lib/actions/setup-pandoc@v1 + + - name: Create and Activate Python Virtual Environment + shell: bash + run: | + python3 -m venv .venv + chmod 755 .venv/bin/activate + source .venv/bin/activate + + - name: Install R Packages Dependencies + run: | + R -f install-pkgs.R + + - name: Install Schematic + shell: bash + run: | + source .venv/bin/activate + # use 'poetry' to install schematic from the develop branch + pip3 install poetry + git clone --single-branch --branch develop https://github.com/Sage-Bionetworks/schematic.git + cd schematic + poetry build + pip3 install dist/schematicpy-1.0.0-py3-none-any.whl + + - name: Set Configurations for Schematic + shell: bash + run: | + source .venv/bin/activate + # download data model to the correct location + R -e ' + config <- yaml::yaml.load_file(".github/schematic_config.yml"); + url <- config$model$input$download_url; + path <- config$model$input$location; + system(sprintf("mkdir -p %s", dirname(path))); + system(sprintf("wget %s -O %s", url, path)); + ' + # overwrite the config.yml in schematic + mv -f .github/schematic_config.yml schematic/config.yml + # write out configuration files using github secrets + echo "${{ secrets.SCHEMATIC_SYNAPSE_CONFIG }}" > schematic/.synapseConfig + echo "${{ secrets.SCHEMATIC_SERVICE_ACCT_CREDS }}" > schematic/schematic_service_account_creds.json + echo "${{ secrets.SCHEMATIC_CREDS_PATH }}" > schematic/credentials.json + echo "${{ secrets.SCHEMATIC_TOKEN_PICKLE }}" | base64 -d > schematic/token.pickle + + - name: Run schematic API service + shell: bash + run: | + echo "SYNAPSE_PAT='${{ secrets.SYNAPSE_PAT }}'" > .Renviron + source .venv/bin/activate + cd schematic + pip3 uninstall -y markupsafe + pip3 install markupsafe==2.0.1 + python3 run_api.py & + + - name: Run tests + shell: Rscript {0} + run: | + devtools::test() + + + diff --git a/.gitignore b/.gitignore index a682436c..efe572ba 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,4 @@ schematic_config.yml .settings/** .venv # tmp not ignore config.json -!www/config.json \ No newline at end of file +!www/config.json diff --git a/DESCRIPTION b/DESCRIPTION new file mode 100644 index 00000000..b1c50b7e --- /dev/null +++ b/DESCRIPTION @@ -0,0 +1,11 @@ +Package: datacurator +Version: 1.0.0 +Title: HTAN Metadata Ingress Shiny App +Description: This Shiny app allows users to validate metadata files and upload to Synapse. +Authors: Rongrong Chai, Xengie Doan, Milen Nikolov, Sujay Patil, Robert Allaway, Bruno Grande, Anthony Williams, Loren Wolfe +License: file LICENSE +Encoding: UTF-8 +Roxygen: list(markdown = TRUE) +RoxygenNote: 7.2.1 +Suggests: + covr diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..51a6dd9b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM sagebionetworks/shiny-base:release-1.1 +LABEL maintainer="Anthony anthony.williams@sagebase.org" + +USER root +RUN apt-get update +RUN apt-get install -y libxml2 libglpk-dev libicu-dev libicu70 curl + +# Update node. https://github.com/nodesource/distributions +RUN apt-get remove nodejs +RUN curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - && apt-get install -y nodejs + +USER shiny + +WORKDIR /srv/shiny-server/app +COPY --chown=shiny ./ ./ + +# set up r packages via renv. Use binary lib matching the shiny-base ubuntu version +# to speed up installatioon. +RUN Rscript -e 'renv::restore(repos="https://packagemanager.rstudio.com/all/__linux__/jammy/latest"); renv::install("./")' + +CMD ["./dca_startup.sh"] diff --git a/NAMESPACE b/NAMESPACE new file mode 100644 index 00000000..69ce606e --- /dev/null +++ b/NAMESPACE @@ -0,0 +1,16 @@ +# Generated by roxygen2: do not edit by hand + +export(get_asset_view_table) +export(manifest_download) +export(manifest_generate) +export(manifest_populate) +export(manifest_validate) +export(model_component_requirements) +export(model_submit) +export(storage_dataset_files) +export(storage_project_datasets) +export(storage_projects) +export(synapse_access) +export(synapse_get) +export(synapse_is_certified) +export(synapse_user_profile) diff --git a/R/schematic_rest_api.R b/R/schematic_rest_api.R new file mode 100644 index 00000000..6ea63789 --- /dev/null +++ b/R/schematic_rest_api.R @@ -0,0 +1,306 @@ +#' @description Check if a httr request succeeded. +#' @param x An httr response object +check_success <- function(x){ + if (!inherits(x, "response")) stop("Input must be an httr reponse object") + status <- httr::http_status(x) + if (tolower(status$category) == "success") { + return() + } else { + stop(sprintf("Response from server: %s", status$reason)) + } +} + +#' @description Download an existing manifest +#' @param url URI of API endpoint +#' @param input_token Synapse PAT +#' @param asset_view ID of view listing all project data assets +#' @param dataset_id the parent ID of the manifest +#' @param as_json if True return the manifest in JSON format +#' @returns a csv of the manifest +#' @export +manifest_download <- function(url="http://localhost:3001/v1/manifest/download", + input_token, asset_view, dataset_id, as_json=TRUE){ + req <- httr::GET(url, + query = list( + asset_view = asset_view, + dataset_id = dataset_id, + as_json = as_json, + input_token = input_token + )) + + check_success(req) + manifest <- httr::content(req, as = "text") + jsonlite::fromJSON(manifest) +} + +#' schematic rest api to generate manifest +#' +#' @param title Name of dataset +#' @param data_type Type of dataset +#' @param oauth true or false STRING passed to python +#' @param use_annotations true or false STRING passed to python +#' @param dataset_id Synapse ID of existing manifest +#' +#' @returns a URL to a google sheet +#' @export +manifest_generate <- function(url="http://localhost:3001/v1/manifest/generate", + schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #nolint + title, data_type, + use_annotations="false", dataset_id=NULL, + asset_view, output_format, input_token = NULL) { + + req <- httr::GET(url, + query = list( + schema_url=schema_url, + title=title, + data_type=data_type, + use_annotations=use_annotations, + dataset_id=dataset_id, + asset_view=asset_view, + output_format=output_format, + input_token = input_token + )) + + check_success(req) + manifest_url <- httr::content(req) + manifest_url +} + +#' Populate a manifest sheet +#' +#' @param url URL to schematic API endpoint +#' @param schema_url URL to a schema jsonld +#' @param data_type Type of dataset +#' @param title Title of csv +#' @param csv_file Filepath of csv to validate +#' @export +manifest_populate <- function(url="http://localhost:3001/v1/manifest/populate", + schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #notlint + data_type, title, return_excel=FALSE, csv_file) { + + req <- httr::POST(url, + query=list( + schema_url=schema_url, + data_type=data_type, + title=title, + return_excel=return_excel), + body=list(csv_file=httr::upload_file(csv_file, type = "text/csv")) + ) + check_success(req) + req + +} + + +#' schematic rest api to validate metadata +#' +#' @param url URL to schematic API endpoint +#' @param schema_url URL to a schema jsonld +#' @param data_type Type of dataset +#' @param file_name Filepath of csv to validate +#' +#' @returns An empty list() if sucessfully validated. Or a list of errors. +#' @export +manifest_validate <- function(url="http://localhost:3001/v1/model/validate", + schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #nolint + data_type, file_name, restrict_rules=FALSE) { + req <- httr::POST(url, + query=list( + schema_url=schema_url, + data_type=data_type, + restrict_rules=restrict_rules), + body=list(file_name=httr::upload_file(file_name)) + ) + + # Format server error in a way validationResult can handle + if (httr::http_status(req)$category == "Server error") { + return( + list( + list( + "errors" = list( + Row = NA, Column = NA, Value = NA, + Error = sprintf("Cannot validate manifest: %s", + httr::http_status(req)$message) + ) + ) + ) + ) + } + check_success(req) + annotation_status <- httr::content(req) + annotation_status +} + + +#' schematic rest api to submit metadata +#' +#' @param url URL to schematic API endpoint +#' @param schema_url URL to a schema jsonld +#' @param data_type Type of dataset +#' @param dataset_id Synapse ID of existing manifest +#' @param input_token Synapse login cookie, PAT, or API key. +#' @param csv_file Filepath of csv to validate +#' +#' @returns TRUE if successful upload or validate errors if not. +#' @export +model_submit <- function(url="http://localhost:3001/v1/model/submit", + schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #notlint + data_type, dataset_id, restrict_rules=FALSE, input_token, json_str=NULL, asset_view, + use_schema_label=TRUE, manifest_record_type="table", file_name, + table_manipulation="replace") { + req <- httr::POST(url, + #add_headers(Authorization=paste0("Bearer ", pat)), + query=list( + schema_url=schema_url, + data_type=data_type, + dataset_id=dataset_id, + input_token=input_token, + restrict_rules=restrict_rules, + json_str=json_str, + asset_view=asset_view, + use_schema_label=use_schema_label, + manifest_record_type=manifest_record_type, + table_manipulation=table_manipulation), + body=list(file_name=httr::upload_file(file_name)) + #body=list(file_name=file_name) + ) + + check_success(req) + manifest_id <- httr::content(req) + manifest_id +} + +#' Given a source model component (see https://w3id.org/biolink/vocab/category for definnition of component), return all components required by it. +#' +#' @param schema_url Data Model URL +#' @param source_component an attribute label indicating the source component. (i.e. Patient, Biospecimen, ScRNA-seqLevel1, ScRNA-seqLevel2) +#' @param as_graph if False return component requirements as a list; if True return component requirements as a dependency graph (i.e. a DAG) +#' +#' @returns A list of required components associated with the source component. +#' @export +model_component_requirements <- function(url="http://localhost:3001/v1/model/component-requirements", + schema_url, source_component, + as_graph = FALSE) { + + req <- httr::GET(url, + query = list( + schema_url = schema_url, + source_component = source_component, + as_graph = as_graph + )) + + check_success(req) + cont <- httr::content(req) + + if (inherits(cont, "xml_document")){ + err_msg <- xml2::xml_text(xml2::xml_child(cont, "head/title")) + stop(sprintf("%s", err_msg)) + } + + cont + +} + + +#' Gets all datasets in folder under a given storage project that the current user has access to. +#' +#' @param url URL to schematic API endpoint +#' @param syn_master_file_view synapse ID of master file view. +#' @param syn_master_file_name Synapse storage manifest file name. +#' @param project_id synapse ID of a storage project. +#' @param input_token synapse PAT +#' +#'@export +storage_project_datasets <- function(url="http://localhost:3001/v1/storage/project/datasets", + asset_view, + project_id, + input_token) { + + req <- httr::GET(url, + #add_headers(Authorization=paste0("Bearer ", pat)), + query=list( + asset_view=asset_view, + project_id=project_id, + input_token=input_token) + ) + + check_success(req) + httr::content(req) +} + +#' Get all storage projects the current user has access to +#' +#' @param url URL to schematic API endpoint +#' @param syn_master_file_view synapse ID of master file view. +#' @param syn_master_file_name Synapse storage manifest file name. +#' @param input_token synapse PAT +#' +#' @export +storage_projects <- function(url="http://localhost:3001/v1/storage/projects", + asset_view, + input_token) { + + req <- httr::GET(url, + query = list( + asset_view=asset_view, + input_token=input_token + )) + + check_success(req) + httr::content(req) +} + +#' /storage/dataset/files +#' +#' @param url URL to schematic API endpoint +#' @param syn_master_file_view synapse ID of master file view. +#' @param syn_master_file_name Synapse storage manifest file name. +#' @param dataset_id synapse ID of a storage dataset. +#' @param file_names a list of files with particular names (i.e. Sample_A.txt). If you leave it empty, it will return all dataset files under the dataset ID. +#' @param full_path Boolean. If True return the full path as part of this filename; otherwise return just base filename +#' @param input_token synapse PAT +#' +#' @export +storage_dataset_files <- function(url="http://localhost:3001/v1/storage/dataset/files", + asset_view, + dataset_id, file_names=list(), + full_path=FALSE, input_token) { + + req <- httr::GET(url, + #add_headers(Authorization=paste0("Bearer ", pat)), + query=list( + asset_view=asset_view, + dataset_id=dataset_id, + file_names=file_names, + full_path=full_path, + input_token=input_token)) + check_success(req) + httr::content(req) + +} + +#' /storage/asset/table +#' +#' @param url URL to schematic API endpoint +#' @param input_token synapse PAT +#' @param asset_view Synapse ID of asset view +#' @export +get_asset_view_table <- function(url="http://localhost:3001/v1/storage/assets/tables", + input_token, asset_view, return_type="json") { + + req <- httr::GET(url, + query=list( + asset_view=asset_view, + input_token=input_token, + return_type=return_type)) + + check_success(req) + if (return_type=="json") { + return(list2DF(fromJSON(httr::content(req)))) + } else { + csv <- readr::read_csv(httr::content(req)) + return(csv) + } + +} + diff --git a/R/synapse_rest_api.R b/R/synapse_rest_api.R new file mode 100644 index 00000000..916a349d --- /dev/null +++ b/R/synapse_rest_api.R @@ -0,0 +1,100 @@ +#' @title Get Synapse user profile info +#' @details +#' For information on authenticating synapse REST API queries +#' https://docs.synapse.org/rest/#org.sagebionetworks.auth.controller.AuthenticationController +#' Authentication to Synapse services requires an access token passed in the +#' HTTP Authorization header, as per the HTTP bearer authorization standard. +#' https://datatracker.ietf.org/doc/html/rfc6750#section-2.1 +#' https://docs.synapse.org/rest/#org.sagebionetworks.repo.web.controller.UserProfileController +#' PAT is the synapse personal access token OR the browser cookie from a logged-in session. +#' +#' @param url Synapse REST API url for userProfile +#' @param auth Synapse PAT or authorization token +#' +#' @export +synapse_user_profile <- function( + url="https://repo-prod.prod.sagebase.org/repo/v1/userProfile", auth=NULL) { + req <- httr::GET(url, httr::add_headers(Authorization=paste0("Bearer ", auth))) + httr::content(req) +} + +#' @title Is a Synapse user certified? +#' @details +#' https://rest-docs.synapse.org/rest/GET/user/id/certifiedUserPassingRecord.html +#' Based on python client https://github.com/Sage-Bionetworks/synapsePythonClient/blob/3da21b9e2542c7da8cdab9925926737fe2162a54/synapseclient/client.py#L606 +#' +#' @param url URL to Synapse REST API for certification +#' @param endpoint The Synapse API endpoint +#' @param auth Synapse PAT or authorization token +#' +#' @export +synapse_is_certified <- function(url="https://repo-prod.prod.sagebase.org/repo/v1/user", + endpoint="certifiedUserPassingRecord", + auth=NULL) { + + # Get user profile and ownerId + user_profile <- synapse_user_profile(auth=auth) + if (!"ownerId" %in% names(user_profile)) return(FALSE) + ownerid <- user_profile[["ownerId"]] + url_req <- file.path(url, ownerid, endpoint) + req <- httr::GET(url_req) + httr::content(req)[["passed"]] + +} + + +#content(GET(sprintf("https://repo-prod.prod.sagebase.org/repo/v1/user/%s/certifiedUserPassingRecord", "3438856"))) +#https://repo-prod.prod.sagebase.org/repo/v1/user + +#' @title GET Synapse Entity +#' @description Wrapper for https://rest-docs.synapse.org/rest/GET/entity/id.html +#' +#' @param url URL of synapse REST API GET table entity endpoint +#' @param id ID of synapse table +#' @param auth Synapse PAT +#' +#' @export +synapse_get <- function(url = "https://repo-prod.prod.sagebase.org/repo/v1/entity/", + id, auth) { + + if (is.null(id)) stop("id cannot be NULL") + req_url <- file.path(url, id) + req <- httr::GET(req_url, + httr::add_headers(Authorization=paste0("Bearer ", auth))) + + # Send error if unsuccessful query + status <- httr::http_status(req) + if (status$category != "Success") stop(status$message) + + cont <- httr::content(req) + dplyr::bind_rows(cont) + +} + + +#' @title Check Access Permissions to a Synapse Entity +#' @description wrapper for https://rest-docs.synapse.org/rest/GET/entity/id/access.html +#' +#' @param url URL to REST API endpoint +#' @param id Synapse ID +#' @param access Access Type to check +#' @param auth Synapse authentication token +#' +#' @export +synapse_access <- function(url = "https://repo-prod.prod.sagebase.org/repo/v1/entity", + id, access, auth) { + + if (is.null(id)) stop("id cannot be NULL") + req_url <- file.path(url, id, "access") + req <- httr::GET(req_url, + httr::add_headers(Authorization=paste0("Bearer ", auth)), + query = list(accessType=access)) + + # Send error if unsuccessful query + status <- httr::http_status(req) + if (status$category != "Success") stop(status$message) + + cont <- httr::content(req) + cont$result + +} diff --git a/README.md b/README.md index 5f8b08c2..f4f9deb0 100644 --- a/README.md +++ b/README.md @@ -2,51 +2,91 @@ ## Introduction -The _Data Curator App_ is an R Shiny app that serves as the _frontend_ to the schematic Python package. It allows data contributors to easily annotate, validate and submit their metadata. +The _Data Curator App_ is an R Shiny app that serves as the _frontend_ to the [schematic Python package](github.com/sage-Bionetworks/schematic/). It allows data contributors to easily annotate, validate and submit their metadata. ---- -## Get Started and Installation +## Quickstart {#quickstart} -Follow the steps below to make sure the _Data Curator App_ is fully setup to work with the [schematic]: +Sage Bionetworks hosts a version of Data Curator App for its collaborators. [Access it here](link TBD). +To configure your project for this version, edit [dcc_config.csv](dcc_config.csv) and submit a pull request. +[dcc_config.csv](dcc_config.csv) contains the following. **Bold fields** are required: -### Data Curator App Setup +- **project_name**: The display name of your project +- **synapse_asset_view**: The synapse ID of your project's asset view +- **data_model_url**: A URL to your data model. Must be the **raw** file if using GitHub +- **template_menu_config_file**: www/template_config/_config.json. This file can be generated by hand or with [config_schema.py](.github/config_schema.py) +- **manifest_output_format**: "excel" +- **submit_use_schema_labels**: Schematic option to use schema labels when submitting (default TRUE) TRUE or FALSE +- **submit_table_manipulation**: Schematic option when submitting (default "replace") "replace" or "upsert" +- **use_compliance_dashboard**: (default FALSE) TRUE or FALSE +- primary_col: (default Sage theme) hexadecimal color code +- secondary_col; (default Sage theme) hexadecimal color code +- sidebar_col: (default Sage theme) hexadecimal color code + +Your pull request should include: +- The modifications to [dcc_config.csv](dcc_config.csv) +- A dropdown template config [www/template_config/_config.json](www/template_config/config.json) +- Optional: A .png or .svg logo for your project in `www/img` -1. Clone this repo (front-end) with one single branch (i.e., _main_): +Other things you will need: +- A Synapse asset view for you project +- A [data model](#datamodel) - git clone --single-branch --branch main https://github.com/Sage-Bionetworks/data_curator.git +## Setup a local instance of DCA -2. Create and modify the configuration file ([How to obtain OAuth Credential](https://github.com/Sage-Bionetworks/data_curator#Authentication)): +### 1. Clone this repo and install required R packages. - cp example_oauth_config.yml oauth_config.yml - chmod 400 oauth_config.yml +``` +git clone https://github.com/Sage-Bionetworks/data_curator.git +cd data_curator +R -e "renv::restore()" +``` + +### 2. Set up [schematic](github.com/sage-Bionetworks/schematic/) -3. Create and activate a virtual environment within which you can install the package: +DCA can use Schematic through [reticulate](https://rstudio.github.io/reticulate/) or a REST API. - python -m venv .venv - source .venv/bin/activate +Using Schematic with reticulate requires python 3.9 or greater. Create a python virtual environment named `.venv` and install schematicpy through [pypi](https://pypi.org/project/schematicpy/) or from [GitHub](github.com/sage-Bionetworks/schematic/) using [poetry](https://python-poetry.org/docs/). Follow the links to Schematic for more details on installation. -4. Install required R pacakges dependencies: +``` +# python virtual env must be named .venv +python3 -m venv .venv - R -f install-pkgs.R +# For pypi release of schematic, run this line +pip3 install schematicpy -### Schematic Setup +# Or for development schematic, run the following. Note you'll need to install poetry. +git clone https://github.com/Sage-Bionetworks/schematic.git +cd schematic +poetry shell +poetry install -1. Clone the [schematic] (backend) as a folder `schematic` inside the `data_curator` folder: +# At this point you can also run the REST API service locally +# This will be accessible at http://0.0.0.0:3001 +poetry run python3 run_api.py +``` - git clone --single-branch --branch develop https://github.com/Sage-Bionetworks/schematic.git +To use Schematic through its REST API, run the service locally using the commands above. Or access [Schematic hosted by Sage Bionetwork](link TBD). -2. Install the latest release of the `schematic` via `pip`. IF NOT USING CONDA, install the devel version below: +### 3. Configure App - python -m pip install schematicpy +Many app and schematic configurations are set in `dcc_config.yml` as described in [Quickstart](#quickstart). The following are stored as environment variables. Add these to `.Renviron`. - For development and test with the latest update from `schematic`, install the `schematic` via [poetry]: +Schematic configurations +``` +DCA_SCHEMATIC_API_TYPE: "rest", "reticulate", or "offline" +DCA_API_HOST: "" (blank string) if not using the REST API, otherwise URL to schematic service +DCA_API_PORT: "" (blank string) if not using the REST API **LOCALLY**, otherwise the port. Usually 3001. +``` - cd schematic - poetry build - pip install dist/schematicpy-1.0.0-py3-none-any.whl +OAuth-related variables +``` +DCA_CLIENT_ID: OAuth client ID +DCA_CLIENT_SECRET: OAuth client secret +DCA_APP_URL: OAuth redirect URL +``` -3. Modify the `schematic_config.yml` to set up schematic configuration. To do so, follow the instructions on the [schematic's documentation](https://sage-schematic.readthedocs.io/en/develop/index.html#package-installation-and-setup) +--- ### Data Model Configuration @@ -58,29 +98,27 @@ For local testing, run below snippet to generate `www/config.json` and check the 2. Clone your data model repo, i.e: - git clone https://github.com/Sage-Bionetworks/data-models +``` +git clone https://github.com/Sage-Bionetworks/data-models +``` 3. Create `config.json` and placed it in the `www` folder - python3 .github/generate_config_json.py \ - -jd data-models/example.model.jsonld \ - -schema 'Sage-Bionetworks/data-models' \ - -service Sage-Bionetworks/schematic' - ---- +``` +python3 .github/generate_config_json.py \ + -jd data-models/example.model.jsonld \ + -schema 'Sage-Bionetworks/data-models' \ + -service Sage-Bionetworks/schematic' +``` ## Authentication -This utilizes a Synapse Authentication (OAuth) client (code motivated by [ShinyOAuthExample](https://github.com/brucehoff/ShinyOAuthExample) and [app.R](https://gist.github.com/jcheng5/44bd750764713b5a1df7d9daf5538aea). Each application is required to have its own OAuth client as these clients cannot be shared between one another. View instructions [here](https://docs.synapse.org/articles/using_synapse_as_an_oauth_server.html) to learn how to request a client. Once you obtain the client, make sure to add it to the configuration yaml file: - -- `CLIENT_ID` and `CLIENT_SECRET` -- `APP_URL`: the redirection url to your app +This utilizes a Synapse Authentication (OAuth) client (code motivated by [ShinyOAuthExample](https://github.com/brucehoff/ShinyOAuthExample) and [app.R](https://gist.github.com/jcheng5/44bd750764713b5a1df7d9daf5538aea). Each application is required to have its own OAuth client as these clients cannot be shared between one another. View instructions [here](https://help.synapse.org/docs/Using-Synapse-as-an-OAuth-Server.2048327904.html) to learn how to request a client. Once you obtain the client, make sure to add the corresponding [environment variables](#configureapp) ---- ## Deployment -To deploy the app to shinyapps.io, please follow the instructions in the [shinyapps_deploy.md](./shinyapps_deploy.md). +To deploy the app to shinyapps.io, please follow the instructions in the [shinyapps_deploy.md](docs/shinyapps_deploy.md). ## Contributors @@ -89,7 +127,7 @@ Main contributors and developers: - [Rongrong Chai](https://github.com/rrchai) - [Anthony Williams](https://github.com/afwillia) - [Milen Nikolov](https://github.com/milen-sage) -- [Lauren Wolfe](https://github.com/lakikowolfe) +- [Loren Wolfe](https://github.com/lakikowolfe) - [Robert Allaway](https://github.com/allaway) - [Bruno Grande](https://github.com/BrunoGrandePhD) - [Xengie Doan](https://github.com/xdoan) @@ -98,4 +136,4 @@ Main contributors and developers: [schematic]: https://github.com/Sage-Bionetworks/schematic/tree/develop -[poetry]: https://github.com/python-poetry/poetry \ No newline at end of file +[poetry]: https://github.com/python-poetry/poetry diff --git a/dca_startup.sh b/dca_startup.sh new file mode 100755 index 00000000..32baa0d8 --- /dev/null +++ b/dca_startup.sh @@ -0,0 +1,8 @@ +#!/usr/bin/bash + +# Pass environment variable to Shiny +echo "" >> .Renviron +echo R_CONFIG_ACTIVE=$R_CONFIG_ACTIVE >> .Renviron + +# Now run the base start-up script +./startup.sh diff --git a/dcc_config.csv b/dcc_config.csv new file mode 100644 index 00000000..e7187941 --- /dev/null +++ b/dcc_config.csv @@ -0,0 +1,5 @@ +project_name,synapse_asset_view,data_model_url,template_menu_config_file,manifest_output_format,submit_use_schema_labels,submit_table_manipulation,use_compliance_dashboard,primary_col,secondary_col,sidebar_col +DCA Demo,syn33715412,https://raw.githubusercontent.com/Sage-Bionetworks/data-models/main/example.model.jsonld,www/template_config/example_config.json,excel,TRUE,replace,FALSE,#2a668d,#184e71,#191919 +HTAN All Projects,syn20446927,https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld,www/template_config/htan_config.json,excel,TRUE,replace,FALSE,#605ca8,#5F008C,#191919 +Cancer Complexity Knowledge Portal - Database,syn27210848,https://raw.githubusercontent.com/mc2-center/data-models/main/mc2.model.jsonld,www/template_config/mc2_config.json,excel,FALSE,upsert,FALSE,#407BA0,#5BB0B5,#191919 +INCLUDE Data Management Core,syn30109515,https://raw.githubusercontent.com/include-dcc/include-linkml/schematic-updates/src/schematic/include_schematic_linkml.jsonld,www/template_config/include_config.json,excel,TRUE,replace,FALSE,#2a668d,#184e71,#191919 diff --git a/shinyapps_deploy.md b/docs/shinyapps_deploy.md similarity index 61% rename from shinyapps_deploy.md rename to docs/shinyapps_deploy.md index 24f0042b..4e863237 100644 --- a/shinyapps_deploy.md +++ b/docs/shinyapps_deploy.md @@ -2,7 +2,7 @@ Anthony Williams - 02-23-2022 +Updated 15-03-2023 Deploying an app to shinyapps.io is done with the [rsconnect package](https://github.com/rstudio/rsconnect/). This can be done manually or automated in a [GitHub action](.github/workflows/shinyapps_deploy.yml). @@ -11,7 +11,8 @@ The GitHub action installs data curator's system and python dependencies on an U the entire directory to shinyapps.io using `rsconnect`. ## Setup Configuration of OAuth and Schematic -- See ["Data Curator App Setup #2"](https://github.com/Sage-Bionetworks/data_curator#data-curator-app-setup) to set up `oauth_config.yml` schematic. +- Follow [these instructions](https://help.synapse.org/docs/Using-Synapse-as-an-OAuth-Server.2048327904.html) to set up an OAuth client for your application. +- For schematic-related credentials, follow [these instructions](https://github.com/Sage-Bionetworks/schematic/blob/develop/docs/md/details.md). You will need to generate `service_account_creds.json` - See ["Schematic Setup #3"](https://github.com/Sage-Bionetworks/data_curator#schematic-setup) to set up `schematic_config.yml` ## Access shinyapps.io @@ -26,12 +27,9 @@ for the repo. *OAUTH_CLIENT_SECRET # your OAuth client secret *SCHEMATIC_SYNAPSE_CONFIG # content of .synapseConfig *SCHEMATIC_SERVICE_ACCT_CREDS # content of schematic_service_account_creds.json -*SCHEMATIC_CREDS_PATH # content of credentials.json -*SCHEMATIC_TOKEN_PICKLE # content of token.pickle RSCONNECT_USER # see the shinyapps.io record in LastPass RSCONNECT_TOKEN # see the shinyapps.io record in LastPass RSCONNECT_SECRET # see the shinyapps.io record in LastPass -REPO_PAT # GitHub personal access token ``` To automatically add credentials with * to GH secrets via [GitHub CLI](https://cli.github.com/manual/index) (Note: you still need to manually add shinyapps.io's credentials and GH PAT): @@ -46,12 +44,18 @@ Rscript set_gh_secrets.R oauth_config.yml schematic_config.yml ## Configure the [GitHub workflow file](.github/workflows/shinyapps_deploy.yml) ### Specify branches to push to which instances -There are two instances of data curator on shinyapps.io, "production" and "staging". By default, the action is configured to deploy the app to the "production" instance if there are pushes on tags named as semantic versions (e.g. `v1.0.0`). While pushing changes to branches named "main" or "develop" will auto-deploy app to the staging instance. +There are three instances of data curator on shinyapps.io, "production" and "staging", and "testing". By default, the action is configured to deploy the app to the "production" instance tagged commits with semantic versions (e.g. `v1.0.0`) are pushed. While pushing changes to branches named "main" or "develop*" will auto-deploy app to the staging and testing instances, respectively. ### Install data curator dependencies Currently, data curator requires several system dependencies to run on shinyapps.io in addition to schematic's python dependencies. libcurl4-openssl-dev -Set up a python virtual environment, install the `develop` version of schematic with `poetry`, and install necessary R packages as instructed in the [README](https://github.com/Sage-Bionetworks/data_curator/blob/main/README.md). +### Determine which schematic API to use +By default, DCA will use schematic via reticulate by setting the R env var `DCA_SCHEMATIC_API_TYPE="reticulate"`. You can set this to "rest" if you want to use Schematic's REST API. In thise case, also set `DCA_API_HOST` to the REST API URL. This is useful if you want the latest features of Schematic, because at the moment shinyapps.io cannot use any release or development version of Schematic after 23.1.1. + +### Python set up for reticulate API +Note, currently shinyapps.io cannot use the development releases of Schematic. You can only install schematic via `pip install schematicpy=-23.1.1` + +Outdated: Set up a python virtual environment, install the `develop` version of schematic with `poetry`, and install necessary R packages as instructed in the [README](https://github.com/Sage-Bionetworks/data_curator/blob/main/README.md). ### Update schematic configurations Use GitHub secrets to write out the credentials files for OAuth and schematic, which are required for the configuration of data curator. diff --git a/functions/dashboardFuns.R b/functions/dashboardFuns.R index 480021f7..0aeca5dd 100644 --- a/functions/dashboardFuns.R +++ b/functions/dashboardFuns.R @@ -4,19 +4,28 @@ #' @param datasets a list of folder syn Ids, named by folder names #' @param ncores number of cpu to run parallelization #' @return data frame that contains manifest essential information for dashboard -get_dataset_metadata <- function(syn.store, datasets, ncores = 1) { +get_dataset_metadata <- function(syn.store, datasets, ncores = 1, schematic_api="reticulate", + access_token, fileview) { # TODO: if the component could be retrieve directly from storage object: # remove codes to download all manifests # get data for all manifests within the specified datasets - file_view <- syn.store$storageFileviewTable %>% - filter(name == "synapse_storage_manifest.csv" & parentId %in% datasets) + file_view <- switch(schematic_api, + reticulate = syn.store$storageFileviewTable, + rest = get_asset_view_table(url = file.path(api_uri, "v1/storage/assets/tables"), + input_token = access_token, + asset_view=fileview) + ) %>% + filter(grepl("synapse_storage_manifest_", name) & parentId %in% datasets) # datasets don't have a manifest ds_no_manifest <- datasets[which(!datasets %in% file_view$parentId)] manifest_info <- list() modified_user <- list() + manifest_dfs <- list() # return empty data frame if no manifest or no component in the manifest + metadata <- data.frame() + # create with column names to prevent dplyr funcs from failing on empty df cols <- c( "SynapseID", @@ -38,34 +47,80 @@ get_dataset_metadata <- function(syn.store, datasets, ncores = 1) { if (length(manifest_ids) > 0) { # in case, multiple manifests exist in the same dataset for (id in manifest_ids) { - info <- syn$get(id) - manifest_info <<- append(manifest_info, info) - user <- syn$getUserProfile(info["properties"]["modifiedBy"])["userName"] - modified_user <<- append(modified_user, user) + if (schematic_api == "reticulate"){ + info <- syn$get(id) + manifest_info <<- append(manifest_info, info) + user <- syn$getUserProfile(info["properties"]["modifiedBy"])["userName"] + modified_user <<- append(modified_user, user) + } else if (schematic_api == "rest"){ + info <- synapse_get(id = id, auth = access_token) + manifest <- manifest_download( + url = file.path(api_uri, "v1/manifest/download"), + input_token = access_token, + asset_view = fileview, + dataset_id = info$parentId, + as_json = TRUE + ) + + # refactor this not to write files but save in a object + #tmp_man <- tempfile() + info$Path <- NA_character_ + #write_csv(manifest, tmp_man) + manifest_dfs[[id]] <<- manifest + manifest_info <<- append(manifest_info, list(unlist(info))) + user <- synapse_user_profile(auth=access_token)[["userName"]] + modified_user <<- append(modified_user, user) + } + } } }) if (length(manifest_info) > 0) { metadata <- parallel::mclapply(seq_along(manifest_info), function(i) { - info <- manifest_info[[i]] - # extract manifest essential information for dashboard - manifest_path <- info["path"] - manifest_df <- data.table::fread(manifest_path) - # keep invalid component values as 'Missing' - manifest_component <- ifelse("Component" %in% colnames(manifest_df) & nrow(manifest_df) > 0, - manifest_df$Component[1], "Unknown" - ) - metadata <- data.frame( - SynapseID = info["properties"]["id"], - Component = manifest_component, - CreatedOn = as.Date(info["properties"]["createdOn"]), - ModifiedOn = as.Date(info["properties"]["modifiedOn"]), - ModifiedUser = paste0("@", modified_user[[i]]), - Path = manifest_path, - Folder = names(datasets)[which(datasets == info["properties"]["parentId"])], - FolderSynId = info["properties"]["parentId"] - ) + if (schematic_api == "reticulate"){ + info <- manifest_info[[i]] + # extract manifest essential information for dashboard + manifest_path <- info["path"] + manifest_df <- data.table::fread(manifest_path) + # keep invalid component values as 'Missing' + manifest_component <- ifelse("Component" %in% colnames(manifest_df) & nrow(manifest_df) > 0, + manifest_df$Component[1], "Unknown" + ) + metadata <- data.frame( + SynapseID = info["properties"]["id"], + Component = manifest_component, + CreatedOn = as.Date(info["properties"]["createdOn"]), + ModifiedOn = as.Date(info["properties"]["modifiedOn"]), + ModifiedUser = paste0("@", modified_user[[i]]), + Path = manifest_path, + Folder = names(datasets)[which(datasets == info["properties"]["parentId"])], + FolderSynId = info["properties"]["parentId"] + ) + } else if (schematic_api == "rest"){ + info <- manifest_info[[i]] + # extract manifest essential information for dashboard + manifest_path <- info["Path"] + # See above - don't read from file, read from object + #manifest_df <- data.table::fread(manifest_path) + manifest_df <- manifest_dfs[[i]] + # keep all manifests used for validation, even if it has invalid component value + # if manifest doesn't have "Component" column, or empty, return NA for component + manifest_component <- ifelse("Component" %in% colnames(manifest_df) & nrow(manifest_df) > 0, + manifest_df$Component[1], NA_character_ + ) + metadata <- tibble( + SynapseID = info["id"], + Component = manifest_component, + CreatedOn = as.Date(info["createdOn"]), + ModifiedOn = as.Date(info["modifiedOn"]), + ModifiedUser = paste0("@", modified_user[[i]]), + Path = manifest_path, + Folder = names(datasets)[which(datasets == info["parentId"])], + FolderSynId = info["parentId"], + manifest = manifest_df + ) + } }, mc.cores = ncores) %>% bind_rows() } @@ -87,14 +142,13 @@ get_dataset_metadata <- function(syn.store, datasets, ncores = 1) { #' @param metadata output from \code{get_dataset_metadata}. #' @param project.scope list of project ids used for cross-manifest validation #' @return data frame contains required data types for tree plot -validate_metadata <- function(metadata, project.scope) { +validate_metadata <- function(metadata, project.scope, schematic_api, schema_url) { stopifnot(is.list(project.scope)) - if (nrow(metadata) == 0) { return(metadata) } - parallel::mclapply(1:nrow(metadata), function(i) { + lapply(1:nrow(metadata), function(i) { manifest <- metadata[i, ] if (is.na(manifest$Component)) { data.frame( @@ -111,19 +165,17 @@ validate_metadata <- function(metadata, project.scope) { WarnMsg = "'Component' is missing" ) } else { - validation_res <- tryCatch( - metadata_model$validateModelManifest( + validation_res <- switch(schematic_api, + reticulate = manifest_validate_py( manifestPath = manifest$Path, rootNode = manifest$Component, restrict_rules = TRUE, # set true to disable great expectation project_scope = project.scope ), - # for invalid components, it will return NULL and relay as 'Out of Date', e.g.: - # "LungCancerTier3", "BreastCancerTier3", "ScRNA-seqAssay", "MolecularTest", "NaN", "" ... - error = function(e) { - warning("'validateModelManifest' failed: ", sQuote(manifest$SynapseID), ":\n", e$message) - return(NULL) - } + rest = manifest_validate(url=file.path(api_uri, "v1/model/validate"), + data_type=manifest$Component, + schema_url = schema_url, + json_str = jsonlite::toJSON(manifest$manifest)) ) # clean validation res from schematicpy clean_res <- validationResult(validation_res, manifest$Component, dashboard = TRUE) @@ -132,11 +184,11 @@ validate_metadata <- function(metadata, project.scope) { Result = clean_res$result, # change wrong schema to out-of-date type ErrorType = if_else(clean_res$error_type == "Wrong Schema", "Out of Date", clean_res$error_type), - errorMsg = if_else(is.null(clean_res$error_msg[1]), "Valid", clean_res$error_msg[1]), - WarnMsg = if_else(length(clean_res$warning_msg) == 0, "Valid", clean_res$warning_msg) + errorMsg = if_else(is.null(clean_res$error_msg[1]), "Valid", paste(clean_res$error_msg[1], collapse="; ")), + WarnMsg = if_else(is.null(clean_res$warning_msg[1]), "Valid", paste(clean_res$warning_msg[1], collapse = "; ")) ) } - }, mc.cores = ncores) %>% + }) %>% bind_rows() %>% cbind(metadata, .) # expand metadata with validation results } @@ -145,9 +197,15 @@ validate_metadata <- function(metadata, project.scope) { #' #' @param schema data type of selected data type or template. #' @return list of requirements for \code{schema} or string of \code{schema} if no requirements found -get_schema_nodes <- function(schema) { +get_schema_nodes <- function(schema, schematic_api, url, schema_url) { requirement <- tryCatch( - metadata_model$get_component_requirements(schema, as_graph = TRUE), + switch(schematic_api, + reticulate = get_component_requirements_py(schema, as_graph = TRUE), + rest = model_component_requirements( + url=url, + schema_url=schema_url, + source_component = schema, + as_graph = TRUE)), error = function(e) { warning("'get_schema_nodes' failed: ", sQuote(schema), ":\n", e$message) return(list()) @@ -168,7 +226,8 @@ get_schema_nodes <- function(schema) { #' #' @param metadata output from \code{get_dataset_metadata}. #' @return data frame of nodes contains source and target used for tree plot -get_metadata_nodes <- function(metadata, ncores = 1) { +get_metadata_nodes <- function(metadata, ncores = 1, schematic_api, + schema_url, url) { if (nrow(metadata) == 0) { return(data.frame(from = NA, to = NA, folder = NA, folderSynId = NA, nMiss = NA)) } else { @@ -176,7 +235,14 @@ get_metadata_nodes <- function(metadata, ncores = 1) { manifest <- metadata[i, ] # get all required data types nodes <- tryCatch( - metadata_model$get_component_requirements(manifest$Component, as_graph = TRUE), + switch(schematic_api, + reticulate = get_component_requirements_py(manifest$Component, as_graph = TRUE), + rest = model_component_requirements( + url=url, + schema_url=schema_url, + source_component = manifest$Component, + as_graph = TRUE) + ), error = function(e) { warning("'get_metadata_nodes' failed: ", sQuote(manifest$Component), ":\n", e$message) return(list()) @@ -199,4 +265,5 @@ get_metadata_nodes <- function(metadata, ncores = 1) { ) }, mc.cores = ncores) %>% bind_rows() } + } diff --git a/functions/dcWaiter.R b/functions/dcWaiter.R index 06e73964..61cc3d65 100644 --- a/functions/dcWaiter.R +++ b/functions/dcWaiter.R @@ -1,10 +1,16 @@ +# Convert hex color with no alpha to hex with alpha +col2rgba <- function(x, alpha=255) { + y <- col2rgb(x) + rgb(y[1], y[2], y[3], alpha = alpha, maxColorValue = 255) +} + # This is script to wrap up the waiter screen for data curator app dcWaiter <- function(stage = c("show", "update", "hide"), id = NULL, landing = FALSE, userName = NULL, isLogin = TRUE, isCertified = TRUE, isPermission = TRUE, sleep = 2, msg = NULL, style = NULL, spin = NULL, custom_spinner = FALSE, url = "", - color = "rgba(66, 72, 116, .9)") { + color = col2rgba("#424874", 255*0.9)) { # validate arguments if (!is.logical(landing)) stop("landing must be a boolean") if (!is.logical(isLogin)) stop("isLogin must be a boolean") @@ -26,15 +32,15 @@ dcWaiter <- function(stage = c("show", "update", "hide"), if (stage == "show") { waiter_show_on_load( html = tagList( - img(src = "img/loading.gif"), - h4("Retrieving Synapse information...") + img(src = "img/Logo_Sage_Logomark.png"), + h4("Logging into Data Curator App") ), - color = "#424874" + color = col2rgba("#2a668d", 255*0.9) ) } else if (!isCertified) { # when user is not certified synapse user waiter_update(html = tagList( - img(src = "img/synapse_logo.png", height = "120px"), + img(src = "img/Logo_Sage_Logomark.png", height = "120px"), h3("Looks like you're not a synapse certified user!"), span( "Please follow the ", @@ -48,7 +54,7 @@ dcWaiter <- function(stage = c("show", "update", "hide"), } else if (!isPermission) { # when user is not certified synapse user waiter_update(html = tagList( - img(src = "img/synapse_logo.png", height = "120px"), + img(src = "img/Logo_Sage_Logomark.png", height = "120px"), h3("Fileview/Project Access Denied!"), span("You may not have sufficient permissions for curation. Please contact your team and project administrators.") @@ -56,7 +62,7 @@ dcWaiter <- function(stage = c("show", "update", "hide"), } else { # success loading page; userName needed to provide waiter_update(html = tagList( - img(src = "img/synapse_logo.png", height = "120px"), + img(src = "img/Logo_Sage_Logomark.png", height = "120px"), h3(sprintf("Welcome, %s!", userName)) )) Sys.sleep(sleep) @@ -84,7 +90,7 @@ dcWaiter <- function(stage = c("show", "update", "hide"), waiter_show( id = id, html = spinner, - color = color + color = col2rgba(color, 255*0.9) ) } else { Sys.sleep(2) # wait at least 2s to update diff --git a/functions/schematic_rest_api.R b/functions/schematic_rest_api.R new file mode 100644 index 00000000..f204449d --- /dev/null +++ b/functions/schematic_rest_api.R @@ -0,0 +1,290 @@ +#' @description Download an existing manifest +#' @param url URI of API endpoint +#' @param input_token Synapse PAT +#' @param asset_view ID of view listing all project data assets +#' @param dataset_id the parent ID of the manifest +#' @param as_json if True return the manifest in JSON format +#' @returns a csv of the manifest +#' @export +manifest_download <- function(url="http://localhost:3001/v1/manifest/download", + input_token, asset_view, dataset_id, as_json=TRUE){ + req <- httr::GET(url, + query = list( + asset_view = asset_view, + dataset_id = dataset_id, + as_json = as_json, + input_token = input_token + )) + manifest <- httr::content(req, as = "text") + jsonlite::fromJSON(manifest) +} + +#' schematic rest api to generate manifest +#' +#' @param title Name of dataset +#' @param data_type Type of dataset +#' @param oauth true or false STRING passed to python +#' @param use_annotations true or false STRING passed to python +#' @param dataset_id Synapse ID of existing manifest +#' +#' @returns a URL to a google sheet +#' @export +manifest_generate <- function(url="http://localhost:3001/v1/manifest/generate", + schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #nolint + title, data_type, oauth="true", + use_annotations="false", dataset_id=NULL, + asset_view, output_format, input_token = NULL) { + + req <- httr::GET(url, + query = list( + schema_url=schema_url, + title=title, + data_type=data_type, + oauth=oauth, + use_annotations=use_annotations, + dataset_id=dataset_id, + asset_view=asset_view, + output_format=output_format, + input_token = input_token + )) + + manifest_url <- httr::content(req) + manifest_url +} + +#' Populate a manifest sheet +#' +#' @param url URL to schematic API endpoint +#' @param schema_url URL to a schema jsonld +#' @param data_type Type of dataset +#' @param title Title of csv +#' @param csv_file Filepath of csv to validate +#' @export +manifest_populate <- function(url="http://localhost:3001/v1/manifest/populate", + schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #notlint + data_type, title, return_excel=FALSE, csv_file) { + + req <- httr::POST(url, + query=list( + schema_url=schema_url, + data_type=data_type, + title=title, + return_excel=return_excel), + body=list(csv_file=httr::upload_file(csv_file, type = "text/csv")) + ) + req + +} + + +#' schematic rest api to validate metadata +#' +#' @param url URL to schematic API endpoint +#' @param schema_url URL to a schema jsonld +#' @param data_type Type of dataset +#' @param csv_file Filepath of csv to validate +#' +#' @returns An empty list() if sucessfully validated. Or a list of errors. +#' @export +manifest_validate <- function(url="http://localhost:3001/v1/model/validate", + schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #nolint + data_type, json_str=NULL, file_name) { + req <- httr::POST(url, + query=list( + schema_url=schema_url, + data_type=data_type, + json_str=json_str), + body=list(file_name=httr::upload_file(file_name)) + ) + + # Format server error in a way validationResult can handle + if (httr::http_status(req)$category == "Server error") { + return( + list( + list( + "errors" = list( + Row = NA, Column = NA, Value = NA, + Error = sprintf("Cannot validate manifest: %s", + httr::http_status(req)$message) + ) + ) + ) + ) + } + annotation_status <- httr::content(req) + annotation_status +} + + +#' schematic rest api to submit metadata +#' +#' @param url URL to schematic API endpoint +#' @param schema_url URL to a schema jsonld +#' @param data_type Type of dataset +#' @param dataset_id Synapse ID of existing manifest +#' @param input_token Synapse login cookie, PAT, or API key. +#' @param csv_file Filepath of csv to validate +#' +#' @returns TRUE if successful upload or validate errors if not. +#' @export +model_submit <- function(url="http://localhost:3001/v1/model/submit", + schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #notlint + data_type, dataset_id, restrict_rules=FALSE, input_token, json_str=NULL, asset_view, + use_schema_label=TRUE, manifest_record_type="table", file_name, + table_manipulation="replace") { + req <- httr::POST(url, + #add_headers(Authorization=paste0("Bearer ", pat)), + query=list( + schema_url=schema_url, + data_type=data_type, + dataset_id=dataset_id, + input_token=input_token, + restrict_rules=restrict_rules, + json_str=json_str, + asset_view=asset_view, + use_schema_label=use_schema_label, + manifest_record_type=manifest_record_type, + table_manipulation=table_manipulation), + body=list(file_name=httr::upload_file(file_name)) + #body=list(file_name=file_name) + ) + + if (tolower(httr::http_status(req)$category) != "success") { + stop(sprintf("Error submitting manifest: %s", httr::http_status(req)$reason)) + } + manifest_id <- httr::content(req) + manifest_id +} + +#' Given a source model component (see https://w3id.org/biolink/vocab/category for definnition of component), return all components required by it. +#' +#' @param schema_url Data Model URL +#' @param source_component an attribute label indicating the source component. (i.e. Patient, Biospecimen, ScRNA-seqLevel1, ScRNA-seqLevel2) +#' @param as_graph if False return component requirements as a list; if True return component requirements as a dependency graph (i.e. a DAG) +#' +#' @returns A list of required components associated with the source component. +#' @export +model_component_requirements <- function(url="http://localhost:3001/v1/model/component-requirements", + schema_url, source_component, + as_graph = FALSE) { + + req <- httr::GET(url, + query = list( + schema_url = schema_url, + source_component = source_component, + as_graph = as_graph + )) + + if (httr::http_error(req)) stop(httr::http_status(req)$reason) + cont <- httr::content(req) + + if (inherits(cont, "xml_document")){ + err_msg <- xml2::xml_text(xml2::xml_child(cont, "head/title")) + stop(sprintf("%s", err_msg)) + } + + cont + +} + + +#' Gets all datasets in folder under a given storage project that the current user has access to. +#' +#' @param url URL to schematic API endpoint +#' @param syn_master_file_view synapse ID of master file view. +#' @param syn_master_file_name Synapse storage manifest file name. +#' @param project_id synapse ID of a storage project. +#' @param input_token synapse PAT +#' +#'@export +storage_project_datasets <- function(url="http://localhost:3001/v1/storage/project/datasets", + asset_view, + project_id, + input_token) { + + req <- httr::GET(url, + #add_headers(Authorization=paste0("Bearer ", pat)), + query=list( + asset_view=asset_view, + project_id=project_id, + input_token=input_token) + ) + + httr::content(req) +} + +#' Get all storage projects the current user has access to +#' +#' @param url URL to schematic API endpoint +#' @param syn_master_file_view synapse ID of master file view. +#' @param syn_master_file_name Synapse storage manifest file name. +#' @param input_token synapse PAT +#' +#' @export +storage_projects <- function(url="http://localhost:3001/v1/storage/projects", + asset_view, + input_token) { + + req <- httr::GET(url, + query = list( + asset_view=asset_view, + input_token=input_token + )) + + httr::content(req) +} + +#' /storage/dataset/files +#' +#' @param url URL to schematic API endpoint +#' @param syn_master_file_view synapse ID of master file view. +#' @param syn_master_file_name Synapse storage manifest file name. +#' @param dataset_id synapse ID of a storage dataset. +#' @param file_names a list of files with particular names (i.e. Sample_A.txt). If you leave it empty, it will return all dataset files under the dataset ID. +#' @param full_path Boolean. If True return the full path as part of this filename; otherwise return just base filename +#' @param input_token synapse PAT +#' +#' @export +storage_dataset_files <- function(url="http://localhost:3001/v1/storage/dataset/files", + asset_view, + dataset_id, file_names=list(), + full_path=FALSE, input_token) { + + req <- httr::GET(url, + #add_headers(Authorization=paste0("Bearer ", pat)), + query=list( + asset_view=asset_view, + dataset_id=dataset_id, + file_names=file_names, + full_path=full_path, + input_token=input_token)) + httr::content(req) + +} + +#' /storage/asset/table +#' +#' @param url URL to schematic API endpoint +#' @param input_token synapse PAT +#' @param asset_view Synapse ID of asset view +#' @export +get_asset_view_table <- function(url="http://localhost:3001/v1/storage/assets/tables", + input_token, asset_view, return_type="json") { + + req <- httr::GET(url, + query=list( + asset_view=asset_view, + input_token=input_token, + return_type=return_type)) + + if (httr::http_status(req)$category == "Success") { + if (return_type=="json") { + return(list2DF(fromJSON(httr::content(req)))) + } else { + csv <- readr::read_csv(httr::content(req)) + return(csv) + } + } else stop("File could not be downloaded from Synapse.") + +} + diff --git a/functions/schematic_reticulate.R b/functions/schematic_reticulate.R new file mode 100644 index 00000000..a2659693 --- /dev/null +++ b/functions/schematic_reticulate.R @@ -0,0 +1,105 @@ +setup_synapse_driver <- function(){ + # old way + # library(reticulate) + # use_virtualenv(file.path(getwd(), ".venv"), required = TRUE) + # syn <- import("synapseclient")$Synapse() + # source_python("functions/metadataModel.py") # schemaGenerator object + # synapse_driver <- import("schematic.store.synapse")$SynapseStorage + + # new way + reticulate::use_virtualenv(file.path(getwd(), ".venv"), required = TRUE) + syn <<- reticulate::import("synapseclient")$Synapse() + + MetadataModel <<- reticulate::import("schematic.models.metadata")$MetadataModel + CONFIG <<- reticulate::import("schematic")$CONFIG + SchemaGenerator <<- reticulate::import("schematic.schemas.generator")$SchemaGenerator + + config = CONFIG$load_config("schematic_config.yml") + + inputMModelLocation = config$model$input$location + inputMModelLocationType = config$model$input$file_type + + manifest_title = config$manifest$title + manifest_data_type = config$manifest$data_type[1] + + metadata_model <<- MetadataModel(inputMModelLocation, inputMModelLocationType) + + # create schema generator object for associateMetadataWithFiles + schema_generator <<- SchemaGenerator(inputMModelLocation) + + synapse_driver <<- reticulate::import("schematic.store.synapse")$SynapseStorage + +} + +storage_projects_py <- function(synapse_driver, access_token) { + + tryCatch( + { + # get syn storage + syn_store <<- synapse_driver(access_token = access_token) + # get user's common projects + syn_store$getStorageProjects() + }, + error = function(e) { + message(e$message) + return(NULL) + } + ) +} + +storage_projects_datasets_py <- function(synapse_driver, project_id) { + syn_store$getStorageDatasetsInProject(project_id) #%>% list2Vector() +} + +storage_dataset_files_py <- function(project_id) { + file_list <- syn_store$getFilesInStorageDataset(project_id) +} + +manifest_generate_py <- function(title, rootNode, filenames=NULL, datasetId){ + metadata_model$getModelManifest( + title = title, + rootNode = rootNode, + filenames = filenames, + datasetId = datasetId + ) +} + +manifest_validate_py <- function(manifestPath, rootNode, restrict_rules=TRUE, project_scope){ + tryCatch( + metadata_model$validateModelManifest( + manifestPath = manifestPath, + rootNode = rootNode, + restrict_rules = TRUE, # set true to disable great expectation + project_scope = project_scope + ), + error = function(e) { + message("'validateModelManifest' failed:\n", e$message) + return(NULL) + } + ) +} + +manifest_populate_py <- function(title, manifestPath, rootNode) { + metadata_model$populateModelManifest( + title = title, + manifestPath = manifestPath, + rootNode = rootNode + ) +} + +model_submit_py <- function(SchemaGenerator, metadataManifestPath, datasetId, manifest_record_type="table", restrict_manifest=FALSE) { + syn_store$associateMetadataWithFiles( + schemaGenerator = SchemaGenerator, + metadataManifestPath = metadataManifestPath, + datasetId = datasetId, + manifest_record_type = manifest_record_type, + restrict_manifest = restrict_manifest + ) +} + +synapse_user_profile_py <- function() syn$getUserProfile()$userName + +get_component_requirements_py <- function(schema, as_graph) { + metadata_model$get_component_requirements(schema, as_graph = as_graph) +} + diff --git a/functions/synapse_rest_api.R b/functions/synapse_rest_api.R new file mode 100644 index 00000000..09b6773f --- /dev/null +++ b/functions/synapse_rest_api.R @@ -0,0 +1,101 @@ +#' @title Get Synapse user profile info +#' @details +#' For information on authenticating synapse REST API queries +#' https://docs.synapse.org/rest/#org.sagebionetworks.auth.controller.AuthenticationController +#' Authentication to Synapse services requires an access token passed in the +#' HTTP Authorization header, as per the HTTP bearer authorization standard. +#' https://datatracker.ietf.org/doc/html/rfc6750#section-2.1 +#' https://docs.synapse.org/rest/#org.sagebionetworks.repo.web.controller.UserProfileController +#' PAT is the synapse personal access token OR the browser cookie from a logged-in session. +#' +#' @param url Synapse REST API url for userProfile +#' @param auth Synapse PAT or authorization token +#' +#' @export +synapse_user_profile <- function( + url="https://repo-prod.prod.sagebase.org/repo/v1/userProfile", auth=NULL) { + req <- httr::GET(url, httr::add_headers(Authorization=paste0("Bearer ", auth))) + httr::content(req) +} + +#' @title Is a Synapse user certified? +#' @details +#' https://rest-docs.synapse.org/rest/GET/user/id/certifiedUserPassingRecord.html +#' Based on python client https://github.com/Sage-Bionetworks/synapsePythonClient/blob/3da21b9e2542c7da8cdab9925926737fe2162a54/synapseclient/client.py#L606 +#' +#' @param url URL to Synapse REST API for certification +#' @param endpoint The Synapse API endpoint +#' @param auth Synapse PAT or authorization token +#' +#' @export +synapse_is_certified <- function(url="https://repo-prod.prod.sagebase.org/repo/v1/user", + endpoint="certifiedUserPassingRecord", + auth=NULL) { + + # Get user profile and ownerId + user_profile <- synapse_user_profile(auth=auth) + if (!"ownerId" %in% names(user_profile)) return(FALSE) + ownerid <- user_profile[["ownerId"]] + url_req <- file.path(url, ownerid, endpoint) + req <- httr::GET(url_req) + httr::content(req)[["passed"]] + +} + + +#content(GET(sprintf("https://repo-prod.prod.sagebase.org/repo/v1/user/%s/certifiedUserPassingRecord", "3438856"))) +#https://repo-prod.prod.sagebase.org/repo/v1/user + +#' @title GET Synapse Entity +#' @description Wrapper for https://rest-docs.synapse.org/rest/GET/entity/id.html +#' +#' @param url URL of synapse REST API GET table entity endpoint +#' @param id ID of synapse table +#' @param auth Synapse PAT +#' +#' @export +synapse_get <- function(url = "https://repo-prod.prod.sagebase.org/repo/v1/entity/", + id, auth) { + + if (is.null(id)) stop("id cannot be NULL") + req_url <- file.path(url, id) + req <- httr::GET(req_url, + httr::add_headers(Authorization=paste0("Bearer ", auth))) + + # Send error if unsuccessful query + status <- httr::http_status(req) + if (status$category != "Success") stop(status$message) + + cont <- httr::content(req) + dplyr::bind_rows(cont) + +} + + +#' @title Check Access Permissions to a Synapse Entity +#' @description wrapper for https://rest-docs.synapse.org/rest/GET/entity/id/access.html +#' +#' @param url URL to REST API endpoint +#' @param id Synapse ID +#' @param access Access Type to check +#' @param auth Synapse authentication token +#' +#' @export +synapse_access <- function(url = "https://repo-prod.prod.sagebase.org/repo/v1/entity", + id, access, auth) { + + if (is.null(id)) stop("id cannot be NULL") + req_url <- file.path(url, id, "access") + req <- httr::GET(req_url, + httr::add_headers(Authorization=paste0("Bearer ", auth)), + query = list(accessType=access)) + + # Send error if unsuccessful query + status <- httr::http_status(req) + if (status$category != "Success") stop(status$message) + + cont <- httr::content(req) + cont$result + +} + diff --git a/functions/utils.R b/functions/utils.R index ec40860c..38177864 100644 --- a/functions/utils.R +++ b/functions/utils.R @@ -36,3 +36,41 @@ addTooltip <- function(.data, message, position = c("top")) { tagAppendAttributes(`aria-label` = message) %>% tagAppendAttributes(class = tooltip_class) } + +# parse environment variables for configuration +parse_env_var <- function(x, el_delim=",", kv_delim=":"){ + if (!grepl(kv_delim, x)) stop(sprintf("%s delimiter not in %s", kv_delim, x)) + # assume string of key-value pairs + elements <- stringr::str_split(x, el_delim, simplify = TRUE) + unlist(lapply(elements, function(y){ + kv <- stringr::str_split(y, kv_delim, n=2) + setNames(kv[[1]][[2]], kv[[1]][[1]]) + })) +} + +# Map logo information for each synapse ID. +update_logo <- function(project = "sage") { + + img <- switch(project, + syn20446927 = list(href = "https://humantumoratlas.org/", + img_src = "img/HTAN_text_logo.png"), + syn27210848 = list(href = "https://cancercomplexity.synapse.org/", + img_src = "img/cckp_logo.png"), + syn30109515 = list(href = "https://https://includedcc.org/", + img_src = "img/INCLUDE DCC Logo-01.png"), + list(href = "https://synapse.org", + img_src = "img/Logo_Sage_Logomark.png") + ) + + tags$li( + class = "dropdown", id = "logo", + tags$a( + href = img$href, + target = "_blank", + tags$img( + height = "40px", alt = "LOGO", + src = img$img_src + ) + ) + ) +} \ No newline at end of file diff --git a/functions/validationResult.R b/functions/validationResult.R index 7db26368..d1c133b2 100644 --- a/functions/validationResult.R +++ b/functions/validationResult.R @@ -27,10 +27,10 @@ validationResult <- function(anno.res, template, manifest = NULL, dashboard = FA error_help_msg = "Please contact DCC staff for assistance." )) } - errors <- anno.res[[1]] - warns <- anno.res[[2]] - + warns <- list() + if (length(anno.res) > 1) warns <- anno.res[[2]] + # format the errors if (length(errors) != 0) { # mismatched template index @@ -47,14 +47,14 @@ validationResult <- function(anno.res, template, manifest = NULL, dashboard = FA x[[2]] ) })) - + if (length(inx_mt) > 0) { # mismatched error(s): selected template mismatched with validating template error_type <- "Mismatched Template" # get all mismatched components error_values <- sapply(errors[inx_mt], function(x) x[[4]][[1]]) %>% unique() - + # error messages for mismatch mismatch_c <- error_values %>% sQuote() %>% @@ -88,7 +88,7 @@ validationResult <- function(anno.res, template, manifest = NULL, dashboard = FA lapply(unique(error_table$Column), function(col) { highlight_values[[col]] <<- error_table$Value[error_table$Column == col] }) - + # concatenated similar errors into one error message # TODO: remove below code chunck if the backend error messages are standardized error_table <- error_table %>% @@ -101,7 +101,7 @@ validationResult <- function(anno.res, template, manifest = NULL, dashboard = FA ) %>% ungroup() %>% mutate(new_error = paste0("", Value, " from row(s) ", Row, " in column '", Column, "': ", Error)) - + # sort rows based on input column names error_table <- error_table[order(match(error_table$Column, colnames(manifest))), ] error_msg <- error_table$new_error @@ -111,7 +111,7 @@ validationResult <- function(anno.res, template, manifest = NULL, dashboard = FA result <- "valid" error_type <- "No Error" } - + # format the warnings if (length(warns) != 0) { # add warning values to highlight @@ -130,10 +130,10 @@ validationResult <- function(anno.res, template, manifest = NULL, dashboard = FA highlight_values[[warn[[2]]]] <<- append(highlight_values[[warn[[2]]]], warn_values) warning_msg <<- append(warning_msg, warn[[3]]) }) - + warning_help_msg <- "View all the warning(s) highlighted in the preview table above" } - + return(list( result = result, error_msg = error_msg, diff --git a/global.R b/global.R index 0831ad30..17d4982d 100644 --- a/global.R +++ b/global.R @@ -24,16 +24,42 @@ suppressPackageStartupMessages({ library(r2d3) }) +# import R files +source_files <- list.files(c("functions", "modules"), pattern = "*\\.R$", recursive = TRUE, full.names = TRUE) +sapply(source_files, FUN = source) + +dcc_config <- read_csv("dcc_config.csv") + ## Set Up OAuth -oauth_client <- yaml.load_file("oauth_config.yml") +client_id <- Sys.getenv("DCA_CLIENT_ID") +client_secret <- Sys.getenv("DCA_CLIENT_SECRET") +app_url <- Sys.getenv("DCA_APP_URL") + +if (is.null(client_id) || nchar(client_id) == 0) stop("missing DCA_CLIENT_ID environmental variable") +if (is.null(client_secret) || nchar(client_secret) == 0) stop("missing DCA_CLIENT_SECRET environmental variable") +if (is.null(app_url) || nchar(app_url) == 0) stop("missing DCA_APP_URL environmental variable") -client_id <- toString(oauth_client$CLIENT_ID) -client_secret <- toString(oauth_client$CLIENT_SECRET) -app_url <- toString(oauth_client$APP_URL) +schematic_config <- yaml.load_file("schematic_config.yml") +manifest_basename <- schematic_config$synapse$manifest_basename -if (is.null(client_id) || nchar(client_id) == 0) stop("oauth_config.yml is missing CLIENT_ID") -if (is.null(client_secret) || nchar(client_secret) == 0) stop("oauth_config.yml is missing CLIENT_SECRET") -if (is.null(app_url) || nchar(app_url) == 0) stop("oauth_config.yml is missing APP_URL") +dca_schematic_api <- Sys.getenv("DCA_SCHEMATIC_API_TYPE") +if (!dca_schematic_api %in% c("rest", "reticulate", "offline")) { + stop(sprintf("DCA_SCHEMATIC_API_TYPE environment variable must be one of: %s", c("rest", "reticulate", "offline"))) +} +if (dca_schematic_api == "rest") { + api_uri <- ifelse(Sys.getenv("DCA_API_PORT") == "", + Sys.getenv("DCA_API_HOST"), + paste(Sys.getenv("DCA_API_HOST"), + Sys.getenv("DCA_API_PORT"), + sep = ":") + ) +} + +syn_themes <- c( + "syn20446927" = "www/dca_themes/htan_theme_config.rds", + "syn27210848" = "www/dca_themes/mc2_theme_config.rds", + "syn30109515" = "www/dca_themes/include_theme_config.rds" + ) # update port if running app locally if (interactive()) { @@ -85,40 +111,36 @@ api <- oauth_endpoint( # The 'openid' scope is required by the protocol for retrieving user information. scope <- "openid view download modify" +template_config_files <- setNames(dcc_config$template_menu_config_file, + dcc_config$synapse_asset_view) + ## Set Up Virtual Environment # ShinyAppys has a limit of 7000 files which this app' grossly exceeds # due to its Python dependencies. To get around the limit we zip up # the virtual environment before deployment and unzip it here. - # unzip virtual environment, named as ".venv.zip" -if (!file.exists(".venv")) utils::unzip(".venv.zip") - -# We get a '126' error (non-executable) if we don't do this: -system("chmod -R +x .venv") - -# Don't necessarily have to set `RETICULATE_PYTHON` env variable -Sys.unsetenv("RETICULATE_PYTHON") -reticulate::use_virtualenv(file.path(getwd(), ".venv"), required = TRUE) - -## Import functions/modules -# import synapse client -syn <- import("synapseclient")$Synapse() -# import schematic modules -source_python("functions/metadataModel.py") -# import R files -source_files <- list.files(c("functions", "modules"), pattern = "*\\.R$", recursive = TRUE, full.names = TRUE) %>% - .[!grepl("dashboard", .)] -sapply(source_files, FUN = source) - -## Read config.json -if (!file.exists("www/config.json")) { - system( - "python3 .github/config_schema.py -c schematic_config.yml --service_repo 'Sage-Bionetworks/schematic' --overwrite" - ) +if (dca_schematic_api == "reticulate"){ + if (!file.exists(".venv")) utils::unzip(".venv.zip") + + # We get a '126' error (non-executable) if we don't do this: + system("chmod -R +x .venv") + # Don't necessarily have to set `RETICULATE_PYTHON` env variable + Sys.unsetenv("RETICULATE_PYTHON") + #setup_synapse_driver() + + ## Read config.json + if (!file.exists("www/config.json")) { +# system( +# "python3 .github/config_schema.py -c schematic_config.yml --service_repo 'Sage-Bionetworks/schematic' --overwrite" +# ) + } } -config_file <- fromJSON("www/config.json") +config_file <- fromJSON("www/template_config/config.json") + ## Global variables -dropdown_types <- c("project", "folder", "datatype") +dropdown_types <- c("project", "folder", "template") # set up cores used for parallelization ncores <- parallel::detectCores() - 1 +datatypes <- c("project", "folder", "template") +options(sass.cache = FALSE) diff --git a/inst/testdata/HTAN-Biospecimen-Tier-1-2-fail.csv b/inst/testdata/HTAN-Biospecimen-Tier-1-2-fail.csv new file mode 100644 index 00000000..daecc4cc --- /dev/null +++ b/inst/testdata/HTAN-Biospecimen-Tier-1-2-fail.csv @@ -0,0 +1,2 @@ +Sample ID,Patient ID,Tissue Status,Component,Storage Method,Fixative Type +123,456,Healthy,Biospecimen,Unknown,Unknown \ No newline at end of file diff --git a/inst/testdata/HTAN-Biospecimen-Tier-1-2-pass.csv b/inst/testdata/HTAN-Biospecimen-Tier-1-2-pass.csv new file mode 100644 index 00000000..33570b4c --- /dev/null +++ b/inst/testdata/HTAN-Biospecimen-Tier-1-2-pass.csv @@ -0,0 +1,2 @@ +Component,HTAN Biospecimen ID,HTAN Parent ID,Timepoint Label,Collection Days from Index,Adjacent Biospecimen IDs,Biospecimen Type,Acquisition Method Type,Fixative Type,Site of Resection or Biopsy,Storage Method,Processing Days from Index,Protocol Link,Site Data Source,Collection Media,Mounting Medium,Processing Location,Histology Assessment By,Histology Assessment Medium,Preinvasive Morphology,Tumor Infiltrating Lymphocytes,Degree of Dysplasia,Dysplasia Fraction,Number Proliferating Cells,Percent Eosinophil Infiltration,Percent Granulocyte Infiltration,Percent Inflam Infiltration,Percent Lymphocyte Infiltration,Percent Monocyte Infiltration,Percent Necrosis,Percent Neutrophil Infiltration,Percent Normal Cells,Percent Stromal Cells,Percent Tumor Cells,Percent Tumor Nuclei,Fiducial Marker,Slicing Method,Lysis Buffer,Method of Nucleic Acid Isolation,Acquisition Method Other Specify,Analyte Type,Biospecimen Dimension 1,Biospecimen Dimension 2,Biospecimen Dimension 3,Dimensions Unit,Fixation Duration,Histologic Morphology Code,Ischemic Temperature,Ischemic Time,Portion Weight,Preservation Method,Section Number in Sequence,Section Thickness Value,Sectioning Days from Index,Shipping Condition Type,Slide Charge Type,Specimen Laterality,Total Volume,Total Volume Unit,Tumor Tissue Type,entityId +Biospecimen,123,456,a,b,,Cells Biospecimen Type,Not specified,None,Brain stem,Not Applicable,1,d,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,syn27428515 \ No newline at end of file diff --git a/install-pkgs.R b/install-pkgs.R deleted file mode 100644 index 373ee15b..00000000 --- a/install-pkgs.R +++ /dev/null @@ -1,49 +0,0 @@ - -cran <- c( - "ellipsis==0.3.2", - "shiny==1.7.1", - "fontawesome==0.3.0", - "httr==1.4.2", - "yaml==2.2.1", - "shinyjs==2.1.0", - "dplyr==1.0.7", - "shinythemes==1.2.0", - "shinydashboard==0.7.2", - "stringr==1.4.0", - "DT==0.20", - "jsonlite==1.7.3", - "reticulate==1.23", - # "shinydashboardPlus==2.0.3", - "waiter==0.2.5", - "readr==2.1.1", - "sass==0.4.1", - "remotes==2.4.2", - "rsconnect==0.8.25", - "png==0.1.7", - "tidyr==1.1.4", - "data.table==1.14.2", - "igraph==1.2.11", - "networkD3==0.4", - "data.tree==1.0.0", - "r2d3==0.2.6" -) -gh <- c( - "dreamRs/shinypop", - # switch back to use cran install 'shinydashboardPlus' - # once they make a release to fix icons - # https://github.com/RinteRface/shinydashboardPlus - "RinteRface/shinydashboardPlus" -) - -# The binary package distributions from R Studio dramatically speed up installation time -# For Ubuntu 18.04 (Bionic) it's https://packagemanager.rstudio.com/all/__linux__/bionic/latest -# For Ubuntu 20.04 (Focal) it's https://packagemanager.rstudio.com/all/__linux__/focal/latest -options(repos = c(REPO_NAME = "https://packagemanager.rstudio.com/all/__linux__/focal/latest", getOption("repos"))) - -install.packages("remotes") -invisible( - lapply(strsplit(cran, "=="), function(cran_pkg) { - remotes::install_version(cran_pkg[1], version = cran_pkg[2]) - }) -) -remotes::install_github(gh) diff --git a/man/get_asset_view_table.Rd b/man/get_asset_view_table.Rd new file mode 100644 index 00000000..2abe1d0f --- /dev/null +++ b/man/get_asset_view_table.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/schematic_rest_api.R +\name{get_asset_view_table} +\alias{get_asset_view_table} +\title{/storage/asset/table} +\usage{ +get_asset_view_table( + url = "http://localhost:3001/v1/storage/assets/tables", + input_token, + asset_view, + return_type = "json" +) +} +\arguments{ +\item{url}{URL to schematic API endpoint} + +\item{input_token}{synapse PAT} + +\item{asset_view}{Synapse ID of asset view} +} +\description{ +/storage/asset/table +} diff --git a/man/manifest_generate.Rd b/man/manifest_generate.Rd new file mode 100644 index 00000000..4af1fc37 --- /dev/null +++ b/man/manifest_generate.Rd @@ -0,0 +1,36 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/schematic_rest_api.R +\name{manifest_generate} +\alias{manifest_generate} +\title{schematic rest api to generate manifest} +\usage{ +manifest_generate( + url = "http://localhost:3001/v1/manifest/generate", + schema_url = + "https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", + title, + data_type, + oauth = "true", + use_annotations = "false", + dataset_id = NULL, + asset_view, + output_format +) +} +\arguments{ +\item{title}{Name of dataset} + +\item{data_type}{Type of dataset} + +\item{oauth}{true or false STRING passed to python} + +\item{use_annotations}{true or false STRING passed to python} + +\item{dataset_id}{Synapse ID of existing manifest} +} +\value{ +a URL to a google sheet +} +\description{ +schematic rest api to generate manifest +} diff --git a/man/manifest_populate.Rd b/man/manifest_populate.Rd new file mode 100644 index 00000000..59bfe157 --- /dev/null +++ b/man/manifest_populate.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/schematic_rest_api.R +\name{manifest_populate} +\alias{manifest_populate} +\title{Populate a manifest sheet} +\usage{ +manifest_populate( + url = "http://localhost:3001/v1/manifest/populate", + schema_url = + "https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", + data_type, + title, + csv_file +) +} +\arguments{ +\item{url}{URL to schematic API endpoint} + +\item{schema_url}{URL to a schema jsonld} + +\item{data_type}{Type of dataset} + +\item{title}{Title of csv} + +\item{csv_file}{Filepath of csv to validate} +} +\description{ +Populate a manifest sheet +} diff --git a/man/manifest_validate.Rd b/man/manifest_validate.Rd new file mode 100644 index 00000000..482b6e7d --- /dev/null +++ b/man/manifest_validate.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/schematic_rest_api.R +\name{manifest_validate} +\alias{manifest_validate} +\title{schematic rest api to validate metadata} +\usage{ +manifest_validate( + url = "http://localhost:3001/v1/model/validate", + schema_url = + "https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", + data_type, + json_str +) +} +\arguments{ +\item{url}{URL to schematic API endpoint} + +\item{schema_url}{URL to a schema jsonld} + +\item{data_type}{Type of dataset} + +\item{csv_file}{Filepath of csv to validate} +} +\value{ +An empty list() if sucessfully validated. Or a list of errors. +} +\description{ +schematic rest api to validate metadata +} diff --git a/man/model_component_requirements.Rd b/man/model_component_requirements.Rd new file mode 100644 index 00000000..6d66d91b --- /dev/null +++ b/man/model_component_requirements.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/schematic_rest_api.R +\name{model_component_requirements} +\alias{model_component_requirements} +\title{Given a source model component (see https://w3id.org/biolink/vocab/category for definnition of component), return all components required by it.} +\usage{ +model_component_requirements( + url = "http://localhost:3001/v1/model/component-requirements", + schema_url, + source_component, + as_graph = FALSE +) +} +\arguments{ +\item{schema_url}{Data Model URL} + +\item{source_component}{an attribute label indicating the source component. (i.e. Patient, Biospecimen, ScRNA-seqLevel1, ScRNA-seqLevel2)} + +\item{as_graph}{if False return component requirements as a list; if True return component requirements as a dependency graph (i.e. a DAG)} +} +\value{ +A list of required components associated with the source component. +} +\description{ +Given a source model component (see https://w3id.org/biolink/vocab/category for definnition of component), return all components required by it. +} diff --git a/man/model_submit.Rd b/man/model_submit.Rd new file mode 100644 index 00000000..b049e6cf --- /dev/null +++ b/man/model_submit.Rd @@ -0,0 +1,37 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/schematic_rest_api.R +\name{model_submit} +\alias{model_submit} +\title{schematic rest api to submit metadata} +\usage{ +model_submit( + url = "http://localhost:3001/v1/model/submit", + schema_url = + "https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", + data_type, + dataset_id, + restrict_rules = FALSE, + input_token, + json_str, + asset_view +) +} +\arguments{ +\item{url}{URL to schematic API endpoint} + +\item{schema_url}{URL to a schema jsonld} + +\item{data_type}{Type of dataset} + +\item{dataset_id}{Synapse ID of existing manifest} + +\item{input_token}{Synapse login cookie, PAT, or API key.} + +\item{csv_file}{Filepath of csv to validate} +} +\value{ +TRUE if successful upload or validate errors if not. +} +\description{ +schematic rest api to submit metadata +} diff --git a/man/storage_dataset_files.Rd b/man/storage_dataset_files.Rd new file mode 100644 index 00000000..c09e5d46 --- /dev/null +++ b/man/storage_dataset_files.Rd @@ -0,0 +1,33 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/schematic_rest_api.R +\name{storage_dataset_files} +\alias{storage_dataset_files} +\title{/storage/dataset/files} +\usage{ +storage_dataset_files( + url = "http://localhost:3001/v1/storage/dataset/files", + asset_view, + dataset_id, + file_names = list(), + full_path = FALSE, + input_token +) +} +\arguments{ +\item{url}{URL to schematic API endpoint} + +\item{dataset_id}{synapse ID of a storage dataset.} + +\item{file_names}{a list of files with particular names (i.e. Sample_A.txt). If you leave it empty, it will return all dataset files under the dataset ID.} + +\item{full_path}{Boolean. If True return the full path as part of this filename; otherwise return just base filename} + +\item{input_token}{synapse PAT} + +\item{syn_master_file_view}{synapse ID of master file view.} + +\item{syn_master_file_name}{Synapse storage manifest file name.} +} +\description{ +/storage/dataset/files +} diff --git a/man/storage_project_datasets.Rd b/man/storage_project_datasets.Rd new file mode 100644 index 00000000..1093cbc9 --- /dev/null +++ b/man/storage_project_datasets.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/schematic_rest_api.R +\name{storage_project_datasets} +\alias{storage_project_datasets} +\title{Gets all datasets in folder under a given storage project that the current user has access to.} +\usage{ +storage_project_datasets( + url = "http://localhost:3001/v1/storage/project/datasets", + asset_view, + project_id, + input_token +) +} +\arguments{ +\item{url}{URL to schematic API endpoint} + +\item{project_id}{synapse ID of a storage project.} + +\item{input_token}{synapse PAT} + +\item{syn_master_file_view}{synapse ID of master file view.} + +\item{syn_master_file_name}{Synapse storage manifest file name.} +} +\description{ +Gets all datasets in folder under a given storage project that the current user has access to. +} diff --git a/man/storage_projects.Rd b/man/storage_projects.Rd new file mode 100644 index 00000000..f10d1f8f --- /dev/null +++ b/man/storage_projects.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/schematic_rest_api.R +\name{storage_projects} +\alias{storage_projects} +\title{Get all storage projects the current user has access to} +\usage{ +storage_projects( + url = "http://localhost:3001/v1/storage/projects", + asset_view, + input_token +) +} +\arguments{ +\item{url}{URL to schematic API endpoint} + +\item{input_token}{synapse PAT} + +\item{syn_master_file_view}{synapse ID of master file view.} + +\item{syn_master_file_name}{Synapse storage manifest file name.} +} +\description{ +Get all storage projects the current user has access to +} diff --git a/man/synapse_access.Rd b/man/synapse_access.Rd new file mode 100644 index 00000000..aff9bea5 --- /dev/null +++ b/man/synapse_access.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/synapse_rest_api.R +\name{synapse_access} +\alias{synapse_access} +\title{Check Access Permissions to a Synapse Entity} +\usage{ +synapse_access( + url = "https://repo-prod.prod.sagebase.org/repo/v1/entity", + id, + access, + auth +) +} +\arguments{ +\item{url}{URL to REST API endpoint} + +\item{id}{Synapse ID} + +\item{access}{Access Type to check} + +\item{auth}{Synapse authentication token} +} +\description{ +wrapper for https://rest-docs.synapse.org/rest/GET/entity/id/access.html +} diff --git a/man/synapse_get.Rd b/man/synapse_get.Rd new file mode 100644 index 00000000..c621bfb8 --- /dev/null +++ b/man/synapse_get.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/synapse_rest_api.R +\name{synapse_get} +\alias{synapse_get} +\title{GET Synapse Entity} +\usage{ +synapse_get( + url = "https://repo-prod.prod.sagebase.org/repo/v1/entity/", + id, + auth +) +} +\arguments{ +\item{url}{URL of synapse REST API GET table entity endpoint} + +\item{id}{ID of synapse table} + +\item{auth}{Synapse PAT} +} +\description{ +Wrapper for https://rest-docs.synapse.org/rest/GET/entity/id.html +} diff --git a/man/synapse_is_certified.Rd b/man/synapse_is_certified.Rd new file mode 100644 index 00000000..a6087864 --- /dev/null +++ b/man/synapse_is_certified.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/synapse_rest_api.R +\name{synapse_is_certified} +\alias{synapse_is_certified} +\title{Is a Synapse user certified?} +\usage{ +synapse_is_certified( + url = "https://repo-prod.prod.sagebase.org/repo/v1/user", + endpoint = "certifiedUserPassingRecord", + auth = NULL +) +} +\arguments{ +\item{url}{URL to Synapse REST API for certification} + +\item{endpoint}{The Synapse API endpoint} + +\item{auth}{Synapse PAT or authorization token} +} +\description{ +Is a Synapse user certified? +} +\details{ +https://rest-docs.synapse.org/rest/GET/user/id/certifiedUserPassingRecord.html +Based on python client https://github.com/Sage-Bionetworks/synapsePythonClient/blob/3da21b9e2542c7da8cdab9925926737fe2162a54/synapseclient/client.py#L606 +} diff --git a/man/synapse_user_profile.Rd b/man/synapse_user_profile.Rd new file mode 100644 index 00000000..b3d5a40c --- /dev/null +++ b/man/synapse_user_profile.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/synapse_rest_api.R +\name{synapse_user_profile} +\alias{synapse_user_profile} +\title{Get Synapse user profile info} +\usage{ +synapse_user_profile( + url = "https://repo-prod.prod.sagebase.org/repo/v1/userProfile", + auth = NULL +) +} +\arguments{ +\item{url}{Synapse REST API url for userProfile} + +\item{auth}{Synapse PAT or authorization token} +} +\description{ +Get Synapse user profile info +} +\details{ +For information on authenticating synapse REST API queries +https://docs.synapse.org/rest/#org.sagebionetworks.auth.controller.AuthenticationController +Authentication to Synapse services requires an access token passed in the +HTTP Authorization header, as per the HTTP bearer authorization standard. +https://datatracker.ietf.org/doc/html/rfc6750#section-2.1 +https://docs.synapse.org/rest/#org.sagebionetworks.repo.web.controller.UserProfileController +PAT is the synapse personal access token OR the browser cookie from a logged-in session. +} diff --git a/modules/DTable.R b/modules/DTable.R index 78c72cd4..4237679d 100644 --- a/modules/DTable.R +++ b/modules/DTable.R @@ -45,6 +45,7 @@ DTableServer <- function(id, data, escape = TRUE, # but the column names of the data are , Component, ... if (! col %in% names(data)) next() values <- highlightValues[[col]] + if (length(values) == 0) next() # if NULL is provided for values, it will highlight entire columns if ("ht_entire_column" %in% values) style <- "yellow" else style <- styleEqual(values, rep("yellow", length(values))) df <- df %>% formatStyle(col, backgroundColor = style) diff --git a/modules/dashboard/dashboard.R b/modules/dashboard/dashboard.R index b16d92cc..b59f9184 100644 --- a/modules/dashboard/dashboard.R +++ b/modules/dashboard/dashboard.R @@ -52,7 +52,9 @@ dashboardUI <- function(id) { #' @param disable_ids selector ids to be disable during the process of dashboard #' @param ncores number of cpu to run parallelization #' -dashboard <- function(id, syn.store, project.scope, schema, schema.display.name, disable.ids = NULL, ncores = 1) { +dashboard <- function(id, syn.store, project.scope, schema, schema.display.name, + disable.ids = NULL, ncores = 1, access_token, fileview, + folder, schematic_api="reticulate", schema_url) { moduleServer( id, function(input, output, session) { @@ -75,7 +77,6 @@ dashboard <- function(id, syn.store, project.scope, schema, schema.display.name, hide("toggle-btn-container") shinydashboardPlus::updateBox("box", action = "restore") }) - # retrieving data progress for dashboard should not be executed until dashboard visiable # get all uploaded manifests once the project/folder changed observeEvent(c(project.scope(), input$box$visible), { @@ -84,7 +85,7 @@ dashboard <- function(id, syn.store, project.scope, schema, schema.display.name, dcWaiter( "show", id = ns("tab-container"), url = "www/img/logo.svg", custom_spinner = TRUE, - msg = "Loading, please wait...", style = "color: #000;", color = transparent(1) + msg = "Loading, please wait...", style = "color: #000;", color = transparent(0.95) ) # disable selection to prevent changes until all uploaded manifests are queried @@ -92,17 +93,27 @@ dashboard <- function(id, syn.store, project.scope, schema, schema.display.name, lapply(disable.ids, FUN = disable, asis = TRUE) # get all datasets from selected project - folder_list <- syn.store$getStorageDatasetsInProject(project.scope()) + folder_list <- switch(schematic_api, + "rest" = storage_project_datasets(url=file.path(api_uri, "v1/storage/project/datasets"), + asset_view = fileview, + project_id=folder, + input_token=access_token), + "reticulate" = storage_projects_datasets_py(syn.store, project.scope()) + ) folder_list <- list2Vector(folder_list) # get all uploaded manifests for selected project metadata <- get_dataset_metadata( syn.store = syn.store, datasets = folder_list, - ncores = ncores + ncores = ncores, + schematic_api = schematic_api, + access_token = access_token, + fileview = fileview ) - metadata <- validate_metadata(metadata, project.scope = list(project.scope())) + metadata <- validate_metadata(metadata, project.scope = list(project.scope()), + schematic_api = schematic_api, schema_url=schema_url) # update reactive value uploaded_manifests(metadata) }) @@ -110,7 +121,9 @@ dashboard <- function(id, syn.store, project.scope, schema, schema.display.name, # get requirements for selected data type selected_datatype_requirement <- eventReactive(c(schema(), input$box$visible), { req(input$box$visible) - get_schema_nodes(schema()) + get_schema_nodes(schema(), schematic_api = schematic_api, + url=file.path(api_uri, "v1/model/component-requirements"), + schema_url = schema_url) }) # get requirements for all uploaded manifests @@ -119,7 +132,8 @@ dashboard <- function(id, syn.store, project.scope, schema, schema.display.name, req(uploaded_manifests()) # remove rows with invalid component name metadata <- uploaded_manifests() %>% filter(!is.na(Component), Component != "Unknown") - get_metadata_nodes(metadata, ncores = ncores) + get_metadata_nodes(metadata, ncores = ncores, schematic_api=schematic_api, + schema_url = schema_url, url = file.path(api_uri, "v1/model/component-requirements")) }) # render info/plots for selected datatype @@ -144,7 +158,9 @@ dashboard <- function(id, syn.store, project.scope, schema, schema.display.name, observeEvent(c(uploaded_manifests_requirement(), input$box$visible), { req(input$box$visible) req(uploaded_manifests()) - user_name <- syn$getUserProfile()$userName + user_name <- switch(schematic_api, + reticulate = synapse_user_profile_py(), + rest = synapse_user_profile(auth=access_token)[["userName"]]) # remove rows with invalid component name metadata <- uploaded_manifests() %>% filter(!is.na(Component), Component != "Unknown") selectedProjectTab( diff --git a/modules/dashboard/selectedDatatypeTab/dbCheckList.R b/modules/dashboard/selectedDatatypeTab/dbCheckList.R new file mode 100644 index 00000000..7a5be542 --- /dev/null +++ b/modules/dashboard/selectedDatatypeTab/dbCheckList.R @@ -0,0 +1,82 @@ +dbCheckListUI <- function(id) { + ns <- NS(id) + + uiOutput(ns("checklist")) +} + +dbCheckList <- function(id, metadata, nodes) { + moduleServer( + id, + function(input, output, session) { + ns <- session$ns + + all_req <- union(nodes, names(nodes)) + not_up <- setdiff(all_req, metadata$Component) + up <- intersect(all_req, metadata$Component) + + output$checklist <- renderUI({ + tagList( + helpText( + align = "center", + HTML(paste0("Click ", icon("folder-open"), " to see the dataset(s) containing the data type")) + ), + div( + class = "checklist-container", + div( + class = "checklist-item-container", + if (length(not_up) > 0) { + lapply(not_up, function(name) { + div( + class = "checklist-item", + tagList( + icon("circle", class = "missing"), + span(name) + ) + ) + }) + }, + if (length(up) > 0) { + lapply(up, function(name) { + dups_inx <- which(metadata$Component == name) + div( + class = "checklist-item", + tagList( + icon("circle", class = "completed"), + span(name), + actionButton( + class = "icon-btn", ns(paste0(name, "-dropdown")), + tagList( + # create custom drop down to show all available datasets that have this datatype + icon("folder-open"), + div( + class = "dropdown-list", id = ns(paste0(name, "-dropdown-item")), + lapply(dups_inx, function(i) { + tags$a(metadata$Folder[i], + target = "_blank", + href = paste0("https://www.synapse.org/#!Synapse:", metadata$SynapseID[i]) + ) + }) + ) + ) + ) + ) + ) + }) + } + ) + ) + ) + }) + + # detect folder icon btn changes to show/hide dropdown + lapply(up, function(name) { + dropdown_id <- paste0(name, "-dropdown") + dropdown_item_id <- paste0(name, "-dropdown-item") + observeEvent(input[[dropdown_id]], { + addClass(dropdown_item_id, "open-dropdown") + onevent("mouseleave", dropdown_id, removeClass(dropdown_item_id, "open-dropdown")) + }) + }) + } + ) +} diff --git a/modules/dashboard/shared/setTabTitle.R b/modules/dashboard/shared/setTabTitle.R new file mode 100644 index 00000000..ef557f82 --- /dev/null +++ b/modules/dashboard/shared/setTabTitle.R @@ -0,0 +1,15 @@ +setTabTitleUI <- function(id) { + ns <- NS(id) + uiOutput(ns("dashboard-tab-title")) +} + +setTabTitle <- function(id, title) { + moduleServer( + id, + function(input, output, session) { + output$`dashboard-tab-title` <- renderUI({ + p(title, class = "tab-title") + }) + } + ) +} diff --git a/renv.lock b/renv.lock new file mode 100644 index 00000000..6396bd27 --- /dev/null +++ b/renv.lock @@ -0,0 +1,1412 @@ +{ + "R": { + "Version": "4.1.3", + "Repositories": [ + { + "Name": "CRAN", + "URL": "https://cran.rstudio.com" + }, + { + "Name": "Sage", + "URL": "http://ran.synapse.org" + }, + { + "Name": "Rstudio-binaries", + "URL": "https://packagemanager.rstudio.com/all/__linux__/focal/latest" + } + ] + }, + "Packages": { + "DT": { + "Package": "DT", + "Version": "0.27", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "crosstalk", + "htmltools", + "htmlwidgets", + "jquerylib", + "jsonlite", + "magrittr", + "promises" + ], + "Hash": "3444e6ed78763f9f13aaa39f2481eb34" + }, + "Matrix": { + "Package": "Matrix", + "Version": "1.5-3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "graphics", + "grid", + "lattice", + "methods", + "stats", + "utils" + ], + "Hash": "4006dffe49958d2dd591c17e61e60591" + }, + "R6": { + "Package": "R6", + "Version": "2.5.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "470851b6d5d0ac559e9d01bb352b4021" + }, + "Rcpp": { + "Package": "Rcpp", + "Version": "1.0.10", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "methods", + "utils" + ], + "Hash": "e749cae40fa9ef469b6050959517453c" + }, + "RcppTOML": { + "Package": "RcppTOML", + "Version": "0.2.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "Rcpp" + ], + "Hash": "c232938949fcd8126034419cc529333a" + }, + "askpass": { + "Package": "askpass", + "Version": "1.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "sys" + ], + "Hash": "e8a22846fff485f0be3770c2da758713" + }, + "base64enc": { + "Package": "base64enc", + "Version": "0.1-3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "543776ae6848fde2f48ff3816d0628bc" + }, + "bit": { + "Package": "bit", + "Version": "4.0.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "d242abec29412ce988848d0294b208fd" + }, + "bit64": { + "Package": "bit64", + "Version": "4.0.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "bit", + "methods", + "stats", + "utils" + ], + "Hash": "9fe98599ca456d6552421db0d6772d8f" + }, + "brio": { + "Package": "brio", + "Version": "1.1.3", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "976cf154dfb043c012d87cddd8bca363" + }, + "bslib": { + "Package": "bslib", + "Version": "0.4.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "base64enc", + "cachem", + "grDevices", + "htmltools", + "jquerylib", + "jsonlite", + "memoise", + "mime", + "rlang", + "sass" + ], + "Hash": "a7fbf03946ad741129dc81098722fca1" + }, + "cachem": { + "Package": "cachem", + "Version": "1.0.7", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "fastmap", + "rlang" + ], + "Hash": "cda74447c42f529de601fe4d4050daef" + }, + "callr": { + "Package": "callr", + "Version": "3.7.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "processx", + "utils" + ], + "Hash": "9b2191ede20fa29828139b9900922e51" + }, + "cli": { + "Package": "cli", + "Version": "3.6.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "utils" + ], + "Hash": "3177a5a16c243adc199ba33117bd9657" + }, + "clipr": { + "Package": "clipr", + "Version": "0.8.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "utils" + ], + "Hash": "3f038e5ac7f41d4ac41ce658c85e3042" + }, + "commonmark": { + "Package": "commonmark", + "Version": "1.8.1", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "b6e3e947d1d7ebf3d2bdcea1bde63fe7" + }, + "covr": { + "Package": "covr", + "Version": "3.6.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "crayon", + "digest", + "httr", + "jsonlite", + "methods", + "rex", + "stats", + "utils", + "withr", + "yaml" + ], + "Hash": "a861cee34fbb4b107a73dd414ef56724" + }, + "cpp11": { + "Package": "cpp11", + "Version": "0.4.3", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "ed588261931ee3be2c700d22e94a29ab" + }, + "crayon": { + "Package": "crayon", + "Version": "1.5.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "grDevices", + "methods", + "utils" + ], + "Hash": "e8a1e41acf02548751f45c718d55aa6a" + }, + "crosstalk": { + "Package": "crosstalk", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R6", + "htmltools", + "jsonlite", + "lazyeval" + ], + "Hash": "6aa54f69598c32177e920eb3402e8293" + }, + "curl": { + "Package": "curl", + "Version": "5.0.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "e4f97056611e8e6b8b852d13b7400cf1" + }, + "data.table": { + "Package": "data.table", + "Version": "1.14.8", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "b4c06e554f33344e044ccd7fdca750a9" + }, + "data.tree": { + "Package": "data.tree", + "Version": "1.0.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "methods", + "stringi" + ], + "Hash": "c0ddced80c3f074cb39a7a781698f68a" + }, + "desc": { + "Package": "desc", + "Version": "1.4.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "cli", + "rprojroot", + "utils" + ], + "Hash": "6b9602c7ebbe87101a9c8edb6e8b6d21" + }, + "diffobj": { + "Package": "diffobj", + "Version": "0.3.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "crayon", + "methods", + "stats", + "tools", + "utils" + ], + "Hash": "bcaa8b95f8d7d01a5dedfd959ce88ab8" + }, + "digest": { + "Package": "digest", + "Version": "0.6.31", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "utils" + ], + "Hash": "8b708f296afd9ae69f450f9640be8990" + }, + "dplyr": { + "Package": "dplyr", + "Version": "1.1.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "cli", + "generics", + "glue", + "lifecycle", + "magrittr", + "methods", + "pillar", + "rlang", + "tibble", + "tidyselect", + "utils", + "vctrs" + ], + "Hash": "d3c34618017e7ae252d46d79a1b9ec32" + }, + "ellipsis": { + "Package": "ellipsis", + "Version": "0.3.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "rlang" + ], + "Hash": "bb0eec2fe32e88d9e2836c2f73ea2077" + }, + "evaluate": { + "Package": "evaluate", + "Version": "0.20", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "4b68aa51edd89a0e044a66e75ae3cc6c" + }, + "fansi": { + "Package": "fansi", + "Version": "1.0.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "utils" + ], + "Hash": "1d9e7ad3c8312a192dea7d3db0274fde" + }, + "fastmap": { + "Package": "fastmap", + "Version": "1.1.1", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "f7736a18de97dea803bde0a2daaafb27" + }, + "fontawesome": { + "Package": "fontawesome", + "Version": "0.5.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "htmltools", + "rlang" + ], + "Hash": "e80750aec5717dedc019ad7ee40e4a7c" + }, + "fresh": { + "Package": "fresh", + "Version": "0.2.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "htmltools", + "rstudioapi", + "sass", + "shiny" + ], + "Hash": "fa54367040deb4537da49b7ac0ee5770" + }, + "fs": { + "Package": "fs", + "Version": "1.6.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "f4dcd23b67e33d851d2079f703e8b985" + }, + "generics": { + "Package": "generics", + "Version": "0.1.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "15e9634c0fcd294799e9b2e929ed1b86" + }, + "glue": { + "Package": "glue", + "Version": "1.6.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "4f2596dfb05dac67b9dc558e5c6fba2e" + }, + "here": { + "Package": "here", + "Version": "1.0.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "rprojroot" + ], + "Hash": "24b224366f9c2e7534d2344d10d59211" + }, + "highr": { + "Package": "highr", + "Version": "0.10", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "xfun" + ], + "Hash": "06230136b2d2b9ba5805e1963fa6e890" + }, + "hms": { + "Package": "hms", + "Version": "1.1.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "ellipsis", + "lifecycle", + "methods", + "pkgconfig", + "rlang", + "vctrs" + ], + "Hash": "41100392191e1244b887878b533eea91" + }, + "htmltools": { + "Package": "htmltools", + "Version": "0.5.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "base64enc", + "digest", + "ellipsis", + "fastmap", + "grDevices", + "rlang", + "utils" + ], + "Hash": "9d27e99cc90bd701c0a7a63e5923f9b7" + }, + "htmlwidgets": { + "Package": "htmlwidgets", + "Version": "1.6.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "grDevices", + "htmltools", + "jsonlite", + "knitr", + "rmarkdown", + "yaml" + ], + "Hash": "b677ee5954471eaa974c0d099a343a1a" + }, + "httpuv": { + "Package": "httpuv", + "Version": "1.6.9", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "Rcpp", + "later", + "promises", + "utils" + ], + "Hash": "1046aa31a57eae8b357267a56a0b6d8b" + }, + "httr": { + "Package": "httr", + "Version": "1.4.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "curl", + "jsonlite", + "mime", + "openssl" + ], + "Hash": "f6844033201269bec3ca0097bc6c97b3" + }, + "igraph": { + "Package": "igraph", + "Version": "1.4.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "Matrix", + "grDevices", + "graphics", + "magrittr", + "methods", + "pkgconfig", + "rlang", + "stats", + "utils" + ], + "Hash": "a7ef0d811cb66d8be9da0d7b5ab80ded" + }, + "jquerylib": { + "Package": "jquerylib", + "Version": "0.1.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "htmltools" + ], + "Hash": "5aab57a3bd297eee1c1d862735972182" + }, + "jsonlite": { + "Package": "jsonlite", + "Version": "1.8.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "methods" + ], + "Hash": "a4269a09a9b865579b2635c77e572374" + }, + "knitr": { + "Package": "knitr", + "Version": "1.42", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "evaluate", + "highr", + "methods", + "tools", + "xfun", + "yaml" + ], + "Hash": "8329a9bcc82943c8069104d4be3ee22d" + }, + "later": { + "Package": "later", + "Version": "1.3.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "Rcpp", + "rlang" + ], + "Hash": "7e7b457d7766bc47f2a5f21cc2984f8e" + }, + "lattice": { + "Package": "lattice", + "Version": "0.20-45", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "graphics", + "grid", + "stats", + "utils" + ], + "Hash": "b64cdbb2b340437c4ee047a1f4c4377b" + }, + "lazyeval": { + "Package": "lazyeval", + "Version": "0.2.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "d908914ae53b04d4c0c0fd72ecc35370" + }, + "lifecycle": { + "Package": "lifecycle", + "Version": "1.0.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "glue", + "rlang" + ], + "Hash": "001cecbeac1cff9301bdc3775ee46a86" + }, + "magrittr": { + "Package": "magrittr", + "Version": "2.0.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "7ce2733a9826b3aeb1775d56fd305472" + }, + "memoise": { + "Package": "memoise", + "Version": "2.0.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "cachem", + "rlang" + ], + "Hash": "e2817ccf4a065c5d9d7f2cfbe7c1d78c" + }, + "mime": { + "Package": "mime", + "Version": "0.12", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "tools" + ], + "Hash": "18e9c28c1d3ca1560ce30658b22ce104" + }, + "networkD3": { + "Package": "networkD3", + "Version": "0.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "htmlwidgets", + "igraph", + "magrittr" + ], + "Hash": "38310ec4ddb1398359abdd603c151067" + }, + "openssl": { + "Package": "openssl", + "Version": "2.0.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "askpass" + ], + "Hash": "b04c27110bf367b4daa93f34f3d58e75" + }, + "pillar": { + "Package": "pillar", + "Version": "1.8.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "cli", + "fansi", + "glue", + "lifecycle", + "rlang", + "utf8", + "utils", + "vctrs" + ], + "Hash": "f2316df30902c81729ae9de95ad5a608" + }, + "pkgconfig": { + "Package": "pkgconfig", + "Version": "2.0.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "utils" + ], + "Hash": "01f28d4278f15c76cddbea05899c5d6f" + }, + "pkgload": { + "Package": "pkgload", + "Version": "1.3.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "crayon", + "desc", + "fs", + "glue", + "methods", + "rlang", + "rprojroot", + "utils", + "withr" + ], + "Hash": "6b0c222c5071efe0f3baf3dae9aa40e2" + }, + "png": { + "Package": "png", + "Version": "0.1-8", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "bd54ba8a0a5faded999a7aab6e46b374" + }, + "praise": { + "Package": "praise", + "Version": "1.0.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "a555924add98c99d2f411e37e7d25e9f" + }, + "prettyunits": { + "Package": "prettyunits", + "Version": "1.1.1", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "95ef9167b75dde9d2ccc3c7528393e7e" + }, + "processx": { + "Package": "processx", + "Version": "3.8.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "ps", + "utils" + ], + "Hash": "a33ee2d9bf07564efb888ad98410da84" + }, + "progress": { + "Package": "progress", + "Version": "1.2.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R6", + "crayon", + "hms", + "prettyunits" + ], + "Hash": "14dc9f7a3c91ebb14ec5bb9208a07061" + }, + "promises": { + "Package": "promises", + "Version": "1.2.0.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R6", + "Rcpp", + "later", + "magrittr", + "rlang", + "stats" + ], + "Hash": "4ab2c43adb4d4699cf3690acd378d75d" + }, + "ps": { + "Package": "ps", + "Version": "1.7.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "utils" + ], + "Hash": "68dd03d98a5efd1eb3012436de45ba83" + }, + "purrr": { + "Package": "purrr", + "Version": "1.0.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "lifecycle", + "magrittr", + "rlang", + "vctrs" + ], + "Hash": "d71c815267c640f17ddbf7f16144b4bb" + }, + "r2d3": { + "Package": "r2d3", + "Version": "0.2.6", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "htmltools", + "htmlwidgets", + "jsonlite", + "rstudioapi" + ], + "Hash": "86e5144cf31e5e102b2f75be50176248" + }, + "rappdirs": { + "Package": "rappdirs", + "Version": "0.3.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "5e3c5dc0b071b21fa128676560dbe94d" + }, + "readr": { + "Package": "readr", + "Version": "2.1.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "cli", + "clipr", + "cpp11", + "crayon", + "hms", + "lifecycle", + "methods", + "rlang", + "tibble", + "tzdb", + "utils", + "vroom" + ], + "Hash": "b5047343b3825f37ad9d3b5d89aa1078" + }, + "rematch2": { + "Package": "rematch2", + "Version": "2.1.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "tibble" + ], + "Hash": "76c9e04c712a05848ae7a23d2f170a40" + }, + "renv": { + "Package": "renv", + "Version": "0.17.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "utils" + ], + "Hash": "ce3065fc1a0b64a859f55ac3998d6927" + }, + "reticulate": { + "Package": "reticulate", + "Version": "1.28", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "Matrix", + "R", + "Rcpp", + "RcppTOML", + "graphics", + "here", + "jsonlite", + "methods", + "png", + "rappdirs", + "utils", + "withr" + ], + "Hash": "86c441bf33e1d608db773cb94b848458" + }, + "rex": { + "Package": "rex", + "Version": "1.2.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "lazyeval" + ], + "Hash": "ae34cd56890607370665bee5bd17812f" + }, + "rlang": { + "Package": "rlang", + "Version": "1.0.6", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "utils" + ], + "Hash": "4ed1f8336c8d52c3e750adcdc57228a7" + }, + "rmarkdown": { + "Package": "rmarkdown", + "Version": "2.20", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "bslib", + "evaluate", + "htmltools", + "jquerylib", + "jsonlite", + "knitr", + "methods", + "stringr", + "tinytex", + "tools", + "utils", + "xfun", + "yaml" + ], + "Hash": "716fde5382293cc94a71f68c85b78d19" + }, + "rprojroot": { + "Package": "rprojroot", + "Version": "2.0.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "1de7ab598047a87bba48434ba35d497d" + }, + "rstudioapi": { + "Package": "rstudioapi", + "Version": "0.14", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "690bd2acc42a9166ce34845884459320" + }, + "sass": { + "Package": "sass", + "Version": "0.4.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R6", + "fs", + "htmltools", + "rappdirs", + "rlang" + ], + "Hash": "2bb4371a4c80115518261866eab6ab11" + }, + "shiny": { + "Package": "shiny", + "Version": "1.7.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "bslib", + "cachem", + "commonmark", + "crayon", + "ellipsis", + "fastmap", + "fontawesome", + "glue", + "grDevices", + "htmltools", + "httpuv", + "jsonlite", + "later", + "lifecycle", + "methods", + "mime", + "promises", + "rlang", + "sourcetools", + "tools", + "utils", + "withr", + "xtable" + ], + "Hash": "c2eae3d8c670fa9dfa35a12066f4a1d5" + }, + "shinydashboard": { + "Package": "shinydashboard", + "Version": "0.7.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "htmltools", + "promises", + "shiny", + "utils" + ], + "Hash": "e418b532e9bb4eb22a714b9a9f1acee7" + }, + "shinydashboardPlus": { + "Package": "shinydashboardPlus", + "Version": "2.0.4.9000", + "Source": "GitHub", + "RemoteType": "github", + "RemoteHost": "api.github.com", + "RemoteRepo": "shinydashboardPlus", + "RemoteUsername": "RinteRface", + "RemoteRef": "HEAD", + "RemoteSha": "c8c7b13046d408201ec87016e382fc7b0d8227d5", + "Hash": "c5e57f590bdfd8d91275d984f905476f", + "Requirements": [ + "fresh", + "htmltools", + "lifecycle", + "shiny", + "shinydashboard", + "waiter" + ] + }, + "shinyjs": { + "Package": "shinyjs", + "Version": "2.1.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "digest", + "jsonlite", + "shiny" + ], + "Hash": "802e4786b353a4bb27116957558548d5" + }, + "shinypop": { + "Package": "shinypop", + "Version": "0.1.1", + "Source": "GitHub", + "RemoteType": "github", + "RemoteHost": "api.github.com", + "RemoteUsername": "dreamRs", + "RemoteRepo": "shinypop", + "RemoteRef": "master", + "RemoteSha": "7745a0b1ecf31f783fa59603f3c5cede0c948118", + "Requirements": [ + "R", + "htmltools", + "jsonlite", + "shiny" + ], + "Hash": "ee29555190ce6a726046a5909cc91d9b" + }, + "shinythemes": { + "Package": "shinythemes", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "shiny" + ], + "Hash": "30f0ebc41feba25691073626ff5e2cf4" + }, + "sourcetools": { + "Package": "sourcetools", + "Version": "0.1.7-1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "5f5a7629f956619d519205ec475fe647" + }, + "stringi": { + "Package": "stringi", + "Version": "1.7.12", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "stats", + "tools", + "utils" + ], + "Hash": "ca8bd84263c77310739d2cf64d84d7c9" + }, + "stringr": { + "Package": "stringr", + "Version": "1.5.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "glue", + "lifecycle", + "magrittr", + "rlang", + "stringi", + "vctrs" + ], + "Hash": "671a4d384ae9d32fc47a14e98bfa3dc8" + }, + "sys": { + "Package": "sys", + "Version": "3.4.1", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "34c16f1ef796057bfa06d3f4ff818a5d" + }, + "testthat": { + "Package": "testthat", + "Version": "3.1.6", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "brio", + "callr", + "cli", + "desc", + "digest", + "ellipsis", + "evaluate", + "jsonlite", + "lifecycle", + "magrittr", + "methods", + "pkgload", + "praise", + "processx", + "ps", + "rlang", + "utils", + "waldo", + "withr" + ], + "Hash": "7910146255835c66e9eb272fb215248d" + }, + "tibble": { + "Package": "tibble", + "Version": "3.1.8", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "fansi", + "lifecycle", + "magrittr", + "methods", + "pillar", + "pkgconfig", + "rlang", + "utils", + "vctrs" + ], + "Hash": "56b6934ef0f8c68225949a8672fe1a8f" + }, + "tidyr": { + "Package": "tidyr", + "Version": "1.3.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "cpp11", + "dplyr", + "glue", + "lifecycle", + "magrittr", + "purrr", + "rlang", + "stringr", + "tibble", + "tidyselect", + "utils", + "vctrs" + ], + "Hash": "e47debdc7ce599b070c8e78e8ac0cfcf" + }, + "tidyselect": { + "Package": "tidyselect", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "glue", + "lifecycle", + "rlang", + "vctrs", + "withr" + ], + "Hash": "79540e5fcd9e0435af547d885f184fd5" + }, + "tinytex": { + "Package": "tinytex", + "Version": "0.44", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "xfun" + ], + "Hash": "c0f007e2eeed7722ce13d42b84a22e07" + }, + "tzdb": { + "Package": "tzdb", + "Version": "0.3.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cpp11" + ], + "Hash": "b2e1cbce7c903eaf23ec05c58e59fb5e" + }, + "utf8": { + "Package": "utf8", + "Version": "1.2.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "1fe17157424bb09c48a8b3b550c753bc" + }, + "vctrs": { + "Package": "vctrs", + "Version": "0.5.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "glue", + "lifecycle", + "rlang" + ], + "Hash": "e4ffa94ceed5f124d429a5a5f0f5b378" + }, + "vroom": { + "Package": "vroom", + "Version": "1.6.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "bit64", + "cli", + "cpp11", + "crayon", + "glue", + "hms", + "lifecycle", + "methods", + "progress", + "rlang", + "stats", + "tibble", + "tidyselect", + "tzdb", + "vctrs", + "withr" + ], + "Hash": "7015a74373b83ffaef64023f4a0f5033" + }, + "waiter": { + "Package": "waiter", + "Version": "0.2.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R6", + "htmltools", + "shiny" + ], + "Hash": "93e6b6c8ae3f81d4be77a0dc74e5cf5e" + }, + "waldo": { + "Package": "waldo", + "Version": "0.4.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "cli", + "diffobj", + "fansi", + "glue", + "methods", + "rematch2", + "rlang", + "tibble" + ], + "Hash": "035fba89d0c86e2113120f93301b98ad" + }, + "withr": { + "Package": "withr", + "Version": "2.5.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "graphics", + "stats" + ], + "Hash": "c0e49a9760983e81e55cdd9be92e7182" + }, + "xfun": { + "Package": "xfun", + "Version": "0.37", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "stats", + "tools" + ], + "Hash": "a6860e1400a8fd1ddb6d9b4230cc34ab" + }, + "xml2": { + "Package": "xml2", + "Version": "1.3.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "40682ed6a969ea5abfd351eb67833adc" + }, + "xtable": { + "Package": "xtable", + "Version": "1.8-4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "stats", + "utils" + ], + "Hash": "b8acdf8af494d9ec19ccb2481a9b11c2" + }, + "yaml": { + "Package": "yaml", + "Version": "2.3.7", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "0d0056cc5383fbc240ccd0cb584bf436" + } + } +} + diff --git a/renv/.gitignore b/renv/.gitignore new file mode 100644 index 00000000..22a0d01d --- /dev/null +++ b/renv/.gitignore @@ -0,0 +1,7 @@ +sandbox/ +library/ +local/ +cellar/ +lock/ +python/ +staging/ diff --git a/renv/activate.R b/renv/activate.R new file mode 100644 index 00000000..360dd528 --- /dev/null +++ b/renv/activate.R @@ -0,0 +1,1020 @@ + +local({ + + # the requested version of renv + version <- "0.17.0" + + # the project directory + project <- getwd() + + # figure out whether the autoloader is enabled + enabled <- local({ + + # first, check config option + override <- getOption("renv.config.autoloader.enabled") + if (!is.null(override)) + return(override) + + # next, check environment variables + # TODO: prefer using the configuration one in the future + envvars <- c( + "RENV_CONFIG_AUTOLOADER_ENABLED", + "RENV_AUTOLOADER_ENABLED", + "RENV_ACTIVATE_PROJECT" + ) + + for (envvar in envvars) { + envval <- Sys.getenv(envvar, unset = NA) + if (!is.na(envval)) + return(tolower(envval) %in% c("true", "t", "1")) + } + + # enable by default + TRUE + + }) + + if (!enabled) + return(FALSE) + + # avoid recursion + if (identical(getOption("renv.autoloader.running"), TRUE)) { + warning("ignoring recursive attempt to run renv autoloader") + return(invisible(TRUE)) + } + + # signal that we're loading renv during R startup + options(renv.autoloader.running = TRUE) + on.exit(options(renv.autoloader.running = NULL), add = TRUE) + + # signal that we've consented to use renv + options(renv.consent = TRUE) + + # load the 'utils' package eagerly -- this ensures that renv shims, which + # mask 'utils' packages, will come first on the search path + library(utils, lib.loc = .Library) + + # unload renv if it's already been loaded + if ("renv" %in% loadedNamespaces()) + unloadNamespace("renv") + + # load bootstrap tools + `%||%` <- function(x, y) { + if (is.environment(x) || length(x)) x else y + } + + bootstrap <- function(version, library) { + + # attempt to download renv + tarball <- tryCatch(renv_bootstrap_download(version), error = identity) + if (inherits(tarball, "error")) + stop("failed to download renv ", version) + + # now attempt to install + status <- tryCatch(renv_bootstrap_install(version, tarball, library), error = identity) + if (inherits(status, "error")) + stop("failed to install renv ", version) + + } + + renv_bootstrap_tests_running <- function() { + getOption("renv.tests.running", default = FALSE) + } + + renv_bootstrap_repos <- function() { + + # check for repos override + repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA) + if (!is.na(repos)) + return(repos) + + # check for lockfile repositories + repos <- tryCatch(renv_bootstrap_repos_lockfile(), error = identity) + if (!inherits(repos, "error") && length(repos)) + return(repos) + + # if we're testing, re-use the test repositories + if (renv_bootstrap_tests_running()) { + repos <- getOption("renv.tests.repos") + if (!is.null(repos)) + return(repos) + } + + # retrieve current repos + repos <- getOption("repos") + + # ensure @CRAN@ entries are resolved + repos[repos == "@CRAN@"] <- getOption( + "renv.repos.cran", + "https://cloud.r-project.org" + ) + + # add in renv.bootstrap.repos if set + default <- c(FALLBACK = "https://cloud.r-project.org") + extra <- getOption("renv.bootstrap.repos", default = default) + repos <- c(repos, extra) + + # remove duplicates that might've snuck in + dupes <- duplicated(repos) | duplicated(names(repos)) + repos[!dupes] + + } + + renv_bootstrap_repos_lockfile <- function() { + + lockpath <- Sys.getenv("RENV_PATHS_LOCKFILE", unset = "renv.lock") + if (!file.exists(lockpath)) + return(NULL) + + lockfile <- tryCatch(renv_json_read(lockpath), error = identity) + if (inherits(lockfile, "error")) { + warning(lockfile) + return(NULL) + } + + repos <- lockfile$R$Repositories + if (length(repos) == 0) + return(NULL) + + keys <- vapply(repos, `[[`, "Name", FUN.VALUE = character(1)) + vals <- vapply(repos, `[[`, "URL", FUN.VALUE = character(1)) + names(vals) <- keys + + return(vals) + + } + + renv_bootstrap_download <- function(version) { + + # if the renv version number has 4 components, assume it must + # be retrieved via github + nv <- numeric_version(version) + components <- unclass(nv)[[1]] + + # if this appears to be a development version of 'renv', we'll + # try to restore from github + dev <- length(components) == 4L + + # begin collecting different methods for finding renv + methods <- c( + renv_bootstrap_download_tarball, + if (dev) + renv_bootstrap_download_github + else c( + renv_bootstrap_download_cran_latest, + renv_bootstrap_download_cran_archive + ) + ) + + for (method in methods) { + path <- tryCatch(method(version), error = identity) + if (is.character(path) && file.exists(path)) + return(path) + } + + stop("failed to download renv ", version) + + } + + renv_bootstrap_download_impl <- function(url, destfile) { + + mode <- "wb" + + # https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715 + fixup <- + Sys.info()[["sysname"]] == "Windows" && + substring(url, 1L, 5L) == "file:" + + if (fixup) + mode <- "w+b" + + args <- list( + url = url, + destfile = destfile, + mode = mode, + quiet = TRUE + ) + + if ("headers" %in% names(formals(utils::download.file))) + args$headers <- renv_bootstrap_download_custom_headers(url) + + do.call(utils::download.file, args) + + } + + renv_bootstrap_download_custom_headers <- function(url) { + + headers <- getOption("renv.download.headers") + if (is.null(headers)) + return(character()) + + if (!is.function(headers)) + stopf("'renv.download.headers' is not a function") + + headers <- headers(url) + if (length(headers) == 0L) + return(character()) + + if (is.list(headers)) + headers <- unlist(headers, recursive = FALSE, use.names = TRUE) + + ok <- + is.character(headers) && + is.character(names(headers)) && + all(nzchar(names(headers))) + + if (!ok) + stop("invocation of 'renv.download.headers' did not return a named character vector") + + headers + + } + + renv_bootstrap_download_cran_latest <- function(version) { + + spec <- renv_bootstrap_download_cran_latest_find(version) + type <- spec$type + repos <- spec$repos + + message("* Downloading renv ", version, " ... ", appendLF = FALSE) + + baseurl <- utils::contrib.url(repos = repos, type = type) + ext <- if (identical(type, "source")) + ".tar.gz" + else if (Sys.info()[["sysname"]] == "Windows") + ".zip" + else + ".tgz" + name <- sprintf("renv_%s%s", version, ext) + url <- paste(baseurl, name, sep = "/") + + destfile <- file.path(tempdir(), name) + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (inherits(status, "condition")) { + message("FAILED") + return(FALSE) + } + + # report success and return + message("OK (downloaded ", type, ")") + destfile + + } + + renv_bootstrap_download_cran_latest_find <- function(version) { + + # check whether binaries are supported on this system + binary <- + getOption("renv.bootstrap.binary", default = TRUE) && + !identical(.Platform$pkgType, "source") && + !identical(getOption("pkgType"), "source") && + Sys.info()[["sysname"]] %in% c("Darwin", "Windows") + + types <- c(if (binary) "binary", "source") + + # iterate over types + repositories + for (type in types) { + for (repos in renv_bootstrap_repos()) { + + # retrieve package database + db <- tryCatch( + as.data.frame( + utils::available.packages(type = type, repos = repos), + stringsAsFactors = FALSE + ), + error = identity + ) + + if (inherits(db, "error")) + next + + # check for compatible entry + entry <- db[db$Package %in% "renv" & db$Version %in% version, ] + if (nrow(entry) == 0) + next + + # found it; return spec to caller + spec <- list(entry = entry, type = type, repos = repos) + return(spec) + + } + } + + # if we got here, we failed to find renv + fmt <- "renv %s is not available from your declared package repositories" + stop(sprintf(fmt, version)) + + } + + renv_bootstrap_download_cran_archive <- function(version) { + + name <- sprintf("renv_%s.tar.gz", version) + repos <- renv_bootstrap_repos() + urls <- file.path(repos, "src/contrib/Archive/renv", name) + destfile <- file.path(tempdir(), name) + + message("* Downloading renv ", version, " ... ", appendLF = FALSE) + + for (url in urls) { + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (identical(status, 0L)) { + message("OK") + return(destfile) + } + + } + + message("FAILED") + return(FALSE) + + } + + renv_bootstrap_download_tarball <- function(version) { + + # if the user has provided the path to a tarball via + # an environment variable, then use it + tarball <- Sys.getenv("RENV_BOOTSTRAP_TARBALL", unset = NA) + if (is.na(tarball)) + return() + + # allow directories + if (dir.exists(tarball)) { + name <- sprintf("renv_%s.tar.gz", version) + tarball <- file.path(tarball, name) + } + + # bail if it doesn't exist + if (!file.exists(tarball)) { + + # let the user know we weren't able to honour their request + fmt <- "* RENV_BOOTSTRAP_TARBALL is set (%s) but does not exist." + msg <- sprintf(fmt, tarball) + warning(msg) + + # bail + return() + + } + + fmt <- "* Bootstrapping with tarball at path '%s'." + msg <- sprintf(fmt, tarball) + message(msg) + + tarball + + } + + renv_bootstrap_download_github <- function(version) { + + enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE") + if (!identical(enabled, "TRUE")) + return(FALSE) + + # prepare download options + pat <- Sys.getenv("GITHUB_PAT") + if (nzchar(Sys.which("curl")) && nzchar(pat)) { + fmt <- "--location --fail --header \"Authorization: token %s\"" + extra <- sprintf(fmt, pat) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "curl", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } else if (nzchar(Sys.which("wget")) && nzchar(pat)) { + fmt <- "--header=\"Authorization: token %s\"" + extra <- sprintf(fmt, pat) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "wget", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } + + message("* Downloading renv ", version, " from GitHub ... ", appendLF = FALSE) + + url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version) + name <- sprintf("renv_%s.tar.gz", version) + destfile <- file.path(tempdir(), name) + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (!identical(status, 0L)) { + message("FAILED") + return(FALSE) + } + + message("OK") + return(destfile) + + } + + renv_bootstrap_install <- function(version, tarball, library) { + + # attempt to install it into project library + message("* Installing renv ", version, " ... ", appendLF = FALSE) + dir.create(library, showWarnings = FALSE, recursive = TRUE) + + # invoke using system2 so we can capture and report output + bin <- R.home("bin") + exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" + r <- file.path(bin, exe) + + args <- c( + "--vanilla", "CMD", "INSTALL", "--no-multiarch", + "-l", shQuote(path.expand(library)), + shQuote(path.expand(tarball)) + ) + + output <- system2(r, args, stdout = TRUE, stderr = TRUE) + message("Done!") + + # check for successful install + status <- attr(output, "status") + if (is.numeric(status) && !identical(status, 0L)) { + header <- "Error installing renv:" + lines <- paste(rep.int("=", nchar(header)), collapse = "") + text <- c(header, lines, output) + writeLines(text, con = stderr()) + } + + status + + } + + renv_bootstrap_platform_prefix <- function() { + + # construct version prefix + version <- paste(R.version$major, R.version$minor, sep = ".") + prefix <- paste("R", numeric_version(version)[1, 1:2], sep = "-") + + # include SVN revision for development versions of R + # (to avoid sharing platform-specific artefacts with released versions of R) + devel <- + identical(R.version[["status"]], "Under development (unstable)") || + identical(R.version[["nickname"]], "Unsuffered Consequences") + + if (devel) + prefix <- paste(prefix, R.version[["svn rev"]], sep = "-r") + + # build list of path components + components <- c(prefix, R.version$platform) + + # include prefix if provided by user + prefix <- renv_bootstrap_platform_prefix_impl() + if (!is.na(prefix) && nzchar(prefix)) + components <- c(prefix, components) + + # build prefix + paste(components, collapse = "/") + + } + + renv_bootstrap_platform_prefix_impl <- function() { + + # if an explicit prefix has been supplied, use it + prefix <- Sys.getenv("RENV_PATHS_PREFIX", unset = NA) + if (!is.na(prefix)) + return(prefix) + + # if the user has requested an automatic prefix, generate it + auto <- Sys.getenv("RENV_PATHS_PREFIX_AUTO", unset = NA) + if (auto %in% c("TRUE", "True", "true", "1")) + return(renv_bootstrap_platform_prefix_auto()) + + # empty string on failure + "" + + } + + renv_bootstrap_platform_prefix_auto <- function() { + + prefix <- tryCatch(renv_bootstrap_platform_os(), error = identity) + if (inherits(prefix, "error") || prefix %in% "unknown") { + + msg <- paste( + "failed to infer current operating system", + "please file a bug report at https://github.com/rstudio/renv/issues", + sep = "; " + ) + + warning(msg) + + } + + prefix + + } + + renv_bootstrap_platform_os <- function() { + + sysinfo <- Sys.info() + sysname <- sysinfo[["sysname"]] + + # handle Windows + macOS up front + if (sysname == "Windows") + return("windows") + else if (sysname == "Darwin") + return("macos") + + # check for os-release files + for (file in c("/etc/os-release", "/usr/lib/os-release")) + if (file.exists(file)) + return(renv_bootstrap_platform_os_via_os_release(file, sysinfo)) + + # check for redhat-release files + if (file.exists("/etc/redhat-release")) + return(renv_bootstrap_platform_os_via_redhat_release()) + + "unknown" + + } + + renv_bootstrap_platform_os_via_os_release <- function(file, sysinfo) { + + # read /etc/os-release + release <- utils::read.table( + file = file, + sep = "=", + quote = c("\"", "'"), + col.names = c("Key", "Value"), + comment.char = "#", + stringsAsFactors = FALSE + ) + + vars <- as.list(release$Value) + names(vars) <- release$Key + + # get os name + os <- tolower(sysinfo[["sysname"]]) + + # read id + id <- "unknown" + for (field in c("ID", "ID_LIKE")) { + if (field %in% names(vars) && nzchar(vars[[field]])) { + id <- vars[[field]] + break + } + } + + # read version + version <- "unknown" + for (field in c("UBUNTU_CODENAME", "VERSION_CODENAME", "VERSION_ID", "BUILD_ID")) { + if (field %in% names(vars) && nzchar(vars[[field]])) { + version <- vars[[field]] + break + } + } + + # join together + paste(c(os, id, version), collapse = "-") + + } + + renv_bootstrap_platform_os_via_redhat_release <- function() { + + # read /etc/redhat-release + contents <- readLines("/etc/redhat-release", warn = FALSE) + + # infer id + id <- if (grepl("centos", contents, ignore.case = TRUE)) + "centos" + else if (grepl("redhat", contents, ignore.case = TRUE)) + "redhat" + else + "unknown" + + # try to find a version component (very hacky) + version <- "unknown" + + parts <- strsplit(contents, "[[:space:]]")[[1L]] + for (part in parts) { + + nv <- tryCatch(numeric_version(part), error = identity) + if (inherits(nv, "error")) + next + + version <- nv[1, 1] + break + + } + + paste(c("linux", id, version), collapse = "-") + + } + + renv_bootstrap_library_root_name <- function(project) { + + # use project name as-is if requested + asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") + if (asis) + return(basename(project)) + + # otherwise, disambiguate based on project's path + id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) + paste(basename(project), id, sep = "-") + + } + + renv_bootstrap_library_root <- function(project) { + + prefix <- renv_bootstrap_profile_prefix() + + path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) + if (!is.na(path)) + return(paste(c(path, prefix), collapse = "/")) + + path <- renv_bootstrap_library_root_impl(project) + if (!is.null(path)) { + name <- renv_bootstrap_library_root_name(project) + return(paste(c(path, prefix, name), collapse = "/")) + } + + renv_bootstrap_paths_renv("library", project = project) + + } + + renv_bootstrap_library_root_impl <- function(project) { + + root <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) + if (!is.na(root)) + return(root) + + type <- renv_bootstrap_project_type(project) + if (identical(type, "package")) { + userdir <- renv_bootstrap_user_dir() + return(file.path(userdir, "library")) + } + + } + + renv_bootstrap_validate_version <- function(version) { + + loadedversion <- utils::packageDescription("renv", fields = "Version") + if (version == loadedversion) + return(TRUE) + + # assume four-component versions are from GitHub; + # three-component versions are from CRAN + components <- strsplit(loadedversion, "[.-]")[[1]] + remote <- if (length(components) == 4L) + paste("rstudio/renv", loadedversion, sep = "@") + else + paste("renv", loadedversion, sep = "@") + + fmt <- paste( + "renv %1$s was loaded from project library, but this project is configured to use renv %2$s.", + "Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile.", + "Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library.", + sep = "\n" + ) + + msg <- sprintf(fmt, loadedversion, version, remote) + warning(msg, call. = FALSE) + + FALSE + + } + + renv_bootstrap_hash_text <- function(text) { + + hashfile <- tempfile("renv-hash-") + on.exit(unlink(hashfile), add = TRUE) + + writeLines(text, con = hashfile) + tools::md5sum(hashfile) + + } + + renv_bootstrap_load <- function(project, libpath, version) { + + # try to load renv from the project library + if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) + return(FALSE) + + # warn if the version of renv loaded does not match + renv_bootstrap_validate_version(version) + + # execute renv load hooks, if any + hooks <- getHook("renv::autoload") + for (hook in hooks) + if (is.function(hook)) + tryCatch(hook(), error = warning) + + # load the project + renv::load(project) + + TRUE + + } + + renv_bootstrap_profile_load <- function(project) { + + # if RENV_PROFILE is already set, just use that + profile <- Sys.getenv("RENV_PROFILE", unset = NA) + if (!is.na(profile) && nzchar(profile)) + return(profile) + + # check for a profile file (nothing to do if it doesn't exist) + path <- renv_bootstrap_paths_renv("profile", profile = FALSE, project = project) + if (!file.exists(path)) + return(NULL) + + # read the profile, and set it if it exists + contents <- readLines(path, warn = FALSE) + if (length(contents) == 0L) + return(NULL) + + # set RENV_PROFILE + profile <- contents[[1L]] + if (!profile %in% c("", "default")) + Sys.setenv(RENV_PROFILE = profile) + + profile + + } + + renv_bootstrap_profile_prefix <- function() { + profile <- renv_bootstrap_profile_get() + if (!is.null(profile)) + return(file.path("profiles", profile, "renv")) + } + + renv_bootstrap_profile_get <- function() { + profile <- Sys.getenv("RENV_PROFILE", unset = "") + renv_bootstrap_profile_normalize(profile) + } + + renv_bootstrap_profile_set <- function(profile) { + profile <- renv_bootstrap_profile_normalize(profile) + if (is.null(profile)) + Sys.unsetenv("RENV_PROFILE") + else + Sys.setenv(RENV_PROFILE = profile) + } + + renv_bootstrap_profile_normalize <- function(profile) { + + if (is.null(profile) || profile %in% c("", "default")) + return(NULL) + + profile + + } + + renv_bootstrap_path_absolute <- function(path) { + + substr(path, 1L, 1L) %in% c("~", "/", "\\") || ( + substr(path, 1L, 1L) %in% c(letters, LETTERS) && + substr(path, 2L, 3L) %in% c(":/", ":\\") + ) + + } + + renv_bootstrap_paths_renv <- function(..., profile = TRUE, project = NULL) { + renv <- Sys.getenv("RENV_PATHS_RENV", unset = "renv") + root <- if (renv_bootstrap_path_absolute(renv)) NULL else project + prefix <- if (profile) renv_bootstrap_profile_prefix() + components <- c(root, renv, prefix, ...) + paste(components, collapse = "/") + } + + renv_bootstrap_project_type <- function(path) { + + descpath <- file.path(path, "DESCRIPTION") + if (!file.exists(descpath)) + return("unknown") + + desc <- tryCatch( + read.dcf(descpath, all = TRUE), + error = identity + ) + + if (inherits(desc, "error")) + return("unknown") + + type <- desc$Type + if (!is.null(type)) + return(tolower(type)) + + package <- desc$Package + if (!is.null(package)) + return("package") + + "unknown" + + } + + renv_bootstrap_user_dir <- function() { + dir <- renv_bootstrap_user_dir_impl() + path.expand(chartr("\\", "/", dir)) + } + + renv_bootstrap_user_dir_impl <- function() { + + # use local override if set + override <- getOption("renv.userdir.override") + if (!is.null(override)) + return(override) + + # use R_user_dir if available + tools <- asNamespace("tools") + if (is.function(tools$R_user_dir)) + return(tools$R_user_dir("renv", "cache")) + + # try using our own backfill for older versions of R + envvars <- c("R_USER_CACHE_DIR", "XDG_CACHE_HOME") + for (envvar in envvars) { + root <- Sys.getenv(envvar, unset = NA) + if (!is.na(root)) + return(file.path(root, "R/renv")) + } + + # use platform-specific default fallbacks + if (Sys.info()[["sysname"]] == "Windows") + file.path(Sys.getenv("LOCALAPPDATA"), "R/cache/R/renv") + else if (Sys.info()[["sysname"]] == "Darwin") + "~/Library/Caches/org.R-project.R/R/renv" + else + "~/.cache/R/renv" + + } + + + renv_json_read <- function(file = NULL, text = NULL) { + + jlerr <- NULL + + # if jsonlite is loaded, use that instead + if ("jsonlite" %in% loadedNamespaces()) { + + json <- catch(renv_json_read_jsonlite(file, text)) + if (!inherits(json, "error")) + return(json) + + jlerr <- json + + } + + # otherwise, fall back to the default JSON reader + json <- catch(renv_json_read_default(file, text)) + if (!inherits(json, "error")) + return(json) + + # report an error + if (!is.null(jlerr)) + stop(jlerr) + else + stop(json) + + } + + renv_json_read_jsonlite <- function(file = NULL, text = NULL) { + text <- paste(text %||% read(file), collapse = "\n") + jsonlite::fromJSON(txt = text, simplifyVector = FALSE) + } + + renv_json_read_default <- function(file = NULL, text = NULL) { + + # find strings in the JSON + text <- paste(text %||% read(file), collapse = "\n") + pattern <- '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]' + locs <- gregexpr(pattern, text, perl = TRUE)[[1]] + + # if any are found, replace them with placeholders + replaced <- text + strings <- character() + replacements <- character() + + if (!identical(c(locs), -1L)) { + + # get the string values + starts <- locs + ends <- locs + attr(locs, "match.length") - 1L + strings <- substring(text, starts, ends) + + # only keep those requiring escaping + strings <- grep("[[\\]{}:]", strings, perl = TRUE, value = TRUE) + + # compute replacements + replacements <- sprintf('"\032%i\032"', seq_along(strings)) + + # replace the strings + mapply(function(string, replacement) { + replaced <<- sub(string, replacement, replaced, fixed = TRUE) + }, strings, replacements) + + } + + # transform the JSON into something the R parser understands + transformed <- replaced + transformed <- gsub("{}", "`names<-`(list(), character())", transformed, fixed = TRUE) + transformed <- gsub("[[{]", "list(", transformed, perl = TRUE) + transformed <- gsub("[]}]", ")", transformed, perl = TRUE) + transformed <- gsub(":", "=", transformed, fixed = TRUE) + text <- paste(transformed, collapse = "\n") + + # parse it + json <- parse(text = text, keep.source = FALSE, srcfile = NULL)[[1L]] + + # construct map between source strings, replaced strings + map <- as.character(parse(text = strings)) + names(map) <- as.character(parse(text = replacements)) + + # convert to list + map <- as.list(map) + + # remap strings in object + remapped <- renv_json_remap(json, map) + + # evaluate + eval(remapped, envir = baseenv()) + + } + + renv_json_remap <- function(json, map) { + + # fix names + if (!is.null(names(json))) { + lhs <- match(names(json), names(map), nomatch = 0L) + rhs <- match(names(map), names(json), nomatch = 0L) + names(json)[rhs] <- map[lhs] + } + + # fix values + if (is.character(json)) + return(map[[json]] %||% json) + + # handle true, false, null + if (is.name(json)) { + text <- as.character(json) + if (text == "true") + return(TRUE) + else if (text == "false") + return(FALSE) + else if (text == "null") + return(NULL) + } + + # recurse + if (is.recursive(json)) { + for (i in seq_along(json)) { + json[i] <- list(renv_json_remap(json[[i]], map)) + } + } + + json + + } + + # load the renv profile, if any + renv_bootstrap_profile_load(project) + + # construct path to library root + root <- renv_bootstrap_library_root(project) + + # construct library prefix for platform + prefix <- renv_bootstrap_platform_prefix() + + # construct full libpath + libpath <- file.path(root, prefix) + + # attempt to load + if (renv_bootstrap_load(project, libpath, version)) + return(TRUE) + + # load failed; inform user we're about to bootstrap + prefix <- paste("# Bootstrapping renv", version) + postfix <- paste(rep.int("-", 77L - nchar(prefix)), collapse = "") + header <- paste(prefix, postfix) + message(header) + + # perform bootstrap + bootstrap(version, libpath) + + # exit early if we're just testing bootstrap + if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) + return(TRUE) + + # try again to load + if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { + message("* Successfully installed and loaded renv ", version, ".") + return(renv::load()) + } + + # failed to download or load renv; warn the user + msg <- c( + "Failed to find an renv installation: the project will not be loaded.", + "Use `renv::activate()` to re-initialize the project." + ) + + warning(paste(msg, collapse = "\n"), call. = FALSE) + +}) diff --git a/schematic_config.yml b/schematic_config.yml index d09bb089..fd602f0c 100644 --- a/schematic_config.yml +++ b/schematic_config.yml @@ -1,38 +1,28 @@ -# Do not change the 'definitions' section unless you know what you're doing schematic: - branch: 'empty' - sha: 'empty' - release_version: 'empty' - + branch: empty + sha: empty + release_version: empty definitions: - synapse_config: '.synapseConfig' - creds_path: 'credentials.json' - token_pickle: 'token.pickle' - service_acct_creds: 'schematic_service_account_creds.json' - + synapse_config: .synapseConfig + creds_path: credentials.json + token_pickle: token.pickle + service_acct_creds: schematic_service_account_creds.json synapse: - master_fileview: 'syn33715412' - manifest_folder: 'manifests' - manifest_basename: 'synapse_storage_manifest' - token_creds: 'syn23643259' - service_acct_creds: 'syn25171627' - + master_fileview: syn27210848 + manifest_folder: manifests + manifest_basename: synapse_storage_manifest + token_creds: syn23643259 + service_acct_creds: syn25171627 manifest: - # if making many manifests, just include name prefix - title: 'example' - # to make all manifests enter only 'all manifests' + title: example data_type: - - 'Biospecimen' - - 'Patient' - + - Biospecimen + - Patient model: input: - # if both 'download_url' and 'repo' present, `repo` will be used - download_url: 'https://raw.githubusercontent.com/Sage-Bionetworks/data-models/main/example.model.jsonld' # url to download JSON-LD data model - repo: 'Sage-Bionetworks/data-models' # data model repo url (//), version or branch is optional - location: 'data-models/example.model.jsonld' # path to JSON-LD file - file_type: 'local' - + download_url: https://raw.githubusercontent.com/mc2-center/data-models/main/mc2.model.jsonld + location: data-models/example.model.jsonld + file_type: local style: google_manifest: req_bg_color: @@ -43,5 +33,7 @@ style: red: 1.0 green: 1.0 blue: 0.9019 - master_template_id: '1LYS5qE4nV9jzcYw5sXwCza25slDfRA1CIg3cs-hCdpU' - strict_validation: true + master_template_id: 1LYS5qE4nV9jzcYw5sXwCza25slDfRA1CIg3cs-hCdpU + strict_validation: yes +api: + type: reticulate diff --git a/server.R b/server.R index f4b0fcf5..8fab1621 100644 --- a/server.R +++ b/server.R @@ -7,49 +7,76 @@ shinyServer(function(input, output, session) { options(shiny.reactlog = TRUE) params <- parseQueryString(isolate(session$clientData$url_search)) - if (!has_auth_code(params)) { + if (!has_auth_code(params) & dca_schematic_api != "offline") { return() } + redirect_url <- paste0( api$access, "?", "redirect_uri=", app_url, "&grant_type=", "authorization_code", "&code=", params$code ) - # get the access_token and userinfo token - req <- POST(redirect_url, encode = "form", body = "", authenticate(app$key, app$secret, - type = "basic" - ), config = list()) - # Stop the code if anything other than 2XX status code is returned - stop_for_status(req, task = "get an access token") - token_response <- content(req, type = NULL) - access_token <- token_response$access_token - - session$userData$access_token <- access_token - - synapse_driver <- import("schematic.store.synapse")$SynapseStorage - + + if (dca_schematic_api != "offline") { + # get the access_token and userinfo token + req <- POST(redirect_url, encode = "form", body = "", authenticate(app$key, app$secret, + type = "basic" + ), config = list()) + + # Stop the code if anything other than 2XX status code is returned + stop_for_status(req, task = "get an access token") + token_response <- content(req, type = NULL) + access_token <- token_response$access_token + + session$userData$access_token <- access_token + } else { + dcWaiter("show", "Cannot connect to Synapse. Running in offline mode.") + } + ######## session global variables ######## # read config in - config <- config_file - config_schema <- as.data.frame(config[[1]]) - + def_config <- ifelse(dca_schematic_api == "offline", + fromJSON("www/template_config/config_offline.json"), + fromJSON("www/template_config/config.json") + ) + config <- reactiveVal() + config_schema <- reactiveVal(def_config) + model_ops <- setNames(dcc_config$data_model_url, + dcc_config$synapse_asset_view) + # mapping from display name to schema name - template_namedList <- config_schema$schema_name - names(template_namedList) <- config_schema$display_name - - # data available to the user - syn_store <- NULL # gets list of projects they have access to - + template_namedList <- reactiveVal() + #names(template_namedList) <- config_schema$display_name + + all_asset_views <- setNames(dcc_config$synapse_asset_view, + dcc_config$project_name) + asset_views <- reactiveVal(c("mock dca fileview"="syn33715412")) + + dcc_config_react <- reactiveVal(dcc_config) + data_list <- list( projects = reactiveVal(NULL), folders = reactiveVal(NULL), - schemas = reactiveVal(template_namedList), files = reactiveVal(NULL) + template = reactiveVal(setNames(def_config$schema_name, def_config$display_name)), + files = reactiveVal(NULL), + master_asset_view = reactiveVal(NULL) ) # synapse ID of selected data selected <- list( project = reactiveVal(NULL), folder = reactiveVal(""), - schema = reactiveVal(NULL), schema_type = reactiveVal(NULL) + schema = reactiveVal(NULL), schema_type = reactiveVal(NULL), + master_asset_view = reactiveVal(NULL), + master_asset_view_label = reactiveVal(NULL) ) - + isUpdateFolder <- reactiveVal(FALSE) + + data_model_options <- setNames(dcc_config$data_model_url, + dcc_config$synapse_asset_view) + data_model = reactiveVal(NULL) + + # data available to the user + syn_store <- NULL # gets list of projects they have access to + + asset_views <- reactiveVal(c("mock dca fileview (syn33715412)"="syn33715412")) tabs_list <- c("tab_data", "tab_template", "tab_upload") clean_tags <- c( @@ -63,6 +90,9 @@ shinyServer(function(input, output, session) { ######## Initiate Login Process ######## # synapse cookies session$sendCustomMessage(type = "readCookie", message = list()) + + shinyjs::useShinyjs() + shinyjs::hide(selector = ".sidebar-menu") # initial loading page # @@ -79,65 +109,187 @@ shinyServer(function(input, output, session) { # work in any domain and is scoped to the access required by the # Shiny app' # - access_token <- session$userData$access_token - - syn$login(authToken = access_token, rememberMe = FALSE) - - data_list$projects( - tryCatch( - { - # get syn storage - syn_store <<- synapse_driver(access_token = access_token) - # get user's common projects - list2Vector(syn_store$getStorageProjects()) - }, - error = function(e) { - message(e$message) - return(NULL) - } + + if (dca_schematic_api != "offline") { + access_token <- session$userData$access_token + has_access <- vapply(all_asset_views, function(x) { + synapse_access(id=x, access="DOWNLOAD", auth=access_token) + }, 1L) + asset_views(all_asset_views[has_access==1]) + + if (length(asset_views) == 0) stop("You do not have DOWNLOAD access to any supported Asset Views.") + updateSelectInput(session, "dropdown_asset_view", + choices = asset_views()) + + user_name <- synapse_user_profile(auth=access_token)$firstName + + is_certified <- synapse_is_certified(auth=access_token) + # is_certified <- switch(dca_schematic_api, + # reticulate = syn$is_certified(user_name), + # rest = synapse_is_certified(auth=access_token)) + if (!is_certified) { + dcWaiter("update", landing = TRUE, isCertified = FALSE) + } else { + # update waiter loading screen once login successful + dcWaiter("update", landing = TRUE, userName = user_name) + } + } else { + updateSelectInput(session, "dropdown_asset_view", + choices = c("Offline mock data (synXXXXXX)"="synXXXXXX")) + dcWaiter("hide") + } + + ######## Arrow Button ######## + lapply(1:4, function(i) { + switchTabServer(id = paste0("switchTab", i), tabId = "tabs", tab = reactive(input$tabs)(), tabList = tabs_list, parent = session) + }) + + }) + + observeEvent(input$btn_asset_view, { + selected$master_asset_view(input$dropdown_asset_view) + av_names <- names(asset_views()[asset_views() %in% selected$master_asset_view()]) + selected$master_asset_view_label(av_names) + + dcc_config_react(dcc_config[dcc_config$synapse_asset_view == selected$master_asset_view(), ]) + + dcWaiter("show", msg = paste0("Getting data from ", selected$master_asset_view_label(),". This may take a minute."), + color=col2rgba(col2rgb("#CD0BBC01"))) + + data_model(data_model_options[selected$master_asset_view()]) + + output$sass <- renderUI({ + tags$head(tags$style(css())) + }) + css <- reactive({ + # Don't change theme for default projects + #if (dca_theme_file != "www/dca_themes/sage_theme_config.rds") { + sass(input = list(primary_col=dcc_config_react()$primary_col, + htan_col=dcc_config_react()$secondary_col, + sidebar_col=dcc_config_react()$sidebar_col, + sass_file("www/scss/main.scss"))) + #} + }) + + dcWaiter("show", msg = paste0("Getting data from ", selected$master_asset_view_label(), ". This may take a minute."), + color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) + + output$logo <- renderUI({update_logo(selected$master_asset_view())}) + + if (dca_schematic_api == "reticulate") { + # Update schematic_config and login + schematic_config <- yaml::read_yaml("schematic_config.yml") + schematic_config$synapse$master_fileview <- selected$master_asset_view() + schematic_config$model$input$download_url <- model_ops[names(model_ops) == selected$master_asset_view()] + yaml::write_yaml(schematic_config, "schematic_config.yml") + download.file(schematic_config$model$input$download_url, schematic_config$model$input$location) + setup_synapse_driver() + syn$login(authToken = access_token, rememberMe = FALSE) + syn_store <<- synapse_driver(access_token = access_token) + + system( + "python3 .github/config_schema.py -c schematic_config.yml --service_repo 'Sage-Bionetworks/schematic' --overwrite" ) + + } + + conf_file <- reactiveVal(template_config_files[input$dropdown_asset_view]) + config_df <- jsonlite::fromJSON(conf_file()) + + conf_template <- setNames(config_df[[1]]$schema_name, config_df[[1]]$display_name) + config(config_df) + config_schema(config_df) + data_list$template(conf_template) + + data_list_raw <- switch(dca_schematic_api, + reticulate = storage_projects_py(synapse_driver, access_token), + rest = storage_projects(url=file.path(api_uri, "v1/storage/projects"), + asset_view = selected$master_asset_view(), + input_token = access_token), + list(list("Offline Project A", "Offline Project")) ) - + data_list$projects(list2Vector(data_list_raw)) + if (is.null(data_list$projects()) || length(data_list$projects()) == 0) { dcWaiter("update", landing = TRUE, isPermission = FALSE) } else { - + # updates project dropdown lapply(c("header_dropdown_", "dropdown_"), function(x) { lapply(c(1, 3), function(i) { updateSelectInput(session, paste0(x, dropdown_types[i]), - choices = sort(names(data_list[[i]]())) + choices = sort(names(data_list[[i]]())) ) }) }) - - user_name <- syn$getUserProfile()$userName - - if (!syn$is_certified(user_name)) { - dcWaiter("update", landing = TRUE, isCertified = FALSE) - } else { - # update waiter loading screen once login successful - dcWaiter("update", landing = TRUE, userName = user_name) - } } + + ######## Update Folder List ######## + lapply(c("header_dropdown_", "dropdown_"), function(x) { + observeEvent(ignoreInit = TRUE, input[[paste0(x, "project")]], { + + # get synID of selected project + project_id <- data_list$projects()[input[[paste0(x, "project")]]] + + dcWaiter("show", msg = paste0("Getting project data from ", selected$master_asset_view_label(), ". This may take a minute."), + color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) + + # gets folders per project + folder_list_raw <- switch(dca_schematic_api, + reticulate = storage_projects_datasets_py(synapse_driver, project_id), + rest = storage_project_datasets(url=file.path(api_uri, "v1/storage/project/datasets"), + asset_view = selected$master_asset_view(), + project_id=project_id, + input_token=access_token), + list(list("DatatypeA", "DatatypeA"), list("DatatypeB","DatatypeB")) + ) + folder_list <- list2Vector(folder_list_raw) + + if (length(folder_list) > 0) folder_names <- sort(names(folder_list)) else folder_names <- " " + + # update folder names + updateSelectInput(session, paste0(x, "folder"), choices = folder_names) + + if (x == "dropdown_") { + selected$project(project_id) + data_list$folders(folder_list) + } + + if (isUpdateFolder()) { + # sync with header dropdown + updateSelectInput(session, "dropdown_folder", selected = input[["header_dropdown_folder"]]) + isUpdateFolder(FALSE) + } + + dcWaiter("hide") + + }) + }) + + updateTabsetPanel(session, "tabs", + selected = "tab_data") + + shinyjs::show(selector = ".sidebar-menu") + + dcWaiter("hide") }) ######## Arrow Button ######## - lapply(1:3, function(i) { + lapply(1:4, function(i) { switchTabServer(id = paste0("switchTab", i), tabId = "tabs", tab = reactive(input$tabs)(), tabList = tabs_list, parent = session) }) ######## Header Dropdown Button ######## # Adjust header selection dropdown based on tabs observe({ - if (input[["tabs"]] == "tab_data") { + if (input[["tabs"]] %in% c("tab_data", "tab_asset_view")) { hide("header_selection_dropdown") } else { show("header_selection_dropdown") addClass(id = "header_selection_dropdown", class = "open") } }) - + # sync header dropdown with main dropdown lapply(dropdown_types, function(x) { observeEvent(input[[paste0("dropdown_", x)]], { @@ -167,34 +319,6 @@ shinyServer(function(input, output, session) { }) }) - ######## Update Folder List ######## - lapply(c("header_dropdown_", "dropdown_"), function(x) { - observeEvent(ignoreInit = TRUE, input[[paste0(x, "project")]], { - - # get synID of selected project - project_id <- data_list$projects()[input[[paste0(x, "project")]]] - - # gets folders per project - folder_list <- syn_store$getStorageDatasetsInProject(project_id) %>% list2Vector() - - if (length(folder_list) > 0) folder_names <- sort(names(folder_list)) else folder_names <- " " - - # update folder names - updateSelectInput(session, paste0(x, "folder"), choices = folder_names) - - if (x == "dropdown_") { - selected$project(project_id) - data_list$folders(folder_list) - } - - if (isUpdateFolder()) { - # sync with header dropdown - updateSelectInput(session, "dropdown_folder", selected = input[["header_dropdown_folder"]]) - isUpdateFolder(FALSE) - } - }) - }) - ######## Update Folder ######## # update selected folder synapse id and name observeEvent(input$dropdown_folder, { @@ -205,53 +329,64 @@ shinyServer(function(input, output, session) { ######## Update Template ######## # update selected schema template name - observeEvent(input$dropdown_datatype, { + observeEvent(input$dropdown_template, { # update reactive selected values for schema - selected$schema(data_list$schemas()[input$dropdown_datatype]) - schema_type <- config_schema$type[which(config_schema$display_name == input$dropdown_datatype)] + selected$schema(data_list$template()[input$dropdown_template]) + schema_type <- config_schema()[[1]]$type[which(config_schema()[[1]]$display_name == input$dropdown_template)] selected$schema_type(schema_type) # clean all tags related with selected template sapply(clean_tags, FUN = hide) - }) + }, ignoreInit = TRUE) ######## Dashboard ######## -# dashboard( -# id = "dashboard", -# syn.store = syn_store, -# project.scope = selected$project, -# schema = selected$schema, -# schema.display.name = reactive(input$dropdown_datatype), -# disable.ids = c("box_pick_project", "box_pick_manifest"), -# ncores = ncores -# ) + dashboard( + id = "dashboard", + syn.store = syn_store, + project.scope = selected$project, + schema = selected$schema, + schema.display.name = reactive(input$dropdown_datatype), + disable.ids = c("box_pick_project", "box_pick_manifest"), + ncores = ncores, + access_token = access_token, + fileview = selected$master_asset_view(), + folder = selected$project(), + schematic_api = dca_schematic_api, + schema_url = data_model() + ) + manifest_url <- reactiveVal(NULL) + ######## Template Google Sheet Link ######## # validate before generating template observeEvent(c(selected$folder(), selected$schema(), input$tabs), { req(input$tabs %in% c("tab_template", "tab_validate")) - warn_text <- NULL - - if (length(data_list$folder()) == 0) { + if (length(data_list$folders()) == 0) { # add warning if there is no folder in the selected project warn_text <- paste0( "please create a folder in the ", strong(sQuote(input$dropdown_project)), " prior to submitting templates." ) - } else if (selected$schema_type() == "file") { + } else if (selected$schema_type() %in% c("record", "file")) { # check number of files if it's file-based template - dcWaiter("show", msg = paste0("Getting files in ", input$dropdown_folder, "...")) + dcWaiter("show", msg = paste0("Getting files in ", input$dropdown_folder, "."), color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) # get file list in selected folder - file_list <- syn_store$getFilesInStorageDataset(selected$folder()) + file_list <- switch(dca_schematic_api, + reticulate = storage_dataset_files_py(selected$folder()), + rest = storage_dataset_files(url=file.path(api_uri, "v1/storage/dataset/files"), + asset_view = selected$master_asset_view(), + dataset_id = selected$folder(), + input_token=access_token), + list(list("DatatypeA", "DatatypeA"), list("DatatypeB", "DatatypeB"))) # update files list in the folder data_list$files(list2Vector(file_list)) dcWaiter("hide") - - if (length(data_list$files()) == 0) { + + if (is.null(data_list$files())) { # display warning message if folder is empty and data type is file-based warn_text <- paste0( strong(sQuote(input$dropdown_folder)), " folder is empty, @@ -270,27 +405,100 @@ shinyServer(function(input, output, session) { output$text_template_warn <- renderUI(tagList(br(), span(class = "warn_msg", HTML(warn_text)))) show("div_template_warn") }) + + observeEvent(c(input$dropdown_folder, input$tabs), { + # if (input$tabs == "tab_template" && Sys.getenv("DCA_MANIFEST_OUTPUT_FORMAT") == "excel") { + # dcWaiter("show", msg = "Downloading data from Synapse...", color = dca_theme()$primary_col) + # #schematic rest api to generate manifest + # manifest_data <- switch(dca_schematic_api, + # reticulate = manifest_generate_py(title = input$dropdown_template, + # rootNode = selected$schema(), + # datasetId = selected$folder()), + # rest = manifest_generate(url=file.path(api_uri, "v1/manifest/generate"), + # schema_url = data_model(), + # title = input$dropdown_template, + # data_type = selected$schema(), + # dataset_id = selected$folder(), + # asset_view = selected$master_asset_view(), + # output_format = Sys.getenv("DCA_MANIFEST_OUTPUT_FORMAT"), + # input_token=access_token), + # "offline-no-gsheet-url" + # ) + # manifest_url(manifest_data) + # + # dcWaiter("hide", sleep = 1) + # + # } + }) + + # Bookmarking this thread in case we can't use writeBin... + # Use a db connection instead + # https://community.rstudio.com/t/how-to-let-download-button-work-with-eventreactive/20937 + + # The giant anonymous content function lets users click through the app and + # only download the manifest if they need to. Originally, this was in the + # observeEvent above. + output$downloadData <- downloadHandler( + filename = function() sprintf("%s.xlsx", input$dropdown_template), + content = function(file) { + dcWaiter("show", msg = "Downloading manifest. This may take a minute.", color = dcc_config_react()$primary_col) + manifest_data <- switch(dca_schematic_api, + reticulate = manifest_generate_py(title = input$dropdown_template, + rootNode = selected$schema(), + datasetId = selected$folder()), + rest = manifest_generate(url=file.path(api_uri, "v1/manifest/generate"), + schema_url = data_model(), + title = input$dropdown_template, + data_type = selected$schema(), + dataset_id = selected$folder(), + asset_view = selected$master_asset_view(), + output_format = Sys.getenv("DCA_MANIFEST_OUTPUT_FORMAT"), + input_token=access_token), + "offline-no-gsheet-url" + ) + dcWaiter("hide", sleep = 1) + writeBin(manifest_data, file) + #capture.output(print(manifest_url()), file=file) # actually kinda works + # Just shows NULL + # sink(file) + # print(manifest_url()) + # sink() + } + ) + + if (dca_schematic_api == "offline") { + mock_offline_manifest <- tibble("column1"="mock offline data") + output$downloadData <- downloadHandler( + filename = function() sprintf("%s.csv", input$dropdown_template), + content = function(file) { + write_csv(mock_offline_manifest, file) + } + ) + } # generate template observeEvent(input$btn_template, { - # loading screen for template link generation - dcWaiter("show", msg = "Generating link...") - - manifest_url <- - metadata_model$getModelManifest( - title = paste0(config$community, " ", input$dropdown_datatype), - rootNode = selected$schema(), - filenames = switch((selected$schema_type() == "file") + 1, - NULL, - as.list(names(data_list$files())) - ), - datasetId = selected$folder() - ) + dcWaiter("show", msg = "Generating link...", color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) + manifest_url(switch(dca_schematic_api, + reticulate = manifest_generate_py(title = input$dropdown_template, + rootNode = selected$schema(), + datasetId = selected$folder()), + rest = manifest_generate(url=file.path(api_uri, "v1/manifest/generate"), + schema_url = data_model(), + title = input$dropdown_template, + data_type = selected$schema(), + dataset_id = selected$folder(), + asset_view = selected$master_asset_view(), + output_format = Sys.getenv("DCA_MANIFEST_OUTPUT_FORMAT"), + input_token=access_token), + "offline-no-gsheet-url" + ) + ) # generate link output$text_template <- renderUI( - tags$a(id = "template_link", href = manifest_url, list(icon("hand-point-right"), manifest_url), target = "_blank") + tags$a(id = "template_link", href = manifest_url(), list(icon("hand-point-right"), manifest_url()), target = "_blank") ) dcWaiter("hide", sleep = 1) @@ -298,20 +506,21 @@ shinyServer(function(input, output, session) { nx_confirm( inputId = "btn_template_confirm", title = "Go to the template now?", - message = paste0("click 'Go' to edit your ", sQuote(input$dropdown_datatype), " template on the google sheet"), + message = paste0("click 'Go' to edit your ", sQuote(input$dropdown_template), " template on the google sheet"), button_ok = "Go", ) # display link show("div_template") # TODO: add progress bar on (loading) screen }) - + observeEvent(input$btn_template_confirm, { req(input$btn_template_confirm == TRUE) runjs("$('#template_link')[0].click();") }) ######## Reads .csv File ######## + # Check out module and don't use filepath. Keep file in memory inFile <- csvInfileServer("inputFile", colsAsCharacters = TRUE, keepBlank = TRUE) observeEvent(inFile$data(), { @@ -325,24 +534,27 @@ shinyServer(function(input, output, session) { observeEvent(input$btn_validate, { # loading screen for validating metadata - dcWaiter("show", msg = "Validating...") - - annotation_status <- - tryCatch( - metadata_model$validateModelManifest( - manifestPath = inFile$raw()$datapath, - rootNode = selected$schema(), - restrict_rules = TRUE, # set true to disable great expectation - project_scope = list(selected$project()) - ), - error = function(e) { - message("'validateModelManifest' failed:\n", e$message) - return(NULL) - } - ) + dcWaiter("show", msg = "Validating manifest...", color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) + annotation_status <- switch(dca_schematic_api, + reticulate = manifest_validate_py(inFile$raw()$datapath, + selected$schema(), + TRUE, + list(selected$project())), + rest = manifest_validate(url=file.path(api_uri, "v1/model/validate"), + schema_url=data_model(), + data_type=selected$schema(), + file_name=inFile$raw()$datapath), + #json_str=jsonlite::toJSON(read_csv(inFile$raw()$datapath))), + list(list( + "errors" = list( + Row = NA, Column = NA, Value = NA, + Error = "Mock error for offline mode." + ) + )) + ) # validation messages - validation_res <- validationResult(annotation_status, input$dropdown_datatype, inFile$data()) + validation_res <- validationResult(annotation_status, input$dropdown_template, inFile$data()) ValidationMsgServer("text_validate", validation_res) # if there is a file uploaded @@ -364,10 +576,16 @@ shinyServer(function(input, output, session) { output$submit <- renderUI(actionButton("btn_submit", "Submit to Synapse", class = "btn-primary-color")) dcWaiter("update", msg = paste0(validation_res$error_type, " Found !!! "), spin = spin_inner_circles(), sleep = 2.5) } else { - output$val_gsheet <- renderUI( - actionButton("btn_val_gsheet", " Generate Google Sheet Link", icon = icon("table"), class = "btn-primary-color") - ) - dcWaiter("update", msg = paste0(validation_res$error_type, " Found !!! "), spin = spin_pulsar(), sleep = 2.5) + if (dca_schematic_api != "offline" & Sys.getenv("DCA_MANIFEST_OUTPUT_FORMAT") == "google_sheet") { + output$val_gsheet <- renderUI( + actionButton("btn_val_gsheet", " Generate Google Sheet Link", icon = icon("table"), class = "btn-primary-color") + ) + } else if (dca_schematic_api == "offline") { + output$dl_manifest <- renderUI({ + downloadButton("downloadData_good", "Download Corrected Data") + }) + } + dcWaiter("update", msg = paste0(validation_res$error_type, " Found !!! "), spin = spin_pulsar(), sleep = 2.5) } } else { dcWaiter("hide") @@ -379,28 +597,43 @@ shinyServer(function(input, output, session) { # if user click gsheet_btn, generating gsheet observeEvent(input$btn_val_gsheet, { # loading screen for Google link generation - dcWaiter("show", msg = "Generating link...") + dcWaiter("show", msg = "Generating link...", color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) + filled_manifest <- switch(dca_schematic_api, + reticulate = manifest_populate_py(paste0(config$community, " ", input$dropdown_template), + inFile$raw()$datapath, + selected$schema()), + rest = manifest_populate(url=file.path(api_uri, "v1/manifest/populate"), + schema_url = data_model(), + title = paste0(config$community, " ", input$dropdown_template), + data_type = selected$schema(), + return_excel = FALSE, + csv_file = inFile$raw()$datapath), + "offline-no-gsheet-url") - filled_manifest <- metadata_model$populateModelManifest( - title = paste0(config$community, " ", input$dropdown_datatype), - manifestPath = inFile$raw()$datapath, - rootNode = selected$schema() - ) # rerender and change button to link - output$val_gsheet <- renderUI({ - HTML(paste0("Edit on the Google Sheet.")) - }) - + if (dca_schematic_api != "offline") { + output$val_gsheet <- renderUI({ + HTML(paste0("Edit on the Google Sheet.")) + }) + } dcWaiter("hide") }) + + # Offline version of downloading a failed manifest + mock_offline_manifest_2 <- tibble("column1"="fixed offline data") + output$downloadData_good <- downloadHandler( + filename = function() sprintf("%s.csv", input$dropdown_template), + content = function(file) { + write_csv(mock_offline_manifest_2, file) + } + ) ######## Submission Section ######## observeEvent(input$btn_submit, { # loading screen for submitting data - dcWaiter("show", msg = "Submitting...") - + dcWaiter("show", msg = "Submitting to Synapse. This may take a minute.", color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) if (is.null(selected$folder())) { @@ -411,8 +644,9 @@ shinyServer(function(input, output, session) { # abort submission if no folder selected req(selected$folder()) - tmp_out_dir <- "./manifest" - tmp_file_path <- file.path(tmp_out_dir, "synapse_storage_manifest.csv") + manifest_filename <- sprintf("%s_%s.csv", manifest_basename, tolower(selected$schema())) + tmp_out_dir <- tempdir() + tmp_file_path <- file.path(tmp_out_dir, manifest_filename) dir.create(tmp_out_dir, showWarnings = FALSE) # reads file csv again @@ -420,18 +654,24 @@ shinyServer(function(input, output, session) { # If a file-based component selected (define file-based components) note for future # the type to filter (eg file-based) on could probably also be a config choice - display_names <- config_schema$manifest_schemas$display_name[config_schema$manifest_schemas$type == "file"] + display_names <- config_schema()$manifest_schemas$display_name[config_schema()$manifest_schemas$type == "file"] - if (input$dropdown_datatype %in% display_names) { + if (input$dropdown_template %in% display_names) { # make into a csv or table for file-based components already has entityId if ("entityId" %in% colnames(submit_data)) { + # Convert this to JSON instead and submit write.csv(submit_data, file = tmp_file_path, quote = TRUE, row.names = FALSE, na = "" ) } else { - file_list <- syn_store$getFilesInStorageDataset(selected$folder()) - data_list$files(list2Vector(file_list)) + file_list_raw <- switch(dca_schematic_api, + reticulate = storage_dataset_files_py(selected$folder()), + rest = storage_dataset_files(url=file.path(api_uri, "v1/storage/dataset/files"), + asset_view = selected$master_asset_view(), + dataset_id = selected$folder(), + input_token=access_token)) + data_list$files(list2Vector(file_list_raw)) # better filename checking is needed # TODO: crash if no file existing @@ -439,21 +679,32 @@ shinyServer(function(input, output, session) { # adds entityID, saves it as synapse_storage_manifest.csv, then associates with synapse files colnames(files_df) <- c("entityId", "Filename") files_entity <- inner_join(submit_data, files_df, by = "Filename") - + # convert this to JSON instead and submit write.csv(files_entity, file = tmp_file_path, quote = TRUE, row.names = FALSE, na = "" ) } - + # associates metadata with data and returns manifest id - manifest_id <- syn_store$associateMetadataWithFiles( - schemaGenerator = schema_generator, - metadataManifestPath = tmp_file_path, - datasetId = selected$folder(), - manifest_record_type = "table", - restrict_manifest = FALSE - ) + manifest_id <- switch(dca_schematic_api, + reticulate = model_submit_py(schema_generator, + tmp_file_path, + selected$folder(), + "table", + FALSE), + rest = model_submit(url=file.path(api_uri, "v1/model/submit"), + schema_url = data_model(), + data_type = selected$schema(), + dataset_id = selected$folder(), + input_token = access_token, + restrict_rules = FALSE, + file_name = tmp_file_path, + asset_view = selected$master_asset_view(), + use_schema_label=dcc_config_react()$submit_use_schema_labels, + manifest_record_type="table", + table_manipulation=dcc_config_react()$submit_table_manipulation) + ) manifest_path <- tags$a(href = paste0("https://www.synapse.org/#!Synapse:", manifest_id), manifest_id, target = "_blank") # add log message @@ -463,7 +714,7 @@ shinyServer(function(input, output, session) { if (startsWith(manifest_id, "syn") == TRUE) { dcWaiter("hide") nx_report_success("Success!", HTML(paste0("Manifest submitted to: ", manifest_path))) - + # clean up old inputs/results sapply(clean_tags, FUN = hide) reset("inputFile-file") @@ -477,18 +728,30 @@ shinyServer(function(input, output, session) { } } else { # if not file-based type template + # convert this to JSON and submit write.csv(submit_data, file = tmp_file_path, quote = TRUE, row.names = FALSE, na = "" ) # associates metadata with data and returns manifest id - manifest_id <- syn_store$associateMetadataWithFiles( - schemaGenerator = schema_generator, - metadataManifestPath = tmp_file_path, - datasetId = selected$folder(), - manifest_record_type = "table", - restrict_manifest = FALSE + manifest_id <- switch(dca_schematic_api, + reticulate = model_submit_py(schema_generator, + tmp_file_path, + selected$folder(), + "table", + FALSE), + rest = model_submit(url=file.path(api_uri, "v1/model/submit"), + schema_url = data_model(), + data_type = selected$schema(), + dataset_id = selected$folder(), + input_token = access_token, + restrict_rules = FALSE, + file_name = tmp_file_path, + asset_view = selected$master_asset_view(), + use_schema_label=dcc_config_react()$submit_use_schema_labels, + manifest_record_type="table", + table_manipulation=dcc_config_react()$submit_table_manipulation) ) manifest_path <- tags$a(href = paste0("https://www.synapse.org/#!Synapse:", manifest_id), manifest_id, target = "_blank") diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 00000000..1611dcc8 --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,3 @@ +library(testthat) + +test_check("datacurator") diff --git a/tests/testthat/test_schematic_rest_api.R b/tests/testthat/test_schematic_rest_api.R new file mode 100644 index 00000000..0fa2a1a1 --- /dev/null +++ b/tests/testthat/test_schematic_rest_api.R @@ -0,0 +1,136 @@ +context("test schematic rest api wrappers") + +### Test that schematic server is online. Make sure schematic_url matches the actual +### schematic server URL https://github.com/Sage-Bionetworks/schematic/tree/develop/api +### If not available, skip these tests. + +schematic_url <- "https://schematic.api.sagebionetworks.org" +ping <- try(httr::GET(schematic_url), silent = TRUE) +skip_it <- function(skip=ping) { + if (inherits(ping, "try-error")) skip(sprintf("schematic server URL unavailable (%s). Is it running locally?", schematic_url)) #nolint +} + +schema_url <- "https://raw.githubusercontent.com/Sage-Bionetworks/data-models/main/example.model.jsonld" +pass_csv <- system.file("testdata", "HTAN-Biospecimen-Tier-1-2-pass.csv", + package = "datacurator") +fail_csv <- system.file("testdata", "HTAN-Biospecimen-Tier-1-2-fail.csv", + package = "datacurator") + +test_that("manifest_generate returns a URL if sucessful", { + skip_it() + + url <- manifest_generate(url=file.path(schematic_url, "v1/manifest/generate"), + schema_url = schema_url, input_token = Sys.getenv("SNYAPSE_PAT"), + title="Test biospecimen", data_type="Biospecimen", + use_annotations = FALSE, + dataset_id="syn33715357", asset_view="syn33715412", + output_format = "google_sheet") + expect_true(grepl("^https://docs.google", url)) +}) + +test_that("manifest_generate returns an xlsx", { + skip_it() + + xlsx <- manifest_generate(title="Test biospecimen", data_type="Biospecimen", + asset_view="syn33715412", output_format="excel") + +}) + +test_that("manifest_populate returns a google sheet link with records filled", { + skip_it() + req <- manifest_populate(data_type="Biospecimen", title="Example", + csv_file = pass_csv) +}) + +test_that("manifest_validate passes and fails correctly", { + skip_it() + + pass <- manifest_validate(data_type="Biospecimen", csv_file=pass_csv) + expect_identical(pass, list()) + + fail <- manifest_validate(data_type="Biospecimen", csv_file=fail_csv) + expect_true(length(unlist(fail)) > 0L) +}) + +test_that("model_submit successfully uploads to synapse", { + skip_it() + + submit <- model_submit(url=file.path(schematic_url,"v1/model/submit"), + schema_url = schema_url, + data_type="Biospecimen", dataset_id="syn20977135", + restrict_rules = FALSE, input_token=Sys.getenv("SYNAPSE_PAT"), + asset_view="syn33715412", file_name=pass_csv, + use_schema_label = TRUE, manifest_record_type="table", + table_manipulation="replace" + ) + expect_true(submit) +}) + +test_that("storage_project_datasets returns available datasets", { + skip_it() + storage_project_datasets(asset_view="syn23643253", + project_id="syn26251192", + input_token=Sys.getenv("SYNAPSE_PAT")) +}) + +test_that("storage_projects returns available projects", { + skip_it() + storage_projects(url=file.path(schematic_url, "v1/storage/project/datasets"), + asset_view="syn23643253", + input_token=Sys.getenv("SYNAPSE_PAT")) +}) + +test_that("storage_dataset_files returns files", { + skip_it() + storage_dataset_files(asset_view = "syn23643253", + dataset_id = "syn23643250", + input_token=Sys.getenv("SYNAPSE_PAT")) +}) + +test_that("model_component_requirements returns list of required components", { + skip_it() + good <- model_component_requirements(url="http://localhost:3001/v1/model/component-requirements", + schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", + source_component="Patient", + as_graph = FALSE) + expect_equal(length(good), 8L) + + expect_error(model_component_requirements(url="http://localhost:3001/v1/model/component-requirements", + schema_url="https://aaaabad.url.jsonld", + source_component="Patient", + as_graph = FALSE)) + +}) + +test_that("manifest_download returns a csv.", { + skip_it() + csv <- manifest_download(input_token=Sys.getenv("SYNAPSE_PAT"), + asset_view="syn28559058", + dataset_id="syn28268700") + exp <- setNames(c("BulkRNA-seqAssay", "CSV/TSV", "Sample_A", "GRCm38", NA, 2022L, "syn28278954"), + c("Component", "File Format", "Filename", "Genome Build", "Genome FASTA", "Sample ID", "entityId")) + expect_equal(unlist(csv), exp) +}) + +test_that("get_asset_view_table returns asset view table", { + skip_it() + av <- get_asset_view_table(input_token = Sys.getenv("SYNAPSE_PAT"), + asset_view="syn23643253") + storage_tbl <- subset(av, av$name == "synapse_storage_manifest.csv") + expect_true(inherits(av, "data.frame"), "name" %in% names(av)) +}) + +test_that("asset_tables returns a data.frame", { + skip_it() + tst <- get_asset_view_table(url=file.path(schematic_url, "v1/storage/assets/tables"), + asset_view = "syn28559058", + input_token = Sys.getenv("SYNAPSE_TOKEN"), + as_json=TRUE) + expect_identical(nrow(tst), 3L) + + tst2 <- get_asset_view_table(url=file.path(schematic_url, "v1/storage/assets/tables"), + asset_view = "syn28559058", + input_token = Sys.getenv("SYNAPSE_TOKEN"), + as_json=FALSE) + expect_identical(nrow(tst2), 3L) +}) diff --git a/tests/testthat/test_synapse_rest_api.R b/tests/testthat/test_synapse_rest_api.R new file mode 100644 index 00000000..e7a91124 --- /dev/null +++ b/tests/testthat/test_synapse_rest_api.R @@ -0,0 +1,36 @@ +context("test synapse rest api wrappers") + +test_that("synapse_user_profile returns list with successful auth", { + req <- synapse_user_profile(auth=Sys.getenv("SYNAPSE_PAT")) + expect_true(all(c("ownerId", "userName") %in% names(req))) +}) + +test_that("synapse_user_profile bad auth token returns message", { + req <- synapse_user_profile(auth="bad token") + expect_identical(req, list(reason="Invalid access token")) +}) + +test_that("synapse_user_profile returns list with NULL auth", { + req <- synapse_user_profile(auth=NULL) + req_name <- req$userName + expect_identical(req_name, "anonymous") +}) + +test_that("is_certified returns TRUE or FALSE", { + + expect_true(synapse_is_certified(auth=Sys.getenv("SYNAPSE_PAT"))) + expect_true(synapse_is_certified(auth=NULL)) + expect_false(synapse_is_certified(auth="bad auth")) + +}) + +test_that("get returns a tibble or error", { + + good_req <- synapse_get(id="syn23643255", auth=Sys.getenv("SYNAPSE_PAT")) + expect_true(nrow(good_req) == 1) + + expect_error(synapse_get(id="bad", auth=Sys.getenv("SYNAPSE_PAT"))) + expect_error(synapse_get(id=NULL, auth=Sys.getenv("SYNAPSE_PAT"))) + expect_error(synapse_get(id="bad", auth="bad")) + +}) diff --git a/tests/testthat/test_utils.R b/tests/testthat/test_utils.R new file mode 100644 index 00000000..2c2f430b --- /dev/null +++ b/tests/testthat/test_utils.R @@ -0,0 +1,7 @@ +context("Test utils") + +testthat("parse_env_var handles empty string", { + expect_error(parse_env_var(""), "delimiter not in") + expect_error(parse_env_var(Sys.getenv(".fake_env")), "delimiter not in") +}) + \ No newline at end of file diff --git a/ui.R b/ui.R index 986961dd..92534dde 100644 --- a/ui.R +++ b/ui.R @@ -21,7 +21,7 @@ ui <- shinydashboardPlus::dashboardPage( dropdownBlock( id = "header_selection_dropdown", title = "Selection", - icon = icon("sliders"), + icon = icon("sliders-h"), badgeStatus = "info", fluidRow( lapply(dropdown_types, function(x) { @@ -34,30 +34,31 @@ ui <- shinydashboardPlus::dashboardPage( ) ) }), - actionButton("btn_header_update", NULL, icon("rotate"), class = "btn-shiny-effect") + actionButton("btn_header_update", NULL, icon("sync-alt"), class = "btn-shiny-effect") ) ) ), - tags$li( - class = "dropdown", id = "HTAN_logo", - tags$a( - href = "https://synapse.org/", - target = "_blank", - tags$img( - height = "40px", alt = "SYNAPSE LOGO", - src = "img/synapse_logo.png" - ) - ) - ) + uiOutput("logo") ), dashboardSidebar( width = 250, sidebarMenu( id = "tabs", + # uiOutput("title"), + # menuItem( + # "Instructions", + # tabName = "tab_instructions", + # icon = icon("book-open") + # ), + menuItem( + "Select DCC", + tabName = "tab_asset_view", + icon = icon("database") + ), menuItem( "Select your Dataset", tabName = "tab_data", - icon = icon("arrow-pointer") + icon = icon("mouse-pointer") ), menuItem( "Get Metadata Template", @@ -72,8 +73,7 @@ ui <- shinydashboardPlus::dashboardPage( # add sidebar footer here tags$a( id = "sidebar_footer", `data-toggle` = "tab", - tags$div(icon("heart")), - tags$footer(HTML('Powered by and Sage Bionetworks')) + tags$footer(HTML(' Powered by and Sage Bionetworks')) ) ) ), @@ -83,11 +83,56 @@ ui <- shinydashboardPlus::dashboardPage( singleton(includeScript("www/js/readCookie.js")), tags$script(htmlwidgets::JS("setTimeout(function(){history.pushState({}, 'Data Curator', window.location.pathname);},2000);")) ), + uiOutput("sass"), # load dependencies use_notiflix_report(width = "400px"), use_waiter(), + #dcamodules::use_dca(), tabItems( - # data selection & dashboard tab content + # First tab content + # tabItem( + # tabName = "tab_instructions", + # h2("Instructions for the Data Curator App (DCA):"), + # h3( + # "1. Go to", + # strong("Select your Dataset"), + # "tab - select your project; choose your folder and metadata template type matching your metadata." + # ), + # h3( + # "2. Go to", + # strong("Get Metadata Template"), + # "tab - click on the link to generate the metadata template, then fill out and download the file as a CSV. If you already have an annotated metadata template, you may skip this step." + # ), + # h3( + # "3. Go to", + # strong("Submit and Validate Metadata"), + # "tab - upload your filled CSV and validate your metadata. If you receive errors correct them, reupload your CSV, and revalidate until you receive no more errors. When your metadata is valid, you will be able to see a 'Submit' button. Press it to submit your metadata." + # ), + # switchTabUI("switchTab1", direction = "right") + # ), + # second tab content + tabItem( + tabName = "tab_asset_view", + #h2("Select the asset view"), + fluidRow( + box( + id = "box_pick_asset_view", + status = "primary", + width = 6, + title = "Select a DCC: ", + selectInput( + inputId = "dropdown_asset_view", + label = NULL, #"Asset View:", + choices = setNames(dcc_config$project_name, + dcc_config$synapse_asset_view)#"Generating..." + ), + actionButton("btn_asset_view", "Click to confirm", + class = "btn-primary-color" + ) + ) + )#, + #switchTabUI("switchTab1", direction = "right") # remove arrow from assetview page. + ), tabItem( tabName = "tab_data", h2("Set Dataset and Data Type for Curation"), @@ -117,20 +162,21 @@ ui <- shinydashboardPlus::dashboardPage( width = 6, title = "Choose a Data Type: ", selectInput( - inputId = "dropdown_datatype", - label = "Data Type:", + inputId = "dropdown_template", + label = "Data Type Template:", choices = "Generating..." ) - )#, - #dashboardUI("dashboard") + ), + if (dca_schematic_api != "offline" && Sys.getenv("DCA_COMPLIANCE_DASHBOARD")==TRUE) dashboardUI("dashboard") ), - switchTabUI("switchTab1", direction = "right") + switchTabUI("switchTab2", direction = "right") ), - # template tab item + # Third tab item tabItem( tabName = "tab_template", useShinyjs(), h2("Download Template for Selected Folder"), + if (Sys.getenv("DCA_MANIFEST_OUTPUT_FORMAT") != "excel") { fluidRow( box( title = "Get Link, Annotate, and Download Template as CSV", @@ -151,12 +197,33 @@ ui <- shinydashboardPlus::dashboardPage( htmlOutput("text_template") ) ), + helpText("This link will lead to an empty template or your previously submitted template with new files if applicable.") + ) + )}else{ + fluidRow( + box( + title = "Or download data as an Excel sheet", + status = "primary", + width = 12, + downloadButton("downloadData", "Download Excel Spreadsheet."), + hidden( + div( + id = "div_template_warn_xls", + height = "100%", + htmlOutput("text_template_warn_xls") + ), + div( + id = "div_template_xls", + height = "100%", + htmlOutput("text_template_xls") + ) + ), helpText("This link will leads to an empty template or your previously submitted template with new files if applicable.") ) - ), - switchTabUI("switchTab2", direction = "both") + )}, + switchTabUI("switchTab3", direction = "both") ), - # upload & submit tab content + # Fourth tab content tabItem( tabName = "tab_upload", h2("Submit & Validate a Filled Metadata Template"), @@ -187,6 +254,7 @@ ui <- shinydashboardPlus::dashboardPage( ), DTableUI("tbl_validate"), uiOutput("val_gsheet"), + uiOutput("dl_manifest"), helpText( HTML("If you have an error, please try editing locally or on google sheet. Reupload your CSV and press the validate button as needed.") @@ -199,7 +267,7 @@ ui <- shinydashboardPlus::dashboardPage( uiOutput("submit") ) ), - switchTabUI("switchTab3", direction = "left") + switchTabUI("switchTab4", direction = "left") ) ), # waiter loading screen @@ -208,12 +276,16 @@ ui <- shinydashboardPlus::dashboardPage( ) uiFunc <- function(req) { + if (dca_schematic_api == "offline") { + message("dca_schematic_api set to offline. Running in offline mode.") + return(ui) + } if (!has_auth_code(parseQueryString(req$QUERY_STRING))) { authorization_url <- oauth2.0_authorize_url(api, app, scope = scope) - return(tags$script(HTML(sprintf( - "location.replace(\"%s\");", - authorization_url - )))) + return(tags$script(HTML(sprintf( + "location.replace(\"%s\");", + authorization_url + )))) } else { ui } diff --git a/www/img/INCLUDE DCC Logo-01.png b/www/img/INCLUDE DCC Logo-01.png new file mode 100644 index 0000000000000000000000000000000000000000..3f19beed0d482cd4684e1f1d5a30bdefc6fb3963 GIT binary patch literal 185531 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 NKT&>MC~X}0{{Ua2|3?4- literal 0 HcmV?d00001 diff --git a/www/img/Logo_Sage_Logomark.png b/www/img/Logo_Sage_Logomark.png new file mode 100644 index 0000000000000000000000000000000000000000..722ee5c483c83ec4c29d7858f84f4384d96b1796 GIT binary patch literal 4873 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 vKow;x&CDH0qUB$_8w;6lGp&=_iX literal 0 HcmV?d00001 diff --git a/www/img/cckp_logo.png b/www/img/cckp_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..19b12ad37ca38a2911adcbac2f2e21aec773445c GIT binary patch literal 14950 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 li > a { - color: #000; - background-color: transparent; - border: 0.5px solid rgba($primary_col, 0.4); - - &:hover { - color: #fff; - background-color: rgba($primary_col, 0.4); - cursor: pointer; - } - } - & > li.active > a { color: #fff !important; background-color: rgba($primary_col, 0.8) !important; - border: none !important; } - } - .waiter-overlay { - margin-top: -1px; // match better with tab button border + & > li > a { + color: #000; + background-color: transparent; + } } } diff --git a/www/scss/section/dashboard/_selectedDatatypeTab.scss b/www/scss/section/dashboard/_selectedDatatypeTab.scss index 2fcea2a9..cbd99cc1 100644 --- a/www/scss/section/dashboard/_selectedDatatypeTab.scss +++ b/www/scss/section/dashboard/_selectedDatatypeTab.scss @@ -1,18 +1,7 @@ .selectedDataTypeTab-container { - .instruction-container { - display: flex; - flex-direction: column; - margin-top: 12px; - - li { - min-height: 36px; - padding: 8px; - margin-top: 12px; - } - } - .network-container { - margin-top: 32px; + .bottom-title-container { + display: flex; } .legend-container { @@ -29,3 +18,4 @@ } } } + diff --git a/www/scss/section/dashboard/_selectedProjectTab.scss b/www/scss/section/dashboard/_selectedProjectTab.scss index abea206b..e35591ce 100644 --- a/www/scss/section/dashboard/_selectedProjectTab.scss +++ b/www/scss/section/dashboard/_selectedProjectTab.scss @@ -3,19 +3,6 @@ display: flex; align-items: center; } - - #dashboard-tab-selected-project-checkbox-evaluate { - margin-top: 24px; - } - - .shiny-input-checkboxgroup.shiny-input-container-inline - label - ~ .shiny-options-group - > label - > span { - color: $primary_col; - font-weight: 600; - } } @media (max-width: $desktop) { diff --git a/www/template_config/config.json b/www/template_config/config.json new file mode 100644 index 00000000..f85b5768 --- /dev/null +++ b/www/template_config/config.json @@ -0,0 +1,31 @@ +{ + "manifest_schemas": [ + { + "display_name": "Biospecimen", + "schema_name": "Biospecimen", + "type": "record" + }, + { + "display_name": "Patient", + "schema_name": "Patient", + "type": "record" + }, + { + "display_name": "Bulk RNA-seq Assay", + "schema_name": "BulkRNA-seqAssay", + "type": "file" + }, + { + "display_name": "Other Assay", + "schema_name": "OtherAssay", + "type": "file" + }, + { + "display_name": "MockComponent", + "schema_name": "MockComponent", + "type": "record" + } + ], + "service_version": "v23.1.1", + "schema_version": "" +} \ No newline at end of file diff --git a/www/template_config/config_offline.json b/www/template_config/config_offline.json new file mode 100644 index 00000000..2b7b76ed --- /dev/null +++ b/www/template_config/config_offline.json @@ -0,0 +1,16 @@ +{ + "manifest_schemas": [ + { + "display_name": "Datatype A - record", + "schema_name": "DatatypeA", + "type": "record" + }, + { + "display_name": "Datatype B - file", + "schema_name": "DatatypeB", + "type": "file" + } + ], + "service_version": "v23.1.1", + "schema_version": "" +} \ No newline at end of file diff --git a/www/template_config/example_config.json b/www/template_config/example_config.json new file mode 100644 index 00000000..f85b5768 --- /dev/null +++ b/www/template_config/example_config.json @@ -0,0 +1,31 @@ +{ + "manifest_schemas": [ + { + "display_name": "Biospecimen", + "schema_name": "Biospecimen", + "type": "record" + }, + { + "display_name": "Patient", + "schema_name": "Patient", + "type": "record" + }, + { + "display_name": "Bulk RNA-seq Assay", + "schema_name": "BulkRNA-seqAssay", + "type": "file" + }, + { + "display_name": "Other Assay", + "schema_name": "OtherAssay", + "type": "file" + }, + { + "display_name": "MockComponent", + "schema_name": "MockComponent", + "type": "record" + } + ], + "service_version": "v23.1.1", + "schema_version": "" +} \ No newline at end of file diff --git a/www/template_config/htan_config.json b/www/template_config/htan_config.json new file mode 100644 index 00000000..113b1e9f --- /dev/null +++ b/www/template_config/htan_config.json @@ -0,0 +1,306 @@ +{ + "manifest_schemas": [ + { + "display_name": "Patient", + "schema_name": "Patient", + "type": "record" + }, + { + "display_name": "Demographics", + "schema_name": "Demographics", + "type": "record" + }, + { + "display_name": "Family History", + "schema_name": "FamilyHistory", + "type": "record" + }, + { + "display_name": "Exposure", + "schema_name": "Exposure", + "type": "record" + }, + { + "display_name": "Follow Up", + "schema_name": "FollowUp", + "type": "record" + }, + { + "display_name": "Diagnosis", + "schema_name": "Diagnosis", + "type": "record" + }, + { + "display_name": "Therapy", + "schema_name": "Therapy", + "type": "record" + }, + { + "display_name": "Molecular Test", + "schema_name": "MolecularTest", + "type": "record" + }, + { + "display_name": "Biospecimen", + "schema_name": "Biospecimen", + "type": "record" + }, + { + "display_name": "Clinical Data Tier 2", + "schema_name": "ClinicalDataTier2", + "type": "record" + }, + { + "display_name": "SRRS Clinical Data Tier 2", + "schema_name": "SRRSClinicalDataTier2", + "type": "record" + }, + { + "display_name": "Lung Cancer Tier 3", + "schema_name": "LungCancerTier3", + "type": "record" + }, + { + "display_name": "Colorectal Cancer Tier 3", + "schema_name": "ColorectalCancerTier3", + "type": "record" + }, + { + "display_name": "Breast Cancer Tier 3", + "schema_name": "BreastCancerTier3", + "type": "record" + }, + { + "display_name": "Neuroblastoma and Glioma Tier 3", + "schema_name": "NeuroblastomaandGliomaTier3", + "type": "record" + }, + { + "display_name": "Acute Lymphoblastic Leukemia Tier 3", + "schema_name": "AcuteLymphoblasticLeukemiaTier3", + "type": "record" + }, + { + "display_name": "Ovarian Cancer Tier 3", + "schema_name": "OvarianCancerTier3", + "type": "record" + }, + { + "display_name": "Prostate Cancer Tier 3", + "schema_name": "ProstateCancerTier3", + "type": "record" + }, + { + "display_name": "Sarcoma Tier 3", + "schema_name": "SarcomaTier3", + "type": "record" + }, + { + "display_name": "Pancreatic Cancer Tier 3", + "schema_name": "PancreaticCancerTier3", + "type": "record" + }, + { + "display_name": "Melanoma Tier 3", + "schema_name": "MelanomaTier3", + "type": "record" + }, + { + "display_name": "SRRS Biospecimen", + "schema_name": "SRRSBiospecimen", + "type": "record" + }, + { + "display_name": "Other Assay", + "schema_name": "OtherAssay", + "type": "file" + }, + { + "display_name": "scRNA-seq Level 1", + "schema_name": "ScRNA-seqLevel1", + "type": "file" + }, + { + "display_name": "scRNA-seq Level 2", + "schema_name": "ScRNA-seqLevel2", + "type": "file" + }, + { + "display_name": "scRNA-seq Level 3", + "schema_name": "ScRNA-seqLevel3", + "type": "file" + }, + { + "display_name": "scRNA-seq Level 4", + "schema_name": "ScRNA-seqLevel4", + "type": "file" + }, + { + "display_name": "Bulk RNA-seq Level 1", + "schema_name": "BulkRNA-seqLevel1", + "type": "file" + }, + { + "display_name": "Bulk RNA-seq Level 2", + "schema_name": "BulkRNA-seqLevel2", + "type": "file" + }, + { + "display_name": "Bulk RNA-seq Level 3", + "schema_name": "BulkRNA-seqLevel3", + "type": "file" + }, + { + "display_name": "Bulk WES Level 1", + "schema_name": "BulkWESLevel1", + "type": "file" + }, + { + "display_name": "Bulk WES Level 2", + "schema_name": "BulkWESLevel2", + "type": "file" + }, + { + "display_name": "Bulk WES Level 3", + "schema_name": "BulkWESLevel3", + "type": "file" + }, + { + "display_name": "scATAC-seq Level 1", + "schema_name": "ScATAC-seqLevel1", + "type": "file" + }, + { + "display_name": "scATAC-seq Level 2", + "schema_name": "ScATAC-seqLevel2", + "type": "file" + }, + { + "display_name": "scATAC-seq Level 3", + "schema_name": "ScATAC-seqLevel3", + "type": "file" + }, + { + "display_name": "scmC-seq Level 1", + "schema_name": "ScmC-seqLevel1", + "type": "file" + }, + { + "display_name": "scmC-seq Level 2", + "schema_name": "ScmC-seqLevel2", + "type": "file" + }, + { + "display_name": "scATAC-seq Level 4", + "schema_name": "ScATAC-seqLevel4", + "type": "file" + }, + { + "display_name": "scDNA-seq Level 1", + "schema_name": "ScDNA-seqLevel1", + "type": "file" + }, + { + "display_name": "scDNA-seq Level 2", + "schema_name": "ScDNA-seqLevel2", + "type": "file" + }, + { + "display_name": "Bulk Methylation-seq Level 1", + "schema_name": "BulkMethylation-seqLevel1", + "type": "file" + }, + { + "display_name": "Bulk Methylation-seq Level 2", + "schema_name": "BulkMethylation-seqLevel2", + "type": "file" + }, + { + "display_name": "Bulk Methylation-seq Level 3", + "schema_name": "BulkMethylation-seqLevel3", + "type": "file" + }, + { + "display_name": "Imaging Level 1", + "schema_name": "ImagingLevel1", + "type": "file" + }, + { + "display_name": "Imaging Level 2", + "schema_name": "ImagingLevel2", + "type": "file" + }, + { + "display_name": "Imaging Level 3 Segmentation", + "schema_name": "ImagingLevel3Segmentation", + "type": "file" + }, + { + "display_name": "Imaging Level 3 Image", + "schema_name": "ImagingLevel3Image", + "type": "file" + }, + { + "display_name": "Imaging Level 3 Channels", + "schema_name": "ImagingLevel3Channels", + "type": "record" + }, + { + "display_name": "10x Visium Spatial Transcriptomics - RNA-seq Level 1", + "schema_name": "10xVisiumSpatialTranscriptomics-RNA-seqLevel1", + "type": "file" + }, + { + "display_name": "10x Visium Spatial Transcriptomics - RNA-seq Level 2", + "schema_name": "10xVisiumSpatialTranscriptomics-RNA-seqLevel2", + "type": "file" + }, + { + "display_name": "10x Visium Spatial Transcriptomics - Auxiliary Files Level 2", + "schema_name": "10xVisiumSpatialTranscriptomics-AuxiliaryFilesLevel2", + "type": "file" + }, + { + "display_name": "10x Visium Spatial Transcriptomics - RNA-seq Level 3", + "schema_name": "10xVisiumSpatialTranscriptomics-RNA-seqLevel3", + "type": "file" + }, + { + "display_name": "Imaging Level 4", + "schema_name": "ImagingLevel4", + "type": "file" + }, + { + "display_name": "SRRS Imaging Level 2", + "schema_name": "SRRSImagingLevel2", + "type": "file" + }, + { + "display_name": "RPPA Level 2", + "schema_name": "RPPALevel2", + "type": "file" + }, + { + "display_name": "HTAN RPPA Antibody Table", + "schema_name": "HTANRPPAAntibodyTable", + "type": "file" + }, + { + "display_name": "Mass Spectrometry Level 1", + "schema_name": "MassSpectrometryLevel1", + "type": "file" + }, + { + "display_name": "RPPA Level 3", + "schema_name": "RPPALevel3", + "type": "file" + }, + { + "display_name": "RPPA Level 4", + "schema_name": "RPPALevel4", + "type": "file" + } + ], + "service_version": "v23.1.1", + "schema_version": "" +} \ No newline at end of file diff --git a/www/template_config/include_config.json b/www/template_config/include_config.json new file mode 100644 index 00000000..89df2c95 --- /dev/null +++ b/www/template_config/include_config.json @@ -0,0 +1,12 @@ +{ + "manifest_schemas": [ + {"display_name": "Study", "schema_name": "Study", "type": "record"}, + {"display_name": "Participant", "schema_name": "Participant", "type": "record"}, + {"display_name": "Biospecimen", "schema_name": "Biospecimen", "type": "record"}, + {"display_name": "Condition", "schema_name": "Condition", "type": "record"}, + {"display_name": "Data File", "schema_name": "DataFile", "type": "file"} + + ], + "main_fileview" : "syn30109515", + "community" : "INCLUDE" +} diff --git a/www/template_config/mc2_config.json b/www/template_config/mc2_config.json new file mode 100644 index 00000000..f3da2a0c --- /dev/null +++ b/www/template_config/mc2_config.json @@ -0,0 +1,131 @@ +{ + "manifest_schemas": [ + { + "display_name": "Tool Grant", + "schema_name": "ToolGrant", + "type": "record" + }, + { + "display_name": "Tool", + "schema_name": "Tool", + "type": "record" + }, + { + "display_name": "Grant", + "schema_name": "Grant", + "type": "record" + }, + { + "display_name": "Publication Grant", + "schema_name": "PublicationGrant", + "type": "record" + }, + { + "display_name": "Publication", + "schema_name": "Publication", + "type": "record" + }, + { + "display_name": "Dataset Grant", + "schema_name": "DatasetGrant", + "type": "record" + }, + { + "display_name": "Dataset", + "schema_name": "Dataset", + "type": "record" + }, + { + "display_name": "Consortium Grant", + "schema_name": "ConsortiumGrant", + "type": "record" + }, + { + "display_name": "Consortium", + "schema_name": "Consortium", + "type": "record" + }, + { + "display_name": "Project", + "schema_name": "Project", + "type": "record" + }, + { + "display_name": "Person Consortium", + "schema_name": "PersonConsortium", + "type": "record" + }, + { + "display_name": "Person", + "schema_name": "Person", + "type": "record" + }, + { + "display_name": "Theme Grant", + "schema_name": "ThemeGrant", + "type": "record" + }, + { + "display_name": "Theme", + "schema_name": "Theme", + "type": "record" + }, + { + "display_name": "Institution Grant", + "schema_name": "InstitutionGrant", + "type": "record" + }, + { + "display_name": "Institution", + "schema_name": "Institution", + "type": "record" + }, + { + "display_name": "File Grant", + "schema_name": "FileGrant", + "type": "record" + }, + { + "display_name": "File", + "schema_name": "File", + "type": "record" + }, + { + "display_name": "Tool View", + "schema_name": "ToolView", + "type": "record" + }, + { + "display_name": "Publication View", + "schema_name": "PublicationView", + "type": "record" + }, + { + "display_name": "Dataset View", + "schema_name": "DatasetView", + "type": "record" + }, + { + "display_name": "Person View", + "schema_name": "PersonView", + "type": "record" + }, + { + "display_name": "File View", + "schema_name": "FileView", + "type": "record" + }, + { + "display_name": "Grant View", + "schema_name": "GrantView", + "type": "record" + }, + { + "display_name": "Project View", + "schema_name": "ProjectView", + "type": "record" + } + ], + "service_version": "v22.11.2", + "schema_version": "v2.1.1" +} \ No newline at end of file From 9f7ccd0b21918162ff10e2a67f0fb31f32ee83a3 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Wed, 10 May 2023 09:03:16 -0700 Subject: [PATCH 04/40] Beta schematic rest api (#524) * add rsconnect * Don't source renv files. * Beta shinyapps deploy (#504) * show files to see if R/ directory is still present. * change devel branch in deployment * remove NAMESPACE and DESCRIPTION * install packages manually to avoid shinypop error. * add packages missing from rsconnect deploy error. * Add missing env vars to deployment script * Add template menu config env var to deployment workflow * Change url to testing1 * Add data model url env var to deployment. * Use single quotes * Fix typo to use .Renviron instead of .Rprofile. * Download and save data model before writing schematic config. * Revert listing files for debugging. * use download.file() instead of system(curl...) * Change branch in shinyapps deploy * Only use one DCC because shinyapps.io workers share the same reticulate environment. So different sessions can only use the same schematic config. * Add 'Step X' to sidebar tabs to guide users. * Revert add step X to sidebar. * Beta schematic rest api (#507) * Set up test instance for fork * Move parse_env_vars() and update_logo() to utils.R * Add test to parse_env_vars that checks if it errors out on empty string. * source files at top of global.R * Revert change to appName * Add naming logic back to deployment workflow * Beta schematic rest api (#508) * Set up test instance for fork * Move parse_env_vars() and update_logo() to utils.R * Add test to parse_env_vars that checks if it errors out on empty string. * source files at top of global.R * Revert change to appName * Add naming logic back to deployment workflow * Make manifest_url() reactive so link shows up correctly. * Beta submit body (#513) * Add 'Step X' to sidebar tabs to guide users. * Revert add step X to sidebar. * Update submit and validate to upload data in the request body, not header. * Copy R/schematic_rest_api.R to functions/ so it works on shinyappps.io * Update validate and submit calls to use a file name instead of passing a json object. * Add renv::isolate to Dockerfile * source renv activate on startup. * Simplify renv install and use jammy repo * Use updated shiny-base image that has libglpk-dev to avoid networkD3 error installing from binary. * Try renv and setting the repo to focal binaries. * Add renv::isolate inside of rsconnect deploy step * Only do deployApp() to test where deployment fails * try avoiding cache * use checkout v3 * use ubuntulatest and jammy repo. * Use focal once again. * update renv.lock * Update renv lock from ubuntu * Back to install-pkgs script * fix spacing * ubuntu 20.04 * checkout v3 and pandoc v2 * Don't source renv/activate.R * Remove R package install script to use renv * Update renv.lock for R4.1.3 * Use renv in workflow. restore and isolate in deploy step * Remove repo from Dockerfile because it's in the renv.lock file. * Use shinydashboardplus from github, not cran * need to source activate.R in Rprofile * Update renv activate to v0.17 * Update base image for stringi lib. * Update shiny-base image to fix stringi error * Update base image to afwillia/shiny-base:release-1.2 * Use same versioon of R as the AWS deployment. * Test removing renv calls from deployApp step * confirmed removing renv from deployApps crashes. Add renv::restore() back. * Install rsconnect in deployapps step * Remove rsconnect, remotes, and packrat from renv.lock because these are specific to shinyapps.io deployment workflow. * Add updated renv/activate.R and .gitignore * Revert changes to renv/activate.R because this breaks the shinyapps.io deployment * use sagebionetworks/shiny-base and install extra libs in DCA dockerfile * Try installing from source to avoid stringri error on jammy * Add rstudio jammy repo to dockerfile to speed up package installation. * Test overriding renv repo in workkflow to jammy binary * Still uses ubuntu20 cache, despite running on ubuntu22. What happens if we bypass_cache. * Add 'with' in setup-renv * Don't indent 'with' in setup-renv * shinydashboardPlus dependency fastmap fails to install because there is no 'GLIBCXX_3.4.29'. Abandon this quest and use previously functional setup. * Try updating glibc and avoid shinydashboardplus and fastmap error * Updating glibc-source did not work. Go back to existing workflow. * Update node for security patch * Remove default node then install from nodesource * Update renv gitignore with defaults from v0.17 * Beta dcc config (#515) * Add config file for DCC settings * Use dcc config file instead of env vars. * Use dcc config file in ui instead of env vars. * Use dcc config file in server instead of env vars. * Add theme colors to dcc config file * Change theme switching to use dcc config file instead of rds files. * Remove rds theme files. * rename csbc template file to mc2 * Make waiter background transparent. * Update waiter background to be slightly transparent * Update waiter landing text from ux feedback * Change waiter text and add things may take a minute. * welcome user by name not synapse username. * Remove env variables from shinyapps.io workflow that are in dcc_config.csv * Update staging app to use the schematic rest api service. * Don't install schematic with rest api deploy * Remove data model config from workflow since those files are hard coded. * Set host instead of port in Renviron. * Update README for new configuration options with environment variables. * Renviron instead of Rprofile. * Format files for github rendering * clean up readme for github formatting * Fixes FDS-73. An issue where validation was failing. * move shinyapps_deploy documentation to docs/ * Update link to shinyapps.io deployment docs * Update link to OAuth client setup docs * Update shinyapps.io deployment for the REST/reticulate version of DCA. * Fix extra characters in code chunks. * Add error checking for DCA_SCHEMATIC_API_TYPE env var * Update workflow to conditionally install python if using reticulate * Remove space between $ and {. * fix variable name in if statement * add shell: bash to venv activate * Write .Renviron after checking out repo * checkout repo first. * update system then checkout * try using setup-python for caching * Don't use setup-py yet * try caching venv * put IFs into one line * update python cache * try using setup-python with pip not pipenv * fix typo * set env path * update cache dir * update cache dir 2 * set hash files in key * update cache key * create cache key from release 23.1.1 schematic poetry lock * download with wget * fix expression * use double & * only cache venv.zip * move if statements * don't use pip cache dir * add pip cache dir * add pip cache dir 2 * add pip cache dir 4 * add pip download cache dir * print out pip version * move schematic install * Try installing schematicpy outside of venv for caching * upgrade pip * use default pip * create data model config * update testing1 instance with schematic rest api * hotfix when validationResult returns a warning to highlight particular columns, if the vector is length 0 then DTable quietly breaks. This results in the submit button not showing up. To address this, check the length of cells to highlight. If it's 0, skip highlighting. Need to determine why validationResult is not returning the correct cells to highlight. * add error checking to model_submit * write manifest to temp dir * Add error handling and table_manipulation arguments to model_submit. * Update tests for model_submit * Update model_submit arguments to use dcc_config and add arguments. * Redploy app with reticulate to staging instance. * Add restrict rules to validate * update validate tests * Update README * Don't cache python installation, it doesn't speed things up much. * Install schematicpy in its own step * use github_token in secrets context instead of github context to hopefully resolve an error with renv installing a github repo * upgrade to renv 0.17.2 * Print dca_schematic_rest_api env var to console for debugging on shinyapps.io * Check if just deploying an existing app speeds up deployment. * Switch names in asset view display the label instead of synapse id * Add synapse REST API wrappers for getting project data * Use synapse API instead of schematic to get storage projects. * Use synapse API instead of schematic to get the dataset folders within a project. * Use synapse API to get files in a dataset folder. * Fix storage_projects_files_py so it returns output. * remove debugging message * configuring and deploying app in same step does not change deploy times. * remove duplicate github_token in workflow * fix merge conflicts * Remove extra quote * Add toggle to download file to return data.frame * Update manifest/download * Download manifest from synapse api. * Update manifest_record_type in submit to table_and_file * Add optional code for using synapse REST api. * Show project name instead of synapse Id in asset view menu * Add roxygen documentation to synapse functions * Toggle synapse and schematic APIs by using DCA_SYNAPSE_PROJECT_API env var. * Move waiter screens on data selection page before any business logic is performed * Update testing instance with schematic REST API * Use dev schematic api service. * Don't use synapse API to download manifest file. * Update schematic rest api functions * make use_annotations FALSE instead of 'false' * set output format for manifest/generate * Add manifest output env var to shinyapps deployment * In waiter, display project name. * only create files vector if files are in dataset folder * Use updated shiny-base with longer http_keepalive_timeout * Use correct image repo * Use promises package * Use manifest_generate asynchronously with future_promise(). Need to test this on AWS with concurrent users. Also, need to add a waiter screen if successful * Add future package to renv * Load future package and create multisession plan * Move waiter screens to work with future_promise. * Add async call to model/validate. * Set up global.R to work in offline mode with synapse REST API and dcc_config.csv * Get offline mode to work with synapse REST API and dcc_config.csv. Let async schematic calls work in offline mode. * WIP move all of validation step into an async call. Need to add formatting validation table to async. * Set up submit for offline mode mock submission. * update shiny-baes image with simple_scheduler = 1 * Add async functionality to model_submit * Use shiny-base 1.4 which does not set simple_scheduler. * remove a reactive variable from inside a future_promises() statement. * Add test manifests. * Add a shiny server config file * Copy this repo's shiny server conf into the docker image * Create one tab for each menu, to add a waiter between each screen for async. * Add tabs to global.R * Refactor server to handle one tab per dropdown menu. Add async calls to synapse API. * Set default waiter sleep to 0. Sleep messes with concurrent users, causing unnecessary delays. * Have multiple observers for buttons with various priorities. * Remove async call from fast synapse api calls, but change the waiter default to not sleep. * Simplify waiter screen messages. * Fix bug in data_list if no files were downloaded from Synapse. * use all available cores for asynchronous calls. * Say asset view loading may take a minute in the waiter. MC2 took much longer than the demo. * Add async to synapse asset view scope call. It takes a long time with MC2. * Fix waiter screens after selecting asset view so they don't overlay. * Add async calls to synapse folder and file apis. * Disable the asset view button after it has been clicked. Then enable it only if the asset view dropdown has changed. This avoids a bug with the asynchronous code where the waiter will not disappear on subsequent clicks of the same button. * Disable project, template, and folder buttons after clicking. Enable them again if the input changes. This avoids a bug with the asynchronous code. * Remove header dropdown from UI. * Remove header selection bar from server. * Hide the side bar selectors so users progress through the app once tab at a time. Allow manifest download to trigger after clicking the download tab or proceed button from folder selection screen. * add config options for AD Knowledge Portal (#525) * temp not ignore config json and csv * AD template config * add AD config * add AD portal logo * re-ignore json and csv * fix typo * use json-ld for data model * Fix typo in include logo href * Add adknowledgeportal logo * Make tab icons unique * Various UI text tweaks to simplify layout * Give boxes in validation page an id so we can hide them. * Hide and show the validation tab boxes to guide users through the submission process * Add reminder below file upload to remove blank rows from csv. * Add reminder that spreadsheet apps may add extra rows to template. * Add help text to prompt user to click download template tab to continue. * Set waiter sleeps to 0 to help with concurrent users. * Only show submit box if validation passes. * Remove placeholder text for get template tab. * Use available cores for futures * Add manifest_record_type to dcc_config * In submit, use manifest_record_type from config file * Add manifest_record_type to dcc_config in README * Change text of download template header to show the template and folder * Set name of download template header based on selected folder and template * Use file_only instead of file for AD manifest_record_type. * Move the right button closer to the left sidebar * Remove the hover animation from disabled buttons. * Use schematic storage/project/datasets to get folders within a project. Don't use synapse entity children because it doesn't return the corect information for nested file views. * After selecting folder automatically switch to download template tab. * update csvInFile module to remove empty columns and rows. * Remove Synapse from waiter messages. * After trimming blank columns and rows from uploaded csv, write it so it can be passed to schematic submit endpoint. * Use shiny base image with updated node. * Don't deploy to shinyapps.io with AD configuration. * add config info for VEOIBD DCA (#526) * Disable dashboard for now. * Add VEOIBD logo * Clarify instructions in README * Add BTC * Fix bug where validation error table doesn't display. Show error message waiter for 2.5 seconds like current app does. * remove defunct code from ui. * Update UI description comments * Clean up indentation in UI code * On folder selection tab, remove note and button. * Clean up indentation in server code. * Suppress rowname warning on tibble in csv upload * don't show column type message reading dcc_config.csv * Clean up whitespace in global.R * Remove theme files from global.R * download template as excel by default. Opt in to google sheet. * Add DCA_SYNAPSE_PROJECT_API env var to global var. * add google sheet and excel download button in the same box, just display the button based on what is actually generated. * use dcc_config.csv instead of env var to generate a google sheet or excel. output a link or download button depending on what output format is specified. also, fix template name issue in google sheet * Add an example template config file for BTC. Use google_sheet for Demo instance template as a test. * remove browser statement from server. * Sort items in folder dropdown menu --------- Co-authored-by: Abby Vander Linden <11965371+avanlinden@users.noreply.github.com> Co-authored-by: Dan Lu <90745557+danlu1@users.noreply.github.com> --- .github/workflows/shinyapps_deploy.yml | 12 +- Dockerfile | 6 +- R/schematic_rest_api.R | 36 +- R/synapse_rest_api.R | 183 +++ README.md | 3 +- dcc_config.csv | 13 +- functions/dcWaiter.R | 2 +- functions/schematic_rest_api.R | 84 +- functions/schematic_reticulate.R | 2 +- functions/utils.R | 8 +- global.R | 36 +- .../biospecimen-record-example-model-fail.csv | 3 + .../biospecimen-record-example-model-pass.csv | 2 + modules/csvInfile.R | 12 +- modules/switchTab.R | 2 +- renv.lock | 15 +- renv/activate.R | 2 +- server.R | 1076 +++++++++-------- shiny-server.conf | 30 + ui.R | 416 +++---- www/img/ADKnowledgePortal.png | Bin 0 -> 41928 bytes www/img/VEOIBD Logo temp.png | Bin 0 -> 26451 bytes www/scss/basic/_button.scss | 3 +- www/template_config/VEOIBD_config.json | 31 + www/template_config/adkp_config.json | 46 + 25 files changed, 1204 insertions(+), 819 deletions(-) create mode 100644 inst/testdata/biospecimen-record-example-model-fail.csv create mode 100644 inst/testdata/biospecimen-record-example-model-pass.csv create mode 100644 shiny-server.conf create mode 100644 www/img/ADKnowledgePortal.png create mode 100644 www/img/VEOIBD Logo temp.png create mode 100644 www/template_config/VEOIBD_config.json create mode 100644 www/template_config/adkp_config.json diff --git a/.github/workflows/shinyapps_deploy.yml b/.github/workflows/shinyapps_deploy.yml index 6fabe65f..39c68424 100644 --- a/.github/workflows/shinyapps_deploy.yml +++ b/.github/workflows/shinyapps_deploy.yml @@ -6,7 +6,6 @@ on: - main - develop* - develop-* - - beta-schematic-rest-api tags: - v[0-9]+.[0-9]+.[0-9]+ paths-ignore: @@ -18,9 +17,9 @@ jobs: runs-on: ubuntu-latest container: rocker/rstudio:4.1.2 env: - GITHUB_PAT: ${{ github.GITHUB_TOKEN }} - DCA_SCHEMATIC_API_TYPE: reticulate - DCA_API_HOST: "https://schematic.api.sagebionetworks.org" + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + DCA_SCHEMATIC_API_TYPE: rest + DCA_API_HOST: "https://schematic-dev.api.sagebionetworks.org" DCA_API_PORT: "" steps: - name: Install System Dependencies @@ -119,6 +118,9 @@ jobs: echo 'DCA_SCHEMATIC_API_TYPE="${{ env.DCA_SCHEMATIC_API_TYPE }}"' >> .Renviron echo 'DCA_API_PORT="${{ env.DCA_API_PORT }}"' >> .Renviron echo 'DCA_API_HOST="${{ env.DCA_API_HOST }}"' >> .Renviron + + echo 'DCA_SYNAPSE_PROJECT_API=TRUE' >> .Renviron + echo 'DCA_MANIFEST_OUTPUT_FORMAT="excel"' >> .Renviron echo 'GITHUB_PAT="${{ secrets.GITHUB_TOKEN }}"' >> .Renviron @@ -135,7 +137,7 @@ jobs: # if tag is v*.*.*, deploy to prod, if main to staging, otherwise to test if (grepl("v[0-9]+.[0-9]+.[0-9]+", refName)) { message("Deploying release version of app") - } else if (refName %in% c("main", "beta-schematic-rest-api")) { + } else if (refName %in% c("main")) { appName <- paste(appName, "staging", sep = "-") message("Deploying staging version of app") } else { diff --git a/Dockerfile b/Dockerfile index 51a6dd9b..ecee0b32 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,14 @@ -FROM sagebionetworks/shiny-base:release-1.1 +FROM ghcr.io/afwillia/shiny-base:release-update-node LABEL maintainer="Anthony anthony.williams@sagebase.org" USER root RUN apt-get update RUN apt-get install -y libxml2 libglpk-dev libicu-dev libicu70 curl +# overwrite the default config with our modified copy +COPY shiny-server.conf /etc/shiny-server/shiny-server.conf +RUN chmod 777 /etc/shiny-server/shiny-server.conf + # Update node. https://github.com/nodesource/distributions RUN apt-get remove nodejs RUN curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - && apt-get install -y nodejs diff --git a/R/schematic_rest_api.R b/R/schematic_rest_api.R index 6ea63789..160d29d1 100644 --- a/R/schematic_rest_api.R +++ b/R/schematic_rest_api.R @@ -18,19 +18,29 @@ check_success <- function(x){ #' @param as_json if True return the manifest in JSON format #' @returns a csv of the manifest #' @export -manifest_download <- function(url="http://localhost:3001/v1/manifest/download", - input_token, asset_view, dataset_id, as_json=TRUE){ - req <- httr::GET(url, - query = list( - asset_view = asset_view, - dataset_id = dataset_id, - as_json = as_json, - input_token = input_token - )) +manifest_download <- function(url = "http://localhost:3001/v1/manifest/download", input_token, asset_view, dataset_id, as_json=TRUE, new_manifest_name=NULL) { + request <- httr::GET( + url = url, + query = list( + input_token = input_token, + asset_view = asset_view, + dataset_id = dataset_id, + as_json = as_json, + new_manifest_name = new_manifest_name + ) + ) + + check_success(request) + response <- httr::content(request, type = "application/json") + + # Output can have many NULL values which get dropped or cause errors. Set them to NA + nullToNA <- function(x) { + x[sapply(x, is.null)] <- NA + return(x) + } + df <- do.call(rbind, lapply(response, rbind)) + nullToNA(df) - check_success(req) - manifest <- httr::content(req, as = "text") - jsonlite::fromJSON(manifest) } #' schematic rest api to generate manifest @@ -146,7 +156,7 @@ manifest_validate <- function(url="http://localhost:3001/v1/model/validate", model_submit <- function(url="http://localhost:3001/v1/model/submit", schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #notlint data_type, dataset_id, restrict_rules=FALSE, input_token, json_str=NULL, asset_view, - use_schema_label=TRUE, manifest_record_type="table", file_name, + use_schema_label=TRUE, manifest_record_type="table_and_file", file_name, table_manipulation="replace") { req <- httr::POST(url, #add_headers(Authorization=paste0("Bearer ", pat)), diff --git a/R/synapse_rest_api.R b/R/synapse_rest_api.R index 916a349d..54da3cdb 100644 --- a/R/synapse_rest_api.R +++ b/R/synapse_rest_api.R @@ -98,3 +98,186 @@ synapse_access <- function(url = "https://repo-prod.prod.sagebase.org/repo/v1/en cont$result } + +#' @title Get children of a synapse entity +#' https://rest-docs.synapse.org/rest/POST/entity/children.html +#' @param url Synapse api endpoint +#' @param auth Synapse token +#' @param parentId Synapse ID of parent folder +#' @param nextPageToken Synapse next page token +#' @param includeTypes Types to return +#' @param sortBy Variable to sort by +#' @param sortDirection sort direction +#' @param includeTotalChildCount boolean include count of children +#' @param includeSumFileSizes boolean include sum of file sizes +synapse_entity_children <- function(url = "https://repo-prod.prod.sagebase.org/repo/v1/entity/children", + auth, parentId=NULL, nextPageToken=NULL, includeTypes="project", sortBy="NAME", + sortDirection="ASC", includeTotalChildCount=FALSE, includeSumFileSizes=FALSE) { + + output <- list() + req <- httr::POST(url, + httr::add_headers(Authorization=paste0("Bearer ", auth)), + body = + list(parentId=parentId, + nextPageToken=nextPageToken, + includeTypes=includeTypes, + sortBy=sortBy, + sortDirection=sortDirection, + includeTotalChildCount=includeTotalChildCount, + includeSumFileSizes=includeSumFileSizes), + encode="json") + + resp <- httr::content(req) + output <- resp$page + + while (!is.null(resp$nextPageToken)) { + req <- httr::POST(url, + httr::add_headers(Authorization=paste0("Bearer ", auth)), + body = + list(parentId=parentId, + nextPageToken=resp$nextPageToken, + includeTypes=includeTypes, + sortBy=sortBy, + sortDirection=sortDirection, + includeTotalChildCount=includeTotalChildCount, + includeSumFileSizes=includeSumFileSizes), + encode="json") + resp <- httr::content(req) + output <- c(output, resp$page) + } + bind_rows(output) + +} + +#' @title Get projects a user has access to +#' +#' @param url Synapse api endpoint +#' @param auth Synapse token +#' @param nextPageToken Synapse next page token +synapse_projects_user <- function(url = "https://repo-prod.prod.sagebase.org/repo/v1/projects/user", auth, nextPageToken=NULL) { + principalId <- synapse_user_profile(auth = auth)[["ownerId"]] + hreq <- httr::GET(url = file.path(url, principalId), + query = list(nextPageToken=nextPageToken)) + output <- list() + resp <- httr::content(hreq) + output <- resp$results + while (!is.null(resp$nextPageToken)) { + hreq <- httr::GET(url = file.path(url, principalId), + query = list(nextPageToken=resp$nextPageToken)) + resp <- httr::content(hreq) + output <- c(output, resp$results) + } + dplyr::bind_rows(output) +} + +#' @title Get projects within scope of Synapse project +#' +#' @param url Synapse api endpoint +#' @param id Synapse ID +#' @param auth Synapse token +synapse_get_project_scope <- function(url = "https://repo-prod.prod.sagebase.org/repo/v1/entity/", + id, auth) { + if (is.null(id)) stop("id cannot be NULL") + req_url <- file.path(url, id) + req <- httr::GET(req_url, + httr::add_headers(Authorization=paste0("Bearer ", auth))) + + # Send error if unsuccessful query + status <- httr::http_status(req) + if (status$category != "Success") stop(status$message) + + cont <- httr::content(req) + unlist(cont$scopeIds) +} + +#' @param title Query a Synapse Table +#' https://rest-docs.synapse.org/rest/GET/entity/id/table/query/async/get/asyncToken.html +#' @param id Synapse table ID +#' @param auth Synapse token +#' @param query An sql query +#' @param partMask The part of the Synapse response to get. Defaults to everything. +synapse_table_query <- function(id, auth, query, partMask=0x7F) { + url <- file.path("https://repo-prod.prod.sagebase.org/repo/v1/entity",id, "table/query/async/start") + req <- httr::POST(url = url, + httr::add_headers(Authorization=paste0("Bearer ", auth)), + body = list( + query = list(sql=query), + partMask = partMask + ), + encode = "json" + ) + httr::content(req) +} + +#' @param title Get results of synapse_table_query +#' https://rest-docs.synapse.org/rest/GET/entity/id/table/query/async/get/asyncToken.html +#' @param id Synapse table ID +#' @param async_token Token from synapse_table_query +#' @param auth Synapse token +synapse_table_get <- function(id, async_token, auth) { + url <- file.path("https://repo-prod.prod.sagebase.org/repo/v1/entity", id,"table/query/async/get", async_token) + req <- httr::GET(url = url, + httr::add_headers(Authorization=paste0("Bearer ", auth))) + httr::content(req) +} + +#' @title Get column names from a Synapse table +#' https://rest-docs.synapse.org/rest/GET/entity/id/table/query/async/get/asyncToken.html +#' Uses a table query to get the column names from a Synapse table +#' @param id Synapse ID of table +#' @param auth Synapse token +get_synapse_table_names <- function(id, auth) { + query <- sprintf("select id from %s limit 1", id) + request <- synapse_table_query(id, auth, query, partMask = 0x10) + Sys.sleep(1) + response <- synapse_table_get(id, request$token, auth) + vapply(response$columnModels, function(x) x$name, character(1L)) +} + +#' @title Get storage projects within a Synapse table +#' https://rest-docs.synapse.org/rest/GET/entity/id/table/query/async/get/asyncToken.html +#' @param id Synapse ID of table +#' @param auth Synapse token +#' @param select_cols Columns to get from table +synapse_storage_projects <- function(id, auth, select_cols = c("id", "name", "parentId", "projectId", "type", "columnType")) { + table_cols <- get_synapse_table_names(id, auth) + select_cols <- intersect(select_cols, table_cols) + select_cols_format <- paste(select_cols, collapse = ", ") + query <- sprintf("select distinct %s from %s", select_cols_format, id) + request <- synapse_table_query(id, auth, query, partMask = 0x1) + Sys.sleep(1) + response <- synapse_table_get(id, request$token, auth) + + setNames( + tibble::as_tibble( + t( + vapply( + response$queryResult$queryResults$rows, function(x) { + unlist(x$values) + }, + character(length(select_cols))))), + select_cols) +} + +#' @title Download a synapse file from its URL +#' https://rest-docs.synapse.org/rest/GET/file/id.html +#' @param dataFileHandleId The dataFileHandleId from an entity +#' @param id The synapse ID of the file to download +#' @param auth Synapse token +#' @param filepath Optional path to download data. If NULL, return a data frame. +synapse_download_file_handle <- function(dataFileHandleId, id, auth, filepath=NULL) { + url <- sprintf("https://repo-prod.prod.sagebase.org/file/v1/file/%s", dataFileHandleId) + request <- httr::GET(url = url, + httr::add_headers( Authorization=paste0("Bearer ", auth)), + query = list( + redirect = FALSE, + fileAssociateId = id, + fileAssociateType = "FileEntity" + ) + ) + download_url <- httr::content(request) + destfile <- ifelse(is.null(filepath), tempfile(), filepath) + download.file(download_url, destfile) + if (is.null(filepath)) readr::read_csv(destfile) + +} diff --git a/README.md b/README.md index f4f9deb0..fdbe3bd1 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The _Data Curator App_ is an R Shiny app that serves as the _frontend_ to the [s ## Quickstart {#quickstart} Sage Bionetworks hosts a version of Data Curator App for its collaborators. [Access it here](link TBD). -To configure your project for this version, edit [dcc_config.csv](dcc_config.csv) and submit a pull request. +To configure your project for this version, fork this repo and append [dcc_config.csv](dcc_config.csv). Then submit a pull request to [this branch](https://github.com/Sage-Bionetworks/data_curator/tree/beta-schematic-rest-api). [dcc_config.csv](dcc_config.csv) contains the following. **Bold fields** are required: - **project_name**: The display name of your project @@ -18,6 +18,7 @@ To configure your project for this version, edit [dcc_config.csv](dcc_config.csv - **manifest_output_format**: "excel" - **submit_use_schema_labels**: Schematic option to use schema labels when submitting (default TRUE) TRUE or FALSE - **submit_table_manipulation**: Schematic option when submitting (default "replace") "replace" or "upsert" +- **submit_manifest_record_type**: Schematic option when submitting. - **use_compliance_dashboard**: (default FALSE) TRUE or FALSE - primary_col: (default Sage theme) hexadecimal color code - secondary_col; (default Sage theme) hexadecimal color code diff --git a/dcc_config.csv b/dcc_config.csv index e7187941..cfd4bb32 100644 --- a/dcc_config.csv +++ b/dcc_config.csv @@ -1,5 +1,8 @@ -project_name,synapse_asset_view,data_model_url,template_menu_config_file,manifest_output_format,submit_use_schema_labels,submit_table_manipulation,use_compliance_dashboard,primary_col,secondary_col,sidebar_col -DCA Demo,syn33715412,https://raw.githubusercontent.com/Sage-Bionetworks/data-models/main/example.model.jsonld,www/template_config/example_config.json,excel,TRUE,replace,FALSE,#2a668d,#184e71,#191919 -HTAN All Projects,syn20446927,https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld,www/template_config/htan_config.json,excel,TRUE,replace,FALSE,#605ca8,#5F008C,#191919 -Cancer Complexity Knowledge Portal - Database,syn27210848,https://raw.githubusercontent.com/mc2-center/data-models/main/mc2.model.jsonld,www/template_config/mc2_config.json,excel,FALSE,upsert,FALSE,#407BA0,#5BB0B5,#191919 -INCLUDE Data Management Core,syn30109515,https://raw.githubusercontent.com/include-dcc/include-linkml/schematic-updates/src/schematic/include_schematic_linkml.jsonld,www/template_config/include_config.json,excel,TRUE,replace,FALSE,#2a668d,#184e71,#191919 +project_name,synapse_asset_view,data_model_url,template_menu_config_file,manifest_output_format,submit_use_schema_labels,submit_table_manipulation,submit_manifest_record_type,use_compliance_dashboard,primary_col,secondary_col,sidebar_col +DCA Demo,syn33715412,https://raw.githubusercontent.com/Sage-Bionetworks/data-models/main/example.model.jsonld,www/template_config/example_config.json,google_sheet,TRUE,replace,table_and_file,FALSE,#2a668d,#184e71,#191919 +HTAN All Projects,syn20446927,https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld,www/template_config/htan_config.json,excel,TRUE,replace,table_and_file,FALSE,#605ca8,#5F008C,#191919 +Cancer Complexity Knowledge Portal - Database,syn27210848,https://raw.githubusercontent.com/mc2-center/data-models/main/mc2.model.jsonld,www/template_config/mc2_config.json,excel,FALSE,upsert,table_and_file,FALSE,#407BA0,#5BB0B5,#191919 +INCLUDE Data Management Core,syn30109515,https://raw.githubusercontent.com/include-dcc/include-linkml/schematic-updates/src/schematic/include_schematic_linkml.jsonld,www/template_config/include_config.json,excel,TRUE,replace,table_and_file,FALSE,#2a668d,#184e71,#191919 +AD Knowledge Portal,syn51324810,https://raw.githubusercontent.com/adknowledgeportal/data-models/main/divco.data.model.v1.jsonld,www/template_config/adkp_config.json,excel,TRUE,replace,file_only,FALSE,#2a668d,#184e71,#191919 +VEOIBD,syn51397378,https://raw.githubusercontent.com/VEOIBD/data_models/main/veoibd.data.model.jsonld,www/template_config/VEOIBD_config.json,excel,TRUE,replace,table_and_file,FALSE,#2a668d,#184e72,#191920 +BTC DCC,syn51407795,https://github.com/Sage-Bionetworks/btc-data-models/blob/main/btc.model.jsonld,www/template_config/example_config.json,excel,TRUE,replace,table_and_file,FALSLE,#2a668d,#184e72,#191920 diff --git a/functions/dcWaiter.R b/functions/dcWaiter.R index 61cc3d65..be5c537f 100644 --- a/functions/dcWaiter.R +++ b/functions/dcWaiter.R @@ -8,7 +8,7 @@ col2rgba <- function(x, alpha=255) { dcWaiter <- function(stage = c("show", "update", "hide"), id = NULL, landing = FALSE, userName = NULL, isLogin = TRUE, isCertified = TRUE, isPermission = TRUE, - sleep = 2, msg = NULL, style = NULL, + sleep = 0, msg = NULL, style = NULL, spin = NULL, custom_spinner = FALSE, url = "", color = col2rgba("#424874", 255*0.9)) { # validate arguments diff --git a/functions/schematic_rest_api.R b/functions/schematic_rest_api.R index f204449d..160d29d1 100644 --- a/functions/schematic_rest_api.R +++ b/functions/schematic_rest_api.R @@ -1,3 +1,15 @@ +#' @description Check if a httr request succeeded. +#' @param x An httr response object +check_success <- function(x){ + if (!inherits(x, "response")) stop("Input must be an httr reponse object") + status <- httr::http_status(x) + if (tolower(status$category) == "success") { + return() + } else { + stop(sprintf("Response from server: %s", status$reason)) + } +} + #' @description Download an existing manifest #' @param url URI of API endpoint #' @param input_token Synapse PAT @@ -6,17 +18,29 @@ #' @param as_json if True return the manifest in JSON format #' @returns a csv of the manifest #' @export -manifest_download <- function(url="http://localhost:3001/v1/manifest/download", - input_token, asset_view, dataset_id, as_json=TRUE){ - req <- httr::GET(url, - query = list( - asset_view = asset_view, - dataset_id = dataset_id, - as_json = as_json, - input_token = input_token - )) - manifest <- httr::content(req, as = "text") - jsonlite::fromJSON(manifest) +manifest_download <- function(url = "http://localhost:3001/v1/manifest/download", input_token, asset_view, dataset_id, as_json=TRUE, new_manifest_name=NULL) { + request <- httr::GET( + url = url, + query = list( + input_token = input_token, + asset_view = asset_view, + dataset_id = dataset_id, + as_json = as_json, + new_manifest_name = new_manifest_name + ) + ) + + check_success(request) + response <- httr::content(request, type = "application/json") + + # Output can have many NULL values which get dropped or cause errors. Set them to NA + nullToNA <- function(x) { + x[sapply(x, is.null)] <- NA + return(x) + } + df <- do.call(rbind, lapply(response, rbind)) + nullToNA(df) + } #' schematic rest api to generate manifest @@ -31,7 +55,7 @@ manifest_download <- function(url="http://localhost:3001/v1/manifest/download", #' @export manifest_generate <- function(url="http://localhost:3001/v1/manifest/generate", schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #nolint - title, data_type, oauth="true", + title, data_type, use_annotations="false", dataset_id=NULL, asset_view, output_format, input_token = NULL) { @@ -40,7 +64,6 @@ manifest_generate <- function(url="http://localhost:3001/v1/manifest/generate", schema_url=schema_url, title=title, data_type=data_type, - oauth=oauth, use_annotations=use_annotations, dataset_id=dataset_id, asset_view=asset_view, @@ -48,6 +71,7 @@ manifest_generate <- function(url="http://localhost:3001/v1/manifest/generate", input_token = input_token )) + check_success(req) manifest_url <- httr::content(req) manifest_url } @@ -72,6 +96,7 @@ manifest_populate <- function(url="http://localhost:3001/v1/manifest/populate", return_excel=return_excel), body=list(csv_file=httr::upload_file(csv_file, type = "text/csv")) ) + check_success(req) req } @@ -82,18 +107,18 @@ manifest_populate <- function(url="http://localhost:3001/v1/manifest/populate", #' @param url URL to schematic API endpoint #' @param schema_url URL to a schema jsonld #' @param data_type Type of dataset -#' @param csv_file Filepath of csv to validate +#' @param file_name Filepath of csv to validate #' #' @returns An empty list() if sucessfully validated. Or a list of errors. #' @export manifest_validate <- function(url="http://localhost:3001/v1/model/validate", schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #nolint - data_type, json_str=NULL, file_name) { + data_type, file_name, restrict_rules=FALSE) { req <- httr::POST(url, query=list( schema_url=schema_url, data_type=data_type, - json_str=json_str), + restrict_rules=restrict_rules), body=list(file_name=httr::upload_file(file_name)) ) @@ -111,6 +136,7 @@ manifest_validate <- function(url="http://localhost:3001/v1/model/validate", ) ) } + check_success(req) annotation_status <- httr::content(req) annotation_status } @@ -130,7 +156,7 @@ manifest_validate <- function(url="http://localhost:3001/v1/model/validate", model_submit <- function(url="http://localhost:3001/v1/model/submit", schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #notlint data_type, dataset_id, restrict_rules=FALSE, input_token, json_str=NULL, asset_view, - use_schema_label=TRUE, manifest_record_type="table", file_name, + use_schema_label=TRUE, manifest_record_type="table_and_file", file_name, table_manipulation="replace") { req <- httr::POST(url, #add_headers(Authorization=paste0("Bearer ", pat)), @@ -149,9 +175,7 @@ model_submit <- function(url="http://localhost:3001/v1/model/submit", #body=list(file_name=file_name) ) - if (tolower(httr::http_status(req)$category) != "success") { - stop(sprintf("Error submitting manifest: %s", httr::http_status(req)$reason)) - } + check_success(req) manifest_id <- httr::content(req) manifest_id } @@ -175,7 +199,7 @@ model_component_requirements <- function(url="http://localhost:3001/v1/model/com as_graph = as_graph )) - if (httr::http_error(req)) stop(httr::http_status(req)$reason) + check_success(req) cont <- httr::content(req) if (inherits(cont, "xml_document")){ @@ -210,6 +234,7 @@ storage_project_datasets <- function(url="http://localhost:3001/v1/storage/proje input_token=input_token) ) + check_success(req) httr::content(req) } @@ -231,6 +256,7 @@ storage_projects <- function(url="http://localhost:3001/v1/storage/projects", input_token=input_token )) + check_success(req) httr::content(req) } @@ -258,6 +284,7 @@ storage_dataset_files <- function(url="http://localhost:3001/v1/storage/dataset/ file_names=file_names, full_path=full_path, input_token=input_token)) + check_success(req) httr::content(req) } @@ -277,14 +304,13 @@ get_asset_view_table <- function(url="http://localhost:3001/v1/storage/assets/ta input_token=input_token, return_type=return_type)) - if (httr::http_status(req)$category == "Success") { - if (return_type=="json") { - return(list2DF(fromJSON(httr::content(req)))) - } else { - csv <- readr::read_csv(httr::content(req)) - return(csv) - } - } else stop("File could not be downloaded from Synapse.") + check_success(req) + if (return_type=="json") { + return(list2DF(fromJSON(httr::content(req)))) + } else { + csv <- readr::read_csv(httr::content(req)) + return(csv) + } } diff --git a/functions/schematic_reticulate.R b/functions/schematic_reticulate.R index a2659693..c1a1d532 100644 --- a/functions/schematic_reticulate.R +++ b/functions/schematic_reticulate.R @@ -52,7 +52,7 @@ storage_projects_datasets_py <- function(synapse_driver, project_id) { } storage_dataset_files_py <- function(project_id) { - file_list <- syn_store$getFilesInStorageDataset(project_id) + syn_store$getFilesInStorageDataset(project_id) } manifest_generate_py <- function(title, rootNode, filenames=NULL, datasetId){ diff --git a/functions/utils.R b/functions/utils.R index 38177864..533a26f8 100644 --- a/functions/utils.R +++ b/functions/utils.R @@ -56,8 +56,12 @@ update_logo <- function(project = "sage") { img_src = "img/HTAN_text_logo.png"), syn27210848 = list(href = "https://cancercomplexity.synapse.org/", img_src = "img/cckp_logo.png"), - syn30109515 = list(href = "https://https://includedcc.org/", + syn30109515 = list(href = "https://includedcc.org/", img_src = "img/INCLUDE DCC Logo-01.png"), + syn51324810 = list(href = "https://adknowledgeportal.synapse.org/", + img_src = "img/ADKnowledgePortal.png"), + syn51397378 = list(href = "veoibd.org", + img_src = "img/VEOIBD Logo temp.png"), list(href = "https://synapse.org", img_src = "img/Logo_Sage_Logomark.png") ) @@ -73,4 +77,4 @@ update_logo <- function(project = "sage") { ) ) ) -} \ No newline at end of file +} diff --git a/global.R b/global.R index 17d4982d..ab8f4163 100644 --- a/global.R +++ b/global.R @@ -16,6 +16,8 @@ suppressPackageStartupMessages({ library(readr) library(sass) library(shinydashboardPlus) + library(promises) + library(future) # dashboard library(purrr) library(data.table) @@ -24,11 +26,16 @@ suppressPackageStartupMessages({ library(r2d3) }) +# Set up futures/promises for asynchronous calls +ncores <- availableCores() +message(sprintf("Available cores: %s", ncores)) +plan(multisession, workers = ncores) + # import R files source_files <- list.files(c("functions", "modules"), pattern = "*\\.R$", recursive = TRUE, full.names = TRUE) sapply(source_files, FUN = source) -dcc_config <- read_csv("dcc_config.csv") +dcc_config <- read_csv("dcc_config.csv", show_col_types = FALSE) ## Set Up OAuth client_id <- Sys.getenv("DCA_CLIENT_ID") @@ -48,18 +55,14 @@ if (!dca_schematic_api %in% c("rest", "reticulate", "offline")) { } if (dca_schematic_api == "rest") { api_uri <- ifelse(Sys.getenv("DCA_API_PORT") == "", - Sys.getenv("DCA_API_HOST"), - paste(Sys.getenv("DCA_API_HOST"), - Sys.getenv("DCA_API_PORT"), - sep = ":") + Sys.getenv("DCA_API_HOST"), + paste(Sys.getenv("DCA_API_HOST"), + Sys.getenv("DCA_API_PORT"), + sep = ":") ) } -syn_themes <- c( - "syn20446927" = "www/dca_themes/htan_theme_config.rds", - "syn27210848" = "www/dca_themes/mc2_theme_config.rds", - "syn30109515" = "www/dca_themes/include_theme_config.rds" - ) +dca_synapse_api <- Sys.getenv("DCA_SYNAPSE_PROJECT_API") # update port if running app locally if (interactive()) { @@ -112,7 +115,9 @@ api <- oauth_endpoint( scope <- "openid view download modify" template_config_files <- setNames(dcc_config$template_menu_config_file, - dcc_config$synapse_asset_view) +dcc_config$synapse_asset_view) +if (dca_schematic_api == "offline") template_config_files <- setNames("www/template_config/config_offline.json", + "synXXXXXX") ## Set Up Virtual Environment # ShinyAppys has a limit of 7000 files which this app' grossly exceeds @@ -130,9 +135,9 @@ if (dca_schematic_api == "reticulate"){ ## Read config.json if (!file.exists("www/config.json")) { -# system( -# "python3 .github/config_schema.py -c schematic_config.yml --service_repo 'Sage-Bionetworks/schematic' --overwrite" -# ) + # system( + # "python3 .github/config_schema.py -c schematic_config.yml --service_repo 'Sage-Bionetworks/schematic' --overwrite" + # ) } } config_file <- fromJSON("www/template_config/config.json") @@ -140,7 +145,4 @@ config_file <- fromJSON("www/template_config/config.json") ## Global variables dropdown_types <- c("project", "folder", "template") -# set up cores used for parallelization -ncores <- parallel::detectCores() - 1 -datatypes <- c("project", "folder", "template") options(sass.cache = FALSE) diff --git a/inst/testdata/biospecimen-record-example-model-fail.csv b/inst/testdata/biospecimen-record-example-model-fail.csv new file mode 100644 index 00000000..ff02bb34 --- /dev/null +++ b/inst/testdata/biospecimen-record-example-model-fail.csv @@ -0,0 +1,3 @@ +Sample ID,Patient ID,Tissue Status,Component +123,123,Healthy,Biospecimen +555,555,,Biospecimen \ No newline at end of file diff --git a/inst/testdata/biospecimen-record-example-model-pass.csv b/inst/testdata/biospecimen-record-example-model-pass.csv new file mode 100644 index 00000000..a3277954 --- /dev/null +++ b/inst/testdata/biospecimen-record-example-model-pass.csv @@ -0,0 +1,2 @@ +Sample ID,Patient ID,Tissue Status,Component +123,123,Healthy,Biospecimen \ No newline at end of file diff --git a/modules/csvInfile.R b/modules/csvInfile.R index 87b78a8d..616b767b 100644 --- a/modules/csvInfile.R +++ b/modules/csvInfile.R @@ -11,7 +11,7 @@ csvInfileUI <- function(id) { ) } -csvInfileServer <- function(id, na = c("", "NA"), colsAsCharacters = FALSE, keepBlank = FALSE) { +csvInfileServer <- function(id, na = c("", "NA"), colsAsCharacters = FALSE, keepBlank = FALSE, trimEmptyRows = TRUE) { moduleServer( id, function(input, output, session) { @@ -26,6 +26,10 @@ csvInfileServer <- function(id, na = c("", "NA"), colsAsCharacters = FALSE, keep } else { infile <- read_csv(input$file$datapath, na = na, col_types = cols()) } + + if (trimEmptyRows) { + infile <- infile %>% filter_all(any_vars(!is.na(.))) + } if (keepBlank) { # change NA to blank to match schematic output @@ -33,11 +37,11 @@ csvInfileServer <- function(id, na = c("", "NA"), colsAsCharacters = FALSE, keep } # remove empty rows/columns where readr called it 'X'[digit] for unnamed col - infile <- infile[, !grepl("^X", colnames(infile))] + infile <- infile[, !grepl("^\\.\\.\\.", colnames(infile))] infile <- infile[rowSums(is.na(infile)) != ncol(infile), ] # add 1 to row index to match spreadsheet's row index - rownames(infile) <- as.numeric(rownames(infile)) + 1 - + suppressWarnings(rownames(infile) <- as.numeric(rownames(infile)) + 1) + return(infile) }) diff --git a/modules/switchTab.R b/modules/switchTab.R index ae48f388..c3537041 100644 --- a/modules/switchTab.R +++ b/modules/switchTab.R @@ -13,7 +13,7 @@ switchTabUI <- function(id, direction = c("left", "right", "both")) { btn_next <- actionButton(ns(tagID[1]), class = "switch-tab-next", lapply(1:3, function(i) tags$i(class = "fa fa-angle-right"))) fluidRow( if (direction == "right") { - column(1, offset = 10, btn_next) + column(1, offset = 5, btn_next) } else if (direction == "left") { column(1, offset = 1, btn_prev) } else { diff --git a/renv.lock b/renv.lock index 6396bd27..89f7e957 100644 --- a/renv.lock +++ b/renv.lock @@ -430,6 +430,11 @@ ], "Hash": "f4dcd23b67e33d851d2079f703e8b985" }, + "future": { + "Package": "future", + "Version": "1.32.0", + "Source": "Repository" + }, "generics": { "Package": "generics", "Version": "0.1.3", @@ -904,13 +909,10 @@ }, "renv": { "Package": "renv", - "Version": "0.17.0", - "Source": "Repository", + "Version": "0.17.2", + "OS_type": null, "Repository": "CRAN", - "Requirements": [ - "utils" - ], - "Hash": "ce3065fc1a0b64a859f55ac3998d6927" + "Source": "Repository" }, "reticulate": { "Package": "reticulate", @@ -1409,4 +1411,3 @@ } } } - diff --git a/renv/activate.R b/renv/activate.R index 360dd528..e17d5886 100644 --- a/renv/activate.R +++ b/renv/activate.R @@ -2,7 +2,7 @@ local({ # the requested version of renv - version <- "0.17.0" + version <- "0.17.2" # the project directory project <- getwd() diff --git a/server.R b/server.R index 8fab1621..c23eb00d 100644 --- a/server.R +++ b/server.R @@ -8,7 +8,7 @@ shinyServer(function(input, output, session) { options(shiny.reactlog = TRUE) params <- parseQueryString(isolate(session$clientData$url_search)) if (!has_auth_code(params) & dca_schematic_api != "offline") { - return() + return() } redirect_url <- paste0( @@ -21,7 +21,7 @@ shinyServer(function(input, output, session) { req <- POST(redirect_url, encode = "form", body = "", authenticate(app$key, app$secret, type = "basic" ), config = list()) - + # Stop the code if anything other than 2XX status code is returned stop_for_status(req, task = "get an access token") token_response <- content(req, type = NULL) @@ -35,26 +35,29 @@ shinyServer(function(input, output, session) { ######## session global variables ######## # read config in def_config <- ifelse(dca_schematic_api == "offline", - fromJSON("www/template_config/config_offline.json"), - fromJSON("www/template_config/config.json") + fromJSON("www/template_config/config_offline.json"), + fromJSON("www/template_config/config.json") ) config <- reactiveVal() config_schema <- reactiveVal(def_config) model_ops <- setNames(dcc_config$data_model_url, - dcc_config$synapse_asset_view) + dcc_config$synapse_asset_view) # mapping from display name to schema name template_namedList <- reactiveVal() - #names(template_namedList) <- config_schema$display_name all_asset_views <- setNames(dcc_config$synapse_asset_view, - dcc_config$project_name) + dcc_config$project_name) asset_views <- reactiveVal(c("mock dca fileview"="syn33715412")) dcc_config_react <- reactiveVal(dcc_config) + manifest_data <- reactiveVal() + validation_res <- reactiveVal() + manifest_id <- reactiveVal() + data_list <- list( - projects = reactiveVal(NULL), folders = reactiveVal(NULL), + projects = reactiveVal(NA), folders = reactiveVal(NULL), template = reactiveVal(setNames(def_config$schema_name, def_config$display_name)), files = reactiveVal(NULL), master_asset_view = reactiveVal(NULL) @@ -70,110 +73,116 @@ shinyServer(function(input, output, session) { isUpdateFolder <- reactiveVal(FALSE) data_model_options <- setNames(dcc_config$data_model_url, - dcc_config$synapse_asset_view) + dcc_config$synapse_asset_view) data_model = reactiveVal(NULL) # data available to the user syn_store <- NULL # gets list of projects they have access to asset_views <- reactiveVal(c("mock dca fileview (syn33715412)"="syn33715412")) - - tabs_list <- c("tab_data", "tab_template", "tab_upload") + + # All of tabName from the tabs in ui.R + tabs_list <- c("tab_asset_view", + "tab_project", + "tab_template_select", + "tab_folder", + "tab_template", + "tab_upload") clean_tags <- c( "div_template", "div_template_warn", "div_validate", NS("tbl_validate", "table"), "btn_val_gsheet", "btn_submit" ) - + # add box effects boxEffect(zoom = FALSE, float = TRUE) - + ######## Initiate Login Process ######## # synapse cookies session$sendCustomMessage(type = "readCookie", message = list()) shinyjs::useShinyjs() shinyjs::hide(selector = ".sidebar-menu") - + shinyjs::hide("box_preview") + shinyjs::hide("box_validate") + shinyjs::hide("box_submit") + # initial loading page + observeEvent(input$cookie, { + + # login and update session # - # TODO: If we don't use cookies, then what event should trigger this? + # The original code pulled the auth token from a cookie, but it + # should actually come from session$userData. The former is + # the Synapse login, only works when the Shiny app' is hosted + # in the synapse.org domain, and is unscoped. The latter will + # work in any domain and is scoped to the access required by the + # Shiny app' # - observeEvent(input$cookie, { - - # login and update session - # - # The original code pulled the auth token from a cookie, but it - # should actually come from session$userData. The former is - # the Synapse login, only works when the Shiny app' is hosted - # in the synapse.org domain, and is unscoped. The latter will - # work in any domain and is scoped to the access required by the - # Shiny app' - # + + if (dca_schematic_api != "offline") { + access_token <- session$userData$access_token + has_access <- vapply(all_asset_views, function(x) { + synapse_access(id=x, access="DOWNLOAD", auth=access_token) + }, 1L) + asset_views(all_asset_views[has_access==1]) - if (dca_schematic_api != "offline") { - access_token <- session$userData$access_token - has_access <- vapply(all_asset_views, function(x) { - synapse_access(id=x, access="DOWNLOAD", auth=access_token) - }, 1L) - asset_views(all_asset_views[has_access==1]) + if (length(asset_views) == 0) stop("You do not have DOWNLOAD access to any supported Asset Views.") + updateSelectInput(session, "dropdown_asset_view", + choices = asset_views()) - if (length(asset_views) == 0) stop("You do not have DOWNLOAD access to any supported Asset Views.") - updateSelectInput(session, "dropdown_asset_view", - choices = asset_views()) + user_name <- synapse_user_profile(auth=access_token)$firstName - user_name <- synapse_user_profile(auth=access_token)$firstName - - is_certified <- synapse_is_certified(auth=access_token) - # is_certified <- switch(dca_schematic_api, - # reticulate = syn$is_certified(user_name), - # rest = synapse_is_certified(auth=access_token)) - if (!is_certified) { - dcWaiter("update", landing = TRUE, isCertified = FALSE) - } else { - # update waiter loading screen once login successful - dcWaiter("update", landing = TRUE, userName = user_name) - } + is_certified <- synapse_is_certified(auth=access_token) + if (!is_certified) { + dcWaiter("update", landing = TRUE, isCertified = FALSE) } else { - updateSelectInput(session, "dropdown_asset_view", - choices = c("Offline mock data (synXXXXXX)"="synXXXXXX")) - dcWaiter("hide") + # update waiter loading screen once login successful + dcWaiter("update", landing = TRUE, userName = user_name) } - - ######## Arrow Button ######## - lapply(1:4, function(i) { - switchTabServer(id = paste0("switchTab", i), tabId = "tabs", tab = reactive(input$tabs)(), tabList = tabs_list, parent = session) - }) - + } else { + updateSelectInput(session, "dropdown_asset_view", + choices = c("Offline mock data (synXXXXXX)"="synXXXXXX")) + dcWaiter("hide") + } + + ######## Arrow Button ######## + lapply(1:6, function(i) { + switchTabServer(id = paste0("switchTab", i), tabId = "tabs", tab = reactive(input$tabs)(), tabList = tabs_list, parent = session) + }) + }) + # Goal of this observer is to retrieve a list of projects the users can access + # within the selected asset view. observeEvent(input$btn_asset_view, { + dcWaiter("show", msg = paste0("Getting data. This may take a minute."), + color=col2rgba(col2rgb("#CD0BBC01"))) + shinyjs::disable("btn_asset_view") + selected$master_asset_view(input$dropdown_asset_view) av_names <- names(asset_views()[asset_views() %in% selected$master_asset_view()]) selected$master_asset_view_label(av_names) dcc_config_react(dcc_config[dcc_config$synapse_asset_view == selected$master_asset_view(), ]) - - dcWaiter("show", msg = paste0("Getting data from ", selected$master_asset_view_label(),". This may take a minute."), - color=col2rgba(col2rgb("#CD0BBC01"))) + if (dca_schematic_api == "offline") dcc_config_react(dcc_config[dcc_config$project_name == "DCA Demo", ]) data_model(data_model_options[selected$master_asset_view()]) - + output$sass <- renderUI({ - tags$head(tags$style(css())) + tags$head(tags$style(css())) }) css <- reactive({ - # Don't change theme for default projects - #if (dca_theme_file != "www/dca_themes/sage_theme_config.rds") { - sass(input = list(primary_col=dcc_config_react()$primary_col, - htan_col=dcc_config_react()$secondary_col, - sidebar_col=dcc_config_react()$sidebar_col, - sass_file("www/scss/main.scss"))) - #} - }) - - dcWaiter("show", msg = paste0("Getting data from ", selected$master_asset_view_label(), ". This may take a minute."), - color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) - + # Don't change theme for default projects + sass(input = list(primary_col=dcc_config_react()$primary_col, + htan_col=dcc_config_react()$secondary_col, + sidebar_col=dcc_config_react()$sidebar_col, + sass_file("www/scss/main.scss"))) + }) + + dcWaiter("hide") + dcWaiter("show", msg = paste0("Getting data. This may take a minute."), + color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) + output$logo <- renderUI({update_logo(selected$master_asset_view())}) if (dca_schematic_api == "reticulate") { @@ -188,9 +197,9 @@ shinyServer(function(input, output, session) { syn_store <<- synapse_driver(access_token = access_token) system( - "python3 .github/config_schema.py -c schematic_config.yml --service_repo 'Sage-Bionetworks/schematic' --overwrite" + "python3 .github/config_schema.py -c schematic_config.yml --service_repo 'Sage-Bionetworks/schematic' --overwrite" ) - + } conf_file <- reactiveVal(template_config_files[input$dropdown_asset_view]) @@ -200,115 +209,217 @@ shinyServer(function(input, output, session) { config(config_df) config_schema(config_df) data_list$template(conf_template) + + if (dca_synapse_api == TRUE & dca_schematic_api != "offline") { + #This chunk gets projects using the synapse REST API + #Check for user access to project scopes within asset view + + .asset_view <- selected$master_asset_view() + promises::future_promise({ + scopes <- synapse_get_project_scope(id = .asset_view, auth = access_token) + scope_access <- vapply(scopes, function(x) { + synapse_access(id=x, access="DOWNLOAD", auth=access_token) + }, 1L) + scopes <- scopes[scope_access==1] + projects <- bind_rows( + lapply(scopes, function(x) synapse_get(id=x, auth=access_token)) + ) %>% arrange(name) + setNames(projects$id, projects$name) + + }) %...>% data_list$projects() - data_list_raw <- switch(dca_schematic_api, - reticulate = storage_projects_py(synapse_driver, access_token), - rest = storage_projects(url=file.path(api_uri, "v1/storage/projects"), - asset_view = selected$master_asset_view(), - input_token = access_token), - list(list("Offline Project A", "Offline Project")) - ) - data_list$projects(list2Vector(data_list_raw)) - + } else { + data_list_raw <- switch(dca_schematic_api, + reticulate = storage_projects_py(synapse_driver, access_token), + rest = storage_projects(url=file.path(api_uri, "v1/storage/projects"), + asset_view = selected$master_asset_view(), + input_token = access_token), + list(list("Offline Project A", "Offline Project")) + ) + data_list$projects(list2Vector(data_list_raw)) + } + }) + + observeEvent(data_list$projects(), ignoreInit = TRUE, { if (is.null(data_list$projects()) || length(data_list$projects()) == 0) { dcWaiter("update", landing = TRUE, isPermission = FALSE) } else { - + # updates project dropdown - lapply(c("header_dropdown_", "dropdown_"), function(x) { - lapply(c(1, 3), function(i) { - updateSelectInput(session, paste0(x, dropdown_types[i]), - choices = sort(names(data_list[[i]]())) - ) - }) + lapply(c("dropdown_"), function(x) { + lapply(c(1, 3), function(i) { + updateSelectInput(session, paste0(x, dropdown_types[i]), + choices = sort(names(data_list[[i]]())) + ) + }) }) } + updateTabsetPanel(session, "tabs", selected = "tab_project") + + shinyjs::show(selector = ".sidebar-menu") + shinyjs::hide(select = "li:nth-child(3)") + shinyjs::hide(select = "li:nth-child(4)") + shinyjs::hide(select = "li:nth-child(5)") + shinyjs::hide(select = "li:nth-child(6)") + + dcWaiter("hide") + }) + + observeEvent(input$dropdown_asset_view, { + shinyjs::enable("btn_asset_view") + }) + + # Goal of this observer is to get all of the folders within the selected + # project. + observeEvent(input$btn_project, { ######## Update Folder List ######## - lapply(c("header_dropdown_", "dropdown_"), function(x) { - observeEvent(ignoreInit = TRUE, input[[paste0(x, "project")]], { - - # get synID of selected project - project_id <- data_list$projects()[input[[paste0(x, "project")]]] - - dcWaiter("show", msg = paste0("Getting project data from ", selected$master_asset_view_label(), ". This may take a minute."), - color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) - - # gets folders per project - folder_list_raw <- switch(dca_schematic_api, - reticulate = storage_projects_datasets_py(synapse_driver, project_id), - rest = storage_project_datasets(url=file.path(api_uri, "v1/storage/project/datasets"), - asset_view = selected$master_asset_view(), - project_id=project_id, - input_token=access_token), - list(list("DatatypeA", "DatatypeA"), list("DatatypeB","DatatypeB")) + dcWaiter("show", msg = paste0("Getting data"), + color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) + shinyjs::disable("btn_project") + selected$project(data_list$projects()[names(data_list$projects()) == input$dropdown_project]) + + observeEvent(input[["dropdown_project"]], { + # get synID of selected project + project_id <- selected$project() + + .asset_view <- selected$master_asset_view() + + promises::future_promise({ + folder_list_raw <- switch( + dca_schematic_api, + reticulate = storage_projects_datasets_py( + synapse_driver, + project_id), + rest = storage_project_datasets( + url=file.path(api_uri, "v1/storage/project/datasets"), + asset_view = .asset_view, + project_id=project_id, + input_token=access_token), + list(list("DatatypeA", "DatatypeA"), list("DatatypeB","DatatypeB")) ) - folder_list <- list2Vector(folder_list_raw) - - if (length(folder_list) > 0) folder_names <- sort(names(folder_list)) else folder_names <- " " - - # update folder names - updateSelectInput(session, paste0(x, "folder"), choices = folder_names) - - if (x == "dropdown_") { - selected$project(project_id) - data_list$folders(folder_list) - } - - if (isUpdateFolder()) { - # sync with header dropdown - updateSelectInput(session, "dropdown_folder", selected = input[["header_dropdown_folder"]]) - isUpdateFolder(FALSE) - } - dcWaiter("hide") - - }) + folder_list <- list2Vector(folder_list_raw) + folder_list[sort(names(folder_list))] + + }) %...>% data_list$folders() }) - + }) + + observeEvent(data_list$folders(), ignoreInit = TRUE, { updateTabsetPanel(session, "tabs", - selected = "tab_data") - - shinyjs::show(selector = ".sidebar-menu") - + selected = "tab_template_select") + shinyjs::show(select = "li:nth-child(3)") dcWaiter("hide") }) - - ######## Arrow Button ######## - lapply(1:4, function(i) { - switchTabServer(id = paste0("switchTab", i), tabId = "tabs", tab = reactive(input$tabs)(), tabList = tabs_list, parent = session) + + observeEvent(input$dropdown_project, { + shinyjs::enable("btn_project") }) - - ######## Header Dropdown Button ######## - # Adjust header selection dropdown based on tabs - observe({ - if (input[["tabs"]] %in% c("tab_data", "tab_asset_view")) { - hide("header_selection_dropdown") - } else { - show("header_selection_dropdown") - addClass(id = "header_selection_dropdown", class = "open") - } + + # Goal of this button is to updpate the template reactive object + # with the template the user chooses + observeEvent(input$btn_template_select, { + dcWaiter("show", msg = "Please wait", color = col2rgba(dcc_config_react()$primary_col, 255*0.9), sleep=0) + shinyjs::disable("btn_template_select") + selected$schema(data_list$template()[input$dropdown_template]) + updateSelectInput(session, "dropdown_folder", choices = data_list$folders()) + updateTabsetPanel(session, "tabs", selected = "tab_folder") + shinyjs::show(select = "li:nth-child(4)") + dcWaiter("hide") }) - # sync header dropdown with main dropdown - lapply(dropdown_types, function(x) { - observeEvent(input[[paste0("dropdown_", x)]], { - updateSelectInput(session, paste0("header_dropdown_", x), - selected = input[[paste0("dropdown_", x)]] - ) + observeEvent(input$dropdown_template, { + shinyjs::enable("btn_template_select") + }) + + # Goal of this button is to get the files within a folder the user selects + observeEvent(input$btn_folder, { + + dcWaiter("show", msg = paste0("Getting data"), color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) + shinyjs::disable("btn_folder") + shinyjs::show(select = "li:nth-child(5)") + shinyjs::show(select = "li:nth-child(6)") + + updateTabsetPanel(session, "tabs", + selected = "tab_template") + + selected_folder <- data_list$folders()[which(data_list$folders() == input$dropdown_folder)] + output$template_title <- renderText({ sprintf("Get %s template for %s", + selected$schema(), + names(selected_folder)) }) + selected$folder(selected_folder) + # clean tags in generating-template tab + sapply(clean_tags[1:2], FUN = hide) + + + if (selected$schema_type() %in% c("record", "file")) { + # check number of files if it's file-based template + # This gets files using the synapse REST API + # get file list in selected folder + if (dca_synapse_api == TRUE & dca_schematic_api != "offline") { + .folder <- selected$folder() + promises::future_promise({ + files <- synapse_entity_children(auth = access_token, parentId=.folder, includeTypes = list("file")) + if (nrow(files) > 0) { files_vec <- setNames(files$id, files$name) + } else files_vec <- NA_character_ + files_vec + }) %...>% data_list$files() + + } else { + + file_list <- switch(dca_schematic_api, + reticulate = storage_dataset_files_py(selected$folder()), + rest = storage_dataset_files(url=file.path(api_uri, "v1/storage/dataset/files"), + asset_view = selected$master_asset_view(), + dataset_id = selected$folder(), + input_token=access_token), + list(list("DatatypeA", "DatatypeA"), list("DatatypeB", "DatatypeB"))) + + # update files list in the folder + data_list$files(list2Vector(file_list)) + } + } }) - - observeEvent(input$btn_header_update, { - nx_confirm( - inputId = "update_confirm", - title = "Are you sure to update?", - message = "previous selections will also change", - button_ok = "Sure!", - button_cancel = "Nope!" - ) + + observeEvent(input$dropdown_folder,{ + shinyjs::enable("btn_folder") }) - + + observeEvent(data_list$files(), ignoreInit = TRUE, { + warn_text <- NULL + if (length(data_list$folders()) == 0) { + # add warning if there is no folder in the selected project + warn_text <- paste0( + "please create a folder in the ", + strong(sQuote(input$dropdown_project)), + " prior to submitting templates." + ) + } + if (is.null(data_list$files())) { + # display warning message if folder is empty and data type is file-based + warn_text <- paste0( + strong(sQuote(input$dropdown_folder)), " folder is empty, + please upload your data before generating manifest.", + "
", strong(sQuote(input$dropdown_template)), + " requires data files to be uploaded prior to generating and submitting templates.", + "
", "Filling in a template before uploading your data, + may result in errors and delays in your data submission later." + ) + } + + # if there is warning from above checks + if (!is.null(warn_text)){ + # display warnings + output$text_template_warn <- renderUI(tagList(br(), span(class = "warn_msg", HTML(warn_text)))) + show("div_template_warn") + } + + dcWaiter("hide") + + }) + observeEvent(input$update_confirm, { req(input$update_confirm == TRUE) isUpdateFolder(TRUE) @@ -318,15 +429,7 @@ shinyServer(function(input, output, session) { ) }) }) - - ######## Update Folder ######## - # update selected folder synapse id and name - observeEvent(input$dropdown_folder, { - selected$folder(data_list$folders()[input$dropdown_folder]) - # clean tags in generating-template tab - sapply(clean_tags[1:2], FUN = hide) - }) - + ######## Update Template ######## # update selected schema template name observeEvent(input$dropdown_template, { @@ -337,98 +440,79 @@ shinyServer(function(input, output, session) { # clean all tags related with selected template sapply(clean_tags, FUN = hide) }, ignoreInit = TRUE) - + ######## Dashboard ######## - dashboard( - id = "dashboard", - syn.store = syn_store, - project.scope = selected$project, - schema = selected$schema, - schema.display.name = reactive(input$dropdown_datatype), - disable.ids = c("box_pick_project", "box_pick_manifest"), - ncores = ncores, - access_token = access_token, - fileview = selected$master_asset_view(), - folder = selected$project(), - schematic_api = dca_schematic_api, - schema_url = data_model() - ) - + # dashboard( + # id = "dashboard", + # syn.store = syn_store, + # project.scope = selected$project, + # schema = selected$schema, + # schema.display.name = reactive(input$dropdown_datatype), + # disable.ids = c("box_pick_project", "box_pick_manifest"), + # ncores = ncores, + # access_token = access_token, + # fileview = selected$master_asset_view(), + # folder = selected$project(), + # schematic_api = dca_schematic_api, + # schema_url = data_model() + # ) + manifest_url <- reactiveVal(NULL) ######## Template Google Sheet Link ######## # validate before generating template observeEvent(c(selected$folder(), selected$schema(), input$tabs), { - req(input$tabs %in% c("tab_template", "tab_validate")) - warn_text <- NULL - if (length(data_list$folders()) == 0) { - # add warning if there is no folder in the selected project - warn_text <- paste0( - "please create a folder in the ", - strong(sQuote(input$dropdown_project)), - " prior to submitting templates." - ) - } else if (selected$schema_type() %in% c("record", "file")) { - # check number of files if it's file-based template - - dcWaiter("show", msg = paste0("Getting files in ", input$dropdown_folder, "."), color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) - # get file list in selected folder - file_list <- switch(dca_schematic_api, - reticulate = storage_dataset_files_py(selected$folder()), - rest = storage_dataset_files(url=file.path(api_uri, "v1/storage/dataset/files"), - asset_view = selected$master_asset_view(), - dataset_id = selected$folder(), - input_token=access_token), - list(list("DatatypeA", "DatatypeA"), list("DatatypeB", "DatatypeB"))) - - # update files list in the folder - data_list$files(list2Vector(file_list)) - - dcWaiter("hide") - - if (is.null(data_list$files())) { - # display warning message if folder is empty and data type is file-based - warn_text <- paste0( - strong(sQuote(input$dropdown_folder)), " folder is empty, - please upload your data before generating manifest.", - "
", strong(sQuote(input$dropdown_template)), - " requires data files to be uploaded prior to generating and submitting templates.", - "
", "Filling in a template before uploading your data, - may result in errors and delays in your data submission later." - ) - } - } - - # if there is warning from above checks - req(warn_text) - # display warnings - output$text_template_warn <- renderUI(tagList(br(), span(class = "warn_msg", HTML(warn_text)))) - show("div_template_warn") + }) + + observeEvent(c(input$`switchTab4-Next`, input$tabs), { + + req(input$tabs == "tab_template") + dcWaiter("show", msg = "Getting template. This may take a minute.", color = dcc_config_react()$primary_col) + + ### This doesn't work - try moving manifest_generate outside of downloadButton + .schema <- selected$schema() + .datasetId <- selected$folder() + .schema_url <- data_model() + .asset_view <- selected$master_asset_view() + .template <- paste( + dcc_config_react()$project_name, + "-", + input$dropdown_template + ) + .url <- ifelse(dca_schematic_api != "offline", + file.path(api_uri, "v1/manifest/generate"), + NA) + .output_format <- dcc_config_react()$manifest_output_format - observeEvent(c(input$dropdown_folder, input$tabs), { - # if (input$tabs == "tab_template" && Sys.getenv("DCA_MANIFEST_OUTPUT_FORMAT") == "excel") { - # dcWaiter("show", msg = "Downloading data from Synapse...", color = dca_theme()$primary_col) - # #schematic rest api to generate manifest - # manifest_data <- switch(dca_schematic_api, - # reticulate = manifest_generate_py(title = input$dropdown_template, - # rootNode = selected$schema(), - # datasetId = selected$folder()), - # rest = manifest_generate(url=file.path(api_uri, "v1/manifest/generate"), - # schema_url = data_model(), - # title = input$dropdown_template, - # data_type = selected$schema(), - # dataset_id = selected$folder(), - # asset_view = selected$master_asset_view(), - # output_format = Sys.getenv("DCA_MANIFEST_OUTPUT_FORMAT"), - # input_token=access_token), - # "offline-no-gsheet-url" - # ) - # manifest_url(manifest_data) - # - # dcWaiter("hide", sleep = 1) - # - # } + promises::future_promise({ + switch(dca_schematic_api, + rest = manifest_generate( + url=.url, + schema_url = .schema_url, + title = .template, + data_type = .schema, + dataset_id = .datasetId, + asset_view = .asset_view, + use_annotations = FALSE, + output_format = .output_format, + input_token=access_token + ), + { + message("Downloading offline manifest") + Sys.sleep(0) + tibble(a="b", c="d") + } + ) + }) %...>% manifest_data() + + }) + + observeEvent(manifest_data(), { + if (dcc_config_react()$manifest_output_format == "google_sheet") { + shinyjs::show("div_template") + } else shinyjs::show("div_download_data") + dcWaiter("hide") }) # Bookmarking this thread in case we can't use writeBin... @@ -440,32 +524,19 @@ shinyServer(function(input, output, session) { # observeEvent above. output$downloadData <- downloadHandler( filename = function() sprintf("%s.xlsx", input$dropdown_template), + #filename = function() sprintf("%s.csv", input$dropdown_template), content = function(file) { - dcWaiter("show", msg = "Downloading manifest. This may take a minute.", color = dcc_config_react()$primary_col) - manifest_data <- switch(dca_schematic_api, - reticulate = manifest_generate_py(title = input$dropdown_template, - rootNode = selected$schema(), - datasetId = selected$folder()), - rest = manifest_generate(url=file.path(api_uri, "v1/manifest/generate"), - schema_url = data_model(), - title = input$dropdown_template, - data_type = selected$schema(), - dataset_id = selected$folder(), - asset_view = selected$master_asset_view(), - output_format = Sys.getenv("DCA_MANIFEST_OUTPUT_FORMAT"), - input_token=access_token), - "offline-no-gsheet-url" - ) - dcWaiter("hide", sleep = 1) - writeBin(manifest_data, file) - #capture.output(print(manifest_url()), file=file) # actually kinda works - # Just shows NULL - # sink(file) - # print(manifest_url()) - # sink() + dcWaiter("show", msg = "Downloading data", color = dcc_config_react()$primary_col) + dcWaiter("hide", sleep = 0) + writeBin(manifest_data(), file) } ) + # generate link + output$text_template <- renderUI( + tags$a(id = "template_link", href = manifest_data(), list(icon("hand-point-right"), manifest_data()), target = "_blank") + ) + if (dca_schematic_api == "offline") { mock_offline_manifest <- tibble("column1"="mock offline data") output$downloadData <- downloadHandler( @@ -475,204 +546,203 @@ shinyServer(function(input, output, session) { } ) } - - # generate template - observeEvent(input$btn_template, { - # loading screen for template link generation - dcWaiter("show", msg = "Generating link...", color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) - manifest_url(switch(dca_schematic_api, - reticulate = manifest_generate_py(title = input$dropdown_template, - rootNode = selected$schema(), - datasetId = selected$folder()), - rest = manifest_generate(url=file.path(api_uri, "v1/manifest/generate"), - schema_url = data_model(), - title = input$dropdown_template, - data_type = selected$schema(), - dataset_id = selected$folder(), - asset_view = selected$master_asset_view(), - output_format = Sys.getenv("DCA_MANIFEST_OUTPUT_FORMAT"), - input_token=access_token), - "offline-no-gsheet-url" - ) - ) - - # generate link - output$text_template <- renderUI( - tags$a(id = "template_link", href = manifest_url(), list(icon("hand-point-right"), manifest_url()), target = "_blank") - ) - - dcWaiter("hide", sleep = 1) - - nx_confirm( - inputId = "btn_template_confirm", - title = "Go to the template now?", - message = paste0("click 'Go' to edit your ", sQuote(input$dropdown_template), " template on the google sheet"), - button_ok = "Go", - ) - - # display link - show("div_template") # TODO: add progress bar on (loading) screen - }) observeEvent(input$btn_template_confirm, { - req(input$btn_template_confirm == TRUE) - runjs("$('#template_link')[0].click();") + req(input$btn_template_confirm == TRUE) + runjs("$('#template_link')[0].click();") }) - + ######## Reads .csv File ######## # Check out module and don't use filepath. Keep file in memory - inFile <- csvInfileServer("inputFile", colsAsCharacters = TRUE, keepBlank = TRUE) - + inFile <- csvInfileServer("inputFile", colsAsCharacters = TRUE, keepBlank = TRUE, trimEmptyRows = TRUE) + observeEvent(inFile$data(), { + # After trimming blank rows and columns from data, write to the filepath + # so it can be passed to the submit endpoint. + readr::write_csv(inFile$data(), inFile$raw()$datapath) # hide the validation section when upload a new file sapply(clean_tags[-c(1:2)], FUN = hide) # renders in DT for preview DTableServer("tbl_preview", inFile$data(), filter = "top") + shinyjs::show("box_preview") + shinyjs::show("box_validate") }) - + ######## Validation Section ####### observeEvent(input$btn_validate, { - + + dcWaiter("show", msg = "Validating manifest. This may take a minute.", color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) + + # Reset validation_result in case user reuploads the same file. This makes + # the validation_res observer trigger any time this button is pressed. + validation_res(NULL) + # loading screen for validating metadata - dcWaiter("show", msg = "Validating manifest...", color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) - annotation_status <- switch(dca_schematic_api, - reticulate = manifest_validate_py(inFile$raw()$datapath, - selected$schema(), - TRUE, - list(selected$project())), - rest = manifest_validate(url=file.path(api_uri, "v1/model/validate"), - schema_url=data_model(), - data_type=selected$schema(), - file_name=inFile$raw()$datapath), - #json_str=jsonlite::toJSON(read_csv(inFile$raw()$datapath))), - list(list( - "errors" = list( - Row = NA, Column = NA, Value = NA, - Error = "Mock error for offline mode." - ) - )) - ) - - # validation messages - validation_res <- validationResult(annotation_status, input$dropdown_template, inFile$data()) - ValidationMsgServer("text_validate", validation_res) - + .datapath <- inFile$raw()$datapath + .schema <- selected$schema() + .project <- list(selected$project()) + .data_model <- data_model() + .infile_data <- inFile$data() + .dd_template <- input$dropdown_template + + promises::future_promise({ + annotation_status <- switch(dca_schematic_api, + reticulate = manifest_validate_py( + .datapath, + .schema, + TRUE, + .project), + rest = manifest_validate( + url=file.path(api_uri, "v1/model/validate"), + schema_url=.data_model, + data_type=.schema, + file_name=.datapath), + { + Sys.sleep(0) + list(list( + "errors" = list( + Row = NA, Column = NA, Value = NA, + Error = "Mock error for offline mode." + ) + )) + } + ) + + # validation messages + validationResult(annotation_status, .dd_template, .infile_data) + + }) %...>% validation_res() + + }) + + observeEvent(validation_res(), { # if there is a file uploaded - if (!is.null(validation_res$result)) { - + if (!is.null(validation_res()$result)) { + + ValidationMsgServer("text_validate", validation_res()) + # highlight invalue cells in preview table - if (validation_res$error_type == "Wrong Schema") { + if (validation_res()$error_type == "Wrong Schema") { DTableServer("tbl_preview", data = inFile$data(), highlight = "full") } else { DTableServer( - "tbl_preview", - data = inFile$data(), - highlight = "partial", highlightValues = validation_res$preview_highlight + "tbl_preview", + data = inFile$data(), + highlight = "partial", highlightValues = validation_res()$preview_highlight ) } - - if (validation_res$result == "valid") { + + if (validation_res()$result == "valid" | dca_schematic_api == "offline" && grepl("fixed", inFile$data()[1,1])) { # show submit button - output$submit <- renderUI(actionButton("btn_submit", "Submit to Synapse", class = "btn-primary-color")) - dcWaiter("update", msg = paste0(validation_res$error_type, " Found !!! "), spin = spin_inner_circles(), sleep = 2.5) + output$submit <- renderUI(actionButton("btn_submit", "Submit data", class = "btn-primary-color")) + dcWaiter("update", msg = paste0(validation_res()$error_type, " Found !!! "), spin = spin_inner_circles(), sleep = 2.5) + shinyjs::show("box_submit") } else { - if (dca_schematic_api != "offline" & Sys.getenv("DCA_MANIFEST_OUTPUT_FORMAT") == "google_sheet") { - output$val_gsheet <- renderUI( - actionButton("btn_val_gsheet", " Generate Google Sheet Link", icon = icon("table"), class = "btn-primary-color") - ) + if (dca_schematic_api != "offline" & dcc_config_react()$manifest_output_format == "google_sheet") { + output$val_gsheet <- renderUI( + actionButton("btn_val_gsheet", " Generate Google Sheet Link", icon = icon("table"), class = "btn-primary-color") + ) } else if (dca_schematic_api == "offline") { - output$dl_manifest <- renderUI({ - downloadButton("downloadData_good", "Download Corrected Data") - }) + output$dl_manifest <- renderUI({ + downloadButton("downloadData_good", "Download Corrected Data") + }) } - dcWaiter("update", msg = paste0(validation_res$error_type, " Found !!! "), spin = spin_pulsar(), sleep = 2.5) - } + dcWaiter("update", msg = paste0(validation_res()$error_type, " Found !!! "), spin = spin_pulsar(), sleep = 2.5) + } } else { - dcWaiter("hide") + dcWaiter("hide") } - + show("div_validate") + }) - + # if user click gsheet_btn, generating gsheet observeEvent(input$btn_val_gsheet, { # loading screen for Google link generation dcWaiter("show", msg = "Generating link...", color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) filled_manifest <- switch(dca_schematic_api, - reticulate = manifest_populate_py(paste0(config$community, " ", input$dropdown_template), - inFile$raw()$datapath, - selected$schema()), - rest = manifest_populate(url=file.path(api_uri, "v1/manifest/populate"), - schema_url = data_model(), - title = paste0(config$community, " ", input$dropdown_template), - data_type = selected$schema(), - return_excel = FALSE, - csv_file = inFile$raw()$datapath), - "offline-no-gsheet-url") - - + reticulate = manifest_populate_py(paste0(config$community, " ", input$dropdown_template), + inFile$raw()$datapath, + selected$schema()), + rest = manifest_populate(url=file.path(api_uri, "v1/manifest/populate"), + schema_url = data_model(), + title = paste0(config$community, " ", input$dropdown_template), + data_type = selected$schema(), + return_excel = FALSE, + csv_file = inFile$raw()$datapath), + "offline-no-gsheet-url") + + # rerender and change button to link if (dca_schematic_api != "offline") { output$val_gsheet <- renderUI({ - HTML(paste0("
Edit on the Google Sheet.")) + HTML(paste0("Edit on the Google Sheet.")) }) } dcWaiter("hide") }) - # Offline version of downloading a failed manifest - mock_offline_manifest_2 <- tibble("column1"="fixed offline data") + # Offline version of downloading a failed manifest + mock_offline_manifest_2 <- tibble("column1"="fixed offline data") output$downloadData_good <- downloadHandler( filename = function() sprintf("%s.csv", input$dropdown_template), content = function(file) { write_csv(mock_offline_manifest_2, file) - } - ) - - + } + ) + + ######## Submission Section ######## observeEvent(input$btn_submit, { # loading screen for submitting data - dcWaiter("show", msg = "Submitting to Synapse. This may take a minute.", color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) - - + dcWaiter("show", msg = "Submitting data. This may take a minute.", color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) + + if (is.null(selected$folder())) { # add waiter if no folder selected - dcWaiter("update", msg = paste0("Please select a folder to submit"), spin = spin_pulsar(), sleep = 2.5) + dcWaiter("update", msg = paste0("Please select a folder to submit"), spin = spin_pulsar(), sleep = 0) } - + # abort submission if no folder selected req(selected$folder()) - + manifest_filename <- sprintf("%s_%s.csv", manifest_basename, tolower(selected$schema())) tmp_out_dir <- tempdir() tmp_file_path <- file.path(tmp_out_dir, manifest_filename) dir.create(tmp_out_dir, showWarnings = FALSE) - + # reads file csv again submit_data <- csvInfileServer("inputFile")$data() - + # If a file-based component selected (define file-based components) note for future # the type to filter (eg file-based) on could probably also be a config choice display_names <- config_schema()$manifest_schemas$display_name[config_schema()$manifest_schemas$type == "file"] - + if (input$dropdown_template %in% display_names) { # make into a csv or table for file-based components already has entityId if ("entityId" %in% colnames(submit_data)) { # Convert this to JSON instead and submit write.csv(submit_data, - file = tmp_file_path, - quote = TRUE, row.names = FALSE, na = "" + file = tmp_file_path, + quote = TRUE, row.names = FALSE, na = "" ) } else { - file_list_raw <- switch(dca_schematic_api, - reticulate = storage_dataset_files_py(selected$folder()), - rest = storage_dataset_files(url=file.path(api_uri, "v1/storage/dataset/files"), - asset_view = selected$master_asset_view(), - dataset_id = selected$folder(), - input_token=access_token)) - data_list$files(list2Vector(file_list_raw)) - + # Get file list from synapse REST API + if (dca_synapse_api == TRUE & dca_schematic_api != "offline") { + files <- synapse_entity_children(auth = access_token, parentId=selected$folder(), includeTypes = list("file")) + data_list$files(setNames(files$id, files$name)) + } else { + file_list_raw <- switch(dca_schematic_api, + reticulate = storage_dataset_files_py(selected$folder()), + rest = storage_dataset_files(url=file.path(api_uri, "v1/storage/dataset/files"), + asset_view = selected$master_asset_view(), + dataset_id = selected$folder(), + input_token=access_token)) + + data_list$files(list2Vector(file_list_raw)) + } + # better filename checking is needed # TODO: crash if no file existing files_df <- stack(data_list$files()) @@ -681,111 +751,107 @@ shinyServer(function(input, output, session) { files_entity <- inner_join(submit_data, files_df, by = "Filename") # convert this to JSON instead and submit write.csv(files_entity, - file = tmp_file_path, + file = tmp_file_path, quote = TRUE, row.names = FALSE, na = "" ) } + .folder <- selected$folder() + .data_model <- data_model() + .schema <- selected$schema() + .asset_view <- selected$master_asset_view() + .submit_use_schema_labels <- dcc_config_react()$submit_use_schema_labels + .table_manipulation <- dcc_config_react()$submit_table_manipulation + .submit_manifest_record_type <- dcc_config_react()$submit_manifest_record_type + # associates metadata with data and returns manifest id - manifest_id <- switch(dca_schematic_api, - reticulate = model_submit_py(schema_generator, - tmp_file_path, - selected$folder(), - "table", - FALSE), - rest = model_submit(url=file.path(api_uri, "v1/model/submit"), - schema_url = data_model(), - data_type = selected$schema(), - dataset_id = selected$folder(), - input_token = access_token, - restrict_rules = FALSE, - file_name = tmp_file_path, - asset_view = selected$master_asset_view(), - use_schema_label=dcc_config_react()$submit_use_schema_labels, - manifest_record_type="table", - table_manipulation=dcc_config_react()$submit_table_manipulation) - ) - manifest_path <- tags$a(href = paste0("https://www.synapse.org/#!Synapse:", manifest_id), manifest_id, target = "_blank") - - # add log message - message(paste0("Manifest :", sQuote(manifest_id), " has been successfully uploaded")) - - # if no error - if (startsWith(manifest_id, "syn") == TRUE) { - dcWaiter("hide") - nx_report_success("Success!", HTML(paste0("Manifest submitted to: ", manifest_path))) - - # clean up old inputs/results - sapply(clean_tags, FUN = hide) - reset("inputFile-file") - DTableServer("tbl_preview", data.frame(NULL)) - } else { - dcWaiter("update", msg = HTML(paste0( - "Uh oh, looks like something went wrong!", - manifest_id, - " is not a valid Synapse ID. Try again?" - )), sleep = 3) - } - } else { - # if not file-based type template - # convert this to JSON and submit - write.csv(submit_data, - file = tmp_file_path, quote = TRUE, - row.names = FALSE, na = "" - ) - - # associates metadata with data and returns manifest id - manifest_id <- switch(dca_schematic_api, - reticulate = model_submit_py(schema_generator, - tmp_file_path, - selected$folder(), - "table", - FALSE), - rest = model_submit(url=file.path(api_uri, "v1/model/submit"), - schema_url = data_model(), - data_type = selected$schema(), - dataset_id = selected$folder(), - input_token = access_token, - restrict_rules = FALSE, - file_name = tmp_file_path, - asset_view = selected$master_asset_view(), - use_schema_label=dcc_config_react()$submit_use_schema_labels, - manifest_record_type="table", - table_manipulation=dcc_config_react()$submit_table_manipulation) + promises::future_promise({ + switch(dca_schematic_api, + reticulate = model_submit_py(schema_generator, + tmp_file_path, + .folder, + "table", + FALSE), + rest = model_submit(url=file.path(api_uri, "v1/model/submit"), + schema_url = .data_model, + data_type = .schema, + dataset_id = .folder, + input_token = access_token, + restrict_rules = FALSE, + file_name = tmp_file_path, + asset_view = .asset_view, + use_schema_label=.submit_use_schema_labels, + manifest_record_type=.submit_manifest_record_type, + table_manipulation=.table_manipulation), + "synXXXX - No data uploaded" ) - manifest_path <- tags$a(href = paste0("https://www.synapse.org/#!Synapse:", manifest_id), manifest_id, target = "_blank") - - # add log message - message(paste0("Manifest :", sQuote(manifest_id), " has been successfully uploaded")) - - # if uploaded provided valid synID message - if (startsWith(manifest_id, "syn") == TRUE) { - dcWaiter("hide") - nx_report_success("Success!", HTML(paste0("Manifest submitted to: ", manifest_path))) - - # clear inputs - sapply(clean_tags, FUN = hide) - - # rerenders fileinput UI - output$fileInput_ui <- renderUI({ - fileInput("file1", "Upload CSV File", accept = c( - "text/csv", "text/comma-separated-values", - ".csv" - )) - }) - # renders empty df - output$tbl_preview <- renderDT(datatable(as.data.frame(matrix(0, - ncol = 0, - nrow = 0 - )))) - } else { - dcWaiter("update", msg = HTML(paste0( - "Uh oh, looks like something went wrong!", - manifest_id, " is not a valid Synapse ID. Try again?" - )), sleep = 3) - } + }) %...>% manifest_id() + + } else { + # if not file-based type template + # convert this to JSON and submit + write.csv(submit_data, + file = tmp_file_path, quote = TRUE, + row.names = FALSE, na = "" + ) + + # associates metadata with data and returns manifest id + .folder <- selected$folder() + .data_model <- data_model() + .schema <- selected$schema() + .asset_view <- selected$master_asset_view() + .submit_use_schema_labels <- dcc_config_react()$submit_use_schema_labels + .table_manipulation <- dcc_config_react()$submit_table_manipulation + .submit_manifest_record_type <- dcc_config_react()$submit_manifest_record_type + + # associates metadata with data and returns manifest id + promises::future_promise({ + switch(dca_schematic_api, + reticulate = model_submit_py(schema_generator, + tmp_file_path, + .folder, + "table", + FALSE), + rest = model_submit(url=file.path(api_uri, "v1/model/submit"), + schema_url = .data_model, + data_type = .schema, + dataset_id = .folder, + input_token = access_token, + restrict_rules = FALSE, + file_name = tmp_file_path, + asset_view = .asset_view, + use_schema_label=.submit_use_schema_labels, + manifest_record_type=.submit_manifest_record_type, + table_manipulation=.table_manipulation), + "synXXXX - No data uploaded" + ) + }) %...>% manifest_id() + + } + + }) + + observeEvent(manifest_id(), { + manifest_path <- tags$a(href = paste0("https://www.synapse.org/#!Synapse:", manifest_id()), manifest_id(), target = "_blank") + + # add log message + message(paste0("Manifest :", sQuote(manifest_id()), " has been successfully uploaded")) + + # if no error + if (startsWith(manifest_id(), "syn") == TRUE) { + dcWaiter("hide") + nx_report_success("Success!", HTML(paste0("Manifest submitted to: ", manifest_path))) + + # clean up old inputs/results + sapply(clean_tags, FUN = hide) + reset("inputFile-file") + DTableServer("tbl_preview", data.frame(NULL)) + } else { + dcWaiter("update", msg = HTML(paste0( + "Uh oh, looks like something went wrong!", + manifest_id, + " is not a valid Synapse ID. Try again?" + )), sleep = 0) } - # delete tmp manifest folder - unlink(tmp_file_path) }) }) diff --git a/shiny-server.conf b/shiny-server.conf new file mode 100644 index 00000000..6fd89378 --- /dev/null +++ b/shiny-server.conf @@ -0,0 +1,30 @@ +# Instruct Shiny Server to run applications as the user "shiny" +run_as shiny; + +# enable local app configurations using a file named .shiny_app.conf. +# This file can be placed within an application directory (alongside the server.R and ui.R files) +allow_app_override true; + +# Default time to keep http requests open is 45 seconds. +# Increase this for long connections with schematic. +http_keepalive_timeout 180; + +# Define a server that listens on port 3838 +server { + listen 3838; + + # Define a location at the base URL + location / { + # Directory in which to find the single application + # hosted by this server + app_dir /srv/shiny-server/app; + + # Log all Shiny output to files in this directory + log_dir /var/log/shiny-server; + + # When a user visits the base URL rather than a particular application, + # an index of the applications available in this directory will NOT be shown. + directory_index off; + + } +} diff --git a/ui.R b/ui.R index 92534dde..9009d529 100644 --- a/ui.R +++ b/ui.R @@ -1,12 +1,12 @@ -# This is the user-interface definition of a Shiny web application. -# You can find out more about building applications with Shiny here: -# -# http://shiny.rstudio.com -# -# This interface has been modified to be used specifically on Sage Bionetworks Synapse pages -# to log into Synapse as the currently logged in user from the web portal using the session token. -# -# https://www.synapse.org +# This R Shiny app functions as a frontend GUI to the Schematic python module +# github.com/sage-Bionetworks/schematic +# +# The UI is a dashboard that acts like a wizard. The user steps through each tab +# and selects from dropdown menus. +# After input selection, a spreadsheet is downloaded, modified outside of the +# app, and then uploaded to the app. +# The final steps are to validate the spreadsheet with Schematic and either +# report validation errors to the user or submit the data to synapse.org ui <- shinydashboardPlus::dashboardPage( title = "Data Curator", @@ -17,56 +17,39 @@ ui <- shinydashboardPlus::dashboardPage( span(class = "logo-lg", "Data Curator"), span(class = "logo-mini", "DCA") ), - leftUi = tagList( - dropdownBlock( - id = "header_selection_dropdown", - title = "Selection", - icon = icon("sliders-h"), - badgeStatus = "info", - fluidRow( - lapply(dropdown_types, function(x) { - div( - id = paste0("header_content_", x), - selectInput( - inputId = paste0("header_dropdown_", x), - label = NULL, - choices = character(0) - ) - ) - }), - actionButton("btn_header_update", NULL, icon("sync-alt"), class = "btn-shiny-effect") - ) - ) - ), uiOutput("logo") ), dashboardSidebar( width = 250, sidebarMenu( id = "tabs", - # uiOutput("title"), - # menuItem( - # "Instructions", - # tabName = "tab_instructions", - # icon = icon("book-open") - # ), menuItem( "Select DCC", tabName = "tab_asset_view", + icon = icon("server") + ), + menuItem( + "Select Project", + tabName = "tab_project", icon = icon("database") ), menuItem( - "Select your Dataset", - tabName = "tab_data", - icon = icon("mouse-pointer") + "Select Template", + tabName = "tab_template_select", + icon = icon("table") + ), + menuItem( + "Select Folder", + tabName = "tab_folder", + icon = icon("folder") ), menuItem( - "Get Metadata Template", + "Download Template", tabName = "tab_template", - icon = icon("table") + icon = icon("download") ), menuItem( - "Submit & Validate Metadata", + "Validate & Submit Metadata", tabName = "tab_upload", icon = icon("upload") ), @@ -78,200 +61,183 @@ ui <- shinydashboardPlus::dashboardPage( ) ), dashboardBody( - tags$head( - tags$style(sass(sass_file("www/scss/main.scss"))), - singleton(includeScript("www/js/readCookie.js")), - tags$script(htmlwidgets::JS("setTimeout(function(){history.pushState({}, 'Data Curator', window.location.pathname);},2000);")) - ), - uiOutput("sass"), - # load dependencies - use_notiflix_report(width = "400px"), - use_waiter(), - #dcamodules::use_dca(), - tabItems( - # First tab content - # tabItem( - # tabName = "tab_instructions", - # h2("Instructions for the Data Curator App (DCA):"), - # h3( - # "1. Go to", - # strong("Select your Dataset"), - # "tab - select your project; choose your folder and metadata template type matching your metadata." - # ), - # h3( - # "2. Go to", - # strong("Get Metadata Template"), - # "tab - click on the link to generate the metadata template, then fill out and download the file as a CSV. If you already have an annotated metadata template, you may skip this step." - # ), - # h3( - # "3. Go to", - # strong("Submit and Validate Metadata"), - # "tab - upload your filled CSV and validate your metadata. If you receive errors correct them, reupload your CSV, and revalidate until you receive no more errors. When your metadata is valid, you will be able to see a 'Submit' button. Press it to submit your metadata." - # ), - # switchTabUI("switchTab1", direction = "right") - # ), - # second tab content - tabItem( - tabName = "tab_asset_view", - #h2("Select the asset view"), - fluidRow( - box( - id = "box_pick_asset_view", - status = "primary", - width = 6, - title = "Select a DCC: ", - selectInput( - inputId = "dropdown_asset_view", - label = NULL, #"Asset View:", - choices = setNames(dcc_config$project_name, - dcc_config$synapse_asset_view)#"Generating..." - ), - actionButton("btn_asset_view", "Click to confirm", - class = "btn-primary-color" - ) + tags$head( + tags$style(sass(sass_file("www/scss/main.scss"))), + singleton(includeScript("www/js/readCookie.js")), + tags$script(htmlwidgets::JS("setTimeout(function(){history.pushState({}, 'Data Curator', window.location.pathname);},2000);")) + ), + uiOutput("sass"), + # load dependencies + use_notiflix_report(width = "400px"), + use_waiter(), + tabItems( + # second tab content + tabItem( + tabName = "tab_asset_view", + fluidRow( + box( + id = "box_pick_asset_view", + status = "primary", + width = 6, + title = "Select a DCC: ", + selectInput( + inputId = "dropdown_asset_view", + label = NULL, + choices = setNames(dcc_config$synapse_asset_view, + dcc_config$project_name) + ), + actionButton("btn_asset_view", "Go", + class = "btn-primary-color" + ) + ) + ) + ), + tabItem( + tabName = "tab_project", + fluidRow( + box( + id = "box_pick_project", + status = "primary", + width = 6, + title = "Select a Project: ", + selectInput( + inputId = "dropdown_project", + label = NULL, + choices = "Generating..." + ), + actionButton("btn_project", "Go", + class = "btn-primary-color" ) - )#, - #switchTabUI("switchTab1", direction = "right") # remove arrow from assetview page. ), - tabItem( - tabName = "tab_data", - h2("Set Dataset and Data Type for Curation"), - fluidRow( - box( - id = "box_pick_project", - status = "primary", - width = 6, - title = "Choose a Project and Folder: ", - selectInput( - inputId = "dropdown_project", - label = "Project:", - choices = "Generating..." - ), - selectInput( - inputId = "dropdown_folder", - label = "Dataset:", - choices = "Generating..." - ), - helpText( - "If your recently updated folder does not appear, please wait for a few minutes and refresh" - ) - ), - box( - id = "box_pick_manifest", - status = "primary", - width = 6, - title = "Choose a Data Type: ", - selectInput( - inputId = "dropdown_template", - label = "Data Type Template:", - choices = "Generating..." - ) - ), - if (dca_schematic_api != "offline" && Sys.getenv("DCA_COMPLIANCE_DASHBOARD")==TRUE) dashboardUI("dashboard") + ), + ), + tabItem( + tabName = "tab_template_select", + fluidRow( + box( + id = "box_pick_template", + status = "primary", + width = 6, + title = "Select a Template: ", + selectInput( + inputId = "dropdown_template", + label = NULL, + choices = "Generating..." ), - switchTabUI("switchTab2", direction = "right") + actionButton("btn_template_select", "Go", + class = "btn-primary-color" + ) + ) + ), + ), + tabItem( + tabName = "tab_folder", + fluidRow( + box( + id = "box_pick_folder", + status = "primary", + width = 6, + title = "Select a Folder: ", + selectInput( + inputId = "dropdown_folder", + label = NULL, + choices = "Generating..." ), - # Third tab item - tabItem( - tabName = "tab_template", - useShinyjs(), - h2("Download Template for Selected Folder"), - if (Sys.getenv("DCA_MANIFEST_OUTPUT_FORMAT") != "excel") { - fluidRow( - box( - title = "Get Link, Annotate, and Download Template as CSV", - status = "primary", - width = 12, - actionButton("btn_template", "Click to Generate Google Sheets Template", - class = "btn-primary-color" + actionButton("btn_folder", "Go", + class = "btn-primary-color" + ) + ) + ) + ), + tabItem( + tabName = "tab_template", + useShinyjs(), + fluidRow( + box( + title = textOutput('template_title'), + status = "primary", + width = 12, + hidden( + div( + id = "div_download_data", + height = "100%", + downloadButton("downloadData", "Download") ), - hidden( - div( - id = "div_template_warn", - height = "100%", - htmlOutput("text_template_warn") - ), - div( - id = "div_template", - height = "100%", - htmlOutput("text_template") - ) + div( + id = "div_template", + height = "100%", + htmlOutput("text_template") ), - helpText("This link will lead to an empty template or your previously submitted template with new files if applicable.") - ) - )}else{ - fluidRow( - box( - title = "Or download data as an Excel sheet", - status = "primary", - width = 12, - downloadButton("downloadData", "Download Excel Spreadsheet."), - hidden( - div( - id = "div_template_warn_xls", - height = "100%", - htmlOutput("text_template_warn_xls") - ), - div( - id = "div_template_xls", - height = "100%", - htmlOutput("text_template_xls") - ) + helpText("Note: After downloading, spreadsheet apps may add blank", + "rows that must be removed before validating.") ), - helpText("This link will leads to an empty template or your previously submitted template with new files if applicable.") - ) - )}, - switchTabUI("switchTab3", direction = "both") - ), - # Fourth tab content - tabItem( - tabName = "tab_upload", - h2("Submit & Validate a Filled Metadata Template"), - fluidRow( - box( - title = "Upload Filled Metadata as a CSV", - status = "primary", - width = 12, - csvInfileUI("inputFile") - ), - box( - title = "Metadata Preview", - collapsible = TRUE, - status = "primary", - width = 12, - DTableUI("tbl_preview") - ), - box( - title = "Validate Filled Metadata", - status = "primary", - collapsible = TRUE, - width = 12, - actionButton("btn_validate", "Validate Metadata", class = "btn-primary-color"), + hidden( div( - id = "div_validate", + id = "div_template_warn_xls", height = "100%", - ValidationMsgUI("text_validate") + htmlOutput("text_template_warn_xls") ), - DTableUI("tbl_validate"), - uiOutput("val_gsheet"), - uiOutput("dl_manifest"), - helpText( - HTML("If you have an error, please try editing locally or on google sheet. - Reupload your CSV and press the validate button as needed.") + div( + id = "div_template_xls", + height = "100%", + htmlOutput("text_template_xls") ) - ), - box( - title = "Submit Validated Metadata to Synapse", - status = "primary", - width = 12, - uiOutput("submit") ) ), - switchTabUI("switchTab4", direction = "left") + ), + switchTabUI("switchTab5", direction = "right") + ), + # Fourth tab content + tabItem( + tabName = "tab_upload", + fluidRow( + box( + title = "Upload Filled Metadata as a CSV", + status = "primary", + width = 12, + csvInfileUI("inputFile"), + helpText("Note: Remove blank rows from your file before uploading.") + ), + box( + title = "Metadata Preview", + collapsible = TRUE, + status = "primary", + width = 12, + DTableUI("tbl_preview"), + id = "box_preview" + ), + box( + title = "Validate Filled Metadata", + status = "primary", + collapsible = TRUE, + width = 12, + actionButton("btn_validate", "Validate Metadata", class = "btn-primary-color"), + div( + id = "div_validate", + height = "100%", + ValidationMsgUI("text_validate") + ), + DTableUI("tbl_validate"), + uiOutput("val_gsheet"), + uiOutput("dl_manifest"), + helpText( + HTML("If you have an error, please try editing locally or on google sheet. + Reupload your CSV and press the validate button as needed.") + ), + id = "box_validate" + ), + box( + title = "Submit Validated Metadata to Synapse", + status = "primary", + width = 12, + uiOutput("submit"), + id = "box_submit" ) ), - # waiter loading screen - dcWaiter("show", landing = TRUE) + ) + ), + # waiter loading screen + dcWaiter("show", landing = TRUE) ) ) @@ -282,10 +248,10 @@ uiFunc <- function(req) { } if (!has_auth_code(parseQueryString(req$QUERY_STRING))) { authorization_url <- oauth2.0_authorize_url(api, app, scope = scope) - return(tags$script(HTML(sprintf( - "location.replace(\"%s\");", - authorization_url - )))) + redir <- tags$script(HTML( + sprintf("location.replace(\"%s\");", authorization_url) + )) + return(redir) } else { ui } diff --git a/www/img/ADKnowledgePortal.png b/www/img/ADKnowledgePortal.png new file mode 100644 index 0000000000000000000000000000000000000000..9baf7ef954ee8a4675897377b68c003f32ef70ae GIT binary patch literal 41928 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 literal 0 HcmV?d00001 diff --git a/www/img/VEOIBD Logo temp.png b/www/img/VEOIBD Logo temp.png new file mode 100644 index 0000000000000000000000000000000000000000..72d2c688f3d9972e7fdced896a7779d091e34ce3 GIT binary patch literal 26451 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 literal 0 HcmV?d00001 diff --git a/www/scss/basic/_button.scss b/www/scss/basic/_button.scss index be6fa71d..a1c13979 100644 --- a/www/scss/basic/_button.scss +++ b/www/scss/basic/_button.scss @@ -192,7 +192,8 @@ cursor: pointer; outline: none !important; - &:hover { +// Don't apply a hover animation when the button is disabled + &:not(.disable):hover { background: $primary_col; color: #fff; box-shadow: $float_shadow2; diff --git a/www/template_config/VEOIBD_config.json b/www/template_config/VEOIBD_config.json new file mode 100644 index 00000000..86d50933 --- /dev/null +++ b/www/template_config/VEOIBD_config.json @@ -0,0 +1,31 @@ +{ + "manifest_schemas": [ + { + "display_name": "Clinical Metadata Template", + "schema_name": "ClinicalMetadataTemplate", + "type": "record" + }, + { + "display_name": "Biospecimen Metadata Template", + "schema_name": "BiospecimenMetadataTemplate", + "type": "record" + }, + { + "display_name": "Bulk RNAseq Template", + "schema_name": "BulkRNAseqTemplate", + "type": "record" + }, + { + "display_name": "scRNAseq Template", + "schema_name": "ScRNAseqTemplate", + "type": "record" + }, + { + "display_name": "Organoid Template", + "schema_name": "OrganoidTemplate", + "type": "record" + } + ], + "service_version": "v23.1.1", + "schema_version": "" + } \ No newline at end of file diff --git a/www/template_config/adkp_config.json b/www/template_config/adkp_config.json new file mode 100644 index 00000000..5cd6c497 --- /dev/null +++ b/www/template_config/adkp_config.json @@ -0,0 +1,46 @@ +{ + "manifest_schemas": [ + { + "display_name": "Diverse Cohorts Clinical Metadata", + "schema_name": "DiverseCohortsClinicalMetadata", + "type": "record" + }, + { + "display_name": "Diverse Cohorts Biospecimen Metadata", + "schema_name": "DiverseCohortsBiospecimenMetadata", + "type": "record" + }, + { + "display_name": "WGS Assay Metadata", + "schema_name": "WholeGenomeSeqAssayMetadata", + "type": "record" + }, + { + "display_name": "WGS VCF Files", + "schema_name": "WholeGenomeSeqVCFFileAnnotations", + "type": "file" + }, + { + "display_name": "WGS Raw Files", + "schema_name": "WholeGenomeSeqRawFileAnnotations", + "type": "file" + }, + { + "display_name": "Bulk RNAseq Assay Metadata", + "schema_name": "BulkRNASeqAssayMetadata", + "type": "record" + }, + { + "display_name": "Bulk RNAseq Raw Files", + "schema_name": "BulkRNASeqRawFileAnnotations", + "type": "file" + }, + { + "display_name": "Bulk RNAseq Counts Files", + "schema_name": "BulkRNASeqCountsFileAnnotations", + "type": "file" + } + ], + "service_version": "v23.1.1", + "schema_version": "" + } \ No newline at end of file From 357de88ed72b89dd914943127da1dc4b98d0eb2a Mon Sep 17 00:00:00 2001 From: Dan Lu <90745557+danlu1@users.noreply.github.com> Date: Thu, 11 May 2023 13:59:15 -0700 Subject: [PATCH 05/40] update config files, logo, and data models (#529) * update config files, logo, and data models * update config * Update dcc_config.csv * Update dcc_config.csv --- dcc_config.csv | 2 +- www/img/VEOIBD Logo temp.png | Bin 26451 -> 0 bytes www/img/VEOIBD Logo.png | Bin 0 -> 35321 bytes www/template_config/VEOIBD_config.json | 70 ++++++++++++++----------- 4 files changed, 41 insertions(+), 31 deletions(-) delete mode 100644 www/img/VEOIBD Logo temp.png create mode 100644 www/img/VEOIBD Logo.png diff --git a/dcc_config.csv b/dcc_config.csv index cfd4bb32..1eef0fd4 100644 --- a/dcc_config.csv +++ b/dcc_config.csv @@ -4,5 +4,5 @@ HTAN All Projects,syn20446927,https://raw.githubusercontent.com/ncihtan/data-mod Cancer Complexity Knowledge Portal - Database,syn27210848,https://raw.githubusercontent.com/mc2-center/data-models/main/mc2.model.jsonld,www/template_config/mc2_config.json,excel,FALSE,upsert,table_and_file,FALSE,#407BA0,#5BB0B5,#191919 INCLUDE Data Management Core,syn30109515,https://raw.githubusercontent.com/include-dcc/include-linkml/schematic-updates/src/schematic/include_schematic_linkml.jsonld,www/template_config/include_config.json,excel,TRUE,replace,table_and_file,FALSE,#2a668d,#184e71,#191919 AD Knowledge Portal,syn51324810,https://raw.githubusercontent.com/adknowledgeportal/data-models/main/divco.data.model.v1.jsonld,www/template_config/adkp_config.json,excel,TRUE,replace,file_only,FALSE,#2a668d,#184e71,#191919 -VEOIBD,syn51397378,https://raw.githubusercontent.com/VEOIBD/data_models/main/veoibd.data.model.jsonld,www/template_config/VEOIBD_config.json,excel,TRUE,replace,table_and_file,FALSE,#2a668d,#184e72,#191920 +VEOIBD,syn51397378,https://raw.githubusercontent.com/VEOIBD/data_models/main/veoibd.data.model.jsonld,www/template_config/VEOIBD_config.json,excel,TRUE,upsert,file_only,FALSE,#2a668d,#184e72,#191920 BTC DCC,syn51407795,https://github.com/Sage-Bionetworks/btc-data-models/blob/main/btc.model.jsonld,www/template_config/example_config.json,excel,TRUE,replace,table_and_file,FALSLE,#2a668d,#184e72,#191920 diff --git a/www/img/VEOIBD Logo temp.png b/www/img/VEOIBD Logo temp.png deleted file mode 100644 index 72d2c688f3d9972e7fdced896a7779d091e34ce3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26451 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 diff --git a/www/img/VEOIBD Logo.png b/www/img/VEOIBD Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e7843df9e834c644eb48af26352004497d39123c GIT binary patch literal 35321 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

Ms{bN!LA2wO+chyAga(B#IVvp8~ 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 literal 0 HcmV?d00001 diff --git a/www/template_config/VEOIBD_config.json b/www/template_config/VEOIBD_config.json index 86d50933..0b14089a 100644 --- a/www/template_config/VEOIBD_config.json +++ b/www/template_config/VEOIBD_config.json @@ -1,31 +1,41 @@ { - "manifest_schemas": [ - { - "display_name": "Clinical Metadata Template", - "schema_name": "ClinicalMetadataTemplate", - "type": "record" - }, - { - "display_name": "Biospecimen Metadata Template", - "schema_name": "BiospecimenMetadataTemplate", - "type": "record" - }, - { - "display_name": "Bulk RNAseq Template", - "schema_name": "BulkRNAseqTemplate", - "type": "record" - }, - { - "display_name": "scRNAseq Template", - "schema_name": "ScRNAseqTemplate", - "type": "record" - }, - { - "display_name": "Organoid Template", - "schema_name": "OrganoidTemplate", - "type": "record" - } - ], - "service_version": "v23.1.1", - "schema_version": "" - } \ No newline at end of file + "manifest_schemas": [ + { + "display_name": "Clinical Metadata Template", + "schema_name": "ClinicalMetadataTemplate", + "type": "record" + }, + { + "display_name": "Biospecimen Metadata Template", + "schema_name": "BiospecimenMetadataTemplate", + "type": "record" + }, + { + "display_name": "Bulk RNAseq Assay Template", + "schema_name": "BulkRNAseqAssayTemplate", + "type": "record" + }, + { + "display_name": "scRNAseq Assay Template", + "schema_name": "ScRNAseqAssayTemplate", + "type": "record" + }, + { + "display_name": "Bulk RNAseq Raw File Annotations", + "schema_name": "BulkRNAseqRawFileAnnotations", + "type": "file" + }, + { + "display_name": "Bulk RNASeq Counts File Annotations", + "schema_name": "BulkRNASeqCountsFileAnnotations", + "type": "file" + }, + { + "display_name": "Metadata File Annotations", + "schema_name": "MetadataFileAnnotations", + "type": "file" + } + ], + "service_version": "v23.1.1", + "schema_version": "" +} \ No newline at end of file From 4e096122fba0f5f639786f0dd68a507ea8c0dc51 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Thu, 11 May 2023 14:01:28 -0700 Subject: [PATCH 06/40] Dev schematic access token (#527) * Change input_token to access_token for schematic update * Change input_token to access_token for schematic update in REST API functions * Change input_token to access_token for schematic update in REST API functions in functions dir --- R/schematic_rest_api.R | 40 +++++++++++++++++----------------- functions/schematic_rest_api.R | 40 +++++++++++++++++----------------- server.R | 14 ++++++------ 3 files changed, 47 insertions(+), 47 deletions(-) diff --git a/R/schematic_rest_api.R b/R/schematic_rest_api.R index 160d29d1..d498fe3a 100644 --- a/R/schematic_rest_api.R +++ b/R/schematic_rest_api.R @@ -12,17 +12,17 @@ check_success <- function(x){ #' @description Download an existing manifest #' @param url URI of API endpoint -#' @param input_token Synapse PAT +#' @param access_token Synapse PAT #' @param asset_view ID of view listing all project data assets #' @param dataset_id the parent ID of the manifest #' @param as_json if True return the manifest in JSON format #' @returns a csv of the manifest #' @export -manifest_download <- function(url = "http://localhost:3001/v1/manifest/download", input_token, asset_view, dataset_id, as_json=TRUE, new_manifest_name=NULL) { +manifest_download <- function(url = "http://localhost:3001/v1/manifest/download", access_token, asset_view, dataset_id, as_json=TRUE, new_manifest_name=NULL) { request <- httr::GET( url = url, query = list( - input_token = input_token, + access_token = access_token, asset_view = asset_view, dataset_id = dataset_id, as_json = as_json, @@ -57,7 +57,7 @@ manifest_generate <- function(url="http://localhost:3001/v1/manifest/generate", schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #nolint title, data_type, use_annotations="false", dataset_id=NULL, - asset_view, output_format, input_token = NULL) { + asset_view, output_format, access_token = NULL) { req <- httr::GET(url, query = list( @@ -68,7 +68,7 @@ manifest_generate <- function(url="http://localhost:3001/v1/manifest/generate", dataset_id=dataset_id, asset_view=asset_view, output_format=output_format, - input_token = input_token + access_token = access_token )) check_success(req) @@ -148,14 +148,14 @@ manifest_validate <- function(url="http://localhost:3001/v1/model/validate", #' @param schema_url URL to a schema jsonld #' @param data_type Type of dataset #' @param dataset_id Synapse ID of existing manifest -#' @param input_token Synapse login cookie, PAT, or API key. +#' @param access_token Synapse login cookie, PAT, or API key. #' @param csv_file Filepath of csv to validate #' #' @returns TRUE if successful upload or validate errors if not. #' @export model_submit <- function(url="http://localhost:3001/v1/model/submit", schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #notlint - data_type, dataset_id, restrict_rules=FALSE, input_token, json_str=NULL, asset_view, + data_type, dataset_id, restrict_rules=FALSE, access_token, json_str=NULL, asset_view, use_schema_label=TRUE, manifest_record_type="table_and_file", file_name, table_manipulation="replace") { req <- httr::POST(url, @@ -164,7 +164,7 @@ model_submit <- function(url="http://localhost:3001/v1/model/submit", schema_url=schema_url, data_type=data_type, dataset_id=dataset_id, - input_token=input_token, + access_token=access_token, restrict_rules=restrict_rules, json_str=json_str, asset_view=asset_view, @@ -218,20 +218,20 @@ model_component_requirements <- function(url="http://localhost:3001/v1/model/com #' @param syn_master_file_view synapse ID of master file view. #' @param syn_master_file_name Synapse storage manifest file name. #' @param project_id synapse ID of a storage project. -#' @param input_token synapse PAT +#' @param access_token synapse PAT #' #'@export storage_project_datasets <- function(url="http://localhost:3001/v1/storage/project/datasets", asset_view, project_id, - input_token) { + access_token) { req <- httr::GET(url, #add_headers(Authorization=paste0("Bearer ", pat)), query=list( asset_view=asset_view, project_id=project_id, - input_token=input_token) + access_token=access_token) ) check_success(req) @@ -243,17 +243,17 @@ storage_project_datasets <- function(url="http://localhost:3001/v1/storage/proje #' @param url URL to schematic API endpoint #' @param syn_master_file_view synapse ID of master file view. #' @param syn_master_file_name Synapse storage manifest file name. -#' @param input_token synapse PAT +#' @param access_token synapse PAT #' #' @export storage_projects <- function(url="http://localhost:3001/v1/storage/projects", asset_view, - input_token) { + access_token) { req <- httr::GET(url, query = list( asset_view=asset_view, - input_token=input_token + access_token=access_token )) check_success(req) @@ -268,13 +268,13 @@ storage_projects <- function(url="http://localhost:3001/v1/storage/projects", #' @param dataset_id synapse ID of a storage dataset. #' @param file_names a list of files with particular names (i.e. Sample_A.txt). If you leave it empty, it will return all dataset files under the dataset ID. #' @param full_path Boolean. If True return the full path as part of this filename; otherwise return just base filename -#' @param input_token synapse PAT +#' @param access_token synapse PAT #' #' @export storage_dataset_files <- function(url="http://localhost:3001/v1/storage/dataset/files", asset_view, dataset_id, file_names=list(), - full_path=FALSE, input_token) { + full_path=FALSE, access_token) { req <- httr::GET(url, #add_headers(Authorization=paste0("Bearer ", pat)), @@ -283,7 +283,7 @@ storage_dataset_files <- function(url="http://localhost:3001/v1/storage/dataset/ dataset_id=dataset_id, file_names=file_names, full_path=full_path, - input_token=input_token)) + access_token=access_token)) check_success(req) httr::content(req) @@ -292,16 +292,16 @@ storage_dataset_files <- function(url="http://localhost:3001/v1/storage/dataset/ #' /storage/asset/table #' #' @param url URL to schematic API endpoint -#' @param input_token synapse PAT +#' @param access_token synapse PAT #' @param asset_view Synapse ID of asset view #' @export get_asset_view_table <- function(url="http://localhost:3001/v1/storage/assets/tables", - input_token, asset_view, return_type="json") { + access_token, asset_view, return_type="json") { req <- httr::GET(url, query=list( asset_view=asset_view, - input_token=input_token, + access_token=access_token, return_type=return_type)) check_success(req) diff --git a/functions/schematic_rest_api.R b/functions/schematic_rest_api.R index 160d29d1..d498fe3a 100644 --- a/functions/schematic_rest_api.R +++ b/functions/schematic_rest_api.R @@ -12,17 +12,17 @@ check_success <- function(x){ #' @description Download an existing manifest #' @param url URI of API endpoint -#' @param input_token Synapse PAT +#' @param access_token Synapse PAT #' @param asset_view ID of view listing all project data assets #' @param dataset_id the parent ID of the manifest #' @param as_json if True return the manifest in JSON format #' @returns a csv of the manifest #' @export -manifest_download <- function(url = "http://localhost:3001/v1/manifest/download", input_token, asset_view, dataset_id, as_json=TRUE, new_manifest_name=NULL) { +manifest_download <- function(url = "http://localhost:3001/v1/manifest/download", access_token, asset_view, dataset_id, as_json=TRUE, new_manifest_name=NULL) { request <- httr::GET( url = url, query = list( - input_token = input_token, + access_token = access_token, asset_view = asset_view, dataset_id = dataset_id, as_json = as_json, @@ -57,7 +57,7 @@ manifest_generate <- function(url="http://localhost:3001/v1/manifest/generate", schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #nolint title, data_type, use_annotations="false", dataset_id=NULL, - asset_view, output_format, input_token = NULL) { + asset_view, output_format, access_token = NULL) { req <- httr::GET(url, query = list( @@ -68,7 +68,7 @@ manifest_generate <- function(url="http://localhost:3001/v1/manifest/generate", dataset_id=dataset_id, asset_view=asset_view, output_format=output_format, - input_token = input_token + access_token = access_token )) check_success(req) @@ -148,14 +148,14 @@ manifest_validate <- function(url="http://localhost:3001/v1/model/validate", #' @param schema_url URL to a schema jsonld #' @param data_type Type of dataset #' @param dataset_id Synapse ID of existing manifest -#' @param input_token Synapse login cookie, PAT, or API key. +#' @param access_token Synapse login cookie, PAT, or API key. #' @param csv_file Filepath of csv to validate #' #' @returns TRUE if successful upload or validate errors if not. #' @export model_submit <- function(url="http://localhost:3001/v1/model/submit", schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #notlint - data_type, dataset_id, restrict_rules=FALSE, input_token, json_str=NULL, asset_view, + data_type, dataset_id, restrict_rules=FALSE, access_token, json_str=NULL, asset_view, use_schema_label=TRUE, manifest_record_type="table_and_file", file_name, table_manipulation="replace") { req <- httr::POST(url, @@ -164,7 +164,7 @@ model_submit <- function(url="http://localhost:3001/v1/model/submit", schema_url=schema_url, data_type=data_type, dataset_id=dataset_id, - input_token=input_token, + access_token=access_token, restrict_rules=restrict_rules, json_str=json_str, asset_view=asset_view, @@ -218,20 +218,20 @@ model_component_requirements <- function(url="http://localhost:3001/v1/model/com #' @param syn_master_file_view synapse ID of master file view. #' @param syn_master_file_name Synapse storage manifest file name. #' @param project_id synapse ID of a storage project. -#' @param input_token synapse PAT +#' @param access_token synapse PAT #' #'@export storage_project_datasets <- function(url="http://localhost:3001/v1/storage/project/datasets", asset_view, project_id, - input_token) { + access_token) { req <- httr::GET(url, #add_headers(Authorization=paste0("Bearer ", pat)), query=list( asset_view=asset_view, project_id=project_id, - input_token=input_token) + access_token=access_token) ) check_success(req) @@ -243,17 +243,17 @@ storage_project_datasets <- function(url="http://localhost:3001/v1/storage/proje #' @param url URL to schematic API endpoint #' @param syn_master_file_view synapse ID of master file view. #' @param syn_master_file_name Synapse storage manifest file name. -#' @param input_token synapse PAT +#' @param access_token synapse PAT #' #' @export storage_projects <- function(url="http://localhost:3001/v1/storage/projects", asset_view, - input_token) { + access_token) { req <- httr::GET(url, query = list( asset_view=asset_view, - input_token=input_token + access_token=access_token )) check_success(req) @@ -268,13 +268,13 @@ storage_projects <- function(url="http://localhost:3001/v1/storage/projects", #' @param dataset_id synapse ID of a storage dataset. #' @param file_names a list of files with particular names (i.e. Sample_A.txt). If you leave it empty, it will return all dataset files under the dataset ID. #' @param full_path Boolean. If True return the full path as part of this filename; otherwise return just base filename -#' @param input_token synapse PAT +#' @param access_token synapse PAT #' #' @export storage_dataset_files <- function(url="http://localhost:3001/v1/storage/dataset/files", asset_view, dataset_id, file_names=list(), - full_path=FALSE, input_token) { + full_path=FALSE, access_token) { req <- httr::GET(url, #add_headers(Authorization=paste0("Bearer ", pat)), @@ -283,7 +283,7 @@ storage_dataset_files <- function(url="http://localhost:3001/v1/storage/dataset/ dataset_id=dataset_id, file_names=file_names, full_path=full_path, - input_token=input_token)) + access_token=access_token)) check_success(req) httr::content(req) @@ -292,16 +292,16 @@ storage_dataset_files <- function(url="http://localhost:3001/v1/storage/dataset/ #' /storage/asset/table #' #' @param url URL to schematic API endpoint -#' @param input_token synapse PAT +#' @param access_token synapse PAT #' @param asset_view Synapse ID of asset view #' @export get_asset_view_table <- function(url="http://localhost:3001/v1/storage/assets/tables", - input_token, asset_view, return_type="json") { + access_token, asset_view, return_type="json") { req <- httr::GET(url, query=list( asset_view=asset_view, - input_token=input_token, + access_token=access_token, return_type=return_type)) check_success(req) diff --git a/server.R b/server.R index c23eb00d..22a4b685 100644 --- a/server.R +++ b/server.R @@ -233,7 +233,7 @@ shinyServer(function(input, output, session) { reticulate = storage_projects_py(synapse_driver, access_token), rest = storage_projects(url=file.path(api_uri, "v1/storage/projects"), asset_view = selected$master_asset_view(), - input_token = access_token), + access_token = access_token), list(list("Offline Project A", "Offline Project")) ) data_list$projects(list2Vector(data_list_raw)) @@ -295,7 +295,7 @@ shinyServer(function(input, output, session) { url=file.path(api_uri, "v1/storage/project/datasets"), asset_view = .asset_view, project_id=project_id, - input_token=access_token), + access_token=access_token), list(list("DatatypeA", "DatatypeA"), list("DatatypeB","DatatypeB")) ) @@ -374,7 +374,7 @@ shinyServer(function(input, output, session) { rest = storage_dataset_files(url=file.path(api_uri, "v1/storage/dataset/files"), asset_view = selected$master_asset_view(), dataset_id = selected$folder(), - input_token=access_token), + access_token=access_token), list(list("DatatypeA", "DatatypeA"), list("DatatypeB", "DatatypeB"))) # update files list in the folder @@ -496,7 +496,7 @@ shinyServer(function(input, output, session) { asset_view = .asset_view, use_annotations = FALSE, output_format = .output_format, - input_token=access_token + access_token=access_token ), { message("Downloading offline manifest") @@ -738,7 +738,7 @@ shinyServer(function(input, output, session) { rest = storage_dataset_files(url=file.path(api_uri, "v1/storage/dataset/files"), asset_view = selected$master_asset_view(), dataset_id = selected$folder(), - input_token=access_token)) + access_token=access_token)) data_list$files(list2Vector(file_list_raw)) } @@ -776,7 +776,7 @@ shinyServer(function(input, output, session) { schema_url = .data_model, data_type = .schema, dataset_id = .folder, - input_token = access_token, + access_token = access_token, restrict_rules = FALSE, file_name = tmp_file_path, asset_view = .asset_view, @@ -816,7 +816,7 @@ shinyServer(function(input, output, session) { schema_url = .data_model, data_type = .schema, dataset_id = .folder, - input_token = access_token, + access_token = access_token, restrict_rules = FALSE, file_name = tmp_file_path, asset_view = .asset_view, From 4e6d24ff7f5f94a33fab822455a40607bc1e06a3 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Fri, 12 May 2023 10:52:20 -0700 Subject: [PATCH 07/40] Update veoibd logo (#530) --- functions/utils.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/utils.R b/functions/utils.R index 533a26f8..6e0428d4 100644 --- a/functions/utils.R +++ b/functions/utils.R @@ -61,7 +61,7 @@ update_logo <- function(project = "sage") { syn51324810 = list(href = "https://adknowledgeportal.synapse.org/", img_src = "img/ADKnowledgePortal.png"), syn51397378 = list(href = "veoibd.org", - img_src = "img/VEOIBD Logo temp.png"), + img_src = "img/VEOIBD Logo.png"), list(href = "https://synapse.org", img_src = "img/Logo_Sage_Logomark.png") ) From d7aa486da0f0ddced0f70381ac13b4dd16514964 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Fri, 12 May 2023 11:31:39 -0700 Subject: [PATCH 08/40] Fair demo upsert (#531) * Add a new demo fileview that submits with upsert * Add template dropdown config for upsert demo --- dcc_config.csv | 3 ++- www/template_config/demo_upsert_config.json | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 www/template_config/demo_upsert_config.json diff --git a/dcc_config.csv b/dcc_config.csv index 1eef0fd4..c55a0ab5 100644 --- a/dcc_config.csv +++ b/dcc_config.csv @@ -1,8 +1,9 @@ project_name,synapse_asset_view,data_model_url,template_menu_config_file,manifest_output_format,submit_use_schema_labels,submit_table_manipulation,submit_manifest_record_type,use_compliance_dashboard,primary_col,secondary_col,sidebar_col DCA Demo,syn33715412,https://raw.githubusercontent.com/Sage-Bionetworks/data-models/main/example.model.jsonld,www/template_config/example_config.json,google_sheet,TRUE,replace,table_and_file,FALSE,#2a668d,#184e71,#191919 +DCA Demo Upsert,syn51489635,https://raw.githubusercontent.com/Sage-Bionetworks/schematic/develop/tests/data/example.single_rule.model.jsonld,www/template_config/demo_upsert_config.json,excel,FALSE,upsert,table_and_file,FALSE,#2a668d,#184e71,#191919 HTAN All Projects,syn20446927,https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld,www/template_config/htan_config.json,excel,TRUE,replace,table_and_file,FALSE,#605ca8,#5F008C,#191919 Cancer Complexity Knowledge Portal - Database,syn27210848,https://raw.githubusercontent.com/mc2-center/data-models/main/mc2.model.jsonld,www/template_config/mc2_config.json,excel,FALSE,upsert,table_and_file,FALSE,#407BA0,#5BB0B5,#191919 INCLUDE Data Management Core,syn30109515,https://raw.githubusercontent.com/include-dcc/include-linkml/schematic-updates/src/schematic/include_schematic_linkml.jsonld,www/template_config/include_config.json,excel,TRUE,replace,table_and_file,FALSE,#2a668d,#184e71,#191919 AD Knowledge Portal,syn51324810,https://raw.githubusercontent.com/adknowledgeportal/data-models/main/divco.data.model.v1.jsonld,www/template_config/adkp_config.json,excel,TRUE,replace,file_only,FALSE,#2a668d,#184e71,#191919 VEOIBD,syn51397378,https://raw.githubusercontent.com/VEOIBD/data_models/main/veoibd.data.model.jsonld,www/template_config/VEOIBD_config.json,excel,TRUE,upsert,file_only,FALSE,#2a668d,#184e72,#191920 -BTC DCC,syn51407795,https://github.com/Sage-Bionetworks/btc-data-models/blob/main/btc.model.jsonld,www/template_config/example_config.json,excel,TRUE,replace,table_and_file,FALSLE,#2a668d,#184e72,#191920 +BTC DCC,syn51407795,https://github.com/Sage-Bionetworks/btc-data-models/blob/main/btc.model.jsonld,www/template_config/example_config.json,excel,TRUE,replace,table_and_file,FALSLE,#2a668d,#184e72,#191920 \ No newline at end of file diff --git a/www/template_config/demo_upsert_config.json b/www/template_config/demo_upsert_config.json new file mode 100644 index 00000000..ba4b01d1 --- /dev/null +++ b/www/template_config/demo_upsert_config.json @@ -0,0 +1,11 @@ +{ + "manifest_schemas": [ + { + "display_name": "MockRDB", + "schema_name": "MockRDB", + "type": "record" + } + ], + "service_version": "", + "schema_version": "" +} From 5766314f8091b4e5ed3f14977660629181c1c04c Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Tue, 16 May 2023 11:54:02 -0700 Subject: [PATCH 09/40] Dev update dcc docs (#533) * Update template config script name * Clarify how to set up config.json for a simple data model. * Remove header link --- README.md | 114 +++++++++++++++++++++++++++--------------------------- 1 file changed, 58 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index fdbe3bd1..49b5cd9c 100644 --- a/README.md +++ b/README.md @@ -2,54 +2,49 @@ ## Introduction -The _Data Curator App_ is an R Shiny app that serves as the _frontend_ to the [schematic Python package](github.com/sage-Bionetworks/schematic/). It allows data contributors to easily annotate, validate and submit their metadata. - - -## Quickstart {#quickstart} - -Sage Bionetworks hosts a version of Data Curator App for its collaborators. [Access it here](link TBD). -To configure your project for this version, fork this repo and append [dcc_config.csv](dcc_config.csv). Then submit a pull request to [this branch](https://github.com/Sage-Bionetworks/data_curator/tree/beta-schematic-rest-api). -[dcc_config.csv](dcc_config.csv) contains the following. **Bold fields** are required: - -- **project_name**: The display name of your project -- **synapse_asset_view**: The synapse ID of your project's asset view -- **data_model_url**: A URL to your data model. Must be the **raw** file if using GitHub -- **template_menu_config_file**: www/template_config/_config.json. This file can be generated by hand or with [config_schema.py](.github/config_schema.py) -- **manifest_output_format**: "excel" -- **submit_use_schema_labels**: Schematic option to use schema labels when submitting (default TRUE) TRUE or FALSE -- **submit_table_manipulation**: Schematic option when submitting (default "replace") "replace" or "upsert" -- **submit_manifest_record_type**: Schematic option when submitting. -- **use_compliance_dashboard**: (default FALSE) TRUE or FALSE -- primary_col: (default Sage theme) hexadecimal color code -- secondary_col; (default Sage theme) hexadecimal color code -- sidebar_col: (default Sage theme) hexadecimal color code - -Your pull request should include: -- The modifications to [dcc_config.csv](dcc_config.csv) -- A dropdown template config [www/template_config/_config.json](www/template_config/config.json) -- Optional: A .png or .svg logo for your project in `www/img` - -Other things you will need: -- A Synapse asset view for you project -- A [data model](#datamodel) +The *Data Curator App* is an R Shiny app that serves as the *frontend* to the [schematic Python package](github.com/sage-Bionetworks/schematic/). It allows data contributors to easily annotate, validate and submit their metadata. + +## Quickstart + +Sage Bionetworks hosts a version of Data Curator App for its collaborators. [Access it here](link%20TBD).\ +To configure your project for this version, fork this repo and append [dcc_config.csv](dcc_config.csv). Then submit a pull request to [this branch](https://github.com/Sage-Bionetworks/data_curator/tree/beta-schematic-rest-api). [dcc_config.csv](dcc_config.csv) contains the following. **Bold fields** are required: + +- **project_name**: The display name of your project\ +- **synapse_asset_view**: The synapse ID of your project's asset view\ +- **data_model_url**: A URL to your data model. Must be the **raw** file if using GitHub\ +- **template_menu_config_file**: www/template_config/\_config.json. This file can be generated by hand or with [config_schema.py](.github/config_schema.py)\ +- **manifest_output_format**: "excel"\ +- **submit_use_schema_labels**: Schematic option to use schema labels when submitting (default TRUE) TRUE or FALSE\ +- **submit_table_manipulation**: Schematic option when submitting (default "replace") "replace" or "upsert"\ +- **submit_manifest_record_type**: Schematic option when submitting. +- **use_compliance_dashboard**: (default FALSE) TRUE or FALSE\ +- primary_col: (default Sage theme) hexadecimal color code\ +- secondary_col; (default Sage theme) hexadecimal color code\ +- sidebar_col: (default Sage theme) hexadecimal color code + +Your pull request should include: - The modifications to [dcc_config.csv](dcc_config.csv) - A dropdown template config [www/template_config/\_config.json](www/template_config/config.json) - Optional: A .png or .svg logo for your project in `www/img` + +Other things you will need:\ +- A Synapse asset view for you project\ +- A [data model](#datamodel) ## Setup a local instance of DCA -### 1. Clone this repo and install required R packages. +### 1. Clone this repo and install required R packages. -``` +``` git clone https://github.com/Sage-Bionetworks/data_curator.git cd data_curator R -e "renv::restore()" ``` - + ### 2. Set up [schematic](github.com/sage-Bionetworks/schematic/) DCA can use Schematic through [reticulate](https://rstudio.github.io/reticulate/) or a REST API. -Using Schematic with reticulate requires python 3.9 or greater. Create a python virtual environment named `.venv` and install schematicpy through [pypi](https://pypi.org/project/schematicpy/) or from [GitHub](github.com/sage-Bionetworks/schematic/) using [poetry](https://python-poetry.org/docs/). Follow the links to Schematic for more details on installation. +Using Schematic with reticulate requires python 3.9 or greater. Create a python virtual environment named `.venv` and install schematicpy through [pypi](https://pypi.org/project/schematicpy/) or from [GitHub](github.com/sage-Bionetworks/schematic/) using [poetry](https://python-poetry.org/docs/). Follow the links to Schematic for more details on installation. -``` +``` # python virtual env must be named .venv python3 -m venv .venv @@ -67,27 +62,29 @@ poetry install poetry run python3 run_api.py ``` -To use Schematic through its REST API, run the service locally using the commands above. Or access [Schematic hosted by Sage Bionetwork](link TBD). +To use Schematic through its REST API, run the service locally using the commands above. Or access [Schematic hosted by Sage Bionetwork](link%20TBD). ### 3. Configure App Many app and schematic configurations are set in `dcc_config.yml` as described in [Quickstart](#quickstart). The following are stored as environment variables. Add these to `.Renviron`. -Schematic configurations -``` +Schematic configurations + +``` DCA_SCHEMATIC_API_TYPE: "rest", "reticulate", or "offline" DCA_API_HOST: "" (blank string) if not using the REST API, otherwise URL to schematic service DCA_API_PORT: "" (blank string) if not using the REST API **LOCALLY**, otherwise the port. Usually 3001. ``` OAuth-related variables -``` + +``` DCA_CLIENT_ID: OAuth client ID DCA_CLIENT_SECRET: OAuth client secret DCA_APP_URL: OAuth redirect URL ``` ---- +------------------------------------------------------------------------ ### Data Model Configuration @@ -99,24 +96,32 @@ For local testing, run below snippet to generate `www/config.json` and check the 2. Clone your data model repo, i.e: -``` +``` git clone https://github.com/Sage-Bionetworks/data-models ``` -3. Create `config.json` and placed it in the `www` folder +3. Create `config.json` and placed it in the `www` folder. For this script to work, your data model needs at least one record with a non-empty `dependsOn Component`. This could be a new record with a mock component that depends on a template component. For example, +``` +Attribute,Description,Valid Values,DependsOn,Properties,Required,Parent,DependsOn Component,Source,Validation Rules +Bulk RNA-seq Level 1,Bulk RNA-seq [EFO_0003738],,"Component, Filename, File Format, HTAN Parent Biospecimen ID, HTAN Data File ID, Library Layout, Read Indicator, Nucleic Acid Source, Sequencing Platform, Sequencing Batch ID, Read Length, Library Selection Method, Library Preparation Kit Name, Library Preparation Kit Vendor, Library Preparation Kit Version, Library Preparation Days from Index, Spike In, Adapter Name, Adapter Sequence, Base Caller Name, Base Caller Version, Flow, Cell Barcode, Fragment Maximum Length, Fragment Mean Length, Fragment Minimum Length, Fragment Standard Deviation Length, Lane Number, Library Strand, Multiplex Barcode, Size Selection Range, Target Depth, To Trim Adapter Sequence, Transcript Integrity Number, RIN, DV200, Adapter Content, Basic Statistics, Encoding, Kmer Content, Overrepresented Sequences, Per Base N Content, Per Base Sequence Content, Per Base Sequence Quality, Per Sequence GC Content, Per Sequence Quality Score, Per Tile Sequence Quality, Percent GC Content, Sequence Duplication Levels, Sequence Length Distribution, Total Reads, QC Workflow Type, QC Workflow Version, QC Workflow Link, Test Attribute",,FALSE,Sequencing,,http://www.ebi.ac.uk/efo/EFO_0003738, +mockComponent,mockComponent to enable config_schema.py,,,,FALSE,mock,Bulk RNA-seq Level 1,, ``` -python3 .github/generate_config_json.py \ + +``` +python3 .github/config_schema.py \ -jd data-models/example.model.jsonld \ -schema 'Sage-Bionetworks/data-models' \ - -service Sage-Bionetworks/schematic' + -service Sage-Bionetworks/schematic' \ + --overwrite ``` +If necesary, delete the mock component record from `config.json`. + ## Authentication This utilizes a Synapse Authentication (OAuth) client (code motivated by [ShinyOAuthExample](https://github.com/brucehoff/ShinyOAuthExample) and [app.R](https://gist.github.com/jcheng5/44bd750764713b5a1df7d9daf5538aea). Each application is required to have its own OAuth client as these clients cannot be shared between one another. View instructions [here](https://help.synapse.org/docs/Using-Synapse-as-an-OAuth-Server.2048327904.html) to learn how to request a client. Once you obtain the client, make sure to add the corresponding [environment variables](#configureapp) - ## Deployment To deploy the app to shinyapps.io, please follow the instructions in the [shinyapps_deploy.md](docs/shinyapps_deploy.md). @@ -125,16 +130,13 @@ To deploy the app to shinyapps.io, please follow the instructions in the [shinya Main contributors and developers: -- [Rongrong Chai](https://github.com/rrchai) -- [Anthony Williams](https://github.com/afwillia) -- [Milen Nikolov](https://github.com/milen-sage) -- [Loren Wolfe](https://github.com/lakikowolfe) -- [Robert Allaway](https://github.com/allaway) -- [Bruno Grande](https://github.com/BrunoGrandePhD) -- [Xengie Doan](https://github.com/xdoan) -- [Sujay Patil](https://github.com/sujaypatil96) +- [Rongrong Chai](https://github.com/rrchai) +- [Anthony Williams](https://github.com/afwillia) +- [Milen Nikolov](https://github.com/milen-sage) +- [Loren Wolfe](https://github.com/lakikowolfe) +- [Robert Allaway](https://github.com/allaway) +- [Bruno Grande](https://github.com/BrunoGrandePhD) +- [Xengie Doan](https://github.com/xdoan) +- [Sujay Patil](https://github.com/sujaypatil96) - -[schematic]: https://github.com/Sage-Bionetworks/schematic/tree/develop -[poetry]: https://github.com/python-poetry/poetry From af80cfe1239dd3f04e3a5eda5fc862091cc13d7a Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Tue, 23 May 2023 11:43:37 -0700 Subject: [PATCH 10/40] Dev use dcc config repo (#534) * Read dcc_config file from an environment variable to customize its source. * Change default template config to download from demo on github * Read template config file from file or from github * remove template config files. These are in sage-bionetworks/data_curator_config * Update server code to get logo image and link from dcc_config. * remove reading template config file from global.R * remove dcc_config.csv --- dcc_config.csv | 9 - global.R | 5 +- server.R | 36 ++- www/template_config/VEOIBD_config.json | 41 --- www/template_config/adkp_config.json | 46 --- www/template_config/config.json | 31 -- www/template_config/config_offline.json | 16 - www/template_config/demo_upsert_config.json | 11 - www/template_config/example_config.json | 31 -- www/template_config/htan_config.json | 306 -------------------- www/template_config/include_config.json | 12 - www/template_config/mc2_config.json | 131 --------- 12 files changed, 34 insertions(+), 641 deletions(-) delete mode 100644 dcc_config.csv delete mode 100644 www/template_config/VEOIBD_config.json delete mode 100644 www/template_config/adkp_config.json delete mode 100644 www/template_config/config.json delete mode 100644 www/template_config/config_offline.json delete mode 100644 www/template_config/demo_upsert_config.json delete mode 100644 www/template_config/example_config.json delete mode 100644 www/template_config/htan_config.json delete mode 100644 www/template_config/include_config.json delete mode 100644 www/template_config/mc2_config.json diff --git a/dcc_config.csv b/dcc_config.csv deleted file mode 100644 index c55a0ab5..00000000 --- a/dcc_config.csv +++ /dev/null @@ -1,9 +0,0 @@ -project_name,synapse_asset_view,data_model_url,template_menu_config_file,manifest_output_format,submit_use_schema_labels,submit_table_manipulation,submit_manifest_record_type,use_compliance_dashboard,primary_col,secondary_col,sidebar_col -DCA Demo,syn33715412,https://raw.githubusercontent.com/Sage-Bionetworks/data-models/main/example.model.jsonld,www/template_config/example_config.json,google_sheet,TRUE,replace,table_and_file,FALSE,#2a668d,#184e71,#191919 -DCA Demo Upsert,syn51489635,https://raw.githubusercontent.com/Sage-Bionetworks/schematic/develop/tests/data/example.single_rule.model.jsonld,www/template_config/demo_upsert_config.json,excel,FALSE,upsert,table_and_file,FALSE,#2a668d,#184e71,#191919 -HTAN All Projects,syn20446927,https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld,www/template_config/htan_config.json,excel,TRUE,replace,table_and_file,FALSE,#605ca8,#5F008C,#191919 -Cancer Complexity Knowledge Portal - Database,syn27210848,https://raw.githubusercontent.com/mc2-center/data-models/main/mc2.model.jsonld,www/template_config/mc2_config.json,excel,FALSE,upsert,table_and_file,FALSE,#407BA0,#5BB0B5,#191919 -INCLUDE Data Management Core,syn30109515,https://raw.githubusercontent.com/include-dcc/include-linkml/schematic-updates/src/schematic/include_schematic_linkml.jsonld,www/template_config/include_config.json,excel,TRUE,replace,table_and_file,FALSE,#2a668d,#184e71,#191919 -AD Knowledge Portal,syn51324810,https://raw.githubusercontent.com/adknowledgeportal/data-models/main/divco.data.model.v1.jsonld,www/template_config/adkp_config.json,excel,TRUE,replace,file_only,FALSE,#2a668d,#184e71,#191919 -VEOIBD,syn51397378,https://raw.githubusercontent.com/VEOIBD/data_models/main/veoibd.data.model.jsonld,www/template_config/VEOIBD_config.json,excel,TRUE,upsert,file_only,FALSE,#2a668d,#184e72,#191920 -BTC DCC,syn51407795,https://github.com/Sage-Bionetworks/btc-data-models/blob/main/btc.model.jsonld,www/template_config/example_config.json,excel,TRUE,replace,table_and_file,FALSLE,#2a668d,#184e72,#191920 \ No newline at end of file diff --git a/global.R b/global.R index ab8f4163..fb883145 100644 --- a/global.R +++ b/global.R @@ -35,7 +35,8 @@ plan(multisession, workers = ncores) source_files <- list.files(c("functions", "modules"), pattern = "*\\.R$", recursive = TRUE, full.names = TRUE) sapply(source_files, FUN = source) -dcc_config <- read_csv("dcc_config.csv", show_col_types = FALSE) +dcc_config_file <- Sys.getenv("DCA_DCC_CONFIG") +dcc_config <- read_csv(dcc_config_file, show_col_types = FALSE) ## Set Up OAuth client_id <- Sys.getenv("DCA_CLIENT_ID") @@ -140,8 +141,6 @@ if (dca_schematic_api == "reticulate"){ # ) } } -config_file <- fromJSON("www/template_config/config.json") - ## Global variables dropdown_types <- c("project", "folder", "template") diff --git a/server.R b/server.R index 22a4b685..77b88b27 100644 --- a/server.R +++ b/server.R @@ -35,8 +35,8 @@ shinyServer(function(input, output, session) { ######## session global variables ######## # read config in def_config <- ifelse(dca_schematic_api == "offline", - fromJSON("www/template_config/config_offline.json"), - fromJSON("www/template_config/config.json") + fromJSON("https://raw.githubusercontent.com/Sage-Bionetworks/data_curator_config/main/demo/dca-template-config.json"), + fromJSON("https://raw.githubusercontent.com/Sage-Bionetworks/data_curator_config/main/demo/dca-template-config.json") ) config <- reactiveVal() config_schema <- reactiveVal(def_config) @@ -183,7 +183,29 @@ shinyServer(function(input, output, session) { dcWaiter("show", msg = paste0("Getting data. This may take a minute."), color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) - output$logo <- renderUI({update_logo(selected$master_asset_view())}) + logo_img <- ifelse(!is.na(dcc_config_react()$logo_location), + paste0("https://raw.githubusercontent.com/Sage-Bionetworks/data_curator_config/main/", + dcc_config_react()$logo_location), + "https://raw.githubusercontent.com/Sage-Bionetworks/data_curator_config/main/demo/Logo_Sage_Logomark.png") + + logo_link <- ifelse(!is.na(dcc_config_react()$logo_link), + dcc_config_react()$logo_link, + "https://synapse.org" + ) + + output$logo <- renderUI({ + tags$li( + class = "dropdown", id = "logo", + tags$a( + href = logo_link, + target = "_blank", + tags$img( + height = "40px", alt = "LOGO", + src = logo_img + ) + ) + ) + }) if (dca_schematic_api == "reticulate") { # Update schematic_config and login @@ -201,8 +223,14 @@ shinyServer(function(input, output, session) { ) } - conf_file <- reactiveVal(template_config_files[input$dropdown_asset_view]) + if (!file.exists(conf_file())){ + conf_file( + file.path("https://raw.githubusercontent.com/Sage-Bionetworks/data_curator_config/main", + conf_file() + ) + ) + } config_df <- jsonlite::fromJSON(conf_file()) conf_template <- setNames(config_df[[1]]$schema_name, config_df[[1]]$display_name) diff --git a/www/template_config/VEOIBD_config.json b/www/template_config/VEOIBD_config.json deleted file mode 100644 index 0b14089a..00000000 --- a/www/template_config/VEOIBD_config.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "manifest_schemas": [ - { - "display_name": "Clinical Metadata Template", - "schema_name": "ClinicalMetadataTemplate", - "type": "record" - }, - { - "display_name": "Biospecimen Metadata Template", - "schema_name": "BiospecimenMetadataTemplate", - "type": "record" - }, - { - "display_name": "Bulk RNAseq Assay Template", - "schema_name": "BulkRNAseqAssayTemplate", - "type": "record" - }, - { - "display_name": "scRNAseq Assay Template", - "schema_name": "ScRNAseqAssayTemplate", - "type": "record" - }, - { - "display_name": "Bulk RNAseq Raw File Annotations", - "schema_name": "BulkRNAseqRawFileAnnotations", - "type": "file" - }, - { - "display_name": "Bulk RNASeq Counts File Annotations", - "schema_name": "BulkRNASeqCountsFileAnnotations", - "type": "file" - }, - { - "display_name": "Metadata File Annotations", - "schema_name": "MetadataFileAnnotations", - "type": "file" - } - ], - "service_version": "v23.1.1", - "schema_version": "" -} \ No newline at end of file diff --git a/www/template_config/adkp_config.json b/www/template_config/adkp_config.json deleted file mode 100644 index 5cd6c497..00000000 --- a/www/template_config/adkp_config.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "manifest_schemas": [ - { - "display_name": "Diverse Cohorts Clinical Metadata", - "schema_name": "DiverseCohortsClinicalMetadata", - "type": "record" - }, - { - "display_name": "Diverse Cohorts Biospecimen Metadata", - "schema_name": "DiverseCohortsBiospecimenMetadata", - "type": "record" - }, - { - "display_name": "WGS Assay Metadata", - "schema_name": "WholeGenomeSeqAssayMetadata", - "type": "record" - }, - { - "display_name": "WGS VCF Files", - "schema_name": "WholeGenomeSeqVCFFileAnnotations", - "type": "file" - }, - { - "display_name": "WGS Raw Files", - "schema_name": "WholeGenomeSeqRawFileAnnotations", - "type": "file" - }, - { - "display_name": "Bulk RNAseq Assay Metadata", - "schema_name": "BulkRNASeqAssayMetadata", - "type": "record" - }, - { - "display_name": "Bulk RNAseq Raw Files", - "schema_name": "BulkRNASeqRawFileAnnotations", - "type": "file" - }, - { - "display_name": "Bulk RNAseq Counts Files", - "schema_name": "BulkRNASeqCountsFileAnnotations", - "type": "file" - } - ], - "service_version": "v23.1.1", - "schema_version": "" - } \ No newline at end of file diff --git a/www/template_config/config.json b/www/template_config/config.json deleted file mode 100644 index f85b5768..00000000 --- a/www/template_config/config.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "manifest_schemas": [ - { - "display_name": "Biospecimen", - "schema_name": "Biospecimen", - "type": "record" - }, - { - "display_name": "Patient", - "schema_name": "Patient", - "type": "record" - }, - { - "display_name": "Bulk RNA-seq Assay", - "schema_name": "BulkRNA-seqAssay", - "type": "file" - }, - { - "display_name": "Other Assay", - "schema_name": "OtherAssay", - "type": "file" - }, - { - "display_name": "MockComponent", - "schema_name": "MockComponent", - "type": "record" - } - ], - "service_version": "v23.1.1", - "schema_version": "" -} \ No newline at end of file diff --git a/www/template_config/config_offline.json b/www/template_config/config_offline.json deleted file mode 100644 index 2b7b76ed..00000000 --- a/www/template_config/config_offline.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "manifest_schemas": [ - { - "display_name": "Datatype A - record", - "schema_name": "DatatypeA", - "type": "record" - }, - { - "display_name": "Datatype B - file", - "schema_name": "DatatypeB", - "type": "file" - } - ], - "service_version": "v23.1.1", - "schema_version": "" -} \ No newline at end of file diff --git a/www/template_config/demo_upsert_config.json b/www/template_config/demo_upsert_config.json deleted file mode 100644 index ba4b01d1..00000000 --- a/www/template_config/demo_upsert_config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "manifest_schemas": [ - { - "display_name": "MockRDB", - "schema_name": "MockRDB", - "type": "record" - } - ], - "service_version": "", - "schema_version": "" -} diff --git a/www/template_config/example_config.json b/www/template_config/example_config.json deleted file mode 100644 index f85b5768..00000000 --- a/www/template_config/example_config.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "manifest_schemas": [ - { - "display_name": "Biospecimen", - "schema_name": "Biospecimen", - "type": "record" - }, - { - "display_name": "Patient", - "schema_name": "Patient", - "type": "record" - }, - { - "display_name": "Bulk RNA-seq Assay", - "schema_name": "BulkRNA-seqAssay", - "type": "file" - }, - { - "display_name": "Other Assay", - "schema_name": "OtherAssay", - "type": "file" - }, - { - "display_name": "MockComponent", - "schema_name": "MockComponent", - "type": "record" - } - ], - "service_version": "v23.1.1", - "schema_version": "" -} \ No newline at end of file diff --git a/www/template_config/htan_config.json b/www/template_config/htan_config.json deleted file mode 100644 index 113b1e9f..00000000 --- a/www/template_config/htan_config.json +++ /dev/null @@ -1,306 +0,0 @@ -{ - "manifest_schemas": [ - { - "display_name": "Patient", - "schema_name": "Patient", - "type": "record" - }, - { - "display_name": "Demographics", - "schema_name": "Demographics", - "type": "record" - }, - { - "display_name": "Family History", - "schema_name": "FamilyHistory", - "type": "record" - }, - { - "display_name": "Exposure", - "schema_name": "Exposure", - "type": "record" - }, - { - "display_name": "Follow Up", - "schema_name": "FollowUp", - "type": "record" - }, - { - "display_name": "Diagnosis", - "schema_name": "Diagnosis", - "type": "record" - }, - { - "display_name": "Therapy", - "schema_name": "Therapy", - "type": "record" - }, - { - "display_name": "Molecular Test", - "schema_name": "MolecularTest", - "type": "record" - }, - { - "display_name": "Biospecimen", - "schema_name": "Biospecimen", - "type": "record" - }, - { - "display_name": "Clinical Data Tier 2", - "schema_name": "ClinicalDataTier2", - "type": "record" - }, - { - "display_name": "SRRS Clinical Data Tier 2", - "schema_name": "SRRSClinicalDataTier2", - "type": "record" - }, - { - "display_name": "Lung Cancer Tier 3", - "schema_name": "LungCancerTier3", - "type": "record" - }, - { - "display_name": "Colorectal Cancer Tier 3", - "schema_name": "ColorectalCancerTier3", - "type": "record" - }, - { - "display_name": "Breast Cancer Tier 3", - "schema_name": "BreastCancerTier3", - "type": "record" - }, - { - "display_name": "Neuroblastoma and Glioma Tier 3", - "schema_name": "NeuroblastomaandGliomaTier3", - "type": "record" - }, - { - "display_name": "Acute Lymphoblastic Leukemia Tier 3", - "schema_name": "AcuteLymphoblasticLeukemiaTier3", - "type": "record" - }, - { - "display_name": "Ovarian Cancer Tier 3", - "schema_name": "OvarianCancerTier3", - "type": "record" - }, - { - "display_name": "Prostate Cancer Tier 3", - "schema_name": "ProstateCancerTier3", - "type": "record" - }, - { - "display_name": "Sarcoma Tier 3", - "schema_name": "SarcomaTier3", - "type": "record" - }, - { - "display_name": "Pancreatic Cancer Tier 3", - "schema_name": "PancreaticCancerTier3", - "type": "record" - }, - { - "display_name": "Melanoma Tier 3", - "schema_name": "MelanomaTier3", - "type": "record" - }, - { - "display_name": "SRRS Biospecimen", - "schema_name": "SRRSBiospecimen", - "type": "record" - }, - { - "display_name": "Other Assay", - "schema_name": "OtherAssay", - "type": "file" - }, - { - "display_name": "scRNA-seq Level 1", - "schema_name": "ScRNA-seqLevel1", - "type": "file" - }, - { - "display_name": "scRNA-seq Level 2", - "schema_name": "ScRNA-seqLevel2", - "type": "file" - }, - { - "display_name": "scRNA-seq Level 3", - "schema_name": "ScRNA-seqLevel3", - "type": "file" - }, - { - "display_name": "scRNA-seq Level 4", - "schema_name": "ScRNA-seqLevel4", - "type": "file" - }, - { - "display_name": "Bulk RNA-seq Level 1", - "schema_name": "BulkRNA-seqLevel1", - "type": "file" - }, - { - "display_name": "Bulk RNA-seq Level 2", - "schema_name": "BulkRNA-seqLevel2", - "type": "file" - }, - { - "display_name": "Bulk RNA-seq Level 3", - "schema_name": "BulkRNA-seqLevel3", - "type": "file" - }, - { - "display_name": "Bulk WES Level 1", - "schema_name": "BulkWESLevel1", - "type": "file" - }, - { - "display_name": "Bulk WES Level 2", - "schema_name": "BulkWESLevel2", - "type": "file" - }, - { - "display_name": "Bulk WES Level 3", - "schema_name": "BulkWESLevel3", - "type": "file" - }, - { - "display_name": "scATAC-seq Level 1", - "schema_name": "ScATAC-seqLevel1", - "type": "file" - }, - { - "display_name": "scATAC-seq Level 2", - "schema_name": "ScATAC-seqLevel2", - "type": "file" - }, - { - "display_name": "scATAC-seq Level 3", - "schema_name": "ScATAC-seqLevel3", - "type": "file" - }, - { - "display_name": "scmC-seq Level 1", - "schema_name": "ScmC-seqLevel1", - "type": "file" - }, - { - "display_name": "scmC-seq Level 2", - "schema_name": "ScmC-seqLevel2", - "type": "file" - }, - { - "display_name": "scATAC-seq Level 4", - "schema_name": "ScATAC-seqLevel4", - "type": "file" - }, - { - "display_name": "scDNA-seq Level 1", - "schema_name": "ScDNA-seqLevel1", - "type": "file" - }, - { - "display_name": "scDNA-seq Level 2", - "schema_name": "ScDNA-seqLevel2", - "type": "file" - }, - { - "display_name": "Bulk Methylation-seq Level 1", - "schema_name": "BulkMethylation-seqLevel1", - "type": "file" - }, - { - "display_name": "Bulk Methylation-seq Level 2", - "schema_name": "BulkMethylation-seqLevel2", - "type": "file" - }, - { - "display_name": "Bulk Methylation-seq Level 3", - "schema_name": "BulkMethylation-seqLevel3", - "type": "file" - }, - { - "display_name": "Imaging Level 1", - "schema_name": "ImagingLevel1", - "type": "file" - }, - { - "display_name": "Imaging Level 2", - "schema_name": "ImagingLevel2", - "type": "file" - }, - { - "display_name": "Imaging Level 3 Segmentation", - "schema_name": "ImagingLevel3Segmentation", - "type": "file" - }, - { - "display_name": "Imaging Level 3 Image", - "schema_name": "ImagingLevel3Image", - "type": "file" - }, - { - "display_name": "Imaging Level 3 Channels", - "schema_name": "ImagingLevel3Channels", - "type": "record" - }, - { - "display_name": "10x Visium Spatial Transcriptomics - RNA-seq Level 1", - "schema_name": "10xVisiumSpatialTranscriptomics-RNA-seqLevel1", - "type": "file" - }, - { - "display_name": "10x Visium Spatial Transcriptomics - RNA-seq Level 2", - "schema_name": "10xVisiumSpatialTranscriptomics-RNA-seqLevel2", - "type": "file" - }, - { - "display_name": "10x Visium Spatial Transcriptomics - Auxiliary Files Level 2", - "schema_name": "10xVisiumSpatialTranscriptomics-AuxiliaryFilesLevel2", - "type": "file" - }, - { - "display_name": "10x Visium Spatial Transcriptomics - RNA-seq Level 3", - "schema_name": "10xVisiumSpatialTranscriptomics-RNA-seqLevel3", - "type": "file" - }, - { - "display_name": "Imaging Level 4", - "schema_name": "ImagingLevel4", - "type": "file" - }, - { - "display_name": "SRRS Imaging Level 2", - "schema_name": "SRRSImagingLevel2", - "type": "file" - }, - { - "display_name": "RPPA Level 2", - "schema_name": "RPPALevel2", - "type": "file" - }, - { - "display_name": "HTAN RPPA Antibody Table", - "schema_name": "HTANRPPAAntibodyTable", - "type": "file" - }, - { - "display_name": "Mass Spectrometry Level 1", - "schema_name": "MassSpectrometryLevel1", - "type": "file" - }, - { - "display_name": "RPPA Level 3", - "schema_name": "RPPALevel3", - "type": "file" - }, - { - "display_name": "RPPA Level 4", - "schema_name": "RPPALevel4", - "type": "file" - } - ], - "service_version": "v23.1.1", - "schema_version": "" -} \ No newline at end of file diff --git a/www/template_config/include_config.json b/www/template_config/include_config.json deleted file mode 100644 index 89df2c95..00000000 --- a/www/template_config/include_config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "manifest_schemas": [ - {"display_name": "Study", "schema_name": "Study", "type": "record"}, - {"display_name": "Participant", "schema_name": "Participant", "type": "record"}, - {"display_name": "Biospecimen", "schema_name": "Biospecimen", "type": "record"}, - {"display_name": "Condition", "schema_name": "Condition", "type": "record"}, - {"display_name": "Data File", "schema_name": "DataFile", "type": "file"} - - ], - "main_fileview" : "syn30109515", - "community" : "INCLUDE" -} diff --git a/www/template_config/mc2_config.json b/www/template_config/mc2_config.json deleted file mode 100644 index f3da2a0c..00000000 --- a/www/template_config/mc2_config.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "manifest_schemas": [ - { - "display_name": "Tool Grant", - "schema_name": "ToolGrant", - "type": "record" - }, - { - "display_name": "Tool", - "schema_name": "Tool", - "type": "record" - }, - { - "display_name": "Grant", - "schema_name": "Grant", - "type": "record" - }, - { - "display_name": "Publication Grant", - "schema_name": "PublicationGrant", - "type": "record" - }, - { - "display_name": "Publication", - "schema_name": "Publication", - "type": "record" - }, - { - "display_name": "Dataset Grant", - "schema_name": "DatasetGrant", - "type": "record" - }, - { - "display_name": "Dataset", - "schema_name": "Dataset", - "type": "record" - }, - { - "display_name": "Consortium Grant", - "schema_name": "ConsortiumGrant", - "type": "record" - }, - { - "display_name": "Consortium", - "schema_name": "Consortium", - "type": "record" - }, - { - "display_name": "Project", - "schema_name": "Project", - "type": "record" - }, - { - "display_name": "Person Consortium", - "schema_name": "PersonConsortium", - "type": "record" - }, - { - "display_name": "Person", - "schema_name": "Person", - "type": "record" - }, - { - "display_name": "Theme Grant", - "schema_name": "ThemeGrant", - "type": "record" - }, - { - "display_name": "Theme", - "schema_name": "Theme", - "type": "record" - }, - { - "display_name": "Institution Grant", - "schema_name": "InstitutionGrant", - "type": "record" - }, - { - "display_name": "Institution", - "schema_name": "Institution", - "type": "record" - }, - { - "display_name": "File Grant", - "schema_name": "FileGrant", - "type": "record" - }, - { - "display_name": "File", - "schema_name": "File", - "type": "record" - }, - { - "display_name": "Tool View", - "schema_name": "ToolView", - "type": "record" - }, - { - "display_name": "Publication View", - "schema_name": "PublicationView", - "type": "record" - }, - { - "display_name": "Dataset View", - "schema_name": "DatasetView", - "type": "record" - }, - { - "display_name": "Person View", - "schema_name": "PersonView", - "type": "record" - }, - { - "display_name": "File View", - "schema_name": "FileView", - "type": "record" - }, - { - "display_name": "Grant View", - "schema_name": "GrantView", - "type": "record" - }, - { - "display_name": "Project View", - "schema_name": "ProjectView", - "type": "record" - } - ], - "service_version": "v22.11.2", - "schema_version": "v2.1.1" -} \ No newline at end of file From f8c96ed28d96aecc95b445b7c6b131aef328e3e4 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Thu, 1 Jun 2023 10:45:03 -0700 Subject: [PATCH 11/40] Dev sprint 11 UI updates (#540) * if user has access to only one DCC, simulate a click of asset view button to skip the selection page. (#536) * Dev selection banner (#537) * Add data selection banner to top. * Update header selection banner with project, template, and folder. * Add placeholder text to selection header boxes * Read use_annotations argument to manifest/generate from dcc_config (#538) * Dev skip download (#539) * On folder selection page, add a button to skip ahead to validation tab without downloading a manifest * Add logic to skip template download if user desires. * show download and validate tabs if skipping download * show selection header automatically after DCC page. Remove badge from the button. * remove badge from selection header dropdown and change text to 'selected data' * Folder selection in header now updates. * show data selection banner after selecting project * hide data selection banner until template selection * Hide sidebar at startup --- server.R | 38 +++++++++++++++++++++++++++++++++----- ui.R | 48 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/server.R b/server.R index 77b88b27..4cd1fd2d 100644 --- a/server.R +++ b/server.R @@ -144,6 +144,10 @@ shinyServer(function(input, output, session) { choices = c("Offline mock data (synXXXXXX)"="synXXXXXX")) dcWaiter("hide") } + + if (length(asset_views()) == 1L) { + click("btn_asset_view") + } ######## Arrow Button ######## lapply(1:6, function(i) { @@ -338,6 +342,8 @@ shinyServer(function(input, output, session) { updateTabsetPanel(session, "tabs", selected = "tab_template_select") shinyjs::show(select = "li:nth-child(3)") + updateSelectInput(session, "header_dropdown_project", + choices = selected$project()) dcWaiter("hide") }) @@ -353,6 +359,8 @@ shinyServer(function(input, output, session) { selected$schema(data_list$template()[input$dropdown_template]) updateSelectInput(session, "dropdown_folder", choices = data_list$folders()) updateTabsetPanel(session, "tabs", selected = "tab_folder") + updateSelectInput(session, "header_dropdown_template", + choices = selected$schema()) shinyjs::show(select = "li:nth-child(4)") dcWaiter("hide") }) @@ -372,12 +380,10 @@ shinyServer(function(input, output, session) { updateTabsetPanel(session, "tabs", selected = "tab_template") - selected_folder <- data_list$folders()[which(data_list$folders() == input$dropdown_folder)] output$template_title <- renderText({ sprintf("Get %s template for %s", selected$schema(), - names(selected_folder)) + names(selected$folder())) }) - selected$folder(selected_folder) # clean tags in generating-template tab sapply(clean_tags[1:2], FUN = hide) @@ -413,6 +419,10 @@ shinyServer(function(input, output, session) { observeEvent(input$dropdown_folder,{ shinyjs::enable("btn_folder") + selected_folder <- data_list$folders()[which(data_list$folders() == input$dropdown_folder)] + selected$folder(selected_folder) + updateSelectInput(session, "header_dropdown_folder", + choices = selected$folder()) }) observeEvent(data_list$files(), ignoreInit = TRUE, { @@ -448,6 +458,13 @@ shinyServer(function(input, output, session) { }) + observeEvent(input$btn_folder_have_template, { + shinyjs::show(select = "li:nth-child(5)") + shinyjs::show(select = "li:nth-child(6)") + updateTabsetPanel(session, "tabs", + selected = "tab_upload") + }) + observeEvent(input$update_confirm, { req(input$update_confirm == TRUE) isUpdateFolder(TRUE) @@ -493,6 +510,16 @@ shinyServer(function(input, output, session) { }) + observeEvent(input$tabs, { + req(input$tabs %in% c("tab_project", "tab_template_select", "tab_folder", "tab_template", "tab_upload")) + shinyjs::addClass(id = "header_selection_dropdown", class = "dropdown open") + }) + + observeEvent(input$tabs, { + req(input$tabs == "tab_template_select") + shinyjs::show("header_selection_dropdown") + }) + observeEvent(c(input$`switchTab4-Next`, input$tabs), { req(input$tabs == "tab_template") @@ -512,6 +539,7 @@ shinyServer(function(input, output, session) { file.path(api_uri, "v1/manifest/generate"), NA) .output_format <- dcc_config_react()$manifest_output_format + .use_annotations <- dcc_config_react()$manifest_use_annotations promises::future_promise({ switch(dca_schematic_api, @@ -522,7 +550,7 @@ shinyServer(function(input, output, session) { data_type = .schema, dataset_id = .datasetId, asset_view = .asset_view, - use_annotations = FALSE, + use_annotations = .use_annotations, output_format = .output_format, access_token=access_token ), @@ -746,7 +774,7 @@ shinyServer(function(input, output, session) { # If a file-based component selected (define file-based components) note for future # the type to filter (eg file-based) on could probably also be a config choice display_names <- config_schema()$manifest_schemas$display_name[config_schema()$manifest_schemas$type == "file"] - + if (input$dropdown_template %in% display_names) { # make into a csv or table for file-based components already has entityId if ("entityId" %in% colnames(submit_data)) { diff --git a/ui.R b/ui.R index 9009d529..816d86ed 100644 --- a/ui.R +++ b/ui.R @@ -17,10 +17,47 @@ ui <- shinydashboardPlus::dashboardPage( span(class = "logo-lg", "Data Curator"), span(class = "logo-mini", "DCA") ), - uiOutput("logo") + uiOutput("logo"), + leftUi = hidden( + tagList( + dropdownBlock( + id = "header_selection_dropdown", + title = "Selected data", + icon = icon("sliders"), + badgeStatus = NULL, + fluidRow( + div( + id = "header_content_project", + selectInput( + inputId = "header_dropdown_project", + label = NULL, + choices = "No project selected" + ) + ), + div( + id = "header_content_template", + selectInput( + inputId = "header_dropdown_template", + label = NULL, + choices = "No template selected" + ) + ), + div( + id = "header_content_folder", + selectInput( + inputId = "header_dropdown_folder", + label = NULL, + choices = "No folder selected" + ) + ) + ) + ) + ) + ) # end hidden ), dashboardSidebar( width = 250, + hidden( sidebarMenu( id = "tabs", menuItem( @@ -59,6 +96,7 @@ ui <- shinydashboardPlus::dashboardPage( tags$footer(HTML(' Powered by and Sage Bionetworks')) ) ) + ) ), dashboardBody( tags$head( @@ -143,9 +181,13 @@ ui <- shinydashboardPlus::dashboardPage( label = NULL, choices = "Generating..." ), - actionButton("btn_folder", "Go", + actionButton("btn_folder", "Download template", class = "btn-primary-color" - ) + ), + actionButton("btn_folder_have_template", + "I already have a template or manifest", + class = "btn-primary-color" + ) ) ) ), From 1f64a65bfbb5e9b2ce2f2ff0260f811487017e60 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Thu, 8 Jun 2023 12:15:40 -0700 Subject: [PATCH 12/40] use restrict_rules from dcc_config in model/validate and model/submit. (#541) --- server.R | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/server.R b/server.R index 4cd1fd2d..5059e65b 100644 --- a/server.R +++ b/server.R @@ -640,6 +640,7 @@ shinyServer(function(input, output, session) { .data_model <- data_model() .infile_data <- inFile$data() .dd_template <- input$dropdown_template + .restrict_rules <- dcc_config_react()$validate_restrict_rules promises::future_promise({ annotation_status <- switch(dca_schematic_api, @@ -652,7 +653,8 @@ shinyServer(function(input, output, session) { url=file.path(api_uri, "v1/model/validate"), schema_url=.data_model, data_type=.schema, - file_name=.datapath), + file_name=.datapath, + restrict_rules = .restrict_rules), { Sys.sleep(0) list(list( @@ -819,6 +821,7 @@ shinyServer(function(input, output, session) { .submit_use_schema_labels <- dcc_config_react()$submit_use_schema_labels .table_manipulation <- dcc_config_react()$submit_table_manipulation .submit_manifest_record_type <- dcc_config_react()$submit_manifest_record_type + .restrict_rules <- dcc_config_react()$validate_restrict_rules # associates metadata with data and returns manifest id promises::future_promise({ @@ -833,7 +836,7 @@ shinyServer(function(input, output, session) { data_type = .schema, dataset_id = .folder, access_token = access_token, - restrict_rules = FALSE, + restrict_rules = .restrict_rules, file_name = tmp_file_path, asset_view = .asset_view, use_schema_label=.submit_use_schema_labels, @@ -859,6 +862,7 @@ shinyServer(function(input, output, session) { .submit_use_schema_labels <- dcc_config_react()$submit_use_schema_labels .table_manipulation <- dcc_config_react()$submit_table_manipulation .submit_manifest_record_type <- dcc_config_react()$submit_manifest_record_type + .restrict_rules <- dcc_config_react()$validate_restrict_rules # associates metadata with data and returns manifest id promises::future_promise({ @@ -873,7 +877,7 @@ shinyServer(function(input, output, session) { data_type = .schema, dataset_id = .folder, access_token = access_token, - restrict_rules = FALSE, + restrict_rules = .restrict_rules, file_name = tmp_file_path, asset_view = .asset_view, use_schema_label=.submit_use_schema_labels, From 81c4b9fc2e29a854665438672e09b8a626f35e8a Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Thu, 8 Jun 2023 12:16:07 -0700 Subject: [PATCH 13/40] Dev fix selection header (#542) * Use selectizeInput with options maxItems = '1' to remove the dropdown icon from the selection header. * Remove grey background from header selection boxes. --- ui.R | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/ui.R b/ui.R index 816d86ed..0485b74b 100644 --- a/ui.R +++ b/ui.R @@ -26,28 +26,42 @@ ui <- shinydashboardPlus::dashboardPage( icon = icon("sliders"), badgeStatus = NULL, fluidRow( + # Set color and background for items to remove the grey background + # when using the option maxItems = 1. Note, this affects all dropdowns + # but it's difficult to notice. + tags$style(HTML( + ".item { + background: white !important; + color: black !important; + }" + )), div( id = "header_content_project", - selectInput( + # Use selectizeInput instead of selectInput to specify options + # maxItems = "1" will remove the dropdown triangle from the box. + selectizeInput( inputId = "header_dropdown_project", label = NULL, - choices = "No project selected" + choices = "No project selected", + options = list(maxItems = "1") ) ), div( id = "header_content_template", - selectInput( + selectizeInput( inputId = "header_dropdown_template", label = NULL, - choices = "No template selected" + choices = "No template selected", + options = list(maxItems = "1") ) ), div( id = "header_content_folder", - selectInput( + selectizeInput( inputId = "header_dropdown_folder", label = NULL, - choices = "No folder selected" + choices = "No folder selected", + options = list(maxItems = "1") ) ) ) From 386214ebb145e379e07765a86138c4b776c06784 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Fri, 9 Jun 2023 10:48:10 -0700 Subject: [PATCH 14/40] shinyapps.io deployment workflow now uses the current env configuration variables. Point to a branch of data_curator_config that does uses existing shinyapps.io DCCs. (#543) --- .github/workflows/shinyapps_deploy.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/shinyapps_deploy.yml b/.github/workflows/shinyapps_deploy.yml index 39c68424..52fa41fe 100644 --- a/.github/workflows/shinyapps_deploy.yml +++ b/.github/workflows/shinyapps_deploy.yml @@ -4,8 +4,7 @@ on: push: branches: - main - - develop* - - develop-* + - dev* tags: - v[0-9]+.[0-9]+.[0-9]+ paths-ignore: @@ -20,7 +19,6 @@ jobs: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} DCA_SCHEMATIC_API_TYPE: rest DCA_API_HOST: "https://schematic-dev.api.sagebionetworks.org" - DCA_API_PORT: "" steps: - name: Install System Dependencies run: | @@ -116,11 +114,10 @@ jobs: echo 'DCA_CLIENT_SECRET="${{ secrets.OAUTH_CLIENT_SECRET }}"' >> .Renviron echo 'DCA_SCHEMATIC_API_TYPE="${{ env.DCA_SCHEMATIC_API_TYPE }}"' >> .Renviron - echo 'DCA_API_PORT="${{ env.DCA_API_PORT }}"' >> .Renviron echo 'DCA_API_HOST="${{ env.DCA_API_HOST }}"' >> .Renviron echo 'DCA_SYNAPSE_PROJECT_API=TRUE' >> .Renviron - echo 'DCA_MANIFEST_OUTPUT_FORMAT="excel"' >> .Renviron + echo 'DCA_DCC_CONFIG="https://raw.githubusercontent.com/Sage-Bionetworks/data_curator_config/shinyapps-io/dcc_config.csv"' >> .Renviron echo 'GITHUB_PAT="${{ secrets.GITHUB_TOKEN }}"' >> .Renviron From be82df085dabc88a51597f8dacf5c1781332246c Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Wed, 21 Jun 2023 09:16:09 -0700 Subject: [PATCH 15/40] remove generate google sheet link after failed validation until it gets fixed (#548) --- server.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server.R b/server.R index 5059e65b..4f23d3b5 100644 --- a/server.R +++ b/server.R @@ -697,9 +697,9 @@ shinyServer(function(input, output, session) { shinyjs::show("box_submit") } else { if (dca_schematic_api != "offline" & dcc_config_react()$manifest_output_format == "google_sheet") { - output$val_gsheet <- renderUI( - actionButton("btn_val_gsheet", " Generate Google Sheet Link", icon = icon("table"), class = "btn-primary-color") - ) + #output$val_gsheet <- renderUI( + #actionButton("btn_val_gsheet", " Generate Google Sheet Link", icon = icon("table"), class = "btn-primary-color") + #) } else if (dca_schematic_api == "offline") { output$dl_manifest <- renderUI({ downloadButton("downloadData_good", "Download Corrected Data") From 4effe996e5a09d1c3feb134cc069a6cd0d54e305 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Thu, 29 Jun 2023 13:54:23 -0700 Subject: [PATCH 16/40] Submit hide blanks (#549) * Use hide_blanks from config file * Add hide_blanks argument to model_submit --- R/schematic_rest_api.R | 5 +++-- server.R | 10 +++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/R/schematic_rest_api.R b/R/schematic_rest_api.R index d498fe3a..8128da74 100644 --- a/R/schematic_rest_api.R +++ b/R/schematic_rest_api.R @@ -157,7 +157,7 @@ model_submit <- function(url="http://localhost:3001/v1/model/submit", schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #notlint data_type, dataset_id, restrict_rules=FALSE, access_token, json_str=NULL, asset_view, use_schema_label=TRUE, manifest_record_type="table_and_file", file_name, - table_manipulation="replace") { + table_manipulation="replace", hide_blanks=FALSE) { req <- httr::POST(url, #add_headers(Authorization=paste0("Bearer ", pat)), query=list( @@ -170,7 +170,8 @@ model_submit <- function(url="http://localhost:3001/v1/model/submit", asset_view=asset_view, use_schema_label=use_schema_label, manifest_record_type=manifest_record_type, - table_manipulation=table_manipulation), + table_manipulation=table_manipulation, + hide_blanks=hide_blanks), body=list(file_name=httr::upload_file(file_name)) #body=list(file_name=file_name) ) diff --git a/server.R b/server.R index 4f23d3b5..fe58cd63 100644 --- a/server.R +++ b/server.R @@ -822,6 +822,7 @@ shinyServer(function(input, output, session) { .table_manipulation <- dcc_config_react()$submit_table_manipulation .submit_manifest_record_type <- dcc_config_react()$submit_manifest_record_type .restrict_rules <- dcc_config_react()$validate_restrict_rules + .hide_blanks <- dcc_config_react()$submit_hide_blanks # associates metadata with data and returns manifest id promises::future_promise({ @@ -841,7 +842,8 @@ shinyServer(function(input, output, session) { asset_view = .asset_view, use_schema_label=.submit_use_schema_labels, manifest_record_type=.submit_manifest_record_type, - table_manipulation=.table_manipulation), + table_manipulation=.table_manipulation, + hide_blanks=.hide_blanks), "synXXXX - No data uploaded" ) }) %...>% manifest_id() @@ -863,7 +865,8 @@ shinyServer(function(input, output, session) { .table_manipulation <- dcc_config_react()$submit_table_manipulation .submit_manifest_record_type <- dcc_config_react()$submit_manifest_record_type .restrict_rules <- dcc_config_react()$validate_restrict_rules - + .hide_blanks <- dcc_config_react()$submit_hide_blanks + # associates metadata with data and returns manifest id promises::future_promise({ switch(dca_schematic_api, @@ -882,7 +885,8 @@ shinyServer(function(input, output, session) { asset_view = .asset_view, use_schema_label=.submit_use_schema_labels, manifest_record_type=.submit_manifest_record_type, - table_manipulation=.table_manipulation), + table_manipulation=.table_manipulation, + hide_blanks=.hide_blanks), "synXXXX - No data uploaded" ) }) %...>% manifest_id() From 0a624b8d22c259729cfacf5c185fd5e4f72cf156 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Wed, 5 Jul 2023 11:29:50 -0700 Subject: [PATCH 17/40] Set manifest_id reactive to NULL after submitting. Require manifest_id to not be NULL in manifest_id observer (#550) --- server.R | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server.R b/server.R index fe58cd63..d3bec989 100644 --- a/server.R +++ b/server.R @@ -896,6 +896,8 @@ shinyServer(function(input, output, session) { }) observeEvent(manifest_id(), { + + req(!is.null(manifest_id())) manifest_path <- tags$a(href = paste0("https://www.synapse.org/#!Synapse:", manifest_id()), manifest_id(), target = "_blank") # add log message @@ -917,5 +919,6 @@ shinyServer(function(input, output, session) { " is not a valid Synapse ID. Try again?" )), sleep = 0) } + manifest_id(NULL) }) }) From 3892d282291ec5eeba3b818d5afb6f8e8a413814 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Thu, 6 Jul 2023 06:35:38 -0700 Subject: [PATCH 18/40] Dev swap folder template tab (#551) * Set manifest_id reactive to NULL after submitting. Require manifest_id to not be NULL in manifest_id observer * Swap order of folder and template selection tabs. * Switch template download buttons to template tab instead of folder tab. The two tabs were reordered in ui.R * Move template and folder name display to template download tab logic. Make tab transition to template download tab after clicking download. * Change text of buttons for clarity. * Change text of download header --- server.R | 36 +++++++++++++++++----------- ui.R | 72 ++++++++++++++++++++++++++++---------------------------- 2 files changed, 58 insertions(+), 50 deletions(-) diff --git a/server.R b/server.R index d3bec989..6da39f19 100644 --- a/server.R +++ b/server.R @@ -340,10 +340,11 @@ shinyServer(function(input, output, session) { observeEvent(data_list$folders(), ignoreInit = TRUE, { updateTabsetPanel(session, "tabs", - selected = "tab_template_select") + selected = "tab_folder") shinyjs::show(select = "li:nth-child(3)") updateSelectInput(session, "header_dropdown_project", choices = selected$project()) + updateSelectInput(session, "dropdown_folder", choices = data_list$folders()) dcWaiter("hide") }) @@ -357,16 +358,17 @@ shinyServer(function(input, output, session) { dcWaiter("show", msg = "Please wait", color = col2rgba(dcc_config_react()$primary_col, 255*0.9), sleep=0) shinyjs::disable("btn_template_select") selected$schema(data_list$template()[input$dropdown_template]) - updateSelectInput(session, "dropdown_folder", choices = data_list$folders()) - updateTabsetPanel(session, "tabs", selected = "tab_folder") - updateSelectInput(session, "header_dropdown_template", - choices = selected$schema()) - shinyjs::show(select = "li:nth-child(4)") + shinyjs::show(select = "li:nth-child(5)") + shinyjs::show(select = "li:nth-child(6)") + updateTabsetPanel(session, "tabs", + selected = "tab_template") dcWaiter("hide") }) observeEvent(input$dropdown_template, { - shinyjs::enable("btn_template_select") + shinyjs::enable("btn_template") + updateSelectInput(session, "header_dropdown_template", + choices = input$dropdown_template) }) # Goal of this button is to get the files within a folder the user selects @@ -374,16 +376,12 @@ shinyServer(function(input, output, session) { dcWaiter("show", msg = paste0("Getting data"), color = col2rgba(dcc_config_react()$primary_col, 255*0.9)) shinyjs::disable("btn_folder") - shinyjs::show(select = "li:nth-child(5)") - shinyjs::show(select = "li:nth-child(6)") + shinyjs::show(select = "li:nth-child(4)") + updateTabsetPanel(session, "tabs", - selected = "tab_template") + selected = "tab_template_select") - output$template_title <- renderText({ sprintf("Get %s template for %s", - selected$schema(), - names(selected$folder())) - }) # clean tags in generating-template tab sapply(clean_tags[1:2], FUN = hide) @@ -510,6 +508,16 @@ shinyServer(function(input, output, session) { }) + observeEvent(input$tabs, { + req(input$tabs %in% "tab_template") + output$template_title <- renderText({ + sprintf("Go to %s template for %s folder", + selected$schema(), + names(selected$folder()) + ) + }) + }) + observeEvent(input$tabs, { req(input$tabs %in% c("tab_project", "tab_template_select", "tab_folder", "tab_template", "tab_upload")) shinyjs::addClass(id = "header_selection_dropdown", class = "dropdown open") diff --git a/ui.R b/ui.R index 0485b74b..6ded2efd 100644 --- a/ui.R +++ b/ui.R @@ -47,20 +47,20 @@ ui <- shinydashboardPlus::dashboardPage( ) ), div( - id = "header_content_template", + id = "header_content_folder", selectizeInput( - inputId = "header_dropdown_template", + inputId = "header_dropdown_folder", label = NULL, - choices = "No template selected", + choices = "No folder selected", options = list(maxItems = "1") ) ), div( - id = "header_content_folder", + id = "header_content_template", selectizeInput( - inputId = "header_dropdown_folder", + inputId = "header_dropdown_template", label = NULL, - choices = "No folder selected", + choices = "No template selected", options = list(maxItems = "1") ) ) @@ -84,16 +84,16 @@ ui <- shinydashboardPlus::dashboardPage( tabName = "tab_project", icon = icon("database") ), - menuItem( - "Select Template", - tabName = "tab_template_select", - icon = icon("table") - ), menuItem( "Select Folder", tabName = "tab_folder", icon = icon("folder") ), + menuItem( + "Select Template", + tabName = "tab_template_select", + icon = icon("table") + ), menuItem( "Download Template", tabName = "tab_template", @@ -138,7 +138,7 @@ ui <- shinydashboardPlus::dashboardPage( choices = setNames(dcc_config$synapse_asset_view, dcc_config$project_name) ), - actionButton("btn_asset_view", "Go", + actionButton("btn_asset_view", "Next", class = "btn-primary-color" ) ) @@ -157,53 +157,53 @@ ui <- shinydashboardPlus::dashboardPage( label = NULL, choices = "Generating..." ), - actionButton("btn_project", "Go", + actionButton("btn_project", "Next", class = "btn-primary-color" ) ), ), ), tabItem( - tabName = "tab_template_select", + tabName = "tab_folder", fluidRow( box( - id = "box_pick_template", + id = "box_pick_folder", status = "primary", width = 6, - title = "Select a Template: ", - selectInput( - inputId = "dropdown_template", + title = "Select a Folder: ", + selectInput( + inputId = "dropdown_folder", label = NULL, choices = "Generating..." ), - actionButton("btn_template_select", "Go", - class = "btn-primary-color" + actionButton("btn_folder", "Next", + class = "btn-primary-color" ) ) - ), + ) ), tabItem( - tabName = "tab_folder", + tabName = "tab_template_select", fluidRow( box( - id = "box_pick_folder", + id = "box_pick_template", status = "primary", width = 6, - title = "Select a Folder: ", - selectInput( - inputId = "dropdown_folder", - label = NULL, - choices = "Generating..." - ), - actionButton("btn_folder", "Download template", - class = "btn-primary-color" + title = "Select a Template: ", + selectInput( + inputId = "dropdown_template", + label = NULL, + choices = "Generating..." ), - actionButton("btn_folder_have_template", - "I already have a template or manifest", - class = "btn-primary-color" - ) + actionButton("btn_template_select", "Download template", + class = "btn-primary-color" + ), + actionButton("btn_folder_have_template", + "Skip to validation", + class = "btn-primary-color" + ) ) - ) + ), ), tabItem( tabName = "tab_template", From 7c132a9d45669c37a7d63185eefbd1d3e0392247 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Wed, 12 Jul 2023 11:40:07 -0700 Subject: [PATCH 19/40] Add service desk link in feature_request.md (#553) * Add service desk link in feature_request.md Add service desk link to issues template. * Add service desk link in bug_report.md Add service desk link in bug report issues. --- .github/ISSUE_TEMPLATE/bug_report.md | 29 +---------------------- .github/ISSUE_TEMPLATE/feature_request.md | 22 +---------------- 2 files changed, 2 insertions(+), 49 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f40079e0..818170fc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,31 +7,4 @@ assignees: '' --- -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Priority** (select one) -- [ ] Minor ⬇️ -- [ ] Major 📢 -- [ ] Critical 🆘 - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (if applicable, please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. +Please use [Sage Bionetwork's FAIR Data service desk](https://sagebionetworks.jira.com/servicedesk/customer/portal/5/group/8) to create issues. The GitHub Issues in this repo are not regularly monitored. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index b571f835..4239e0ab 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -7,24 +7,4 @@ assignees: '' --- -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**How important is this feature?** Select from the options below: -• 🏝 Low - it's an enhancement but not crucial for work -• 🌗 Medium - can do work without it; but it's important (e.g. to save time or for convenience) -• 🌋 Important - it's a blocker and can't do work without it - -**When will use cases depending on this become relevant?** Select from the options below: -• Short-term - 2-4 weeks -• Mid-term - 2-4 months -• Long-term - 6 months - 1 year - -**Additional context** -Add any other context or screenshots about the feature request here. -**Additional context** -Add any other context or screenshots about the feature request here. - +Please use [Sage Bionetwork's FAIR Data service desk](https://sagebionetworks.jira.com/servicedesk/customer/portal/5/group/8) to create issues. The GitHub Issues in this repo are not regularly monitored. From 13960e761dd0e681653500ae566b9b1c3da68cc6 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Wed, 12 Jul 2023 11:42:19 -0700 Subject: [PATCH 20/40] Fix links README.md (#554) * Fix links README.md Update links to dcc_config documentation. * Update README.md remove links to sage apps --- README.md | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 49b5cd9c..6f68de35 100644 --- a/README.md +++ b/README.md @@ -4,25 +4,10 @@ The *Data Curator App* is an R Shiny app that serves as the *frontend* to the [schematic Python package](github.com/sage-Bionetworks/schematic/). It allows data contributors to easily annotate, validate and submit their metadata. -## Quickstart - -Sage Bionetworks hosts a version of Data Curator App for its collaborators. [Access it here](link%20TBD).\ -To configure your project for this version, fork this repo and append [dcc_config.csv](dcc_config.csv). Then submit a pull request to [this branch](https://github.com/Sage-Bionetworks/data_curator/tree/beta-schematic-rest-api). [dcc_config.csv](dcc_config.csv) contains the following. **Bold fields** are required: - -- **project_name**: The display name of your project\ -- **synapse_asset_view**: The synapse ID of your project's asset view\ -- **data_model_url**: A URL to your data model. Must be the **raw** file if using GitHub\ -- **template_menu_config_file**: www/template_config/\_config.json. This file can be generated by hand or with [config_schema.py](.github/config_schema.py)\ -- **manifest_output_format**: "excel"\ -- **submit_use_schema_labels**: Schematic option to use schema labels when submitting (default TRUE) TRUE or FALSE\ -- **submit_table_manipulation**: Schematic option when submitting (default "replace") "replace" or "upsert"\ -- **submit_manifest_record_type**: Schematic option when submitting. -- **use_compliance_dashboard**: (default FALSE) TRUE or FALSE\ -- primary_col: (default Sage theme) hexadecimal color code\ -- secondary_col; (default Sage theme) hexadecimal color code\ -- sidebar_col: (default Sage theme) hexadecimal color code - -Your pull request should include: - The modifications to [dcc_config.csv](dcc_config.csv) - A dropdown template config [www/template_config/\_config.json](www/template_config/config.json) - Optional: A .png or .svg logo for your project in `www/img` +## Sage's Multitenant DCA + +Sage Bionetworks hosts a version of Data Curator App for its collaborators.\ +To configure your project for this version, fork [data_curator_config](https://github.com/Sage-Bionetworks/data_curator_config/) and follow the instructions in the [README](https://github.com/Sage-Bionetworks/data_curator_config/#readme). Other things you will need:\ - A Synapse asset view for you project\ @@ -62,18 +47,19 @@ poetry install poetry run python3 run_api.py ``` -To use Schematic through its REST API, run the service locally using the commands above. Or access [Schematic hosted by Sage Bionetwork](link%20TBD). +To use Schematic through its REST API, run the service locally using the commands above. Or access Schematic hosted by Sage Bionetwork. ### 3. Configure App -Many app and schematic configurations are set in `dcc_config.yml` as described in [Quickstart](#quickstart). The following are stored as environment variables. Add these to `.Renviron`. +Many app and schematic configurations are set in [dcc_config.csv](https://github.com/Sage-Bionetworks/data_curator_config/blob/main/dcc_config.csv). The following are stored as environment variables. Add these to `.Renviron`. Schematic configurations ``` DCA_SCHEMATIC_API_TYPE: "rest", "reticulate", or "offline" DCA_API_HOST: "" (blank string) if not using the REST API, otherwise URL to schematic service -DCA_API_PORT: "" (blank string) if not using the REST API **LOCALLY**, otherwise the port. Usually 3001. +DCA_DCC_CONFIG: URL or filepath to dcc_config.csv file +DCA_SYNAPSE_PROJECT_API: TRUE or FALSE whether to use the Synapse API instead of Schematic for some fileview queries ``` OAuth-related variables From 595b8bde1dca15f9c061f81b287807855d3df6df Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Wed, 12 Jul 2023 11:42:58 -0700 Subject: [PATCH 21/40] Dev cleanup for release (#552) * Develop (#522) * Develop no poetry (#477) * Set instance size to xxxlarge and log level to verbose during app deploy. * Only install schematic from pip, never use develop branch. * configureApp() will fail if the app does not exist yet. Put after deployApp() in case of first deployment. * try setting upload to FALSE and then configure it. The configApp() fails after deployApp() * if app exists, configure then deploy. Otherwise, deploy then configure. * change appName to testingXXX so it creates a new app instance and tests if the rsconnect configuration logic works. * Logic worked, go back to testing1 name * coerce warning to character so strsplit() does not fail. (#520) --------- Co-authored-by: Rongrong Chai <73901500+rrchai@users.noreply.github.com> * Update trigger for docker build workflow to use release version tag and dev branches * Remove empty line at EOF --------- Co-authored-by: Rongrong Chai <73901500+rrchai@users.noreply.github.com> --- .github/workflows/docker_build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index 54728c7b..ec0f2172 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -8,6 +8,9 @@ on: push: tags: - '*beta*' + - v[0-9]+.[0-9]+.[0-9]+ + branches: + - 'dev*' env: REGISTRY: ghcr.io From 09477156b51dbc7647a2e57fe84b49b821f120da Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Wed, 12 Jul 2023 12:48:11 -0700 Subject: [PATCH 22/40] Upgrade renv to v1.0.0 (#558) * Upgrade renv to v1.0.0 * quote dev branch to capture regex pattern --- .github/workflows/shinyapps_deploy.yml | 2 +- renv.lock | 2 +- renv/activate.R | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/shinyapps_deploy.yml b/.github/workflows/shinyapps_deploy.yml index c1d7509e..ab9961d8 100644 --- a/.github/workflows/shinyapps_deploy.yml +++ b/.github/workflows/shinyapps_deploy.yml @@ -4,7 +4,7 @@ on: push: branches: - main - - dev* + - 'dev*' tags: - v[0-9]+.[0-9]+.[0-9]+ paths-ignore: diff --git a/renv.lock b/renv.lock index 89f7e957..7ebbb457 100644 --- a/renv.lock +++ b/renv.lock @@ -909,7 +909,7 @@ }, "renv": { "Package": "renv", - "Version": "0.17.2", + "Version": "1.0.0", "OS_type": null, "Repository": "CRAN", "Source": "Repository" diff --git a/renv/activate.R b/renv/activate.R index e17d5886..c37000a9 100644 --- a/renv/activate.R +++ b/renv/activate.R @@ -2,7 +2,7 @@ local({ # the requested version of renv - version <- "0.17.2" + version <- "1.0.0" # the project directory project <- getwd() From f0960a782ba8c867ddf24e3747650f72c48be9a3 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Wed, 12 Jul 2023 12:49:09 -0700 Subject: [PATCH 23/40] Update schem configs (#556) * Remove schematic_config.yml from .github * Use example data model in schematic_config.ym --- .github/schematic_config.yml | 42 ------------------------------------ schematic_config.yml | 4 +--- 2 files changed, 1 insertion(+), 45 deletions(-) delete mode 100644 .github/schematic_config.yml diff --git a/.github/schematic_config.yml b/.github/schematic_config.yml deleted file mode 100644 index 01f46044..00000000 --- a/.github/schematic_config.yml +++ /dev/null @@ -1,42 +0,0 @@ -# During the github workflow to auto deploy the app -# This config file will be used to overwrite the config.yml in the schematic folder -# -# Please modify the configuration values based on your project - -# Do not change the 'definitions' section unless you know what you're doing -definitions: - synapse_config: '.synapseConfig' - creds_path: 'credentials.json' - token_pickle: 'token.pickle' - service_acct_creds: 'schematic_service_account_creds.json' - -synapse: - master_fileview: 'syn20446927' # fileview of project with datasets on Synapse - manifest_folder: 'manifests' # manifests will be downloaded to this folder - manifest_filename: 'synapse_storage_manifest.csv' # name of the manifest file in the project dataset - token_creds: 'syn23643259' # synapse ID of credentials.json file - service_acct_creds: 'syn25171627' # synapse ID of service_account_creds.json file - -manifest: - title: 'Patient Manifest' # title of metadata manifest file - data_type: 'Patient' # component or data type from the data model - -model: - input: - download_url: 'https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld' # url to download JSON-LD data model - location: 'data-models/HTAN.model.jsonld' # path to JSON-LD data model - file_type: 'local' # only type "local" is supported currently - -style: - google_manifest: - req_bg_color: - red: 0.9215 - green: 0.9725 - blue: 0.9803 - opt_bg_color: - red: 1.0 - green: 1.0 - blue: 0.9019 - master_template_id: '1LYS5qE4nV9jzcYw5sXwCza25slDfRA1CIg3cs-hCdpU' - strict_validation: true - diff --git a/schematic_config.yml b/schematic_config.yml index fd602f0c..54ca0a82 100644 --- a/schematic_config.yml +++ b/schematic_config.yml @@ -20,7 +20,7 @@ manifest: - Patient model: input: - download_url: https://raw.githubusercontent.com/mc2-center/data-models/main/mc2.model.jsonld + download_url: https://raw.githubusercontent.com/Sage-Bionetworks/data-models/main/example.model.jsonld location: data-models/example.model.jsonld file_type: local style: @@ -35,5 +35,3 @@ style: blue: 0.9019 master_template_id: 1LYS5qE4nV9jzcYw5sXwCza25slDfRA1CIg3cs-hCdpU strict_validation: yes -api: - type: reticulate From e6c3552e49be890e1069d30699671f73423f8a4d Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Wed, 26 Jul 2023 11:12:25 -0700 Subject: [PATCH 24/40] Dev fds690 table error (#559) * use future = TRUE in renderDT to allow asynchronous table processing. * Use future plan multicore instead of multisession. This appears to be more stable with renderDT(future=TRUE,...) * Add Renviron with RENV_CONFIG_SANDBOX_ENABLED = FALSE to avoid long startup times with renv v1.0.0 --- .Renviron | 1 + global.R | 2 +- modules/DTable.R | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 .Renviron diff --git a/.Renviron b/.Renviron new file mode 100644 index 00000000..7be1ac24 --- /dev/null +++ b/.Renviron @@ -0,0 +1 @@ +RENV_CONFIG_SANDBOX_ENABLED = FALSE diff --git a/global.R b/global.R index fb883145..088114fa 100644 --- a/global.R +++ b/global.R @@ -29,7 +29,7 @@ suppressPackageStartupMessages({ # Set up futures/promises for asynchronous calls ncores <- availableCores() message(sprintf("Available cores: %s", ncores)) -plan(multisession, workers = ncores) +plan(multicore, workers = ncores) # import R files source_files <- list.files(c("functions", "modules"), pattern = "*\\.R$", recursive = TRUE, full.names = TRUE) diff --git a/modules/DTable.R b/modules/DTable.R index 4237679d..8632c398 100644 --- a/modules/DTable.R +++ b/modules/DTable.R @@ -57,7 +57,7 @@ DTableServer <- function(id, data, escape = TRUE, df <- df %>% formatStyle(1:ncol(data), border = "1px solid #ddd") } - output$table <- renderDT(df) + output$table <- renderDT(df, future = TRUE) } ) } From 2018e467964e36a2f45ef5754fd495c3daa2e906 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Wed, 26 Jul 2023 14:15:22 -0700 Subject: [PATCH 25/40] =?UTF-8?q?Refactor=20trimEmptyRows=20to=20avoid=20a?= =?UTF-8?q?n=20error=20uploading=20manifests=20with=2074+=E2=80=A6=20(#560?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor trimEmptyRows to avoid an error uploading manifests with 74+ columns. This issue only happens in the Ubuntu docker image, not Mac. The error is * Revert to renv 0.17.3 * Remove Renviron with RENV_CONFIG_SANDBOX_ENABLED = FALSE since we have reverted to renv 0.17.3 --- .Renviron | 1 - modules/csvInfile.R | 5 ++++- renv.lock | 2 +- renv/activate.R | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) delete mode 100644 .Renviron diff --git a/.Renviron b/.Renviron deleted file mode 100644 index 7be1ac24..00000000 --- a/.Renviron +++ /dev/null @@ -1 +0,0 @@ -RENV_CONFIG_SANDBOX_ENABLED = FALSE diff --git a/modules/csvInfile.R b/modules/csvInfile.R index 616b767b..9afe2fb2 100644 --- a/modules/csvInfile.R +++ b/modules/csvInfile.R @@ -28,7 +28,10 @@ csvInfileServer <- function(id, na = c("", "NA"), colsAsCharacters = FALSE, keep } if (trimEmptyRows) { - infile <- infile %>% filter_all(any_vars(!is.na(.))) + # Originally used the following, but got C buffer error w/ 74+ columns + #infile <- infile %>% filter_all(any_vars(!is.na(.))) + keep_rows <- apply(infile, 1, function(x) !all(is.na(x))) + if (length(keep_rows) > 0) infile <- infile[which(keep_rows), ] } if (keepBlank) { diff --git a/renv.lock b/renv.lock index 7ebbb457..ca056fa4 100644 --- a/renv.lock +++ b/renv.lock @@ -909,7 +909,7 @@ }, "renv": { "Package": "renv", - "Version": "1.0.0", + "Version": "0.17.3", "OS_type": null, "Repository": "CRAN", "Source": "Repository" diff --git a/renv/activate.R b/renv/activate.R index c37000a9..d77c4c83 100644 --- a/renv/activate.R +++ b/renv/activate.R @@ -2,7 +2,7 @@ local({ # the requested version of renv - version <- "1.0.0" + version <- "0.17.3" # the project directory project <- getwd() From 801f1a6f9556d9cd3b703eddeb6f05b3b3d347f1 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Wed, 2 Aug 2023 11:16:12 -0700 Subject: [PATCH 26/40] set data_type to NULL in submit to avoid validation. Read csv as character to avoid uppercase booleans. (#562) --- server.R | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server.R b/server.R index 6da39f19..a3070167 100644 --- a/server.R +++ b/server.R @@ -779,7 +779,7 @@ shinyServer(function(input, output, session) { dir.create(tmp_out_dir, showWarnings = FALSE) # reads file csv again - submit_data <- csvInfileServer("inputFile")$data() + submit_data <- csvInfileServer("inputFile", colsAsCharacters = TRUE, keepBlank = TRUE, trimEmptyRows = TRUE)$data() # If a file-based component selected (define file-based components) note for future # the type to filter (eg file-based) on could probably also be a config choice @@ -842,7 +842,7 @@ shinyServer(function(input, output, session) { FALSE), rest = model_submit(url=file.path(api_uri, "v1/model/submit"), schema_url = .data_model, - data_type = .schema, + data_type = NULL, # NULL to bypass validation dataset_id = .folder, access_token = access_token, restrict_rules = .restrict_rules, @@ -874,7 +874,7 @@ shinyServer(function(input, output, session) { .submit_manifest_record_type <- dcc_config_react()$submit_manifest_record_type .restrict_rules <- dcc_config_react()$validate_restrict_rules .hide_blanks <- dcc_config_react()$submit_hide_blanks - + # associates metadata with data and returns manifest id promises::future_promise({ switch(dca_schematic_api, @@ -885,7 +885,7 @@ shinyServer(function(input, output, session) { FALSE), rest = model_submit(url=file.path(api_uri, "v1/model/submit"), schema_url = .data_model, - data_type = .schema, + data_type = NULL, # NULL to bypass validation dataset_id = .folder, access_token = access_token, restrict_rules = .restrict_rules, From afec23ccab1ae44b00813a9a9132063081aee617 Mon Sep 17 00:00:00 2001 From: afwillia Date: Tue, 22 Aug 2023 16:06:00 -0700 Subject: [PATCH 27/40] When a project contains no folders, display a popup box instead of crashing without error. --- server.R | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server.R b/server.R index a3070167..2aa3c0be 100644 --- a/server.R +++ b/server.R @@ -318,6 +318,7 @@ shinyServer(function(input, output, session) { .asset_view <- selected$master_asset_view() promises::future_promise({ + try({ folder_list_raw <- switch( dca_schematic_api, reticulate = storage_projects_datasets_py( @@ -333,6 +334,7 @@ shinyServer(function(input, output, session) { folder_list <- list2Vector(folder_list_raw) folder_list[sort(names(folder_list))] + }, silent = TRUE) }) %...>% data_list$folders() }) @@ -345,6 +347,10 @@ shinyServer(function(input, output, session) { updateSelectInput(session, "header_dropdown_project", choices = selected$project()) updateSelectInput(session, "dropdown_folder", choices = data_list$folders()) + + if (inherits(data_list$folders(), "try-error")) { + nx_report_error(title = "Error retrieving folders", message = "Project contains no folders") + } dcWaiter("hide") }) From b142940b392c315c891dad6367ea35e883f9a81e Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Wed, 23 Aug 2023 12:12:23 -0700 Subject: [PATCH 28/40] Get dca-template-config from the same branch in data_curator_config as dcc_config.csv (#565) --- server.R | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/server.R b/server.R index a3070167..de8dd0ee 100644 --- a/server.R +++ b/server.R @@ -34,10 +34,12 @@ shinyServer(function(input, output, session) { ######## session global variables ######## # read config in - def_config <- ifelse(dca_schematic_api == "offline", - fromJSON("https://raw.githubusercontent.com/Sage-Bionetworks/data_curator_config/main/demo/dca-template-config.json"), - fromJSON("https://raw.githubusercontent.com/Sage-Bionetworks/data_curator_config/main/demo/dca-template-config.json") - ) + if (grepl("dev", dcc_config_file)) { + def_config <- fromJSON("https://raw.githubusercontent.com/Sage-Bionetworks/data_curator_config/dev/demo/dca-template-config.json") + } else if (grepl("staging", dcc_config_file)) { + def_config <- fromJSON("https://raw.githubusercontent.com/Sage-Bionetworks/data_curator_config/staging/demo/dca-template-config.json") + } else def_config <- fromJSON("https://raw.githubusercontent.com/Sage-Bionetworks/data_curator_config/main/demo/dca-template-config.json") + config <- reactiveVal() config_schema <- reactiveVal(def_config) model_ops <- setNames(dcc_config$data_model_url, @@ -227,13 +229,29 @@ shinyServer(function(input, output, session) { ) } + # Use the template dropdown config file from the appropriate branch of + # data_curator_config conf_file <- reactiveVal(template_config_files[input$dropdown_asset_view]) if (!file.exists(conf_file())){ - conf_file( - file.path("https://raw.githubusercontent.com/Sage-Bionetworks/data_curator_config/main", - conf_file() - ) + if (grepl("dev", dcc_config_file)) { + conf_file( + file.path("https://raw.githubusercontent.com/Sage-Bionetworks/data_curator_config/dev", + conf_file() + ) + ) + } else if (grepl("staging", dcc_config_file)) { + conf_file( + file.path("https://raw.githubusercontent.com/Sage-Bionetworks/data_curator_config/staging", + conf_file() + ) ) + } else { + conf_file( + file.path("https://raw.githubusercontent.com/Sage-Bionetworks/data_curator_config/main", + conf_file() + ) + ) + } } config_df <- jsonlite::fromJSON(conf_file()) From dbd1c55639e6940e370426a75c6867d5b6cec4e9 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Wed, 23 Aug 2023 13:10:53 -0700 Subject: [PATCH 29/40] Use precommit (#566) * Get dca-template-config from the same branch in data_curator_config as dcc_config.csv * Add precommit file from precommit::use_precommit * Add .pre-commit-config.yml to .Rbuildignore * Don't build container on dev branches. Build with rc tag * Don't run test schematic workflow on push * Don't run shiny deploy workflow * Run workflows on pr into main * Make pr trigger pull_request --- .Rbuildignore | 1 + .github/workflows/docker_build.yml | 4 +- .github/workflows/shinyapps_deploy.yml | 10 +--- .github/workflows/test_schematic_api.yml | 9 +-- .pre-commit-config.yaml | 73 ++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 18 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.Rbuildignore b/.Rbuildignore index 0e9fd074..dcae32c1 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -1,2 +1,3 @@ +^\.pre-commit-config\.yaml$ ^renv$ ^renv\.lock$ diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index ec0f2172..41957007 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -8,9 +8,8 @@ on: push: tags: - '*beta*' + - '*rc*' - v[0-9]+.[0-9]+.[0-9]+ - branches: - - 'dev*' env: REGISTRY: ghcr.io @@ -51,4 +50,3 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - diff --git a/.github/workflows/shinyapps_deploy.yml b/.github/workflows/shinyapps_deploy.yml index ab9961d8..727c0afb 100644 --- a/.github/workflows/shinyapps_deploy.yml +++ b/.github/workflows/shinyapps_deploy.yml @@ -1,16 +1,10 @@ name: shiny-deploy on: - push: + pull_request: branches: - main - - 'dev*' - tags: - - v[0-9]+.[0-9]+.[0-9]+ - paths-ignore: - - '.github/ISSUE_TEMPLATE/**' - - '**/*.md' - - '**/.gitignore' + jobs: shiny-deploy: runs-on: ubuntu-latest diff --git a/.github/workflows/test_schematic_api.yml b/.github/workflows/test_schematic_api.yml index b331cd1b..e2bb7dd7 100644 --- a/.github/workflows/test_schematic_api.yml +++ b/.github/workflows/test_schematic_api.yml @@ -7,11 +7,9 @@ name: test-schematic-api on: - push: + pull_request: branches: - - develop - paths-ignore: - - .github/ISSUE_TEMPLATE/** + - main jobs: test-schematic-rest-api: @@ -88,6 +86,3 @@ jobs: shell: Rscript {0} run: | devtools::test() - - - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..6b6ea5ea --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,73 @@ +# All available hooks: https://pre-commit.com/hooks.html +# R specific hooks: https://github.com/lorenzwalthert/precommit +repos: +- repo: https://github.com/lorenzwalthert/precommit + rev: v0.3.2.9019 + hooks: + - id: style-files + args: [--style_pkg=styler, --style_fun=tidyverse_style] + - id: roxygenize + # codemeta must be above use-tidy-description when both are used + # - id: codemeta-description-updated + - id: use-tidy-description + - id: spell-check + exclude: > + (?x)^( + .*\.[rR]| + .*\.feather| + .*\.jpeg| + .*\.pdf| + .*\.png| + .*\.py| + .*\.RData| + .*\.rds| + .*\.Rds| + .*\.Rproj| + .*\.sh| + (.*/|)\.gitignore| + (.*/|)\.gitlab-ci\.yml| + (.*/|)\.lintr| + (.*/|)\.pre-commit-.*| + (.*/|)\.Rbuildignore| + (.*/|)\.Renviron| + (.*/|)\.Rprofile| + (.*/|)\.travis\.yml| + (.*/|)appveyor\.yml| + (.*/|)NAMESPACE| + (.*/|)renv/settings\.dcf| + (.*/|)renv\.lock| + (.*/|)WORDLIST| + \.github/workflows/.*| + data/.*| + )$ + - id: lintr + - id: readme-rmd-rendered + - id: parsable-R + - id: no-browser-statement + - id: no-debug-statement + - id: deps-in-desc +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + args: ['--maxkb=200'] + - id: file-contents-sorter + files: '^\.Rbuildignore$' + - id: end-of-file-fixer + exclude: '\.Rd' +- repo: https://github.com/pre-commit-ci/pre-commit-ci-config + rev: v1.5.1 + hooks: + # Only reuiqred when https://pre-commit.ci is used for config validation + - id: check-pre-commit-ci-config +- repo: local + hooks: + - id: forbid-to-commit + name: Don't commit common R artifacts + entry: Cannot commit .Rhistory, .RData, .Rds or .rds. + language: fail + files: '\.(Rhistory|RData|Rds|rds)$' + # `exclude: ` to allow committing specific files + +ci: + autoupdate_schedule: monthly From 53ace37b1cf3bf289de0938cc962d0e5449c08d8 Mon Sep 17 00:00:00 2001 From: afwillia Date: Thu, 24 Aug 2023 11:06:28 -0700 Subject: [PATCH 30/40] Hide the OK button in the error popup so users can't proceed. Format message with more information about the error and how to proceed. --- server.R | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server.R b/server.R index b5d087cd..a494fbec 100644 --- a/server.R +++ b/server.R @@ -367,7 +367,14 @@ shinyServer(function(input, output, session) { updateSelectInput(session, "dropdown_folder", choices = data_list$folders()) if (inherits(data_list$folders(), "try-error")) { - nx_report_error(title = "Error retrieving folders", message = "Project contains no folders") + nx_report_error(title = "Error retrieving folders", + message = tagList( + p("Check if this project contains folders and users have appropriate access permissions."), + p("Refresh the app to try again or contact the DCC for help."), + p("For debugging: ", data_list$folders()) + ) + ) + hide(selector = "#NXReportButton") # hide OK button so users can't continue } dcWaiter("hide") }) From 86fc77a66cdac3bd90df6bda8a463b3eb7506516 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Thu, 24 Aug 2023 11:09:45 -0700 Subject: [PATCH 31/40] Wrap synapse_get_project in try() to better handle instances where no projects are returned. If no projects are returned, update the waiter screen with a message about fileview and project permissions (#567) --- server.R | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/server.R b/server.R index de8dd0ee..2e66d545 100644 --- a/server.R +++ b/server.R @@ -266,16 +266,17 @@ shinyServer(function(input, output, session) { .asset_view <- selected$master_asset_view() promises::future_promise({ - scopes <- synapse_get_project_scope(id = .asset_view, auth = access_token) - scope_access <- vapply(scopes, function(x) { - synapse_access(id=x, access="DOWNLOAD", auth=access_token) - }, 1L) - scopes <- scopes[scope_access==1] - projects <- bind_rows( - lapply(scopes, function(x) synapse_get(id=x, auth=access_token)) - ) %>% arrange(name) - setNames(projects$id, projects$name) - + try({ + scopes <- synapse_get_project_scope(id = .asset_view, auth = access_token) + scope_access <- vapply(scopes, function(x) { + synapse_access(id=x, access="DOWNLOAD", auth=access_token) + }, 1L) + scopes <- scopes[scope_access==1] + projects <- bind_rows( + lapply(scopes, function(x) synapse_get(id=x, auth=access_token)) + ) %>% arrange(name) + setNames(projects$id, projects$name) + }, silent = FALSE) }) %...>% data_list$projects() } else { @@ -291,7 +292,8 @@ shinyServer(function(input, output, session) { }) observeEvent(data_list$projects(), ignoreInit = TRUE, { - if (is.null(data_list$projects()) || length(data_list$projects()) == 0) { + if (is.null(data_list$projects()) || length(data_list$projects()) == 0 || + inherits(data_list$projects(), "try-error")) { dcWaiter("update", landing = TRUE, isPermission = FALSE) } else { @@ -303,7 +305,6 @@ shinyServer(function(input, output, session) { ) }) }) - } updateTabsetPanel(session, "tabs", selected = "tab_project") @@ -314,6 +315,7 @@ shinyServer(function(input, output, session) { shinyjs::hide(select = "li:nth-child(6)") dcWaiter("hide") + } }) observeEvent(input$dropdown_asset_view, { From 3db9b11f13a69fdc3f984c40972f18bdba472c10 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Thu, 24 Aug 2023 11:23:38 -0700 Subject: [PATCH 32/40] =?UTF-8?q?enable=20template=20download=20button=20a?= =?UTF-8?q?fter=20changing=20the=20template=20dropdown=20=E2=80=A6=20(#563?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enable template download button after changing the template dropdown input * Enable the download template button if previous tab inputs change but manifest type stays the same. --- server.R | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server.R b/server.R index 2e66d545..efd2f213 100644 --- a/server.R +++ b/server.R @@ -320,6 +320,7 @@ shinyServer(function(input, output, session) { observeEvent(input$dropdown_asset_view, { shinyjs::enable("btn_asset_view") + shinyjs::enable("btn_template_select") }) # Goal of this observer is to get all of the folders within the selected @@ -370,6 +371,7 @@ shinyServer(function(input, output, session) { observeEvent(input$dropdown_project, { shinyjs::enable("btn_project") + shinyjs::enable("btn_template_select") }) # Goal of this button is to updpate the template reactive object @@ -386,7 +388,8 @@ shinyServer(function(input, output, session) { }) observeEvent(input$dropdown_template, { - shinyjs::enable("btn_template") + shinyjs::enable("btn_template") + shinyjs::enable("btn_template_select") updateSelectInput(session, "header_dropdown_template", choices = input$dropdown_template) }) @@ -437,6 +440,7 @@ shinyServer(function(input, output, session) { observeEvent(input$dropdown_folder,{ shinyjs::enable("btn_folder") + shinyjs::enable("btn_template_select") selected_folder <- data_list$folders()[which(data_list$folders() == input$dropdown_folder)] selected$folder(selected_folder) updateSelectInput(session, "header_dropdown_folder", @@ -496,6 +500,7 @@ shinyServer(function(input, output, session) { ######## Update Template ######## # update selected schema template name observeEvent(input$dropdown_template, { + shinyjs::enable("btn_template_select") # update reactive selected values for schema selected$schema(data_list$template()[input$dropdown_template]) schema_type <- config_schema()[[1]]$type[which(config_schema()[[1]]$display_name == input$dropdown_template)] From 1c131a024b2a6162c19fb013013e0746e99a4b57 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Thu, 24 Aug 2023 14:22:55 -0700 Subject: [PATCH 33/40] Add strict_validation argument to manifest_generate. (#561) --- R/schematic_rest_api.R | 6 ++++-- functions/schematic_rest_api.R | 11 +++++++---- server.R | 3 ++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/R/schematic_rest_api.R b/R/schematic_rest_api.R index 8128da74..c2db54ad 100644 --- a/R/schematic_rest_api.R +++ b/R/schematic_rest_api.R @@ -57,7 +57,8 @@ manifest_generate <- function(url="http://localhost:3001/v1/manifest/generate", schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #nolint title, data_type, use_annotations="false", dataset_id=NULL, - asset_view, output_format, access_token = NULL) { + asset_view, output_format, access_token = NULL, + strict_validation = FALSE) { req <- httr::GET(url, query = list( @@ -68,7 +69,8 @@ manifest_generate <- function(url="http://localhost:3001/v1/manifest/generate", dataset_id=dataset_id, asset_view=asset_view, output_format=output_format, - access_token = access_token + access_token = access_token, + strict_validation = strict_validation )) check_success(req) diff --git a/functions/schematic_rest_api.R b/functions/schematic_rest_api.R index d498fe3a..c2db54ad 100644 --- a/functions/schematic_rest_api.R +++ b/functions/schematic_rest_api.R @@ -57,7 +57,8 @@ manifest_generate <- function(url="http://localhost:3001/v1/manifest/generate", schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #nolint title, data_type, use_annotations="false", dataset_id=NULL, - asset_view, output_format, access_token = NULL) { + asset_view, output_format, access_token = NULL, + strict_validation = FALSE) { req <- httr::GET(url, query = list( @@ -68,7 +69,8 @@ manifest_generate <- function(url="http://localhost:3001/v1/manifest/generate", dataset_id=dataset_id, asset_view=asset_view, output_format=output_format, - access_token = access_token + access_token = access_token, + strict_validation = strict_validation )) check_success(req) @@ -157,7 +159,7 @@ model_submit <- function(url="http://localhost:3001/v1/model/submit", schema_url="https://raw.githubusercontent.com/ncihtan/data-models/main/HTAN.model.jsonld", #notlint data_type, dataset_id, restrict_rules=FALSE, access_token, json_str=NULL, asset_view, use_schema_label=TRUE, manifest_record_type="table_and_file", file_name, - table_manipulation="replace") { + table_manipulation="replace", hide_blanks=FALSE) { req <- httr::POST(url, #add_headers(Authorization=paste0("Bearer ", pat)), query=list( @@ -170,7 +172,8 @@ model_submit <- function(url="http://localhost:3001/v1/model/submit", asset_view=asset_view, use_schema_label=use_schema_label, manifest_record_type=manifest_record_type, - table_manipulation=table_manipulation), + table_manipulation=table_manipulation, + hide_blanks=hide_blanks), body=list(file_name=httr::upload_file(file_name)) #body=list(file_name=file_name) ) diff --git a/server.R b/server.R index efd2f213..5a7a70d0 100644 --- a/server.R +++ b/server.R @@ -585,7 +585,8 @@ shinyServer(function(input, output, session) { asset_view = .asset_view, use_annotations = .use_annotations, output_format = .output_format, - access_token=access_token + access_token=access_token, + strict_validation = FALSE ), { message("Downloading offline manifest") From 87adb640981aa5f7eb446c6e64f8b1513a03a9a3 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Thu, 24 Aug 2023 14:49:05 -0700 Subject: [PATCH 34/40] Add DCA and Schematic versions to footer of dashboard. (#564) * WIP: Add DCA and Schematic versions to footer of dashboard. * Get schematic version via api call * Use schematic version from variable in global.R * Add version as an env variable to docker container * When building docker container, use the github ref as a build arg to note the version. * Change version variable to DCA_VERSION * Set DCA_VERSION env var to a variable in the global env * Show DCA version from the env var * Use GITHUB_REF_NAME env var to set version env var. * specify github context for github_ref_name var * Update github ref name to github.REF_NAME * For clarity, set build arg to DCA_VERSION instead of TAG. * Add DCA_VERSION env var to .Renviron so it is loaded as env var in shiny server. * Make DCA_VERSION from docker build-arg an env var in the container. Then in dca_startup.sh, add the env var to Renviron * Add error handling for schematic version api call --- .github/workflows/docker_build.yml | 4 ++++ Dockerfile | 6 +++++- dca_startup.sh | 1 + global.R | 10 ++++++++++ ui.R | 3 +++ 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index 41957007..da37c1c6 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -14,6 +14,7 @@ on: env: REGISTRY: ghcr.io IMAGE_PATH: ghcr.io/${{ github.repository }} + DCA_VERSION: ${{ github.REF_NAME }} jobs: build-and-push-image: @@ -50,3 +51,6 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + build-args: | + DCA_VERSION=${{ env.DCA_VERSION }} + diff --git a/Dockerfile b/Dockerfile index ecee0b32..98204915 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,9 @@ FROM ghcr.io/afwillia/shiny-base:release-update-node -LABEL maintainer="Anthony anthony.williams@sagebase.org" + +# add version tag as a build argument +ARG DCA_VERSION + +ENV DCA_VERSION=$DCA_VERSION USER root RUN apt-get update diff --git a/dca_startup.sh b/dca_startup.sh index 32baa0d8..c1c0cc33 100755 --- a/dca_startup.sh +++ b/dca_startup.sh @@ -3,6 +3,7 @@ # Pass environment variable to Shiny echo "" >> .Renviron echo R_CONFIG_ACTIVE=$R_CONFIG_ACTIVE >> .Renviron +echo DCA_VERSION=$DCA_VERSION >> .Renviron # Now run the base start-up script ./startup.sh diff --git a/global.R b/global.R index 088114fa..585042df 100644 --- a/global.R +++ b/global.R @@ -61,8 +61,18 @@ if (dca_schematic_api == "rest") { Sys.getenv("DCA_API_PORT"), sep = ":") ) + + # Get Schematic version + get_schematic_version <- try(httr::GET(file.path(api_uri, "v1/version")), silent=TRUE) + if (inherits(get_schematic_version, "try-error")) { + schematic_version <- "" + } else if (httr::http_error(get_schematic_version)) { + schematic_version <- "" + } else schematic_version <- httr::content(get_schematic_version) } +dca_version <- Sys.getenv("DCA_VERSION") + dca_synapse_api <- Sys.getenv("DCA_SYNAPSE_PROJECT_API") # update port if running app locally diff --git a/ui.R b/ui.R index 6ded2efd..dd4a377f 100644 --- a/ui.R +++ b/ui.R @@ -294,6 +294,9 @@ ui <- shinydashboardPlus::dashboardPage( ), # waiter loading screen dcWaiter("show", landing = TRUE) + ), + footer = dashboardFooter( + left = sprintf("DCA %s - Schematic %s", dca_version, schematic_version) ) ) From e5dd9df73b07e348f716d7daf820c3e2ccee7163 Mon Sep 17 00:00:00 2001 From: afwillia Date: Wed, 30 Aug 2023 11:44:17 -0700 Subject: [PATCH 35/40] Fix missing bracket and parenthesis from merge resolution --- server.R | 1 + 1 file changed, 1 insertion(+) diff --git a/server.R b/server.R index 6bea653f..8150237a 100644 --- a/server.R +++ b/server.R @@ -321,6 +321,7 @@ shinyServer(function(input, output, session) { observeEvent(input$dropdown_asset_view, { shinyjs::enable("btn_asset_view") shinyjs::enable("btn_template_select") + }) # Goal of this observer is to get all of the folders within the selected # project. From 46054b79770e6701df8ba1400f71bee8b88d7762 Mon Sep 17 00:00:00 2001 From: afwillia Date: Mon, 11 Sep 2023 09:38:30 -0700 Subject: [PATCH 36/40] Add a separate check and pop up for a folder list length zero. This indicates a permission issue instead of no folders. --- server.R | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/server.R b/server.R index 9ad5681f..849d6e91 100644 --- a/server.R +++ b/server.R @@ -371,13 +371,23 @@ shinyServer(function(input, output, session) { if (inherits(data_list$folders(), "try-error")) { nx_report_error(title = "Error retrieving folders", message = tagList( - p("Check if this project contains folders and users have appropriate access permissions."), + p("Confirm that this project contains folders."), p("Refresh the app to try again or contact the DCC for help."), p("For debugging: ", data_list$folders()) ) ) hide(selector = "#NXReportButton") # hide OK button so users can't continue } + if (length(data_list$folders()) < 1) { + nx_report_error(title = "Error retrieving folders", + message = tagList( + p("Confirm you have appropriate access permissions."), + p("Refresh the app to try again or contact the DCC for help."), + p("For debugging: ", data_list$folders()) + ) + ) + hide(selector = "#NXReportButton") # hide OK button so users can't continue + } dcWaiter("hide") }) From 114de7f1468539242923376950c323d7860e5406 Mon Sep 17 00:00:00 2001 From: afwillia Date: Mon, 11 Sep 2023 09:44:20 -0700 Subject: [PATCH 37/40] Add a separate check and pop up for a folder list length zero. This indicates a permission issue instead of no folders. --- server.R | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/server.R b/server.R index 9310516e..6a068985 100644 --- a/server.R +++ b/server.R @@ -363,19 +363,19 @@ shinyServer(function(input, output, session) { observeEvent(data_list$folders(), ignoreInit = TRUE, { updateTabsetPanel(session, "tabs", - selected = "tab_folder") + selected = "tab_folder") shinyjs::show(select = "li:nth-child(3)") updateSelectInput(session, "header_dropdown_project", - choices = selected$project()) + choices = selected$project()) updateSelectInput(session, "dropdown_folder", choices = data_list$folders()) - + if (inherits(data_list$folders(), "try-error")) { nx_report_error(title = "Error retrieving folders", - message = tagList( - p("Confirm that this project contains folders."), - p("Refresh the app to try again or contact the DCC for help."), - p("For debugging: ", data_list$folders()) - ) + message = tagList( + p("Confirm that this project contains folders."), + p("Refresh the app to try again or contact the DCC for help."), + p("For debugging: ", data_list$folders()) + ) ) hide(selector = "#NXReportButton") # hide OK button so users can't continue } From e1218d31c797401553d1a35c13e9162aa6dde096 Mon Sep 17 00:00:00 2001 From: afwillia Date: Wed, 13 Sep 2023 08:19:57 -0700 Subject: [PATCH 38/40] wrap manifest/generate in try() then display a popup with the error if it fails --- server.R | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/server.R b/server.R index 8150237a..1c6155b0 100644 --- a/server.R +++ b/server.R @@ -575,6 +575,7 @@ shinyServer(function(input, output, session) { .use_annotations <- dcc_config_react()$manifest_use_annotations promises::future_promise({ + try({ switch(dca_schematic_api, rest = manifest_generate( url=.url, @@ -594,14 +595,26 @@ shinyServer(function(input, output, session) { tibble(a="b", c="d") } ) + }, silent = TRUE) }) %...>% manifest_data() }) observeEvent(manifest_data(), { - if (dcc_config_react()$manifest_output_format == "google_sheet") { - shinyjs::show("div_template") - } else shinyjs::show("div_download_data") + if (inherits(manifest_data(), "try-error")) { + nx_report_error("Failed to get manifest", + tagList( + p("There was a problem downloading the manifest."), + p("Try again or contact the DCC for help"), + p("For debugging: ", manifest_data()) + )) + shinyjs::enable("btn_template_select") + updateTabsetPanel(session, "tab_template_select") + } else { + if (dcc_config_react()$manifest_output_format == "google_sheet") { + shinyjs::show("div_template") + } else shinyjs::show("div_download_data") + } dcWaiter("hide") }) From b69ab93843fe640c61db36c1fbfca49f7aafea95 Mon Sep 17 00:00:00 2001 From: afwillia Date: Tue, 26 Sep 2023 14:01:00 -0700 Subject: [PATCH 39/40] Add error handling to submit --- server.R | 130 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 73 insertions(+), 57 deletions(-) diff --git a/server.R b/server.R index 04080e76..b598ece2 100644 --- a/server.R +++ b/server.R @@ -896,26 +896,28 @@ shinyServer(function(input, output, session) { # associates metadata with data and returns manifest id promises::future_promise({ - switch(dca_schematic_api, - reticulate = model_submit_py(schema_generator, - tmp_file_path, - .folder, - "table", - FALSE), - rest = model_submit(url=file.path(api_uri, "v1/model/submit"), - schema_url = .data_model, - data_type = NULL, # NULL to bypass validation - dataset_id = .folder, - access_token = access_token, - restrict_rules = .restrict_rules, - file_name = tmp_file_path, - asset_view = .asset_view, - use_schema_label=.submit_use_schema_labels, - manifest_record_type=.submit_manifest_record_type, - table_manipulation=.table_manipulation, - hide_blanks=.hide_blanks), - "synXXXX - No data uploaded" - ) + try({ + switch(dca_schematic_api, + reticulate = model_submit_py(schema_generator, + tmp_file_path, + .folder, + "table", + FALSE), + rest = model_submit(url=file.path(api_uri, "v1/model/submit"), + schema_url = .data_model, + data_type = NULL, # NULL to bypass validation + dataset_id = .folder, + access_token = access_token, + restrict_rules = .restrict_rules, + file_name = tmp_file_path, + asset_view = .asset_view, + use_schema_label=.submit_use_schema_labels, + manifest_record_type=.submit_manifest_record_type, + table_manipulation=.table_manipulation, + hide_blanks=.hide_blanks), + "synXXXX - No data uploaded" + ) + }, silent = TRUE) }) %...>% manifest_id() } else { @@ -938,26 +940,28 @@ shinyServer(function(input, output, session) { .hide_blanks <- dcc_config_react()$submit_hide_blanks # associates metadata with data and returns manifest id promises::future_promise({ - switch(dca_schematic_api, - reticulate = model_submit_py(schema_generator, - tmp_file_path, - .folder, - "table", - FALSE), - rest = model_submit(url=file.path(api_uri, "v1/model/submit"), - schema_url = .data_model, - data_type = NULL, # NULL to bypass validation - dataset_id = .folder, - access_token = access_token, - restrict_rules = .restrict_rules, - file_name = tmp_file_path, - asset_view = .asset_view, - use_schema_label=.submit_use_schema_labels, - manifest_record_type=.submit_manifest_record_type, - table_manipulation=.table_manipulation, - hide_blanks=.hide_blanks), - "synXXXX - No data uploaded" - ) + try({ + switch(dca_schematic_api, + reticulate = model_submit_py(schema_generator, + tmp_file_path, + .folder, + "table", + FALSE), + rest = model_submit(url=file.path(api_uri, "v1/model/submit"), + schema_url = .data_model, + data_type = NULL, # NULL to bypass validation + dataset_id = .folder, + access_token = access_token, + restrict_rules = .restrict_rules, + file_name = tmp_file_path, + asset_view = .asset_view, + use_schema_label=.submit_use_schema_labels, + manifest_record_type=.submit_manifest_record_type, + table_manipulation=.table_manipulation, + hide_blanks=.hide_blanks), + "synXXXX - No data uploaded" + ) + }, silent = TRUE) }) %...>% manifest_id() } @@ -967,27 +971,39 @@ shinyServer(function(input, output, session) { observeEvent(manifest_id(), { req(!is.null(manifest_id())) - manifest_path <- tags$a(href = paste0("https://www.synapse.org/#!Synapse:", manifest_id()), manifest_id(), target = "_blank") - # add log message - message(paste0("Manifest :", sQuote(manifest_id()), " has been successfully uploaded")) - - # if no error - if (startsWith(manifest_id(), "syn") == TRUE) { + if (inherits(manifes_id(), "try-error")) { dcWaiter("hide") - nx_report_success("Success!", HTML(paste0("Manifest submitted to: ", manifest_path))) - - # clean up old inputs/results - sapply(clean_tags, FUN = hide) - reset("inputFile-file") - DTableServer("tbl_preview", data.frame(NULL)) + nx_report_error(title = "Error submitting manifest", + message = tagList( + p("Refresh the app to try again or contact the DCC for help."), + p("For debugging: ", manifest_id()) + ) + ) } else { - dcWaiter("update", msg = HTML(paste0( - "Uh oh, looks like something went wrong!", - manifest_id, - " is not a valid Synapse ID. Try again?" - )), sleep = 0) + manifest_path <- tags$a(href = paste0("https://www.synapse.org/#!Synapse:", manifest_id()), manifest_id(), target = "_blank") + + # add log message + message(paste0("Manifest :", sQuote(manifest_id()), " has been successfully uploaded")) + + # if no error + if (startsWith(manifest_id(), "syn") == TRUE) { + dcWaiter("hide") + nx_report_success("Success!", HTML(paste0("Manifest submitted to: ", manifest_path))) + + # clean up old inputs/results + sapply(clean_tags, FUN = hide) + reset("inputFile-file") + DTableServer("tbl_preview", data.frame(NULL)) + } else { + dcWaiter("update", msg = HTML(paste0( + "Uh oh, looks like something went wrong!", + manifest_id, + " is not a valid Synapse ID. Try again?" + )), sleep = 0) + } } + manifest_id(NULL) }) }) From dbca53d3dee8428e7981b7df6bbf6c8ea0f3d48a Mon Sep 17 00:00:00 2001 From: afwillia Date: Wed, 27 Sep 2023 12:19:07 -0700 Subject: [PATCH 40/40] fix typo --- server.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.R b/server.R index b598ece2..39b8bebd 100644 --- a/server.R +++ b/server.R @@ -972,7 +972,7 @@ shinyServer(function(input, output, session) { req(!is.null(manifest_id())) - if (inherits(manifes_id(), "try-error")) { + if (inherits(manifest_id(), "try-error")) { dcWaiter("hide") nx_report_error(title = "Error submitting manifest", message = tagList(

dwx&c0B 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 J}%GZnp4+4n3wwk40m^#Rq_Ud3AaZ0MwKM!jd% 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