diff --git a/gradle/settings.gradle b/gradle/settings.gradle index 94053b08c09bf8d3f178fed9c7981fcc80f24364..66310dbb5c5af7153a686e4c6335ba304c6d2099 100644 --- a/gradle/settings.gradle +++ b/gradle/settings.gradle @@ -1,4 +1,4 @@ includeFlat 'commonbase', 'common', 'openbis_api', 'openbis-common', 'authentication', 'dbmigration', 'openbis', 'datastore_server', 'screening', 'rtd_phosphonetx', 'deep_sequencing_unit', 'rtd_yeastx', 'openbis_standard_technologies', 'installation', 'image_readers', 'ui-test', 'js-test', 'datamover', - 'plasmid', 'rtd_cina', 'openbis_oai_pmh', 'big_data_link_server' + 'plasmid', 'rtd_cina', 'openbis_oai_pmh', 'big_data_link_server', 'openbis_ng_ui' diff --git a/installation/settings.gradle b/installation/settings.gradle index f4f7784b74b2f9f07a0cfe2bdcb319264b4e13e9..3f3662d1ec0119b599baabe637f170dfdfa0bab0 100644 --- a/installation/settings.gradle +++ b/installation/settings.gradle @@ -1,4 +1,4 @@ includeFlat 'commonbase', 'common', 'openbis_api', 'openbis-common', 'authentication', 'dbmigration', 'openbis', 'datastore_server', 'screening', 'rtd_yeastx', 'rtd_phosphonetx', 'deep_sequencing_unit', 'plasmid', - 'openbis_standard_technologies', 'big_data_link_server' + 'openbis_standard_technologies', 'big_data_link_server', 'openbis_ng_ui' diff --git a/microscopy-migration-tool/src/ethz/ch/Migration.java b/microscopy-migration-tool/src/ethz/ch/Migration.java index ce6107ab308a5ce4c784234dc56dc4a41aa938b0..9004db568d266f19e0b6cbbc0883ef22435ac8d3 100644 --- a/microscopy-migration-tool/src/ethz/ch/Migration.java +++ b/microscopy-migration-tool/src/ethz/ch/Migration.java @@ -63,10 +63,9 @@ public class Migration private static final String OPENBIS_LOCAL_DEV = "http://localhost:8888"; private static final String OPENBIS_LOCAL_PROD = "https://localhost:8443"; private static final String OPENBIS_SCU = "https://openbis-scu.ethz.ch"; - - private static final String OPENBIS_URL = OPENBIS_LOCAL_DEV + "/openbis/openbis" + IApplicationServerApi.SERVICE_URL; - - private static final int TIMEOUT = 300000; + private static final String OPENBIS_SCU_TEST = "https://bs-lamp09.ethz.ch:8443/"; + + private static final int TIMEOUT = Integer.MAX_VALUE; private static final List<String> EXCLUDE_SPACES = Collections.EMPTY_LIST; @@ -80,7 +79,7 @@ public class Migration doTheWork(COMMIT_CHANGES_TO_OPENBIS, URL, user, pass, true, true, true); } else { System.out.println("Example: java -jar microscopy_migration_tool.jar https://openbis-domain.ethz.ch user password"); - //doTheWork(true, OPENBIS_URL, "pontia", "a", true, true, true); + doTheWork(false, OPENBIS_SCU_TEST + "/openbis/openbis" + IApplicationServerApi.SERVICE_URL, "migration", "migrationtool", true, true, true); } } @@ -89,6 +88,15 @@ public class Migration SslCertificateHelper.trustAnyCertificate(URL); IApplicationServerApi v3 = HttpInvokerUtils.createServiceStub(IApplicationServerApi.class, URL, TIMEOUT); String sessionToken = v3.login(userId, pass); + Map<String, String> serverInfo = v3.getServerInformation(sessionToken); + + if(serverInfo.containsKey("project-samples-enabled") && serverInfo.get("project-samples-enabled").equals("true")) { + System.out.println("Project samples enabled."); + } else { + System.out.println("Enable project samples before running the migration."); + return; + } + if(installELNTypes) { installELNTypes(sessionToken, v3, COMMIT_CHANGES_TO_OPENBIS); } diff --git a/openbis_ng_ui/.gitignore b/openbis_ng_ui/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..9abf471530fedae378d9938b8c80d01dd02f4cca --- /dev/null +++ b/openbis_ng_ui/.gitignore @@ -0,0 +1,10 @@ +.vagrant +.project +.settings +ubuntu-bionic-18.04-cloudimg-console.log +nodejs +node_modules +.gradle +.DS_Store +build +package-lock.json diff --git a/openbis_ng_ui/README.md b/openbis_ng_ui/README.md new file mode 100644 index 0000000000000000000000000000000000000000..7b29edbf02850f3f2d24e98847fad13bd306860f --- /dev/null +++ b/openbis_ng_ui/README.md @@ -0,0 +1,29 @@ +# openBIS - prototypes for new UI + +## Setting up the environment + +1. Install latest version of VirtualBox (https://www.virtualbox.org) + +2. Install latest version of Vagrant (https://www.vagrantup.com/downloads.html) + +3. vagrant plugin install vagrant-vbguest vagrant-notify-forwarder vagrant-disksize + +4. cd env/dev + +5. vagrant up + +6. Browse to https://localhost:8122/openbis and accept the self-signed certificate + +* openBIS is now running at https://localhost:8122/openbis (login: admin/password) +* React proto is now running at http://localhost:8124 + +## Additional info for Linux users + +On Ubuntu (and maybe on other Linux distributions, too), you might get errors like this when running "vagrant up": + + terminate called after throwing an instance of 'std::runtime_error' + what(): Could not add watch + +This situation will prevent the development environment from working properly. The reason for the error is https://github.com/mhallin/vagrant-notify-forwarder/issues/5. It can be fixed by increasing the maximum number of watches on the host system. On Ubuntu, this is done like this: + + echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p diff --git a/openbis_ng_ui/build.gradle b/openbis_ng_ui/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..6d7cee42e49c71ed9b3973522ba824f56bb16a14 --- /dev/null +++ b/openbis_ng_ui/build.gradle @@ -0,0 +1,21 @@ +buildscript { + repositories { + mavenCentral() + maven { + url "https://plugins.gradle.org/m2/" + } + } +} + +configure(allprojects) { + apply plugin:'base' + + task wrapper(type: Wrapper) { + gradleVersion = '4.10' + } + + repositories { + mavenCentral() + } +} + diff --git a/openbis_ng_ui/env/dev/Vagrantfile b/openbis_ng_ui/env/dev/Vagrantfile new file mode 100644 index 0000000000000000000000000000000000000000..ade770771848aba272c7361a2801fa00c687d3ba --- /dev/null +++ b/openbis_ng_ui/env/dev/Vagrantfile @@ -0,0 +1,27 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +Vagrant.configure("2") do |config| + config.vm.box = "ubuntu/bionic64" + config.vm.box_version = "20180531.0.0" + + config.vm.provider "virtualbox" do |v| + v.memory = 4096 + v.cpus = 1 + v.name = "openbis-ng-ui" + end + + # Needs to be unique among VMs + config.notify_forwarder.port = 22021 + + config.vm.synced_folder "../..", "/home/vagrant/openbis-ui-proto" + config.vm.synced_folder '.', '/vagrant', disabled: true + config.vm.synced_folder "files", "/files", :mount_options => ["ro"] + + config.vm.network "forwarded_port", guest: 8122, host: 8122, host_ip: "0.0.0.0" + config.vm.network "forwarded_port", guest: 8123, host: 8123, host_ip: "0.0.0.0" + config.vm.network "forwarded_port", guest: 8124, host: 8124, host_ip: "0.0.0.0" + + config.vm.provision :shell, path: "bootstrap.sh" + config.vm.provision :shell, path: "files/start-services.sh", run: "always", privileged: false +end diff --git a/openbis_ng_ui/env/dev/bootstrap.sh b/openbis_ng_ui/env/dev/bootstrap.sh new file mode 100644 index 0000000000000000000000000000000000000000..50efc646eaddff48373bc80fcfdf987ba41364fa --- /dev/null +++ b/openbis_ng_ui/env/dev/bootstrap.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +sudo locale-gen en_US.UTF-8 + +echo " +LC_ALL=en_US.UTF-8 +LANG=en_US.UTF-8 +" >> /etc/environment + +apt update +apt install -y unzip elinks openjdk-8-jdk inotify-tools git postgresql + +cp /files/pg_hba.conf /etc/postgresql/10/main/pg_hba.conf +service postgresql restart +sleep 10 # let the db engine start + +mkdir -p /node/node_modules +chown -R vagrant:vagrant /node + +sudo -E -u postgres -H -i /files/setup-postgres.sh +sudo -E -u vagrant -H -i /files/setup-vagrant.sh diff --git a/openbis_ng_ui/env/dev/files/console.properties b/openbis_ng_ui/env/dev/files/console.properties new file mode 100644 index 0000000000000000000000000000000000000000..faa327b56b38bcb8352771ef160acba48fddf193 --- /dev/null +++ b/openbis_ng_ui/env/dev/files/console.properties @@ -0,0 +1,62 @@ +# +# The path where openBIS will be installed. +# +# Example : +# INSTALL_PATH=/home/openbis/ +# +# will result in the following directory structure +# +# + /home/openbis +# + bin/ +# + servers/ +# + core-plugins/ +# + openBIS-server/ +# + datastore_server/ +# +INSTALL_PATH=/home/vagrant/openbis + +# +# The path where openBIS will keep the imported data (e.g. images, analysis files) +# and its incoming folders. +# +DSS_ROOT_DIR=/home/vagrant/dss_root + +# Possible configuration options +# 'local' - if the openBIS servers will only be accessed from this machine +# 'server' - if the installation is meant to be accessible for remote users +INSTALLATION_TYPE=local + +# Path to the file which should replace the current Java key store file +#KEY_STORE_FILE = <path to key store> + +# Password of the key store +KEY_STORE_PASSWORD = changeit + +# Password of the key +KEY_PASSWORD = changeit + +# Standard technology PROTEOMICS is disabled by default +#PROTEOMICS = true + +# Standard technology SCREENING is disabled by default +#SCREENING = true + +# Standard technology ILLUMINA-NGS (ETH BSSE Setup) is disabled by default +#ILLUMINA-NGS = true + +# Standard technology ELN-LIMS is disabled by default +#ELN-LIMS = true + +# Standard technology MICROSCOPY is disabled by default +#MICROSCOPY = true + +# Standard technology FLOW CYTOMETRY is disabled by default +#FLOW = true + +# Full ELN/LIMS master data is enabled by default. This setting is meaningful only if ELN-LIMS is enabled +ELN-LIMS-MASTER-DATA = false + +# +# Comma-separated list of databases to backup. If the list is empty or undefined all databases +# will be backauped. +#DATABASES_TO_BACKUP = \ No newline at end of file diff --git a/openbis_ng_ui/env/dev/files/pg_hba.conf b/openbis_ng_ui/env/dev/files/pg_hba.conf new file mode 100644 index 0000000000000000000000000000000000000000..5383ee2765aad917cb2c909e743f2a9cf8b335e3 --- /dev/null +++ b/openbis_ng_ui/env/dev/files/pg_hba.conf @@ -0,0 +1,99 @@ +# PostgreSQL Client Authentication Configuration File +# =================================================== +# +# Refer to the "Client Authentication" section in the PostgreSQL +# documentation for a complete description of this file. A short +# synopsis follows. +# +# This file controls: which hosts are allowed to connect, how clients +# are authenticated, which PostgreSQL user names they can use, which +# databases they can access. Records take one of these forms: +# +# local DATABASE USER METHOD [OPTIONS] +# host DATABASE USER ADDRESS METHOD [OPTIONS] +# hostssl DATABASE USER ADDRESS METHOD [OPTIONS] +# hostnossl DATABASE USER ADDRESS METHOD [OPTIONS] +# +# (The uppercase items must be replaced by actual values.) +# +# The first field is the connection type: "local" is a Unix-domain +# socket, "host" is either a plain or SSL-encrypted TCP/IP socket, +# "hostssl" is an SSL-encrypted TCP/IP socket, and "hostnossl" is a +# plain TCP/IP socket. +# +# DATABASE can be "all", "sameuser", "samerole", "replication", a +# database name, or a comma-separated list thereof. The "all" +# keyword does not match "replication". Access to replication +# must be enabled in a separate record (see example below). +# +# USER can be "all", a user name, a group name prefixed with "+", or a +# comma-separated list thereof. In both the DATABASE and USER fields +# you can also write a file name prefixed with "@" to include names +# from a separate file. +# +# ADDRESS specifies the set of hosts the record matches. It can be a +# host name, or it is made up of an IP address and a CIDR mask that is +# an integer (between 0 and 32 (IPv4) or 128 (IPv6) inclusive) that +# specifies the number of significant bits in the mask. A host name +# that starts with a dot (.) matches a suffix of the actual host name. +# Alternatively, you can write an IP address and netmask in separate +# columns to specify the set of hosts. Instead of a CIDR-address, you +# can write "samehost" to match any of the server's own IP addresses, +# or "samenet" to match any address in any subnet that the server is +# directly connected to. +# +# METHOD can be "trust", "reject", "md5", "password", "scram-sha-256", +# "gss", "sspi", "ident", "peer", "pam", "ldap", "radius" or "cert". +# Note that "password" sends passwords in clear text; "md5" or +# "scram-sha-256" are preferred since they send encrypted passwords. +# +# OPTIONS are a set of options for the authentication in the format +# NAME=VALUE. The available options depend on the different +# authentication methods -- refer to the "Client Authentication" +# section in the documentation for a list of which options are +# available for which authentication methods. +# +# Database and user names containing spaces, commas, quotes and other +# special characters must be quoted. Quoting one of the keywords +# "all", "sameuser", "samerole" or "replication" makes the name lose +# its special character, and just match a database or username with +# that name. +# +# This file is read on server startup and when the server receives a +# SIGHUP signal. If you edit the file on a running system, you have to +# SIGHUP the server for the changes to take effect, run "pg_ctl reload", +# or execute "SELECT pg_reload_conf()". +# +# Put your actual configuration here +# ---------------------------------- +# +# If you want to allow non-local connections, you need to add more +# "host" records. In that case you will also need to make PostgreSQL +# listen on a non-local interface via the listen_addresses +# configuration parameter, or via the -i or -h command line switches. + + + + +# DO NOT DISABLE! +# If you change this first entry you will need to make sure that the +# database superuser can access the database using some other method. +# Noninteractive access to all databases is required during automatic +# maintenance (custom daily cronjobs, replication, and similar tasks). +# +# Database administrative login by Unix domain socket +#local all postgres peer + +# TYPE DATABASE USER ADDRESS METHOD + +# "local" is for Unix domain socket connections only +local all all trust +# IPv4 local connections: +host all all 127.0.0.1/32 trust +# IPv6 local connections: +host all all ::1/128 md5 +# Allow replication connections from localhost, by a user with the +# replication privilege. +#local replication all peer +#host replication all 127.0.0.1/32 md5 +#host replication all ::1/128 md5 \ No newline at end of file diff --git a/openbis_ng_ui/env/dev/files/restart-services.sh b/openbis_ng_ui/env/dev/files/restart-services.sh new file mode 100755 index 0000000000000000000000000000000000000000..dec60f6b7fc5589c76db08fdb5ded1deef5be1fc --- /dev/null +++ b/openbis_ng_ui/env/dev/files/restart-services.sh @@ -0,0 +1,2 @@ +screen -ls | grep Detached | cut -d. -f1 | awk '{print $1}' | xargs kill +/files/start-services.sh diff --git a/openbis_ng_ui/env/dev/files/setup-postgres.sh b/openbis_ng_ui/env/dev/files/setup-postgres.sh new file mode 100755 index 0000000000000000000000000000000000000000..4cbd481a4eb5f70eda6808e53c2fc080c4c6c052 --- /dev/null +++ b/openbis_ng_ui/env/dev/files/setup-postgres.sh @@ -0,0 +1 @@ +createuser vagrant diff --git a/openbis_ng_ui/env/dev/files/setup-vagrant.sh b/openbis_ng_ui/env/dev/files/setup-vagrant.sh new file mode 100755 index 0000000000000000000000000000000000000000..cf30c8e5513a2ef14fc62f9bdbef1b60b23f235c --- /dev/null +++ b/openbis_ng_ui/env/dev/files/setup-vagrant.sh @@ -0,0 +1,17 @@ +build="http://stage-jenkins.ethz.ch:8090/job/installation-18.06/lastSuccessfulBuild" +path=$(curl -s "$build/api/xml?xpath=//relativePath"|sed -e "s/<relativePath>//"|sed -e "s/<\/relativePath>//") +wget -q $build/artifact/$path +archive=$(basename $path) +tar xvfz $archive +directory=$(echo "$archive" | cut -f 1 -d '.') +cp /files/console.properties $directory +export ADMIN_PASSWORD='password' +$directory/run-console.sh + +sed -i "/jetty.ssl.port=/ s/=.*/=8122/" /home/vagrant/openbis/servers/openBIS-server/jetty/start.d/ssl.ini +sed -i "/host-address =/ s/=.*/= https:\/\/localhost/" /home/vagrant/openbis/servers/datastore_server/etc/service.properties +sed -i "/port =/ s/=.*/= 8123/" /home/vagrant/openbis/servers/datastore_server/etc/service.properties +sed -i "/server-url =/ s/=.*/= \${host-address}:8122/" /home/vagrant/openbis/servers/datastore_server/etc/service.properties + +cd /home/vagrant/openbis-ui-proto/react +ln -s /node/node_modules diff --git a/openbis_ng_ui/env/dev/files/start-services.sh b/openbis_ng_ui/env/dev/files/start-services.sh new file mode 100755 index 0000000000000000000000000000000000000000..a57f5b7841bf3a3f433523adec4d179f713e9726 --- /dev/null +++ b/openbis_ng_ui/env/dev/files/start-services.sh @@ -0,0 +1,17 @@ +sudo /usr/sbin/VBoxService --timesync-set-start + +/home/vagrant/openbis/bin/allup.sh + +#cd openbis-ui-proto/react +#../gradlew npmInstall +#cd + +#export PATH=$PATH:/home/vagrant/openbis-ui-proto/react/nodejs/node-v10.1.0-linux-x64/bin + +#screen -S dev -t webpack -Adm bash -c "cd openbis-ui-proto/react; npm run dev; bash" + +#echo "Waiting Webpack to launch on 8124..." + +#while ! nc -z localhost 8124; do +# sleep 3 +#done diff --git a/openbis_ng_ui/gradle/wrapper/gradle-wrapper.jar b/openbis_ng_ui/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..91ca28c8b802289c3a438766657a5e98f20eff03 Binary files /dev/null and b/openbis_ng_ui/gradle/wrapper/gradle-wrapper.jar differ diff --git a/openbis_ng_ui/gradle/wrapper/gradle-wrapper.properties b/openbis_ng_ui/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000000000000000000000000000000000..115e6ac0aaba10c8e73340fdd7744897234b59ac --- /dev/null +++ b/openbis_ng_ui/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/openbis_ng_ui/gradlew b/openbis_ng_ui/gradlew new file mode 100755 index 0000000000000000000000000000000000000000..cccdd3d517fc5249beaefa600691cf150f2fa3e6 --- /dev/null +++ b/openbis_ng_ui/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/openbis_ng_ui/gradlew.bat b/openbis_ng_ui/gradlew.bat new file mode 100644 index 0000000000000000000000000000000000000000..f9553162f122c71b34635112e717c3e733b5b212 --- /dev/null +++ b/openbis_ng_ui/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/openbis_ng_ui/react/.babelrc b/openbis_ng_ui/react/.babelrc new file mode 100644 index 0000000000000000000000000000000000000000..99c679001f8169cc931d5c5d7c0d57717d3f0d19 --- /dev/null +++ b/openbis_ng_ui/react/.babelrc @@ -0,0 +1,4 @@ +{ + "presets":["env", "react"], + "plugins": ["transform-class-properties"] +} diff --git a/openbis_ng_ui/react/.eslintrc.js b/openbis_ng_ui/react/.eslintrc.js new file mode 100644 index 0000000000000000000000000000000000000000..c7098787dadc161c6a5d87172499eba27b514eb9 --- /dev/null +++ b/openbis_ng_ui/react/.eslintrc.js @@ -0,0 +1,44 @@ +module.exports = { + root: true, + parser: "babel-eslint", + parserOptions: { + ecmaVersion: 2017, + sourceType: 'module', + ecmaFeatures: { + jsx: true + } + }, + env: { + browser: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:react/recommended' + ], + plugins: [ + 'react' + ], + settings: { + react: { + createClass: "createReactClass", + pragma: "React", + version: "16.4.2" + }, + propWrapperFunctions: [ "forbidExtraProps" ] + }, + rules: { + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + + "indent": ["error", 2], + "linebreak-style": ["error", "unix"], + "quotes": ["error", "single"], + "semi": ["error", "never"], + "eqeqeq": ["error", "always"], + + "react/prop-types": "off", + + // override default options for rules from base configurations + "no-cond-assign": ["error", "always"], + } +} diff --git a/openbis_ng_ui/react/build.gradle b/openbis_ng_ui/react/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..fc7e9c044f67c1e20332096e5bf858b17653dbc3 --- /dev/null +++ b/openbis_ng_ui/react/build.gradle @@ -0,0 +1,14 @@ +plugins { + id "com.moowork.node" version "1.2.0" +} + +node { + download = true + // npmVersion not defined => use the one bundled with node + version = '10.1.0' + workDir = file("${projectDir}/nodejs") + nodeModulesDir = file("${projectDir}") +} + +npm_run_build.dependsOn npm_run_test +build.dependsOn npm_run_build diff --git a/openbis_ng_ui/react/index.html b/openbis_ng_ui/react/index.html new file mode 100644 index 0000000000000000000000000000000000000000..093b537e039a0dc2c9bd426df85ec1cc8c0789e1 --- /dev/null +++ b/openbis_ng_ui/react/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en" style="height: 100%;"> + <head> + <title>openBIS / React</title> + <meta charset="utf-8"> + <script src="/openbis/resources/api/v3/config.js"></script> + <script src="/openbis/resources/api/v3/require.js"></script> + </head> + <body style="min-height:100%; margin: 0px 0px 0px 0px;"> + <div id="app"></div> + </body> +</html> diff --git a/openbis_ng_ui/react/package.json b/openbis_ng_ui/react/package.json new file mode 100644 index 0000000000000000000000000000000000000000..2be6afa14663370ce00e15b968c7870b9cf81966 --- /dev/null +++ b/openbis_ng_ui/react/package.json @@ -0,0 +1,58 @@ +{ + "name": "openbis-ng-ui", + "version": "1.0.0", + "description": "React-based UI proto for openBIS", + "author": "Antti Luomi", + "license": "Apache-2.0", + "dependencies": { + "@material-ui/core": "3.0.2", + "@material-ui/icons": "3.0.1", + "install": "^0.12.2", + "npm": "^6.4.1", + "prop-types": "^15.6.2", + "react": "16.4.2", + "react-dnd": "5.0.0", + "react-dnd-html5-backend": "5.0.1", + "react-dom": "16.4.2", + "react-redux": "5.0.7", + "redux": "4.0.0", + "redux-saga": "0.16.0", + "typeface-roboto": "0.0.54" + }, + "devDependencies": { + "babel-core": "6.26.0", + "babel-loader": "7.1.4", + "babel-preset-env": "1.7.0", + "babel-preset-react": "6.24.1", + "html-webpack-plugin": "3.2.0", + "source-map-loader": "0.2.3", + "webpack": "4.6.0", + "webpack-dev-server": "3.1.3", + "webpack-cli": "2.0.14", + "style-loader": "0.21.0", + "css-loader": "1.0.0", + "url-loader": "1.1.1", + "raw-loader": "0.5.1", + "file-loader": "1.1.11", + "jest": "23.1.0", + "eslint": "4.19.1", + "react-loader": "2.4.5", + "react-hot-loader": "4.3.6", + "eslint-config-standard": "11.0.0", + "eslint-plugin-standard": "3.1.0", + "eslint-plugin-react": "7.11.1", + "eslint-plugin-import": "2.13.0", + "eslint-plugin-node": "6.0.1", + "eslint-plugin-promise": "3.8.0", + "eslint-plugin-jest": "21.17.0", + "babel-eslint": "8.2.5" + }, + "scripts": { + "dev": "webpack-dev-server --hot --inline --watch --host 0.0.0.0 --port 9124 --config webpack.config.dev.js", + "build": "webpack-cli --config webpack.config.js", + "unit": "jest", + "lint": "eslint --ext .js,.jsx src test", + "lint:fix": "eslint --ext .js,.jsx src test --fix", + "test": "npm run lint && npm run unit" + } +} diff --git a/openbis_ng_ui/react/src/components/App.jsx b/openbis_ng_ui/react/src/components/App.jsx new file mode 100644 index 0000000000000000000000000000000000000000..db8d156935d58fdfbc8648a6aabb739efeef86b6 --- /dev/null +++ b/openbis_ng_ui/react/src/components/App.jsx @@ -0,0 +1,116 @@ +import React from 'react' +import Hidden from '@material-ui/core/Hidden' +import { withStyles } from '@material-ui/core/styles' +import { connect } from 'react-redux' +import CircularProgress from '@material-ui/core/CircularProgress' +import HTML5Backend from 'react-dnd-html5-backend' +import { DragDropContext } from 'react-dnd' +import flow from 'lodash/flow' + +import Browser from './Browser.jsx' +import BrowserFilter from './BrowserFilter.jsx' +import BrowserButtons from './BrowserButtons.jsx' +import ModeBar from './ModeBar.jsx' +import TabPanel from './TabPanel.jsx' +import TopBar from './TopBar.jsx' + +const drawerWidth = 400 + +/* eslint-disable-next-line no-unused-vars */ +const styles = theme => ({ + right: { + width: `calc(100% - ${drawerWidth + 4 + 4 + 1}px)`, + paddingLeft: 4, + marginLeft: drawerWidth + 5, + }, + + left: { + float: 'left', + width: drawerWidth, + paddingRight: 4, + borderRight: '1px dotted', + borderColor: '#e3e5ea', + height: '100%', + position: 'absolute', + }, + + browser: { + height: 'calc(100% - 160px)', + overflow: 'auto' + }, + + topMargin: { + marginTop: 8 + }, + + loader: { + position: 'absolute', + paddingTop: '15%', + width: '100%', + height: '100%', + zIndex: 1000, + backgroundColor: '#000000', + opacity: 0.5, + textAlign: 'center', + }, + +}) + +function mapStateToProps(state) { + return { + loading: state.loading, + } +} + +class App extends React.Component { + + render() { + const classes = this.props.classes + + return ( + <div> + { + this.props.loading && + <div className={classes.loader}> + <CircularProgress className={classes.progress} /> + </div> + } + <Hidden mdUp> + <TopBar/> + <div className={classes.topMargin}> + <ModeBar/> + </div> + <BrowserFilter/> + <Browser/> + <BrowserButtons /> + <div className={classes.topMargin}> + <TabPanel /> + </div> + </Hidden> + + <Hidden smDown> + <div className={classes.left}> + <ModeBar/> + <BrowserFilter/> + <div className={classes.browser}> + <Browser /> + </div> + <BrowserButtons /> + </div> + <div className={classes.right}> + <TopBar /> + <div className={classes.topMargin}> + <TabPanel /> + </div> + </div> + </Hidden> + </div> + ) + } +} + +export default flow( + connect(mapStateToProps), + withStyles(styles), + DragDropContext(HTML5Backend) +)(App) diff --git a/openbis_ng_ui/react/src/components/Browser.jsx b/openbis_ng_ui/react/src/components/Browser.jsx new file mode 100644 index 0000000000000000000000000000000000000000..86f0406b1396a689f03a8cfa0d7a1ef4fe1521b1 --- /dev/null +++ b/openbis_ng_ui/react/src/components/Browser.jsx @@ -0,0 +1,40 @@ +import React from 'react' +import { connect } from 'react-redux' +import ListItemText from '@material-ui/core/ListItemText' + +import BrowserList from './BrowserList.jsx' +import actions from '../reducer/actions.js' + + +function mapDispatchToProps(dispatch) { + return { + selectEntity: permId => dispatch(actions.selectEntity(permId)), + } +} + + +function mapStateToProps(state) { + // TODO stack tree nodes here when the final tree model is done + return { + databaseTreeNodes: state.databaseTreeNodes, + selectedEntity: state.openEntities.selectedEntity, + } +} + + +class Browser extends React.Component { + + render() { + return ( + <BrowserList + nodes={ this.props.databaseTreeNodes } + level={ 0 } + selectedNodeId={ this.props.selectedEntity } + onSelect={ node => { if (node.type === 'as.dto.space.Space') this.props.selectEntity(node.id) } } + renderNode={ node => { return (<ListItemText secondary={node.id} />)} } + /> + ) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Browser) diff --git a/openbis_ng_ui/react/src/components/BrowserButtons.jsx b/openbis_ng_ui/react/src/components/BrowserButtons.jsx new file mode 100644 index 0000000000000000000000000000000000000000..1996ecab692d050f39dbaec2eb19d4007ed61ebc --- /dev/null +++ b/openbis_ng_ui/react/src/components/BrowserButtons.jsx @@ -0,0 +1,38 @@ +import React from 'react' +import AppBar from '@material-ui/core/AppBar' +import Toolbar from '@material-ui/core/Toolbar' +import Grid from '@material-ui/core/Grid' +import Button from '@material-ui/core/Button' +import RemoveIcon from '@material-ui/icons/Remove' +import AddIcon from '@material-ui/icons/Add' + +class BrowserButtons extends React.Component { + + render() { + return ( + <AppBar position='static'> + <Toolbar> + <Grid container alignItems='center'> + <Grid item xs={2}> + <Button + variant="contained" + color="primary"> + <RemoveIcon /> + </Button> + </Grid> + <Grid item xs={8} /> + <Grid item xs={2}> + <Button + variant="contained" + color="primary"> + <AddIcon /> + </Button> + </Grid> + </Grid> + </Toolbar> + </AppBar> + ) + } +} + +export default BrowserButtons diff --git a/openbis_ng_ui/react/src/components/BrowserFilter.jsx b/openbis_ng_ui/react/src/components/BrowserFilter.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9ca605b39d7af2357c02a96a2a1de4b0ed24eb2e --- /dev/null +++ b/openbis_ng_ui/react/src/components/BrowserFilter.jsx @@ -0,0 +1,34 @@ +import React from 'react' +import { withStyles } from '@material-ui/core/styles' +import InputAdornment from '@material-ui/core/InputAdornment' +import TextField from '@material-ui/core/TextField' +import FilterIcon from '@material-ui/icons/FilterList' + +/*eslint-disable-next-line no-unused-vars*/ +const styles = theme => ({ + browserFilter: { + width: '100%' + } +}) + +class BrowserFilter extends React.Component { + + render() { + const classes = this.props.classes + + return ( + <TextField + className = { classes.browserFilter } + placeholder = "Filter" + InputProps={{ + startAdornment: ( + <InputAdornment position="start"> + <FilterIcon /> + </InputAdornment> + ), + }}/> + ) + } +} + +export default withStyles(styles)(BrowserFilter) diff --git a/openbis_ng_ui/react/src/components/BrowserList.jsx b/openbis_ng_ui/react/src/components/BrowserList.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8b17fa4a4383bba35b16f9349cd23d1ea5eec18c --- /dev/null +++ b/openbis_ng_ui/react/src/components/BrowserList.jsx @@ -0,0 +1,88 @@ +import React from 'react' +import { connect } from 'react-redux' +import { withStyles } from '@material-ui/core/styles' +import List from '@material-ui/core/List' +import ListItem from '@material-ui/core/ListItem' +import ListItemIcon from '@material-ui/core/ListItemIcon' +import ChevronRightIcon from '@material-ui/icons/ChevronRight' +import ExpandMoreIcon from '@material-ui/icons/ExpandMore' +import HourglassEmptyIcon from '@material-ui/icons/HourglassEmpty' +import PropTypes from 'prop-types' + + +import actions from '../reducer/actions.js' + + +/*eslint-disable-next-line no-unused-vars*/ +const styles = theme => ({ + noPadding: { + paddingTop: '0', + paddingBottom: '0', + }, +}) + + +function mapDispatchToProps(dispatch) { + return { + expandNode: (e, node) => { + e.stopPropagation() + dispatch(actions.expandNode(node)) + }, + collapseNode: (e, node) => { + e.stopPropagation() + dispatch(actions.collapseNode(node)) + }, + } +} + +class BrowserList extends React.Component { + + icon(node) { + if (node.expanded) { + return (<ExpandMoreIcon onClick={ (e) => this.props.collapseNode(e, node) }/>) + } else { + return (<ChevronRightIcon onClick={ (e) => this.props.expandNode(e, node) }/>) + } + } + + render() { + const classes = this.props.classes + + return ( + <List className={classes.noPadding}> + { + this.props.nodes.map(node => + <div key={node.id}> + <ListItem + button + selected={this.props.selectedNodeId === node.id} + key={node.id} + onClick = {() => this.props.onSelect(node)} + style={{ paddingLeft: '' + this.props.level * 20 + 'px' }}> + <ListItemIcon> + {this.icon(node)} + </ListItemIcon> + { node.loading && + <ListItemIcon> + <HourglassEmptyIcon/> + </ListItemIcon> + } + { this.props.renderNode(node) } + </ListItem> + { + node.expanded && + <BrowserList {...this.props} nodes={node.children} level={this.props.level+1}/> + } + </div> + ) + } + </List> + ) + } +} + +BrowserList.propTypes = { + renderNode: PropTypes.func.isRequired, +} + +export default connect(null, mapDispatchToProps)(withStyles(styles)(BrowserList)) diff --git a/openbis_ng_ui/react/src/components/ModeBar.jsx b/openbis_ng_ui/react/src/components/ModeBar.jsx new file mode 100644 index 0000000000000000000000000000000000000000..1a198dd056585a64dbf56208047c8d302ab24a6c --- /dev/null +++ b/openbis_ng_ui/react/src/components/ModeBar.jsx @@ -0,0 +1,39 @@ +import React from 'react' +import AppBar from '@material-ui/core/AppBar' +import Toolbar from '@material-ui/core/Toolbar' +import Tabs from '@material-ui/core/Tabs' +import Tab from '@material-ui/core/Tab' +import { withStyles } from '@material-ui/core/styles' + +/*eslint-disable-next-line no-unused-vars*/ +const styles = theme => ({ + browserTabs: { + width: '100%' + }, + browserTab: { + minWidth: '50px' + } +}) + +class ModeBar extends React.Component { + + render() { + const classes = this.props.classes + + return ( + <AppBar position="static"> + <Toolbar> + <Tabs value={0} fullWidth className = { classes.browserTabs }> + <Tab className = { classes.browserTab } label="Database" /> + <Tab className = { classes.browserTab } label="Types" /> + <Tab className = { classes.browserTab } label="Users" /> + <Tab className = { classes.browserTab } label="Favourites" /> + <Tab className = { classes.browserTab } label="Tools" /> + </Tabs> + </Toolbar> + </AppBar> + ) + } +} + +export default withStyles(styles)(ModeBar) diff --git a/openbis_ng_ui/react/src/components/TabContainer.jsx b/openbis_ng_ui/react/src/components/TabContainer.jsx new file mode 100644 index 0000000000000000000000000000000000000000..30a983e16b4352788f3fc18f9dd85ec044647cad --- /dev/null +++ b/openbis_ng_ui/react/src/components/TabContainer.jsx @@ -0,0 +1,96 @@ +import React from 'react' +import { withStyles } from '@material-ui/core/styles' +import AppBar from '@material-ui/core/AppBar' +import CloseIcon from '@material-ui/icons/Close' +import IconButton from '@material-ui/core/IconButton' +import Toolbar from '@material-ui/core/Toolbar' +import Tabs from '@material-ui/core/Tabs' +import Tab from '@material-ui/core/Tab' +import PropTypes from 'prop-types' + +import TabContent from './TabContent.jsx' + + +/* eslint-disable-next-line no-unused-vars */ +const styles = theme => ({ + entityTabs: { + width: '100%' + }, + inlineElement: { + display: 'inline-block' + }, + hidden: { + display: 'none', + } +}) + +class TabContainer extends React.Component { + + render() { + const classes = this.props.classes + const selectedKey = this.props.selectedKey + const selectedTabIndex = this.props.children.findIndex(child => child.key === selectedKey) + return ( + <div> + <div> + <AppBar position="static"> + <Toolbar> + <Tabs + value={selectedTabIndex} + scrollable={ React.Children.count(this.props.children) > 0 } + scrollButtons="auto" + className = { classes.entityTabs }> + { + React.Children.map(this.props.children, child => + <Tab + component="div" + label={ + <div> + <div className = { classes.inlineElement }>{child.props.name}</div> + { child.props.dirty && + <div className = { classes.inlineElement }>*</div> + } + <div className = { classes.inlineElement }> + <IconButton onClick={ (e) => child.props.onClose(e) } > + <CloseIcon/> + </IconButton> + </div> + </div>} + onClick = {child.props.onSelect}/> + ) + } + </Tabs> + </Toolbar> + </AppBar> + </div> + <div> + { + React.Children.map(this.props.children, child => { + return ( + <div className={ selectedKey === child.key ? {} : classes.hidden }> + {child} + </div> + ) + }) + } + </div> + </div> + ) + } +} + +TabContainer.propTypes = { + selectedKey: PropTypes.string.isRequired, + children: function (props, propName, componentName) { + const prop = props[propName] + let error = null + React.Children.forEach(prop, function (child) { + if (child.type !== TabContent) { + error = new Error('`' + componentName + '` children should be of type `TabContent`.') + } + }) + return error + } +} + +export default withStyles(styles)(TabContainer) diff --git a/openbis_ng_ui/react/src/components/TabContent.jsx b/openbis_ng_ui/react/src/components/TabContent.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d1a925010af5b7ee525fe74ddaa5e52a59bc2b64 --- /dev/null +++ b/openbis_ng_ui/react/src/components/TabContent.jsx @@ -0,0 +1,18 @@ +import React from 'react' +import PropTypes from 'prop-types' + + +class TabContent extends React.Component { + render() { + return <div>{this.props.children}</div> + } +} + +TabContent.propTypes ={ + name: PropTypes.string.isRequired, + dirty: PropTypes.bool.isRequired, + onSelect: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, +} + +export default TabContent diff --git a/openbis_ng_ui/react/src/components/TabPanel.jsx b/openbis_ng_ui/react/src/components/TabPanel.jsx new file mode 100644 index 0000000000000000000000000000000000000000..970e381d19fe46871f6005ce9610670bc04c4ade --- /dev/null +++ b/openbis_ng_ui/react/src/components/TabPanel.jsx @@ -0,0 +1,69 @@ +import React from 'react' +import { connect } from 'react-redux' +import EntityDetails from './database/EntityDetails.jsx' +import TabContainer from './TabContainer.jsx' +import TabContent from './TabContent.jsx' +import actions from '../reducer/actions.js' + + +/** + * This component at the moment only makes tabs for entities. + * In the future, it should be extended for other kinds of tabs + * (settings forms etc.). + */ + +function mapDispatchToProps(dispatch) { + return { + selectEntity: (entityPermId) => dispatch(actions.selectEntity(entityPermId)), + closeEntity: (e, entityPermId) => { + e.stopPropagation() + dispatch(actions.closeEntity(entityPermId)) + } + } +} + + +function mapStateToProps(state) { + const selectedEntity = state.openEntities.selectedEntity + const spaces = state.database.spaces + return { + openEntities: state.openEntities.entities + .filter(permId => permId in spaces) + .map(permId => spaces[permId]), + selectedEntity: selectedEntity, + dirtyEntities: state.dirtyEntities, + } +} + + +class TabPanel extends React.Component { + + render() { + if (this.props.openEntities.length === 0) { + return null + } + return ( + <TabContainer selectedKey={this.props.selectedEntity}> + { + this.props.openEntities.map(entity => { + return( + <TabContent + key={entity.permId.permId} + name={entity.code} + dirty={this.props.dirtyEntities.indexOf(entity.permId.permId) > -1} + onSelect={() => {this.props.selectEntity(entity.permId.permId)}} + onClose={(e) => {this.props.closeEntity(e, entity.permId.permId)}} + > + <EntityDetails + entity={entity} + /> + </TabContent> + ) + }) + } + </TabContainer> + ) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(TabPanel) diff --git a/openbis_ng_ui/react/src/components/TopBar.jsx b/openbis_ng_ui/react/src/components/TopBar.jsx new file mode 100644 index 0000000000000000000000000000000000000000..bae126a64c5994f36cc35cefd1424fd4758b75fe --- /dev/null +++ b/openbis_ng_ui/react/src/components/TopBar.jsx @@ -0,0 +1,78 @@ +import React from 'react' +import AppBar from '@material-ui/core/AppBar' +import Toolbar from '@material-ui/core/Toolbar' +import InputAdornment from '@material-ui/core/InputAdornment' +import TextField from '@material-ui/core/TextField' +import SearchIcon from '@material-ui/icons/Search' +import { withStyles } from '@material-ui/core/styles' +import Grid from '@material-ui/core/Grid' +import Button from '@material-ui/core/Button' +import Hidden from '@material-ui/core/Hidden' +import LogoutIcon from '@material-ui/icons/PowerSettingsNew' + + +const styles = theme => ({ + searchField: { + backgroundColor: 'white', + width: '100%' + }, + leftIcon: { + marginRight: theme.spacing.unit, + }, + grid: { + minWidth: 0 + } +}) + +class TopBar extends React.Component { + + render() { + const classes = this.props.classes + + return ( + <AppBar position='static'> + <Toolbar> + <Grid container alignItems='center' spacing={8}> + + <Grid item xs className={ classes.grid }> + <TextField + className = { classes.searchField } + InputProps={{ + startAdornment: ( + <InputAdornment position="start"> + <SearchIcon /> + </InputAdornment> + ), + }}/> + </Grid> + + <Grid item> + <Button + variant="contained" + color="primary"> + <SearchIcon className={ classes.leftIcon } /> + <Hidden mdUp> + Adv. + </Hidden> + <Hidden smDown> + Adv. search + </Hidden> + </Button> + </Grid> + + <Grid item> + <Button + variant="contained" + color="primary"> + <LogoutIcon /> + </Button> + </Grid> + + </Grid> + </Toolbar> + </AppBar> + ) + } +} + +export default withStyles(styles)(TopBar) diff --git a/openbis_ng_ui/react/src/components/database/EntityDetails.jsx b/openbis_ng_ui/react/src/components/database/EntityDetails.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0a7e97e641911f552642b3ae05d281c829fe6320 --- /dev/null +++ b/openbis_ng_ui/react/src/components/database/EntityDetails.jsx @@ -0,0 +1,148 @@ +import React from 'react' +import { connect } from 'react-redux' +import { withStyles } from '@material-ui/core/styles' +import Button from '@material-ui/core/Button' +import Card from '@material-ui/core/Card' +import CardHeader from '@material-ui/core/CardHeader' +import CardContent from '@material-ui/core/CardContent' +import CardActions from '@material-ui/core/CardActions' +import Avatar from '@material-ui/core/Avatar' +import IconButton from '@material-ui/core/IconButton' +import FavoriteIcon from '@material-ui/icons/Favorite' +import ShareIcon from '@material-ui/icons/Share' +import ExpandMoreIcon from '@material-ui/icons/ExpandMore' +import TextField from '@material-ui/core/TextField' +import PropTypes from 'prop-types' +import _ from 'lodash' + +import OpenBISTable from './OpenBISTable.jsx' +import actions from '../../reducer/actions.js' + + +/* eslint-disable-next-line no-unused-vars */ +const styles = theme => ({ + textField: { + width: '100%' + } +}) + +function mapStateToProps(state) { + return { + dirtyEntities: state.dirtyEntities, + } +} + +function mapDispatchToProps(dispatch) { + return { + setDirty: (entity, dirty) => dispatch(actions.setDirty(entity.permId.permId, dirty)), + saveEntity: (entity) => dispatch(actions.saveEntity(entity)), + } +} + +class EntityDetails extends React.Component { + + constructor(props) { + super(props) + this.state = { + description: this.props.entity ? this.props.entity.description : '', + dirty: false + } + } + + matches(value1, value2) { + if (value1 === null || value1.length === 0) { + return value2 === null || value2.length === 0 + } + return value1 === value2 + } + + handleChange(name, e) { + const currentlyDirty = !this.matches(e.target.value, this.props.entity.description) + const stateDirty = this.props.dirtyEntities.indexOf(this.props.entity.permId.permId) > -1 + if (currentlyDirty !== stateDirty) { + this.props.setDirty(this.props.entity, currentlyDirty) + } + this.setState({ + description: e.target.value, + dirty: currentlyDirty + }) + } + + handleSave(entity) { + const entityCopy = _.cloneDeep(entity) + entityCopy.description = this.state.description + this.props.saveEntity(entityCopy) + } + + render() { + const { classes } = this.props + if (this.props.entity === null) { + return (<div />) + } + const entity = this.props.entity + + const options = { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + } + const created = new Date(entity.registrationDate).toLocaleDateString('en-US', options) + + return( + <Card className={classes.card}> + <CardHeader + avatar={ + <Avatar aria-label="Recipe" className={classes.avatar}> + S + </Avatar> + } + action={ + <Button + color="primary" + disabled={!this.state.dirty} + variant="contained" + onClick={ () => this.handleSave(this.props.entity) }> + Save + </Button> + } + title={ 'Space '+entity.code } + subheader={ 'Created on ' + created } + /> + <CardContent> + <TextField + label='Description' + className={classes.textField} + value={this.state.description ? this.state.description : ''} + onChange={ (e) => { + this.handleChange('description', e) + }} + margin='normal' + /> + <OpenBISTable /> + + </CardContent> + <CardActions disableActionSpacing> + <IconButton aria-label="Add to favorites"> + <FavoriteIcon /> + </IconButton> + <IconButton aria-label="Share"> + <ShareIcon /> + </IconButton> + <IconButton> + <ExpandMoreIcon /> + </IconButton> + </CardActions> + </Card> + ) + } +} + +EntityDetails.propTypes = { + entity: PropTypes.any.isRequired, +} + +export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(EntityDetails)) diff --git a/openbis_ng_ui/react/src/components/database/OpenBISTable.jsx b/openbis_ng_ui/react/src/components/database/OpenBISTable.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7dd715bfc412dac940643756706a41c3952feee5 --- /dev/null +++ b/openbis_ng_ui/react/src/components/database/OpenBISTable.jsx @@ -0,0 +1,119 @@ +import React from 'react' +import { connect } from 'react-redux' +import { withStyles } from '@material-ui/core/styles' +import Table from '@material-ui/core/Table' +import TableBody from '@material-ui/core/TableBody' +import TableCell from '@material-ui/core/TableCell' +import TableHead from '@material-ui/core/TableHead' +import TableRow from '@material-ui/core/TableRow' +import Paper from '@material-ui/core/Paper' +import TableFooter from '@material-ui/core/TableFooter' +import TablePagination from '@material-ui/core/TablePagination' +import TableSortLabel from '@material-ui/core/TableSortLabel' +import InputAdornment from '@material-ui/core/InputAdornment' +import TextField from '@material-ui/core/TextField' +import FilterIcon from '@material-ui/icons/FilterList' +import SettingsIcon from '@material-ui/icons/Settings' + +import OpenBISTableRow from './OpenBISTableRow.jsx' +import actions from '../../reducer/actions.js' + +/* eslint-disable-next-line no-unused-vars */ +const styles = theme => ({ + table: { + marginTop: 30, + overflowX: 'auto' + }, + headerCell: { + backgroundColor: theme.palette.grey[300], + } +}) + +function mapStateToProps(state) { + const table = state.database.table + return { + spaces: state.database.spaces, + columns: table.columns, + data: table.data, + page: table.page, + sortColumn: table.sortColumn, + sortDirection: table.sortDirection, + filter: table.filter + } +} + +function mapDispatchToProps(dispatch) { + return { + selectEntity: e => dispatch(actions.selectEntity(e)), + changePage: page => dispatch(actions.changePage(page)), + sortBy: column => dispatch(actions.sortBy(column)), + setFilter: filter => dispatch(actions.setFilter(filter)) + } +} + +class OpenBISTable extends React.Component { + + render() { + const { data, columns, classes, page } = this.props + return ( + <Paper className={classes.table}> + <Table padding='checkbox'> + <TableHead> + <TableRow> + <TableCell className={classes.headerCell} variant='head'/> + { Object.entries(columns).map(([column, type]) => + <TableCell key={column} className={classes.headerCell} numeric={type === 'int'} variant='head'> + <TableSortLabel + className={classes.headerCell} + active={ this.props.sortColumn === column } + direction={ this.props.sortDirection } + onClick={() => this.props.sortBy(column)} > + { column } + </TableSortLabel> + </TableCell> + )} + </TableRow> + </TableHead> + <TableBody> + { + data.slice(page * 10, page * 10 + 10).map(row => + <OpenBISTableRow key = { row.Id } row = { row } columns = { columns } /> + ) + } + </TableBody> + <TableFooter className={classes.headerCell}> + <TableRow> + <TableCell> + <div + style = {{ cursor: 'pointer' }}> + <SettingsIcon/> + </div> + </TableCell> + <TableCell colSpan={2}> + <TextField + onChange = { e => this.props.setFilter(e.target.value) } + placeholder = "filter rows by value" + InputProps={{ + startAdornment: ( + <InputAdornment position="start" variant="outlined"> + <FilterIcon /> + </InputAdornment> + ), + }}/> + </TableCell> + <TablePagination + count={data.length} + rowsPerPage={10} + rowsPerPageOptions={[10]} + page={page} + onChangePage={(_, page) => this.props.changePage(page)} + /> + </TableRow> + </TableFooter> + </Table> + </Paper> + ) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(OpenBISTable)) diff --git a/openbis_ng_ui/react/src/components/database/OpenBISTableRow.jsx b/openbis_ng_ui/react/src/components/database/OpenBISTableRow.jsx new file mode 100644 index 0000000000000000000000000000000000000000..57cc32ac4ec7d7f3535907704cd6a7e763d997b2 --- /dev/null +++ b/openbis_ng_ui/react/src/components/database/OpenBISTableRow.jsx @@ -0,0 +1,93 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import { connect } from 'react-redux' +import TableCell from '@material-ui/core/TableCell' +import TableRow from '@material-ui/core/TableRow' +import { DragSource } from 'react-dnd' +import { DropTarget } from 'react-dnd' +import DragHandle from '@material-ui/icons/DragHandle' + +import actions from '../../reducer/actions.js' + + +function targetCollect(connect, monitor) { + return { + connectDropTarget: connect.dropTarget() + } +} + +function sourceCollect(connect, monitor) { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging(), + connectDragPreview: connect.dragPreview() + } +} + +const source = { + beginDrag(props, monitor, component) { + return { + id: props.row.Id + } + }, + endDrag(props, monitor, component) { + monitor.getDropResult().fire() + } +} + +const target = { + drop(props, monitor, component) { + return { + source: monitor.getItem(), + target: { + id: props.row.Id + }, + fire: () => props.moveEntity(monitor.getItem().id, props.row.Id) + } + } +} + +function mapStateToProps(state) { + return { + spaces: state.database.spaces, + } +} + +function mapDispatchToProps(dispatch) { + return { + selectEntity: e => dispatch(actions.selectEntity(e)), + moveEntity: (source, target) => dispatch(actions.moveEntity(source, target)) + } +} + +class OpenBISTableRow extends React.Component { + + constructor(props) { + super(props) + } + + render() { + const row = this.props.row + return ( + <TableRow + key ={ row.Id } + ref = { instance => { + this.props.connectDragPreview(ReactDOM.findDOMNode(instance)) + this.props.connectDropTarget(ReactDOM.findDOMNode(instance)) + }}> + <TableCell> + <div + style = {{ cursor: 'pointer' }} + ref = { instance => this.props.connectDragSource(ReactDOM.findDOMNode(instance)) }> + <DragHandle/> + </div> + </TableCell> + { Object.entries(this.props.columns).map(([col, type]) => + <TableCell key={col} numeric={type === 'int'}> { row[col] }</TableCell> + )} + </TableRow> + ) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(DropTarget('row', target, targetCollect)(DragSource('row', source, sourceCollect)(OpenBISTableRow))) diff --git a/openbis_ng_ui/react/src/index.js b/openbis_ng_ui/react/src/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0c7c99656e61395792cb7afe66225cce6b9bf4d1 --- /dev/null +++ b/openbis_ng_ui/react/src/index.js @@ -0,0 +1,37 @@ +/* eslint-disable */ +import "regenerator-runtime/runtime" +import React from 'react' +import ReactDOM from 'react-dom' +import { createStore, applyMiddleware, compose } from 'redux' +import { Provider } from 'react-redux' +import createSagaMiddleware from 'redux-saga' +import reducer from './reducer/reducer.js' +import { watchInit, watchSetSpaces, watchSaveEntity, watchExpandNode } from './reducer/sagas' + +const sagaMiddleware = createSagaMiddleware() +const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; +export const store = createStore(reducer, composeEnhancers(applyMiddleware(sagaMiddleware))) + +sagaMiddleware.run(watchInit) +sagaMiddleware.run(watchSetSpaces) +sagaMiddleware.run(watchSaveEntity) +sagaMiddleware.run(watchExpandNode) + +const render = () => { + const App = require('./components/App.jsx').default + ReactDOM.render( + <Provider store = { store }> + <App /> + </Provider>, + document.getElementById("app") + ) +} + +if (module.hot) { + module.hot.accept('./components/App.jsx', () => setTimeout(render)) + module.hot.accept('./reducer/reducer.js', () => { + const nextRootReducer = require('./reducer/reducer.js').default + store.replaceReducer(nextRootReducer) + }); +} +render() \ No newline at end of file diff --git a/openbis_ng_ui/react/src/reducer/actions.js b/openbis_ng_ui/react/src/reducer/actions.js new file mode 100644 index 0000000000000000000000000000000000000000..213bcd005f8b4f9b6482d00e9cdcc59a23d3fa95 --- /dev/null +++ b/openbis_ng_ui/react/src/reducer/actions.js @@ -0,0 +1,50 @@ +export default { + // TODO setDirty for generic tabs + setDirty: (entityPermId, dirty) => ({ + type: 'SET-DIRTY', + entityPermId: entityPermId, + dirty: dirty + }), + expandNode: (node) => ({ + type: 'EXPAND-NODE', + node: node + }), + collapseNode: (node) => ({ + type: 'COLLAPSE-NODE', + node: node + }), + // database stuff + setSpaces: spaces => ({ + type: 'SET-SPACES', + spaces: spaces, + }), + selectEntity: entityPermId => ({ + type: 'SELECT-ENTITY', + entityPermId: entityPermId + }), + closeEntity: entityPermId => ({ + type: 'CLOSE-ENTITY', + entityPermId: entityPermId + }), + changePage: page => ({ + type: 'CHANGE-PAGE', + page: page + }), + sortBy: column => ({ + type: 'SORT-BY', + column: column + }), + setFilter: filter => ({ + type: 'SET-FILTER', + value: filter + }), + moveEntity: (source, target) => ({ + type: 'MOVE-ENTITY', + source: source, + target: target + }), + saveEntity: (entity) => ({ + type: 'SAVE-ENTITY', + entity: entity + }), +} diff --git a/openbis_ng_ui/react/src/reducer/database/reducer.js b/openbis_ng_ui/react/src/reducer/database/reducer.js new file mode 100644 index 0000000000000000000000000000000000000000..345a5a270a5423e927820fbb72919a93ac159d41 --- /dev/null +++ b/openbis_ng_ui/react/src/reducer/database/reducer.js @@ -0,0 +1,145 @@ +import initialState from '../initialstate.js' + + +function filterOf(filter, columns) { + if (filter == null || filter.length === 0) { + return _ => true + } else { + return entity => + Object.keys(columns) + .map(col => entity[col]) + .map(value => value != null && value.toString().includes(filter)) + .includes(true) + } +} + +function sortOf(orderColumn, direction, columns) { + return (a, b) => { + let valA = a[orderColumn] + let valB = b[orderColumn] + if (valA == null && valB == null) { + return 0 + } + if (valA == null) { + return 1 + } + if (valB == null) { + return -1 + } + const type = columns[orderColumn] + if (type === 'int') { + return direction === 'asc' ? valA - valB : valB - valA + } else { + if (valA < valB){ + return direction === 'asc' ? -1 : 1 + } else if (valA > valB) { + return direction === 'asc' ? 1 : -1 + } else { + return 0 + } + } + } +} + +function transformData(data, columns, filter, orderColumn, direction) { + const filtered = data.filter(filterOf(filter, columns)) + if (orderColumn in columns) { + return filtered.sort(sortOf(orderColumn, direction, columns)) + } else { + return filtered + } +} + +const concat = (...arrays) => [].concat(...arrays) +const take = (array, n) => array.slice(0, n) +const drop = (array, n) => array.slice(n) +const remove = (array, index) => [...take(array, index), ...drop(array, index + 1)] + +function move (array, oldIndex, newIndex) { + const value = array[oldIndex] + const removed = remove(array, oldIndex) + return concat(take(removed, newIndex), [value], drop(removed, newIndex)) +} + + +function entitiesByPermId(entities) { + const byPermId = {} + for (let entity of entities) { + byPermId[entity.permId.permId] = entity + } + return byPermId +} + + +function spaces(spaces = initialState.spaces, action) { + switch (action.type) { + case 'SET-SPACES': { + return entitiesByPermId(action.spaces) + } + case 'SAVED-ENTITY': { + const newSpaces = Object.assign({}, spaces) + newSpaces[action.entity.permId.permId] = action.entity + return newSpaces + } + default: { + return spaces + }} +} + + +function projects(projects = initialState.projects, action) { + switch (action.type) { + case 'SET-PROJECTS': { + return entitiesByPermId(action.projects) + } + default: { + return projects + }} +} + + +function table(table = initialState.table, action) { + switch (action.type) { + case 'CHANGE-PAGE': { + return Object.assign({}, table, { page: action.page }) + } + case 'SORT-BY': { + const column = action.column + const direction = + column === table.sortColumn + ? table.sortDirection === 'asc' ? 'desc' : 'asc' + : 'asc' + return Object.assign({}, table, { + sortColumn: column, + sortDirection: direction, + page: 0, + data: transformData(table.initialData, table.columns, table.filter, column, direction) + }) + } + case 'SET-FILTER': { + return Object.assign({}, table, { + page: 0, + filter: action.value, + data: transformData(table.initialData, table.columns, action.value, table.sortColumn, table.sortDirection) + }) + } + case 'MOVE-ENTITY': { + const sourceIndex = table.data.findIndex(e => e.Id === action.source) + const targetIndex = table.data.findIndex(e => e.Id === action.target) + return Object.assign({}, table, { + data: move(table.data, sourceIndex, targetIndex) + }) + } + default: { + return table + }} +} + + +export default function database(database = initialState.database, action) { + return { + spaces: spaces(database.spaces, action), + projects: projects(database.projects, action), + table: table(database.table, action), + } +} diff --git a/openbis_ng_ui/react/src/reducer/initialstate.js b/openbis_ng_ui/react/src/reducer/initialstate.js new file mode 100644 index 0000000000000000000000000000000000000000..14fc0455a5083b048ccc5a4b5b0b53feb56f8908 --- /dev/null +++ b/openbis_ng_ui/react/src/reducer/initialstate.js @@ -0,0 +1,73 @@ + +function foldLeft(array, acc, reducer) { + if (array.length === 0) { + return acc + } else { + const [head, ...tail] = array + return foldLeft(tail, reducer(acc, head), reducer) + } +} + +const take = (array, n) => array.slice(0, n) + +//const stringColumns = ['Description', 'Color', 'Address', 'Details', 'Info', 'Additonals', 'Other', 'Note', 'Optional', 'Something'] +//const intColumns = ['Weight', 'Length', 'Width', 'Number', 'Amount', 'Depth', 'Temperature', 'Volume', 'Height', 'Opacity'] + +const stringColumns = ['Description', 'Color', 'Address'] +const intColumns = ['Weight', 'Length', 'Width'] + + +function shuffle(arr) { + return arr.slice(0).sort((a, b) => Math.random() * 3 - 1.5) +} + +function randomString() { + return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) +} + +function createEntity(id) { + const numStrings = Math.random() * 3 + const numInts = Math.random() * 3 + let entity = { Id: id } + entity = foldLeft(take(shuffle(stringColumns), numStrings), entity, (acc, a) => Object.assign({}, acc, { [a] : randomString() })) + entity = foldLeft(take(shuffle(intColumns), numInts), entity, (acc, a) => Object.assign({}, acc, { [a] : Math.floor(Math.random() * 100) })) + return entity +} + +const data = [...Array(10000).keys()].map(i => createEntity(i + 1)) + +let tableColumns = foldLeft(stringColumns, {}, (acc, a) => Object.assign({}, acc, { [a]: 'string'})) +tableColumns = foldLeft(intColumns, tableColumns, (acc, a) => Object.assign({}, acc, { [a]: 'int'})) +tableColumns = foldLeft(shuffle(Object.entries(tableColumns)), { Id: 'int' }, (acc, a) => Object.assign({}, acc, { [a[0]] : a[1] })) + +// TODO split initialstate +export default { + database: { + spaces: {}, + projects: {}, + table: { + initialData: data, + data: data, + columns: tableColumns, + page: 0, + sortColumn: '', + sortDirection: 'asc' + }, + }, + + types: {}, + users: {}, + favourites: {}, + tools: {}, + + // TODO replace with generic tab description + openEntities: { + entities: [], + selectedEntity: null, + }, + dirtyEntities: [], + + loading: true, + // TODO generic tree + databaseTreeNodes: [], +} \ No newline at end of file diff --git a/openbis_ng_ui/react/src/reducer/reducer.js b/openbis_ng_ui/react/src/reducer/reducer.js new file mode 100644 index 0000000000000000000000000000000000000000..a88665d184d613c261793832a48a9bbb684fac40 --- /dev/null +++ b/openbis_ng_ui/react/src/reducer/reducer.js @@ -0,0 +1,131 @@ +import merge from 'lodash/merge' +import initialState from './initialstate.js' +import database from './database/reducer.js' + + +function replaceNode(nodes, newNode) { + return nodes.map( node => { + if (node.id === newNode.id) { + return newNode + } + return node + }) +} + +function asTreeNode(entity) { + return { + id: entity.permId.permId, + type: entity['@type'], + expanded: false, + loading: false, + loaded: false, + children: [], + } +} + +// reducers + +function loading(loading = initialState.loading, action) { + switch (action.type) { + case 'SET-SPACES': { + return false + } + case 'SAVE-ENTITY': { + return true + } + case 'SAVED-ENTITY': { + return false + } + default: { + return loading + }} +} + + +function databaseTreeNodes(databaseTreeNodes = initialState.databaseTreeNodes, action) { + switch (action.type) { + case 'SET-SPACES': { + return action.spaces.map(asTreeNode) + } + case 'SET-PROJECTS': { + const oldNode = databaseTreeNodes.filter( node => node.id === action.spacePermId )[0] + const projectNodes = action.projects.map(asTreeNode) + const node = merge({}, oldNode, { loading: false, loaded: true, children: projectNodes }) + return replaceNode(databaseTreeNodes, node) + } + case 'EXPAND-NODE': { + const loading = action.node.loaded === false + const node = merge({}, action.node, { expanded: true, loading: loading }) + return replaceNode(databaseTreeNodes, node) + } + case 'COLLAPSE-NODE': { + const node = merge({}, action.node, { expanded: false }) + return replaceNode(databaseTreeNodes, node) + } + default: { + return databaseTreeNodes + }} +} + +function openEntities(openEntities = initialState.openEntities, action) { + switch (action.type) { + case 'SELECT-ENTITY': { + const entities = openEntities.entities + return { + entities: entities.indexOf(action.entityPermId) > -1 ? entities : [].concat(entities, [action.entityPermId]), + selectedEntity: action.entityPermId, + } + } + case 'CLOSE-ENTITY': { + const newOpenEntities = openEntities.entities.filter(e => e !== action.entityPermId) + if (openEntities.selectedEntity === action.entityPermId) { + const oldIndex = openEntities.entities.indexOf(action.entityPermId) + const newIndex = oldIndex === newOpenEntities.length ? oldIndex - 1 : oldIndex + const selectedEntity = newIndex > -1 ? newOpenEntities[newIndex] : null + return { + entities: newOpenEntities, + selectedEntity: selectedEntity, + } + } else { + return { + entities: newOpenEntities, + selectedEntity: openEntities.selectedEntity, + } + } + } + default: { + return openEntities + }} +} + +function dirtyEntities(dirtyEntities = initialState.dirtyEntities, action) { + switch (action.type) { + case 'SET-DIRTY': { + if (action.dirty) { + return [].concat(dirtyEntities, [action.entityPermId]) + } else { + return dirtyEntities.filter(e => e !== action.entityPermId) + } + } + case 'SAVED-ENTITY': { + return dirtyEntities.filter(permId => permId !== action.entity.permId.permId) + } + case 'CLOSE-ENTITY': { + return dirtyEntities.filter(permId => permId !== action.entityPermId) + } + default: { + return dirtyEntities + }} +} + +function reducer(state = initialState, action) { + return { + database: database(state.database, action), + loading: loading(state.loading, action), + databaseTreeNodes: databaseTreeNodes(state.databaseTreeNodes, action), + openEntities: openEntities(state.openEntities, action), + dirtyEntities: dirtyEntities(state.dirtyEntities, action), + } +} + +export default reducer diff --git a/openbis_ng_ui/react/src/reducer/sagas.js b/openbis_ng_ui/react/src/reducer/sagas.js new file mode 100644 index 0000000000000000000000000000000000000000..45c47e4897a3ed9e1a234e71393a35115fd7686c --- /dev/null +++ b/openbis_ng_ui/react/src/reducer/sagas.js @@ -0,0 +1,46 @@ +import { put, takeEvery, call } from 'redux-saga/effects' +import openbis from '../services/openbis' + +// TODO split sagas + +function* init() { + const spaces = yield call(openbis.getSpaces) + yield put({ type: 'SET-SPACES', spaces: spaces }) +} + +export function* watchInit() { + yield takeEvery('INIT', init) +} + +function* selectSpace(action) { + yield put({ type: 'SELECT-ENTITY', entityPermId: action.spaces[0].permId.permId }) +} + +export function* watchSetSpaces() { + yield takeEvery('SET-SPACES', selectSpace) +} + +function* saveEntity(action) { + yield openbis.updateSpace(action.entity.permId, action.entity.description) + const spaces = yield call(openbis.getSpaces) + const space = spaces.filter(space => space.permId.permId === action.entity.permId.permId)[0] + yield put({ type: 'SAVED-ENTITY', entity: space }) +} + +export function* watchSaveEntity() { + yield takeEvery('SAVE-ENTITY', saveEntity) +} + +function* expandNode(action) { + const node = action.node + if (node.loaded === false) { + if (node.type === 'as.dto.space.Space') { + const projects = yield openbis.searchProjects(node.id) + yield put({ type: 'SET-PROJECTS', projects: projects, spacePermId: node.id }) + } + } +} + +export function* watchExpandNode() { + yield takeEvery('EXPAND-NODE', expandNode) +} diff --git a/openbis_ng_ui/react/src/services/openbis.js b/openbis_ng_ui/react/src/services/openbis.js new file mode 100644 index 0000000000000000000000000000000000000000..0fed4df3db922212e5bcf12e6c76ac488a458cbe --- /dev/null +++ b/openbis_ng_ui/react/src/services/openbis.js @@ -0,0 +1,55 @@ +import { store } from '../index.js' + +let v3 = null +/* eslint-disable-next-line no-undef */ +requirejs([ 'openbis' ], openbis => { + v3 = new openbis() + v3.login('admin', 'password').done(() => store.dispatch({type: 'INIT'})) +}) + +function getSpaces() { + return new Promise( (resolve, reject) => { + /* eslint-disable-next-line no-undef */ + requirejs( + ['as/dto/space/search/SpaceSearchCriteria', 'as/dto/space/fetchoptions/SpaceFetchOptions' ], + (SpaceSearchCriteria, SpaceFetchOptions) => + v3.searchSpaces(new SpaceSearchCriteria(), new SpaceFetchOptions()).done(res => resolve(res.getObjects())) + ) + }) +} + +function updateSpace(permId, description) { + return new Promise( (resolve, reject) => { + /* eslint-disable-next-line no-undef */ + requirejs( + ['as/dto/space/update/SpaceUpdate'], + (SpaceUpdate) => { + let spaceUpdate = new SpaceUpdate() + spaceUpdate.setSpaceId(permId) + spaceUpdate.setDescription(description) + v3.updateSpaces([spaceUpdate]).done(res => resolve(res)) + } + ) + }) +} + +function searchProjects(spacePermId) { + return new Promise( (resolve, reject) => { + /* eslint-disable-next-line no-undef */ + requirejs( + ['as/dto/project/search/ProjectSearchCriteria', 'as/dto/project/fetchoptions/ProjectFetchOptions'], + (ProjectSearchCriteria, ProjectFetchOptions) => { + let searchCriteria = new ProjectSearchCriteria() + searchCriteria.withSpace().withPermId().thatEquals(spacePermId) + let fetchOptions = new ProjectFetchOptions() + v3.searchProjects(searchCriteria, fetchOptions).done(res => resolve(res.getObjects())) + } + ) + }) +} + +export default { + getSpaces: getSpaces, + updateSpace: updateSpace, + searchProjects: searchProjects +} diff --git a/openbis_ng_ui/react/webpack.config.dev.js b/openbis_ng_ui/react/webpack.config.dev.js new file mode 100644 index 0000000000000000000000000000000000000000..f91123fea8c2efed5eada224b76df33ee291c554 --- /dev/null +++ b/openbis_ng_ui/react/webpack.config.dev.js @@ -0,0 +1,61 @@ +/* eslint-disable */ +const HtmlWebpackPlugin = require('html-webpack-plugin') + +module.exports = { + entry: './src/index.js', + output: { + path: __dirname + '/build/npm-build/', + filename: 'bundle.js' + }, + + devServer: { + contentBase: "./src", + hot: true, + https: false, + proxy: { + "/openbis": { + "target": 'https://localhost:8122', + "changeOrigin": true, + "secure": false + } + } + }, + + devtool: "source-map", + + mode: 'development', + + module: { + rules: [ + { + test: /\.(js|jsx)$/, + exclude: /node_modules/, + use: { + loader: "babel-loader" + } + }, + { + test: /\.(css)$/, + use: [ + 'style-loader', + 'css-loader' + ] + }, + { + test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000 + } + } + ] + }, + + plugins: [ + new HtmlWebpackPlugin({ + inject: 'body', + filename: './index.html', + template: './index.html' + }) + ] +}; diff --git a/openbis_ng_ui/react/webpack.config.js b/openbis_ng_ui/react/webpack.config.js new file mode 100644 index 0000000000000000000000000000000000000000..26566999acd67f245e879931d3daa9edf27d2843 --- /dev/null +++ b/openbis_ng_ui/react/webpack.config.js @@ -0,0 +1,46 @@ +/* eslint-disable */ +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +module.exports = { + entry: './src/index.js', + output: { + path: __dirname + '/build/', + filename: 'bundle.[hash].js' + }, + + mode: 'production', + + module: { + rules: [ + { + test: /\.(js|jsx)$/, + exclude: /node_modules/, + use: { + loader: "babel-loader" + } + }, + { + test: /\.(css)$/, + use: [ + 'style-loader', + 'css-loader' + ] + }, + { + test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000 + } + } + ] + }, + + plugins: [ + new HtmlWebpackPlugin({ + inject: 'body', + filename: './index.html', + template: './index.html' + }) + ] +}; diff --git a/openbis_ng_ui/settings.gradle b/openbis_ng_ui/settings.gradle new file mode 100644 index 0000000000000000000000000000000000000000..94b3605cf52df33a56e9df92d4c4c5c7b75fe774 --- /dev/null +++ b/openbis_ng_ui/settings.gradle @@ -0,0 +1,2 @@ +include 'react' + diff --git a/openbis_standard_technologies/build.gradle b/openbis_standard_technologies/build.gradle index b031c41ff62b8d1055fc204a8c8bba385b94e501..10296d8abf578d74c193d852b44572651703076b 100644 --- a/openbis_standard_technologies/build.gradle +++ b/openbis_standard_technologies/build.gradle @@ -39,6 +39,7 @@ evaluationDependsOn(':rtd_yeastx') evaluationDependsOn(':deep_sequencing_unit') evaluationDependsOn(':plasmid') evaluationDependsOn(':big_data_link_server') +evaluationDependsOn(':openbis_ng_ui') apply from: '../gradle/javaproject.gradle' @@ -452,6 +453,17 @@ task clientsAndApis(type: Zip, dependsOn: [dssClientZip, queryApiZip, apiV3Zip, } } +task deleteOpenbisNgUi(type: Delete) { + delete 'dist/core-plugins/openbis-ng-ui/1/as/webapps/openbis-ng-ui/html' +} + +task copyOpenbisNgUiToCorePlugins(type: Copy, dependsOn: [deleteOpenbisNgUi, project(':openbis_ng_ui').tasks.build]) { + from project(':openbis_ng_ui').file('react/build') + into file('dist/core-plugins/openbis-ng-ui/1/as/webapps/openbis-ng-ui/html') +} + +zipCorePlugins.dependsOn copyOpenbisNgUiToCorePlugins + task generateJavadoc(type: Javadoc) { source = configurations.javadoc_sources.collect { zipTree(it).matching { include "**/ch/ethz/sis/openbis/generic/asapi/**/*.java" diff --git a/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/config/StandardProfile.js b/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/config/StandardProfile.js index 52cd3406d34145576a820e5304f301d0601419f7..c04e7b62a87bc3d55ec8a4e88d208cd63d3829dd 100644 --- a/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/config/StandardProfile.js +++ b/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/config/StandardProfile.js @@ -329,18 +329,33 @@ $.extend(StandardProfile.prototype, DefaultProfile.prototype, { var samplesToDelete = null; var changesToDo = null; if((orderStatus === "ORDERED" || orderStatus === "DELIVERED" || orderStatus === "PAID") && !sample.properties["ORDER_STATE"]) { - //Set property - sample.properties["ORDER_STATE"] = window.btoa(unescape(encodeURIComponent(JSON.stringify(JSON.decycle(sample))))); - //Update order state on the requests - changesToDo = []; - var requests = sample.parents; - if(requests) { - for(var rIdx = 0; rIdx < requests.length; rIdx++) { - changesToDo.push({ "permId" : requests[rIdx].permId, "identifier" : requests[rIdx].identifier, "properties" : {"ORDER_STATUS" : orderStatus } }); - } + //Update parents to hold all info + var searchSamples = { entityKind : "SAMPLE", logicalOperator : "OR", rules : {} }; + for(var pIdx = 0; pIdx < sample.parents.length; pIdx++) { + searchSamples.rules["UUIDv4_" + pIdx] = { type : "Attribute", name : "PERM_ID", value : sample.parents[pIdx].permId }; } + + mainController.serverFacade.searchForSamplesAdvanced(searchSamples, { only : true, withProperties : true, withAncestors : true, withAncestorsProperties : true }, function(result) { + sample.parents = mainController.serverFacade.getV3SamplesAsV1(result.objects); + + //Set property + sample.properties["ORDER_STATE"] = window.btoa(unescape(encodeURIComponent(JSON.stringify(sample)))); + + //Update order state on the requests + changesToDo = []; + var requests = sample.parents; + if(requests) { + for(var rIdx = 0; rIdx < requests.length; rIdx++) { + changesToDo.push({ "permId" : requests[rIdx].permId, "identifier" : requests[rIdx].identifier, "properties" : {"ORDER_STATUS" : orderStatus } }); + } + } + + action(sample, null, samplesToDelete, changesToDo); + }); + } else { + action(sample, null, samplesToDelete, changesToDo); } - action(sample, null, samplesToDelete, changesToDo); + } else if(sample.sampleTypeCode === "REQUEST") { mainController.currentView._newProductsController.createAndAddToForm(sample, action); } else if(action) { @@ -357,173 +372,278 @@ $.extend(StandardProfile.prototype, DefaultProfile.prototype, { } else if(sampleTypeCode === "ORDER") { var isExisting = mainController.currentView._sampleFormModel.mode === FormMode.VIEW; var isFromState = false; - if(isExisting) { - // - // Data Structures to Help the reports functionality - // - var order = mainController.currentView._sampleFormModel.sample; - if(order.properties["ORDER_STATE"]) { - isFromState = true; - order = JSON.parse(decodeURIComponent(escape(window.atob(order.properties["ORDER_STATE"])))); - } - - var requests = order.parents; - var providerByPermId = {}; - var productsByProviderPermId = {}; - var quantityByProductPermId = {}; - var absoluteTotalByCurrency = {}; - - // - // Fills data structures - // - for(var rIdx = 0; rIdx < requests.length; rIdx++) { - var request = requests[rIdx]; - var requestProductsAnnotations = FormUtil.getAnnotationsFromSample(request); - var requestProducts = request.parents; - for(var pIdx = 0; pIdx < requestProducts.length; pIdx++) { - var requestProduct = requestProducts[pIdx]; - var requestProductAnnotations = requestProductsAnnotations[requestProduct.permId]; - - if(requestProduct.parents.length === 0) { - Util.showUserError("Product " + requestProduct.code + " does not have a provider, FIX IT!."); - return; - } - var provider = requestProduct.parents[0]; - var providerProducts = productsByProviderPermId[provider.permId]; - if(!providerProducts) { - providerProducts = []; - productsByProviderPermId[provider.permId] = providerProducts; - providerByPermId[provider.permId] = provider; - } - var quantity = quantityByProductPermId[requestProduct.permId]; - if(!quantity) { - quantity = 0; - } - quantity += parseInt(requestProductAnnotations["QUANTITY_OF_ITEMS"]); - if(!quantity) { - Util.showUserError("Product " + requestProduct.code + " from request " + request.code + " does not have a quantity, FIX IT!."); - return; - } - var currencyCode = requestProduct.properties["CURRENCY"]; - if(!currencyCode) { - currencyCode = "N/A"; - } - - var absoluteTotalForCurrency = absoluteTotalByCurrency[currencyCode]; - if(!absoluteTotalForCurrency) { - absoluteTotalForCurrency = 0; - } - - if(requestProduct.properties["PRICE_PER_UNIT"]) { - absoluteTotalForCurrency += parseFloat(requestProduct.properties["PRICE_PER_UNIT"]) * quantity; + if(isExisting) { + var showOrderSummary = function(order) { + var requests = order.parents; + var providerByPermId = {}; + var productsByProviderPermId = {}; + var quantityByProductPermId = {}; + var absoluteTotalByCurrency = {}; + + // + // Fills data structures + // + for(var rIdx = 0; rIdx < requests.length; rIdx++) { + var request = requests[rIdx]; + var requestProductsAnnotations = FormUtil.getAnnotationsFromSample(request); + var requestProducts = request.parents; + for(var pIdx = 0; pIdx < requestProducts.length; pIdx++) { + var requestProduct = requestProducts[pIdx]; + var requestProductAnnotations = requestProductsAnnotations[requestProduct.permId]; + + if(requestProduct.parents.length === 0) { + Util.showUserError("Product " + requestProduct.code + " does not have a provider, FIX IT!."); + return; + } + var provider = requestProduct.parents[0]; + var providerProducts = productsByProviderPermId[provider.permId]; + if(!providerProducts) { + providerProducts = []; + productsByProviderPermId[provider.permId] = providerProducts; + providerByPermId[provider.permId] = provider; + } + var quantity = quantityByProductPermId[requestProduct.permId]; + if(!quantity) { + quantity = 0; + } + quantity += parseInt(requestProductAnnotations["QUANTITY_OF_ITEMS"]); + if(!quantity) { + Util.showUserError("Product " + requestProduct.code + " from request " + request.code + " does not have a quantity, FIX IT!."); + return; + } + var currencyCode = requestProduct.properties["CURRENCY"]; + if(!currencyCode) { + currencyCode = "N/A"; + } + + var absoluteTotalForCurrency = absoluteTotalByCurrency[currencyCode]; + if(!absoluteTotalForCurrency) { + absoluteTotalForCurrency = 0; + } + + if(requestProduct.properties["PRICE_PER_UNIT"]) { + absoluteTotalForCurrency += parseFloat(requestProduct.properties["PRICE_PER_UNIT"]) * quantity; + } + + absoluteTotalByCurrency[currencyCode] = absoluteTotalForCurrency; + + quantityByProductPermId[requestProduct.permId] = quantity; + providerProducts.push(requestProduct); } - - absoluteTotalByCurrency[currencyCode] = absoluteTotalForCurrency; - - quantityByProductPermId[requestProduct.permId] = quantity; - providerProducts.push(requestProduct); } - } - - // - // Button that prints an order report - // - var printOrder = FormUtil.getButtonWithIcon("glyphicon-print",function() { - //Create an order page for each provider - var orderPages = []; - for(var providerPermId in productsByProviderPermId) { - var provider = providerByPermId[providerPermId]; - var preferredSupplierLanguage = provider.properties["COMPANY_LANGUAGE"]; - - var languageLabels = profile.orderLanguage[preferredSupplierLanguage]; - if(!languageLabels) { - languageLabels = profile.orderLanguage["ENGLISH"]; - } - - var providerProducts = productsByProviderPermId[providerPermId]; - - var registrationDate = null; - if(order.registrationDetails && order.registrationDetails.modificationDate) { - registrationDate = order.registrationDetails.modificationDate; - } else if(mainController.currentView._sampleFormModel.sample.registrationDetails && - mainController.currentView._sampleFormModel.sample.registrationDetails.modificationDate) { - registrationDate = mainController.currentView._sampleFormModel.sample.registrationDetails.modificationDate; - } - - var page = languageLabels["ORDER_FORM"]; - page += "\n"; - page += "\n"; - page += languageLabels["ORDER_INFORMATION"]; - page += "\n"; - page += "- " + languageLabels["ORDER_DATE"] + ": " + Util.getFormatedDate(new Date(registrationDate)); - page += "\n"; - page += "- " + languageLabels["ORDER_STATUS"] + ": " + order.properties["ORDER_STATUS"]; - page += "\n"; - page += "- " + languageLabels["ORDER_CODE"] + ": " + order.code; - page += "\n"; - page += "\n"; - page += "\n"; - page += languageLabels["COSTUMER_INFORMATION"]; - page += "\n"; - if(order.properties["SHIP_TO"]) { - page += "- " + languageLabels["SHIP_TO"] + ": " + order.properties["SHIP_TO"]; - page += "\n"; + + // + // Button that prints an order report + // + var printOrder = FormUtil.getButtonWithIcon("glyphicon-print",function() { + //Create an order page for each provider + var orderPages = []; + for(var providerPermId in productsByProviderPermId) { + var provider = providerByPermId[providerPermId]; + var preferredSupplierLanguage = provider.properties["COMPANY_LANGUAGE"]; + + var languageLabels = profile.orderLanguage[preferredSupplierLanguage]; + if(!languageLabels) { + languageLabels = profile.orderLanguage["ENGLISH"]; } - if(order.properties["BILL_TO"]) { - page += "- " + languageLabels["BILL_TO"] + ": " + order.properties["BILL_TO"]; - page += "\n"; + + var providerProducts = productsByProviderPermId[providerPermId]; + + var registrationDate = null; + if(order.registrationDetails && order.registrationDetails.modificationDate) { + registrationDate = order.registrationDetails.modificationDate; + } else if(mainController.currentView._sampleFormModel.sample.registrationDetails && + mainController.currentView._sampleFormModel.sample.registrationDetails.modificationDate) { + registrationDate = mainController.currentView._sampleFormModel.sample.registrationDetails.modificationDate; } - if(order.properties["SHIP_ADDRESS"]) { - page += "- " + languageLabels["SHIP_ADDRESS"] + ": " + order.properties["SHIP_ADDRESS"]; + + var page = languageLabels["ORDER_FORM"]; page += "\n"; - } - if(order.properties["CONTACT_PHONE"]) { - page += "- " + languageLabels["PHONE"] + ": " + order.properties["CONTACT_PHONE"]; page += "\n"; - } - if(order.properties["CONTACT_FAX"]) { - page += "- " + languageLabels["FAX"] + ": " + order.properties["CONTACT_FAX"]; + page += languageLabels["ORDER_INFORMATION"]; page += "\n"; - } - page += "\n"; - page += "\n"; - page += languageLabels["SUPPLIER_INFORMATION"]; - page += "\n"; - if(provider.properties["NAME"]) { - page += "- " + languageLabels["SUPPLIER"] + ": " + provider.properties["NAME"]; + page += "- " + languageLabels["ORDER_DATE"] + ": " + Util.getFormatedDate(new Date(registrationDate)); page += "\n"; - } - if(provider.properties["COMPANY_ADDRESS_LINE_1"]) { - page += "- " + languageLabels["SUPPLIER_ADDRESS_LINE_1"] + ": " + provider.properties["COMPANY_ADDRESS_LINE_1"] + page += "- " + languageLabels["ORDER_STATUS"] + ": " + order.properties["ORDER_STATUS"]; page += "\n"; - } - if(provider.properties["COMPANY_ADDRESS_LINE_2"]) { - page += " " + languageLabels["SUPPLIER_ADDRESS_LINE_2"] + " " + provider.properties["COMPANY_ADDRESS_LINE_2"] + page += "- " + languageLabels["ORDER_CODE"] + ": " + order.code; page += "\n"; - } - if(provider.properties["COMPANY_PHONE"]) { - page += "- " + languageLabels["SUPPLIER_PHONE"] + ": " + provider.properties["COMPANY_PHONE"]; page += "\n"; - } - if(provider.properties["COMPANY_FAX"]) { - page += "- " + languageLabels["SUPPLIER_FAX"] + ": " + provider.properties["COMPANY_FAX"]; page += "\n"; - } - if(provider.properties["COMPANY_EMAIL"]) { - page += "- " + languageLabels["SUPPLIER_EMAIL"] + ": " + provider.properties["COMPANY_EMAIL"]; + page += languageLabels["COSTUMER_INFORMATION"]; page += "\n"; - } - if(provider.properties["CUSTOMER_NUMBER"]) { - page += "- " + languageLabels["CUSTOMER_NUMBER"] + ": " + provider.properties["CUSTOMER_NUMBER"]; + if(order.properties["SHIP_TO"]) { + page += "- " + languageLabels["SHIP_TO"] + ": " + order.properties["SHIP_TO"]; + page += "\n"; + } + if(order.properties["BILL_TO"]) { + page += "- " + languageLabels["BILL_TO"] + ": " + order.properties["BILL_TO"]; + page += "\n"; + } + if(order.properties["SHIP_ADDRESS"]) { + page += "- " + languageLabels["SHIP_ADDRESS"] + ": " + order.properties["SHIP_ADDRESS"]; + page += "\n"; + } + if(order.properties["CONTACT_PHONE"]) { + page += "- " + languageLabels["PHONE"] + ": " + order.properties["CONTACT_PHONE"]; + page += "\n"; + } + if(order.properties["CONTACT_FAX"]) { + page += "- " + languageLabels["FAX"] + ": " + order.properties["CONTACT_FAX"]; + page += "\n"; + } page += "\n"; - } - page += "\n"; - page += "\n"; - page += languageLabels["REQUESTED_PRODUCTS_LABEL"]; - page += "\n"; - page += languageLabels["PRODUCTS_COLUMN_NAMES_LABEL"]; - page += "\n"; - var providerTotalByCurrency = {}; + page += "\n"; + page += languageLabels["SUPPLIER_INFORMATION"]; + page += "\n"; + if(provider.properties["NAME"]) { + page += "- " + languageLabels["SUPPLIER"] + ": " + provider.properties["NAME"]; + page += "\n"; + } + if(provider.properties["COMPANY_ADDRESS_LINE_1"]) { + page += "- " + languageLabels["SUPPLIER_ADDRESS_LINE_1"] + ": " + provider.properties["COMPANY_ADDRESS_LINE_1"] + page += "\n"; + } + if(provider.properties["COMPANY_ADDRESS_LINE_2"]) { + page += " " + languageLabels["SUPPLIER_ADDRESS_LINE_2"] + " " + provider.properties["COMPANY_ADDRESS_LINE_2"] + page += "\n"; + } + if(provider.properties["COMPANY_PHONE"]) { + page += "- " + languageLabels["SUPPLIER_PHONE"] + ": " + provider.properties["COMPANY_PHONE"]; + page += "\n"; + } + if(provider.properties["COMPANY_FAX"]) { + page += "- " + languageLabels["SUPPLIER_FAX"] + ": " + provider.properties["COMPANY_FAX"]; + page += "\n"; + } + if(provider.properties["COMPANY_EMAIL"]) { + page += "- " + languageLabels["SUPPLIER_EMAIL"] + ": " + provider.properties["COMPANY_EMAIL"]; + page += "\n"; + } + if(provider.properties["CUSTOMER_NUMBER"]) { + page += "- " + languageLabels["CUSTOMER_NUMBER"] + ": " + provider.properties["CUSTOMER_NUMBER"]; + page += "\n"; + } + page += "\n"; + page += "\n"; + page += languageLabels["REQUESTED_PRODUCTS_LABEL"]; + page += "\n"; + page += languageLabels["PRODUCTS_COLUMN_NAMES_LABEL"]; + page += "\n"; + var providerTotalByCurrency = {}; + for(var pIdx = 0; pIdx < providerProducts.length; pIdx++) { + var product = providerProducts[pIdx]; + var quantity = quantityByProductPermId[product.permId]; + var unitPriceAsString = product.properties["PRICE_PER_UNIT"]; + var unitPrice = "N/A"; + if(unitPriceAsString) { + unitPrice = parseFloat(unitPriceAsString); + } + var currencyCode = product.properties["CURRENCY"]; + if(!currencyCode) { + currencyCode = "N/A"; + } + page += quantity + "\t\t" + product.properties["NAME"] + "\t\t" + product.properties["CATALOG_NUM"] + "\t\t" + unitPrice + " " + currencyCode; + page += "\n"; + + if(unitPriceAsString) { + var totalForCurrency = providerTotalByCurrency[product.properties["CURRENCY"]]; + if(!totalForCurrency) { + totalForCurrency = 0; + } + totalForCurrency += unitPrice * quantity; + + providerTotalByCurrency[product.properties["CURRENCY"]] = totalForCurrency; + } else { + providerTotalByCurrency[product.properties["CURRENCY"]] = "N/A"; + } + } + page += "\n"; + page += "\n"; + page += "\n"; + + var showTotals = false; + for(var currency in providerTotalByCurrency) { + if(providerTotalByCurrency[currency] > 0) { + showTotals = true; + } + } + if(showTotals) { + page += languageLabels["PRICE_TOTALS_LABEL"] + ":"; + page += "\n"; + for(var currency in providerTotalByCurrency) { + page += providerTotalByCurrency[currency] + " " + currency; + page += "\n"; + } + page += "\n"; + page += "\n"; + page += "\n"; + } + page += languageLabels["ADDITIONAL_INFO_LABEL"]; + page += "\n"; + var additionalInfo = order.properties["ADDITIONAL_INFORMATION"]; + if(!additionalInfo) { + additionalInfo = "N/A"; + } + page += additionalInfo; + orderPages.push(page); + } + + //Print Pages + for(var pageIdx = 0; pageIdx < orderPages.length; pageIdx++) { + Util.downloadTextFile(orderPages[pageIdx], "order_" + sample.code + "_p" + pageIdx + ".txt"); + } + + }, "Print Order"); + + // + // Order Summary Grid + // + var columns = [ { + label : 'Catalog Num', + property : 'catalogNum', + isExportable: true, + sortable : true, + render : function(data) { + return FormUtil.getFormLink(data.catalogNum, "Sample", data.permId); + } + },{ + label : 'Supplier', + property : 'supplier', + isExportable: true, + sortable : true + }, { + label : 'Name', + property : 'name', + isExportable: true, + sortable : true + }, { + label : 'Quantity', + property : 'quantity', + isExportable: true, + sortable : true, + }, { + label : 'Unit Price', + property : 'unitPrice', + isExportable: true, + sortable : true + }, { + label : 'Total Product Cost', + property : 'totalProductCost', + isExportable: true, + sortable : true + }, { + label : 'Currency', + property : 'currency', + isExportable: true, + sortable : true + }]; + + var getDataRows = function(callback) { + var rows = []; + for(var providerPermId in productsByProviderPermId) { + var provider = providerByPermId[providerPermId]; + var providerProducts = productsByProviderPermId[providerPermId]; for(var pIdx = 0; pIdx < providerProducts.length; pIdx++) { var product = providerProducts[pIdx]; var quantity = quantityByProductPermId[product.permId]; @@ -532,157 +652,61 @@ $.extend(StandardProfile.prototype, DefaultProfile.prototype, { if(unitPriceAsString) { unitPrice = parseFloat(unitPriceAsString); } + var rowData = {}; + rowData.permId = product.permId; + rowData.supplier = provider.properties["NAME"]; + rowData.name = product.properties["NAME"]; + rowData.catalogNum = product.properties["CATALOG_NUM"]; + rowData.quantity = quantity; + rowData.unitPrice = unitPrice; + if(unitPrice !== "N/A") { + rowData.totalProductCost = rowData.quantity * rowData.unitPrice; + } else { + rowData.totalProductCost = "N/A"; + } var currencyCode = product.properties["CURRENCY"]; if(!currencyCode) { currencyCode = "N/A"; } - page += quantity + "\t\t" + product.properties["NAME"] + "\t\t" + product.properties["CATALOG_NUM"] + "\t\t" + unitPrice + " " + currencyCode; - page += "\n"; - - if(unitPriceAsString) { - var totalForCurrency = providerTotalByCurrency[product.properties["CURRENCY"]]; - if(!totalForCurrency) { - totalForCurrency = 0; - } - totalForCurrency += unitPrice * quantity; - - providerTotalByCurrency[product.properties["CURRENCY"]] = totalForCurrency; - } else { - providerTotalByCurrency[product.properties["CURRENCY"]] = "N/A"; - } + rowData.currency = currencyCode; + rows.push(rowData); } - page += "\n"; - page += "\n"; - page += "\n"; - var showTotals = false; - for(var currency in providerTotalByCurrency) { - if(providerTotalByCurrency[currency] > 0) { - showTotals = true; - } - } - if(showTotals) { - page += languageLabels["PRICE_TOTALS_LABEL"] + ":"; - page += "\n"; - for(var currency in providerTotalByCurrency) { - page += providerTotalByCurrency[currency] + " " + currency; - page += "\n"; - } - page += "\n"; - page += "\n"; - page += "\n"; - } - page += languageLabels["ADDITIONAL_INFO_LABEL"]; - page += "\n"; - var additionalInfo = order.properties["ADDITIONAL_INFORMATION"]; - if(!additionalInfo) { - additionalInfo = "N/A"; - } - page += additionalInfo; - orderPages.push(page); - } + } + callback(rows); + }; - //Print Pages - for(var pageIdx = 0; pageIdx < orderPages.length; pageIdx++) { - Util.downloadTextFile(orderPages[pageIdx], "order_" + sample.code + "_p" + pageIdx + ".txt"); + var orderSummaryContainer = $("<div>"); + var repTitle = "Order Summary"; + if(isFromState) { + repTitle += " (as saved when ordered)" } - }, "Print Order"); - - // - // Order Summary Grid - // - var columns = [ { - label : 'Catalog Num', - property : 'catalogNum', - isExportable: true, - sortable : true, - render : function(data) { - return FormUtil.getFormLink(data.catalogNum, "Sample", data.permId); - } - },{ - label : 'Supplier', - property : 'supplier', - isExportable: true, - sortable : true - }, { - label : 'Name', - property : 'name', - isExportable: true, - sortable : true - }, { - label : 'Quantity', - property : 'quantity', - isExportable: true, - sortable : true, - }, { - label : 'Unit Price', - property : 'unitPrice', - isExportable: true, - sortable : true - }, { - label : 'Total Product Cost', - property : 'totalProductCost', - isExportable: true, - sortable : true - }, { - label : 'Currency', - property : 'currency', - isExportable: true, - sortable : true - }]; - - var getDataRows = function(callback) { - var rows = []; - for(var providerPermId in productsByProviderPermId) { - var provider = providerByPermId[providerPermId]; - var providerProducts = productsByProviderPermId[providerPermId]; - for(var pIdx = 0; pIdx < providerProducts.length; pIdx++) { - var product = providerProducts[pIdx]; - var quantity = quantityByProductPermId[product.permId]; - var unitPriceAsString = product.properties["PRICE_PER_UNIT"]; - var unitPrice = "N/A"; - if(unitPriceAsString) { - unitPrice = parseFloat(unitPriceAsString); - } - var rowData = {}; - rowData.permId = product.permId; - rowData.supplier = provider.properties["NAME"]; - rowData.name = product.properties["NAME"]; - rowData.catalogNum = product.properties["CATALOG_NUM"]; - rowData.quantity = quantity; - rowData.unitPrice = unitPrice; - if(unitPrice !== "N/A") { - rowData.totalProductCost = rowData.quantity * rowData.unitPrice; - } else { - rowData.totalProductCost = "N/A"; - } - var currencyCode = product.properties["CURRENCY"]; - if(!currencyCode) { - currencyCode = "N/A"; - } - rowData.currency = currencyCode; - rows.push(rowData); - } - + var orderSummary = new DataGridController(repTitle, columns, [], null, getDataRows, null, false, "ORDER_SUMMARY"); + orderSummary.init(orderSummaryContainer); + + var totalsByCurrencyContainer = $("<div>").append($("<br>")).append($("<legend>").append("Total:")); + for(var currency in absoluteTotalByCurrency) { + totalsByCurrencyContainer.append(absoluteTotalByCurrency[currency] + " " + currency).append($("<br>")); } - callback(rows); - }; - - var orderSummaryContainer = $("<div>"); - var repTitle = "Order Summary"; - if(isFromState) { - repTitle += " (as saved when ordered)" + $("#" + containerId).append(orderSummaryContainer).append(totalsByCurrencyContainer).append($("<br>")).append(printOrder); } - var orderSummary = new DataGridController(repTitle, columns, [], null, getDataRows, null, false, "ORDER_SUMMARY"); - orderSummary.init(orderSummaryContainer); - - var totalsByCurrencyContainer = $("<div>").append($("<br>")).append($("<legend>").append("Total:")); - for(var currency in absoluteTotalByCurrency) { - totalsByCurrencyContainer.append(absoluteTotalByCurrency[currency] + " " + currency).append($("<br>")); + // + // Data Structures to Help the reports functionality + // + var orderSample = mainController.currentView._sampleFormModel.sample; + if(orderSample.properties["ORDER_STATE"]) { + isFromState = true; + var order = JSON.parse(decodeURIComponent(escape(window.atob(orderSample.properties["ORDER_STATE"])))); + showOrderSummary(order); + } else { + var searchSamples = { entityKind : "SAMPLE", logicalOperator : "OR", rules : { "UUIDv4" : { type : "Attribute", name : "PERM_ID", value : orderSample.permId } } }; + mainController.serverFacade.searchForSamplesAdvanced(searchSamples, { only : true, withProperties : true, withAncestors : true, withAncestorsProperties : true }, function(result) { + var order = mainController.serverFacade.getV3SamplesAsV1(result.objects)[0]; + showOrderSummary(order); + }); } - $("#" + containerId).append(orderSummaryContainer).append(totalsByCurrencyContainer).append($("<br>")).append(printOrder); } } else if(sampleTypeCode === "REQUEST") { var isEnabled = mainController.currentView._sampleFormModel.mode !== FormMode.VIEW; diff --git a/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/controllers/LayoutManager.js b/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/controllers/LayoutManager.js index 874b7d90daa06ff84d7a8fff1df6524e5ece792b..c3922e869f3269ca830beab2838aa5ce7e239777 100644 --- a/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/controllers/LayoutManager.js +++ b/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/controllers/LayoutManager.js @@ -428,8 +428,6 @@ var LayoutManager = { } } - console.log("reloadView called with isFirstTime:" + isFirstTime); - if (this.FOUND_SIZE === this.DESKTOP_SIZE) { this._setDesktopLayout(view, isFirstTime); } else if (this.FOUND_SIZE === this.TABLET_SIZE) { diff --git a/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/server/ServerFacade.js b/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/server/ServerFacade.js index f1d812e7343b4ffa89221e1e553b7846253cb467..cd600c62a48fb79c32b37f4bc6802390ab8e55ce 100644 --- a/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/server/ServerFacade.js +++ b/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/server/ServerFacade.js @@ -1024,6 +1024,13 @@ function ServerFacade(openbisServer) { childrenFetchOptions.withType(); } } + if(advancedFetchOptions.withAncestors) { + var ancestorsFetchOptions = fetchOptions.withParents(); + if(advancedFetchOptions.withAncestorsProperties) { + ancestorsFetchOptions.withProperties(); + } + ancestorsFetchOptions.withParentsUsing(ancestorsFetchOptions); + } } if(advancedFetchOptions && advancedFetchOptions.cache) { diff --git a/openbis_standard_technologies/settings.gradle b/openbis_standard_technologies/settings.gradle index f996c84ccde18315345bdd3df01884e9b585ec83..c5d11ff9434eedc79a0ebd73a05575c943eaef2f 100644 --- a/openbis_standard_technologies/settings.gradle +++ b/openbis_standard_technologies/settings.gradle @@ -1,2 +1,3 @@ includeFlat 'commonbase', 'common', 'openbis_api', 'openbis-common', 'authentication', 'dbmigration', 'openbis', - 'datastore_server', 'screening', 'rtd_yeastx', 'rtd_phosphonetx', 'deep_sequencing_unit', 'plasmid', 'big_data_link_server' + 'datastore_server', 'screening', 'rtd_yeastx', 'rtd_phosphonetx', 'deep_sequencing_unit', 'plasmid', 'big_data_link_server', + 'openbis_ng_ui'