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 @@
{{ value == "final" ? "Processed" : value == "raw" ? item.raw_files ? "Acquired" : "In acquisition" : value == "ntbp" ? "NTBP" : `Unknown (${status})` }}
+ mdi-upload
+ mdi-upload-outline
+ mdi-upload-off-outline
+ mdi-upload-off
@@ -371,6 +449,7 @@ tr :nth-child(5), tr :nth-child(8), tr :nth-child(11), tr :nth-child(14) {