diff --git a/README.md b/README.md index ab7c0f6..16957c4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This repository contains a collection of generic web frontends for accessing RESTful services from the KIT Data Manager service portfolio. The idea is to have graphical user interfaces available such that certain base services can be directly used for performing basic tasks without the need of integrating them in your own frontends before being able to use them the first time. -However, for some application cases, these generic web frontends might even be sufficient for direct interaction with our services and they might be offered to the end-user. To allow that, all frontends of this collection offer a certain degree of customization to slightly adapt their presentation to specific needs. +However, for some application cases, these generic web frontends might even be sufficient for direct interaction with our services, and they might be offered to the end-user. To allow that, all frontends of this collection offer a certain degree of customization to slightly adapt their presentation to specific needs. ## Installation @@ -10,7 +10,7 @@ Installing the frontend collection is relatively easy, as it is based only on HT If you don't have Python installed, this would be the first step for you. You may either install Python 2.x or 3.x, depending on your preferences. -Afterwards, you have to clone this repository to a directory of your choice, e.g., /home/user/, and change into the 'frontend-collection' subfolder: +Afterwards, you have to clone this repository to a directory of your choice, e.g., /home/user/, and change into the 'frontend-collection' sub folder: ```bash user@hostname:~ git clone https://github.com/kit-data-manager/frontend-collection @@ -34,29 +34,28 @@ user@hostname:~/frontent-collection python -m http.server Serving HTTP on :: port 8000 (http://[::]:8000/) ... ``` -After this, the output should tell you that the content is served at [http://localhost:8000/](http://localhost:8000/). Hostname and/or port might be different depending on your local configuration. +After this, the output should tell you that the content is served at [http://localhost:8000/](http://localhost:8000/). +Hostname and/or port might be different depending on your local configuration. You can now access the HTML pages of the single frontends directly, which are: -| Service | Frontend Location| -|---------------|------------------| -| [base-repo](https://github.com/kit-data-manager/base-repo) | http://localhost:8000/repo-management.html -| [metastore](https://github.com/kit-data-manager/metastore2) | http://localhost:8000/metadata-management.html or http://localhost:8000/schema-management.html +| Service | Frontend Location| +|------------------------------------------------------------------------|------------------| +| [base-repo](https://github.com/kit-data-manager/base-repo) | http://localhost:8000/repo-management.html +| [metastore](https://github.com/kit-data-manager/metastore2) | http://localhost:8000/metadata-management.html or http://localhost:8000/schema-management.html +| [typed-pid-maker](https://github.com/kit-data-manager/pit-service) | http://localhost:8000/typed-pid-maker-ui.html +| [mapping-service](https://github.com/kit-data-manager/mapping-service) | http://localhost:8000/mapping-service-ui.html +| fdo-creator | http://localhost:8000/fdo-creator-ui.html ## Basic Customization -For basic customization please check the .settings.js files in the js subfolder of this repository. For each frontend, you'll find one settings file, e.g., `base-repo.settings.js` for repo-management.html or `metastore.settings.js` for metadata-management.html and schema-management.html. +For basic customization please check the .settings.js files in the `settings` sub folder of this repository. +Settings for the dashboard page can be found in `dashboard.settings.js`. +Settings applied for different frontends the same way can be found in `general.settings.js`. +In addition, for each frontend you'll find a specific settings file, e.g., `base-repo.settings.js` for repo-management.html or +`metastore.settings.js` for metadata-management.html and schema-management.html. -Depending on the service, there might be different settings available depending on the service's capabilities. There are some commons property variables listed in the following table: - -| Variable | Description | -|---------------|------------------| -| ajaxBaseUrl | The base URL for requests to this service, e.g., http://localhost:8080/api/v1/ Be aware, that providing a wrong base URL will result in loading errors in the frontend. -| keycloak | If you want to enable authentication via Keycloak, you have to assign a proper value according to the example to this variable. Otherwise, no user login will be provided. -| showServiceUrl | Setting this variable `true` will allow to change ajaxBaseUrl in the frontend. This setting is meant for developers only. In production environments, only `ajaxBaseUrl` should be used. -| appDescription | Here you can customize the header of your frontend, e.g., by providing a custom logo, title, or subtitle. - -For the base-repo frontend, there is another variable named `tags`. This variable allows to provide a list of tags and their color, which are then presented to the user to tag content elements in the frontend. Please note, that tag colors re only used in the frontend and are not stored at the base-repo service. Browsing a base-repo instance with another instance of the frontend having a different tag coloring scheme will render the tags according to the configuration in the currently used frontend instance. +For more information please refer to the documentation inside the setting files. ## Issues @@ -64,27 +63,29 @@ For the base-repo frontend, there is another variable named `tags`. This variabl **The table showing service entries shows an error and no data is loaded.** :grey_exclamation: -Typically, this is an issue with the ajaxBaseURL, which was either provided in the settings.js file or via the input of the frontend page. At first, you should check the format. The URL should look similar to http://localhost:8080/api/v1/ , i.e., it should contain protocol, hostname and port depending on the addressed service instance followed by api/v1/, which is the base path of the API itself and **must** end with / +Typically, this is an issue with the ajaxBaseURL, which was either provided in the service's settings.js file or via the input of the frontend page. +At first, you should check the format. The URL should look similar to http://localhost:8080/api/v1/ or http://localhost:8090/, i.e., +it should contain protocol, hostname and port depending on the addressed service instance optionally followed by an API base path, e.g., api/v1/, +which is the base path of the API itself. The ajaxBaseURL **must** end with / -If everything looks fine, ensure that no authentication is used by the service you want to address. If Keycloak-based authentication is used, please adapt the settings.js accordingly to gain access. +If everything looks fine, ensure that no authentication is used by the service you want to address. If Keycloak-based +authentication is used by the service, please also adapt the `general.settings.js` accordingly to gain access. -If both checks succeed, please also check by which protocol the server providing your frontend is accessible. If it is `https` you are only allowed to load information from `https` resources. Accessing a service via `http` from a frontend running with `https` is not possible for security reasons. +If both checks succeed, please also check by which protocol the server providing your frontend is accessible. +If it is `https` you are only allowed to load information from `https` resources. Accessing a service via `http` +from a frontend running with `https` is not possible for security reasons. +Finally, if everything looks fine so far there might also be an issue with the service itself. Try to access the service without frontend to ensure +it is up and running. --- :grey_question: -**Applying filters to the tables in `metadata-management.html` and `schema-management.html` results in incomplete pages.** - -:grey_exclamation: -The reason here is, that pagination for the MetaStore frontend is build remotely, whereas filering happens on the client-side. In future this will change in a way, that both is done on the client side. - ---- - -:grey_question: **The tables in `repo-management.html` seem not to contain all resources I have in my system.** +**After applying a filter to the MetaStore and base-repo frontends, no results are shown even when I'm sure that there should be one.** :grey_exclamation: -For the base-repo frontent, pagination is done locally instead of remotely. However, for proper local pagination, the Tabulator library we are using for handling tables requires a certain response format which is not yet provided by base-repo. Therefore, Tabulator-wise only one page with 20 elements is loaded. This will change in future. - +The reason here is, that the tables are loaded page-wise with max. 20 entries at once. New data is only loaded on scrolling. As filtering +happens on the client-side, only the data already loaded is filtered. This may result in an empty table if elements known to match +the filter criteria are not loaded yet. The only solution for this for now is scrolling the table to the end before applying filters. ## License diff --git a/compile.sh b/compile.sh new file mode 100644 index 0000000..57af21c --- /dev/null +++ b/compile.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +for FILE in ./definitions/search/*.handlebars; +do +handlebars $FILE -f "${FILE}.js" +done + +for FILE in ./definitions/base-repo/*.handlebars; +do +handlebars $FILE -f "${FILE}.js" +done + +for FILE in ./definitions/metastore/*.handlebars; +do +handlebars $FILE -f "${FILE}.js" +done diff --git a/dashboard.html b/dashboard.html index 13dd504..0cf3131 100644 --- a/dashboard.html +++ b/dashboard.html @@ -10,8 +10,9 @@ + + href="https://cdn.jsdelivr.net/npm/@kit-data-manager/metadata-editor@0.9.3/deps/opt/fontawesome/css/all.min.css"> + + + +

+ +
+ {app-title} +
{app-subtitle}
+
+
+
Login
+ +
+

+ +
+
+
+
+ Typed PID Maker URL +
+
+ +
+
+ + + +
+
+
+ + +
+
+
+
+
+
+
+ +
+

Adding existing FDOs allows you to use previously created objects to build up relationships between + FDOs. While it is possible to also add externally created FDOs, + only FDOs created by the configured Typed PID Maker instance are fully supported, i.e., can also be + updated by receiving new relational connections, while externally + created FDOs can only be providers of relational connections as they can only be updated by their + respective owner.

+

To add existing FDOs, provide one or more lines in the textarea below in the following format: $PID[,$CUSTOM_NAME] +

+
+
+
+
+
+ + +
+
+
+
+ + Add FDOs +
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+ +
+ Actions +
+
+
+
+ + Link +
+
+ + Unlink +
+
+ + Clear Cache +
+
+ + Remove +
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+ + + + + + + + + +
+ + +
+ + + + + + + diff --git a/fdo-creator-ui.html b/fdo-creator-ui.html deleted file mode 100644 index 12fb664..0000000 --- a/fdo-creator-ui.html +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - Typed PID Maker UI - - - - - - - - - - - - - - - - - - - - - - - - -

- -
- {app-title} -
{app-subtitle}
-
-
-
Login
- -
-

- -
-
-
-
- Typed PID Maker URL -
-
- -
-
- - - -
-
-
- -
-
-
-
- - -
- - -
- - - - diff --git a/js/FDO.js b/js/FDO.js new file mode 100644 index 0000000..e9d0b82 --- /dev/null +++ b/js/FDO.js @@ -0,0 +1,152 @@ +class FDO{ + constructor(customName, pid) { + this.customName = customName; + this.pid = pid; + this.properties = []; + } + + setPid(pid){ + this.pid = pid; + } + + getPid(){ + return this.pid; + } + + setCustomName(customName){ + this.customName = customName; + } + + getCustomName(){ + return this.customName; + } + + addProperty(key, value){ + this.properties.push({"key": key, "value": value}); + } + + removeProperty(idx){ + this.properties.splice(idx, 1); + } + + getProperties(){ + return this.properties; + } + + hasLinkTo(pid){ + for(let i=0;i a.label); + let formOutputObject = JSON.parse(formOutput); + let keys = Object.keys(formOutputObject); + result.setCustomName(formOutputObject["customName"]) + for (let i = 0; i < keys.length; i++) { + //obtain PID from schema + let pid = model['properties'][keys[i]]['pid']; + //check if attribute value is a digitalObjectType label + if (labelMap.indexOf(formOutputObject[keys[i]]) >= 0) { + //We have a type label, set final record value to PID for digitalObjectType label + result.addProperty(pid, known_types.slice(labelMap.indexOf(formOutputObject[keys[i]]), 1).at(0)['pid']); + } else { + //check for checksum + if (pid == '21.T11148/82e2503c49209e987740') { + //process checksum + let checksumValue = formOutputObject[keys[i]]; + let checksumAlg = undefined; + switch (checksumValue.length){ + case 32:{ + //md5 + checksumAlg = "md5"; + break; + } + default:{ + checksumAlg = "sha" + (4 * checksumValue.length); + } + } + let checksumRecordValue = {}; + checksumRecordValue[checksumAlg + "sum"] = checksumValue; + result.addProperty(pid, checksumRecordValue); + }else if(pid == '21.T11148/b415e16fbe4ca40f2270'){ + //topic + result.addProperty(pid, formOutputObject[keys[i]]); + } else { + //we have another attribute value, check if valid and set if true + if (formOutputObject[keys[i]]) { + result.addProperty(pid, formOutputObject[keys[i]]); + } + } + } + } + + return result; + } + + toNode(){ + let node = {}; + node.id = this.pid; + + let typeProperty = this.properties.find((element) => element.key === "21.T11148/1c699a5d1b4ad3ba4956"); + if( typeProperty){ + node.type = typeProperty.value; + } + + node.customName = this.customName; + let props = []; + for(let i=0;i { + let f = new FDO().fromObject(fdo); + this.fdos.set(key, f); + }); + }else{ + this.fdos = new Map(); + } + } + + reset(){ + localStorage.removeItem("fdo_creator_fdos"); + this.fdos = new Map(); + } + + addFdo(fdo){ + this.fdos.set(fdo.getPid(), fdo); + this.store(); + } + + removeFdo(pid){ + this.fdos.delete(pid); + this.store(); + } + + getPids(){ + return Array.from(this.fdos.keys()); + } + + getLinkedFDOs(pid){ + let pids = this.getPids(); + let linkedFdos = [] + let fdo = this.getFdo(pid); + for(let i=0;i { + let node = fdo.toNode(); + data.nodes.push(node); + node.props.forEach((entry) => { + if(pids.includes(entry.value)){ + let link = {"source":node.id, "target":entry.value, "relationType": this.resolver(entry.key)}; + data.links.push(link); + } + }); + }); + return data; + } +} + diff --git a/js/apply-config.js b/js/apply-config.js index b2b6742..99b3543 100644 --- a/js/apply-config.js +++ b/js/apply-config.js @@ -1,48 +1,65 @@ // requires jQuery in HTML, like this: // +let keycloak = undefined; +let config = undefined; -function userLoggedIn(login) { - if (login) { +function userLoggedIn(_login, _messageCallback, _loginCallback) { + let username = undefined; + if (_login) { $("#login_icon").attr("class", "sign-out icon") $("#login_button_text").text("Logout"); - addMessage(0, 'User ' + keycloak.idTokenParsed.preferred_username + ' logged in.'); + _messageCallback(0, 'User ' + keycloak.idTokenParsed.preferred_username + ' logged in.'); config.token = keycloak.token; + localStorage.setItem("userLoggedIn", true); + localStorage.setItem("token", config.token); + username = keycloak.idTokenParsed.preferred_username; } else { $("#login_icon").attr("class", "sign-in icon") $("#login_button_text").text("Login"); config.token = null; + localStorage.removeItem("userLoggedIn", true); + localStorage.removeItem("token", true); } - reloadTable(); + _loginCallback(_login, username); } -function applyConfig(ajaxBaseUrl, keycloak, tags, showServiceUrl, appDescription) { - if (!showServiceUrl) { +function applyConfig(_keycloak, + _showServiceUrl, + _appDescription, + _config, + _messageCallback = (status, message) => console.log((status == 0)?"INFO: ":"ERROR:" + ": " + message), + _loginCallback= (loggedIn, username) => console.log("Logged in: " + loggedIn + "as User: " + username)) { + keycloak = _keycloak; + config = _config; + if (!_showServiceUrl) { $('#service-url-input').empty(); } // set header - $('#app-logo').attr("src", appDescription["app-logo"]); - let header = appDescription["app-title"] + - '
' + appDescription["app-subtitle"] + '
'; + $('#app-logo').attr("src", _appDescription["app-logo"]); + let header = _appDescription["app-title"] + + '
' + _appDescription["app-subtitle"] + '
'; $('#app-title').html(header); - - + if (typeof keycloak != typeof undefined) { keycloak.onAuthSuccess = function () { - userLoggedIn(true); + userLoggedIn(true, _messageCallback, _loginCallback); }; keycloak.onAuthLogout = function () { - userLoggedIn(false); + userLoggedIn(false, _messageCallback, _loginCallback); }; keycloak.onTokenExpired = () => { addMessage(0, 'Keycloak token expired. Trying to refresh.'); keycloak.updateToken(30).success(() => { - addMessage(0, 'Successfully got a new token.'); + _messageCallback(0, 'Successfully got a new token.'); config.token = keycloak.token; + if(loginCallback) loginCallback(true, keycloak.idTokenParsed.preferred_username); }).catch(() => { - addMessage(1, "Failed to refresh keycloak token."); + _messageCallback(1, "Failed to refresh keycloak token."); + userLoggedIn(false, _messageCallback, _loginCallback) config.token = null; + if(loginCallback) loginCallback(false, undefined); }); }; @@ -59,5 +76,8 @@ function applyConfig(ajaxBaseUrl, keycloak, tags, showServiceUrl, appDescription }); } else { $("#login_button").attr("style", "display:none"); + if($("#logged_in_as")) { + $("#logged_in_as").attr("style", "display:none"); + } } } diff --git a/js/base-repo-utils.js b/js/base-repo-utils.js index 9957b9c..52af06b 100644 --- a/js/base-repo-utils.js +++ b/js/base-repo-utils.js @@ -45,7 +45,7 @@ function generateContentEtag(idValue, relativePath) { if (config.token != null) { headers["Authorization"] = "Bearer " + config.token; } - console.debug("CREATE ETAG FOR " + config.ajaxBaseUrl + "dataresources/" + idValue + "/data/" + relativePath); + //console.debug("CREATE ETAG FOR " + config.ajaxBaseUrl + "dataresources/" + idValue + "/data/" + relativePath); return new Promise(function (resolve, reject) { $.ajax({ diff --git a/js/base-repo.settings.js b/js/base-repo.settings.js deleted file mode 100644 index a664161..0000000 --- a/js/base-repo.settings.js +++ /dev/null @@ -1,29 +0,0 @@ -//export let ajaxBaseUrl = "https://demo.datamanager.kit.edu:8443/base-repo/api/v1/"; -export let ajaxBaseUrl = "http://localhost:8081/api/v1/"; -export const keycloak = undefined; -/*Keycloak({ - url: 'https://gateway.datamanager.kit.edu:8443/', - realm: 'dem_testing', - clientId: 'kitdm-services' -});*/ - -export const tags = [ - {"name":"rawData", "color":"red"}, - {"name":"analyzedData", "color":"green"}, - {"name":"document", "color":"blue"}, - {"name":"code", "color":"orange"}, - {"name":"deprecated", "color":"black"} -]; - -export const showServiceUrl = false; -export const searchEnabled = false; - -export const appDescription = { - "app-logo":"./images/disks.jpg", - "app-title":"Base-Repo Demonstrator", - "app-subtitle":"Data Resource Management" -}; - - - - diff --git a/js/editor/lib/js/metadataeditor.js b/js/editor/lib/js/metadataeditor.js index dafbac2..92472da 100644 --- a/js/editor/lib/js/metadataeditor.js +++ b/js/editor/lib/js/metadataeditor.js @@ -386,9 +386,9 @@ editorDefinitionTable.prototype.initializeInputsTable = function (options, rende if (options.items !== undefined && options.items !== null && options.items !== '') { //if (options.items.length <= 6) { this.items = options.items; - // } else { - // _throw("JSON Items List should contain maximal 6 items"); - // } + /*} else { + _throw("JSON Items List should contain maximal 6 items"); + }*/ } else { _throw("JSON Items List is missing"); } @@ -455,9 +455,9 @@ editorDefinitionTable.prototype.initializeInputsTable = function (options, rende this.tableLayout.ajaxURL = options.tableLayout.ajaxURL; }else if (options.data) { this.tableLayout.data = options.data; - }/*else{ + }else{ _throw("Neither paginationURL nor a static input data file are provided."); - }*/ + } this.uiForm = options.uiForm || "*"; @@ -503,42 +503,44 @@ editorDefinitionForm.prototype.render = function (callback, buttonTitle) { */ editorDefinitionTable.prototype.generateTable = function (options) { if (options.readOperation !== undefined) { - this.items.push({formatter: this.readIcon, hozAlign: "center", minWidth: 30, width: 30, headerSort: false, frozen : true, responsive: 0, cellClick: function (e, cell) { + this.items.push({formatter: this.readIcon, responsive:0, hozAlign: "left", minWidth: 40, width: 40, headerSort: false, frozen : true, cellClick: function (e, cell) { emptyElt(formElt); options.readOperation(cell.getRow().getData()); }}); } if (options.updateOperation !== undefined) { - this.items.push({formatter: this.editIcon, hozAlign: "center", minWidth: 30,width: 30, headerSort: false, frozen : true, responsive: 0, cellClick: function (e, cell) { + this.items.push({formatter: this.editIcon, responsive:0, hozAlign: "left", minWidth: 40,width: 40, headerSort: false, frozen : true,cellClick: function (e, cell) { emptyElt(formElt); options.updateOperation(cell.getRow().getData()); }}); } if (options.deleteOperation !== undefined) { - this.items.push({formatter: this.deleteIcon, hozAlign: "center", minWidth: 30,width: 30, headerSort: false, frozen : true, responsive: 0, cellClick: function (e, cell) { + this.items.push({formatter: this.deleteIcon, responsive:0, hozAlign: "left", minWidth: 40,width: 40, headerSort: false, frozen : true,cellClick: function (e, cell) { emptyElt(formElt); options.deleteOperation(cell.getRow().getData()); }}); } if (options.listOperation !== undefined) { - this.items.push({formatter: this.listIcon, hozAlign: "center",minWidth: 30, width: 30, headerSort: false, frozen : true, responsive: 0, cellClick: function (e, cell) { + this.items.push({formatter: this.listIcon, responsive:0, hozAlign: "left",minWidth: 40, width: 40, headerSort: false, frozen : true,cellClick: function (e, cell) { emptyElt(formElt); options.listOperation(cell.getRow().getData()); }}); } + this.tableLayout.columns = this.items; - this.tableLayout.ajaxConfig = (options.ajaxConfig)? options.ajaxConfig:{ + let ajaxConfig = { method:"GET", //set request type to Position headers: { "Accept": 'application/tabulator+json; charset=utf-8', //set specific content type }, }; - console.log("CONF " + JSON.stringify(this.tableLayout.ajaxConfig)); + + this.tableLayout.ajaxConfig = ajaxConfig; var table = new Tabulator(this.tableId, this.tableLayout); //add buttons after table $("
").insertAfter(this.tableId); diff --git a/js/elastic-search-base-repo.settings.js b/js/elastic-search-base-repo.settings.js deleted file mode 100644 index 6544cdc..0000000 --- a/js/elastic-search-base-repo.settings.js +++ /dev/null @@ -1,21 +0,0 @@ -//export let ajaxBaseUrl = "https://demo.datamanager.kit.edu:8443/base-repo/api/v1/"; -export let ajaxBaseUrl = "http://localhost:8081/api/v1/"; -export const keycloak = undefined; -/*Keycloak({ - url: 'https://gateway.datamanager.kit.edu:8443/', - realm: 'dem_testing', - clientId: 'kitdm-services' -});*/ - -export const showServiceUrl = false; -export const page_size = 5; - -export const appDescription = { - "app-logo":"./images/search.jpg", - "app-title":"Repository Search Demonstrator", - "app-subtitle":"Data Resource Search" -}; - - - - diff --git a/js/elastic-search-fdo.settings.js b/js/elastic-search-fdo.settings.js deleted file mode 100644 index 8f70c23..0000000 --- a/js/elastic-search-fdo.settings.js +++ /dev/null @@ -1,21 +0,0 @@ -//export let ajaxBaseUrl = "https://demo.datamanager.kit.edu:8443/typed-pid-maker/api/v1/"; -export let ajaxBaseUrl = "http://localhost:8090/api/v1/"; -export const keycloak = undefined; -/*Keycloak({ - url: 'https://gateway.datamanager.kit.edu:8443/', - realm: 'dem_testing', - clientId: 'kitdm-services' -});*/ - -export const showServiceUrl = false; -export const page_size = 5; - -export const appDescription = { - "app-logo":"./images/search.jpg", - "app-title":"FDO Search Demonstrator", - "app-subtitle":"FAIR Digital Object Search" -}; - - - - diff --git a/js/elastic-search-metastore.settings.js b/js/elastic-search-metastore.settings.js deleted file mode 100644 index 33ed6be..0000000 --- a/js/elastic-search-metastore.settings.js +++ /dev/null @@ -1,15 +0,0 @@ -export let ajaxBaseUrl = "http://metastore.docker:8040/metastore/api/v1/"; -export const keycloak = undefined; - -export const showServiceUrl = false; -export const page_size = 5; - -export const appDescription = { - "app-logo":"./images/search.jpg", - "app-title":"MetaStore Search Demonstrator", - "app-subtitle":"Metadata Search" -}; - - - - diff --git a/js/fdo-graph.js b/js/fdo-graph.js new file mode 100644 index 0000000..5c4c869 --- /dev/null +++ b/js/fdo-graph.js @@ -0,0 +1,476 @@ +let svg = d3.select("svg"), + width = +svg.node().getBoundingClientRect().width, + height = +svg.node().getBoundingClientRect().height; + +// svg objects +let gLink, gNode, dNodes, dLinks, dText, nodeLabels, gOverlay, zoomTransform, dLabels; +// the data - an object with nodes and links +let graph; +let fdoStore; +let colorPid, colorType; +let tip; +let maxTextWidth; +let dragging = false; + +/**Initializer taking an FDOStore instance used to obtain the current data. + * @param _fdoStore The FDOStore instance. + */ +export function chart(_fdoStore) { + fdoStore = _fdoStore; +} + +/**Render the entire graph. This function can also be used for full redrawing after data changes. + */ +export function render() { + svg.selectAll("*").remove(); + graph = buildGraph(fdoStore.toData()); + colorPid = d3.scaleOrdinal(graph.nodes.map(d => d.id).sort(d3.ascending), d3.schemeCategory10); + colorType = d3.scaleOrdinal(graph.links.map(d => d.relationType).sort(d3.ascending), d3.schemeCategory10); + + let defs = svg.join("defs"); + + let set = [...new Set(graph.links.map(d => d.relationType))]; + for (let i = 0; i < set.length; i++) { + let elem = set[i]; + defs + .append("svg:marker") + .attr("id", "marker_" + elem) + .attr("refX", 24) + .attr("refY", 6) + .attr("markerWidth", 30) + .attr("markerHeight", 30) + .attr("markerUnits", "userSpaceOnUse") + .attr("orient", "auto") + .append("path") + .attr("d", "M 0 0 12 6 0 12 3 6") + .style("fill", colorType(elem)); + } + + initializeDisplay(); + initializeSimulation(); +} + +/**Build the internal data structure for rendering. + */ +let buildGraph = (data) => { + const nodes = data.nodes.map(({id, type, customName, props}) => ({ + id, + customName, + sourceLinks: [], + targetLinks: [], + type, + props + })); + + const nodeById = new Map(nodes.map(d => [d.id, d])); + + const links = data.links.map(({source, target, relationType}) => ({ + source: nodeById.get(source), + target: nodeById.get(target), + relationType + })); + + for (const link of links) { + const {source, target} = link; + source.sourceLinks.push(link); + target.targetLinks.push(link); + } + + return {nodes, links}; +} + +//////////// FORCE SIMULATION //////////// + +// force simulator +let simulation = d3.forceSimulation(); + + +// set up the simulation and event to update locations after each tick +function initializeSimulation() { + simulation.nodes(graph.nodes); + initializeForces(); + simulation.on("tick", ticked); +} + +// values for all forces +let forceProperties = { + center: { + x: 0.5, + y: 0.5 + }, + charge: { + enabled: true, + strength: -70, + distanceMin: 40, + distanceMax: 2000 + }, + collide: { + enabled: true, + strength: .7, + iterations: 1, + radius: 5 + }, + forceX: { + enabled: false, + strength: .1, + x: .5 + }, + forceY: { + enabled: false, + strength: .1, + y: .5 + }, + link: { + enabled: true, + distance: 250, + iterations: 1 + } +} + +// add forces to the simulation +function initializeForces() { + // add forces and associate each with a name + simulation + .force("link", d3.forceLink()) + .force("charge", d3.forceManyBody()) + .force("collide", d3.forceCollide()) + .force("center", d3.forceCenter()) + .force("forceX", d3.forceX()) + .force("forceY", d3.forceY()); + // apply properties to each of the forces + updateForces(); +} + +// apply new force properties +function updateForces() { + // get each force by name and update the properties + simulation.force("center") + .x(width * forceProperties.center.x) + .y(height * forceProperties.center.y); + simulation.force("charge") + .strength(forceProperties.charge.strength * forceProperties.charge.enabled) + .distanceMin(forceProperties.charge.distanceMin) + .distanceMax(forceProperties.charge.distanceMax); + simulation.force("collide") + .strength(forceProperties.collide.strength * forceProperties.collide.enabled) + .radius(forceProperties.collide.radius) + .iterations(forceProperties.collide.iterations); + simulation.force("forceX") + .strength(forceProperties.forceX.strength * forceProperties.forceX.enabled) + .x(width * forceProperties.forceX.x); + simulation.force("forceY") + .strength(forceProperties.forceY.strength * forceProperties.forceY.enabled) + .y(height * forceProperties.forceY.y); + simulation.force("link") + .id(function (d) { + return d.id; + }) + .distance(forceProperties.link.distance) + .strength(.01) + .iterations(forceProperties.link.iterations) + .links(forceProperties.link.enabled ? graph.links : []); + + // updates ignored until this is run + // restarts the simulation (important if simulation has already slowed down) + simulation.alpha(1).restart(); +} + +//////////// DISPLAY //////////// + +// generate the svg objects and force simulation +function initializeDisplay() { + let tip = d3.select("body").append("div") + .attr("class", "tooltip") + .style("opacity", 0); + + // set the data and properties of link lines + gLink = svg.append("g") + .attr("class", "links"); + let processed = []; + dLinks = gLink + .selectAll("line") + .data(graph.links) + .enter() + .append("line") + .attr("class", "dummy") + .attr("id", function (d, i) { + return "linkId_" + i; + }) + .attr("stroke-width", 2) + .attr("stroke", d => colorType(d.relationType)) + .attr("opacity", forceProperties.link.enabled ? 2 : 0) + .attr("marker-end", d => 'url(#marker_' + d.relationType + ')') + .attr("marker-fill", 'red') + .attr("class", (d) => { + processed.push(d.source.id + " - " + d.target.id); + if (processed.includes(d.target.id + " - " + d.source.id)) { + return "dashed" + } + return "solid"; + }); + + dText = gLink.selectAll(".marker") + .data(graph.links) + .enter() + .append("text") + .attr("class", "marker") + .attr("font-family", "sans-serif") + .attr("font-size", 8) + .attr("fill", d => colorType(d.relationType)) + .text((d) => d.relationType); + + maxTextWidth = d3.max(dText.nodes(), n => n.getComputedTextLength()); + + gOverlay = svg.append("g") + .attr("id", "overlay") + .attr("class", "marker"); + + // set the data and properties of node circles + gNode = svg.append("g") + .attr("class", "nodes"); + + dNodes = gNode.selectAll("circle") + .data(graph.nodes) + .enter().append("circle") + .attr("r", 16) + .attr("r", d => d.selected ? 20 : 16) + .attr("fill", d => colorPid(d.id)) + .attr("class", "dummy") + .on("mouseover", (e, d) => { + if (dragging) { + //not showing mouse-over information + return; + } + + //obtain linked nodes + let linkNodes = fdoStore.getLinks(d); + + //add external selection + if(externalSelection.length > 0) { + for (let i = 0; i < graph.nodes.length; i++) { + if (externalSelection.includes(graph.nodes[i].id)) { + linkNodes.push(graph.nodes[i]); + } + } + } + hideLabels(); + drawLabels(linkNodes); + /* tip.style("opacity", 1) + .html("") + .style("left", (e.pageX-25) + "px") + .style("top", (e.pageY-75) + "px");*/ + }) + .on("mouseout", (e, d) => { + hideLabels(); + //restore external selection if present + if(externalSelection.length > 0) { + selectNodes(externalSelection) + } + /* tip.style("opacity", 0) + .style("left", "0px") + .style("top", "0px")*/ + }).call(d3.drag() + .on("start", dragstarted) + .on("drag", dragged) + .on("end", dragended)); + + svg.call(d3.zoom() + .extent([[0, 0], [+svg.node().getBoundingClientRect().width, +svg.node().getBoundingClientRect().height]]) + .scaleExtent([0, 8]) + .on("zoom", zoomed)); + + // node tooltip + /* node.append("title") + .text(function(d) { return d.id; });*/ +} + +let externalSelection = []; + +/**External node selection callback. This function applies highlight/unhighlight classes to selected/unselected nodes + * and links and triggers drawing of node labels for selected nodes. + * @param nodeIds List of ids of selected nodes. + */ +export function selectNodes(nodeIds) { + externalSelection = nodeIds; + //remove all selection classes + dNodes.classed("selected", false).order(); + dNodes.classed("unselected", false).order(); + dLinks.classed("selected", false).order(); + dLinks.classed("unselected", false).order(); + dText.classed("selected", false).order(); + dText.classed("unselected", false).order(); + + //select all nodes in nodeIds, unselect all other + if (nodeIds.length > 0) { + dNodes.classed("unselected", d => !nodeIds.includes(d.id)); + dLinks.classed("unselected", d => !(nodeIds.includes(d.source.id) && nodeIds.includes(d.target.id))); + dText.classed("unselected", d => !(nodeIds.includes(d.source.id) && nodeIds.includes(d.target.id))); + + dNodes.classed("selected", d => nodeIds.includes(d.id)); + dLinks.classed("selected", d => (nodeIds.includes(d.source.id) && nodeIds.includes(d.target.id))); + dText.classed("selected", d => (nodeIds.includes(d.source.id) && nodeIds.includes(d.target.id))); + } + + //increase radius for selected nodes + dNodes.attr("r", d => nodeIds.includes(d.id) ? 20 : 16); + + //check for label drawing + if(nodeIds.length == 0){ + //no labels drawn + hideLabels(); + }else { + //get nodes for all selected ids + let selectedNodes = []; + for (let i = 0; i < graph.nodes.length; i++) { + if(nodeIds.includes(graph.nodes[i].id)) { + selectedNodes.push(graph.nodes[i]); + } + } + //redraw labels + hideLabels(); + drawLabels(selectedNodes); + } +} + +/**Draw node labels for the provided list of nodes. + * @param linkNodes A list of selected (or hovered) nodes. + */ +function drawLabels(linkNodes){ + //create layer for labels + dLabels = gOverlay.selectAll("g") + .attr("id", "drawLayer") + .data(linkNodes) + .enter() + .append("g") + .attr("class", "nameLabel"); + //append label path + dLabels.append("path").attr("d", d => `M${d.x - 20} ${d.y - 20} l 6 10 l -10 -4 l -170 0 l 0 -16 l 174 0 Z`); + //append label text + dLabels.append("text") + .attr("font-family", "sans-serif") + .attr("font-size", 12) + .text(d => { + return d.customName ? d.customName : d.id; + }) + .attr("x", (d, i, e) => { + //flexible positioning of label to center in box bounds + let scale = 170.0 / e[i].getComputedTextLength(); + if (scale >= 1) { + return d.x - 107 - e[i].getComputedTextLength() / 2; + } else { + return d.x - 107 - (scale * e[i].getComputedTextLength()) / 2; + } + }) + .attr("y", d => d.y - 18) + .attr("transform", (d, i, e) => { + //set optional transformation for scaling label to fit into box bounds + let scale = 170.0 / e[i].getComputedTextLength(); + let xPos = d.x - 107 - (scale * e[i].getComputedTextLength()) / 2; + let yPos = d.y - 18; + return (scale < 1) ? "translate(" + (-xPos * (scale - 1)) + "," + (-yPos * (scale - 1)) + ")scale(" + scale + "," + scale + ")" : ""; + }); +} + +/**Hide node labels layer. + */ +function hideLabels(){ + svg.selectAll(".nameLabel").remove(); + svg.selectAll("#drawLayer").remove(); + dLabels = undefined; +} + +// update the display positions after each simulation tick +function ticked() { + dLinks + .attr("x1", function (d) { + return d.source.x; + }) + .attr("y1", function (d) { + return d.source.y; + }) + .attr("x2", function (d) { + return d.target.x; + }) + .attr("y2", function (d) { + return d.target.y; + }); + + let processed = []; + dText.attr("transform", (d, i) => { + let slope = (d.target.y - d.source.y) / (d.target.x - d.source.x); + let deg = Math.atan(slope) * 180 / Math.PI; + let centerX = (d.source.x + d.target.x) / 2; + let centerY = (d.source.y + d.target.y) / 2; + let xTrans = -maxTextWidth / 2; + let yTrans = -3; + processed.push(d.source.id + " - " + d.target.id); + if (processed.includes(d.target.id + " - " + d.source.id)) { + yTrans = 8; + } + return "translate(" + centerX + "," + centerY + ")rotate(" + deg + ")translate(" + xTrans + "," + yTrans + ")"; + }); + + dNodes + .attr("cx", function (d) { + return d.x; + }) + .attr("cy", function (d) { + return d.y; + }); + + if (dLabels) { + dLabels + .attr("x", function (d) { + return d.x; + }) + .attr("y", function (d) { + return d.y; + }) + ; + } + d3.select('#alpha_value').style('flex-basis', (simulation.alpha() * 100) + '%'); +} + +//////////// UI EVENTS //////////// +function zoomed({transform}) { + zoomTransform = transform; + gLink.attr("transform", transform); + gNode.attr("transform", transform); + gOverlay.attr("transform", transform); +} + +/**Dragging has started. + *@param e The Event. + *@param d The data of the dragged node. + */ +function dragstarted(e, d) { + if (!e.active) simulation.alphaTarget(0.3).restart(); + hideLabels(); + dragging = true; +} + +/**Update node position on drag. + *@param e The Event. + *@param d The data of the dragged node. + */ +function dragged(e, d) { + d3.select(this).attr("cx", d.x = e.x).attr("cy", d.y = e.y); +} + +/**Dragging has ended. + *@param e The Event. + *@param d The data of the dragged node. + */ +function dragended(e, d) { + if (!e.active) simulation.alphaTarget(0.0001); + d.fx = null; + d.fy = null; + dragging = false; +} + +// update size-related forces +d3.select(window).on("resize", function () { + width = +svg.node().getBoundingClientRect().width; + height = +svg.node().getBoundingClientRect().height; + updateForces(); +}); diff --git a/js/fdo-maker.settings.js b/js/fdo-maker.settings.js deleted file mode 100644 index db38cfa..0000000 --- a/js/fdo-maker.settings.js +++ /dev/null @@ -1,17 +0,0 @@ -//export let ajaxBaseUrl = "https://demo.datamanager.kit.edu:8090/"; -export let ajaxBaseUrl = "http://localhost:8090"; -export const keycloak = undefined; -/*Keycloak({ - url: 'https://gateway.datamanager.kit.edu:8443/', - realm: 'dem_testing', - clientId: 'kitdm-services' -});*/ - -export const tags = [{"name":"tag", "color":"red"}, {"name":"tag2", "color":"blue"},{"name":"tag3", "color":"green"}]; -export const showServiceUrl = true; - -export const appDescription = { - "app-logo":"./images/typed-pid-maker-logo.svg", - "app-title":"FAIR-DO Maker", - "app-subtitle":"Interactively create your FAIR-DOs." -}; diff --git a/js/graph.js b/js/graph.js index 740dcdc..9b37588 100644 --- a/js/graph.js +++ b/js/graph.js @@ -1,65 +1,127 @@ let data = { - "nodes": [ - {"id": "1234", "group": 1}, - {"id": "345", "group": 2}, - {"id": "753", "group": 2}, - {"id": "7745", "group": 1}, - {"id": "1344", "group": 3} - ], - "links": [ - /*{source: "1234", target: "753", value: 1}, - {source: "1234", target: "7745", value: 2}, - {source: "1344", target: "345", value: 13}*/ - ] -} -let margin = ({top: 20, right: 20, bottom: 20, left: 100}); -let step = 28; -let height = (data.nodes.length - 1) * step + margin.top + margin.bottom; -let graph = undefined; + "nodes": [], + "links": [] +} +let margin = ({top: 20, right: 20, bottom: 20, left: 250}); +let step = 25; + +let svg, + graph, + zoomTransform, + height, + y, + color, + firstSelection, + lastSelection, + link_callback, + relations_callback = undefined; let buildGraph = () => { - const nodes = data.nodes.map(({id, group}) => ({ + const nodes = data.nodes.map(({id, type, customName, props}) => ({ id, + customName, sourceLinks: [], targetLinks: [], - group + type, + props })); const nodeById = new Map(nodes.map(d => [d.id, d])); - const links = data.links.map(({source, target, value}) => ({ + const links = data.links.map(({source, target, relationType}) => ({ source: nodeById.get(source), target: nodeById.get(target), - value + relationType })); for (const link of links) { - const {source, target, value} = link; + const {source, target} = link; source.sourceLinks.push(link); target.targetLinks.push(link); } return {nodes, links}; } -let firstSelection = undefined; -let lastSelection = undefined; -graph = buildGraph(); +export function setData(fdos) { + data = fdos; + height = (data.nodes.length - 1) * 2 * step + margin.top + margin.bottom; + graph = buildGraph(); + y = d3.scalePoint(graph.nodes.map(d => d.id).sort(d3.ascending), [margin.top, height - margin.bottom]) + color = d3.scaleOrdinal(graph.nodes.map(d => d.type).sort(d3.ascending), d3.schemeCategory10) +} + +let line, circles, pathLabels, idLayer, overlay, label, path; +let altDown = false; + +export function chart(chartData, link_cb, relations_cb) { + setData(chartData); + link_callback = link_cb; + relations_callback = relations_cb; + svg = d3.select("svg").attr("viewBox", [0, 0, 400, height]).on("mousedown", mousedown).on("mouseup", mouseup); + d3.select("body").on("keydown", function (e) { + altDown = e.altKey; + if (e.altKey) { + svg.on('.zoom', null); + } + }).on("keyup", function (e) { + altDown = e.altKey; + if (!e.altKey) { + svg.call(d3.zoom() + .extent([[0, 0], [640, height]]) + .scaleExtent([0, 8]) + .on("zoom", zoomed)); + } + }); + update(); + return svg.node(); +} +var line, circles; + +function mousedown(e) { + var m = d3.pointer(e); + line = svg.append("line") + .attr("fill", "#444") + .attr("stroke-opacity", 0.6) + .attr("stroke-width", 2.5) + .attr('stroke', "#444") + .attr("x1", m[0]) + .attr("y1", m[1]) + .attr("x2", m[0]) + .attr("y2", m[1]); + + circles.on("mousemove", mousemove); +} -let y = d3.scalePoint(graph.nodes.map(d => d.id).sort(d3.ascending), [margin.top, height - margin.bottom]) -let color = d3.scaleOrdinal(graph.nodes.map(d => d.group).sort(d3.ascending), d3.schemeCategory10) -let svg = undefined; +function mousemove(e) { + var m = d3.pointer(e); + line.attr("x2", m[0]) + .attr("y2", m[1]); +} -export function chart() { - svg = d3.select("svg").attr("viewBox", [0, 0, 640, height]); +function mouseup(e, d) { + console.log(d); + svg.selectAll("line").remove(); + svg.on("mousemove", null); +} +/**Reset the SVG element, i.e., remove all children and add all required styles and definitions. + */ +function reset() { + //clear all + svg.selectAll("*").remove(); + //add styles for hover effects svg.append("style").text(` .hover path { - stroke: #ccc; + //stroke: #ccc; +} + +.fdo_label{ + stroke: #999; } .hover text { - fill: #ccc; + //fill: #ccc; } .hover g.primary text { @@ -78,131 +140,394 @@ export function chart() { g.selected text{ fill: red; - font-weight: bold; + font-weight: bold; } +.tooltip { + background:#fefabc; + pointer-events: none; + border: 1px solid #cccccc; + padding: 5px; + box-shadow: 0px 2px 4px rgba(0,0,0,0.3); + border-radius: 10px; + } + +.first_node { + stroke: #00FF00; + stroke-opacity: 1; + stroke-width: 2; + } + + .second_node { + stroke: #FF0000; + stroke-opacity: 1; + stroke-width: 2; + } `); - update(); + //add defs for selection mark + let defs = svg + .join("defs"); + + defs + .append('marker') + .attr('id', 'arrow') + .attr('viewBox', [0, 0, 40, 40]) + .attr('refX', 20) + .attr('refY', 20) + .attr('markerWidth', 40) + .attr('markerHeight', 40) + .attr('orient', 'reverse') + .append('path') + .attr("fill", "#444") + .attr("stroke-opacity", 0.6) + .attr("stroke-width", 0.5) + .attr('stroke', "#444") + .attr('d', 'M0,0Q15,0,20,10,15,20,0,20A1,1,0,000,0')//d3.line()([[0, 0], [0, 20], [20, 10]])) + .attr('transform', 'scale(2)') + + defs + .append("svg:marker") + .attr("id", "triangle") + .attr("refX", 6) + .attr("refY", 6) + .attr("markerWidth", 30) + .attr("markerHeight", 30) + .attr("markerUnits", "userSpaceOnUse") + .attr("orient", "auto") + .append("path") + .attr("d", "M 0 0 12 6 0 12 3 6") + .style("fill", "black"); + + defs.append("shape") + .attr("id", "label") + .append("path") + .attr("d", "M-3 0 0 5-5 3-32 3-32-5-3-5-3 0") + .style("fill", "none"); + + let grads = defs.selectAll("radialGradient") + .data(graph.nodes) + .enter() + .append("radialGradient") + .attr("gradientUnits", "objectBoundingBox") + .attr("cx", 0) + .attr("cy", 0) + .attr("r", "100%") + .attr("id", function (d, i) { + return "grad" + i; + }); - return svg.node(); -} + grads.append("stop") + .attr("offset", "0%") + .style("stop-color", "white"); -function update() { + grads.append("stop") + .attr("offset", "100%") + .style("stop-color", function (d) { + return color((d.customName ? d.customName : d.id)); + }); + + //clear all selections firstSelection = undefined; lastSelection = undefined; +} - svg.selectAll("g > *").remove(); +/**Update aka. redraw the SVG based on the graph data. + */ +export function update() { + reset(); + //tooltip div + let tip = d3.select("body").append("div") + .attr("class", "tooltip") + .style("opacity", 0); - let defs = svg.select("defs").append("g") - .attr('id', 'pointer') - .attr('transform', 'scale(0.8)'); - defs.append('path') - .attr('d', 'M0-1c-14.5-25.6-14.5-25.7-14.5-33.8c0-8.1,6.5-14.6,14.5-14.6s14.5,6.6,14.5,14.6C14.5-26.7,14.5-26.6,0-1z'); - defs.append('path') - .attr('d', 'M0-49c7.7,0,14,6.3,14,14.1c0,8,0,8.1-14,32.8c-14-24.7-14-24.9-14-32.8C-14-42.7-7.7-49,0-49 M0-50c-8.3,0-15,6.8-15,15.1 S-15-26.5,0,0c15-26.5,15-26.5,15-34.9S8.3-50,0-50L0-50z'); + let options = d3.select("body").append("div").attr("class", "tooltip").style("opacity", 0); - const label = svg.append("g") - .attr("font-family", "sans-serif") - .attr("font-size", 14) - .attr("text-anchor", "end") - .selectAll("g") - .data(graph.nodes) - .join("g") - .attr("transform", d => `translate(${margin.left},${d.y = y(d.id)})`) - .call(g => g.append("text") - .attr("x", -12) - .attr("dy", "0.35em") - .attr("fill", d => d3.lab(color(d.group)).darker(2)) - .text(d => d.id)); - - const path = svg.insert("g", "*") - .attr("fill", "none") - .attr("stroke-opacity", 0.6) - .attr("stroke-width", 1.5) - .selectAll("path") - .data(graph.links) - .join("path") - .attr("stroke", d => d.source.group === d.target.group ? color(d.source.group) : "#aaa") - .attr("d", arc); + let select = options.append("select").attr("id", "relationType"); + select.append("option").attr("value", "first").text("test1"); + select.append("option").attr("value", "second").text("test2") - const overlay = svg.append("g") - .attr("fill", "none") - .attr("pointer-events", "all") - .selectAll("rect") + //draw label layer + label = svg.append("g"); + + //draw hover overlay layer + overlay = svg.append("g"); + idLayer = svg.insert("g"); + + //draw circles (last to allow them to be clicked, otherwise, overlay will catch all events) + circles = svg.append("g") + .selectAll("circle") .data(graph.nodes) - .join("rect") - .attr("width", margin.left + 40) - .attr("height", step) - .attr("y", d => y(d.id) - step / 2) + .join("circle") + .attr("fill", function (d, i) { + return "url(#grad" + i + ")"; + }) + .attr("cx", d => { + d.x = margin.left; + return d.x + }) + .attr("cy", d => { + d.y = y(d.id); + return d.y + }) + .attr("r", 8) + .on("click", (e, d) => selectNode(e, d)) .on("mouseover", (e, d) => { svg.classed("hover", true); - label.classed("primary", n => n === d); - label.classed("secondary", n => n.sourceLinks.some(l => l.target === d) || n.targetLinks.some(l => l.source === d)); path.classed("primary", l => l.source === d || l.target === d).filter(".primary").raise(); + + path.attr('stroke-linecap', 'round') + .filter(".primary") + .attr("marker-end", 'url(#triangle)'); + /* tip.style("opacity", 1) + .html("Incoming: " + d.targetLinks.length + "
Outgoing: " + d.sourceLinks.length) + .style("left", (e.pageX - 25) + "px") + .style("top", (e.pageY - 75) + "px")*/ + relations_callback(d.sourceLinks, d.targetLinks); + + let linkNodes = []; + linkNodes.push(d); + for (let i = 0; i < d.sourceLinks.length; i++) { + linkNodes.push(d.sourceLinks[i].target); + } + for (let i = 0; i < d.targetLinks.length; i++) { + linkNodes.push(d.targetLinks[i].source); + } + + overlay.attr("fill", "#FFF") + .attr("pointer-events", "all") + .selectAll("path") + .data(linkNodes) + .join("path") + .attr("class", "fdo_label") + .attr("x", d => { + d.x = margin.left; + return d.x + }) + .attr("y", d => { + d.y = y(d.id); + return d.y + }) + .attr("d", d => `M${margin.left - 12} ${y(d.id) - 12} l 6 10 l -10 -4 l -170 0 l 0 -16 l 174 0 Z`); + + idLayer.attr("font-family", "sans-serif") + .attr("font-size", 10) + .attr("text-anchor", "end") + .selectAll("g") + .data(linkNodes) + .join("g") + .call(g => g.append("text") + .attr("class", "fdo_label") + .attr("fill", "#0") + .attr("stroke-width", 0) + .attr("x", margin.left - 20) + .attr("y", d => d.y = y(d.id) - 10) + .attr("fill", d => d3.lab(color(d.type)).darker(2)) + .text(d => d.customName ? d.customName : d.id)); + + overlay.classed("primary", n => n && n === d); + overlay.classed("secondary", n => n && (n.sourceLinks.some(l => l.target === d) || n.targetLinks.some(l => l.source === d))); + + overlay.classed("primary", n => n && (n === d || n.sourceLinks.some(l => l.target === d) || n.targetLinks.some(l => l.source === d))); + + //show tooltip and place next to focussed circle + /* tip.style("opacity", 1) + .html(propsToHtml(d.props)) + .style("left", (e.pageX-25) + "px") + .style("top", (e.pageY-75) + "px")*/ + /* options.style("opacity", 1) + .style("left", "500px") + .style("top", "900px")*/ + + //fillInfoBox(infoBox, d.props); + circles.attr("r", n => { + return (n === firstSelection || n === lastSelection || n === d) ? 10 : 8 + }); }) .on("mouseout", e => { svg.classed("hover", false); - label.classed("primary", false); - label.classed("secondary", false); + overlay.classed("primary", false); + overlay.classed("secondary", false); path.classed("primary", false).order(); - }); - - svg.append("g") - .selectAll("circle") - .data(graph.nodes) - .join("circle") - .attr("transform", d => `translate(${margin.left},${d.y = y(d.id)})`) - .attr("r", 6) - .attr("fill", d => color(d.group)) + path.attr('stroke-linecap', 'round') + .attr("marker-end", 'none'); + tip.style("opacity", 0) + .style("left", "0px") + .style("top", "0px") + + relations_callback(); + + //hide tooltip and move out of viewport to avoid receiving mouse over event + /* tip.style("opacity", 0) + .style("left", "0px") + .style("top", "0px")*/ + // fillInfoBox(infoBox, undefined); + + d3.selectAll(".fdo_label").remove(); + circles.attr("r", n => { + return (n === firstSelection || n === lastSelection) ? 10 : 8 + }); + }) .on("click", (e, d) => { - let color = "#00FF00"; - console.log("CLICK"); - if (firstSelection) { - //second one - color = "#FF0000"; - lastSelection = d; + //first selection done, check for unselect + if (firstSelection === d) { + //already selected, unselect and return + firstSelection = undefined; + } else if (lastSelection === d) { + //no unselect, select second node + lastSelection = undefined; } else { - firstSelection = d; + if (firstSelection) { + lastSelection = d; + } else { + firstSelection = d; + } } - var pointer = svg - .append("use") - .attr("fill", color) - .attr("stroke", "#000000") - .attr("stroke-width", "1px") - .attr("href", "#pointer") - .attr("transform", "translate(0,0) scale(0)"); - - pointer - .transition() - .duration(500) - .on("end", () => { - if (firstSelection && lastSelection) { - //store connection - let link = {source: firstSelection.id, target: lastSelection.id, value: 3}; - data.links.push(link); - graph = buildGraph(); - //repaint - firstSelection = undefined; - lastSelection = undefined; - - update(); - } + link_callback(firstSelection, lastSelection); - }) - .attr("x", margin.left) - .attr("y", y(d.id)) - .attr("transform", "scale(1)"); - }); + circles.classed("first_node", n => n === firstSelection); + circles.classed("second_node", n => n === lastSelection); + + circles.attr("r", n => { + return (n === firstSelection || n === lastSelection) ? 10 : 8 + }); + }).on("mousedown", mousedown) + .on("mouseup", mouseup); + + + //draw links layer + path = svg.insert("g", "*") + .attr("fill", "none") + .attr("stroke-opacity", 0.6) + .attr("stroke-width", 0.5) + .selectAll("path") + .data(graph.links) + .join("path") + .attr("id", "wavy") + .attr("stroke", d => color(d.relationType)) + .attr("d", arc); + + pathLabels = svg.insert("g") + .selectAll("text") + .data(graph.links) + .join("text") + .attr("font-family", "sans-serif") + .attr("font-size", 4) + .attr("x", function (d) { + let dir = (d.target.y > d.source.y) ? -2 : 1; + let r = Math.abs(d.target.y - d.source.y) / 2; + return d.source.x + (dir * r); + }) + .attr("y", function (d) { + let r = Math.abs(d.target.y - d.source.y) / 2; + let sy = (d.source.y < d.target.y) ? d.source.y : d.target.y; + return sy + r; + }) + // .attr("xlink:href", "#wavy") + .style("text-anchor", "start") //place the text halfway on the arc + .text(d => d.relationType); + + + /*svg.call(d3.zoom() + .extent([[0, 0], [640, height]]) + .scaleExtent([0, 8]) + .on("zoom", zoomed)); +*/ + //re-apply transform if already stored, e.g., after reset + if (zoomTransform) { + zoomed(zoomTransform); + } +} + +function zoomed({transform}) { + zoomTransform = {transform}; + label.attr("transform", transform); + path.attr("transform", transform); + overlay.attr("transform", transform); + circles.attr("transform", transform); + pathLabels.attr("transform", transform); + idLayer.attr("transform", transform); +} + +function mousedown(e) { + if(!altDown) return; + let m = d3.pointer(e); + line = svg.append("line") + .attr("stroke", "#000") + .attr("stroke-opacity", 0.6) + .attr("stroke-width", 1.5) + .attr("x1", m[0]) + .attr("y1", m[1]) + .attr("x2", m[0]) + .attr("y2", m[1]); + + svg.on("mousemove", mousemove); +} + +function mousemove(e) { + if(!altDown) return; + let m = d3.pointer(e); + line.attr("x2", m[0]) + .attr("y2", m[1]); +} + +function mouseup() { + svg.on("mousemove", null); + svg.selectAll("line").remove(); + svg.call(d3.zoom() + .extent([[0, 0], [640, height]]) + .scaleExtent([0, 8]) + .on("zoom", zoomed)); } +function fillInfoBox(infoBox, props) { + infoBox.selectAll("circle").remove(); + infoBox.selectAll("text").remove(); + if (!props) return; + let keys = Object.keys(props); + for (let i = 0; i < keys.length; i++) { + let value = props[keys[i]]; + infoBox.append("text").attr("x", 410).attr("y", i * 20 - 80).text(keys[i] + " : " + value).style("font-size", "12px").attr("alignment-baseline", "middle") + } +} + +function propsToHtml(props) { + let result = ""; + let keys = Object.keys(props); + for (let i = 0; i < keys.length; i++) { + let value = props[keys[i]]; + result += keys[i] + ":" + value + "
"; + } + return result; +} + +/**Function for creating an arc between two nodes. + */ function arc(d) { - const y1 = d.source.y; - const y2 = d.target.y; - const r = Math.abs(y2 - y1) / 2; - return `M${margin.left},${y1}A${r},${r} 0,0,${y1 < y2 ? 1 : 0} ${margin.left},${y2}`; + /* const y1 = d.source.y; + const y2 = d.target.y; + + const r = Math.abs(y2 - y1) / 2; + + return `M${margin.left + 8},${y1}A${r},${r} 0,0,${y1 < y2 ? 1 : 0} ${margin.left + 12},${y2}`; + */ + let dx = d.target.x - d.source.x, + dy = d.target.y - d.source.y, + dr = Math.sqrt(dx * dx + dy * dy) / 2, + mx = d.source.x + dx, + my = d.source.y + dy, + dir = (d.source.y < d.target.y) ? -1 : 1; + + return [ + "M", d.source.x + (12 * dir), d.source.y, + // "A",dr,dr,0,0,0,mx,my, + "A", dr, dr, 0, 0, 0, d.target.x + (12 * dir), d.target.y + ].join(" "); + + + //M 0 0 C 30 0 50 20 50 40 C 50 60 20 80 1 80 } diff --git a/js/mapping-service.settings.js b/js/mapping-service.settings.js deleted file mode 100644 index d20b7dc..0000000 --- a/js/mapping-service.settings.js +++ /dev/null @@ -1,16 +0,0 @@ - -export let ajaxBaseUrl = "http://localhost:8090"; -export const keycloak = undefined; -/*export const keycloak = Keycloak({ - url: 'https://gateway.datamanager.kit.edu:8443/', - realm: 'dem_testing', - clientId: 'kitdm-services' -});*/ -export const tags = [{ "name": "tag", "color": "red" }, { "name": "tag2", "color": "blue" }, { "name": "tag3", "color": "green" }]; -export const showServiceUrl = true; - -export const appDescription = { - "app-logo": "./images/mapping_service.jpg", - "app-title": "Mapping Service UI", - "app-subtitle": "Extract metadata and map it to json." -}; \ No newline at end of file diff --git a/js/metastore-utils.js b/js/metastore-utils.js index 2ee10c3..cf011c0 100644 --- a/js/metastore-utils.js +++ b/js/metastore-utils.js @@ -12,7 +12,7 @@ function generateEtag(idValue, type) { Accept: accept }; - if (config.token != null) { + if (config.token !== null) { headers["Authorization"] = "Bearer " + config.token; } return new Promise(function (resolve, reject) { @@ -29,7 +29,7 @@ function generateEtag(idValue, type) { let message = "Failed generate ETag for resource with id " + idValue + ". (HTTP " + result.status + ")"; reject(message); } - }) + }); }); } @@ -42,7 +42,7 @@ export function readSchema(schemaUrl) { return new Promise(function (resolve, reject) { let headers = {}; - if (config.token != null) { + if (config.token !== null) { headers["Authorization"] = "Bearer " + config.token; } $.ajax({ @@ -60,37 +60,37 @@ export function readSchema(schemaUrl) { }); }; -export function readSchemaIds(){ - let headers = { - Accept: "application/vnd.datamanager.schema-record+json" - }; +export function readSchemaIds() { + let headers = { + Accept: "application/vnd.datamanager.schema-record+json" + }; - if (config.token != null) { - headers["Authorization"] = "Bearer " + config.token; - } + if (config.token !== null) { + headers["Authorization"] = "Bearer " + config.token; + } - let result = undefined; - $.ajax({ - type: "GET", - url: config.ajaxBaseUrl + "schemas?size=100", - contentType: "application/json", - dataType: 'json', - async: false, - headers: headers, - success: function (output, status, xhr) { - //TODO: send back ETag and use it to check for conflicts later on - //let res = {}; - //res.etag = xhr.getResponseHeader("etag"); - //res.content = output; - result = output; - }, - error: function (result) { - let message = "Failed to read schema ids from URL " + config.ajaxBaseUrl + "/schemas" + ". (HTTP " + result.status + ")"; - result = message; - } - }); + let result = undefined; + $.ajax({ + type: "GET", + url: config.ajaxBaseUrl + "schemas?size=100", + contentType: "application/json", + dataType: 'json', + async: false, + headers: headers, + success: function (output, status, xhr) { + //TODO: send back ETag and use it to check for conflicts later on + //let res = {}; + //res.etag = xhr.getResponseHeader("etag"); + //res.content = output; + result = output; + }, + error: function (result) { + let message = "Failed to read schema ids from URL " + config.ajaxBaseUrl + "/schemas" + ". (HTTP " + result.status + ")"; + result = message; + } + }); - return result; + return result; } /** @@ -105,7 +105,7 @@ export function readSchemaRecord(schemaRecordUrl) { Accept: "application/vnd.datamanager.schema-record+json" }; - if (config.token != null) { + if (config.token !== null) { headers["Authorization"] = "Bearer " + config.token; } @@ -128,7 +128,7 @@ export function readSchemaRecord(schemaRecordUrl) { } }); }); -}; +} /** * updates the metadata record. @@ -153,7 +153,7 @@ export function updateMetadataRecord(valueRecord, metadataDocumentFile) { "If-Match": result }; - if (config.token != null) { + if (config.token !== null) { headers["Authorization"] = "Bearer " + config.token; } @@ -168,19 +168,19 @@ export function updateMetadataRecord(valueRecord, metadataDocumentFile) { resolve("Metadata record successfully updated."); }, error: function (result) { - let message = "Failed to update schema record. (HTTP " + result.status + ")"; + var response = JSON.parse(result.responseText); + let message = "Failed to update metadata record. (HTTP " + result.status + ") -> " + response.detail; reject(message); } }); }); - }) -}; - + }); +} /** * Updates the schema record. * @param {type} valueRecord JSON value of the schema record. - * @param {type} metadataDocumentFile file of the schema document. + * @param {type} schemaDocumentFile file of the schema document. * @returns {undefined} */ export function updateSchemaRecord(valueRecord, schemaDocumentFile) { @@ -189,7 +189,7 @@ export function updateSchemaRecord(valueRecord, schemaDocumentFile) { const recordFile = new File([blobRecord], 'recordFile.json'); formData.append("record", recordFile); - if (schemaDocumentFile != null) { + if (schemaDocumentFile !== null) { formData.append("schema", schemaDocumentFile); } @@ -201,7 +201,7 @@ export function updateSchemaRecord(valueRecord, schemaDocumentFile) { "If-Match": result }; - if (config.token != null) { + if (config.token !== null) { headers["Authorization"] = "Bearer " + config.token; } @@ -216,12 +216,13 @@ export function updateSchemaRecord(valueRecord, schemaDocumentFile) { resolve("Schema record successfully updated."); }, error: function (result) { - let message = "Failed to update schema record. (HTTP " + result.status + ")"; + var response = JSON.parse(result.responseText); + let message = "Failed to update schema record. (HTTP " + result.status + ") -> " + response.detail; reject(message); } }); }); - }) + }); } /** @@ -233,7 +234,7 @@ export function updateSchemaRecord(valueRecord, schemaDocumentFile) { export function createMetadataRecord(valueMetadataRecord, metadataDocumentFile) { let headers = {}; - if (config.token != null) { + if (config.token !== null) { headers["Authorization"] = "Bearer " + config.token; } @@ -254,14 +255,15 @@ export function createMetadataRecord(valueMetadataRecord, metadataDocumentFile) data: formData, headers: headers, success: function () { - resolve("Metadata document successfully created.") + resolve("Metadata document successfully created."); }, error: function (result) { - let message = "Failed to create schema record. (HTTP " + result.status + ")"; + var response = JSON.parse(result.responseText); + let message = "Failed to ingest metadata document. (HTTP " + result.status + ") -> " + response.detail; reject(message); } }); - }) + }); } /** @@ -273,7 +275,7 @@ export function createMetadataRecord(valueMetadataRecord, metadataDocumentFile) export function createSchemaRecord(valueSchemaRecord, schemaDocumentFile) { let headers = {}; - if (config.token != null) { + if (config.token !== null) { headers["Authorization"] = "Bearer " + config.token; } @@ -297,11 +299,13 @@ export function createSchemaRecord(valueSchemaRecord, schemaDocumentFile) { resolve("Schema document successfully created.") }, error: function (result) { - let message = "Failed to create schema record. (HTTP " + result.status + ")"; + var response = JSON.parse(result.responseText); + let message = "Failed to create schema record. (HTTP " + result.status + ") -> " + response.detail; + result.responseText.detail; reject(message); } }); - }) + }); } /** @@ -312,7 +316,7 @@ export function createSchemaRecord(valueSchemaRecord, schemaDocumentFile) { export function readMetadataDocument(metadataDocumentUri) { let headers = {}; - if (config.token != null) { + if (config.token !== null) { headers["Authorization"] = "Bearer " + config.token; } @@ -330,6 +334,5 @@ export function readMetadataDocument(metadataDocumentUri) { reject(message); } }); - }) -}; - + }); +} diff --git a/js/metastore.settings.js b/js/metastore.settings.js deleted file mode 100644 index b33e9c5..0000000 --- a/js/metastore.settings.js +++ /dev/null @@ -1,14 +0,0 @@ -export let ajaxBaseUrl = "http://metastore.docker:8040/metastore/api/v1/"; - -export const keycloak = undefined; - -export const showServiceUrl = false; -export const searchEnabled = true; - -export const appDescription = { - "app-logo":"./images/metadata.jpg", - "app-title":"MetaStore Frontend 4 Docker", - "app-subtitle":"Schema and Metadata Management" -}; - - diff --git a/js/semantic-ui/dimmer.min.js b/js/semantic-ui/dimmer.min.js index 65f6cd7..219e81e 100644 --- a/js/semantic-ui/dimmer.min.js +++ b/js/semantic-ui/dimmer.min.js @@ -1,11 +1 @@ -/* - * # Fomantic UI - 2.9.2 - * https://github.com/fomantic/Fomantic-UI - * https://fomantic-ui.com/ - * - * Copyright 2023 Contributors - * Released under the MIT license - * https://opensource.org/licenses/MIT - * - */ -!function(T,e,S){"use strict";function D(e){return"function"==typeof e&&"number"!=typeof e.nodeType}e=void 0!==e&&e.Math===Math?e:globalThis,T.fn.dimmer=function(v){var b,p=T(this),h=Date.now(),y=[],C=v,w="string"==typeof C,x=[].slice.call(arguments,1);return p.each(function(){var o,i,a=T.isPlainObject(v)?T.extend(!0,{},T.fn.dimmer.settings,v):T.extend({},T.fn.dimmer.settings),n=a.selector,e=a.namespace,t=a.className,d=a.error,r="."+e,s="module-"+e,m=p.selector||"",l="ontouchstart"in S.documentElement?"touchstart":"click",c=T(this),u=this,f=c.data(s),g={preinitialize:function(){o=g.is.dimmer()?(i=c.parent(),c):(i=c,g.has.dimmer()?a.dimmerName?i.find(n.dimmer).filter("."+a.dimmerName):i.find(n.dimmer):g.create())},initialize:function(){g.debug("Initializing dimmer",a),g.bind.events(),g.set.dimmable(),g.instantiate()},instantiate:function(){g.verbose("Storing instance of module",g),f=g,c.data(s,f)},destroy:function(){g.verbose("Destroying previous module",o),g.unbind.events(),g.remove.variation(),i.off(r)},bind:{events:function(){"hover"===a.on?i.on("mouseenter"+r,g.show).on("mouseleave"+r,g.hide):"click"===a.on&&i.on(l+r,g.toggle),g.is.page()&&(g.debug("Setting as a page dimmer",i),g.set.pageDimmer()),g.is.closable()&&(g.verbose("Adding dimmer close event",o),i.on(l+r,n.dimmer,g.event.click))}},unbind:{events:function(){c.removeData(s),i.off(r)}},event:{click:function(e){g.verbose("Determining if event occurred on dimmer",e),0!==o.find(e.target).length&&!T(e.target).is(n.content)||(g.hide(),e.stopImmediatePropagation())}},addContent:function(e){e=T(e);g.debug("Add content to dimmer",e),e.parent()[0]!==o[0]&&e.detach().appendTo(o)},create:function(){var e=T(a.template.dimmer(a));return a.dimmerName&&(g.debug("Creating named dimmer",a.dimmerName),e.addClass(a.dimmerName)),e.appendTo(i),e},show:function(e){e=D(e)?e:function(){},g.is.dimmed()&&!g.is.animating()||!g.is.enabled()?g.debug("Dimmer is already shown or disabled"):!1===a.onShow.call(u)?g.verbose("Show callback returned false cancelling dimmer show"):(g.debug("Showing dimmer",o,a),g.set.variation(),g.animate.show(e),a.onChange.call(u))},hide:function(e){e=D(e)?e:function(){},g.is.dimmed()||g.is.animating()?!1===a.onHide.call(u)?g.verbose("Hide callback returned false cancelling dimmer hide"):(g.debug("Hiding dimmer",o),g.animate.hide(e),a.onChange.call(u)):g.debug("Dimmer is not visible")},toggle:function(){g.verbose("Toggling dimmer visibility",o),g.is.dimmed()?g.is.closable()&&g.hide():g.show()},animate:{show:function(e){e=D(e)?e:function(){},a.useCSS&&void 0!==T.fn.transition?(a.useFlex?(g.debug("Using flex dimmer"),g.remove.legacy()):(g.debug("Using legacy non-flex dimmer"),g.set.legacy()),"auto"!==a.opacity&&g.set.opacity(),o.transition({debug:a.debug,verbose:a.verbose,silent:a.silent,displayType:a.useFlex?"flex":"block",animation:(a.transition.showMethod||a.transition)+" in",queue:!1,duration:g.get.duration(),useFailSafe:!0,onStart:function(){g.set.dimmed()},onComplete:function(){g.set.active(),a.onVisible.call(o),e()}})):(g.verbose("Showing dimmer animation with javascript"),g.set.dimmed(),"auto"===a.opacity&&(a.opacity=.8),o.stop().css({opacity:0,width:"100%",height:"100%"}).fadeTo(g.get.duration(),a.opacity,function(){o.removeAttr("style"),g.set.active(),a.onVisible.call(o),e()}))},hide:function(e){e=D(e)?e:function(){},a.useCSS&&void 0!==T.fn.transition?(g.verbose("Hiding dimmer with css"),o.transition({debug:a.debug,verbose:a.verbose,silent:a.silent,displayType:a.useFlex?"flex":"block",animation:(a.transition.hideMethod||a.transition)+" out",queue:!1,duration:g.get.duration(),useFailSafe:!0,onComplete:function(){g.remove.dimmed(),g.remove.variation(),g.remove.active(),a.onHidden.call(o),e()}})):(g.verbose("Hiding dimmer with javascript"),o.stop().fadeOut(g.get.duration(),function(){g.remove.dimmed(),g.remove.active(),o.removeAttr("style"),a.onHidden.call(o),e()}))}},get:{dimmer:function(){return o},duration:function(){return g.is.active()?a.transition.hideDuration||a.duration.hide||a.duration:a.transition.showDuration||a.duration.show||a.duration}},has:{dimmer:function(){return a.dimmerName?0 .ui.dimmer",content:".ui.dimmer > .content, .ui.dimmer > .content > .center"},template:{dimmer:function(e){var i,n=T("
").addClass("ui dimmer");return e.displayLoader&&(i=T("
").addClass(e.className.loader).addClass(e.loaderVariation),e.loaderText&&(i.text(e.loaderText),i.addClass("text")),n.append(i)),n}}}}(jQuery,window,document); \ No newline at end of file +!function(x,e,F,T){"use strict";e=void 0!==e&&e.Math==Math?e:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(),x.fn.dimmer=function(p){var b,v=x(this),h=(new Date).getTime(),y=[],C=p,w="string"==typeof C,S=[].slice.call(arguments,1);return v.each(function(){var a,i,s,r=x.isPlainObject(p)?x.extend(!0,{},x.fn.dimmer.settings,p):x.extend({},x.fn.dimmer.settings),n=r.selector,e=r.namespace,t=r.className,m=r.error,o="."+e,d="module-"+e,c=v.selector||"",l="ontouchstart"in F.documentElement?"touchstart":"click",u=x(this),f=this,g=u.data(d);(s={preinitialize:function(){a=s.is.dimmer()?(i=u.parent(),u):(i=u,s.has.dimmer()?r.dimmerName?i.find(n.dimmer).filter("."+r.dimmerName):i.find(n.dimmer):s.create())},initialize:function(){s.debug("Initializing dimmer",r),s.bind.events(),s.set.dimmable(),s.instantiate()},instantiate:function(){s.verbose("Storing instance of module",s),g=s,u.data(d,g)},destroy:function(){s.verbose("Destroying previous module",a),s.unbind.events(),s.remove.variation(),i.off(o)},bind:{events:function(){"hover"==r.on?i.on("mouseenter"+o,s.show).on("mouseleave"+o,s.hide):"click"==r.on&&i.on(l+o,s.toggle),s.is.page()&&(s.debug("Setting as a page dimmer",i),s.set.pageDimmer()),s.is.closable()&&(s.verbose("Adding dimmer close event",a),i.on(l+o,n.dimmer,s.event.click))}},unbind:{events:function(){u.removeData(d),i.off(o)}},event:{click:function(e){s.verbose("Determining if event occured on dimmer",e),(0===a.find(e.target).length||x(e.target).is(n.content))&&(s.hide(),e.stopImmediatePropagation())}},addContent:function(e){var i=x(e);s.debug("Add content to dimmer",i),i.parent()[0]!==a[0]&&i.detach().appendTo(a)},create:function(){var e=x(r.template.dimmer());return r.dimmerName&&(s.debug("Creating named dimmer",r.dimmerName),e.addClass(r.dimmerName)),e.appendTo(i),e},show:function(e){e=x.isFunction(e)?e:function(){},s.debug("Showing dimmer",a,r),s.set.variation(),s.is.dimmed()&&!s.is.animating()||!s.is.enabled()?s.debug("Dimmer is already shown or disabled"):(s.animate.show(e),r.onShow.call(f),r.onChange.call(f))},hide:function(e){e=x.isFunction(e)?e:function(){},s.is.dimmed()||s.is.animating()?(s.debug("Hiding dimmer",a),s.animate.hide(e),r.onHide.call(f),r.onChange.call(f)):s.debug("Dimmer is not visible")},toggle:function(){s.verbose("Toggling dimmer visibility",a),s.is.dimmed()?s.hide():s.show()},animate:{show:function(e){e=x.isFunction(e)?e:function(){},r.useCSS&&x.fn.transition!==T&&a.transition("is supported")?(r.useFlex?(s.debug("Using flex dimmer"),s.remove.legacy()):(s.debug("Using legacy non-flex dimmer"),s.set.legacy()),"auto"!==r.opacity&&s.set.opacity(),a.transition({displayType:r.useFlex?"flex":"block",animation:r.transition+" in",queue:!1,duration:s.get.duration(),useFailSafe:!0,onStart:function(){s.set.dimmed()},onComplete:function(){s.set.active(),e()}})):(s.verbose("Showing dimmer animation with javascript"),s.set.dimmed(),"auto"==r.opacity&&(r.opacity=.8),a.stop().css({opacity:0,width:"100%",height:"100%"}).fadeTo(s.get.duration(),r.opacity,function(){a.removeAttr("style"),s.set.active(),e()}))},hide:function(e){e=x.isFunction(e)?e:function(){},r.useCSS&&x.fn.transition!==T&&a.transition("is supported")?(s.verbose("Hiding dimmer with css"),a.transition({displayType:r.useFlex?"flex":"block",animation:r.transition+" out",queue:!1,duration:s.get.duration(),useFailSafe:!0,onStart:function(){s.remove.dimmed()},onComplete:function(){s.remove.variation(),s.remove.active(),e()}})):(s.verbose("Hiding dimmer with javascript"),s.remove.dimmed(),a.stop().fadeOut(s.get.duration(),function(){s.remove.active(),a.removeAttr("style"),e()}))}},get:{dimmer:function(){return a},duration:function(){return"object"==typeof r.duration?s.is.active()?r.duration.hide:r.duration.show:r.duration}},has:{dimmer:function(){return r.dimmerName?0 .ui.dimmer",content:".ui.dimmer > .content, .ui.dimmer > .content > .center"},template:{dimmer:function(){return x("
").attr("class","ui dimmer")}}}}(jQuery,window,document); \ No newline at end of file diff --git a/js/semantic-ui/dropdown.min.js b/js/semantic-ui/dropdown.min.js index 0521558..2bd7a05 100644 --- a/js/semantic-ui/dropdown.min.js +++ b/js/semantic-ui/dropdown.min.js @@ -1,11 +1 @@ -/* - * # Fomantic UI - 2.9.2 - * https://github.com/fomantic/Fomantic-UI - * https://fomantic-ui.com/ - * - * Copyright 2023 Contributors - * Released under the MIT license - * https://opensource.org/licenses/MIT - * - */ -!function(Z,ee,te){"use strict";function ne(e){return"function"==typeof e&&"number"!=typeof e.nodeType}ee=void 0!==ee&&ee.Math===Math?ee:globalThis,Z.fn.dropdown=function(B){var K,W=Z(this),Q=Z(te),Y=W.selector||"",$=Date.now(),_=[],X=B,G="string"==typeof X,J=[].slice.call(arguments,1);return W.each(function(z){var v,e,t,n,i,a,o,s,r,m=Z.isPlainObject(B)?Z.extend(!0,{},Z.fn.dropdown.settings,B):Z.extend({},Z.fn.dropdown.settings),h=m.className,f=m.message,l=m.fields,g=m.keys,p=m.metadata,P=m.namespace,c=m.regExp,b=m.selector,d=m.error,F=m.templates,u="."+P,w="module-"+P,y=Z(this),C=[ee,te].indexOf(m.context)<0?Q.find(m.context):Z(m.context),x=y.find(b.text),S=y.find(b.search),A=y.find(b.sizer),T=y.find(b.input),N=y.find(b.icon),H=y.find(b.clearIcon),L=0").html(i).attr("data-"+p.value,t).attr("data-"+p.text,t).addClass(h.addition).addClass(h.item),m.hideAdditions&&i.addClass(h.hidden),n=void 0===n?i:n.add(i),M.verbose("Creating user choices for value",t,i))}),n)},userLabels:function(e){var t=M.get.userValues();t&&(M.debug("Adding user labels",t),Z.each(t,function(e,t){M.verbose("Adding custom user value"),M.add.label(t,t)}))},menu:function(){k=Z("
").addClass(h.menu).appendTo(y)},sizer:function(){A=Z("").addClass(h.sizer).insertAfter(S)}},search:function(e){e=void 0!==e?e:M.get.query(),M.verbose("Searching for query",e),!1===m.fireOnInit&&M.is.initialLoad()?M.verbose("Skipping callback on initial load",m.onSearch):M.has.minCharacters(e)&&!1!==m.onSearch.call(R,e)?M.filter(e):M.hide(null,!0)},select:{firstUnfiltered:function(){M.verbose("Selecting first non-filtered element"),M.remove.selectedItem(),D.not(b.unselectable).not(b.addition+b.hidden).eq(0).addClass(h.selected)},nextAvailable:function(e){var t=(e=e.eq(0)).nextAll(b.item).not(b.unselectable).eq(0),e=e.prevAll(b.item).not(b.unselectable).eq(0);0").addClass("remove icon").insertBefore(x)),M.is.search()&&!M.has.search()&&(M.verbose("Adding search input"),e=y.prev("label"),S=Z("").addClass(h.search).prop("autocomplete",M.is.chrome()?"fomantic-search":"off"),0").attr("class",T.attr("class")).addClass(h.selection).addClass(h.dropdown).html(F.dropdown(e,l,m.preserveHTML,m.className)).insertBefore(T),T.hasClass(h.multiple)&&!1===T.prop("multiple")&&(M.error(d.missingMultiple),T.prop("multiple",!0)),T.is("[multiple]")&&M.set.multiple(),T.prop("disabled")&&(M.debug("Disabling dropdown"),y.addClass(h.disabled)),T.is("[required]")&&(m.forceSelection=!0),m.allowTab||T.removeAttr("tabindex"),T.prop("required",!1).removeAttr("class").detach().prependTo(y)),M.refresh()},menu:function(e){k.html(F.menu(e,l,m.preserveHTML,m.className)),D=k.find(b.item),I=m.hideDividers?D.parent().children(b.divider):Z()},reference:function(){M.debug("Dropdown behavior was called on select, replacing with closest dropdown"),y=y.parent(b.dropdown),V=y.data(w),R=y[0],M.refresh(),M.setup.returnedObject()},returnedObject:function(){var e=W.slice(0,z),t=W.slice(z+1);W=e.add(y).add(t)}},refresh:function(){M.refreshSelectors(),M.refreshData()},refreshItems:function(){D=k.find(b.item),I=m.hideDividers?D.parent().children(b.divider):Z()},refreshSelectors:function(){M.verbose("Refreshing selector cache"),x=y.find(b.text),S=y.find(b.search),T=y.find(b.input),N=y.find(b.icon),L=0"),Z.each(e,function(e,t){var n=m.templates.deQuote(t[l.value]),i=m.templates.escape(t[l.name]||"",m.preserveHTML);T.append('")}),M.observe.select())}},event:{paste:function(e){var t,n,i,a,o,s=(e.originalEvent.clipboardData||ee.clipboardData).getData("text").split(m.delimiter),r=[];s.forEach(function(e){!1===M.set.selected(M.escape.htmlEntities(e.trim()),null,!0,!0)&&r.push(e)}),e.preventDefault(),0 modified, recreating menu"),M.disconnect.selectObserver(),M.refresh(),M.setup.select(),M.set.selected(),M.observe.select())}},menu:{mutation:function(e){var e=e[0],t=e.addedNodes?Z(e.addedNodes[0]):Z(!1),e=e.removedNodes?Z(e.removedNodes[0]):Z(!1),t=t.add(e),e=t.is(b.addition)||0=m.maxSelections?(M.debug("Maximum selection count reached"),m.useLabels&&(D.addClass(h.filtered),M.add.message(f.maxSelections)),!0):(M.verbose("No longer at maximum selection count"),M.remove.message(),M.remove.filteredItem(),M.is.searchSelection()&&M.filterItems(),!1))},disabled:function(){S.attr("tabindex",M.is.disabled()?-1:0)}},restore:{defaults:function(e){M.clear(e),M.restore.defaultText(),M.restore.defaultValue()},defaultText:function(){var e=M.get.defaultText();e===M.get.placeholderText?(M.debug("Restoring default placeholder text",e),M.set.placeholderText(e)):(M.debug("Restoring default text",e),M.set.text(e))},placeholderText:function(){M.set.placeholderText()},defaultValue:function(){var e=M.get.defaultValue();void 0!==e&&(M.debug("Restoring default value",e),""!==e?(M.set.value(e),M.set.selected()):(M.remove.activeItem(),M.remove.selectedItem()))},labels:function(){m.allowAdditions&&(m.useLabels||(M.error(d.labels),m.useLabels=!0),M.debug("Restoring selected values"),M.create.userLabels()),M.check.maxSelections()},selected:function(){M.restore.values(),M.is.multiple()?(M.debug("Restoring previously selected values and labels"),M.restore.labels()):M.debug("Restoring previously selected values")},values:function(){M.set.initialLoad(),m.apiSettings&&m.saveRemoteData&&M.get.remoteValues()?M.restore.remoteValues():M.set.selected();var e=M.get.value();!e||""===e||Array.isArray(e)&&0===e.length?T.addClass(h.noselection):T.removeClass(h.noselection),M.remove.initialLoad()},remoteValues:function(){var e=M.get.remoteValues();M.debug("Recreating selected from session data",e),e&&(M.is.single()?Z.each(e,function(e,t){M.set.text(t)}):Z.each(e,function(e,t){M.add.label(e,t)}))}},read:{remoteData:function(e){if(void 0!==ee.Storage)return void 0!==(e=sessionStorage.getItem(e+i))&&e;M.error(d.noStorage)}},save:{defaults:function(){M.save.defaultText(),M.save.placeholderText(),M.save.defaultValue()},defaultValue:function(){var e=M.get.value();M.verbose("Saving default value as",e),y.data(p.defaultValue,e)},defaultText:function(){var e=M.get.text();M.verbose("Saving default text as",e),y.data(p.defaultText,e)},placeholderText:function(){var e;!1!==m.placeholder&&x.hasClass(h.placeholder)&&(e=M.get.text(),M.verbose("Saving placeholder text as",e),y.data(p.placeholderText,e))},remoteData:function(e,t){void 0===ee.Storage?M.error(d.noStorage):(M.verbose("Saving remote data to session storage",t,e),sessionStorage.setItem(t+i,e))}},clear:function(e){M.is.multiple()&&m.useLabels?M.remove.labels(y.find(b.label),e):(M.remove.activeItem(),M.remove.selectedItem(),M.remove.filteredItem()),M.set.placeholderText(),M.clearValue(e)},clearValue:function(e){M.set.value("",null,null,e)},scrollPage:function(e,t){var t=t||M.get.selectedItem(),n=t.closest(b.menu),i=n.outerHeight(),a=n.scrollTop(),o=D.eq(0).outerHeight(),i=Math.floor(i/o),a="up"===e?a-o*i:a+o*i,o=D.not(b.unselectable),i="up"===e?o.index(t)-i:o.index(t)+i,i=("up"===e?0<=i:i").addClass(h.label).attr("data-"+p.value,o).html(F.label(o,t,m.preserveHTML,m.className)),i=m.onLabelCreate.call(i,o,t),M.has.label(e)?M.debug("User selection already exists, skipping",o):(m.label.variation&&i.addClass(m.label.variation),!0===n&&m.label.transition?(M.debug("Animating in label",i),i.addClass(h.hidden).insertBefore(a).transition({animation:m.label.transition,debug:m.debug,verbose:m.verbose,silent:m.silent,duration:m.label.duration})):(M.debug("Adding selection label",i),i.insertBefore(a)))},message:function(e){var t=k.children(b.message),e=m.templates.message(M.add.variables(e));0").html(e).addClass(h.message).appendTo(k)},optionValue:function(e){var t=M.escape.value(e);0").prop("value",t).addClass(h.addition).text(e).appendTo(T),M.verbose("Adding user addition as an
+ - - \ No newline at end of file + diff --git a/metadata-management.html b/metadata-management.html index c3b8c22..7142bad 100644 --- a/metadata-management.html +++ b/metadata-management.html @@ -20,6 +20,7 @@ + @@ -42,6 +43,8 @@ + + @@ -119,7 +122,7 @@

-
+
@@ -189,9 +192,8 @@

-
+ @@ -246,6 +248,12 @@

+
+ +
+ +
+
@@ -259,12 +267,9 @@

- - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ +
+ {app-title} +
{app-subtitle}
+
+

+ +
+
+
+
+
+ + + + + diff --git a/repo-landing-page.html b/repo-landing-page.html new file mode 100644 index 0000000..febf105 --- /dev/null +++ b/repo-landing-page.html @@ -0,0 +1,239 @@ + + + + + + BaseRepo UI + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ +
+ {app-title} +
{app-subtitle}
+
+

+ +
+
+
+
+
+ + + + + diff --git a/repo-management.html b/repo-management.html index aa2a194..0e72f3d 100644 --- a/repo-management.html +++ b/repo-management.html @@ -39,6 +39,8 @@ + + @@ -110,7 +112,7 @@

-
+
@@ -178,7 +180,7 @@

-
+ @@ -270,24 +272,25 @@

createDataResource } from './js/base-repo-utils.js'; + import {keycloak, showServiceUrl} from './settings/general.settings.js'; + import {ajaxBaseUrl, tags, searchEnabled, appDescription} from './settings/base-repo.settings.js'; - import {ajaxBaseUrl, keycloak, tags, showServiceUrl, searchEnabled, appDescription} from './js/base-repo.settings.js'; - - fillTags(tags); - - if (!showServiceUrl) { - $('#service-url-input').empty(); - } + applyConfig(keycloak, showServiceUrl,appDescription, config, addMessage, (login, username) => { + if(login){ + $("#logged_in_as").text("Logged in as " + username); + $("#editor-create-button").removeAttr("disabled"); + }else{ + $("#logged_in_as").text("Not logged in."); + $("#editor-create-button").attr("disabled", "disabled"); + } + reloadTable(); + }); if (!searchEnabled) { $('#search-tab').empty(); } - $('#app-logo').attr("src", appDescription["app-logo"]); - let header = appDescription["app-title"] + - '
' + appDescription["app-subtitle"] + '
'; - - $('#app-title').html(header); + fillTags(tags); /* Fill options with model definitions for metadata record. */ let options = { @@ -305,61 +308,6 @@

* and sets the token which is used for authenticated remote operations. * @param {boolean} login TRUE in case of a login, FALSE for logout operation. */ - function userLoggedIn(login) { - if (login) { - $("#login_icon").attr("class", "sign-out icon") - $("#login_button_text").text("Logout"); - addMessage(0, 'User ' + keycloak.idTokenParsed.preferred_username + ' logged in.'); - localStorage.setItem("userLoggedIn", true); - $("#logged_in_as").text("Logged in as " + keycloak.idTokenParsed.preferred_username); - $("#editor-create-button").removeAttr("disabled"); - config.token = keycloak.token; - } else { - $("#login_icon").attr("class", "sign-in icon") - $("#login_button_text").text("Login"); - localStorage.removeItem("userLoggedIn", true); - $("#logged_in_as").text("Not logged in."); - $("#editor-create-button").attr("disabled", "disabled"); - config.token = null; - } - reloadTable(); - } - - if (typeof keycloak != typeof undefined) { - keycloak.onAuthSuccess = function () { - userLoggedIn(true); - }; - keycloak.onAuthLogout = function () { - userLoggedIn(false); - }; - - keycloak.onTokenExpired = () => { - addMessage(0, 'Keycloak token expired. Trying to refresh.'); - keycloak.updateToken(30).success(() => { - addMessage(0, 'Successfully got a new token.'); - config.token = keycloak.token; - }).catch(() => { - addMessage(1, "Failed to refresh keycloak token."); - config.token = null; - userLoggedIn(false); - }); - }; - - keycloak.init({ - responseMode: 'fragment', - }); - - $("#login_button").click(() => { - if ($("#login_button_text").text() === "Login") { - keycloak.login(); - } else { - keycloak.logout(); - } - }); - } else { - $("#logged_in_as").attr("style", "display:none"); - $("#login_button").attr("style", "display:none"); - } /**Apply the provided tag via PATCH operation to the content information entry/entries selected in the table. * @param {String} tag The tag to add. @@ -376,7 +324,6 @@

addMessage(1, error); }); } - } /**Fill in the list of tags. @@ -399,8 +346,6 @@

mainMethod(options); function mainMethod(options) { - - $('.menu .item').tab(); ajaxURL = ajaxBaseUrl + "dataresources/"; @@ -411,7 +356,7 @@

tableDefinitionResource.ajaxError = function (cell, value) { addMessage(1, "Failed to access backend service at " + ajaxURL); }; - tableDefinitionContent.ajaxURL = ajaxURL + "?page=0&size=100"; + tableDefinitionContent.ajaxURL = ajaxURL ; tableDefinitionContent.ajaxError = function (cell, value) { addMessage(1, "Failed to access backend service at " + ajaxURL); }; @@ -733,6 +678,11 @@

reloadTable(); }); + if (localStorage.getItem("userLoggedIn")) { + localStorage.removeItem("userLoggedIn") + $("#login_button").click(); + } + if (typeof keycloak != typeof undefined && config.token == null) { $("#editor-create-button").attr("disabled", "disabled"); } @@ -752,7 +702,7 @@

"Authorization": "Bearer " + config.token } }; - table.setData(ajaxURL + "?page=0&size=50", {}, ajaxConfig); + table.setData(ajaxURL, {}, ajaxConfig); } else { let ajaxConfig = { method: "GET", @@ -760,7 +710,7 @@

"Accept": 'application/tabulator+json; charset=utf-8', } }; - table.setData(ajaxURL + "?page=0&size=50", {}, ajaxConfig); + table.setData(ajaxURL, {}, ajaxConfig); } } } diff --git a/repo-management_new.html b/repo-management_new.html new file mode 100644 index 0000000..60bb362 --- /dev/null +++ b/repo-management_new.html @@ -0,0 +1,144 @@ + + + + + + BaseRepo UI + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + +
+

+ + + + + diff --git a/schema-management.html b/schema-management.html index a681781..a27a3b6 100644 --- a/schema-management.html +++ b/schema-management.html @@ -15,6 +15,7 @@ integrity="sha512-qTXRIMyZIFb8iQcfjXWCO8+M5Tbc38Qi5WzdPOYZHIlZpzBHG3L3by84BBBOiRGiEb7KKtAOAs5qYdUiZiQNNQ==" crossorigin="anonymous" referrerpolicy="no-referrer"> + @@ -23,7 +24,6 @@ - + + @@ -111,7 +113,7 @@

-
+
@@ -181,7 +183,7 @@

-
+ @@ -238,6 +240,12 @@

+
+ +
+ +
+
@@ -262,79 +270,24 @@

updateSchemaRecord, createSchemaRecord } from './js/metastore-utils.js'; - import {ajaxBaseUrl, keycloak, showServiceUrl, searchEnabled, appDescription} from './js/metastore.settings.js'; - - function userLoggedIn(login) { - if (login) { - $("#login_icon").attr("class", "sign-out icon") - $("#login_button_text").text("Logout"); - addMessage(0, 'User ' + keycloak.idTokenParsed.preferred_username + ' logged in.'); - localStorage.setItem("userLoggedIn", true); - $("#logged_in_as").text("Logged in as " + keycloak.idTokenParsed.preferred_username); + import {keycloak, showServiceUrl} from './settings/general.settings.js'; + import {ajaxBaseUrl, searchEnabled, appDescription} from './settings/metastore.settings.js'; + + applyConfig(keycloak, showServiceUrl,appDescription, config, addMessage, (login, username) => { + if(login){ + $("#logged_in_as").text("Logged in as " + username); $("#editor-create-button").removeAttr("disabled"); - config.token = keycloak.token; - } else { - $("#login_icon").attr("class", "sign-in icon") - $("#login_button_text").text("Login"); - localStorage.removeItem("userLoggedIn", true); + }else{ $("#logged_in_as").text("Not logged in."); $("#editor-create-button").attr("disabled", "disabled"); - config.token = null; } reloadTable(); - } - - if (!showServiceUrl) { - $('#service-url-input').empty(); - } + }); if (!searchEnabled) { $('#search-tab').empty(); } - $('#app-logo').attr("src", appDescription["app-logo"]); - let header = appDescription["app-title"] + - '
' + appDescription["app-subtitle"] + '
'; - - $('#app-title').html(header); - - - //enable keycloak if defined - if (typeof keycloak != typeof undefined) { - keycloak.onAuthSuccess = function () { - userLoggedIn(true); - }; - keycloak.onAuthLogout = function () { - userLoggedIn(false); - }; - - keycloak.onTokenExpired = () => { - addMessage(0, 'Keycloak token expired. Trying to refresh.'); - keycloak.updateToken(30).success(() => { - addMessage(0, 'Successfully got a new token.'); - config.token = keycloak.token; - }).catch(() => { - addMessage(1, "Failed to refresh keycloak token."); - config.token = null; - }); - }; - - - keycloak.init({ - responseMode: 'fragment', - }); - - $("#login_button").click(() => { - if ($("#login_button_text").text() === "Login") { - keycloak.login(); - } else { - keycloak.logout(); - } - }); - } else { - $("#logged_in_as").attr("style", "display:none"); - $("#login_button").attr("style", "display:none"); - } let table = null; let ajaxURL = null; let currentAjaxBaseUrl = null; @@ -493,6 +446,18 @@

return result; } + /** Download the current schema from the monaco editor. + */ + function downloadDocumentFromMonaco() { + if (rightModel != null) { + if (rightModel._languageId === "json") { + download(rightModel.getValue(), "schema.json", "application/json"); + } else { + download(rightModel.getValue(), "schema.xsd", "application/xml"); + } + } + } + function doUpdateSchema() { let schema = getDocumentFromMonaco(); if (!validatedRecord) { @@ -808,6 +773,11 @@

}) }); + //document download handler + $("#schema-download-button").click(() => { + downloadDocumentFromMonaco(); + }); + if (localStorage.getItem("userLoggedIn")) { localStorage.removeItem("userLoggedIn") $("#login_button").click(); diff --git a/settings/base-repo.settings.js b/settings/base-repo.settings.js new file mode 100644 index 0000000..7e8cfdc --- /dev/null +++ b/settings/base-repo.settings.js @@ -0,0 +1,32 @@ +//The backend service URL for the base-repo instance used by the frontend. +export const ajaxBaseUrl = "http://localhost:8081/api/v1/"; +//export const ajaxBaseUrl = "https://demo.datamanager.kit.edu:8443/base-repo/api/v1/"; + +//Supported tags which can be used to tag single files in base-repo. Be aware that while the actual tag (name) will be +//stored in base-repo, the color is just used on the frontend-level and might be different/not available when accessing +//the same base-repo instance with a frontend configured differently. +export const tags = [ + {"name":"rawData", "color":"red"}, + {"name":"analyzedData", "color":"green"}, + {"name":"document", "color":"blue"}, + {"name":"code", "color":"orange"}, + {"name":"deprecated", "color":"black"} +]; + +//The app description used to customize the frontend, e.g., for a specific project with a custom title and subtitle. +export const appDescription = { + "app-logo":"./images/disks.jpg", + "app-title":"Base-Repo Demonstrator", + "app-subtitle":"Data Resource Management" +}; + +//Enable/disable the Elastic search functionality. The availability of the search depends on the configuration of +//the configured base-repo instance. If Elastic search is not configured for the underlying base-repo, it should +//also be disabled here. +export const searchEnabled = true; + + + + + + diff --git a/settings/dashboard.settings.js b/settings/dashboard.settings.js new file mode 100644 index 0000000..86e6138 --- /dev/null +++ b/settings/dashboard.settings.js @@ -0,0 +1,36 @@ +//Show/hide the MetaStore element on the dashboard. +let showMetaStore=true; +//Show/hide the base-repo element on the dashboard. +let showBaseRepo=true; +//Show/hide the MappingService element on the dashboard. +let showMappingService=true; +//Show/hide the TypedPIDMaker element on the dashboard. +let showTypedPIDMaker=false; +//Show/hide the FDOBuilder element on the dashboard. +let showFDOBuilder=true +//Show/hide the FDO Maker element on the dashboard. +let showFDOMaker=true; + +//The app description used to customize the frontend, e.g., for a specific project with a custom title and subtitle. +let appDescription = { + "app-logo":"./images/Logo_KIT.png", + "app-title":"Frontend Collection Dashboard", + "app-subtitle":"A collection of web frontends", + "app-description":"

This collection of generic web frontends provides access to RESTful services of the KIT Data Manager\n" + + " service portfolio.\n" + + " The idea is to have graphical user interfaces available such that certain base services can be\n" + + " directly\n" + + " used for performing\n" + + " basic tasks without the need of integrating them in your own frontends before being able to use them\n" + + " the\n" + + " first time.\n" + + "

\n" + + "\n" + + "

\n" + + " However, for some application cases, these generic web frontends might even be sufficient for direct\n" + + " interaction with our\n" + + " services, and they might be offered to the end-user. To allow that, all frontends of this collection\n" + + " offer a certain degree\n" + + " of customization to slightly adapt their presentation to specific needs.\n" + + "

" +}; diff --git a/settings/elastic-search-base-repo.settings.js b/settings/elastic-search-base-repo.settings.js new file mode 100644 index 0000000..484f9e8 --- /dev/null +++ b/settings/elastic-search-base-repo.settings.js @@ -0,0 +1,16 @@ +//The backend service URL for the base-repo instance used by the frontend. +export const ajaxBaseUrl = "http://localhost:8081/api/v1/"; +//export let ajaxBaseUrl = "https://demo.datamanager.kit.edu:8443/base-repo/api/v1/"; + +//The app description used to customize the frontend, e.g., for a specific project with a custom title and subtitle. +export const appDescription = { + "app-logo":"./images/search.jpg", + "app-title":"Repository Search Demonstrator", + "app-subtitle":"Data Resource Search" +}; + +//The max. number of elements obtained in a single search request. +export const page_size = 5; + + + diff --git a/settings/elastic-search-fdo.settings.js b/settings/elastic-search-fdo.settings.js new file mode 100644 index 0000000..f1756b1 --- /dev/null +++ b/settings/elastic-search-fdo.settings.js @@ -0,0 +1,12 @@ +//The backend service URL for the Typed PID Maker instance used by the frontend. +export const ajaxBaseUrl = "http://localhost:8090/api/v1/"; +//export let ajaxBaseUrl = "https://demo.datamanager.kit.edu:8443/typed-pid-maker/api/v1/"; + +//The app description used to customize the frontend, e.g., for a specific project with a custom title and subtitle. +export const appDescription = { + "app-logo":"./images/search.jpg", + "app-title":"FDO Search Demonstrator", + "app-subtitle":"FAIR Digital Object Search" +}; + + diff --git a/settings/elastic-search-metastore.settings.js b/settings/elastic-search-metastore.settings.js new file mode 100644 index 0000000..43b0ba5 --- /dev/null +++ b/settings/elastic-search-metastore.settings.js @@ -0,0 +1,15 @@ +//The backend service URL for the MetaStore instance used by the frontend. +export const ajaxBaseUrl = "http://metastore.docker:8040/metastore/api/v1/"; +//export const ajaxBaseUrl = "https://demo.datamanager.kit.edu:8443/metastore/api/v1/"; + +//The app description used to customize the frontend, e.g., for a specific project with a custom title and subtitle. +export const appDescription = { + "app-logo":"./images/search.jpg", + "app-title":"MetaStore Search Demonstrator", + "app-subtitle":"Metadata Search" +}; + + + + + diff --git a/settings/fdo-builder.settings.js b/settings/fdo-builder.settings.js new file mode 100644 index 0000000..d73ef98 --- /dev/null +++ b/settings/fdo-builder.settings.js @@ -0,0 +1,10 @@ +//The backend service URL for the Typed PID Maker instance used by the frontend. +export let ajaxBaseUrl = "http://localhost:8090"; +//export let ajaxBaseUrl = "https://demo.datamanager.kit.edu:8090/"; + +//The app description used to customize the frontend, e.g., for a specific project with a custom title and subtitle. +export const appDescription = { + "app-logo":"./images/typed-pid-maker-logo.svg", + "app-title":"FAIR-DO Builder", + "app-subtitle":"Interactively create your FAIR-DOs." +}; diff --git a/settings/general.settings.js b/settings/general.settings.js new file mode 100644 index 0000000..b4d4fdb --- /dev/null +++ b/settings/general.settings.js @@ -0,0 +1,13 @@ +//Keycloak configuration to use Keycloak as identity provider for single sign-on. +export const keycloak = undefined; +/*= Keycloak({ + url: 'https://gateway.datamanager.kit.edu:8443/', + realm: 'dem_testing', + clientId: 'kitdm-services' +});*/ + +//Show the input for the backend service URL. This property should only be enabled for debugging. +export const showServiceUrl = false; + +//The max. number of elements obtained in a single search request. +export const page_size = 5; diff --git a/settings/mapping-service.settings.js b/settings/mapping-service.settings.js new file mode 100644 index 0000000..64fee41 --- /dev/null +++ b/settings/mapping-service.settings.js @@ -0,0 +1,9 @@ +//The backend service URL for the MappingService instance used by the frontend. +export const ajaxBaseUrl = "http://localhost:8090"; + +//The app description used to customize the frontend, e.g., for a specific project with a custom title and subtitle. +export const appDescription = { + "app-logo": "./images/mapping_service.jpg", + "app-title": "Mapping Service UI", + "app-subtitle": "Extract metadata and map it to json." +}; diff --git a/settings/metastore.settings.js b/settings/metastore.settings.js new file mode 100644 index 0000000..b3fffcb --- /dev/null +++ b/settings/metastore.settings.js @@ -0,0 +1,18 @@ +//The backend service URL for the MetaStore instance used by the frontend. +export const ajaxBaseUrl = "http://metastore.docker:8040/metastore/api/v1/"; +//export const ajaxBaseUrl = "https://demo.datamanager.kit.edu:8443/metastore/api/v1/"; + +//The app description used to customize the frontend, e.g., for a specific project with a custom title and subtitle. +export const appDescription = { + "app-logo":"./images/metadata.jpg", + "app-title":"MetaStore Frontend", + "app-subtitle":"Schema and Metadata Management" +}; + +//Enable/disable the Elastic search functionality. The availability of the search depends on the configuration of +//the configured MetaStore instance. If Elastic search is not configured for the underlying MetaStore, it should +//also be disabled here. +export const searchEnabled = true; + + + diff --git a/settings/typed-pid-maker.settings.js b/settings/typed-pid-maker.settings.js new file mode 100644 index 0000000..630a0b5 --- /dev/null +++ b/settings/typed-pid-maker.settings.js @@ -0,0 +1,10 @@ +//The backend service URL for the Typed PID Maker instance used by the frontend. +export const ajaxBaseUrl = "http://localhost:8090"; +//export const ajaxBaseUrl = "https://demo.datamanager.kit.edu:8090/"; + +//The app description used to customize the frontend, e.g., for a specific project with a custom title and subtitle. +export const appDescription = { + "app-logo":"./images/typed-pid-maker-logo.svg", + "app-title":"Typed PID Maker UI", + "app-subtitle":"Validate, create, and maintain PIDs." +}; diff --git a/typed-pid-maker-ui.html b/typed-pid-maker-ui.html index ca361fe..83ccd41 100644 --- a/typed-pid-maker-ui.html +++ b/typed-pid-maker-ui.html @@ -14,6 +14,7 @@ +

@@ -55,18 +56,20 @@

+

+ Use this field to connect to an instance of the Typed PID Maker. +

-

- Use this field to connect to an instance of the Typed PID Maker. -

- - + + - \ No newline at end of file +