Skip to content

feat: datafiles action type json #1790

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { ActionDataset } from "./datafiles-action.interfaces";
import { UsersService } from "@scicatproject/scicat-sdk-ts-angular";
import { AuthService } from "shared/services/auth/auth.service";
import { MatSnackBarModule } from "@angular/material/snack-bar";

describe("1000: DatafilesActionComponent", () => {
let component: DatafilesActionComponent;
Expand Down Expand Up @@ -157,6 +158,7 @@ describe("1000: DatafilesActionComponent", () => {
PipesModule,
ReactiveFormsModule,
MatDialogModule,
MatSnackBarModule,
RouterModule,
RouterModule.forRoot([]),
StoreModule.forRoot({}),
Expand Down
92 changes: 79 additions & 13 deletions src/app/datasets/datafiles-actions/datafiles-action.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { UsersService } from "@scicatproject/scicat-sdk-ts-angular";
import { ActionConfig, ActionDataset } from "./datafiles-action.interfaces";
import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces";
import { AuthService } from "shared/services/auth/auth.service";
import { v4 } from "uuid";
import { MatSnackBar } from "@angular/material/snack-bar";

@Component({
selector: "datafiles-action",
Expand Down Expand Up @@ -37,6 +39,7 @@ export class DatafilesActionComponent implements OnInit, OnChanges {
constructor(
private usersService: UsersService,
private authService: AuthService,
private snackBar: MatSnackBar,
) {
this.usersService.usersControllerGetUserJWT().subscribe((jwt) => {
this.jwt = jwt.jwt;
Expand Down Expand Up @@ -111,6 +114,8 @@ export class DatafilesActionComponent implements OnInit, OnChanges {
perform_action() {
const action_type = this.actionConfig.type || "form";
switch (action_type) {
case "json-download":
return this.type_json_download();
case "form":
default:
return this.type_form();
Expand Down Expand Up @@ -159,21 +164,82 @@ export class DatafilesActionComponent implements OnInit, OnChanges {
return true;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Consider handling the fetch promise to manage asynchronous errors.

The current implementation calls fetch without awaiting its result or handling potential errors. This might lead to unhandled rejections or race conditions if the response is used later. Consider refactoring this method as an async function and using async/await, or at least adding a .then/.catch to deal with the promise resolution.

Suggested implementation:

  async handleAction() {
    try {
      const response = await fetch(this.actionConfig.url, {
        method: this.actionConfig.method || "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });

      if (!response.ok) {
        throw new Error(`Network response was not ok: ${response.statusText}`);
      }

      return true;
    } catch (error) {
      console.error("Fetch error:", error);
      return false;
    }

Ensure that any callers of handleAction() are updated to handle the Promise returned by this async method.

}

/*
* future development
*
type_fetch() {
const data = new URLSearchParams();
for (const pair of new FormData(formElement)) {
data.append(pair[0], pair[1]);
type_json_download() {
let payload = "";
if (this.actionConfig.payload) {
payload = this.actionConfig.payload
.replace(/{{ auth_token }}/, `Bearer ${this.authService.getToken().id}`)
.replace(/{{ jwt }}/, this.jwt)
.replace(/{{ datasetPid }}/, this.actionDataset.pid)
.replace(/{{ sourceFolder }}/, this.actionDataset.sourceFolder)
.replace(
/{{ filesPath }}/,
JSON.stringify(
this.files
.filter(
(item) =>
this.actionConfig.files === "all" ||
(this.actionConfig.files === "selected" && item.selected),
)
.map((item) => item.path),
),
);
} else {
const data = {
auth_token: `Bearer ${this.authService.getToken().id}`,
jwt: this.jwt,
dataset: this.actionDataset.pid,
directory: this.actionDataset.sourceFolder,
files: this.files
.filter(
(item) =>
this.actionConfig.files === "all" ||
(this.actionConfig.files === "selected" && item.selected),
)
.map((item) => item.path),
};
payload = JSON.stringify(data);
}

fetch(url, {
method: 'post',
body: data,
const filename = this.actionConfig.filename.replace(/{{ uuid }}/, v4());

fetch(this.actionConfig.url, {
method: this.actionConfig.method || "POST",
headers: {
"Content-Type": "application/json",
},
body: payload,
})
.then(…);
}
.then((response) => {
if (response.ok) {
return response.blob();
} else {
// http error
console.log(`HTTP Error code: ${response.status}`);
return Promise.reject(
new Error(`HTTP Error code: ${response.status}`),
);
}
})
.then((blob) => URL.createObjectURL(blob))
.then((url) => {
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
})
.catch((error) => {
console.log("Datafile action error : ", error);
this.snackBar.open(
"There has been an error performing the action",
"Close",
{
duration: 2000,
},
);
});

return true;
}
*/
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface ActionConfig {
id: string;
description?: string;
order: number;
label: string;
files: string;
Expand All @@ -12,6 +13,8 @@ export interface ActionConfig {
method?: string;
enabled?: string;
disabled?: string;
payload?: string;
filename?: string;
}

export interface ActionDataset {
Expand Down
197 changes: 197 additions & 0 deletions src/app/datasets/datafiles-actions/datafiles-actions.documentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# Datafiles Actions documentation

Datafiles actions are configurable actions specific to all or selected datafiles. They are shown as button in the "Datafiles" tab under the individual dataset page.
The button can be configured with text or icon or both.
At the moment there are only two kind of actions: form and json-download.

## Configuration
The configuration of the datafiles actions is done by adding an array of action objects under the __datafilesActions__ key of the SciCat frontend configuration file.
Each object in the array configure one action.

## Individual action configuration
The action configuration is a json object wiuth the following keys:
- __id__: unique id of the action.
Used for management and tracking purposes.
- _Type_: string
- _Required_: true
- _Example_: "eed8efec-4354-11ef-a3b5-d75573a5d37f",
- __description__: description of the action.
Not shown in the FE, used only for management purposes.
- _Type_: string
- _Optional_: true
- __order__: order in which the related button is rendered in the tab.
The action are ordered from left to right in ascending order.
- _Type_: integer
- _Optional_: true
- _Example_: 5
- __label__: String shown in related button.
If no label is provided, an icon should be defined and the button will use only the icom.
- _Type_: string
- _Optional_: true
- _Example_: Download All
- _Notes_: at least on of the following properties should be present: `label` or `icon` or `mat_icon`
- __files__: which files should be provided when the action is triggered.
- _Type_: string
- _Allowed values_: `selected` , `all`
- __mat_icon__: material icon to be shown on the left of the associated button.
If not provided, the actions should contain at least _label_ or _icon_.
- _Type_: string
- _Example_: "download",
- _Notes_: at least on of the following properties should be present: `label` or `icon` or `mat_icon`
- __icon__: icon to be shown on the left of the associated button.
If not provided, the actions should contain at least _label_ or _mat_icon_.
All browser supported formats are accepted. The path must be a valid one.
- _Type_: string
- _Example_: "/assets/icons/jupyter_logo.png",
- _Notes_: at least on of the following properties should be present: `label` or `icon` or `mat_icon`
- __type__: type of action
- _Type_: string
- _Allowed values_:
- `form`
The action will be triggered as a form submission
- `json_download`
The action will be triggered with a json payload and it expects to save the results as a file
- __url__: this is the url to be used when triggering the request.
- _Type_: string
- _Example_: "https://zip.scicatproject.org/download/all",
- __target__ : if type is set to `form`, it specified if the form should be submitted in the current browser windows/tab or in an another.
Please review the offical documentation for this attribute https://www.w3schools.com/TAgs/att_form_target.asp
- _Type_: string
- _Example_: ": "_blank",
- __enabled__: condition when the action can be triggered and the related button should be active.
The string may contains the keywords listed below and any logical expression of them.
- _Type_: string
- _Examples_: "#SizeLimit" or "#Selected && #SizeLimit",
- _Keywords_: The string can contain any of the following keywords in a logical expression.
The expression will be calculated everytime one of the keywords changes value.
- #SizeLimit :
True if the total size of the files is below the limit indicated in configuration under the key `maxDirectDownloadSize`.
- #Selected :
True if one or more files are selected in the list.
- __authorization__: indicate which user has access to the action and can see the related button.
- _Type_: string[]
- _Examples_: ["#datasetAccess", "#datasetPublic"]
- _Allowed values_:
- "#datasetAccess": users that have access to the dataset
- "#datasetPublic": if the dataset is public
- __payload__: json string to be send in the request body when the action is triggered.
Make sure that the string is properly escaped
- _Type_: string
- _Example_:
Raw (as it should be in the action configuration )
```
"{\"template_id\":\"c975455e-ede3-11ef-94fb-138c9cd51fc0\",\"parameters\":{\"dataset\":\"{{ datasetPid }}\",\"directory\":\"{{ sourceFolder }}\",\"files\": {{ filesPath }},\"jwt\":\"{{ jwt }}\",\"scicat_url\":\"https://my.scicat.instance\",\"file_server_url\":\"sftserver2.esss.dk\",\"file_server_port\":\"22\"}}",
```
Formatted (for human consumption)
```
{
"template_id": "c975455e-ede3-11ef-94fb-138c9cd51fc0",
"parameters": {
"dataset": "{{ datasetPid }}",
"directory": "{{ sourceFolder }}",
"files": {{ filesPath }},
"jwt": "{{ jwt }}",
"scicat_url": "https://my.scicat.instance",
"file_server_url": "my.sft.server",
"file_server_port": "22"
}
}
```
- _Keywords_: The string can contain the following keywords. They will be substituted with the value indicated:
- {{ datasetPid }}: pid of the current dataset
- {{ sourceFolder }}: source folder of the current dataset
- {{ jwt }}: curren tJWT token of the user logged in
- {{ filesPath }}: array containing the list of the the file paths associated with the current dataset. The list might contain all the files or only the selected ones, depending on the value of the field _files_.
- __filename__: name of the file that should be saved by the browser if the action type is set to _json_download_.
- _Type_: string
- _Example_: "{{ uuid }}.ipynb"
- _Keywords_: The string can contain any of the following keywords. They are substituted with the value indicated.
- {{ uuid }}: random uuid v4 generated for each request.


## Example
The following is the configuration example provided together with the code.
The configuration below will create the following 5 buttons under the "Datafiles" tab of the dataset in the order provided:
1. Notebook Selected
Create a jupyter notebook properly populated in order to download the selected files and use the dataset metadata in the python environment.
The jupyter notebook is created by a different services that needs to be correctly configured and it is assumed that expects in input data coming from a submission form.
2. Notebook All (Form)
Create a jupyter notebook properly populated in order to download all the files and use the dataset metadata in the python environment.
The jupyter notebook is created by a different services that needs to be correctly configured and it is assumed that expects in input data coming from a submission form.
3. Notebook All (Download JSON)
Create a jupyter notebook properly populated in order to download all the files and use the dataset metadata in the python environment.
The jupyter notebook is created by a different services that needs to be correctly configured. We assume that the service expects input data in a specific json format specified under payload.
4. Download Selected
Triggers the download of a zip file containing only the selected files.
The zip file is created by an external service, which needs to be properly configured.
5. Donwload ASll
Triggers the download of a zip file containing all the dataset files
The zip file is created by an external service, which needs to be properly configured.

Configuration
```
{
"datafilesActions" : [
{
"id": "eed8efec-4354-11ef-a3b5-d75573a5d37f",
"order": 5,
"label": "Download All",
"files": "all",
"mat_icon": "download",
"type": "form",
"url": "https://zip.scicatproject.org/download/all",
"target": "_blank",
"enabled": "#SizeLimit",
"authorization": ["#datasetAccess", "#datasetPublic"]
},
{
"id": "3072fafc-4363-11ef-b9f9-ebf568222d26",
"order": 4,
"label": "Download Selected",
"files": "selected",
"mat_icon": "download",
"type": "form",
"url": "https://zip.scicatproject.org/download/selected",
"target": "_blank",
"enabled": "#Selected && #SizeLimit",
"authorization": ["#datasetAccess", "#datasetPublic"]
},
{
"id": "4f974f0e-4364-11ef-9c63-03d19f813f4e",
"order": 2,
"label": "Notebook All (Form)",
"files": "all",
"icon": "/assets/icons/jupyter_logo.png",
"type": "form",
"url": "https://www.scicat.info/notebook/all",
"target": "_blank",
"authorization": ["#datasetAccess", "#datasetPublic"]
},
{
"id": "0cd5b592-0b1a-11f0-a42c-23e177127ee7",
"order": 3,
"label": "Notebook All (Download JSON)",
"files": "all",
"type": "json-download",
"icon": "/assets/icons/jupyter_logo.png",
"url": "https://www.sciwyrm.info/notebook",
"target": "_blank",
"authorization": ["#datasetAccess", "#datasetPublic"],
"payload": "{\"template_id\":\"c975455e-ede3-11ef-94fb-138c9cd51fc0\",\"parameters\":{\"dataset\":\"{{ datasetPid }}\",\"directory\":\"{{ sourceFolder }}\",\"files\": {{ filesPath }},\"jwt\":\"{{ jwt }}\",\"scicat_url\":\"https://my.scicat.instance\",\"file_server_url\":\"my.sft.server\",\"file_server_port\":\"22\"}}",
"filename": "{{ uuid }}.ipynb"
},
{
"id": "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd",
"order": 1,
"label": "Notebook Selected",
"files": "selected",
"icon": "/assets/icons/jupyter_logo.png",
"type": "form",
"url": "https://www.scicat.info/notebook/selected",
"target": "_blank",
"enabled": "#Selected",
"authorization": ["#datasetAccess", "#datasetPublic"]
}
]
}
```
Loading
Loading