浏览代码

Create example configuration

Arkadiusz Ryś 2 年之前
父节点
当前提交
d42a1acbd6
共有 11 个文件被更改,包括 1189 次插入66 次删除
  1. 2 0
      .gitignore
  2. 16 12
      README.md
  3. 2 0
      config/backend.env
  4. 10 0
      config/drawio.env
  5. 1 1
      config/fuseki.env
  6. 184 51
      docker-compose.yml
  7. 850 0
      drawio/dtdesign.js
  8. 1 0
      drawio/meta.drawio
  9. 1 0
      drawio/object.drawio
  10. 118 0
      local.yml
  11. 4 2
      tasks.py

+ 2 - 0
.gitignore

@@ -2,5 +2,7 @@ __pycache__/
 venv/
 build/
 dist/
+fuseki/
+fuseki-extra/
 *.egg-info
 *.log

+ 16 - 12
README.md

@@ -4,23 +4,27 @@
 
 Tool to easily deploy a demo for FlandersMake Digital Twin Design project Work Package 2.
 
-[//]: # (`docker network create --subnet=172.40.0.0/16 local`)
+[//]: # (`docker network create --subnet=172.50.0.0/16 twin`)
+
+## Local endpoint
+
+- http://localhost:8445/?p=ftgpm;dtdesign&dev=1&stealth=1#
 
 ## Components
 
-* [WEE](https://msdl.uantwerpen.be/git/lucasalbertins/wee "Workflow Enactment Engine")_
-* [DrawIO](https://msdl.uantwerpen.be/git/jexelmans/drawio)_
-* [drawio2oml](https://msdl.uantwerpen.be/git/jexelmans/drawio2oml)_
-* [drawio2py](https://msdl.uantwerpen.be/git/jexelmans/drawio2py)_
-* [dtdesign](https://msdl.uantwerpen.be/git/lucasalbertins/DTDesign)_
-* [spendpoint](https://msdl.uantwerpen.be/git/arys/spendpoint)_
-* [Graph Exploring Tool](https://msdl.uantwerpen.be/git/arys/graph-exploring-tool)_
-* [xopp2py](https://msdl.uantwerpen.be/git/jexelmans/xopp2py)_
-* [Ontopoint](https://msdl.uantwerpen.be/git/arys/ontopoint)_
-* [Grap View](https://msdl.uantwerpen.be/git/arys/graph-view)_
+- [WEE](https://msdl.uantwerpen.be/git/lucasalbertins/wee "Workflow Enactment Engine")_
+- [DrawIO](https://msdl.uantwerpen.be/git/jexelmans/drawio)_
+- [drawio2oml](https://msdl.uantwerpen.be/git/jexelmans/drawio2oml)_
+- [drawio2py](https://msdl.uantwerpen.be/git/jexelmans/drawio2py)_
+- [DTDesign](https://msdl.uantwerpen.be/git/lucasalbertins/DTDesign)_
+- [spendpoint](https://msdl.uantwerpen.be/git/arys/spendpoint)_
+- [Graph Exploring Tool](https://msdl.uantwerpen.be/git/arys/graph-exploring-tool)_
+- [xopp2py](https://msdl.uantwerpen.be/git/jexelmans/xopp2py)_
+- [Ontopoint](https://msdl.uantwerpen.be/git/arys/ontopoint)_
+- [Graph View](https://msdl.uantwerpen.be/git/arys/graph-view)_
 
 ## History
 
 ### 0.0.0 (yyyy-mm-dd)
 
-* No history yet.
+- No history yet.

+ 2 - 0
config/backend.env

@@ -0,0 +1,2 @@
+WEE_ENDPOINT=wee.rys.app
+FUSEKI_ENDPOINT=fuseki.rys.app

文件差异内容过多而无法显示
+ 10 - 0
config/drawio.env


+ 1 - 1
config/fuseki.env

@@ -1,2 +1,2 @@
-ADMIN_PASSWORD=FusekiAdminPasswordForAccess
+ADMIN_PASSWORD=ChangeThisFusekiPasswordBeforeDeployment
 JVM_ARGS=-Xmx2g

+ 184 - 51
docker-compose.yml

@@ -1,78 +1,211 @@
 version: "3.8"
 
 services:
-  fuseki:
-    image: registry.rys.one/dtdesign/fuseki
-    container_name: fuseki
-    networks:
-      local:
-        ipv4_address: 172.40.200.10
-    ports:
-      - "3030:3030"
-    restart: unless-stopped
-    volumes:
-      - "./fuseki:/fuseki"
-      - "./fuseki-extra:/fuseki-extra"
-    env_file:
-      - "./config/fuseki.env"
+#  dtd-fuseki:
+#    image: registry.rys.one/dtdesign/fuseki
+#    container_name: dtd-fuseki
+#    networks:
+#      twin:
+#        ipv4_address: 172.50.10.10
+#    dns:
+#      - 192.168.0.11
+#      - 1.1.1.1
+#      - 1.0.0.1
+#      - 8.8.8.8
+#      - 8.8.4.4
+#    #ports:
+#    #  - "3030:3030"
+#    labels:
+#      - traefik.enable=true
+#      - traefik.http.routers.dtd-fuseki.entrypoints=web-secure
+#      - traefik.http.routers.dtd-fuseki.rule=Host(`fuseki.rys.app`)
+#      - traefik.http.routers.dtd-fuseki.tls.certresolver=letsencrypt
+#      - traefik.http.routers.dtd-fuseki.service=dtd-fuseki-svc
+#      - traefik.http.services.dtd-fuseki-svc.loadbalancer.server.port=3030
+#    restart: unless-stopped
+#    volumes:
+#      - "./fuseki:/fuseki"
+#      - "./fuseki-extra:/fuseki-extra"
+#    env_file:
+#      - "./config/fuseki.env"
 
-  spendpoint:
-    image: registry.rys.one/dtdesign/spendpoint
-    container_name: spendpoint
+  dtd-spendpoint:
+    image: registry.rys.one/dtdesign/spendpoint:dev
+    container_name: dtd-spendpoint
     networks:
-      local:
-        ipv4_address: 172.40.200.20
-    ports:
-      - "8000:8000"
-#    depends_on:
-#      - models
+      twin:
+        ipv4_address: 172.50.10.20
+    dns:
+      - 192.168.0.11
+      - 1.1.1.1
+      - 1.0.0.1
+      - 8.8.8.8
+      - 8.8.4.4
+    #ports:
+    #  - "8000:8000"
+    labels:
+      - traefik.enable=true
+      - traefik.http.routers.dtd-spendpoint.entrypoints=web-secure
+      - traefik.http.routers.dtd-spendpoint.rule=Host(`spendpoint.rys.app`)
+      - traefik.http.routers.dtd-spendpoint.tls.certresolver=letsencrypt
+      - traefik.http.routers.dtd-spendpoint.service=dtd-spendpoint-svc
+      - traefik.http.services.dtd-spendpoint-svc.loadbalancer.server.port=8000
     restart: unless-stopped
     volumes:
-      - "./data:/app/data:ro"
-#    env_file:
-#      - "./config/spendpoint.env"
+      - "./spendpoint:/app/data:ro"
 
-  outliers:
-    image: registry.rys.one/dtdesign/dtdesign/outliers:main
-    container_name: outliers
+  dtd-outliers:
+    image: registry.rys.one/dtdesign/dtdesign/outliers:dev
+    container_name: dtd-outliers
     networks:
-      local:
-        ipv4_address: 172.40.200.50
-    ports:
-      - "9090:9090"
+      twin:
+        ipv4_address: 172.50.10.30
+    dns:
+      - 192.168.0.11
+      - 1.1.1.1
+      - 1.0.0.1
+      - 8.8.8.8
+      - 8.8.4.4
+    #ports:
+    #  - "9090:9090"
+    labels:
+      - traefik.enable=true
+      - traefik.http.routers.dtd-outliers.entrypoints=web-secure
+      - traefik.http.routers.dtd-outliers.rule=Host(`outliers.rys.app`)
+      - traefik.http.routers.dtd-outliers.tls.certresolver=letsencrypt
+      - traefik.http.routers.dtd-outliers.service=dtd-outliers-svc
+      - traefik.http.services.dtd-outliers-svc.loadbalancer.server.port=9090
     restart: unless-stopped
-    volumes:
-      - "./data:/app/data:ro"
-#    env_file:
-#      - "./config/outlier.env"
-
-networks:
-  local:
-    external: true
+    # TODO This needs to be the csv directory and needs to map to the same dir as the backend csv
+#    volumes:
+#      - "./outliers:/app/data:ro"
 
-#  models:
-#    image: registry.rys.one/dtdesign/models
-#    container_name: models
+#  dtd-models:
+#    image: registry.rys.one/dtdesign/models:dev
+#    container_name: dtd-models
 #    networks:
-#      web:
-#        ipv4_address: 172.40.200.10
+#      twin:
+#        ipv4_address: 172.50.10.40
+#    dns:
+#      - 192.168.0.11
+#      - 1.1.1.1
+#      - 1.0.0.1
+#      - 8.8.8.8
+#      - 8.8.4.4
 #    restart: unless-stopped
 #    volumes:
 #      - "./build:/app/build"
 #    env_file:
 #      - "./config/models.env"
 
-#  graph-exploring-tool:
+#  dtd-graph-exploring-tool:
 #    image: registry.rys.one/dtdesign/graph-exploring-tool
-#    container_name: graph-exploring-tool
+#    container_name: dtd-graph-exploring-tool
 #    networks:
-#      web:
-#        ipv4_address: 172.40.200.30
+#      twin:
+#        ipv4_address: 172.50.10.50
+#    dns:
+#      - 192.168.0.11
+#      - 1.1.1.1
+#      - 1.0.0.1
+#      - 8.8.8.8
+#      - 8.8.4.4
 #    depends_on:
 #      - models
 #      - spendpoint
 #    restart: unless-stopped
 #    volumes:
-#      - "./palette:/app/palette:ro"
+#      - "./get/data:/app/data:ro"
 #    env_file:
 #      - "./config/graph-exploring-tool.env"
+
+  dtd-drawio:
+    image: registry.rys.one/diagram/drawio:dev
+    container_name: dtd-drawio
+    networks:
+      twin:
+        ipv4_address: 172.50.10.60
+    dns:
+      - 192.168.0.11
+      - 1.1.1.1
+      - 1.0.0.1
+      - 8.8.8.8
+      - 8.8.4.4
+    #ports:
+    #  - "8445:8080"
+    #  - "8443:8443"
+    labels:
+      - traefik.enable=true
+      - traefik.http.routers.dtd-drawio.entrypoints=web-secure
+      - traefik.http.routers.dtd-drawio.rule=Host(`workflow.rys.app`)
+      - traefik.http.routers.dtd-drawio.tls.certresolver=letsencrypt
+      - traefik.http.routers.dtd-drawio.service=dtd-drawio-svc
+      - traefik.http.services.dtd-drawio-svc.loadbalancer.server.port=8080
+    restart: unless-stopped
+    volumes:
+      - "./drawio/dtdesign.js:/usr/local/tomcat/webapps/draw/plugins/dtdesign.js"
+      - "./drawio/object.drawio:/usr/local/tomcat/webapps/draw/libraries/object.drawio"
+      - "./drawio/meta.drawio:/usr/local/tomcat/webapps/draw/libraries/meta.drawio"
+    env_file:
+      - "./config/drawio.env"
+
+  dtd-wee:
+    image: registry.rys.one/dtdesign/wee:dev
+    container_name: dtd-wee
+    networks:
+      twin:
+        ipv4_address: 172.50.10.70
+    dns:
+      - 192.168.0.11
+      - 1.1.1.1
+      - 1.0.0.1
+      - 8.8.8.8
+      - 8.8.4.4
+    #ports:
+    #  - "8081:8081"
+    labels:
+      - traefik.enable=true
+      - traefik.http.routers.dtd-wee.entrypoints=web-secure
+      - traefik.http.routers.dtd-wee.rule=Host(`wee.rys.app`)
+      - traefik.http.routers.dtd-wee.tls.certresolver=letsencrypt
+      - traefik.http.routers.dtd-wee.service=dtd-wee-svc
+      - traefik.http.services.dtd-wee-svc.loadbalancer.server.port=8081
+    restart: unless-stopped
+
+  dtd-backend:
+    image: registry.rys.one/dtdesign/drawio2oml/backend:dev
+    container_name: dtd-backend
+    networks:
+      twin:
+        ipv4_address: 172.50.10.80
+    dns:
+      - 192.168.0.11
+      - 1.1.1.1
+      - 1.0.0.1
+      - 8.8.8.8
+      - 8.8.4.4
+    #ports:
+    #  - "5000:5000"
+    labels:
+      - traefik.enable=true
+      - traefik.http.routers.dtd-backend.entrypoints=web-secure
+      - traefik.http.routers.dtd-backend.rule=Host(`git.rys.app`)
+      - traefik.http.routers.dtd-backend.tls.certresolver=letsencrypt
+      - traefik.http.routers.dtd-backend.service=dtd-backend-svc
+      - traefik.http.services.dtd-backend-svc.loadbalancer.server.port=5000
+      
+      - traefik.http.routers.dtd-fuseki.entrypoints=web-secure
+      - traefik.http.routers.dtd-fuseki.rule=Host(`fuseki.rys.app`)
+      - traefik.http.routers.dtd-fuseki.tls.certresolver=letsencrypt
+      - traefik.http.routers.dtd-fuseki.service=dtd-fuseki-svc
+      - traefik.http.services.dtd-fuseki-svc.loadbalancer.server.port=3030
+    restart: unless-stopped
+    volumes:
+      - "./backend/ontology:/app/ontology"
+      - "./backend/shape_lib:/app/shape_lib"
+    env_file:
+      - "./config/backend.env"
+
+networks:
+  twin:
+    external: true

+ 850 - 0
drawio/dtdesign.js

@@ -0,0 +1,850 @@
+Draw.loadPlugin(function(ui) {
+
+const WEE = "wee.rys.app";
+
+const BACKEND = "dtb.rys.app";
+const EXPECTED_BACKEND_VERSION = 6; // expected backend version
+
+const SPARQL_SERVER   = "fuseki.rys.app"
+const SPARQL_ENDPOINT = "/SystemDesignOntology2Layers/"
+
+const dropVocabularyPrefix = str => str.substring(41);
+const dropDescriptionPrefix = str => str.substring(30);
+const dropArtifactPrefix = str => str.substring(41);
+const addFormalismsPrefix = str => "http://ua.be/sdo2l/vocabulary/formalisms/" + str;
+const addArtifactPrefix = str => "http://ua.be/sdo2l/description/artifacts/" + str;
+
+const QUERIES = {
+  // Query that navigates the given link from the given source element, and returns the IRI and most-concrete-type of the target element.
+  // If `reverse` is true, then incoming links are returned instead.
+  getOutgoingLink: (iri, link_type, reverse=false) => `\
+PREFIX object_diagram: <http://ua.be/sdo2l/vocabulary/formalisms/object_diagram#>
+PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+PREFIX pt: <http://ua.be/sdo2l/vocabulary/formalisms/processtraces#>
+
+SELECT DISTINCT ?element ?type
+WHERE {
+  ${reverse ? `?element <${link_type}> <${iri}>` : `<${iri}> <${link_type}> ?element`}.
+  ?element a ?type .
+  {
+    ?type rdfs:subClassOf object_diagram:Object .
+  } UNION {
+    ?type rdfs:subClassOf pt:Event .
+  } UNION {
+    ?type rdfs:subClassOf pt:Artifact .
+  }
+  FILTER(NOT EXISTS {
+    ?more_concrete_type rdfs:subClassOf ?type .
+    ?element a ?more_concrete_type .
+    FILTER(?more_concrete_type != ?type)
+  })
+  FILTER(NOT EXISTS {
+    ?more_concrete_link_type rdfs:subPropertyOf <${link_type}> .
+    ${reverse ? `?element ?more_concrete_link_type <${iri}>` : `<${iri}> ?more_concrete_link_type ?element`} . 
+    FILTER(?more_concrete_link_type != <${link_type}>)
+  })
+}`,
+
+  // Query that gets the IRI and most-concrete-type of a given drawio cell.
+  getCellStuff: (cellId) => `\
+PREFIX drawio: <http://ua.be/sdo2l/vocabulary/formalisms/drawio#>
+PREFIX object_diagram: <http://ua.be/sdo2l/vocabulary/formalisms/object_diagram#>
+PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+
+SELECT DISTINCT ?element ?type
+WHERE {
+  ?element drawio:hasDrawioId "${cellId}" .
+  ?element a ?type .
+  ?type rdfs:subClassOf object_diagram:Object .
+  NOT EXISTS {
+    ?more_concrete_type rdfs:subClassOf ?type .
+    ?element a ?more_concrete_type .
+    FILTER(?more_concrete_type != ?type)
+  }
+}`,
+
+  // Query that, for a given cell IRI, gets the name of the diagram containing the cell, and the ID of the cell.
+  getDiagramAndCellId: cellIri => `\
+PREFIX drawio: <http://ua.be/sdo2l/vocabulary/formalisms/drawio#>
+PREFIX object_diagram: <http://ua.be/sdo2l/vocabulary/formalisms/object_diagram#>
+
+SELECT DISTINCT ?diagramName ?cellId
+WHERE {
+  <${cellIri}> drawio:hasDrawioId ?cellId .
+  <${cellIri}> object_diagram:inModel ?model .
+  ?model object_diagram:hasName ?diagramName .
+}`,
+
+  // Query that gets ALL subclass relations.
+  getSubClassRelations: `
+PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+
+SELECT DISTINCT ?subclass ?superclass
+WHERE {
+  ?subclass rdfs:subClassOf ?superclass
+}`,
+
+  getAllGraphs: `
+SELECT ?g (COUNT(*) as ?count) {GRAPH ?g {?s ?p ?o}} GROUP BY ?g
+`,
+
+  getArtifactFilename: artifactIri => `\
+PREFIX pt: <http://ua.be/sdo2l/vocabulary/formalisms/processtraces#>
+PREFIX pm: <http://ua.be/sdo2l/vocabulary/formalisms/pm#>
+PREFIX base: <http://ua.be/sdo2l/vocabulary/base/base#>
+
+SELECT DISTINCT ?filename ?formalismName
+WHERE {
+  <${artifactIri}> pt:hasLocation ?filename .
+  <${artifactIri}> pt:relatesTo ?pmArtifact .
+  ?pmArtifact pm:hasType ?ftgFormalism .
+  ?ftgFormalism base:hasGUID ?formalismName .
+}`,
+
+  getModels: `\
+PREFIX object_diagram: <http://ua.be/sdo2l/vocabulary/formalisms/object_diagram#>
+PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+PREFIX cs_as: <http://ua.be/sdo2l/vocabulary/formalisms/cs_as#>
+
+SELECT DISTINCT ?model ?type
+WHERE {
+  ?model a object_diagram:Model .
+  ?model a ?type .
+  ?type rdfs:subClassOf object_diagram:Model .
+  FILTER(NOT EXISTS {
+    ?model a cs_as:CorrespondenceModel .
+  })
+  FILTER(NOT EXISTS {
+    ?more_concrete_type rdfs:subClassOf ?type .
+    ?model a ?more_concrete_type .
+    FILTER(?more_concrete_type != ?type)
+  })
+}
+`,
+
+  getProperty: (iri, propertyIri) => `\
+SELECT DISTINCT ?value
+WHERE {
+  <${iri}> <${propertyIri}> ?value .
+}
+`,
+
+  insertTraceLink: (fromIri, toIri) => `\
+PREFIX trace: <http://ua.be/sdo2l/vocabulary/formalisms/traceability_model#>
+INSERT DATA {
+  GRAPH <http://ua.be/sdo2l/description/traces> {
+    <${fromIri}> trace:traceLinkTo   <${toIri}> .
+    <${fromIri}> trace:traceLinkFrom <${toIri}> .
+  }
+}
+`,
+};
+
+
+
+// When rebuilding OML, all graphs will be deleted from Fuseki, except for the following (which are not sourced from OML, but inserted directly as RDF triples into Fuseki)
+GRAPHS_NOT_TO_DELETE = [
+  "http://ua.be/sdo2l/description/traces",
+];
+
+const typeToDescription = new Map([
+  ["ftg#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `ftg "${modelName}"`)],
+  ["xopp#Text", async (element, type, getProperty) => getProperty("xopp#hasText").then(text => `text "${text}"`)],
+  ["xopp#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `xournal++ model "${modelName}"`)],
+  ["drawio#Cell", async (element, type, getProperty) => getProperty("drawio#hasDrawioId").then(id => `cell "${id}"`)],
+  ["drawio#Vertex", async (element, type, getProperty) => getProperty("drawio#hasDrawioId").then(id => `vertex "${id}"`)],
+  ["drawio#Edge", async (element, type, getProperty) => getProperty("drawio#hasDrawioId").then(id => `edge "${id}"`)],
+  ["drawio#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `drawio model "${modelName}"`)],
+  ["pm#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `process model "${modelName}"`)],
+  ["pm#Activity", async (element, type, getProperty) => element],
+  ["pm#Artifact", async (element, type, getProperty) => element],
+  ["pm#CtrlInputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `ctrl inport "${portname}"`)],
+  ["pm#CtrlOutputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `ctrl outport "${portname}"`)],
+  ["pm#DataInputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `data inport "${portname}"`)],
+  ["pm#DataOutputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `data outport "${portname}"`)],
+])
+
+// (Hardcoded) link types exposed to the user.
+const typeToLinkType = new Map([
+  ["object_diagram#Object", [
+    {
+      relation: "cs_as#parsedAs",
+      description: `Is parsed as`,
+    },
+    {
+      relation: "cs_as#renderedAs",
+      description: `Is rendered as`,
+    },
+    // Outcommented, because it's not an interesting relation:
+    {
+      relation: "object_diagram#inModel",
+      description: `Is part of`,
+    },
+    {
+      relation: "traceability_model#traceLinkTo",
+      description: `Trace-link (outgoing)`,
+    },
+    {
+      relation: "traceability_model#traceLinkFrom",
+      description: `Trace-link (incoming)`,
+    },
+  ]],
+  ["drawio#Model", [
+    {
+      relation: "drawio#hasRootCell",
+      description: `Has root`,
+    },
+  ]],
+  // Outcommented, blows up:
+  // ["drawio#Cell", [
+  //   {
+  //     relation: "drawio#hasChild",
+  //     description: `Has child`,
+  //   },
+  // ]],
+  ["pm#Activity", [
+    {
+      relation: "pm#isTransformation",
+      description: `Is typed by`,
+    },
+    {
+      relation: "pm#hasPort",
+      description: `Has`,
+    },
+  ]],
+  ["pm#element", [
+    {
+      relation: "processtraces#relatesTo",
+      description: `Enacted by`,
+      reverse: true,
+    },
+  ]],
+  ["processtraces#element", [
+    {
+      relation: "processtraces#relatesTo",
+      description: `Enactment of`,
+    },
+    // pt-element is not a sub-type of 'Object', so we must repeat this:
+    {
+      relation: "cs_as#renderedAs",
+      description: `Is rendered as`,
+    },
+  ]],
+  ["pm#Artifact", [
+    {
+      relation: "pm#hasType",
+      description: `Is typed by`,
+    },
+  ]],
+  ["ftg#Transformation", [
+    {
+      relation: "pm#occursAsActivity",
+      description: `Occurs as`,
+    }
+  ]],
+  ["ftg#Formalism", [
+    {
+      relation: "pm#occursAsArtifact",
+      description: `Occurs as`,
+    },
+  ]],
+  ["xopp#Model", [
+    {
+      relation: "xopp#hasPage",
+      description: `Has page`,
+    },
+  ]],
+  ["xopp#Page", [
+    {
+      relation: "xopp#hasLayer",
+      description: `Has layer`,
+    },
+  ]],  
+  ["xopp#Layer", [
+    {
+      relation: "xopp#hasElement",
+      description: `Has element`,
+    },
+  ]],
+]);
+
+// mapping from class-IRI to array of superclass-IRIs
+const superclasses = new Map(); // map is populated as soon as the plugin is loaded
+
+function getQueries(type) {
+  const result = [].concat(
+    typeToLinkType.get(dropVocabularyPrefix(type)) || [],
+    ... (superclasses.get(type) || []).map(supertype => typeToLinkType.get(dropVocabularyPrefix(supertype)) || []));
+  console.log("getQueries,type=",type,"superclasses=",superclasses.get(type),"result=",result);
+  return result;
+}
+
+function querySPARQL(query) {
+  const body = new URLSearchParams();
+  body.append("query", query);
+  return fetch(SPARQL_SERVER+SPARQL_ENDPOINT, {
+    headers: new Headers({"Content-Type": "application/x-www-form-urlencoded"}),
+    method: "POST",
+    body,
+  })
+  .then(res => res.json())
+  .then(json => {
+    console.log("Query:\n"+query+"\Result:",json.results.bindings);
+    return json.results.bindings;
+  })  
+}
+
+function updateSPARQL(update) {
+  const body = new URLSearchParams();
+  body.append("update", update);
+  return fetch(SPARQL_SERVER+SPARQL_ENDPOINT, {
+    headers: new Headers({"Content-Type": "application/x-www-form-urlencoded"}),
+    method: "POST",
+    body,
+  })
+  .then(res => {
+    console.log("Update:\n"+update+"\Result:",res);
+  })
+}
+
+
+const defaultSettings = {
+  dialogPosX: 100,
+  dialogPosY: 100,
+  dialogWidth: 240,
+  dialogHeight: 400,
+};
+
+fetch(BACKEND+"/version")
+.then(response => response.json())
+.catch(() => 0) // parsing failed - probably backend doesn't even have a version
+.then(version => {
+
+  if (version !== EXPECTED_BACKEND_VERSION) {
+    alert("Incorrect DTDesign Python backend version.\nExpected: " + EXPECTED_BACKEND_VERSION + ", got: " + version + ".\nRefusing to load plugin. Please upgrade :)");
+    return;
+  }
+  else {
+    console.log("Backend version is ", version);
+    console.log("All good.")
+  }
+
+  // Loads all 'rdfs:subClassOf' relations and stores them in a mapping.
+  function loadSuperclasses() {
+    return querySPARQL(QUERIES.getSubClassRelations)
+    .then(results => {
+      for (const {subclass, superclass} of results) {
+        const superclasslist = superclasses.get(subclass.value) || (() => {
+          const list = [];
+          superclasses.set(subclass.value, list);
+          return list;
+        })();
+        superclasslist.push(superclass.value);
+      }
+    });    
+  }
+
+  loadSuperclasses();
+
+  // Override context menu when right-clicking somewhere in the diagram:
+  const oldFactoryMethod = ui.editor.graph.popupMenuHandler.factoryMethod;
+  ui.editor.graph.popupMenuHandler.factoryMethod = function(menu, cell, evt) {
+    const modelverseMenu = menu.addItem("Knowledge Graph...", null, null);
+    const createLinkMenu = menu.addItem("Create traceability link to...", null, null);
+    menu.addSeparator();
+    oldFactoryMethod.apply(this, arguments);
+
+    const entry = (element, type) => element+':'+type;
+
+    function addMenuItem(element, type, maxRecursion, parentMenuItem, description, alreadyVisited, onClick) {
+      if (alreadyVisited.has(entry(element, type))) {
+        return; // don't go in circles
+      }
+      const shortType = dropVocabularyPrefix(type);
+      const itemDescriptionGen = typeToDescription.get(shortType) || (async () => shortType);
+      itemDescriptionGen(dropArtifactPrefix(element), shortType, property => {
+        return querySPARQL(QUERIES.getProperty(element, addFormalismsPrefix(property)))
+        .then(results => {
+          if (results.length > 0) return results[0].value.value;
+          else return "";
+        });
+      })
+      .then(itemDescription => {
+        const menuItemText = description + ' ' + itemDescription;
+        let createdMenuItem;
+        // If the menu item is a (subtype of a) drawio cell, then clicking on it will take us to that cell in drawio.
+        if (onClick) {
+          createdMenuItem = menu.addItem(menuItemText, null, () => onClick(element), parentMenuItem);
+        }
+        else if ((superclasses.get(type) || []).some(t => t.endsWith("drawio#Cell"))) {
+          createdMenuItem = menu.addItem(menuItemText, null, () => goto(element), parentMenuItem);
+        }
+        else {
+          createdMenuItem = menu.addItem(menuItemText, null, null, parentMenuItem);
+        }
+        if ((superclasses.get(type) || []).some(t => t.endsWith("processtraces#Artifact"))) {
+          querySPARQL(QUERIES.getArtifactFilename(element))
+          .then(results => {
+            for (const {filename, formalismName} of results) {
+              menu.addItem(`File "${filename.value}"`, null, () => (createDownloadHandler(formalismName.value))(filename.value), createdMenuItem);
+            }
+          })
+        }
+        alreadyVisited.add(entry(element, type));
+        if (maxRecursion <= 0) {
+          return;
+        }
+        addSubmenuItems(element, type, createdMenuItem, maxRecursion, alreadyVisited, onClick);        
+      })
+    }
+    function addSubmenuItems(element, type, parentMenuItem, maxRecursion, alreadyVisited, onClick) {
+      const queries = getQueries(type);
+      for (const {relation, description, reverse} of queries) {
+        querySPARQL(QUERIES.getOutgoingLink(element, "http://ua.be/sdo2l/vocabulary/formalisms/"+relation, reverse))
+        .then(results => {
+          for (const {element, type} of results) {
+            addMenuItem(element.value, type.value, maxRecursion-1, parentMenuItem, description, alreadyVisited, onClick);
+          }
+        })
+      }
+    }
+
+    // Populate ModelVerse submenu asynchronously:
+    if (cell && cell.vertex) {
+      querySPARQL(QUERIES.getCellStuff(cell.id))
+      .then(results => {
+        for (const {element, type} of results) {
+          const alreadyVisited = new Set([[element.value, type.value]]);
+          addSubmenuItems(element.value, type.value, modelverseMenu, 3, alreadyVisited);
+
+          querySPARQL(QUERIES.getModels)
+          .then(results => {
+            const alreadyVisited2 = new Set([[element.value, type.value]]);
+            for (const {model, type} of results) {
+              addMenuItem(model.value, type.value, 3, createLinkMenu, "Model",
+                alreadyVisited2,
+                targetIri => updateSPARQL(QUERIES.insertTraceLink(element.value, targetIri)));
+            }
+          });
+        }
+      })
+    }
+  }
+
+
+  function getSetting(prop) {
+    const result = localStorage.getItem("dtdesign-"+prop);
+    if (result !== null) {
+      return JSON.parse(result);
+    }
+    return defaultSettings[prop];
+  }
+  function setSetting(prop, value) {
+    localStorage.setItem("dtdesign-"+prop, JSON.stringify(value));
+  }
+
+  const errWndDiv = document.createElement('div');
+    // wndDiv.style.color = "red";
+    errWndDiv.style.overflow = "auto";
+    errWndDiv.style.height = "100%";
+    errWndDiv.style.padding = '12px 14px 8px 14px';
+  const errWnd = new mxWindow("Error Details",
+    errWndDiv, 300, 300, 300, 360, true, true);
+    errWnd.destroyOnClose = false;
+    errWnd.setMaximizable(false);
+    errWnd.setResizable(true);
+    errWnd.setClosable(true);
+
+  function getErrDetailsAnchor() {
+    const anchor = document.createElement('a');
+      anchor.innerText = "Details";
+      anchor.href = "#";
+      anchor.onclick = function() {
+        errWnd.show();
+      };
+    return anchor;
+  }
+
+  let rebuildOMLPromise = Promise.resolve();
+
+  function rejectIfStatusAbove400(httpResponse) {
+    if (httpResponse.status >= 400) {
+      return httpResponse.text().then(text => {
+        return Promise.reject(text);
+      });
+    }
+    return Promise.resolve();
+  }
+
+  function rebuildOMLForced() {
+    return querySPARQL(QUERIES.getAllGraphs)
+    .then(results => {
+      const graphsToDelete = results
+        .map(({g,count}) => g.value)
+        .filter(graphIri => !GRAPHS_NOT_TO_DELETE.includes(graphIri));
+      // Delete all graphs first
+      return Promise.all(graphsToDelete.map(graphToDelete =>
+        fetch(SPARQL_SERVER+SPARQL_ENDPOINT+"?graph="+graphToDelete, {
+          method: "DELETE",
+        })
+        .then(rejectIfStatusAbove400)
+      ))
+      .then(() => {
+        // Build OML and load the resulting .owl files:
+        return fetch(BACKEND+"/owl_load", {
+          method: "PUT",
+        })
+        .then(rejectIfStatusAbove400)
+      });
+    });
+  }
+
+  function rebuildOMLIncremental() {
+    // Backend takes care of all the heavy work:
+    return fetch(BACKEND+"/owl_reload_incremental", { method: "PUT", })
+    .then(rejectIfStatusAbove400);
+  }
+
+  function showRebuildOMLResult(rebuildAsyncCallback) {
+    // only perform one rebuild at a time -> queue rebuilds.
+    rebuildOMLPromise = rebuildOMLPromise.then(() => {
+      omlStatusDiv.style.color = null;
+      omlStatusDiv.innerText = "Rebuilding OML...";
+      return rebuildAsyncCallback()
+      .then(() => {
+        omlStatusDiv.innerText = "✓ Built OML";
+        omlStatusDiv.style.color = "green";
+      })
+      .catch(errText => {
+        errWndDiv.innerText = errText;
+        omlStatusDiv.innerHTML = "✗ Failed to build OML ";
+        omlStatusDiv.style.color = "red";
+        omlStatusDiv.appendChild(getErrDetailsAnchor());        
+      });
+    });
+  }
+
+  function uploadResponseHandler(statusDiv, refreshListCallback) {
+    statusDiv.innerHTML = "Saving ...";
+    statusDiv.style.color = null;
+    omlStatusDiv.innerHTML = "";
+
+    return res => {
+      if (res.status >= 200 && res.status < 300) {
+        errWndDiv.innerText = "No error.";
+        res.json().then(parsedAs => {
+          statusDiv.innerHTML = "✓ Generated OML (" + parsedAs.join(", ") + ")";
+          statusDiv.style.color = "green";
+        });
+        refreshListCallback();
+        showRebuildOMLResult(rebuildOMLIncremental);
+      }
+      else {
+        statusDiv.innerHTML = "✗ Failed to save/parse. ";
+        statusDiv.appendChild(getErrDetailsAnchor());
+        res.text().then(text => {
+          errWndDiv.innerText = text;
+        });
+        statusDiv.style.color = "red";
+      }
+    };
+  }
+
+  let highlight;
+
+  // Given a cell IRI, open the diagram that contains the cell
+  function goto(cellIri) {
+    querySPARQL(QUERIES.getDiagramAndCellId(cellIri))
+    .then(([{diagramName, cellId}]) => {
+      // drop the "_drawio" at the end:
+      const actualDiagramName = diagramName.value.substring(0, diagramName.value.length-7);
+      loadPage(actualDiagramName)
+      .then(() => {
+        if (highlight) {
+          highlight.destroy();
+        }
+        const [cell] = ui.editor.graph.getCellsById([cellId.value]);
+        highlight = new mxCellHighlight(ui.editor.graph, "#eb34e8", 6);
+        highlight.highlight(ui.editor.graph.view.getState(cell));        
+      })
+    })
+  }
+
+  const wndDiv = document.createElement('div');
+    wndDiv.style.userSelect = 'none';
+    wndDiv.style.height = '100%';
+    wndDiv.style.color = "rgb(112, 112, 112)";
+    wndDiv.style.overflow = 'auto';
+    wndDiv.classList.add("geFormatContainer");
+
+  const omlDiv = document.createElement('div');
+    omlDiv.classList.add('geFormatSection')
+    omlDiv.style.padding = '12px 14px 8px 14px';
+    wndDiv.appendChild(omlDiv);
+
+  const omlLabel = document.createElement('div');
+    omlLabel.innerText = "OML Status";
+    omlLabel.style.fontWeight = 'bold';
+    omlDiv.appendChild(omlLabel);
+
+  const omlStatusDiv = document.createElement('div');
+    omlStatusDiv.innerText = "Ready";
+    omlDiv.appendChild(omlStatusDiv);
+
+  const omlIncButton = mxUtils.button("Incremental rebuild (faster)", function() {
+    showRebuildOMLResult(rebuildOMLIncremental);
+  });
+    omlDiv.appendChild(omlIncButton);
+  const omlForceButton = mxUtils.button("Forced rebuild (slow)", function() {
+    showRebuildOMLResult(rebuildOMLForced);
+  });
+    omlDiv.appendChild(omlForceButton);
+
+  function createSavePageDiv(refreshModelsCallback) {
+    const savePageDiv = document.createElement('div');
+    const saveButton = mxUtils.button("Save Current Page", function() {
+      const responseHandler = uploadResponseHandler(saveStatusDiv, refreshModelsCallback);
+      const headers = new Headers({
+        "Content-Type": "application/xml",
+      });
+      const serializer = new XMLSerializer();
+      const xmlnode = ui.getXmlFileData(
+        null,  // ignore selection
+        true,  // only current page
+        true); // uncompressed
+      const diagram = xmlnode.children[0]
+      const diagramName = diagram.getAttribute("name");
+      const xml = serializer.serializeToString(diagram);
+      return fetch(BACKEND + "/files/drawio/"+diagramName, {
+        method: "PUT",
+        headers,
+        body: xml,
+      })
+      .then(responseHandler);
+    });
+      saveButton.style.width = "100%";
+      saveButton.style.marginTop = "4px";
+      saveButton.style.marginBottom = "2px";
+      savePageDiv.appendChild(saveButton);
+    const saveStatusDiv = document.createElement('div');
+      savePageDiv.appendChild(saveStatusDiv);
+    return savePageDiv;
+  }
+  function createUploadDiv(modelType, mimeType) {
+    return function(refreshModelsCallback) {
+      const uploadDiv = document.createElement('div');
+      const uploadLabel = document.createElement('label');
+        uploadLabel.innerText = "Upload "+modelType+":";
+        uploadLabel.for = "uploadButton";
+        uploadDiv.appendChild(uploadLabel);
+      const uploadButton = document.createElement('input');
+        uploadButton.type = "file";
+        uploadButton.id = "uploadButton";
+        uploadButton.style.width = "100%";
+        uploadButton.style.marginTop = "4px";
+        uploadButton.style.marginBottom = "2px";
+        uploadButton.accept = mimeType;
+        // uploadButton.multiple = true;
+        uploadButton.onchange = e => {
+          const responseHandler = uploadResponseHandler(statusDiv, refreshModelsCallback);
+          Array.from(e.target.files).map(file => {
+            fetch(BACKEND+"/files/"+modelType+"/"+file.name, {
+              method: "PUT",
+              body: file,
+            })
+            .then(responseHandler);
+          });
+        };
+        uploadDiv.appendChild(uploadButton);
+      const statusDiv = document.createElement('div');
+        uploadDiv.appendChild(statusDiv);
+      return uploadDiv;
+    };
+  }
+  function createDownloadHandler(modelType) {
+    return modelName => {
+      const a = document.createElement('a');
+      document.body.appendChild(a);
+      a.setAttribute('href', BACKEND + "/files/" + modelType + "/" + modelName);
+      a.setAttribute('download', modelName);
+      a.setAttribute('target', "_blank"); // for browsers that don't support the 'download' attribute, tell them to open the link in a new tab
+      a.click();
+      document.body.removeChild(a);
+    };
+  }
+  function createModelList(modelType, createUploadDiv, downloadButtonLabel, onDownloadClick, extraStuff) {
+    const containerDiv = document.createElement('div');
+      containerDiv.classList.add('geFormatSection')
+      containerDiv.style.padding = '12px 14px 8px 14px';
+      wndDiv.appendChild(containerDiv);
+    const labelDiv = document.createElement('div');
+      labelDiv.innerText = "Models — " + modelType;
+      labelDiv.style.fontWeight = 'bold';
+      containerDiv.appendChild(labelDiv);
+    const modelListDiv = document.createElement('div');
+      containerDiv.appendChild(modelListDiv);
+
+    // Refreshes the list of models shown in the ModelVerse window
+    function refreshList() {
+      fetch(BACKEND + "/files/" + modelType, {
+        method: "GET",
+      })
+      .then(res => res.json())
+      .then(models => {
+        if (models.length > 0) {
+          modelListDiv.replaceChildren(...models.sort().map(modelName => {
+            const div = document.createElement('div');
+              div.style.padding = '3px 0px';
+            const loadButton = mxUtils.button(downloadButtonLabel, () => onDownloadClick(modelName));
+              loadButton.style.marginLeft = "12px";
+            div.appendChild(document.createTextNode(modelName));
+            div.appendChild(loadButton);
+            const extraStuffWrapper = document.createElement('div');
+            div.appendChild(extraStuffWrapper);
+            function refreshExtraStuff() {
+              extraStuff(modelName)
+              .then(extraDiv =>  {
+                if (extraDiv) {
+                  extraStuffWrapper.replaceChildren(extraDiv);
+                  extraDiv.appendChild(mxUtils.button("⟳", refreshExtraStuff));
+                }
+              });
+            }
+            refreshExtraStuff();
+            return div;
+          }));
+        }
+        else {
+          const div = document.createElement('div');
+              div.style.padding = '3px 0px';
+          div.appendChild(document.createTextNode("No "+modelType+" models."));
+          modelListDiv.replaceChildren(div);
+        }
+      });
+    }
+    refreshList();
+
+    containerDiv.appendChild(createUploadDiv(refreshList));
+
+    return refreshList;
+  }
+
+  function createPTrenderCallback(startTraceIri) {
+    return () => {
+      return fetch(BACKEND+"/render_pt/"+encodeURIComponent(startTraceIri))
+      .then(treatResponseAsPageXml)
+      .then(() => {
+        refreshDrawioModelList();
+        showRebuildOMLResult(rebuildOMLIncremental);
+      });
+    }
+  }
+
+  const extraPMstuff = modelName => {
+    if (modelName.endsWith(":pm")) {
+      const pmModel = addArtifactPrefix(modelName.substr(0, modelName.length-3)+"_pm#model");
+      const urlEncoded = encodeURIComponent(pmModel);
+      const enactButton = mxUtils.button("Start New...", ()=>{
+        // open WEE in new tab:
+        window.open(WEE+"/gettraces?iri="+urlEncoded, "_blank");
+      });
+      const shortTraceName = traceIri => dropArtifactPrefix(traceIri).split('#').findLast(str => str.length > 0);
+      return Promise.all([
+        fetch(WEE+"/traces/active/"+urlEncoded)
+        .then(response => response.json())
+        .then(enactments => {
+          return enactments.map((enactment, i) => mxUtils.button(`Open "${shortTraceName(enactment.iri)}" (ongoing)`, createPTrenderCallback(enactment.iri)))
+        }),
+        fetch(WEE+"/traces/finished/"+urlEncoded)
+        .then(response => response.json())
+        .then(enactments => {
+          return enactments.map((enactment, i) => mxUtils.button(`Open "${shortTraceName(enactment.iri)}" (finished)`, createPTrenderCallback(enactment.iri)))
+        }),
+      ])
+      .then(([ongoing, finished]) => {
+        const pmDiv = document.createElement('div');
+        pmDiv.style.marginLeft = '8px';
+        pmDiv.style.borderWidth = '1px';
+        pmDiv.style.borderStyle = 'solid';
+        pmDiv.appendChild(document.createTextNode("PM Enactment: "));
+        pmDiv.appendChild(enactButton);
+        if (ongoing.length > 0) {
+          ongoing.forEach(o => pmDiv.appendChild(o));
+        }
+        if (finished.length > 0) {
+          finished.forEach(f => pmDiv.appendChild(f));
+        }
+        return pmDiv;
+      });
+    }
+    return Promise.resolve(null);
+  };
+  const noExtraStuff = () => Promise.resolve(null);
+
+  const refreshDrawioModelList = createModelList("drawio", createSavePageDiv, "Open", modelName => loadPage(modelName), extraPMstuff);
+  createModelList("xopp", createUploadDiv("xopp", "application/x-xopp,.xopp"), "Download", createDownloadHandler("xopp"), noExtraStuff);
+  createModelList("csv", createUploadDiv("csv", "text/csv,.csv"), "Download", createDownloadHandler("csv"), noExtraStuff);
+  createModelList("file", createUploadDiv("file", ""), "Download", createDownloadHandler("file"), noExtraStuff);
+
+  // Load a model and add it as a new page to the editor
+  function treatResponseAsPageXml(response) {
+    return response.text()
+    .then(xmltext => {
+      // console.log(xmltext);
+      const parser = new DOMParser();
+      const doc = parser.parseFromString(xmltext, "application/xml");
+      const node = doc.documentElement;
+      const page = new DiagramPage(node);
+      // if page with same name already exists, erase it:
+      ui.pages = ui.pages.filter(pg => pg.node.getAttribute("name") != page.node.getAttribute("name"));
+      ui.pages.push(page);
+      ui.currentPage = page;
+      ui.editor.setGraphXml(node);
+    });
+  }
+
+  function loadPage(pageName) {
+    return fetch(BACKEND + "/files/drawio/" + pageName)
+    .then(treatResponseAsPageXml);
+  }
+
+  // make sure the plugin popup window is always within the bounds of the browser window:
+  const dialogPosX = x => Math.max(0, Math.min(x, window.innerWidth - getSetting("dialogWidth")));
+  const dialogPosY = y => Math.max(0, Math.min(y, window.innerHeight - getSetting("dialogHeight")));
+
+  const wnd = new mxWindow("ModelVerse", wndDiv,
+    dialogPosX(getSetting("dialogPosX")),
+    dialogPosY(getSetting("dialogPosY")),
+    getSetting("dialogWidth"),
+    getSetting("dialogHeight"),
+    true, true);
+    wnd.destroyOnClose = false;
+    wnd.setMaximizable(false);
+    wnd.setResizable(true);
+    wnd.setClosable(false);
+    // remember window geometry in localstorage:
+    wnd.addListener('resize', function(wnd) {
+      // couldn't find a better way to get the new window size but the following:
+      const parsepx = px => parseInt(px.substring(0, px.length-2));
+      setSetting("dialogWidth", parsepx(wnd.div.style.width));
+      setSetting("dialogHeight", parsepx(wnd.div.style.height));
+    })
+    wnd.addListener('move', function() {
+      setSetting("dialogPosX", wnd.getX());
+      setSetting("dialogPosY", wnd.getY());
+    })
+
+  wnd.show();
+
+  window.onresize = event => {
+    // make sure popup window remains within browser window bounds:
+    const newX = dialogPosX(getSetting("dialogPosX"));
+    const newY = dialogPosY(getSetting("dialogPosY"));
+    wnd.div.style.left = newX + "px";
+    wnd.div.style.top = newY + "px";
+    // setSetting("dialogPosX", newX);
+    // setSetting("dialogPosY", newY);
+  }
+
+}) // end of promise that checks the backend version.
+})

文件差异内容过多而无法显示
+ 1 - 0
drawio/meta.drawio


文件差异内容过多而无法显示
+ 1 - 0
drawio/object.drawio


+ 118 - 0
local.yml

@@ -0,0 +1,118 @@
+version: "3.8"
+
+services:
+  fuseki:
+    image: registry.rys.one/dtdesign/fuseki
+    container_name: fuseki
+    networks:
+      twin:
+        ipv4_address: 172.50.10.10
+    ports:
+      - "3030:3030"
+    restart: unless-stopped
+    volumes:
+      - "./fuseki:/fuseki"
+      - "./fuseki-extra:/fuseki-extra"
+    env_file:
+      - "./config/fuseki.env"
+
+  spendpoint:
+    image: registry.rys.one/dtdesign/spendpoint:dev
+    container_name: spendpoint
+    networks:
+      twin:
+        ipv4_address: 172.50.10.20
+    ports:
+      - "8000:8000"
+#    depends_on:
+#      - models
+#      - outliers
+    restart: unless-stopped
+    volumes:
+      - "./spendpoint:/app/data:ro"
+
+  outliers:
+    image: registry.rys.one/dtdesign/dtdesign/outliers:dev
+    container_name: outliers
+    networks:
+      twin:
+        ipv4_address: 172.50.10.30
+    ports:
+      - "9090:9090"
+    restart: unless-stopped
+    # TODO This needs to be the csv directory and needs to map to the same dir as the backend csv
+#    volumes:
+#      - "./outliers:/app/data:ro"
+
+#  models:
+#    image: registry.rys.one/dtdesign/models
+#    container_name: models
+#    networks:
+#      twin:
+#        ipv4_address: 172.50.10.40
+#    restart: unless-stopped
+#    volumes:
+#      - "./build:/app/build"
+#    env_file:
+#      - "./config/models.env"
+
+#  graph-exploring-tool:
+#    image: registry.rys.one/dtdesign/graph-exploring-tool
+#    container_name: graph-exploring-tool
+#    networks:
+#      twin:
+#        ipv4_address: 172.50.10.50
+#    depends_on:
+#      - models
+#      - spendpoint
+#    restart: unless-stopped
+#    volumes:
+#      - "./palette:/app/palette:ro"
+#    env_file:
+#      - "./config/graph-exploring-tool.env"
+
+  drawio:
+    image: registry.rys.one/diagram/drawio:dev
+    container_name: drawio
+    networks:
+      twin:
+        ipv4_address: 172.50.10.60
+    ports:
+      - "8445:8080"
+      - "8443:8443"
+    restart: unless-stopped
+    volumes:
+      - "./drawio/dtdesign.js:/usr/local/tomcat/webapps/draw/plugins/dtdesign.js"
+      - "./drawio/object.drawio:/usr/local/tomcat/webapps/draw/libraries/object.drawio"
+      - "./drawio/meta.drawio:/usr/local/tomcat/webapps/draw/libraries/meta.drawio"
+    env_file:
+      - "./config/drawio.env"
+
+  wee:
+    image: registry.rys.one/dtdesign/wee:dev
+    container_name: wee
+    networks:
+      twin:
+        ipv4_address: 172.50.10.70
+    ports:
+      - "8081:8081"
+    restart: unless-stopped
+
+  backend:
+    image: registry.rys.one/dtdesign/drawio2oml/backend:dev
+    container_name: backend
+    networks:
+      twin:
+        ipv4_address: 172.50.10.80
+    ports:
+      - "5000:5000"
+    restart: unless-stopped
+    volumes:
+      - "./backend/ontology:/app/ontology"
+      - "./backend/shape_lib:/app/shape_lib"
+    env_file:
+      - "./config/backend.env"
+
+networks:
+  twin:
+    external: true

+ 4 - 2
tasks.py

@@ -2,6 +2,8 @@ from invoke import task
 
 
 @task
-def demo(c):
+def local(c):
     """"""
-    pass
+    c.run("mkdir -p config data docs drawio fuseki fuseki-extra")
+    c.run("docker-compose -f local.yml build")
+    c.run("docker-compose -f local.yml up")