diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f5043f..126bdb8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
- Bump `triplestore` and `publication-triplestore` to `v1.3.0-rc.1`. [OP-2492] [OP-3547]
- Add missing docker compose keys. [DL-6490]
- Reorganize delta consumers config to harmonize with the ecosystem
+- Set up the dashboard app [OP-3103]
### Deploy notes
```
drc restart migrations; drc logs -ft --tail=200 migrations
@@ -20,6 +21,26 @@ drc up -d mandatarissen-consumer leidinggevenden-consumer worship-services-main-
drc up -d search elasticsearch
```
+#### Dashboard
+
+The dashboard requires some server specific configuration before it can be used.
+
+##### 1. Dashboard domains
+To configure the domain name that's used by the dashboard frontend, you can add it to the `VIRTUAL_HOST` and `LETSENCRYPT_HOST` environment variables of the identifier service.
+- DEV domain: dev.dashboard.app-organisation-portal.contacthub-dev.s.redhost.be
+- QA domain: qa.dashboard.app-organisation-portal.contacthub-dev.s.redhost.be
+- PROD domain: dashboard.app-organisation-portal.organisaties.s.redpencil.io
+
+##### 2. Dashboard users
+Before we can generate a user we need to add an application salt to the server. Add a `MU_APPLICATION_SALT` environment variable to the `dashboard-login` service in the docker-compose.override.yml file.
+
+Once this is done you can generate a user by running `mu script project-scripts generate-dashboard-login` and following the prompts. A new local migration will be generated which can be run by restarting the migrations service and which will insert the new user into the database.
+
+Store the login credentials as a comment in the docker-compose.override.yml file.
+
+##### 3. (Re)start the needed services
+`drc up -d frontend-dashboard dashboard-login identifier; drc restart dispatcher resource db`
+
## v1.30.5 (2025-03-10)
### Backend
- Added `http://www.w3.org/ns/prov#Location` to delta stream 'public' [DL-6496]
diff --git a/README.md b/README.md
index 4fa218d..1884f32 100644
--- a/README.md
+++ b/README.md
@@ -185,3 +185,26 @@ To make sure the app can share data, producers need to be set up. There is an in
)
}
```
+
+## Configuring the dashboard
+### Accessing the dashboard from your local machine
+
+Since we use dispatcher v2, which dispatches on hostname, we'll have to update `/etc/hosts`.
+Add an entry similar to the following. Ensure the first part of the domain starts with `dashboard`.:
+
+```
+127.0.0.1 dashboard.localhost
+```
+
+### Creating new users
+For now, we use specific logins for the dashboard users. Each environment has its own passwords.
+
+To add a user, make sure to have [mu-cli](https://github.com/mu-semtech/mu-cli) installed first.
+Then in `docker-compose.override.yml`
+```
+ dashboard-login:
+ environment:
+ MU_APPLICATION_SALT: 'a_random_string_with_sufficient_entropy_hence_not_this_one'
+```
+You can now generate a user by running `mu script project-scripts generate-dashboard-login` and following the steps.
+Restart `migrations` and it should insert the new user into the database.
\ No newline at end of file
diff --git a/config/authorization/config.ex b/config/authorization/config.ex
index 1983140..fcb6d7e 100644
--- a/config/authorization/config.ex
+++ b/config/authorization/config.ex
@@ -107,6 +107,19 @@ defmodule Acl.UserGroups.Config do
}"
end
+ defp can_access_dashboard() do
+ %AccessByQuery{
+ vars: [],
+ query: "PREFIX ext:
+ PREFIX mu:
+ SELECT DISTINCT ?account WHERE {
+ ?account.
+ ?account ?session_role.
+ FILTER( ?session_role = \"dashboard-user\" )
+ }"
+ }
+ end
+
def user_groups do
[
%GroupSpec{
@@ -206,6 +219,46 @@ defmodule Acl.UserGroups.Config do
}
]
},
+ # // dashboard users
+ %GroupSpec{
+ name: "dashboard-users",
+ useage: [:read],
+ access: can_access_dashboard(),
+ graphs: [
+ %GraphSpec{
+ graph: "http://mu.semte.ch/graphs/reports",
+ constraint: %ResourceConstraint{
+ resource_types: [
+ "http://lblod.data.gift/vocabularies/reporting/Report",
+ "http://open-services.net/ns/core#Error",
+ "http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#DataContainer",
+ "http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#FileDataObject"
+ ]
+ }
+ },
+ %GraphSpec{
+ graph: "http://mu.semte.ch/graphs/system/jobs",
+ constraint: %ResourceConstraint{
+ resource_types: [
+ "http://vocab.deri.ie/cogs#Job",
+ "http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#DataContainer",
+ "http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#FileDataObject"
+ ]
+ }
+ },
+ # OP stores jobs in multiple graphs, so we configure both until they are merged into one graph
+ %GraphSpec{
+ graph: "http://mu.semte.ch/graphs/jobs",
+ constraint: %ResourceConstraint{
+ resource_types: [
+ "http://vocab.deri.ie/cogs#Job",
+ "http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#DataContainer",
+ "http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#FileDataObject"
+ ]
+ }
+ },
+ ]
+ },
# // CLEANUP
#
diff --git a/config/dispatcher/dispatcher.ex b/config/dispatcher/dispatcher.ex
index a58f69a..f647abf 100644
--- a/config/dispatcher/dispatcher.ex
+++ b/config/dispatcher/dispatcher.ex
@@ -240,6 +240,27 @@ defmodule Dispatcher do
Proxy.forward(conn, path, "http://resource/kbo-organizations/")
end
+ # Dashboard
+ match "/jobs/*path", %{accept: [:json], layer: :api} do
+ Proxy.forward(conn, path, "http://cache/jobs/")
+ end
+
+ match "/tasks/*path", %{accept: [:json], layer: :api} do
+ Proxy.forward(conn, path, "http://cache/tasks/")
+ end
+
+ match "/data-containers/*path", %{accept: [:json], layer: :api} do
+ Proxy.forward(conn, path, "http://cache/data-containers/")
+ end
+
+ match "/job-errors/*path", %{accept: [:json], layer: :api} do
+ Proxy.forward(conn, path, "http://cache/job-errors/")
+ end
+
+ match "/reports/*path", %{accept: [:json], layer: :api} do
+ Proxy.forward(conn, path, "http://resource/reports/")
+ end
+
###############
# LOGIN
###############
@@ -264,6 +285,10 @@ defmodule Dispatcher do
Proxy.forward(conn, path, "http://mocklogin/sessions/")
end
+ match "/sessions/*path", %{ reverse_host: ["dashboard" | _rest] } do
+ Proxy.forward conn, path, "http://dashboard-login/sessions/"
+ end
+
match "/sessions/*path", %{accept: [:any], layer: :api} do
Proxy.forward(conn, path, "http://login/sessions/")
end
@@ -428,6 +453,20 @@ defmodule Dispatcher do
# frontend layer
###############################################################
+ get "/assets/*path", %{ reverse_host: ["dashboard" | _rest], layer: :api } do
+ forward conn, path, "http://frontend-dashboard/assets/"
+ end
+
+ get "/@appuniversum/*path", %{ reverse_host: ["dashboard" | _rest], layer: :api} do
+ forward conn, path, "http://frontend-dashboard/@appuniversum/"
+ end
+
+ match "/*_path", %{ reverse_host: ["dashboard" | _rest], layer: :api } do
+ # *_path allows a path to be supplied, but will not yield
+ # an error that we don't use the path variable.
+ forward conn, [], "http://frontend-dashboard/index.html"
+ end
+
match "/assets/*path", %{layer: :api} do
Proxy.forward(conn, path, "http://frontend/assets/")
end
diff --git a/config/resources/domain.lisp b/config/resources/domain.lisp
index a9fa35f..18fb9c0 100644
--- a/config/resources/domain.lisp
+++ b/config/resources/domain.lisp
@@ -20,3 +20,5 @@
(read-domain-file "files-domain.lisp")
(read-domain-file "dcat.json")
(read-domain-file "privacy-centric-service.json")
+(read-domain-file "job.json")
+(read-domain-file "report.json")
diff --git a/config/resources/files-domain.lisp b/config/resources/files-domain.lisp
index b9dfdcf..36589ef 100644
--- a/config/resources/files-domain.lisp
+++ b/config/resources/files-domain.lisp
@@ -7,7 +7,10 @@
(:created :datetime ,(s-prefix "nfo:fileCreated")))
:has-one `((file :via ,(s-prefix "nie:dataSource")
:inverse t
- :as "download"))
+ :as "download")
+ (data-container :via ,(s-prefix "task:hasFile")
+ :inverse t
+ :as "data-container"))
:resource-base (s-url "http://data.lblod.info/files/")
:features `(no-pagination-defaults include-uri)
:on-path "files")
diff --git a/config/resources/job.json b/config/resources/job.json
new file mode 100644
index 0000000..dba13a1
--- /dev/null
+++ b/config/resources/job.json
@@ -0,0 +1,147 @@
+{
+ "version": "0.1",
+ "prefixes": {
+ "adms": "http://www.w3.org/ns/adms#",
+ "cogs": "http://vocab.deri.ie/cogs#",
+ "dct": "http://purl.org/dc/terms/",
+ "oslc": "http://open-services.net/ns/core#",
+ "nfo": "http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#",
+ "task": "http://redpencil.data.gift/vocabularies/tasks/"
+ },
+ "resources": {
+ "jobs": {
+ "name": "job",
+ "class": "cogs:Job",
+ "attributes": {
+ "created": {
+ "type": "datetime",
+ "predicate": "dct:created"
+ },
+ "modified": {
+ "type": "datetime",
+ "predicate": "dct:modified"
+ },
+ "creator": {
+ "type": "url",
+ "predicate": "dct:creator"
+ },
+ "status": {
+ "type": "url",
+ "predicate": "adms:status"
+ },
+ "operation": {
+ "type": "url",
+ "predicate": "task:operation"
+ }
+ },
+ "relationships": {
+ "error": {
+ "predicate": "task:error",
+ "target": "job-error",
+ "cardinality": "one"
+ },
+ "tasks": {
+ "predicate": "dct:isPartOf",
+ "target": "group",
+ "cardinality": "many",
+ "inverse": true
+ }
+ },
+ "features": ["include-uri"],
+ "new-resource-base": "http://redpencil.data.gift/id/job/"
+ },
+ "tasks": {
+ "name": "task",
+ "class": "task:Task",
+ "attributes": {
+ "created": {
+ "type": "datetime",
+ "predicate": "dct:created"
+ },
+ "modified": {
+ "type": "datetime",
+ "predicate": "dct:modified"
+ },
+ "status": {
+ "type": "url",
+ "predicate": "adms:status"
+ },
+ "operation": {
+ "type": "url",
+ "predicate": "task:operation"
+ },
+ "index": {
+ "type": "string",
+ "predicate": "task:index"
+ }
+ },
+ "relationships": {
+ "error": {
+ "predicate": "task:error",
+ "target": "job-error",
+ "cardinality": "one"
+ },
+ "job": {
+ "predicate": "dct:isPartOf",
+ "target": "job",
+ "cardinality": "one"
+ },
+ "parent-tasks": {
+ "predicate": "cogs:dependsOn",
+ "target": "task",
+ "cardinality": "many"
+ },
+ "results-containers": {
+ "predicate": "task:resultsContainer",
+ "target": "data-container",
+ "cardinality": "many"
+ },
+ "input-containers": {
+ "predicate": "task:inputContainer",
+ "target": "data-container",
+ "cardinality": "many"
+ }
+ },
+ "features": ["include-uri"],
+ "new-resource-base": "http://redpencil.data.gift/id/task/"
+ },
+ "job-errors": {
+ "name": "job-error",
+ "class": "oslc:Error",
+ "attributes": {
+ "message": {
+ "type": "string",
+ "predicate": "oslc:message"
+ }
+ },
+ "features": ["include-uri"],
+ "new-resource-base": "http://redpencil.data.gift/id/jobs/error/"
+ },
+ "data-containers": {
+ "name": "data-container",
+ "class": "nfo:DataContainer",
+ "attributes": {
+ "has-graph": {
+ "type": "url",
+ "predicate": "task:hasGraph"
+ }
+ },
+ "relationships": {
+ "result-from-tasks": {
+ "predicate": "task:resultsContainer",
+ "target": "task",
+ "cardinality": "many",
+ "inverse": true
+ },
+ "input-from-tasks": {
+ "predicate": "task:inputContainer",
+ "target": "task",
+ "cardinality": "many",
+ "inverse":true
+ }
+ },
+ "features": ["include-uri"],
+ "new-resource-base": "http://redpencil.data.gift/id/dataContainers/"
+ }
+ }
+}
\ No newline at end of file
diff --git a/config/resources/report.json b/config/resources/report.json
new file mode 100644
index 0000000..2463efe
--- /dev/null
+++ b/config/resources/report.json
@@ -0,0 +1,36 @@
+{
+ "version": "0.1",
+ "prefixes": {
+ "dct": "http://purl.org/dc/terms/",
+ "prov": "http://www.w3.org/ns/prov#"
+ },
+ "resources": {
+ "reports": {
+ "name": "report",
+ "class": "http://lblod.data.gift/vocabularies/reporting/Report",
+ "attributes": {
+ "created": {
+ "type": "datetime",
+ "predicate": "dct:created"
+ },
+ "title": {
+ "type": "string",
+ "predicate": "dct:title"
+ },
+ "description": {
+ "type": "string",
+ "predicate": "dct:description"
+ }
+ },
+ "relationships": {
+ "file": {
+ "predicate": "prov:generated",
+ "target": "file",
+ "cardinality": "one"
+ }
+ },
+ "features": ["include-uri"],
+ "new-resource-base": "http://data.lblod.info/id/reports/"
+ }
+ }
+}
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 6a8aa37..162eaf3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -147,6 +147,14 @@ services:
logging: *default-logging
links:
- db:database
+ dashboard-login:
+ image: semtech/mu-login-service:3.0.0
+ links:
+ - db:database
+ labels:
+ - "logging=true"
+ restart: always
+ logging: *default-logging
accountdetail:
image: lblod/account-detail-service:0.1.0
labels:
@@ -202,6 +210,12 @@ services:
- "logging=true"
restart: always
logging: *default-logging
+ frontend-dashboard:
+ image: lblod/frontend-dashboard:1.7.0
+ labels:
+ - "logging=true"
+ restart: always
+ logging: *default-logging
uri-info:
image: redpencil/mu-uri-info-service:0.2.1
links:
diff --git a/scripts/project/config.json b/scripts/project/config.json
index 2a8ae6f..471f975 100644
--- a/scripts/project/config.json
+++ b/scripts/project/config.json
@@ -16,6 +16,21 @@
"mounts": {
"app": "/data/app/"
}
+ },
+ {
+ "documentation": {
+ "command": "generate-dashboard-login",
+ "description": "generate a login account for the jobs dashboard",
+ "arguments": []
+ },
+ "environment": {
+ "image": "ruby",
+ "interactive": true,
+ "script": "generate-dashboard-login/run.sh"
+ },
+ "mounts": {
+ "app": "/project/"
+ }
}
]
-}
+}
\ No newline at end of file
diff --git a/scripts/project/generate-dashboard-login/run.sh b/scripts/project/generate-dashboard-login/run.sh
new file mode 100755
index 0000000..03f4aba
--- /dev/null
+++ b/scripts/project/generate-dashboard-login/run.sh
@@ -0,0 +1,80 @@
+#!/usr/bin/env ruby
+require 'bundler/inline'
+require 'yaml'
+require 'securerandom'
+require 'date'
+
+$stdout.sync = true
+print "installing dependencies..."
+gemfile do
+ source 'https://rubygems.org'
+ gem 'bcrypt'
+end
+puts "done"
+
+salt = ""
+begin
+ File.open("/project/docker-compose.override.yml") do |file|
+ config = YAML.load(file)
+ salt = config.dig("services", "dashboard-login", "environment", "MU_APPLICATION_SALT")
+ throw "invalid salt" unless salt
+ end
+rescue
+ puts "Tried to locate login salt in docker-compose.override.yml, but couldn't find it."
+ puts "Please make sure a salt is configured:"
+ puts <
+PREFIX people:
+PREFIX accounts:
+PREFIX dcterms:
+PREFIX mu:
+PREFIX ext:
+PREFIX account:
+PREFIX xsd:
+INSERT DATA {
+ GRAPH {
+ people:#{person_uuid} a foaf:Person ;
+ foaf:name "#{account_name}";
+ foaf:account accounts:#{account_uuid};
+ mu:uuid "#{person_uuid}";
+ dcterms:created "#{now}"^^xsd:datetime;
+ dcterms:modified "#{now}"^^xsd:datetime.
+ accounts:#{account_uuid} a foaf:OnlineAccount;
+ foaf:accountName "#{account_name}";
+ mu:uuid "#{account_uuid}";
+ account:password """#{hashed_password}""";
+ account:salt "#{account_salt}";
+ account:status ;
+ dcterms:created "#{now}"^^xsd:datetime;
+ ext:sessionRole "dashboard-user";
+ dcterms:modified "#{now}"^^xsd:datetime.
+}}
+)
+
+timestamp = DateTime.now.strftime("%Y%m%d%H%M%L")
+filename = "#{timestamp}-create-user-#{account_name.gsub(" ", "-")}.sparql"
+File.write("/project/config/migrations/local/#{filename}", query)
+puts "generated migration ./config/migrations/local/#{filename} for #{account_name.inspect} : #{password.inspect}"
\ No newline at end of file