Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
jmlord committed Nov 14, 2024
2 parents d384f38 + a977be1 commit f7e450f
Show file tree
Hide file tree
Showing 16 changed files with 1,033 additions and 89 deletions.
252 changes: 231 additions & 21 deletions LICENSE

Large diffs are not rendered by default.

643 changes: 643 additions & 0 deletions docs/ui/BonInABoxScriptService/docs/GetHistory200ResponseInner.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion how_to_contribute.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Specifying an example will autofill that input with the example unless changed b
There are several different types of user inputs that can be specified in the YAML file:

| "Type" attribute in the yaml | IU rendering | Description |
|----|----|----|
|------------------------|------------------------|------------------------|
| boolean | Plain text | true/false |
| float, float\[\] | Plain text | positive and negative numbers with a decimal point |
| int, int\[\] | Plain text | integer |
Expand Down
5 changes: 5 additions & 0 deletions how_to_install.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ When modifying scripts in the `/scripts` folder, servers do not need to be resta

When modifying pipelines in the /pipelines folder, servers do not need to be restarted: - In the pipeline editor, click save, paste the file to your file in the pipeline folder and run it from the "pipeline run" page. - When adding or renaming pipelines, refresh the browser page.

### Deploying without pulling the new server
If the server has already been deployed at least once, and low bandwith does not allow to download the latest version (this can be a few gigabytes), the server can be started with option `./server-up.sh --offline`. Use with caution, since this may create errors if elements of the repository depend on new server features.

## Deploying BON in a Box on a server {#deploying-the-servers-remotely}

Installation steps on a server are the same as installing on a Linux computer, but some additional configurations need to be changed:
Expand All @@ -78,6 +81,8 @@ Installation steps on a server are the same as installing on a Linux computer, b

- In `runner.env`, select the "partial" cache cleaning mode. This will allow for calculated results to be long-lived on the server, as long as they are not run again with a new version of the script. The results in the output folder are always accessible by URL.

- In the case where a server is used for demonstration purpose only, set environment variable `BLOCK_RUNS=true`

- The outputs are precious, while the server machine can be re-generated anytime. Consider mounting an external backed-up drive to the output folder of your BON in a Box installation.

- By default, BON in a Box listens for http. To enable https, we hide it behind a reverse proxy and activate https with certbot. How to do this is out of scope for this readme.
Expand Down
10 changes: 10 additions & 0 deletions script-server/src/main/kotlin/org/geobon/server/plugins/Routing.kt
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,16 @@ fun Application.configureRouting() {
}

post("/{type}/{descriptionPath}/run") {
logger.debug("BLOCK_RUNS: ${System.getenv("BLOCK_RUNS")}")

if (System.getenv("BLOCK_RUNS") == "true") {
call.respond(
HttpStatusCode.ServiceUnavailable, "This server does not allow running pipelines and scripts.\n" +
"It was configured to display results only."
)
return@post
}

val singleScript = call.parameters["type"] == "script"

val inputFileContent = call.receive<String>()
Expand Down
4 changes: 2 additions & 2 deletions ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using vite" />
<meta name="description" content="Create and run EBV and indicator pipelines with the BON in a Box modelling tool." />
<!-- material UI font -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;600;700&display=swap"
/>
<link rel="apple-touch-icon" href="/boninabox.jpg" />
<link rel="apple-touch-icon" href="/favicon.ico" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
Expand Down
5 changes: 0 additions & 5 deletions ui/public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,6 @@
"src": "favicon.ico",
"sizes": "100x100 64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "boninabox.jpg",
"type": "image/jpg",
"sizes": "312x312"
}
],
"start_url": ".",
Expand Down
5 changes: 4 additions & 1 deletion ui/src/components/PipelineEditor/IOListItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@ export function IOListItem({ io, id, valueEdited, setter, className }) {
<td className={className}>
<ControlledTextArea className="label" keepWidth={true}
onBlur={e => valueEdited(e.target.value, "label", io, setter)}
onKeyDown={(e) => { if (e.ctrlKey) valueEdited(e.target.value, "label", io, setter) }}
onInput={preventNewLines}
defaultValue={io.label} />
defaultValue={io.label}
/>

<ControlledTextArea className="description" keepWidth={true}
onBlur={e => valueEdited(e.target.value, "description", io, setter)}
onKeyDown={(e) => { if (e.ctrlKey) valueEdited(e.target.value, "description", io, setter) }}
defaultValue={io.description} />

{io.type &&
Expand Down
90 changes: 69 additions & 21 deletions ui/src/components/PipelineEditor/PipelineEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ export default function PipelineEditor(props) {
setModal(currentModal => currentModal === modalName ? null : currentModal)
}, [setModal])

const hideCurrentModal = useCallback(() => {
setModal(null)
}, [setModal])

// We need this since the functions passed through node data retain their old selectedNodes state.
// Note that the stratagem fails if trying to add edges from many sources at the same time.
const [pendingEdgeParams, addEdgeWithHighlight] = useState(null);
Expand Down Expand Up @@ -554,10 +558,14 @@ export default function PipelineEditor(props) {
prev.nodeId === newOutput.nodeId &&
prev.outputId === newOutput.outputId
);
// The label and description of previous outputs might have been modified, so we keep them as is.
return previousOutput && previousOutput.label
? previousOutput
: newOutput;

if(previousOutput && previousOutput.label) {
// The label and description of previous outputs might have been modified, so we keep them as is.
return previousOutput;
}

newOutput.weight = previousOutputs.length;
return newOutput
})

newPipelineOutputs = newPipelineOutputs.sort((a, b) => a.weight - b.weight)
Expand Down Expand Up @@ -606,7 +614,7 @@ export default function PipelineEditor(props) {
if (!reactFlowInstance)
return null

const flow = reactFlowInstance.toObject();
const flow = _lang.cloneDeep(reactFlowInstance.toObject());

// react-flow properties that are not necessary to rebuild graph when loading
flow.nodes.forEach((node) => {
Expand Down Expand Up @@ -640,6 +648,9 @@ export default function PipelineEditor(props) {
// Destructuring copy to leave out fields that are not part of the input description spec.
const { file, nodeId, inputId, ...copy } = input;
copy.weight = i
if(copy.example === undefined)
copy.example = null;

flow.inputs[id] = copy;
});

Expand Down Expand Up @@ -717,7 +728,13 @@ export default function PipelineEditor(props) {
}
}, [showAlert, generateSaveJSON]);

const onLoadFromFileBtnClick = () => inputFile.current.click(); // will call onLoad
const onLoadFromFileBtnClick = useCallback(() => {
if(hasUnsavedChanges) {
setModal("unsavedLoadFromFile")
} else {
inputFile.current.click(); // will call onLoad
}
}, [inputFile, hasUnsavedChanges])

const onLoadFromFile = (clickEvent) => {
clickEvent.stopPropagation();
Expand Down Expand Up @@ -758,20 +775,11 @@ export default function PipelineEditor(props) {
let options = {};
Object.entries(pipelineMap).forEach(([descriptionFile, pipelineName]) =>
(options[descriptionFile + ' (' + pipelineName + ')'] = () => {
api.getPipeline(descriptionFile, (error, data, response) => {
if (error) {
showAlert(
'error',
'Error loading the pipeline',
(response && response.text) ? response.text : error.toString()
)
} else {
setCurrentFileName(descriptionFile);
localStorage.setItem("currentFileName", descriptionFile);
setSavedJSON(JSON.stringify(data, null, 2));
onLoadFlow(data);
}
});
if (hasUnsavedChanges) {
setModal("unsavedLoadFromServer:" + descriptionFile)
} else {
loadFromServer(descriptionFile)
}
})
);
setPopupMenuOptions(options);
Expand Down Expand Up @@ -931,6 +939,23 @@ export default function PipelineEditor(props) {
showAlert
]);

const loadFromServer = useCallback((descriptionFile) => {
api.getPipeline(descriptionFile, (error, data, response) => {
if (error) {
showAlert(
'error',
'Error loading the pipeline',
(response && response.text) ? response.text : error.toString()
)
} else {
setCurrentFileName(descriptionFile);
localStorage.setItem("currentFileName", descriptionFile);
setSavedJSON(JSON.stringify(data, null, 2));
onLoadFlow(data);
}
});
}, [showAlert, setCurrentFileName, setSavedJSON, onLoadFlow])

const saveFileToServer = useCallback((descriptionFile) => {
api.getListOf("pipeline", (error, pipelineList, response) => {
if (error) {
Expand Down Expand Up @@ -1092,7 +1117,7 @@ export default function PipelineEditor(props) {
</DialogContent>
<DialogActions>
<Button onClick={() => setModal('saveAs')}>Cancel</Button>
<Button onClick={() => {hideModal('overwrite'); onSave(currentFileName)}} autoFocus>
<Button onClick={() => { hideModal('overwrite'); onSave(currentFileName) }} autoFocus>
Overwrite
</Button>
</DialogActions>
Expand All @@ -1114,6 +1139,29 @@ export default function PipelineEditor(props) {
</DialogActions>
</Dialog>

<Dialog
open={(modal ?? false) &&
(modal === "unsavedLoadFromFile" || modal.startsWith("unsavedLoadFromServer:"))}
onClose={hideCurrentModal}
>
<DialogTitle>Unsaved changes</DialogTitle>
<DialogContent>
<DialogContentText>
There are unsaved modifications that will be lost when loading the new pipeline.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={hideCurrentModal}>Stay</Button>
<Button onClick={() => {
hideCurrentModal();
if (modal === "unsavedLoadFromFile") inputFile.current.click()
else loadFromServer(modal.substring("unsavedLoadFromServer:".length))
}}>
Leave
</Button>
</DialogActions>
</Dialog>

{alertMessage && alertMessage !== '' && // when in open={...}, there was a flash frame while closing.
<Dialog open={true} onClose={clearAlert}>
<Alert severity={alertSeverity} id="alert-dialog-description" style={{ whiteSpace: "pre-wrap" }}>
Expand Down
4 changes: 2 additions & 2 deletions ui/src/components/StepResult.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@ export const SingleIOResult = memo(({ ioId, value, ioMetadata, componentId, sect
icon = <img src={errorImg} alt="Error" className="error-inline" />
}

if(! /^(.*\|)?[a-z0-9]+(?:_[a-z0-9]+)*$/.test(ioId) && !ioId.startsWith("pipeline@")) {
errorMsg = <p className='warning'>{ioId} should be a snake_case id</p>
if(! /^(.*\|)?[a-z0-9]+(?:_[a-z0-9]+)*$/.test(ioId) && !/pipeline@\d+$/.test(ioId)) {
errorMsg = <p className='warning'>Output id {ioId.replace(/^(.*\|)/, '')} should be a snake_case id</p>
icon = <img src={warningImg} alt="Warning" className="error-inline" />
}
} else {
Expand Down
8 changes: 4 additions & 4 deletions ui/src/components/form/InputFileInput.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const InputForm = ({ inputs, inputFileContent, setInputFileContent }) => {
{Object.entries(inputs)
.sort((a, b) => a[1].weight - b[1].weight)
.map(([inputId, inputDescription]) => {
const { label, description, options, example, ...theRest } =
const { label, description, options, example, weight, ...theRest } =
inputDescription;

return (
Expand All @@ -73,8 +73,8 @@ const InputForm = ({ inputs, inputFileContent, setInputFileContent }) => {
<label htmlFor={inputId}>
{label ? <strong>{label}</strong> : <p className='error'>Missing label for input "{inputId}"</p>}
{! /^(.*\|)?[a-z0-9]+(?:_[a-z0-9]+)*$/.test(inputId)
&& !inputId.startsWith("pipeline@")
&& <p className='warning'>{inputId} should be a snake_case id</p>
&& !/pipeline@\d+$/.test(inputId)
&& <p className='warning'>Input id {inputId.replace(/^(.*\|)/, '')} should be a snake_case id</p>
}
</label>
<ScriptInput
Expand All @@ -96,7 +96,7 @@ const InputForm = ({ inputs, inputFileContent, setInputFileContent }) => {
}

{!isEmptyObject(theRest) && yaml.dump(theRest)}
{example
{example !== undefined
? <>
Example:
<br />
Expand Down
22 changes: 13 additions & 9 deletions ui/src/components/form/ScriptInput.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,23 @@ export default function ScriptInput({ type, value, options, onValueUpdated, cols
}

if (type.endsWith('[]')) {
const onUpdateArray = event => {
const newValue = event.target.value
if (!newValue || newValue === "") {
onValueUpdated([])
} else {
onValueUpdated(event.target.value.split(',').map(v => v.trim()))
}
}

return <AutoResizeTextArea {...passedProps}
value={joinIfArray(fieldValue)}
onChange={e => setFieldValue(e.target.value)}
placeholder={ARRAY_PLACEHOLDER}
keepWidth={true}
cols={cols}
onBlur={e => {
const newValue = e.target.value
if (!newValue || newValue === "") {
onValueUpdated([])
} else {
onValueUpdated(e.target.value.split(',').map(v => v.trim()))
}
}}
onBlur={onUpdateArray}
onKeyDown={(e) => e.ctrlKey && onUpdateArray(e)}
/>
}

Expand Down Expand Up @@ -98,10 +101,11 @@ export default function ScriptInput({ type, value, options, onValueUpdated, cols
}

if (fieldValue && fieldValue.includes("\n")) {
props.onKeyDown = (e) => e.ctrlKey && updateValue(e)
return <AutoResizeTextArea keepWidth={true} cols={cols} {...props} />
} else {
return <input type='text' {...props}
onKeyDown={e => { if (e.key === "Enter") updateValue(e) }} />
onKeyDown={e => { if (e.key === "Enter" || e.ctrlKey) updateValue(e) }} />
}
}
}
8 changes: 5 additions & 3 deletions ui/src/utils/IOId.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ export function toInputId(props) {
* @returns the IO id: step@nodeId|output
*/
export function toOutputId(props) {
return props.file === undefined
? toIOId("pipeline", props.nodeId)
: toIOId(props.file, props.nodeId, props.outputId);
return toIOId(
props.file === undefined ? "pipeline" : props.file,
props.nodeId,
props.outputId
);
}

/**
Expand Down
22 changes: 15 additions & 7 deletions viewer/src/components/Main/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,24 +95,32 @@ export default function Main(props: any) {
if (logTransform) {
expression = `sqrt(${selectedLayer.band_id})`;
}
const obj = {
assets: selectedLayerAssetName,
colormap_name: colormap,
bidx: "1",
expression: expression,
};

let min = data.percentile_2;
let max = data.percentile_98;
let colmap = colormap;
if (min === max) {
min = data.min;
max = data.max;
if (data.min == data.max) {
if (colormap == "inferno" || colormap == "hot") {
colmap = "spectral";
setColormap(colmap);
}
}
}
const obj = {
assets: selectedLayerAssetName,
colormap_name: colmap,
bidx: "1",
expression: expression,
};
const rescale = `${min},${max}`;
const params = new URLSearchParams(obj).toString();
setSelectedLayerTiles(
`${tiler}?url=${selectedLayer.url}&rescale=${rescale}&${params}&expression=${expression}`
);
setLegend(createRangeLegendControl(min, max, cmap(colormap)));
setLegend(createRangeLegendControl(min, max, cmap(colmap)));
}
);
GetCOGBounds(selectedLayer.url).then((b: any) => {
Expand Down
Loading

0 comments on commit f7e450f

Please sign in to comment.