master #14

Merged
m.lettere merged 15 commits from master into prod 2024-06-04 18:54:59 +02:00
6 changed files with 273 additions and 34 deletions

View File

@ -13,6 +13,7 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<script src="https://cdn.cloud-dev.d4science.org/common/js/bss-min-1.2.6.js"></script>
<script src="../storage/d4s-storage.js"></script>
<script src="https://cdn.jsdelivr.net/npm/ol@v9.2.4/dist/ol.js"></script>
</head>
<body class="m-4">

View File

@ -58,6 +58,7 @@ class CCPExecutionForm extends HTMLElement{
<link rel="canonical" href="https://getbootstrap.com/docs/5.0/components/modal/">
<link rel="stylesheet" href="https://cdn.dev.d4science.org/ccp/css/common.css"></link>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"/>
<style>
.ccp-execution-form{
position: relative;
@ -85,7 +86,11 @@ class CCPExecutionForm extends HTMLElement{
</div>
<div class="card-body">
<div class="col form-check">
<input class="form-check-input" type="checkbox" name="auto-archive-outputs" alt="Automatically archive outputs to workspace" title="Automatically archive outputs to workspace" checked="checked">
<input class="form-check-input" type="checkbox" name="auto-archive" alt="Automatically archive the whole execution with metadata to workspace folder" title="Automatically archive the whole execution with metadata to workspace folder">
<label class="form-check-label">Automatically archive whole execution to workspace</label>
</div>
<div class="col form-check">
<input class="form-check-input" type="checkbox" name="auto-archive-outputs" alt="Automatically archive only the outputs to workspace" title="Automatically archive only the outputs to workspace" checked="checked">
<label class="form-check-label">Automatically archive outputs to workspace</label>
</div>
</div>
@ -115,6 +120,8 @@ class CCPExecutionForm extends HTMLElement{
<option value="text/julia" data-ext="jl" title="Generate Julia 1.9.0 code (HTTP 1.10.0, JSON3 1.13.2)">Julia 1.9.0</option>
<option value="application/x-sh+curl" data-ext="sh" title="Generate Bash script (curl)">Bash (curl)</option>
<option value="application/x-sh+wget" data-ext="sh" title="Generate Bash script (wget)">Bash (wget)</option>
<option value="application/json+galaxy" data-ext="json" title="Generate JSON request for Galaxy">Galaxy CCP request (preview)</option>
<option value="application/xml+galaxy" data-ext="xml" title="Generate installable Galaxy tool">Galaxy tool (preview)</option>
</select>
<button name="codegen" title="Generate code" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small">
<svg viewBox="0 96 960 960">
@ -251,11 +258,12 @@ class CCPExecutionForm extends HTMLElement{
if(o.enabled) request.outputs[o.name] = { transmissionMode : "value" };
})
const autoarchiveoption = this.#rootdoc.querySelector("input[name='auto-archive-outputs']")
if (autoarchiveoption.checked){
request.subscribers = [
{ successUri : "http://registry:8080/executions/archive-to-folder" }
]
const autoarchiveoption = this.#rootdoc.querySelector("input[name='auto-archive']")
const autoarchiveoutputsoption = this.#rootdoc.querySelector("input[name='auto-archive-outputs']")
if (autoarchiveoption.checked || autoarchiveoutputoption.checked){
request.subscribers = []
if(autoarchiveoption.checked) request.subscribers.push({ successUri : "http://registry:8080/executions/archive-to-folder" })
if(autoarchiveoutputsoption.checked) request.subscribers.push({ successUri : "http://registry:8080/executions/outputs/archive-to-folder" })
}
return request

View File

@ -78,18 +78,18 @@ class CCPExecutionHistory extends HTMLElement {
<span name="status" class="ml-1 badge"></span>
<div class="d-flex float-right" style="gap: 3px 5px; max-width: 40%; min-width:60px; flex-wrap:wrap;">
${ this.#archive ? `
<button data-index="0" name="archive" title="Archive to workspace" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small">
<button data-index="0" name="archive" title="Archive whole execution to workspace folder" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small">
<svg viewBox="0 96 960 960"><path d="M140 796h680V516H140v280Zm540.118-90Q701 706 715.5 691.382q14.5-14.617 14.5-35.5Q730 635 715.382 620.5q-14.617-14.5-35.5-14.5Q659 606 644.5 620.618q-14.5 14.617-14.5 35.5Q630 677 644.618 691.5q14.617 14.5 35.5 14.5ZM880 456h-85L695 356H265L165 456H80l142-142q8-8 19.278-13 11.278-5 23.722-5h430q12.444 0 23.722 5T738 314l142 142ZM140 856q-24.75 0-42.375-17.625T80 796V456h800v340q0 24.75-17.625 42.375T820 856H140Z"/></svg>
</button>`
: ``
}
<button data-index="0" name="provo" title="Export to Prov-o document" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small">
<!--button data-index="0" name="provo" title="Export to Prov-o document" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small">
<svg viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M13 3H6v18h4v-6h3c3.31 0 6-2.69 6-6s-2.69-6-6-6zm.2 8H10V7h3.2c1.1 0 2 .9 2 2s-.9 2-2 2z"/></svg>
</button>
</button-->
<!-- button data-index="0" name="zip" title="Download as zip archive" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small">
<svg viewBox="0 0 48 48"><path d="M7 40q-1.15 0-2.075-.925Q4 38.15 4 37V11q0-1.15.925-2.075Q5.85 8 7 8h14l3 3h17q1.15 0 2.075.925Q44 12.85 44 14v23q0 1.15-.925 2.075Q42.15 40 41 40Zm25-3h9V14h-9v4.6h4.6v4.6H32v4.6h4.6v4.6H32ZM7 37h20.4v-4.6H32v-4.6h-4.6v-4.6H32v-4.6h-4.6V14h-4.65l-3-3H7v26Zm0-23v-3 26-23Z"/></svg>
</button-->
<button data-index="0" name="archivefolder" title="Archive outputs to workspace" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small">
<button data-index="0" name="archiveoutputs" title="Archive only outputs to workspace folder" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small">
<svg viewBox="0 0 48 48"><path d="M7 40q-1.15 0-2.075-.925Q4 38.15 4 37V11q0-1.15.925-2.075Q5.85 8 7 8h14l3 3h17q1.15 0 2.075.925Q44 12.85 44 14v23q0 1.15-.925 2.075Q42.15 40 41 40Zm25-3h9V14h-9v4.6h4.6v4.6H32v4.6h4.6v4.6H32ZM7 37h20.4v-4.6H32v-4.6h-4.6v-4.6H32v-4.6h-4.6V14h-4.65l-3-3H7v26Zm0-23v-3 26-23Z"/></svg>
</button>
<button data-index="0" name="reexecute1" title="Re-submit this execution" class="btn btn-info ccp-toolbar-button ccp-toolbar-button-small">
@ -208,7 +208,7 @@ class CCPExecutionHistory extends HTMLElement {
const link = ev.target.parentElement.querySelector("input").value
if(link){
if(confirm("Please confirm importing of execution from link?")){
this.fromArchive(link)
this.fromArchiveFolder(link)
}
}
})
@ -425,30 +425,30 @@ class CCPExecutionHistory extends HTMLElement {
}).catch(err=>{ alert(err)})
}
toArchive(id){
this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}/archive`, { method: "POST" })
.then(reply =>{
if (!reply.ok) {
throw "Unable to archive"
}
}).catch(err=>{ alert(err)})
}
toArchiveFolder(id){
this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}/archive-to-folder`, { method: "POST" })
.then(reply =>{
if (!reply.ok) {
throw "Unable to archive"
throw "Unable to archive execution to folder"
}
}).catch(err=>{ alert(err)})
}
fromArchive(url){
toOutputsArchiveFolder(id){
this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}/outputs/archive-to-folder`, { method: "POST" })
.then(reply =>{
if (!reply.ok) {
throw "Unable to archive outputs to folder"
}
}).catch(err=>{ alert(err)})
}
fromArchiveFolder(url){
if(url){
this.#boot.secureFetch(`${this.#serviceurl}/executions/archive?url=${url}`)
.then(reply =>{
if (!reply.ok) {
throw "Unable to fetch from archive"
throw "Unable to fetch from archive folder"
}
return reply.text()
}).then(data=>{
@ -555,17 +555,17 @@ class CCPExecutionHistory extends HTMLElement {
}
}
if(ev.target.getAttribute("name") === "archive"){
if(confirm(" Please confirm archiving of execution to workspace?")){
const id = ev.currentTarget.getAttribute("data-index")
this.toArchive(id)
}
}
if(ev.target.getAttribute("name") === "archivefolder"){
if(confirm(" Please confirm archiving of execution outputs to workspace?")){
if(confirm(" Please confirm archiving of execution to workspace folder?")){
const id = ev.currentTarget.getAttribute("data-index")
this.toArchiveFolder(id)
}
}
if(ev.target.getAttribute("name") === "archiveoutputs"){
if(confirm(" Please confirm archiving of execution outputs to workspace folder?")){
const id = ev.currentTarget.getAttribute("data-index")
this.toOutputsArchiveFolder(id)
}
}
},
recurse : [
{

View File

@ -43,6 +43,11 @@ class CCPInputWidgetController extends HTMLElement {
this.innerHTML += `<d4s-ccp-input-remotefile readonly="${this.#data.schema.readOnly}" default="${this.#data.schema.default}" maxOccurs="${this.#data.maxOccurs}" minOccurs="${this.#data.minOccurs}" name="${this.#data.id}" description="${btoa(this.#data.description)}" title="${this.#data.title}"></d4s-ccp-input-remotefile>`
this.querySelector("d4s-ccp-input-remotefile").default = this.#data.schema.default
} else if (this.isGeo()) {
this.innerHTML += `<d4s-ccp-input-geo readonly="${this.#data.schema.readOnly}" default="${this.#data.schema.default}" maxOccurs="${this.#data.maxOccurs}" minOccurs="${this.#data.minOccurs}" name="${this.#data.id}" description="${btoa(this.#data.description)}" title="${this.#data.title}"></d4s-ccp-input-remotefile>`
this.querySelector("d4s-ccp-input-geo").default = this.#data.schema.default
} else if (this.isSecret()) {
this.innerHTML += `<d4s-ccp-input-secret readonly="${this.#data.schema.readOnly}" default="${this.#data.schema.default}" maxOccurs="${this.#data.maxOccurs}" minOccurs="${this.#data.minOccurs }" name="${this.#data.id}" description="${btoa(this.#data.description)}" title="${this.#data.title}"></d4s-ccp-input-secret>`
@ -130,6 +135,13 @@ class CCPInputWidgetController extends HTMLElement {
(this.#data.schema.format.toLowerCase() === "remotefile")
}
isGeo() {
return (this.#data.schema.type === "string") &&
("format" in this.#data.schema) &&
(this.#data.schema.format != null) &&
(this.#data.schema.format.toLowerCase() === "geo")
}
isDateTime() {
return (this.#data.schema.type === "string") &&
("format" in this.#data.schema) &&
@ -600,11 +612,11 @@ class CCPRemoteFileInputWidgetController extends CCPBaseInputWidgetController {
<div class="mb-1" style="border-bottom: solid 1px gray;">
<div class="d-flex justify-content-between m-0">
<h5 class="text-secondary m-0">Access your workspace</h5>
<span class="btn text-danger p-0" style="font-weight:bold" name="closebtn">x</span>
<span class="material-icons btn text-danger p-0" style="font-weight:bold" name="closebtn">close</span>
</div>
<small class="text-muted m-0">Select an item or drag and drop it to a proper input</small>
</div>
<div style="min-width:350px; max-width:500px;overflow:auto;etxt-wrap:nowrap;text-overflow: ellipsis;min-height:5rem; max-height:10rem;">
<div style="min-width:500px; max-width:640px;overflow:auto;etxt-wrap:nowrap;text-overflow: ellipsis;min-height:5rem; max-height:10rem;">
<d4s-storage-tree
base-url="${addresses[iss]}"
file-download-enabled="true"
@ -771,3 +783,220 @@ class CCPSecretInputWidgetController extends CCPBaseInputWidgetController {
}
}
window.customElements.define('d4s-ccp-input-secret', CCPSecretInputWidgetController);
class CCPGeoInputWidgetController extends CCPBaseInputWidgetController {
#format_dialog = null;
#target = null
#draw = null;
#raster = null
#source = null
#map = null;
#vector = null
#index = null;
#widget = null
constructor() {
super()
}
addToolContent() {
this.rootdoc.querySelector("div[name=tools]").innerHTML += `
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<svg name="trigger" style="width:24;height:24;fill:#007bff; cursor:pointer" viewBox="0 -960 960 960">
<path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm-40-82v-78q-33 0-56.5-23.5T360-320v-40L168-552q-3 18-5.5 36t-2.5 36q0 121 79.5 212T440-162Zm276-102q20-22 36-47.5t26.5-53q10.5-27.5 16-56.5t5.5-59q0-98-54.5-179T600-776v16q0 33-23.5 56.5T520-680h-80v80q0 17-11.5 28.5T400-560h-80v80h240q17 0 28.5 11.5T600-440v120h40q26 0 47 15.5t29 40.5Z"/>
</svg>
<div name="map" class="d-none position-absolute shadow border border-primary bg-light p-2" style="right:0;z-index:1000; line-height:1.5rem;overflow:hidden;padding:5px;">
<div name="maptoolbar" class="mb-1" style="border-bottom: solid 1px gray;">
<div class="d-flex justify-content-between m-0">
<div>
<span title="Draw a rectangular box" name="Box" class="btn text-primary p-0 material-icons">crop_16_9</span>
<span title="Draw a circle" name="Circle" class="btn text-primary p-0 material-icons">radio_button_unchecked</span>
<span title="Select a Point" name="Point" class="btn text-primary p-0 material-icons">my_location</span>
<span title="Draw a Polygon" name="Polygon" class="btn text-primary p-0 material-icons" style="color:transparent">pentagon</span>
<span title="Delete all" name="deletebtn" class="btn text-danger p-0 material-icons">delete</span>
</div>
<div>
<span title="Set geometry" class="btn btn-primary" style="font-weight:bold" name="selectbtn">Export</span>
<span title="Close" class="material-icons btn text-danger p-0" style="font-weight:bold" name="closebtn">close</span>
</div>
</div>
<small class="text-muted m-0">Draw and export geometry</small>
</div>
<div style="width:500px;height:500px;position:relative;">
<style>
div[name=internalmap]{
width: 100%;
height:100%;
position: relative;
}
</style>
<div name="internalmap"></div>
</div>
</div>
<div class="modal fade" style="background-color:rgba(0,0,0,0.3)" name="format" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content shadow-lg border-primary">
<div class="modal-body">
Choose export format.
</div>
<div class="modal-footer">
<span title="Export WKT" name="wkt" class="btn btn-info">WKT</span>
<span title="Export to GML" name="gml" class="btn btn-primary">GML</span>
<span title="Export to GeoJSON" name="geojson" class="btn btn-secondary">GeoJSON</span>
<span title="Cancel" name="cancel" class="btn btn-danger">Cancel</span>
</div>
</div>
</div>
</div>
`
// Init map
this.init()
this.rootdoc.querySelector("div[name=maptoolbar]").addEventListener("click", ev=>{
const type = ev.target.getAttribute("name")
if(type === "closebtn"){
this.#widget.classList.add("d-none")
ev.preventDefault()
ev.stopPropagation()
}if(type === "selectbtn"){
this.#widget.classList.add("d-none")
this.#format_dialog.style.display = "block"
this.#format_dialog.classList.add("show")
this.#index = this.value.length - 1
const alltargets = this.#target = this.rootdoc.querySelectorAll("textarea")
for(let i=0; i < alltargets.length;i++){
if(!alltargets.item(i).value[i]){
this.#index = i
break;
}
}
this.#target = this.rootdoc.querySelectorAll("textarea").item(this.#index)
ev.preventDefault()
ev.stopPropagation()
}else if(["Circle", "Point", "Polygon", "Box"].indexOf(type) !== -1){
ev.stopPropagation();
ev.preventDefault();
this.undoAllDrawings()
if(this.#draw != null && this.#draw.type === type) return;
if(this.#draw != null) this.#map.removeInteraction(this.#draw);
this.#draw = new ol.interaction.Draw({
source: this.#source,
type : type === "Box" ? "Circle" : type,
geometryFunction : type === "Box" ? ol.interaction.Draw.createBox() : null
})
this.#map.addInteraction(this.#draw)
}else if(type === "deletebtn"){
this.undoAllDrawings()
ev.preventDefault()
ev.stopPropagation()
}
})
document.addEventListener("keydown", ev=>{
if (ev.ctrlKey && ev.key === 'z') {
this.undoDrawing()
}else if(ev.key === "Escape"){
this.stopDrawing()
}
})
this.#format_dialog = this.rootdoc.querySelector("div.modal[name=format]")
this.#format_dialog.addEventListener("click", ev => {
ev.stopPropagation()
ev.preventDefault()
const features = this.#source.getFeatures()
if( features.length && this.#index != null && this.#target != null){
const name = ev.target.getAttribute("name")
var format
if(name === "wkt") {
format = new ol.format.WKT()
}else if(name === "geojson"){
format = new ol.format.GeoJSON()
}else if(name === "gml"){
format = new ol.format.GML()
}
const result = features.map(f=>{
const geom = f.getGeometry().getType() === "Circle" ? ol.geom.Polygon.fromCircle(f.getGeometry(), 50) : f.getGeometry()
const tgeom = geom.transform(this.#map.getView().getProjection(), new ol.proj.Projection({code: "EPSG:4326"}))
return format.writeGeometry(tgeom)
}).join("\n")
this.#target.value = this.value[this.#index] = result
const newev = new Event("input", { bubbles : true})
this.dispatchEvent(newev)
}
this.#format_dialog.style.display = "none"
this.#format_dialog.classList.remove("show")
})
this.rootdoc.querySelector("svg[name=trigger]").addEventListener("click", ev => {
this.#widget.classList.toggle("d-none")
ev.preventDefault()
ev.stopPropagation()
})
this.rootdoc.addEventListener("input", ev=>{
if(ev.target.getAttribute("name") === this.name && ev.target.getAttribute("data-index")){
const index = Number(ev.target.getAttribute("data-index"))
this.value[index] = ev.target.value
}
})
}
init(){
this.#widget = this.rootdoc.querySelector("div[name=map]")
this.#draw = null
this.#raster = new ol.layer.Tile({source: new ol.source.OSM()});
this.#source = new ol.source.Vector({wrapX: false});
this.#vector = new ol.layer.Vector({source: this.#source});
this.#map = new ol.Map({
layers: [this.#raster, this.#vector],
target: this.rootdoc.querySelector("div[name=internalmap]"),
view: new ol.View({center: [0, 0], zoom: 4 }),
controls :[]
})
}
undoAllDrawings(){
this.#source.clear()
}
undoDrawing(){
const features = this.#source.getFeatures()
if(features.length){
this.#source.removeFeature(features[features.length-1])
}
}
stopDrawing(){
if(this.#draw){
this.#draw.abortDrawing()
this.#map.removeInteraction(this.#draw);
this.#draw = null
}
}
content() {
if(!this.readonly && !this.querySelector("div[name=map]")) this.addToolContent();
if (this.value.length <= 1) {
return `
<textarea name="${this.name}" data-index="0" class="ccp-input my-2 form-control" placeholder="${this.title}" type="${this.type}" name="${this.name}" rows="5" ${this.readonly ? 'readonly' : ''} ${this.required ? 'required' : ''}>${this.value.length ? this.value[0] : ''}</textarea>
`
}
var out =
this.value.map((c, i) => {
return `
<textarea name="${this.name}" data-index="${i}" class="ccp-input my-2 form-control" placeholder="${this.title}" type="${this.type}" name="${this.name}" rows="5" ${this.readonly ? 'readonly' : ''} ${this.required ? 'required' : ''}>${c}</textarea>
`
}).join("\n")
return out
}
}
window.customElements.define('d4s-ccp-input-geo', CCPGeoInputWidgetController);

View File

@ -127,6 +127,7 @@ class CCPInputWidgetEditorController extends HTMLElement{
<option value="code" ${this.isSelectedFormat('code') ? "selected" : ""}>Code</option>
<option value="file" ${this.isSelectedFormat('file') ? "selected" : ""}>File</option>
<option value="remotefile" ${this.isSelectedFormat('remotefile') ? "selected" : ""}>Workspace file</option>
<option value="geo" ${this.isSelectedFormat('geo') ? "selected" : ""}>Geography</option>
<option value="secret" ${this.isSelectedFormat('secret') ? "selected" : ""}>Secret</option>
<option value="url" ${this.isSelectedFormat('url') ? "selected" : ""}>Url</option>
</select>

View File

@ -890,7 +890,7 @@ class D4SWorkspace {
* @throws the error as string, if occurred
*/
listFolder(folderId, extraHeaders) {
const uri = "/items/" + folderId + "/children";
const uri = "/items/" + folderId + "/children?exclude=hl:accounting";
return this.#callWorkspace("GET", uri, null, null, extraHeaders)
.then(blob => {
return blob.text().then(text => {