From e6727fd631cc293baefeb72e1d985b0db504f529 Mon Sep 17 00:00:00 2001 From: Pieter Verschaffelt Date: Mon, 21 Jun 2021 14:34:33 +0200 Subject: [PATCH 01/26] First work on connecting with the Docker Engine API --- package-lock.json | 192 +++++++++++++++++- package.json | 2 + src/App.vue | 2 + src/assets/images/docker/docker_available.svg | 64 ++++++ src/assets/images/docker/docker_na.svg | 64 ++++++ src/components/pages/SettingsPage.vue | 157 +++++++++++--- src/components/validation/Rules.ts | 13 ++ .../docker/DockerCommunicator.ts | 25 +++ src/logic/configuration/Configuration.ts | 1 + .../configuration/ConfigurationManager.ts | 15 +- 10 files changed, 500 insertions(+), 35 deletions(-) create mode 100644 src/assets/images/docker/docker_available.svg create mode 100644 src/assets/images/docker/docker_na.svg create mode 100644 src/logic/communication/docker/DockerCommunicator.ts diff --git a/package-lock.json b/package-lock.json index d587b4f7..ba325e10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "unipept-desktop", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "1.2.0", + "version": "1.2.1", "hasInstallScript": true, "dependencies": { "@babel/preset-env": "^7.6.0", "@types/async": "^3.2.3", "@types/better-sqlite3": "^5.4.0", + "@types/dockerode": "^3.2.3", "@types/electron-devtools-installer": "^2.2.0", "@types/follow-redirects": "^1.13.0", "@types/marked": "^2.0.2", @@ -25,6 +26,7 @@ "better-sqlite3": "^7.1.2", "chokidar": "^3.3.1", "core-js": "^2.6.9", + "dockerode": "^3.3.0", "electron-devtools-installer": "^3.1.1", "electron-log": "^4.2.2", "electron-updater": "4.3.4", @@ -1640,6 +1642,14 @@ "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==", "dev": true }, + "node_modules/@types/dockerode": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.2.3.tgz", + "integrity": "sha512-nZRhpSxm3PYianRBcRExcHxDvEzYHUPfGCnRL5Fe4/fSEZbtxrRNJ7okzCans3lXxj2t298EynFHGTnTC2f1Iw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/electron-devtools-installer": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@types/electron-devtools-installer/-/electron-devtools-installer-2.2.0.tgz", @@ -4790,7 +4800,6 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, "dependencies": { "safer-buffer": "~2.1.0" } @@ -5279,7 +5288,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, "dependencies": { "tweetnacl": "^0.14.3" } @@ -8627,6 +8635,56 @@ "buffer-indexof": "^1.0.0" } }, + "node_modules/docker-modem": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.0.tgz", + "integrity": "sha512-WwFajJ8I5geZ/dDZ5FDMDA6TBkWa76xWwGIGw8uzUjNUGCN0to83wJ8Oi1AxrJTC0JBn+7fvIxUctnawtlwXeg==", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^0.8.7" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/docker-modem/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/dockerode": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.3.0.tgz", + "integrity": "sha512-St08lfOjpYCOXEM8XA0VLu3B3hRjtddODphNW5GFoA0AS3JHgoPQKOz0Qmdzg3P+hUPxhb02g1o1Cu1G+U3lRg==", + "dependencies": { + "docker-modem": "^3.0.0", + "tar-fs": "~2.0.1" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dockerode/node_modules/tar-fs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", + "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -18943,6 +19001,11 @@ "node": ">=6.0.0" } }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=" + }, "node_modules/split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -18982,6 +19045,30 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, + "node_modules/ssh2": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz", + "integrity": "sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==", + "dependencies": { + "ssh2-streams": "~0.4.10" + }, + "engines": { + "node": ">=5.2.0" + } + }, + "node_modules/ssh2-streams": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz", + "integrity": "sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==", + "dependencies": { + "asn1": "~0.2.0", + "bcrypt-pbkdf": "^1.0.2", + "streamsearch": "~0.1.2" + }, + "engines": { + "node": ">=5.2.0" + } + }, "node_modules/sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -19121,6 +19208,14 @@ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, + "node_modules/streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/streamsink": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/streamsink/-/streamsink-1.2.0.tgz", @@ -20334,8 +20429,7 @@ "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "node_modules/type-check": { "version": "0.3.2", @@ -25340,6 +25434,14 @@ "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==", "dev": true }, + "@types/dockerode": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.2.3.tgz", + "integrity": "sha512-nZRhpSxm3PYianRBcRExcHxDvEzYHUPfGCnRL5Fe4/fSEZbtxrRNJ7okzCans3lXxj2t298EynFHGTnTC2f1Iw==", + "requires": { + "@types/node": "*" + } + }, "@types/electron-devtools-installer": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@types/electron-devtools-installer/-/electron-devtools-installer-2.2.0.tgz", @@ -28023,7 +28125,6 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, "requires": { "safer-buffer": "~2.1.0" } @@ -28450,7 +28551,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, "requires": { "tweetnacl": "^0.14.3" } @@ -31300,6 +31400,51 @@ "buffer-indexof": "^1.0.0" } }, + "docker-modem": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.0.tgz", + "integrity": "sha512-WwFajJ8I5geZ/dDZ5FDMDA6TBkWa76xWwGIGw8uzUjNUGCN0to83wJ8Oi1AxrJTC0JBn+7fvIxUctnawtlwXeg==", + "requires": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^0.8.7" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "dockerode": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.3.0.tgz", + "integrity": "sha512-St08lfOjpYCOXEM8XA0VLu3B3hRjtddODphNW5GFoA0AS3JHgoPQKOz0Qmdzg3P+hUPxhb02g1o1Cu1G+U3lRg==", + "requires": { + "docker-modem": "^3.0.0", + "tar-fs": "~2.0.1" + }, + "dependencies": { + "tar-fs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", + "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + } + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -39828,6 +39973,11 @@ "chalk": "^2.0.1" } }, + "split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=" + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -39863,6 +40013,24 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, + "ssh2": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz", + "integrity": "sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==", + "requires": { + "ssh2-streams": "~0.4.10" + } + }, + "ssh2-streams": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz", + "integrity": "sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==", + "requires": { + "asn1": "~0.2.0", + "bcrypt-pbkdf": "^1.0.2", + "streamsearch": "~0.1.2" + } + }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -39978,6 +40146,11 @@ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "streamsink": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/streamsink/-/streamsink-1.2.0.tgz", @@ -40957,8 +41130,7 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "type-check": { "version": "0.3.2", diff --git a/package.json b/package.json index 33232bd4..3c3cd9c8 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@babel/preset-env": "^7.6.0", "@types/async": "^3.2.3", "@types/better-sqlite3": "^5.4.0", + "@types/dockerode": "^3.2.3", "@types/electron-devtools-installer": "^2.2.0", "@types/follow-redirects": "^1.13.0", "@types/marked": "^2.0.2", @@ -39,6 +40,7 @@ "better-sqlite3": "^7.1.2", "chokidar": "^3.3.1", "core-js": "^2.6.9", + "dockerode": "^3.3.0", "electron-devtools-installer": "^3.1.1", "electron-log": "^4.2.2", "electron-updater": "4.3.4", diff --git a/src/App.vue b/src/App.vue index 7b816a9d..25b282b5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -78,6 +78,7 @@ import { AssayData, QueueManager } from "unipept-web-components"; +import DockerCommunicator from "@/logic/communication/docker/DockerCommunicator"; const electron = require("electron"); const ipcRenderer = electron.ipcRenderer; @@ -196,6 +197,7 @@ export default class App extends Vue implements ErrorListener { NetworkConfiguration.BASE_URL = config.apiSource; NetworkConfiguration.PARALLEL_API_REQUESTS = config.maxParallelRequests; QueueManager.initializeQueue(config.maxLongRunningTasks); + DockerCommunicator.initializeConnection(JSON.parse(config.dockerConfigurationSettings)); } catch (err) { // TODO: show a proper error message to the user in case this happens console.error(err) diff --git a/src/assets/images/docker/docker_available.svg b/src/assets/images/docker/docker_available.svg new file mode 100644 index 00000000..df538e0d --- /dev/null +++ b/src/assets/images/docker/docker_available.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/docker/docker_na.svg b/src/assets/images/docker/docker_na.svg new file mode 100644 index 00000000..24c5164a --- /dev/null +++ b/src/assets/images/docker/docker_na.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + X + + diff --git a/src/components/pages/SettingsPage.vue b/src/components/pages/SettingsPage.vue index e79b792c..5ed225e1 100644 --- a/src/components/pages/SettingsPage.vue +++ b/src/components/pages/SettingsPage.vue @@ -80,23 +80,97 @@ -

Appearance

+

Docker

- -
Use native titlebar
+ - Forces the application to use the native titlebar on Windows. + This application requires a connection with a local Docker installation + in order to provide custom protein database functionality. If you + currently don't have Docker installed locally, you can download it for + free from the Docker website. + We recommend using Docker Desktop, which automatically comes with + Docker Engine (which in turn is required by this application in order + to allow easy communication with the Docker daemon. - - Changing this option requires you to restart the application. + +
+ + +
Connection settings
+ + Provide a valid configuration that's required to connect to your local + Docker installation. All valid configuration options, that will be + accepted by this application can be found + + here. Please note that the default settings provided by this + application work in most cases, you only need to change this + configuration if no connection to your local Docker installation can be + made.
- - +
+ + + + + + + +
+ Docker availability +
+ + Click to refresh status + +
+
+ + +
+ + Checking connection with Docker +
+
+
+ + +
+
+
+ Architecture: {{ dockerInfo.Architecture }} +
+
+ CPUs available: {{ dockerInfo.NCPU }} +
+
+ Total memory available: + {{ dockerInfo.MemTotal }} bytes + ({{ (dockerInfo.MemTotal / (Math.pow(2,30))).toFixed(2) }} GiB) +
+
+ OS type: + {{ dockerInfo.OSType }} ({{ dockerInfo.KernelVersion }}) +
+
+ Docker server version: + {{ dockerInfo.ServerVersion }} +
+
+ ID: {{ dockerInfo.ID }} +
+
+ + We were unable to connect to your local Docker installation. Please + verify that Docker engine has been properly installed on your + system, that it is currently running and that the configuration + provided above is correct. Remember to check your firewall settings + if Docker seems to be running perfectly, but no connection can be + established. + +
@@ -107,7 +181,7 @@ - + @@ -119,9 +193,8 @@ import Configuration from "./../../logic/configuration/Configuration"; import ConfigurationManager from "./../../logic/configuration/ConfigurationManager"; import { Prop, Watch } from "vue-property-decorator"; import Rules from "./../validation/Rules"; -import VForm from "vuetify"; -import Utils from "@/logic/Utils"; -import { NetworkConfiguration } from "unipept-web-components"; +import { NetworkConfiguration, NetworkUtils } from "unipept-web-components"; +import DockerCommunicator from "@/logic/communication/docker/DockerCommunicator"; @Component export default class SettingsPage extends Vue { @@ -134,8 +207,8 @@ export default class SettingsPage extends Vue { private errorVisible: boolean = false; private errorMessage: string = ""; - private validInputs: boolean = true; - private isWindows: boolean = Utils.isWindows(); + private dockerInfo: any = null; + private dockerInfoLoading: boolean = true; private apiSourceRules: ((x: string) => boolean | string)[] = [ Rules.required, @@ -146,18 +219,25 @@ export default class SettingsPage extends Vue { Rules.required, Rules.integer, Rules.gtZero - ] + ]; private maxRequestRules: ((x: string) => boolean | string)[] = [ Rules.required, Rules.integer, Rules.gtZero, Rules.lteTen + ]; + + private dockerConfigRules: ((x: string) => boolean | string)[] = [ + Rules.required, + Rules.json ] private mounted() { let configManager: ConfigurationManager = new ConfigurationManager(); configManager.readConfiguration().then((result) => this.configuration = result); + + this.retrieveDockerInfo(); } private async beforeDestroy() { @@ -182,16 +262,47 @@ export default class SettingsPage extends Vue { return this.configuration.maxParallelRequests.toString(); } + set dockerConnectionSettings(settings: string) { + this.configuration.dockerConfigurationSettings = settings; + } + + get dockerConnectionSettings(): string { + return this.configuration.dockerConfigurationSettings; + } + @Watch("configuration.apiSource") - @Watch("configuration.useNativeTitlebar") @Watch("configuration.maxLongRunningTasks") @Watch("configuration.maxParallelRequests") + @Watch("configuration.dockerConnectionSettings") private async saveChanges(): Promise { NetworkConfiguration.BASE_URL = this.configuration.apiSource; NetworkConfiguration.PARALLEL_API_REQUESTS = this.configuration.maxParallelRequests; - this.updateStore("setUseNativeTitlebar", this.configuration.useNativeTitlebar); + // Update docker connection } + private updateDockerConnection() { + if (this.dockerConfigRules.every(rule => rule(this.configuration.dockerConfigurationSettings))) { + DockerCommunicator.initializeConnection(JSON.parse(this.configuration.dockerConfigurationSettings)); + this.retrieveDockerInfo(); + } + } + + private async retrieveDockerInfo() { + this.dockerInfoLoading = true; + + const dockerCommunicator = new DockerCommunicator(); + + try { + this.dockerInfo = await dockerCommunicator.getDockerInfo(); + console.log(this.dockerInfo); + } catch (e) { + this.dockerInfo = null; + } + + this.dockerInfoLoading = false; + } + + private async updateStore(method: string, value: any) { this.errorVisible = false; if (this.configuration != null && this.$refs.form && this.$refs.form.validate()) { @@ -215,6 +326,10 @@ export default class SettingsPage extends Vue { this.errorVisible = true; this.errorMessage = message; } + + private openInBrowser(url: string): void { + NetworkUtils.openInBrowser(url); + } } @@ -233,10 +348,4 @@ export default class SettingsPage extends Vue { .settings-category-title:not(:first-child) { margin-top: 32px; } - - .v-progress-circular--indeterminate { - position: relative; - left: 50%; - transform: translateX(-50%); - } diff --git a/src/components/validation/Rules.ts b/src/components/validation/Rules.ts index 4b2a1a7e..c6605492 100644 --- a/src/components/validation/Rules.ts +++ b/src/components/validation/Rules.ts @@ -34,6 +34,19 @@ export default class Rules { return pattern.test(x) || "An invalid URL is provided."; } + /** + * Is the provided string a valid JSON object? + * @param x + */ + public static json: (x: string) => boolean | string = (x: string) => { + try { + JSON.parse(x); + return true; + } catch (e) { + return "The provided value is not a valid JSON object."; + } + } + public static integer: (x: string) => boolean | string = (x: string) => { return Number.isInteger(Number.parseFloat(x)) || "Provided value is not a valid integer."; } diff --git a/src/logic/communication/docker/DockerCommunicator.ts b/src/logic/communication/docker/DockerCommunicator.ts new file mode 100644 index 00000000..0d652bd2 --- /dev/null +++ b/src/logic/communication/docker/DockerCommunicator.ts @@ -0,0 +1,25 @@ +import Dockerode, { DockerOptions } from "dockerode"; + + +export default class DockerCommunicator { + public static readonly UNIX_DEFAULT_SETTINGS = JSON.stringify({ socketPath: "/var/run/docker.sock" }); + public static readonly WINDOWS_DEFAULT_SETTINGS = JSON.stringify({ + protocol: "tcp", + host: "127.0.0.1", + port: 2376 + }); + + public static connection: Dockerode; + + public static initializeConnection(config: DockerOptions) { + DockerCommunicator.connection = new Dockerode(config); + } + + /** + * Checks if Docker is installed and ready to use on this system. This method only returns true if Docker is + * completely ready to be utilized by this application. + */ + public getDockerInfo(): Promise { + return DockerCommunicator.connection.info(); + } +} diff --git a/src/logic/configuration/Configuration.ts b/src/logic/configuration/Configuration.ts index bbf2ee19..25e9ed3f 100644 --- a/src/logic/configuration/Configuration.ts +++ b/src/logic/configuration/Configuration.ts @@ -3,4 +3,5 @@ export default interface Configuration { useNativeTitlebar: boolean; maxLongRunningTasks: number; maxParallelRequests: number; + dockerConfigurationSettings: string; } diff --git a/src/logic/configuration/ConfigurationManager.ts b/src/logic/configuration/ConfigurationManager.ts index 03aac886..05bfa011 100644 --- a/src/logic/configuration/ConfigurationManager.ts +++ b/src/logic/configuration/ConfigurationManager.ts @@ -4,6 +4,8 @@ import Configuration from "./Configuration"; import fs from "fs"; // import { promises as fs } from 'fs'; import { App } from "electron"; +import Utils from "@/logic/Utils"; +import DockerCommunicator from "@/logic/communication/docker/DockerCommunicator"; export default class ConfigurationManager { // The name of the file that's used to store the settings in. @@ -14,7 +16,9 @@ export default class ConfigurationManager { apiSource: "https://unipept.ugent.be", useNativeTitlebar: false, maxLongRunningTasks: 8, - maxParallelRequests: 5 + maxParallelRequests: 5, + dockerConfigurationSettings: + Utils.isWindows() ? DockerCommunicator.WINDOWS_DEFAULT_SETTINGS : DockerCommunicator.UNIX_DEFAULT_SETTINGS }; // Reference to the last configuration that was returned by this manager. Can be used to update the current // configuration and write the changes to disk (without having to read it again). @@ -28,6 +32,15 @@ export default class ConfigurationManager { return Number.isInteger(config.maxParallelRequests) && config.maxParallelRequests <= 10 && config.maxParallelRequests >= 1 + }, + (config: Configuration) => { + // Check if the given config value is a valid JSON object. + try { + JSON.parse(config.dockerConfigurationSettings); + return true; + } catch (e) { + return false; + } } ] From dc407d8cb12805f77bbda43036675d2e71b5a899 Mon Sep 17 00:00:00 2001 From: Pieter Verschaffelt Date: Mon, 28 Jun 2021 09:44:56 +0200 Subject: [PATCH 02/26] Connection with Docker works --- src/components/pages/SettingsPage.vue | 58 ++++++++++++++++++- src/logic/configuration/Configuration.ts | 1 + .../configuration/ConfigurationManager.ts | 9 ++- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/components/pages/SettingsPage.vue b/src/components/pages/SettingsPage.vue index 5ed225e1..409f696d 100644 --- a/src/components/pages/SettingsPage.vue +++ b/src/components/pages/SettingsPage.vue @@ -80,6 +80,35 @@
+

Storage

+ + + + + +
Database storage location
+
+ Indicates where the application should store custom database files? Note + that these files can grow quite large in size, depending on the amount + and size of the custom databases you are planning to use. For large + databases, at least 100GiB of free space is required. +
+
+ + + + +
+
+
+

Docker

@@ -231,7 +260,11 @@ export default class SettingsPage extends Vue { private dockerConfigRules: ((x: string) => boolean | string)[] = [ Rules.required, Rules.json - ] + ]; + + private customDbStorageLocationRules: ((x: string) => boolean | string)[] = [ + Rules.required + ]; private mounted() { let configManager: ConfigurationManager = new ConfigurationManager(); @@ -270,6 +303,14 @@ export default class SettingsPage extends Vue { return this.configuration.dockerConfigurationSettings; } + set customDbStorageLocation(value: string) { + this.configuration.customDbStorageLocation = value; + } + + get customDbStorageLocation(): string { + return this.configuration.customDbStorageLocation; + } + @Watch("configuration.apiSource") @Watch("configuration.maxLongRunningTasks") @Watch("configuration.maxParallelRequests") @@ -294,7 +335,6 @@ export default class SettingsPage extends Vue { try { this.dockerInfo = await dockerCommunicator.getDockerInfo(); - console.log(this.dockerInfo); } catch (e) { this.dockerInfo = null; } @@ -322,6 +362,20 @@ export default class SettingsPage extends Vue { } } + private async openDbStorageFileDialog(): Promise { + const electron = require("electron"); + const { dialog } = electron.remote; + + const chosenPath: Electron.OpenDialogReturnValue | undefined = await dialog.showOpenDialog({ + properties: ["openDirectory"] + }); + + if (chosenPath && chosenPath.filePaths.length > 0) { + this.customDbStorageLocation = chosenPath.filePaths[0]; + console.log("Setting directory path to: " + this.customDbStorageLocation); + } + } + private showError(message: string): void { this.errorVisible = true; this.errorMessage = message; diff --git a/src/logic/configuration/Configuration.ts b/src/logic/configuration/Configuration.ts index 25e9ed3f..059f2b45 100644 --- a/src/logic/configuration/Configuration.ts +++ b/src/logic/configuration/Configuration.ts @@ -4,4 +4,5 @@ export default interface Configuration { maxLongRunningTasks: number; maxParallelRequests: number; dockerConfigurationSettings: string; + customDbStorageLocation: string; } diff --git a/src/logic/configuration/ConfigurationManager.ts b/src/logic/configuration/ConfigurationManager.ts index 05bfa011..fdb6728d 100644 --- a/src/logic/configuration/ConfigurationManager.ts +++ b/src/logic/configuration/ConfigurationManager.ts @@ -7,6 +7,7 @@ import { App } from "electron"; import Utils from "@/logic/Utils"; import DockerCommunicator from "@/logic/communication/docker/DockerCommunicator"; + export default class ConfigurationManager { // The name of the file that's used to store the settings in. private static readonly CONFIG_FILE_NAME = "unipept.config"; @@ -18,7 +19,8 @@ export default class ConfigurationManager { maxLongRunningTasks: 8, maxParallelRequests: 5, dockerConfigurationSettings: - Utils.isWindows() ? DockerCommunicator.WINDOWS_DEFAULT_SETTINGS : DockerCommunicator.UNIX_DEFAULT_SETTINGS + Utils.isWindows() ? DockerCommunicator.WINDOWS_DEFAULT_SETTINGS : DockerCommunicator.UNIX_DEFAULT_SETTINGS, + customDbStorageLocation: "/Volumes/T7/unipept-db" }; // Reference to the last configuration that was returned by this manager. Can be used to update the current // configuration and write the changes to disk (without having to read it again). @@ -34,14 +36,15 @@ export default class ConfigurationManager { config.maxParallelRequests >= 1 }, (config: Configuration) => { - // Check if the given config value is a valid JSON object. + // Check if the given Docker-config value is a valid JSON object. try { JSON.parse(config.dockerConfigurationSettings); return true; } catch (e) { return false; } - } + }, + (config: Configuration) => config.customDbStorageLocation !== "" ] private app: App; From 0e8fc574c8e78da92e6553a00bab2aec918101f9 Mon Sep 17 00:00:00 2001 From: Pieter Verschaffelt Date: Mon, 28 Jun 2021 13:37:04 +0200 Subject: [PATCH 03/26] Create new page with databases overview --- src/components/navigation-drawers/Toolbar.vue | 11 +++ src/components/pages/CustomDatabasePage.vue | 84 +++++++++++++++++++ src/logic/custom_database/CustomDatabase.ts | 12 +++ src/main.ts | 8 ++ 4 files changed, 115 insertions(+) create mode 100644 src/components/pages/CustomDatabasePage.vue create mode 100644 src/logic/custom_database/CustomDatabase.ts diff --git a/src/components/navigation-drawers/Toolbar.vue b/src/components/navigation-drawers/Toolbar.vue index 55ce6f03..abd6478a 100644 --- a/src/components/navigation-drawers/Toolbar.vue +++ b/src/components/navigation-drawers/Toolbar.vue @@ -60,6 +60,17 @@ Single peptide analysis + + + mdi-database-cog + + + Custom databases + + + + + +
+

Custom databases

+ + +
+ Below you can find a list of all custom databases that are currently registered to this + application. To create a new custom database, press the floating button in the lower + right corner. A wizard will guide you through the custom database construction process. + Please note that Docker must be configured correctly in the + settings before new databases can be created. +
+ + + +
+ Create custom database +
+
+
+
+
+
+
+ + + + + diff --git a/src/logic/custom_database/CustomDatabase.ts b/src/logic/custom_database/CustomDatabase.ts new file mode 100644 index 00000000..854513d4 --- /dev/null +++ b/src/logic/custom_database/CustomDatabase.ts @@ -0,0 +1,12 @@ +import { NcbiId } from "unipept-web-components"; + +export default class CustomDatabase { + constructor( + public readonly name: String, + public readonly source: String, + public readonly sourceVersion: String, + public readonly taxa: NcbiId[], + // Amount of entries that are present in this filtered database. + public readonly entries: number + ) {} +} diff --git a/src/main.ts b/src/main.ts index dd31b443..6d7461a1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -40,6 +40,7 @@ import DesktopAssayProcessor from "@/logic/communication/DesktopAssayProcessor"; import PeptideAnalysisPage from "@/components/pages/PeptideAnalysisPage.vue"; import SingleAssayAnalysisPage from "@/components/pages/analysis/SingleAssayAnalysisPage.vue"; +import CustomDatabasePage from "@/components/pages/CustomDatabasePage.vue"; const { app } = require("electron").remote; const bt = require("backtrace-js"); @@ -145,6 +146,13 @@ const routes = [ title: "Tryptic peptide analysis" } }, + { + path: "/databases", + component: CustomDatabasePage, + meta: { + title: "Custom databases" + } + }, { path: "/settings", component: SettingsPage, From 23d91b9ecca137bfed484c688446142babefa3e9 Mon Sep 17 00:00:00 2001 From: Pieter Verschaffelt Date: Tue, 29 Jun 2021 08:22:50 +0200 Subject: [PATCH 04/26] Implement a dialog to create new custom databaes --- .../custom-database/CreateCustomDatabase.vue | 119 ++++++++++++++++++ src/components/pages/CustomDatabasePage.vue | 25 +++- src/components/taxon/TaxaBrowser.vue | 51 ++++++++ 3 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 src/components/custom-database/CreateCustomDatabase.vue create mode 100644 src/components/taxon/TaxaBrowser.vue diff --git a/src/components/custom-database/CreateCustomDatabase.vue b/src/components/custom-database/CreateCustomDatabase.vue new file mode 100644 index 00000000..61767fb3 --- /dev/null +++ b/src/components/custom-database/CreateCustomDatabase.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/src/components/pages/CustomDatabasePage.vue b/src/components/pages/CustomDatabasePage.vue index 059f8ed4..b807a9ce 100644 --- a/src/components/pages/CustomDatabasePage.vue +++ b/src/components/pages/CustomDatabasePage.vue @@ -17,6 +17,9 @@
- Create custom database + + Create custom database +
+ + +
@@ -67,14 +83,19 @@ import Vue from "vue"; import Component from "vue-class-component"; import CustomDatabase from "@/logic/custom_database/CustomDatabase"; +import CreateCustomDatabase from "@/components/custom-database/CreateCustomDatabase.vue"; +import { Tooltip } from "unipept-web-components"; -@Component +@Component({ + components: { CreateCustomDatabase, Tooltip } +}) export default class CustomDatabasePage extends Vue { private databases: CustomDatabase[] = [ new CustomDatabase("DB 1", "TrEMBL", "2021.2", [23, 4589, 129, 12324], 36123), new CustomDatabase("DB 2", "SwissProt", "2020.4", [78123, 23834, 3843, 218723], 38723) ]; + private createDatabaseDialog: boolean = false; } diff --git a/src/components/taxon/TaxaBrowser.vue b/src/components/taxon/TaxaBrowser.vue new file mode 100644 index 00000000..88865d56 --- /dev/null +++ b/src/components/taxon/TaxaBrowser.vue @@ -0,0 +1,51 @@ + + + + + From 914cb6f9fb0f84b25fa68a9460546a30a40c6779 Mon Sep 17 00:00:00 2001 From: Pieter Verschaffelt Date: Wed, 30 Jun 2021 10:06:24 +0200 Subject: [PATCH 05/26] Try to start a new container --- .../docker/DockerCommunicator.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/logic/communication/docker/DockerCommunicator.ts b/src/logic/communication/docker/DockerCommunicator.ts index 0d652bd2..d2b84c25 100644 --- a/src/logic/communication/docker/DockerCommunicator.ts +++ b/src/logic/communication/docker/DockerCommunicator.ts @@ -1,4 +1,5 @@ import Dockerode, { DockerOptions } from "dockerode"; +import { NcbiId } from "unipept-web-components"; export default class DockerCommunicator { @@ -22,4 +23,22 @@ export default class DockerCommunicator { public getDockerInfo(): Promise { return DockerCommunicator.connection.info(); } + + /** + * Starts a new Docker container and begins by building the database with the given settings. + * + * @param databaseSources A list of URLs that should be downloaded and processed (UniProt-compatible xml.gz files). + * @param databaseTypes The database types that correspond to the sources given in the first argument. + * @param taxa A list of taxa ID's by which we should filter (these taxa, as well as their children in the lineage + * tree will be included in the file). + * + */ + public async buildDatabase( + databaseSources: string[], + databaseTypes: string[], + taxa: NcbiId[], + destinationFolder: string + ): Promise { + + } } From fdf565f93b998bae0536f21ccc3d8397d28459c2 Mon Sep 17 00:00:00 2001 From: Pieter Verschaffelt Date: Wed, 30 Jun 2021 15:35:37 +0200 Subject: [PATCH 06/26] Starting the container and filtering output works --- src/components/pages/HomePage.vue | 1 + src/components/pages/SettingsPage.vue | 17 ++++++++ .../docker/DockerCommunicator.ts | 43 +++++++++++++++++-- .../docker/ProgressInspectorStream.ts | 25 +++++++++++ 4 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 src/logic/communication/docker/ProgressInspectorStream.ts diff --git a/src/components/pages/HomePage.vue b/src/components/pages/HomePage.vue index 2cf674f5..3c964e22 100644 --- a/src/components/pages/HomePage.vue +++ b/src/components/pages/HomePage.vue @@ -100,6 +100,7 @@ import UpdateNotesDialog from "@/components/dialogs/UpdateNotesDialog.vue"; import GitHubCommunicator from "@/logic/communication/github/GitHubCommunicator"; import Utils from "@/logic/Utils"; import { OpenDialogOptions } from "electron"; +import DockerCommunicator from "@/logic/communication/docker/DockerCommunicator"; const electron = require("electron"); const { dialog } = electron.remote; diff --git a/src/components/pages/SettingsPage.vue b/src/components/pages/SettingsPage.vue index 409f696d..57ada0dc 100644 --- a/src/components/pages/SettingsPage.vue +++ b/src/components/pages/SettingsPage.vue @@ -335,7 +335,24 @@ export default class SettingsPage extends Vue { try { this.dockerInfo = await dockerCommunicator.getDockerInfo(); + await dockerCommunicator.buildDatabase( + [ + "https://ftp.expasy.org/databases/uniprot/current_release/knowledgebase/complete/uniprot_sprot.xml.gz" + ], + [ + "swissprot" + ], + [ + 33090 + ], + "/Volumes/T7/database/mysql", + "/Volumes/T7/database/index", + (step: string, value: number) => console.log("PROGRESS REPORTED FOR " + step + " WITH VALUE " + value) + ); + + console.log("Run has been completed..."); } catch (e) { + console.error(e); this.dockerInfo = null; } diff --git a/src/logic/communication/docker/DockerCommunicator.ts b/src/logic/communication/docker/DockerCommunicator.ts index d2b84c25..d7019535 100644 --- a/src/logic/communication/docker/DockerCommunicator.ts +++ b/src/logic/communication/docker/DockerCommunicator.ts @@ -1,5 +1,6 @@ import Dockerode, { DockerOptions } from "dockerode"; -import { NcbiId } from "unipept-web-components"; +import { NcbiId, ProgressListener } from "unipept-web-components"; +import ProgressInspectorStream from "@/logic/communication/docker/ProgressInspectorStream"; export default class DockerCommunicator { @@ -31,14 +32,50 @@ export default class DockerCommunicator { * @param databaseTypes The database types that correspond to the sources given in the first argument. * @param taxa A list of taxa ID's by which we should filter (these taxa, as well as their children in the lineage * tree will be included in the file). - * + * @param databaseFolder Folder in which the database under construction should be stored. This folder should be + * empty. + * @param indexFolder Folder in which the constructed database index structure should be kept. + * @param progressListener A callback function that can be used to */ public async buildDatabase( databaseSources: string[], databaseTypes: string[], taxa: NcbiId[], - destinationFolder: string + databaseFolder: string, + indexFolder: string, + progressListener: (step: string, progress: number) => void ): Promise { + if (!DockerCommunicator.connection) { + throw new Error("Connection to Docker daemon has not been initialized."); + } + DockerCommunicator.connection.run( + "pverscha/unipept-custom-db:1.1.1", + [], + new ProgressInspectorStream(progressListener), + { + Env: [ + "MYSQL_ROOT_PASSWORD=unipept", + "MYSQL_DATABASE=unipept", + "MYSQL_USER=unipept", + "MYSQL_PASSWORD=unipept", + `DB_TYPES=${databaseTypes.join(",")}`, + `DB_SOURCES=${databaseSources.join(",")}`, + `FILTER_TAXA=${taxa.join(",")}` + ], + PortBindings: { + "3306/tcp": [{ + HostIP: "0.0.0.0", + HostPort: "3306" + }] + }, + Binds: [ + // Mount the folder in which the MySQL-specific database files will be kept + `${databaseFolder}:/var/lib/mysql`, + // Mount the folder in which the reusable database index structure will be kept + `${indexFolder}:/data` + ] + } + ); } } diff --git a/src/logic/communication/docker/ProgressInspectorStream.ts b/src/logic/communication/docker/ProgressInspectorStream.ts new file mode 100644 index 00000000..2acac10f --- /dev/null +++ b/src/logic/communication/docker/ProgressInspectorStream.ts @@ -0,0 +1,25 @@ +import * as stream from "stream"; + +export default class ProgressInspectorStream extends stream.Writable { + constructor( + private readonly progressReporter: (step: string, progress: number) => void + ) { + super(); + } + + _write(chunk: any, enc: string, callback: any) { + const lines = chunk.toString().split("\n"); + + const progressReports: [string, number][] = lines + .filter((l: string) => l.includes("PROGRESS")) + .map((l: string) => l.split("<->").map((x: string) => x.trim()).slice(1)) + .map((parts: string[]) => [parts[0], Number.parseInt(parts[1])]); + + if (progressReports.length > 0) { + const [step, value] = progressReports[progressReports.length - 1]; + this.progressReporter(step, value); + } + + callback(); + } +} From da47efb5cffce50c7f424de5c5de74fee95c387f Mon Sep 17 00:00:00 2001 From: Pieter Verschaffelt Date: Thu, 1 Jul 2021 14:28:31 +0200 Subject: [PATCH 07/26] Receiving progress from container works now --- .../docker/DockerCommunicator.ts | 68 +++++++++++-------- .../docker/ProgressInspectorStream.ts | 3 +- 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/logic/communication/docker/DockerCommunicator.ts b/src/logic/communication/docker/DockerCommunicator.ts index d7019535..cecf5511 100644 --- a/src/logic/communication/docker/DockerCommunicator.ts +++ b/src/logic/communication/docker/DockerCommunicator.ts @@ -1,6 +1,8 @@ import Dockerode, { DockerOptions } from "dockerode"; import { NcbiId, ProgressListener } from "unipept-web-components"; import ProgressInspectorStream from "@/logic/communication/docker/ProgressInspectorStream"; +import { promises as fs } from "fs"; +import mkdirp from "mkdirp"; export default class DockerCommunicator { @@ -49,33 +51,43 @@ export default class DockerCommunicator { throw new Error("Connection to Docker daemon has not been initialized."); } - DockerCommunicator.connection.run( - "pverscha/unipept-custom-db:1.1.1", - [], - new ProgressInspectorStream(progressListener), - { - Env: [ - "MYSQL_ROOT_PASSWORD=unipept", - "MYSQL_DATABASE=unipept", - "MYSQL_USER=unipept", - "MYSQL_PASSWORD=unipept", - `DB_TYPES=${databaseTypes.join(",")}`, - `DB_SOURCES=${databaseSources.join(",")}`, - `FILTER_TAXA=${taxa.join(",")}` - ], - PortBindings: { - "3306/tcp": [{ - HostIP: "0.0.0.0", - HostPort: "3306" - }] - }, - Binds: [ - // Mount the folder in which the MySQL-specific database files will be kept - `${databaseFolder}:/var/lib/mysql`, - // Mount the folder in which the reusable database index structure will be kept - `${indexFolder}:/data` - ] - } - ); + // Clear the database output folder since this one will be filled up again by rebuilding the database. + await fs.rmdir(databaseFolder, { recursive: true }); + await mkdirp(databaseFolder); + + return new Promise((resolve) => { + DockerCommunicator.connection.run( + "pverscha/unipept-custom-db:1.1.1", + [], + new ProgressInspectorStream(progressListener, () => resolve()), + { + Env: [ + "MYSQL_ROOT_PASSWORD=unipept", + "MYSQL_DATABASE=unipept", + "MYSQL_USER=unipept", + "MYSQL_PASSWORD=unipept", + `DB_TYPES=${databaseTypes.join(",")}`, + `DB_SOURCES=${databaseSources.join(",")}`, + `FILTER_TAXA=${taxa.join(",")}` + ], + PortBindings: { + "3306/tcp": [{ + HostIP: "0.0.0.0", + HostPort: "3306" + }] + }, + Binds: [ + // Mount the folder in which the MySQL-specific database files will be kept + `${databaseFolder}:/var/lib/mysql`, + // Mount the folder in which the reusable database index structure will be kept + `${indexFolder}:/data` + ] + } + ); + }); + } + + public async startDatabase(databaseLocation: string): Promise { + } } diff --git a/src/logic/communication/docker/ProgressInspectorStream.ts b/src/logic/communication/docker/ProgressInspectorStream.ts index 2acac10f..fd355334 100644 --- a/src/logic/communication/docker/ProgressInspectorStream.ts +++ b/src/logic/communication/docker/ProgressInspectorStream.ts @@ -2,7 +2,8 @@ import * as stream from "stream"; export default class ProgressInspectorStream extends stream.Writable { constructor( - private readonly progressReporter: (step: string, progress: number) => void + private readonly progressReporter: (step: string, progress: number) => void, + private readonly onReadyListener: () => void ) { super(); } From 68d224468cba0c83b4498c2ad13ffbac040d6ed8 Mon Sep 17 00:00:00 2001 From: Pieter Verschaffelt Date: Tue, 6 Jul 2021 08:50:17 +0200 Subject: [PATCH 08/26] Build placeholders for creating custom databases --- .../custom-database/CreateCustomDatabase.vue | 66 ++++++-- src/components/pages/CustomDatabasePage.vue | 144 +++++++++++------- src/components/pages/SettingsPage.vue | 17 --- src/components/taxon/TaxaBrowser.vue | 13 +- .../docker/DockerCommunicator.ts | 2 +- .../ncbi/CachedNcbiResponseCommunicator.ts | 8 + src/logic/custom_database/CustomDatabase.ts | 6 +- src/main.ts | 5 +- src/state/DockerStore.ts | 113 ++++++++++++++ 9 files changed, 273 insertions(+), 101 deletions(-) create mode 100644 src/state/DockerStore.ts diff --git a/src/components/custom-database/CreateCustomDatabase.vue b/src/components/custom-database/CreateCustomDatabase.vue index 61767fb3..bc69d8eb 100644 --- a/src/components/custom-database/CreateCustomDatabase.vue +++ b/src/components/custom-database/CreateCustomDatabase.vue @@ -22,10 +22,22 @@ - + +
General settings
+
+
+ + + + + + - - + + + - - - - A UniProt mirror that's located in the EU is used by default. Tick this box if you're - located closer to the US. - + + + - Filter +
Filter
Please select a range of taxa that should be included in this custom database.
@@ -58,7 +78,7 @@
Cancel - Continue + Continue
@@ -82,7 +102,8 @@ export default class CreateCustomDatabase extends Vue { "TrEMBL", "SwissProt" ]; - private useUsMirror: boolean = false; + private databaseName: string = ""; + private mirrors: string[] = ["EU (EBI)", "US (UniProt)"]; // All database versions of UniPept that are currently available private versions: String[] = []; @@ -111,9 +132,26 @@ export default class CreateCustomDatabase extends Vue { this.error = true; } } + + private async buildDatabase(): Promise { + this.$store.dispatch( + "buildDatabase", + [ + this.databaseName, + ["https://ftp.expasy.org/databases/uniprot/current_release/knowledgebase/complete/uniprot_sprot.xml.gz"], + ["swissprot"], + [33090], + "/Volumes/T7/database/mysql", + "/Volumes/T7/database/index", + ] + ); + } } diff --git a/src/components/pages/CustomDatabasePage.vue b/src/components/pages/CustomDatabasePage.vue index b807a9ce..73c89348 100644 --- a/src/components/pages/CustomDatabasePage.vue +++ b/src/components/pages/CustomDatabasePage.vue @@ -13,56 +13,47 @@ Please note that Docker must be configured correctly in the settings before new databases can be created. - - @@ -15,50 +41,92 @@ import Component from "vue-class-component"; import { DefaultCommunicationSource, NcbiId, NcbiOntologyProcessor, NcbiTaxon, Ontology } from "unipept-web-components"; import CachedCommunicationSource from "@/logic/communication/source/CachedCommunicationSource"; import CachedNcbiResponseCommunicator from "@/logic/communication/taxonomic/ncbi/CachedNcbiResponseCommunicator"; -import { Watch } from "vue-property-decorator"; +import { Prop, Watch } from "vue-property-decorator"; +import { DataOptions } from "vuetify"; @Component export default class TaxaBrowser extends Vue { private headers = [ + { + text: "", + align: "left", + value: "action", + width: "2%", + sortable: false + }, { text: "Taxon ID", align: "start", - value: "id" + value: "id", + width: "15%" }, { text: "Name", align: "start", - value: "name" + value: "name", + width: "45%" }, { text: "Rank", align: "start", - value: "rank" + value: "rank", + width: "38%" } - ] + ]; - private ncbis: NcbiId[] = []; + private search: string = ""; + + private taxaCount: number = 0; private taxa: NcbiTaxon[] = []; private loading: boolean = true; - private options = {}; + // @ts-ignore + private options: DataOptions = {}; + private ncbiCommunicator: CachedNcbiResponseCommunicator; private ncbiOntologyProcessor: NcbiOntologyProcessor; + private searchDebounceTimer: NodeJS.Timeout; + + private selectedItems: NcbiTaxon[] = []; + private mounted() { this.loading = true; - this.retrieveAllTaxa(); + this.ncbiCommunicator = new CachedNcbiResponseCommunicator(); + this.ncbiOntologyProcessor = new NcbiOntologyProcessor(this.ncbiCommunicator); + this.taxaCount = this.ncbiCommunicator.getNcbiCount(); this.loading = false; } + @Watch("search") + private async onSearchChanged(): Promise { + if (this.searchDebounceTimer) { + clearTimeout(this.searchDebounceTimer); + } + + this.searchDebounceTimer = setTimeout(() => { + this.taxaCount = this.ncbiCommunicator.getNcbiCount(this.search); + this.searchDebounceTimer = null; + }, 500); + } + @Watch("options") + @Watch("search") private async onOptionsChanged(): Promise { - if (this.ncbiOntologyProcessor && this.ncbis) { + if (this.ncbiOntologyProcessor && !this.loading) { const { sortBy, sortDesc, page, itemsPerPage } = this.options; + const ncbis = this.ncbiCommunicator.getNcbiRange( + itemsPerPage * (page - 1), + itemsPerPage * page, + this.search, + sortBy.length > 0 ? sortBy[0] : undefined, + sortDesc.length > 0 ? sortDesc[0] : undefined + ); + const ontology: Ontology = await this.ncbiOntologyProcessor.getOntologyByIds( - this.ncbis.slice(itemsPerPage * (page - 1), itemsPerPage * page), false + ncbis, false ); this.taxa.splice(0, this.taxa.length); @@ -66,12 +134,18 @@ export default class TaxaBrowser extends Vue { } } - private async retrieveAllTaxa(): Promise { - const ncbiManager = new CachedNcbiResponseCommunicator(); - for (const item of ncbiManager.getAllNcbiIds()) { - this.ncbis.push(item); + @Watch("selectedItems") + private onSelectedItemsChanged(): void { + this.$emit("input", this.selectedItems); + } + + private selectItem(item: NcbiTaxon): void { + const idx = this.selectedItems.findIndex(element => element.id === item.id); + if (idx === -1) { + this.selectedItems.push(item); + } else { + this.selectedItems.splice(idx, 1); } - this.ncbiOntologyProcessor = new NcbiOntologyProcessor(ncbiManager); } } diff --git a/src/logic/communication/static/StaticDatabaseManager.ts b/src/logic/communication/static/StaticDatabaseManager.ts index d8d29457..62587241 100644 --- a/src/logic/communication/static/StaticDatabaseManager.ts +++ b/src/logic/communication/static/StaticDatabaseManager.ts @@ -177,7 +177,7 @@ export default class StaticDatabaseManager { */ public getDatabase(): sqlite3.Database { if (!StaticDatabaseManager.db) { - StaticDatabaseManager.db = new sqlite3(this.getDatabasePath()); + StaticDatabaseManager.db = new sqlite3(this.getDatabasePath(), { verbose: console.log }); } return StaticDatabaseManager.db; diff --git a/src/logic/communication/taxonomic/ncbi/CachedNcbiResponseCommunicator.ts b/src/logic/communication/taxonomic/ncbi/CachedNcbiResponseCommunicator.ts index 3a28d6d2..1a7fc0a5 100644 --- a/src/logic/communication/taxonomic/ncbi/CachedNcbiResponseCommunicator.ts +++ b/src/logic/communication/taxonomic/ncbi/CachedNcbiResponseCommunicator.ts @@ -31,17 +31,19 @@ export default class CachedNcbiResponseCommunicator extends NcbiResponseCommunic ); for (const id of codes) { - const row = extractStmt.get(id); - if (row) { - const lineage = ranks.map(rank => row[rank]).map(el => el === "\\N" ? null : el); - lineagesToExtract.push(...lineage); + if (id) { + const row = extractStmt.get(id); + if (row) { + const lineage = ranks.map(rank => row[rank]).map(el => el === "\\N" ? null : el); + lineagesToExtract.push(...lineage); - CachedNcbiResponseCommunicator.codesProcessed.set(id, { - id: row.id, - name: row.name, - rank: row.rank, - lineage: lineage - }); + CachedNcbiResponseCommunicator.codesProcessed.set(id, { + id: row.id, + name: row.name, + rank: row.rank, + lineage: lineage + }); + } } } @@ -52,16 +54,18 @@ export default class CachedNcbiResponseCommunicator extends NcbiResponseCommunic for (const id of lineagesToExtract.filter( (c) => !CachedNcbiResponseCommunicator.codesProcessed.has(c) )) { - const row = extractStmt.get(id); - if (row) { - const lineage = ranks.map(rank => row[rank]).map(el => el === "\\N" ? null : el); + if (id) { + const row = extractStmt.get(id); + if (row) { + const lineage = ranks.map(rank => row[rank]).map(el => el === "\\N" ? null : el); - CachedNcbiResponseCommunicator.codesProcessed.set(id, { - id: row.id, - name: row.name, - rank: row.rank, - lineage: lineage - }); + CachedNcbiResponseCommunicator.codesProcessed.set(id, { + id: row.id, + name: row.name, + rank: row.rank, + lineage: lineage + }); + } } } } @@ -81,9 +85,45 @@ export default class CachedNcbiResponseCommunicator extends NcbiResponseCommunic return CachedNcbiResponseCommunicator.codesProcessed; } - public getAllNcbiIds(): NcbiId[] { + /** + * Returns the amount of NCBI taxa that are known to the database underlying the application. An optional filter + * string can be given that allows the database to be filtered by all taxa that contain a specific text in their + * name. + * + * @param nameFilter A portion of text that should be present in the name of all taxa that are returned by this + * function. + */ + public getNcbiCount(nameFilter: string = ""): number { + return this.db.prepare("SELECT COUNT(id) FROM taxons WHERE name LIKE ?") + .get(`%${nameFilter}%`)["COUNT(id)"]; + } + + /** + * Returns a slice of all NCBI id's from the database starting from row number start (inclusive) and ending at end + * (exclusive). Note that if a specific name filter is given, only taxa that contain this text as portion of their + * name will be returned. + * + * @param start First NCBI id that should be included in the result (inclusive). + * @param end First NCBI id that should not be included in the result (exclusive). + * @param nameFilter A portion of text that should be present in the name of all taxa that are returned by this + * function. + * @param sortBy Which taxon property should be used to sort the table? + * @param sortDescending Sort according to ascending or descending values in the selected column? + */ + public getNcbiRange( + start: number, + end: number, + nameFilter: string = "", + sortBy: "id" | "name" | "rank" = "id", + sortDescending: boolean = true + ): NcbiId[] { + console.log("sortBy: " + sortBy); + console.log("sortDescending: " + sortDescending); + return this.db.prepare( - "SELECT id FROM taxons" - ).all().map(item => item.id); + `SELECT id, name, rank FROM taxons WHERE name LIKE ? ORDER BY ${sortBy} ${ sortDescending ? "ASC": "DESC" } LIMIT ? OFFSET ?` + ) + .all(`%${nameFilter}%`, end - start, start) + .map(item => item.id); } } diff --git a/src/logic/filesystem/docker/CustomDatabaseManager.ts b/src/logic/filesystem/docker/CustomDatabaseManager.ts index 2f6fd801..f570e043 100644 --- a/src/logic/filesystem/docker/CustomDatabaseManager.ts +++ b/src/logic/filesystem/docker/CustomDatabaseManager.ts @@ -82,6 +82,35 @@ export default class CustomDatabaseManager { return fs.writeFile(path, JSON.stringify(db)); } + /** + * Returns the most appropriate URL that can be used to download the specific UniProt database. This function will + * automatically take into account the current location of the user and point the user to the closest FTP mirror. + * Note that location detection of the user is very rudimentary and only provides a rough estimate of the user's + * current point of residence (e.g. Europe, United States, ...). + * + * @param dbVersionId A valid version identifier in the format YYYY.MM (that should point to an existing UniProt + * version) or "current" for the most recent version of the database. + */ + public getUrl( + dbVersionId: string + ): string { + if (dbVersionId.toLowerCase() === "current") { + // The user wants to download the most recent version of the UniProt database and we should check the user's + // current location (e.g. Europe, United States, Africa, ...) to determine what the most appropriate + // mirror is. + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + + // if (timezone && timezone.toLowerCase().includes("europe")) { + // // The user is situated in Europe and we should use Expasy as our default FTP server. + // return + // } else { + // return + // } + } + + return ""; + } + /** * Constructs the path to a folder wherein the database's metadata should be stored. * From b967b4f479b0bba47672b504bc8456bda09e7aa2 Mon Sep 17 00:00:00 2001 From: Pieter Verschaffelt Date: Mon, 13 Sep 2021 13:08:31 +0200 Subject: [PATCH 12/26] Building databases from the app is more polished --- src/App.vue | 2 +- .../custom-database/CreateCustomDatabase.vue | 47 ++++++++++++++----- src/logic/application/BootstrapApplication.ts | 8 +++- .../docker/DockerCommunicator.ts | 9 ++++ .../docker/ProgressInspectorStream.ts | 2 +- .../docker/CustomDatabaseManager.ts | 34 +++++++------- src/state/DockerStore.ts | 35 +++++++++++--- 7 files changed, 98 insertions(+), 39 deletions(-) diff --git a/src/App.vue b/src/App.vue index 6004eb1d..7dc29577 100644 --- a/src/App.vue +++ b/src/App.vue @@ -194,7 +194,7 @@ export default class App extends Vue implements ErrorListener { this.loading = true; let configurationManager = new ConfigurationManager(); try { - const appBootstrap = new BootstrapApplication(); + const appBootstrap = new BootstrapApplication(this.$store); appBootstrap.loadApplicationComponents(); } catch (err) { // TODO: show a proper error message to the user in case this happens diff --git a/src/components/custom-database/CreateCustomDatabase.vue b/src/components/custom-database/CreateCustomDatabase.vue index ae5956d4..07d9f041 100644 --- a/src/components/custom-database/CreateCustomDatabase.vue +++ b/src/components/custom-database/CreateCustomDatabase.vue @@ -81,7 +81,7 @@ label="UniProt mirror" :items="mirrors" :rules="[value => !! value || 'You must select a UniProt mirror']" - :v-model="selectedMirror" + v-model="selectedMirror" persistent-hint hint="Select the mirror that's closest to your physical location to help speed up the download process."> @@ -201,6 +201,7 @@ import { } from "unipept-web-components"; import { Prop, Watch } from "vue-property-decorator"; import CachedNcbiResponseCommunicator from "@/logic/communication/taxonomic/ncbi/CachedNcbiResponseCommunicator"; +import ConfigurationManager from "@/logic/configuration/ConfigurationManager"; @Component({ components: { TaxaBrowser } }) @@ -221,12 +222,12 @@ export default class CreateCustomDatabase extends Vue { private databaseName: string = ""; - private mirrors: string[] = ["EU (EBI)", "US (UniProt)"]; - private selectedMirror: string = "EU (EBI)"; + private mirrors: string[] = ["UK (EBI)", "EU (Expasy)", "US (UniProt)"]; + private selectedMirror: string = "EU (Expasy)"; // All database versions of UniPept that are currently available private versions: String[] = []; - private selectedVersion: string = "current"; + private selectedVersion: string = "Current"; private selectedTaxa: NcbiTaxon[] = []; @@ -234,6 +235,7 @@ export default class CreateCustomDatabase extends Vue { private async mounted() { this.onValueChanged(); + this.selectedMirror = this.getMostSuitableMirror(); await this.retrieveUniProtVersions(); } @@ -265,28 +267,49 @@ export default class CreateCustomDatabase extends Vue { } private validateAndContinue(): void { - // if (this.$refs.databaseForm.validate()) { - this.currentStep = 2; - // } + if (this.$refs.databaseForm.validate()) { + this.currentStep = 2; + } } private async buildDatabase(): Promise { + const sourceUrlMap = { + "TrEMBL": "https://ftp.expasy.org/databases/uniprot/current_release/knowledgebase/complete/uniprot_trembl.xml.gz", + "SwissProt": "https://ftp.expasy.org/databases/uniprot/current_release/knowledgebase/complete/uniprot_sprot.xml.gz" + } + const configManager = new ConfigurationManager(); this.$store.dispatch( "buildDatabase", [ this.databaseName, - ["https://ftp.expasy.org/databases/uniprot/current_release/knowledgebase/complete/uniprot_sprot.xml.gz"], - ["swissprot"], - [33090], - "/Volumes/T7/database/mysql", - "/Volumes/T7/database/index", + this.selectedSources.map(source => sourceUrlMap[source]), + this.selectedSources, + this.selectedTaxa.map(taxon => taxon.id), + await configManager.readConfiguration() ] ); this.dialogActive = false; } + /** + * This function checks the user's current timezone settings and uses this information to determine what the most + * suitable mirror is that can be used for his present location. Note that this is only an estimate. + */ + private getMostSuitableMirror(): string { + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + + if (timezone.includes("London") || timezone.includes("Dublin")) { + // Return UK-based mirror + return "UK (EBI)"; + } else if (timezone.includes("Europe")) { + return "EU (Expasy)"; + } else { + return "US (UniProt)"; + } + } + @Watch("value") private onValueChanged() { this.dialogActive = this.value; diff --git a/src/logic/application/BootstrapApplication.ts b/src/logic/application/BootstrapApplication.ts index a160128a..5b9bebbe 100644 --- a/src/logic/application/BootstrapApplication.ts +++ b/src/logic/application/BootstrapApplication.ts @@ -47,7 +47,11 @@ export default class BootstrapApplication { private async initializeCustomDatabases(config: Configuration): Promise { const customDatabaseManager = new CustomDatabaseManager(); - const dbs = await customDatabaseManager.listAllIncompleteDatabases(config.customDbStorageLocation); - this.store.dispatch("customDatabase/initializeQueue", dbs); + + const completeDbs = await customDatabaseManager.listAllBuildDatabases(config.customDbStorageLocation); + this.store.dispatch("initializeReadyDatabases", completeDbs) + + const incompleteDbs = await customDatabaseManager.listAllIncompleteDatabases(config.customDbStorageLocation); + this.store.dispatch("initializeQueue", [incompleteDbs, config]); } } diff --git a/src/logic/communication/docker/DockerCommunicator.ts b/src/logic/communication/docker/DockerCommunicator.ts index 788462e6..2f119025 100644 --- a/src/logic/communication/docker/DockerCommunicator.ts +++ b/src/logic/communication/docker/DockerCommunicator.ts @@ -5,6 +5,8 @@ import { promises as fs } from "fs"; import mkdirp from "mkdirp"; import CustomDatabase from "@/logic/custom_database/CustomDatabase"; import StringNotifierInspectorStream from "@/logic/communication/docker/StringNotifierInspectorStream"; +import CustomDatabaseManager from "@/logic/filesystem/docker/CustomDatabaseManager"; +import ConfigurationManager from "@/logic/configuration/ConfigurationManager"; export default class DockerCommunicator { @@ -95,6 +97,13 @@ export default class DockerCommunicator { ); }); + // Write the metadata JSON file. + const configMng = new ConfigurationManager(); + const config = await configMng.readConfiguration(); + + const customManager = new CustomDatabaseManager(); + customManager.updateMetadata(config.customDbStorageLocation, customDb); + // Now, stop this container const buildContainer = await this.getContainerByName(DockerCommunicator.BUILD_DB_CONTAINER_NAME); return new Promise( diff --git a/src/logic/communication/docker/ProgressInspectorStream.ts b/src/logic/communication/docker/ProgressInspectorStream.ts index 27e9d215..916c7a5f 100644 --- a/src/logic/communication/docker/ProgressInspectorStream.ts +++ b/src/logic/communication/docker/ProgressInspectorStream.ts @@ -23,7 +23,7 @@ export default class ProgressInspectorStream extends stream.Writable { // If this text appears in the Docker logs, we now that the server has been started. if ( - chunk.toString().contains( + chunk.toString().includes( "No existing UUID has been found, so we assume that this is the first time that this server " + "has been started." ) diff --git a/src/logic/filesystem/docker/CustomDatabaseManager.ts b/src/logic/filesystem/docker/CustomDatabaseManager.ts index f570e043..27ebc7d2 100644 --- a/src/logic/filesystem/docker/CustomDatabaseManager.ts +++ b/src/logic/filesystem/docker/CustomDatabaseManager.ts @@ -26,23 +26,25 @@ export default class CustomDatabaseManager { public async listAllDatabases(dbRootFolder: string): Promise { const databases = []; for (const dir of (await fs.readdir(path.join(dbRootFolder, "databases")))) { - // Check if a metadata file is present in the folder that was found. If it is present, we should read the - // database name and other metadata from this file. - const metadata = JSON.parse(await fs.readFile( - path.join(dbRootFolder, "databases", dir, "metadata.json"), - { encoding: "utf-8" } - )); + if ((await fs.lstat(path.join(dbRootFolder, "databases", dir))).isDirectory()) { + // Check if a metadata file is present in the folder that was found. If it is present, we should read + // the database name and other metadata from this file. + const metadata = JSON.parse(await fs.readFile( + path.join(dbRootFolder, "databases", dir, "metadata.json"), + { encoding: "utf-8" } + )); - databases.push( - new CustomDatabase( - metadata.name, - metadata.sources, - metadata.sourceTypes, - metadata.taxa, - metadata.entries, - metadata.complete - ) - ) + databases.push( + new CustomDatabase( + metadata.name, + metadata.sources, + metadata.sourceTypes, + metadata.taxa, + metadata.entries, + metadata.complete + ) + ); + } } return databases; } diff --git a/src/state/DockerStore.ts b/src/state/DockerStore.ts index 308a0708..4e034f4f 100644 --- a/src/state/DockerStore.ts +++ b/src/state/DockerStore.ts @@ -114,6 +114,22 @@ const databaseMutations: MutationTree = { inProgress: boolean ) { state.constructionInProgress = inProgress; + }, + + ADD_TO_DB_LIST( + state: CustomDatabaseState, + list: CustomDatabase[] + ) { + for (const db of list) { + state.databases.push({ + database: db, + progress: { + value: 1, + step: "" + }, + ready: true + }); + } } } @@ -127,15 +143,13 @@ const databaseActions: ActionTree = { databaseSources, databaseTypes, taxa, - databaseFolder, - indexFolder + configuration ]: [ string, string[], string[], NcbiId[], - string, - string + Configuration ] ) { // TODO count exact number of entries that will be present in this database. @@ -155,8 +169,8 @@ const databaseActions: ActionTree = { const dockerCommunicator = new DockerCommunicator(); await dockerCommunicator.buildDatabase( customDb, - databaseFolder, - indexFolder, + path.join(configuration.customDbStorageLocation, "databases", dbName), + path.join(configuration.customDbStorageLocation, "index"), (step, value) => { store.commit("UPDATE_DATABASE_PROGRESS", [customDb, step, value]); } @@ -171,7 +185,7 @@ const databaseActions: ActionTree = { store: ActionContext, [queue, configuration]: [CustomDatabase[], Configuration] ) { - store.commit("INITIALIZE_QUEUE", [queue]); + store.commit("INITIALIZE_QUEUE", queue); setTimeout( async() => { @@ -199,6 +213,13 @@ const databaseActions: ActionTree = { }, 1000 ); + }, + + initializeReadyDatabases( + store: ActionContext, + dbList: CustomDatabase[] + ) { + store.commit("ADD_TO_DB_LIST", dbList); } } From 4da8af5faac3cc3d67476890f097da12e466404b Mon Sep 17 00:00:00 2001 From: Pieter Verschaffelt Date: Tue, 14 Sep 2021 09:45:42 +0200 Subject: [PATCH 13/26] Databases are now correctly build and cleaned up --- src/background.ts | 2 +- src/components/pages/CustomDatabasePage.vue | 13 +++- src/components/taxon/TaxaBrowser.vue | 2 +- src/logic/application/BootstrapApplication.ts | 4 ++ .../docker/DockerCommunicator.ts | 7 +-- .../static/StaticDatabaseManager.ts | 2 +- .../ncbi/CachedNcbiResponseCommunicator.ts | 8 +-- .../docker/CustomDatabaseManager.ts | 33 +++++----- src/state/DockerStore.ts | 60 ++++++++++--------- 9 files changed, 73 insertions(+), 58 deletions(-) diff --git a/src/background.ts b/src/background.ts index 533d89ac..d75b7dd9 100644 --- a/src/background.ts +++ b/src/background.ts @@ -43,7 +43,7 @@ async function createWindow() { let options = { width: 1200, height: 1000, - webPreferences: { nodeIntegration: true, enableRemoteModule: true }, + webPreferences: { nodeIntegration: true, nodeIntegrationInWorker: true, enableRemoteModule: true }, show: false }; diff --git a/src/components/pages/CustomDatabasePage.vue b/src/components/pages/CustomDatabasePage.vue index d14a864f..7a781332 100644 --- a/src/components/pages/CustomDatabasePage.vue +++ b/src/components/pages/CustomDatabasePage.vue @@ -36,10 +36,17 @@ v-slot:expanded-item="{ headers, item }">
-
- +
+ + mdi-check + +
+ This custom database has been constructed successfully and can be + used as part of an analysis. Head over to the analysis page and + open up a new sample to get started. +
-
+
= new Map(); + private static staticDbProgress: Promise; + private static worker: Worker; + constructor() { super(); try { @@ -117,13 +120,10 @@ export default class CachedNcbiResponseCommunicator extends NcbiResponseCommunic sortBy: "id" | "name" | "rank" = "id", sortDescending: boolean = true ): NcbiId[] { - console.log("sortBy: " + sortBy); - console.log("sortDescending: " + sortDescending); - return this.db.prepare( `SELECT id, name, rank FROM taxons WHERE name LIKE ? ORDER BY ${sortBy} ${ sortDescending ? "ASC": "DESC" } LIMIT ? OFFSET ?` ) .all(`%${nameFilter}%`, end - start, start) - .map(item => item.id); + .map((item: any) => item.id); } } diff --git a/src/logic/filesystem/docker/CustomDatabaseManager.ts b/src/logic/filesystem/docker/CustomDatabaseManager.ts index 27ebc7d2..3a4fdfeb 100644 --- a/src/logic/filesystem/docker/CustomDatabaseManager.ts +++ b/src/logic/filesystem/docker/CustomDatabaseManager.ts @@ -29,21 +29,26 @@ export default class CustomDatabaseManager { if ((await fs.lstat(path.join(dbRootFolder, "databases", dir))).isDirectory()) { // Check if a metadata file is present in the folder that was found. If it is present, we should read // the database name and other metadata from this file. - const metadata = JSON.parse(await fs.readFile( - path.join(dbRootFolder, "databases", dir, "metadata.json"), - { encoding: "utf-8" } - )); + try { + const metadata = JSON.parse(await fs.readFile( + path.join(dbRootFolder, "databases", dir, "metadata.json"), + { encoding: "utf-8" } + )); - databases.push( - new CustomDatabase( - metadata.name, - metadata.sources, - metadata.sourceTypes, - metadata.taxa, - metadata.entries, - metadata.complete - ) - ); + databases.push( + new CustomDatabase( + metadata.name, + metadata.sources, + metadata.sourceTypes, + metadata.taxa, + metadata.entries, + metadata.complete + ) + ); + } catch (e) { + // The inspected directory probably doesn't contain a database and we should do nothing in this + // case. + } } } return databases; diff --git a/src/state/DockerStore.ts b/src/state/DockerStore.ts index 4e034f4f..f70ebdb2 100644 --- a/src/state/DockerStore.ts +++ b/src/state/DockerStore.ts @@ -6,6 +6,7 @@ import Vue from "vue"; import { queue } from "async"; import Configuration from "@/logic/configuration/Configuration"; import * as path from "path"; +import CustomDatabaseManager from "@/logic/filesystem/docker/CustomDatabaseManager"; type CustomDatabaseInfo = { database: CustomDatabase @@ -163,20 +164,11 @@ const databaseActions: ActionTree = { store.commit("ADD_DATABASE", customDb); - if (!store.getters.constructionInProgress) { - store.commit("UPDATE_CONSTRUCTION_STATUS", true); - - const dockerCommunicator = new DockerCommunicator(); - await dockerCommunicator.buildDatabase( - customDb, - path.join(configuration.customDbStorageLocation, "databases", dbName), - path.join(configuration.customDbStorageLocation, "index"), - (step, value) => { - store.commit("UPDATE_DATABASE_PROGRESS", [customDb, step, value]); - } - ); + const customDbMng = new CustomDatabaseManager(); + customDbMng.updateMetadata(configuration.customDbStorageLocation, customDb); - store.commit("UPDATE_DATABASE_STATUS", [customDb, true]); + if (!store.getters.constructionInProgress) { + await startDatabaseConstruction(store, customDb, configuration); } } }, @@ -185,6 +177,9 @@ const databaseActions: ActionTree = { store: ActionContext, [queue, configuration]: [CustomDatabase[], Configuration] ) { + for (const db of queue) { + store.commit("ADD_DATABASE", db); + } store.commit("INITIALIZE_QUEUE", queue); setTimeout( @@ -193,21 +188,7 @@ const databaseActions: ActionTree = { // If no databases are currently being constructed, and at least one database is waiting in the // queue, we should start the construction process for this database. if (store.getters.queue.length > 0) { - store.commit("UPDATE_CONSTRUCTION_STATUS", true); - - const customDb = store.getters.queue[0]; - - const dockerCommunicator = new DockerCommunicator(); - await dockerCommunicator.buildDatabase( - customDb, - path.join(configuration.customDbStorageLocation, "databases"), - path.join(configuration.customDbStorageLocation, "index"), - (step, value) => { - store.commit("UPDATE_DATABASE_PROGRESS", [customDb, step, value]); - } - ); - - store.commit("UPDATE_CONSTRUCTION_STATUS", false); + await startDatabaseConstruction(store, store.getters.queue[0], configuration); } } }, @@ -223,6 +204,29 @@ const databaseActions: ActionTree = { } } +const startDatabaseConstruction = async function( + store: ActionContext, + customDb: CustomDatabase, + configuration: Configuration +) { + store.commit("UPDATE_CONSTRUCTION_STATUS", true); + + const dockerCommunicator = new DockerCommunicator(); + await dockerCommunicator.buildDatabase( + customDb, + path.join(configuration.customDbStorageLocation, "databases", customDb.name), + path.join(configuration.customDbStorageLocation, "index"), + (step, value) => { + store.commit("UPDATE_DATABASE_PROGRESS", [customDb, step, value]); + } + ); + + const customManager = new CustomDatabaseManager(); + customManager.updateMetadata(configuration.customDbStorageLocation, customDb); + + store.commit("UPDATE_DATABASE_STATUS", [customDb, true]); +} + export const customDatabaseStore = { state: databaseState, getters: databaseGetters, From c9f9958883466f89399a950d814579021b0f0929 Mon Sep 17 00:00:00 2001 From: Pieter Verschaffelt Date: Tue, 19 Oct 2021 08:18:40 +0200 Subject: [PATCH 14/26] Reworked creation of assays --- link_with_web_components_watch.js | 23 +- package-lock.json | 30 +- package.json | 2 +- src/App.vue | 16 +- src/components/analysis/AnalysisSummary.vue | 35 +- .../analysis/PeptideSummaryTable.vue | 8 +- src/components/assay/AnalysisSourceSelect.vue | 66 ++ src/components/assay/CreateAssay.vue | 56 -- src/components/assay/CreateAssayDialog.vue | 590 ++++++++++++++++++ .../custom-database/CreateCustomDatabase.vue | 4 +- .../CustomDatabaseProgressReport.vue | 77 +++ .../navigation-drawers/AssayItem.vue | 40 +- .../navigation-drawers/ProjectExplorer.vue | 109 +++- .../navigation-drawers/StudyItem.vue | 75 +-- src/components/navigation-drawers/Toolbar.vue | 69 -- src/components/pages/CustomDatabasePage.vue | 18 +- src/components/pages/PeptideAnalysisPage.vue | 4 +- src/components/pages/SettingsPage.vue | 2 +- .../pages/analysis/AnalysisPage.vue | 249 ++++---- .../analysis/ComparativeAnalysisPage.vue | 4 +- src/components/taxon/TaxaBrowser.vue | 4 +- src/db/migrations/v2_to_v3.sql | 24 + src/db/schemas/schema_v2.sql | 44 +- src/db/schemas/schema_v3.sql | 57 ++ src/logic/application/BootstrapApplication.ts | 9 +- .../communication/DesktopAssayProcessor.ts | 140 +++-- .../analysis/CachedCustomDbAnalysisSource.ts | 44 ++ .../analysis/CachedOnlineAnalysisSource.ts | 48 ++ .../docker/DockerCommunicator.ts | 33 +- .../docker/ProgressInspectorStream.ts | 24 +- .../peptides/CachedPept2DataCommunicator.ts | 44 ++ .../peptides/LocalPept2DataCommunicator.ts | 58 ++ .../raw/CachedPept2DataCommunicator.ts | 63 -- .../source/CachedCommunicationSource.ts | 2 +- src/logic/custom_database/CustomDatabase.ts | 3 +- .../analysis/AnalysisSourceSerializer.ts | 54 ++ .../assay/AssayFileSystemMetaDataReader.ts | 34 +- .../assay/AssayFileSystemMetaDataWriter.ts | 23 +- .../assay/FileSystemAssayChangeListener.ts | 2 +- .../assay/processed/CachedAssayData.ts | 12 + ...ssayManager.ts => CachedResultsManager.ts} | 135 +++- .../assay/processed/ProcessedAssayResult.ts | 15 - .../filesystem/database/DatabaseManager.ts | 2 +- .../database/DatabaseMigratorV2ToV3.ts | 45 ++ src/logic/filesystem/database/Schema.ts | 6 +- .../docker/CustomDatabaseManager.ts | 7 +- .../filesystem/project/FileSystemWatcher.ts | 13 +- .../filesystem/project/ProjectManager.ts | 2 +- .../study/FileSystemStudyChangeListener.ts | 3 +- .../study/StudyFileSystemDataReader.ts | 11 +- src/main.ts | 64 +- src/state/DockerStore.ts | 45 +- src/state/ProjectStore.ts | 5 +- 53 files changed, 1849 insertions(+), 703 deletions(-) create mode 100644 src/components/assay/AnalysisSourceSelect.vue delete mode 100644 src/components/assay/CreateAssay.vue create mode 100644 src/components/assay/CreateAssayDialog.vue create mode 100644 src/components/custom-database/CustomDatabaseProgressReport.vue create mode 100644 src/db/migrations/v2_to_v3.sql create mode 100644 src/db/schemas/schema_v3.sql create mode 100644 src/logic/communication/analysis/CachedCustomDbAnalysisSource.ts create mode 100644 src/logic/communication/analysis/CachedOnlineAnalysisSource.ts create mode 100644 src/logic/communication/peptides/CachedPept2DataCommunicator.ts create mode 100644 src/logic/communication/peptides/LocalPept2DataCommunicator.ts delete mode 100644 src/logic/communication/raw/CachedPept2DataCommunicator.ts create mode 100644 src/logic/filesystem/analysis/AnalysisSourceSerializer.ts create mode 100644 src/logic/filesystem/assay/processed/CachedAssayData.ts rename src/logic/filesystem/assay/processed/{ProcessedAssayManager.ts => CachedResultsManager.ts} (60%) delete mode 100644 src/logic/filesystem/assay/processed/ProcessedAssayResult.ts create mode 100644 src/logic/filesystem/database/DatabaseMigratorV2ToV3.ts diff --git a/link_with_web_components_watch.js b/link_with_web_components_watch.js index 608a169f..cf9aeb2b 100644 --- a/link_with_web_components_watch.js +++ b/link_with_web_components_watch.js @@ -10,7 +10,8 @@ const chokidar = require('chokidar'); const fs = require('fs'); const path = require('path'); -const directory = './../unipept-web-components/dist' +const distDirectory = './../unipept-web-components/dist' +const typesDirectory = './../unipept-web-components/types'; const errHandler = function(err) { if (err) { @@ -21,7 +22,7 @@ const errHandler = function(err) { console.log("Watching file system for changes..."); // Watch for changes to the web components directory. -chokidar.watch(directory).on('all', (event, currentPath) => { +chokidar.watch(distDirectory).on('all', (event, currentPath) => { if (!currentPath.includes('/node_modules') && !currentPath.includes('/.git')) { const filteredPath = currentPath.replace('../unipept-web-components', './node_modules/unipept-web-components'); const directory = path.dirname(filteredPath); @@ -40,3 +41,21 @@ chokidar.watch(directory).on('all', (event, currentPath) => { console.log(event, currentPath); } }); + +chokidar.watch(typesDirectory).on('all', (event, currentPath) => { + const filteredPath = currentPath.replace('../unipept-web-components', './node_modules/unipept-web-components'); + const directory = path.dirname(filteredPath); + switch(event) { + case 'add': + fs.mkdirSync(directory, { recursive: true }, errHandler); + fs.copyFile(currentPath, filteredPath, errHandler); + break; + case 'change': + fs.copyFile(currentPath, filteredPath, errHandler); + break; + case 'unlink': + fs.unlink(filteredPath, errHandler); + break; + } + console.log(event, currentPath); +}); diff --git a/package-lock.json b/package-lock.json index 1bdd1fbe..0690df6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "marked": "^2.0.3", "node-abi": "^2.19.3", "regenerator-runtime": "^0.13.3", - "shared-memory-datastructures": "0.1.8", + "shared-memory-datastructures": "0.1.9", "unipept-web-components": "^1.4.3", "uuid": "^7.0.3", "vue": "^2.6.12", @@ -18538,9 +18538,9 @@ } }, "node_modules/shared-memory-datastructures": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/shared-memory-datastructures/-/shared-memory-datastructures-0.1.8.tgz", - "integrity": "sha512-jRiFwm2HYrtwxtET5zn9YLWIoquz9pkKqOW8BEvPZj3lX74yU3MG1qmpPzgWhKQ2yc/JIRlVDTyapsa9r88ehw==", + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/shared-memory-datastructures/-/shared-memory-datastructures-0.1.9.tgz", + "integrity": "sha512-p1cYSTQZM53dTDkurFwvN5c5+a9Yrf8zhCpD2ZfhdrIu83CjzMjBF+Xj/gYJRihDJ/TJaer0BkeaIJAGoOCVow==", "dependencies": { "fnv-plus": "^1.3.1" } @@ -20958,14 +20958,6 @@ "whatwg-fetch": "^3.0.0" } }, - "node_modules/unipept-web-components/node_modules/shared-memory-datastructures": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/shared-memory-datastructures/-/shared-memory-datastructures-0.1.9.tgz", - "integrity": "sha512-p1cYSTQZM53dTDkurFwvN5c5+a9Yrf8zhCpD2ZfhdrIu83CjzMjBF+Xj/gYJRihDJ/TJaer0BkeaIJAGoOCVow==", - "dependencies": { - "fnv-plus": "^1.3.1" - } - }, "node_modules/unipept-web-components/node_modules/uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", @@ -39636,9 +39628,9 @@ } }, "shared-memory-datastructures": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/shared-memory-datastructures/-/shared-memory-datastructures-0.1.8.tgz", - "integrity": "sha512-jRiFwm2HYrtwxtET5zn9YLWIoquz9pkKqOW8BEvPZj3lX74yU3MG1qmpPzgWhKQ2yc/JIRlVDTyapsa9r88ehw==", + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/shared-memory-datastructures/-/shared-memory-datastructures-0.1.9.tgz", + "integrity": "sha512-p1cYSTQZM53dTDkurFwvN5c5+a9Yrf8zhCpD2ZfhdrIu83CjzMjBF+Xj/gYJRihDJ/TJaer0BkeaIJAGoOCVow==", "requires": { "fnv-plus": "^1.3.1" } @@ -41569,14 +41561,6 @@ "whatwg-fetch": "^3.0.0" }, "dependencies": { - "shared-memory-datastructures": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/shared-memory-datastructures/-/shared-memory-datastructures-0.1.9.tgz", - "integrity": "sha512-p1cYSTQZM53dTDkurFwvN5c5+a9Yrf8zhCpD2ZfhdrIu83CjzMjBF+Xj/gYJRihDJ/TJaer0BkeaIJAGoOCVow==", - "requires": { - "fnv-plus": "^1.3.1" - } - }, "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", diff --git a/package.json b/package.json index ecad8f70..9fcf46db 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "marked": "^2.0.3", "node-abi": "^2.19.3", "regenerator-runtime": "^0.13.3", - "shared-memory-datastructures": "0.1.8", + "shared-memory-datastructures": "0.1.9", "unipept-web-components": "^1.4.3", "uuid": "^7.0.3", "vue": "^2.6.12", diff --git a/src/App.vue b/src/App.vue index 7dc29577..f766aea8 100644 --- a/src/App.vue +++ b/src/App.vue @@ -11,7 +11,6 @@ @@ -75,8 +74,8 @@ import { Assay, ProteomicsAssay, NetworkConfiguration, - AssayData, - QueueManager + QueueManager, + AssayAnalysisStatus } from "unipept-web-components"; import DockerCommunicator from "@/logic/communication/docker/DockerCommunicator"; import BootstrapApplication from "@/logic/application/BootstrapApplication"; @@ -99,8 +98,8 @@ const { app } = require("electron").remote; assaysInProgress: { get(): Assay[] { return this.$store.getters.assays - .filter((a: AssayData) => a.analysisMetaData.progress < 1) - .map((a: AssayData) => a.assay); + .filter((a: AssayAnalysisStatus) => !a.analysisReady) + .map((a: AssayAnalysisStatus) => a.assay); } }, isMini: { @@ -122,6 +121,7 @@ export default class App extends Vue implements ErrorListener { private updatedColor: string = "info"; private toolbarWidth: number = 210; + // Has this component been initialized before? private static previouslyInitialized: boolean = false; @@ -180,7 +180,7 @@ export default class App extends Vue implements ErrorListener { electron.remote.BrowserWindow.getAllWindows()[0].setProgressBar(-1); } else { const average: number = assays.reduce( - (prev: number, currentAssay: Assay) => prev + this.$store.getters.assayData(currentAssay).analysisMetaData.progress, 0 + (prev: number, currentAssay: Assay) => prev + this.$store.getters["assayData"](currentAssay).progress.value, 0 ) / assays.length; electron.remote.BrowserWindow.getAllWindows()[0].setProgressBar(average); } @@ -210,10 +210,6 @@ export default class App extends Vue implements ErrorListener { private onToolbarWidthUpdated(newValue: number) { this.toolbarWidth = newValue; } - - private onActivateDataset(value: ProteomicsAssay) { - this.$store.dispatch("setActiveDataset", value); - } } diff --git a/src/components/analysis/AnalysisSummary.vue b/src/components/analysis/AnalysisSummary.vue index 0eca6349..b8200806 100644 --- a/src/components/analysis/AnalysisSummary.vue +++ b/src/components/analysis/AnalysisSummary.vue @@ -111,7 +111,6 @@ import { PeptideTrust, Pept2DataCommunicator, ExportResultsButton, - AssayData, NetworkConfiguration, Tooltip } from "unipept-web-components"; @@ -136,11 +135,13 @@ export default class AnalysisSummary extends Vue { get dirty(): boolean { const currentConfig = this.assay.getSearchConfiguration(); - return this.assay.getDatabaseVersion() !== this.dbVersion || - this.assay.getEndpoint() !== this.endpoint || - currentConfig.equateIl !== this.equateIl || - currentConfig.filterDuplicates !== this.filterDuplicates || - currentConfig.enableMissingCleavageHandling !== this.missedCleavage; + // return this.assay.getDatabaseVersion() !== this.dbVersion || + // this.assay.getEndpoint() !== this.endpoint || + // currentConfig.equateIl !== this.equateIl || + // currentConfig.filterDuplicates !== this.filterDuplicates || + // currentConfig.enableMissingCleavageHandling !== this.missedCleavage; + + return false; } get endpoint(): string { @@ -148,12 +149,14 @@ export default class AnalysisSummary extends Vue { } get peptideCountTable(): CountTable { - return this.$store.getters.assayData(this.assay)?.peptideCountTable; + return undefined; + // return this.$store.getters.assayData(this.assay)?.peptideCountTable; } get progress(): number { - const assayData: AssayData = this.$store.getters.assayData(this.assay); - return assayData ? assayData.analysisMetaData.progress : 0; + return 0; + // const assayData: AssayData = this.$store.getters.assayData(this.assay); + // return assayData ? assayData.analysisMetaData.progress : 0; } private async mounted() { @@ -175,18 +178,18 @@ export default class AnalysisSummary extends Vue { @Watch("peptideCountTable") private async onPeptideCountTableChanged() { if (this.peptideCountTable) { - const communicator: Pept2DataCommunicator = - this.$store.getters.assayData(this.assay)?.communicationSource.getPept2DataCommunicator(); - this.peptideTrust = await communicator?.getPeptideTrust( - this.peptideCountTable, - this.assay.getSearchConfiguration() - ); + // const communicator: Pept2DataCommunicator = + // this.$store.getters.assayData(this.assay)?.communicationSource.getPept2DataCommunicator(); + // this.peptideTrust = await communicator?.getPeptideTrust( + // this.peptideCountTable, + // this.assay.getSearchConfiguration() + // ); } } private update() { const config = new SearchConfiguration(this.equateIl, this.filterDuplicates, this.missedCleavage); - this.$store.dispatch("processAssay", [this.assay, true, config]); + this.$store.dispatch("analyseAssay", this.assay); } private getHumanReadableAssayDate(): string { diff --git a/src/components/analysis/PeptideSummaryTable.vue b/src/components/analysis/PeptideSummaryTable.vue index 02899435..2334cf88 100644 --- a/src/components/analysis/PeptideSummaryTable.vue +++ b/src/components/analysis/PeptideSummaryTable.vue @@ -25,7 +25,7 @@ import Vue from "vue"; import Component from "vue-class-component"; import { Prop, Watch } from "vue-property-decorator"; -import { ProteomicsAssay, CountTable, Peptide, AssayData } from "unipept-web-components"; +import { ProteomicsAssay, CountTable, Peptide } from "unipept-web-components"; import { DataOptions } from "vuetify"; import { ItemType } from "@/state/PeptideSummaryTable.worker"; @@ -33,8 +33,10 @@ import { ItemType } from "@/state/PeptideSummaryTable.worker"; computed: { progress: { get(): number { - const assayData: AssayData = this.$store.getters.assayData(this.assay); - return assayData ? assayData.analysisMetaData.progress : 0; + // const assayData: AssayData = this.$store.getters.assayData(this.assay); + // return assayData ? assayData.analysisMetaData.progress : 0; + + return 0; } }, headers: { diff --git a/src/components/assay/AnalysisSourceSelect.vue b/src/components/assay/AnalysisSourceSelect.vue new file mode 100644 index 00000000..ac97935e --- /dev/null +++ b/src/components/assay/AnalysisSourceSelect.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/src/components/assay/CreateAssay.vue b/src/components/assay/CreateAssay.vue deleted file mode 100644 index 057d48d1..00000000 --- a/src/components/assay/CreateAssay.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - - - diff --git a/src/components/assay/CreateAssayDialog.vue b/src/components/assay/CreateAssayDialog.vue new file mode 100644 index 00000000..4292a354 --- /dev/null +++ b/src/components/assay/CreateAssayDialog.vue @@ -0,0 +1,590 @@ + + + + + diff --git a/src/components/custom-database/CreateCustomDatabase.vue b/src/components/custom-database/CreateCustomDatabase.vue index 07d9f041..fb7a4ee9 100644 --- a/src/components/custom-database/CreateCustomDatabase.vue +++ b/src/components/custom-database/CreateCustomDatabase.vue @@ -267,6 +267,7 @@ export default class CreateCustomDatabase extends Vue { } private validateAndContinue(): void { + // @ts-ignore if (this.$refs.databaseForm.validate()) { this.currentStep = 2; } @@ -275,6 +276,7 @@ export default class CreateCustomDatabase extends Vue { private async buildDatabase(): Promise { const sourceUrlMap = { "TrEMBL": "https://ftp.expasy.org/databases/uniprot/current_release/knowledgebase/complete/uniprot_trembl.xml.gz", + // "TrEMBL": "host.docker.internal:8000/uniprot_trembl.xml.gz", "SwissProt": "https://ftp.expasy.org/databases/uniprot/current_release/knowledgebase/complete/uniprot_sprot.xml.gz" } @@ -284,7 +286,7 @@ export default class CreateCustomDatabase extends Vue { "buildDatabase", [ this.databaseName, - this.selectedSources.map(source => sourceUrlMap[source]), + this.selectedSources.map(source => (sourceUrlMap as any)[source]), this.selectedSources, this.selectedTaxa.map(taxon => taxon.id), await configManager.readConfiguration() diff --git a/src/components/custom-database/CustomDatabaseProgressReport.vue b/src/components/custom-database/CustomDatabaseProgressReport.vue new file mode 100644 index 00000000..0398d50f --- /dev/null +++ b/src/components/custom-database/CustomDatabaseProgressReport.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/src/components/navigation-drawers/AssayItem.vue b/src/components/navigation-drawers/AssayItem.vue index 1b8e0e1f..5451f577 100644 --- a/src/components/navigation-drawers/AssayItem.vue +++ b/src/components/navigation-drawers/AssayItem.vue @@ -152,7 +152,8 @@ import { CountTable, Peptide, Assay, - AssayData + AssayAnalysisStatus, + PeptideData } from "unipept-web-components"; import ExperimentSummaryDialog from "./../analysis/ExperimentSummaryDialog.vue"; @@ -160,6 +161,7 @@ import AssayFileSystemDestroyer from "@/logic/filesystem/assay/AssayFileSystemDe import { promises as fs } from "fs"; import { v4 as uuidv4 } from "uuid"; import { AssayFileSystemMetaDataWriter } from "@/logic/filesystem/assay/AssayFileSystemMetaDataWriter"; +import { ShareableMap } from "shared-memory-datastructures"; const { remote } = require("electron"); const { Menu, MenuItem } = remote; @@ -209,27 +211,28 @@ export default class AssayItem extends Vue { } get progress(): number { - const assayData: AssayData = this.$store.getters.assayData(this.assay); - return assayData ? assayData.analysisMetaData.progress : 0; + const assayData: AssayAnalysisStatus = this.$store.getters.assayData(this.assay); + return assayData ? assayData.progress.value : 0; } get peptideCountTable(): CountTable { - return this.$store.getters.assayData(this.assay)?.peptideCountTable; + return this.$store.getters.assayData(this.assay)?.filteredData.peptideCountTable; } get errorStatus(): boolean { - const assayData: AssayData = this.$store.getters.assayData(this.assay); - return assayData?.analysisMetaData.status === "error"; + const assayData: AssayAnalysisStatus = this.$store.getters.assayData(this.assay); + return assayData?.error.status; } get cancelStatus(): boolean { - const assayData: AssayData = this.$store.getters.assayData(this.assay); - return assayData?.analysisMetaData.status === "cancelled"; + return false; + // const assayData: AssayData = this.$store.getters.assayData(this.assay); + // return assayData?.analysisMetaData.status === "cancelled"; } - get pept2dataCommunicator(): Pept2DataCommunicator { + get pept2Data(): ShareableMap { const processingResult = this.$store.getters.assayData(this.assay); - return processingResult?.communicationSource?.getPept2DataCommunicator(); + return processingResult?.pept2Data; } private cancelAnalysis() { @@ -310,20 +313,21 @@ export default class AssayItem extends Vue { this.assayName = this.assay.getName(); } - @Watch("pept2dataCommunicator", { immediate: true }) + @Watch("pept2Data", { immediate: true }) private async computePeptideTrust(): Promise { - if (this.assay && this.pept2dataCommunicator) { + if (this.assay && this.pept2Data) { const processingResult = this.$store.getters.assayData(this.assay); - const countTable = processingResult?.peptideCountTable; - this.peptideTrust = await this.pept2dataCommunicator.getPeptideTrust( - countTable, - this.assay.getSearchConfiguration() - ); + const countTable = processingResult?.filteredData.peptideCountTable; + // TODO get trust + // this.peptideTrust = await this.pept2dataCommunicator.getPeptideTrust( + // countTable, + // this.assay.getSearchConfiguration() + // ); } } private reanalyse() { - this.$store.dispatch("processAssay", [this.assay, true, this.assay.getSearchConfiguration()]); + this.$store.dispatch("analyseAssay", this.assay); } private selectAssay() { diff --git a/src/components/navigation-drawers/ProjectExplorer.vue b/src/components/navigation-drawers/ProjectExplorer.vue index a9c291f1..d5c3694f 100644 --- a/src/components/navigation-drawers/ProjectExplorer.vue +++ b/src/components/navigation-drawers/ProjectExplorer.vue @@ -1,22 +1,30 @@ @@ -26,6 +34,7 @@ import Component from "vue-class-component"; import { Study, Tooltip } from "unipept-web-components"; import StudyItem from "@/components/navigation-drawers/StudyItem.vue"; import mkdirp from "mkdirp"; +import { Watch } from "vue-property-decorator"; @Component({ components: { @@ -43,6 +52,14 @@ import mkdirp from "mkdirp"; } }) export default class ProjectExplorer extends Vue { + private minExplorerWidth: number = 169; + private originalExplorerWidth: number = 210; + private explorerWidth: number = this.originalExplorerWidth; + + mounted() { + this.setupDraggableExplorer(); + } + private createStudy() { // Check which studies already exist, and make sure there isn't one with the same name. const unknowns: number[] = this.$store.getters.studies @@ -60,6 +77,41 @@ export default class ProjectExplorer extends Vue { // pick this up. mkdirp(`${this.$store.getters.projectLocation}${studyName}`); } + + private setupDraggableExplorer() { + const toolbar = this.$refs.projectExplorer as Element; + const drawerBorder = toolbar.querySelector(".v-navigation-drawer__border"); + + let initialMousePos: number = 0; + + const mouseMoveListener = (moveE: MouseEvent) => { + const xDifference = initialMousePos - moveE.x; + const computedWidth = this.originalExplorerWidth -xDifference; + if (computedWidth >= this.minExplorerWidth) { + this.explorerWidth = computedWidth; + } + }; + + drawerBorder.addEventListener("mousedown", (e: MouseEvent) => { + initialMousePos = e.x; + document.addEventListener("mousemove", mouseMoveListener); + }); + + document.addEventListener("mouseup", (e: MouseEvent) => { + // Reset start position for next mousedown-event + this.originalExplorerWidth = this.explorerWidth; + document.removeEventListener("mousemove", mouseMoveListener); + }); + } + + @Watch("explorerWidth") + private onExplorerWidthChanged(newWidth: number) { + this.$emit("widthChange", newWidth); + } + + private createAssay(study: Study) { + this.$emit("createAssay", study); + } } @@ -67,8 +119,29 @@ export default class ProjectExplorer extends Vue { .sample-list-placeholder { margin-left: 8px; margin-right: 8px; - position: relative; - top: 16px; + margin-top: 16px; text-align: center; } + + .project-explorer { + height: 100%; + position: fixed; + left: 55px; + background-color: white; + border-right: 1px solid rgba(0, 0, 0, 0.12); + display: block; + } + + .project-explorer .v-list-item__action { + min-width: 48px; + } + + .project-explorer .v-list-item__action span:first-child { + margin-right: 8px; + } + + .project-explorer-container { + height: calc(100vh - 64px); + overflow-y: auto; + } diff --git a/src/components/navigation-drawers/StudyItem.vue b/src/components/navigation-drawers/StudyItem.vue index 4366d710..56f0bac1 100644 --- a/src/components/navigation-drawers/StudyItem.vue +++ b/src/components/navigation-drawers/StudyItem.vue @@ -54,30 +54,16 @@ {{ nameError }} - - @@ -99,12 +93,10 @@ import Vue from "vue"; import Component from "vue-class-component"; import { Prop, Watch } from "vue-property-decorator"; import { Tooltip } from "unipept-web-components"; -import ProjectExplorer from "@/components/navigation-drawers/ProjectExplorer.vue"; @Component({ components: { Tooltip, - ProjectExplorer }, computed: { isMini: { @@ -115,81 +107,20 @@ import ProjectExplorer from "@/components/navigation-drawers/ProjectExplorer.vue } }) export default class Toolbar extends Vue { - private minToolbarWidth: number = 169; private originalToolbarWidth: number = 210; private toolbarWidth: number = this.originalToolbarWidth; - mounted() { - this.setupDraggableToolbar(); - } - private navigate(routeToGo: string, activateSidebar: boolean) { if (this.$route.path !== routeToGo) { this.$router.replace(routeToGo); } } - private setupDraggableToolbar() { - const toolbar = this.$refs.toolbar as Element; - const drawerBorder = toolbar.querySelector(".v-navigation-drawer__border"); - - let initialMousePos: number = 0; - - const mouseMoveListener = (moveE: MouseEvent) => { - const xDifference = initialMousePos - moveE.x; - const computedWidth = this.originalToolbarWidth -1 * xDifference; - if (computedWidth >= this.minToolbarWidth) { - this.toolbarWidth = computedWidth; - } - }; - - drawerBorder.addEventListener("mousedown", (e: MouseEvent) => { - initialMousePos = e.x; - document.addEventListener("mousemove", mouseMoveListener); - }); - document.addEventListener("mouseup", (e: MouseEvent) => { - // Reset start position for next mousedown-event - this.originalToolbarWidth = this.toolbarWidth; - document.removeEventListener("mousemove", mouseMoveListener); - }); - } - - @Watch("toolbarWidth") - private onToolbarWidthChanged(newWidth: number) { - this.$emit("update:toolbar-width", newWidth); - } } diff --git a/src/components/custom-database/CustomDatabaseProgressReport.vue b/src/components/custom-database/CustomDatabaseProgressReport.vue deleted file mode 100644 index 0398d50f..00000000 --- a/src/components/custom-database/CustomDatabaseProgressReport.vue +++ /dev/null @@ -1,77 +0,0 @@ - - - - - diff --git a/src/components/games/Snake.vue b/src/components/games/Snake.vue index a905ac15..b121305d 100644 --- a/src/components/games/Snake.vue +++ b/src/components/games/Snake.vue @@ -16,12 +16,12 @@ Use the arrow keys on your keyboard to move around and try to eat the apple, without biting your own tale.

-
+
mdi-chevron-up
-
+
mdi-chevron-left @@ -43,6 +43,9 @@ +
+ Close +
@@ -66,6 +69,8 @@ export default class Snake extends Vue { private grid = 16; private count = 0; + private animationFrameListener: number = -1; + private snake: {x: number, y: number, dx: number, dy: number, cells: CellType[], maxCells: number} = { x: 160, y: 160, @@ -100,7 +105,6 @@ export default class Snake extends Vue { }); } - private async activateSnake(): Promise { this.snakeActive = true; @@ -108,7 +112,15 @@ export default class Snake extends Vue { this.canvas = this.$refs.snake as HTMLCanvasElement; this.context = this.canvas.getContext("2d"); - requestAnimationFrame(this.loop); + this.animationFrameListener = requestAnimationFrame(this.loop); + } + + private async deactivateSnake(): Promise { + this.snakeActive = false; + if (this.animationFrameListener >= 0) { + cancelAnimationFrame(this.animationFrameListener); + this.animationFrameListener = -1; + } } // get random whole numbers in a specific range @@ -119,7 +131,7 @@ export default class Snake extends Vue { // game loop private loop() { - requestAnimationFrame(this.loop); + this.animationFrameListener = requestAnimationFrame(this.loop); // slow game loop to 15 fps instead of 60 (60/15 = 4) if (++this.count < 4) { diff --git a/src/components/navigation-drawers/AssayItem.vue b/src/components/navigation-drawers/AssayItem.vue index 5c35f0bb..14037c98 100644 --- a/src/components/navigation-drawers/AssayItem.vue +++ b/src/components/navigation-drawers/AssayItem.vue @@ -13,7 +13,7 @@
- + - {{ nameError }} + {{ errorMessage }} @@ -230,11 +230,11 @@ export default class AssayItem extends Vue { } get analysisReady(): boolean { - return this.activeAssay?.analysisReady || false; + return this.$store.getters.assayData(this.assay)?.analysisReady || false; } get analysisInProgress(): boolean { - return this.activeAssay?.analysisInProgress || false; + return this.$store.getters.assayData(this.assay)?.analysisInProgress || false; } get errorStatus(): boolean { @@ -242,6 +242,10 @@ export default class AssayItem extends Vue { return assayData?.error.status; } + get errorMessage(): boolean { + return this.$store.getters.assayData(this.assay)?.error.message || this.nameError; + } + get cancelStatus(): boolean { return false; // const assayData: AssayData = this.$store.getters.assayData(this.assay); diff --git a/src/components/pages/CustomDatabasePage.vue b/src/components/pages/CustomDatabasePage.vue index 2701283c..c1b9dc6b 100644 --- a/src/components/pages/CustomDatabasePage.vue +++ b/src/components/pages/CustomDatabasePage.vue @@ -48,7 +48,7 @@
- +
@@ -76,10 +76,10 @@ import CreateCustomDatabase from "@/components/custom-database/CreateCustomDatab import { Tooltip } from "unipept-web-components"; import { CustomDatabaseInfo } from "@/state/DockerStore"; import DockerCommunicator from "@/logic/communication/docker/DockerCommunicator"; -import CustomDatabaseProgressReport from "@/components/custom-database/CustomDatabaseProgressReport.vue"; +import ProgressReportSummary from "@/components/analysis/ProgressReportSummary.vue"; @Component({ - components: { CustomDatabaseProgressReport, CreateCustomDatabase, Tooltip }, + components: { ProgressReportSummary, CreateCustomDatabase, Tooltip }, computed: { databases: { get(): CustomDatabaseInfo[] { diff --git a/src/components/pages/analysis/AnalysisPage.vue b/src/components/pages/analysis/AnalysisPage.vue index 3fae705b..02132674 100644 --- a/src/components/pages/analysis/AnalysisPage.vue +++ b/src/components/pages/analysis/AnalysisPage.vue @@ -7,28 +7,46 @@ 'width': `calc(100vw - 55px - ${explorerWidth}px)`, 'float': 'right' }"> + + + -
- - mdi-wifi-strength-4-alert - -

- A network communication error occurred while processing this assay. Please check that you - are connected to the internet, or that your Unipept API-endpoint is correctly set and - try again. -

-
-
+ + + +
+ An unexpected error has occurred during the analysis of this assay. Details about this specific + error are shown below. You can restart the analysis to try again. If + you believe that this error is not the result of a user action, then please contact us and + provide the error details below. Make sure to check your internet connection before continuing. +
+ +
Error details
+
{{ errorObject ? errorObject.stack : errorMessage }}
+
+ + + + You chose to cancel the analysis of this assay. Click here to restart this + assay's analysis. Previously generated analysis results + will be lost if the analysis is restarted. The analysis of + this assay will be scheduled to start after all currently active analysis processes have finished. + + +

Empty project

You have created an empty project. If this is the first time you're using this application, you can use @@ -59,31 +77,49 @@

-
- mdi-cancel -

- You chose to cancel the analysis of this assay. Click here to restart this - assay's analysis. -

-
+
+
+
+
+ + + {{ Math.round(activeProgress.currentValue) }}% + + +
+ {{ activeProgress.eta !== -1 ? `Approximately ${msToTimeString(activeProgress.eta)} remaining.` : "Computing estimated time remaining..." }} +
+
+ Analysis started {{ msToTimeString(currentTime - activeProgress.startTimes[activeProgress.currentStep]) }} ago. +
+
+ Note that assays are processed sequentially and that the estimated time is only computed once the + analysis for this assay has been started. +
+
+ + + +
+
-
- - {{ Math.round((activeAssay ? activeProgress : maxProgress) * 100) }}% - -

- {{ activeEta ? secondsToTimeString(activeEta) : secondsToTimeString(minEta) }} -

-

- Note that assays are processed sequentially and that the estimated time is only computed once the - analysis for this assay has been started. -

-
+ + + This assay is queued for analysis. Only one analysis will be analysed by this application at a + time. Note, however, that parallelism at the single assay analysis level is present in order to + maximize the usage of your system's available resources. You will see the progress of this + assay's analysis once the actual analysis process has started. + + +
@@ -97,15 +133,17 @@ @@ -205,7 +228,7 @@ export default class AnalysisPage extends Vue { } .inner-status-container { - max-width: 600px; + max-width: 1000px; display: flex; justify-content: center; flex-direction: column; @@ -213,9 +236,6 @@ export default class AnalysisPage extends Vue { align-items: center; } - .game-container { - justify-content: flex-start; - } .status-container { width: 100%; diff --git a/src/state/DockerStore.ts b/src/state/DockerStore.ts index 2653f8aa..52a3719d 100644 --- a/src/state/DockerStore.ts +++ b/src/state/DockerStore.ts @@ -1,6 +1,6 @@ import CustomDatabase from "@/logic/custom_database/CustomDatabase"; import { ActionContext, ActionTree, GetterTree, MutationTree } from "vuex"; -import { NcbiId } from "unipept-web-components"; +import { NcbiId, ProgressReport } from "unipept-web-components"; import DockerCommunicator from "@/logic/communication/docker/DockerCommunicator"; import Vue from "vue"; import { queue } from "async"; @@ -8,25 +8,31 @@ import Configuration from "@/logic/configuration/Configuration"; import * as path from "path"; import CustomDatabaseManager from "@/logic/filesystem/docker/CustomDatabaseManager"; -const PROGRESS_STEPS_LENGTH: number = 12; - type CustomDatabaseInfo = { - database: CustomDatabase - progress: { - // Progress value ([0 - 100]) or -1 if indeterminate - value: number, - step: string, - progress_step: number, - startTimes: number[], - endTimes: number[], - previousStep: number - }, + database: CustomDatabase, + progress: ProgressReport, // Whether the database has been constructed successfully. ready: boolean }; export { CustomDatabaseInfo }; +const progressSteps: string[] = [ + "Creating taxon tables", + "Initializing database build process", + "Downloading database", + "Processing chunks", + "Started building main database tables", + "Calculating lowest common ancestors", + "Calculating functional annotations", + "Sorting peptides", + "Creating sequence table", + "Fetching EC numbers", + "Fetching GO terms", + "Fetching InterPro entries", + "Filling database and computing indices" +]; + export interface CustomDatabaseState { databases: CustomDatabaseInfo[], // A list of all custom database objects that are queued for construction. Once the previous database has been @@ -70,12 +76,12 @@ const databaseMutations: MutationTree = { state.databases.push({ database: database, progress: { - value: -1, - step: "Initializing database construction", - progress_step: 0, - startTimes: new Array(PROGRESS_STEPS_LENGTH).fill(-1), - endTimes: new Array(PROGRESS_STEPS_LENGTH).fill(-1), - previousStep: -1 + steps: progressSteps, + startTimes: new Array(progressSteps.length).fill(0), + endTimes: new Array(progressSteps.length).fill(0), + currentStep: 0, + currentValue: -1, + eta: -1 }, ready: false }); @@ -83,29 +89,58 @@ const databaseMutations: MutationTree = { UPDATE_DATABASE_PROGRESS( state: CustomDatabaseState, - [database, step, value, progress_step]: [CustomDatabase, string, number, number] + [database, step, value]: [CustomDatabase, number, number] ) { const dbObj = state.databases.find(db => db.database.name === database.name); - dbObj.progress.value = value; - dbObj.progress.step = step; - dbObj.progress.progress_step = progress_step; - - if (dbObj.progress.previousStep !== progress_step) { - const time = new Date().getTime(); - dbObj.progress.startTimes[progress_step] = time; - dbObj.progress.endTimes[dbObj.progress.previousStep] = time; - } + dbObj.progress.currentValue = value; + dbObj.progress.currentStep = step; + + const time = new Date().getTime(); - console.log(JSON.stringify(dbObj.progress)); + for (let i = step - 1; i > 0; i--) { + if (dbObj.progress.endTimes[i] === 0) { + dbObj.progress.endTimes[i] = time; + } - dbObj.progress.previousStep = progress_step; + if (dbObj.progress.startTimes[i] === 0) { + dbObj.progress.startTimes[i] = time; + } + } + + if (dbObj.progress.startTimes[step] === 0) { + dbObj.progress.startTimes[step] = time; + } }, UPDATE_DATABASE_STATUS( state: CustomDatabaseState, [database, status]: [CustomDatabase, boolean] ) { - state.databases.find(db => db.database.name === database.name).ready = status; + const dbObj = state.databases.find(db => db.database.name === database.name); + const progressObj = dbObj.progress; + + const time = new Date().getTime(); + + if (status) { + // Store end time for the last progress + for (let i = 0; i < progressObj.steps.length; i++) { + if (progressObj.startTimes[i] === 0) { + progressObj.startTimes[i] = time; + } + + if (progressObj.endTimes[i] === 0) { + progressObj.endTimes[i] = time; + } + } + } else { + // Reset progress values + for (let i = 0; i < progressObj.steps.length; i++) { + progressObj.startTimes[i] = 0; + progressObj.endTimes[i] = 0; + } + } + + dbObj.ready = status; }, INITIALIZE_QUEUE( @@ -146,12 +181,12 @@ const databaseMutations: MutationTree = { state.databases.push({ database: db, progress: { - value: 1, - step: "", - progress_step: 0, - startTimes: new Array(PROGRESS_STEPS_LENGTH).fill(-1), - endTimes: new Array(PROGRESS_STEPS_LENGTH).fill(-1), - previousStep: -1 + steps: progressSteps, + startTimes: new Array(progressSteps.length).fill(0), + endTimes: new Array(progressSteps.length).fill(0), + currentStep: 0, + currentValue: -1, + eta: -1 }, ready: true }); From 5e658b0f57984e0adc71dfae9ffd017062ea81b7 Mon Sep 17 00:00:00 2001 From: Pieter Verschaffelt Date: Tue, 30 Nov 2021 13:48:22 +0100 Subject: [PATCH 17/26] Furhter preparing the application for the next version --- src/components/analysis/AnalysisSummary.vue | 135 ++++++++++-------- .../analysis/PeptideSummaryTable.vue | 4 +- src/components/assay/CreateAssayDialog.vue | 29 ++-- .../navigation-drawers/AssayItem.vue | 10 +- .../pages/analysis/AnalysisPage.vue | 4 +- .../analysis/SingleAssayAnalysisPage.vue | 10 +- .../metadata/MetadataCommunicator.ts | 9 +- .../assay/AssayFileSystemDataReader.ts | 35 ++--- .../assay/AssayFileSystemDataWriter.ts | 17 +-- .../configuration/SearchConfigManager.ts | 66 +++++++++ src/logic/filesystem/database/Schema.ts | 7 +- .../filesystem/metadata/StorageMetadata.ts | 12 ++ .../metadata/StorageMetadataManager.ts | 63 ++++++++ 13 files changed, 260 insertions(+), 141 deletions(-) create mode 100644 src/logic/filesystem/configuration/SearchConfigManager.ts create mode 100644 src/logic/filesystem/metadata/StorageMetadata.ts create mode 100644 src/logic/filesystem/metadata/StorageMetadataManager.ts diff --git a/src/components/analysis/AnalysisSummary.vue b/src/components/analysis/AnalysisSummary.vue index b8200806..e08a886d 100644 --- a/src/components/analysis/AnalysisSummary.vue +++ b/src/components/analysis/AnalysisSummary.vue @@ -22,43 +22,67 @@ {{ peptideTrust.searchedPeptides }} peptides in assay
Last analysed on {{ getHumanReadableAssayDate() }}
-
- - - mdi-alert-outline - - - {{ assay.getDatabaseVersion() }} -
-
- - - mdi-alert-outline - - - {{ assay.getEndpoint() }} + + +
+
-
- Either the selected endpoint, the supported UniProt database version or the selected - search settings changed since the last time you analysed this assay. It is - recommended that you reanalyse this assay. + + +
+
+ + Checking cache validity... + +
+
+ Analysis is up-to-date, no need to restart the analysis. +
+
+ + Something about the selected analysis source changed since the last time + this assay has been analysed. Restart the analysis for these changes to + come to effect. + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{ + this.cacheValidityLoading = true; + const metadataMng = new StorageMetadataManager(this.$store.getters.dbManager); + const metadata = await metadataMng.readMetadata(this.assay.getId()); + + this.cacheIsValid = await this.assay.getAnalysisSource().verifyEquality(metadata.fingerprint); + + this.cacheValidityLoading = false; } private update() { diff --git a/src/components/analysis/PeptideSummaryTable.vue b/src/components/analysis/PeptideSummaryTable.vue index 2334cf88..a2f35945 100644 --- a/src/components/analysis/PeptideSummaryTable.vue +++ b/src/components/analysis/PeptideSummaryTable.vue @@ -46,13 +46,13 @@ import { ItemType } from "@/state/PeptideSummaryTable.worker"; text: "Peptide", align: "start", value: "peptide", - width: "30%" + width: "25%" }, { text: "Occurrence", align: "start", value: "count", - width: "15%" + width: "20%" }, { text: "Lowest common ancestor", align: "start", diff --git a/src/components/assay/CreateAssayDialog.vue b/src/components/assay/CreateAssayDialog.vue index 05d766a6..91f98161 100644 --- a/src/components/assay/CreateAssayDialog.vue +++ b/src/components/assay/CreateAssayDialog.vue @@ -35,23 +35,13 @@ New assays - - - Add new assay manually. - - - - - Import assays from file(s). - + + Add assay manually + + + + Import assay from file +