diff --git a/.gitignore b/.gitignore index a2ecfbe..455c5e7 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ lib/www/client/source/dist/ lib/www/client/dist/ etc/surveys/*.yaml !etc/surveys/_*.yaml +etc/ssl/* diff --git a/bin/runner.sh b/bin/runner.sh index 3a44fbf..db07b2b 100755 --- a/bin/runner.sh +++ b/bin/runner.sh @@ -114,6 +114,9 @@ run $BINDIR/human_exports_qc.py print_log "Export sequence data" run $BINDIR/human_exports_seis.py +print_log "Process ASAQC queue" +run $DOUGAL_ROOT/lib/www/server/queues/asaqc/index.js + rm "$LOCKFILE" print_info "End run" diff --git a/etc/config.yaml b/etc/config.yaml index 7b611bf..a4864f6 100644 --- a/etc/config.yaml +++ b/etc/config.yaml @@ -31,4 +31,16 @@ imports: # For a file to be imported, it must have been last modified at # least this many seconds ago. file_min_age: 60 - + +queues: + asaqc: + request: + url: "https://api.gateway.equinor.com/vt/v1/api/upload-file-encoded" + args: + method: POST + headers: + Content-Type: application/json + httpsAgent: # The paths here are relative to $DOUGAL_ROOT + cert: etc/ssl/asaqc.crt + key: etc/ssl/asaqc.key + diff --git a/etc/db/upgrades/upgrade08-81d9ea19→74b3de5c.sql b/etc/db/upgrades/upgrade08-81d9ea19→74b3de5c.sql new file mode 100644 index 0000000..4f023c4 --- /dev/null +++ b/etc/db/upgrades/upgrade08-81d9ea19→74b3de5c.sql @@ -0,0 +1,75 @@ +-- Upgrade the database from commit 81d9ea19 to 74b3de5c. +-- +-- This upgrade affects the `public` schema only. +-- +-- It creates a new table, `queue_items`, for storing +-- requests and responses related to inter-API communication. +-- At the moment this means Equinor's ASAQC API, but it +-- should be applicable to others as well if the need +-- arises. +-- +-- As well as the table, it adds: +-- +-- * `queue_item_status`, an ENUM type. +-- * `update_timestamp`, a trigger function. +-- * Two triggers on `queue_items`. +-- +-- To apply, run as the dougal user: +-- +-- psql < $THIS_FILE +-- +-- NOTE: It will fail harmlessly if applied twice. + + +-- Queues are global, not per project, +-- so they go in the `public` schema. + + +CREATE TYPE queue_item_status +AS ENUM ( + 'queued', + 'cancelled', + 'failed', + 'sent' +); + +CREATE TABLE IF NOT EXISTS queue_items ( + item_id serial NOT NULL PRIMARY KEY, + -- One day we may want multiple queues, in that case we will + -- have a queue_id and a relation of queue definitions. + -- But not right now. + -- queue_id integer NOT NULL REFERENCES queues (queue_id), + status queue_item_status NOT NULL DEFAULT 'queued', + payload jsonb NOT NULL, + results jsonb NOT NULL DEFAULT '{}'::jsonb, + created_on timestamptz NOT NULL DEFAULT current_timestamp, + updated_on timestamptz NOT NULL DEFAULT current_timestamp, + not_before timestamptz NOT NULL DEFAULT '1970-01-01T00:00:00Z', + parent_id integer NULL REFERENCES queue_items (item_id) +); + +-- Sets `updated_on` to current_timestamp unless an explicit +-- timestamp is part of the update. +-- +-- This function can be reused with any table that has (or could have) +-- an `updated_on` column of time timestamptz. +CREATE OR REPLACE FUNCTION update_timestamp () RETURNS trigger AS +$$ + BEGIN + IF NEW.updated_on IS NOT NULL THEN + NEW.updated_on := current_timestamp; + END IF; + RETURN NEW; + EXCEPTION + WHEN undefined_column THEN RETURN NEW; + END; +$$ +LANGUAGE plpgsql; + +CREATE TRIGGER queue_items_tg0 +BEFORE INSERT OR UPDATE ON public.queue_items +FOR EACH ROW EXECUTE FUNCTION public.update_timestamp(); + +CREATE TRIGGER queue_items_tg1 +AFTER INSERT OR DELETE OR UPDATE ON public.queue_items +FOR EACH ROW EXECUTE FUNCTION public.notify('queue_items'); diff --git a/etc/ssl/README.md b/etc/ssl/README.md new file mode 100644 index 0000000..055991a --- /dev/null +++ b/etc/ssl/README.md @@ -0,0 +1,3 @@ +# TLS certificates directory + +Drop TLS certificates required by Dougal in this directory. It is excluded by [`.gitignore`](../../.gitignore) so its contents should never be committed by accident (and shouldn't be committed on purpose!). diff --git a/lib/www/client/source/babel.config.js b/lib/www/client/source/babel.config.js index e955840..8d383d5 100644 --- a/lib/www/client/source/babel.config.js +++ b/lib/www/client/source/babel.config.js @@ -1,5 +1,8 @@ module.exports = { presets: [ '@vue/cli-plugin-babel/preset' + ], + plugins: [ + '@babel/plugin-proposal-logical-assignment-operators' ] } diff --git a/lib/www/client/source/package-lock.json b/lib/www/client/source/package-lock.json index 4848aae..4d5517d 100644 --- a/lib/www/client/source/package-lock.json +++ b/lib/www/client/source/package-lock.json @@ -28,6 +28,7 @@ "vuex": "^3.6.2" }, "devDependencies": { + "@babel/plugin-proposal-logical-assignment-operators": "^7.14.5", "@vue/cli-plugin-babel": "~4.4.0", "@vue/cli-plugin-router": "~4.4.0", "@vue/cli-plugin-vuex": "~4.4.0", @@ -250,10 +251,13 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } }, "node_modules/@babel/helper-regex": { "version": "7.10.4", @@ -412,6 +416,22 @@ "@babel/plugin-syntax-json-strings": "^7.8.0" } }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.5.tgz", + "integrity": "sha512-YGn2AvZAo9TwyhlLvCCWxD90Xq8xJ4aSgaX3G5D/8DW94L8aaT+dS5cSP+Z06+rCJERGSr9GxMBZ601xoc2taw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz", @@ -540,6 +560,18 @@ "@babel/helper-plugin-utils": "^7.10.4" } }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", @@ -13524,9 +13556,9 @@ } }, "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", "dev": true }, "@babel/helper-regex": { @@ -13680,6 +13712,16 @@ "@babel/plugin-syntax-json-strings": "^7.8.0" } }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.5.tgz", + "integrity": "sha512-YGn2AvZAo9TwyhlLvCCWxD90Xq8xJ4aSgaX3G5D/8DW94L8aaT+dS5cSP+Z06+rCJERGSr9GxMBZ601xoc2taw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, "@babel/plugin-proposal-nullish-coalescing-operator": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz", @@ -13805,6 +13847,15 @@ "@babel/helper-plugin-utils": "^7.10.4" } }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, "@babel/plugin-syntax-nullish-coalescing-operator": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", diff --git a/lib/www/client/source/package.json b/lib/www/client/source/package.json index a18da27..5ba8262 100644 --- a/lib/www/client/source/package.json +++ b/lib/www/client/source/package.json @@ -26,6 +26,7 @@ "vuex": "^3.6.2" }, "devDependencies": { + "@babel/plugin-proposal-logical-assignment-operators": "^7.14.5", "@vue/cli-plugin-babel": "~4.4.0", "@vue/cli-plugin-router": "~4.4.0", "@vue/cli-plugin-vuex": "~4.4.0", diff --git a/lib/www/client/source/src/lib/throttle.js b/lib/www/client/source/src/lib/throttle.js new file mode 100644 index 0000000..746eefd --- /dev/null +++ b/lib/www/client/source/src/lib/throttle.js @@ -0,0 +1,33 @@ +/** + * Throttle a function call. + * + * It delays `callback` by `delay` ms and ignores any + * repeated calls from `caller` within at most `maxWait` + * milliseconds. + * + * Used to react to server events in cases where we get + * a separate notification for each row of a bulk update. + */ +function throttle (callback, caller, delay = 100, maxWait = 500) { + + const schedule = async () => { + caller.triggeredAt = Date.now(); + caller.timer = setTimeout(async () => { + await callback(); + caller.timer = null; + }, delay); + } + + if (!caller.timer) { + schedule(); + } else { + const elapsed = Date.now() - caller.triggeredAt; + if (elapsed > maxWait) { + cancelTimeout(caller.timer); + schedule(); + } + } + +} + +export default throttle; diff --git a/lib/www/client/source/src/views/SequenceList.vue b/lib/www/client/source/src/views/SequenceList.vue index 64aaf26..13cccf2 100644 --- a/lib/www/client/source/src/views/SequenceList.vue +++ b/lib/www/client/source/src/views/SequenceList.vue @@ -80,6 +80,67 @@ PDF + + + + + + + Send to ASAQC + + + mdi-tray-plus + + + + + + Cancel sending to ASAQC + + Queued since: {{contextMenuItemInTransferQueue.created_on}} + + + + mdi-tray-remove + + + + + + Resend to ASAQC + + Last sent on: {{ contextMenuItemInTransferQueue.created_on }} + + + + mdi-tray-plus + + + + + + Send to ASAQC + + Last send cancelled on: {{contextMenuItemInTransferQueue.updated_on}} + + + + mdi-tray-plus + + + @@ -271,6 +332,23 @@ @@ -371,6 +449,7 @@ tr :nth-child(5), tr :nth-child(8), tr :nth-child(11), tr :nth-child(14) {