;
+ const mockAppConfigService = {
+ getConfig: () => {
+ return {
+ maxDirectDownloadSize: 10000,
+ datafilesActionsEnabled: true,
+ };
+ },
+ };
+
+ const actionsConfig = [
+ {
+ id: "eed8efec-4354-11ef-a3b5-d75573a5d37f",
+ order: 4,
+ label: "Download All",
+ files: "all",
+ mat_icon: "download",
+ url: "",
+ target: "_blank",
+ enabled: "#SizeLimit",
+ authorization: ["#datasetAccess", "#datasetPublic"],
+ },
+ {
+ id: "3072fafc-4363-11ef-b9f9-ebf568222d26",
+ order: 3,
+ label: "Download Selected",
+ files: "selected",
+ mat_icon: "download",
+ url: "",
+ target: "_blank",
+ enabled: "#Selected && #SizeLimit",
+ authorization: ["#datasetAccess", "#datasetPublic"],
+ },
+ {
+ id: "4f974f0e-4364-11ef-9c63-03d19f813f4e",
+ order: 2,
+ label: "Notebook All",
+ files: "all",
+ icon: "/assets/icons/jupyter_logo.png",
+ url: "",
+ target: "_blank",
+ authorization: ["#datasetAccess", "#datasetPublic"],
+ },
+ {
+ id: "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd",
+ order: 1,
+ label: "Notebook Selected",
+ files: "selected",
+ icon: "/assets/icons/jupyter_logo.png",
+ url: "",
+ target: "_blank",
+ enabled: "#Selected",
+ authorization: ["#datasetAccess", "#datasetPublic"],
+ },
+ ];
+
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ schemas: [NO_ERRORS_SCHEMA],
+ imports: [
+ MatButtonModule,
+ MatIconModule,
+ MatTableModule,
+ PipesModule,
+ ReactiveFormsModule,
+ MatDialogModule,
+ RouterModule,
+ RouterModule.forRoot([]),
+ StoreModule.forRoot({}),
+ ],
+ declarations: [DatafilesActionsComponent],
+ });
+ TestBed.overrideComponent(DatafilesActionsComponent, {
+ set: {
+ providers: [
+ { provide: UserApi, useClass: MockUserApi },
+ { provide: MatDialogRef, useClass: MockMatDialogRef },
+ { provide: AppConfigService, useValue: mockAppConfigService },
+ { provide: UserApi, useClass: MockUserApi },
+ ],
+ },
+ });
+ TestBed.compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DatafilesActionsComponent);
+ component = fixture.componentInstance;
+ component.files = [
+ {
+ path: "test1",
+ size: 5000,
+ time: "2019-09-06T13:11:37.102Z",
+ chk: "string",
+ uid: "string",
+ gid: "string",
+ perm: "string",
+ selected: false,
+ hash: "",
+ },
+ {
+ path: "test2",
+ size: 10000,
+ time: "2019-09-06T13:11:37.102Z",
+ chk: "string",
+ uid: "string",
+ gid: "string",
+ perm: "string",
+ selected: false,
+ hash: "",
+ },
+ ];
+ component.actionsConfig = actionsConfig;
+ component.actionDataset = {
+ pid: "57eb0ad6-48d4-11ef-814b-df221a8e3571",
+ sourceFolder: "/level_1/level_2/level3",
+ };
+ fixture.detectChanges();
+ });
+
+ afterEach(() => {
+ fixture.destroy();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("sorted actions should be sorted", () => {
+ const sortedActionsConfig = component.sortedActionsConfig;
+
+ for (let i = 1; i < sortedActionsConfig.length; i++) {
+ expect(
+ sortedActionsConfig[i].order >= sortedActionsConfig[i - 1].order,
+ ).toEqual(true);
+ }
+ });
+
+ it("actions should be visible when enabled in configuration", () => {
+ expect(component.visible).toEqual(true);
+ });
+
+ it("actions should be visible when disabled in configuration", () => {
+ spyOn(mockAppConfigService, "getConfig").and.returnValue({
+ maxDirectDownloadSize: 10000,
+ datafilesActionsEnabled: false,
+ });
+ expect(component.visible).toEqual(false);
+ });
+
+ it("max file size should be the same as set in configuration, aka 10000", () => {
+ expect(component.maxFileSize).toEqual(10000);
+ });
+
+ it("max file size should be the same as set in configuration, aka 5000", () => {
+ spyOn(mockAppConfigService, "getConfig").and.returnValue({
+ maxDirectDownloadSize: 5000,
+ datafilesActionsEnabled: true,
+ });
+ expect(component.maxFileSize).toEqual(5000);
+ });
+
+ it("actions should be visible with default configuration", () => {
+ spyOn(mockAppConfigService, "getConfig").and.returnValue({
+ maxDirectDownloadSize: 10000,
+ datafilesActionsEnabled: true,
+ });
+ expect(component.visible).toEqual(true);
+ });
+
+ it("there should be 4 actions as defined in default configuration", async () => {
+ expect(component.sortedActionsConfig.length).toEqual(actionsConfig.length);
+ const htmlElement: HTMLElement = fixture.nativeElement;
+ const htmlActions = htmlElement.querySelectorAll("datafiles-action");
+ expect(htmlActions.length).toEqual(actionsConfig.length);
+ });
+
+ it("there should be 0 actions with no actions configured", async () => {
+ component.actionsConfig = [];
+ fixture.detectChanges();
+ expect(component.sortedActionsConfig.length).toEqual(0);
+ const htmlElement: HTMLElement = fixture.nativeElement;
+ const htmlActions = htmlElement.querySelectorAll("datafiles-action");
+ expect(htmlActions.length).toEqual(0);
+ });
+});
diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.component.ts b/src/app/datasets/datafiles-actions/datafiles-actions.component.ts
new file mode 100644
index 000000000..f449b4c35
--- /dev/null
+++ b/src/app/datasets/datafiles-actions/datafiles-actions.component.ts
@@ -0,0 +1,48 @@
+import { Component, Input, OnInit } from "@angular/core";
+import { ActionConfig, ActionDataset } from "./datafiles-action.interfaces";
+import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces";
+import { AppConfigService } from "app-config.service";
+//import { DatafilesActionComponent } from "./datafiles-action.component";
+
+@Component({
+ selector: "datafiles-actions",
+ //standalone: true,
+ //imports: [DatafilesActionComponent],
+ templateUrl: "./datafiles-actions.component.html",
+ styleUrls: ["./datafiles-actions.component.scss"],
+})
+export class DatafilesActionsComponent {
+ private _sortedActionsConfig: ActionConfig[];
+
+ @Input({ required: true }) actionsConfig: ActionConfig[];
+ @Input({ required: true }) actionDataset: ActionDataset;
+ @Input({ required: true }) files: DataFiles_File[];
+
+ constructor(public appConfigService: AppConfigService) {}
+
+ // ngOnInit() {
+ // this.sortedActionsConfig = this.actionsConfig;
+ // this.sortedActionsConfig.sort((a: ActionConfig, b: ActionConfig) =>
+ // a.order && b.order ? a.order - b.order : 0,
+ // );
+ // }
+
+ get visible(): boolean {
+ return (
+ this.appConfigService.getConfig().datafilesActionsEnabled &&
+ this.files.length > 0
+ );
+ }
+
+ get maxFileSize(): number {
+ return this.appConfigService.getConfig().maxDirectDownloadSize || 0;
+ }
+
+ get sortedActionsConfig(): ActionConfig[] {
+ this._sortedActionsConfig = this.actionsConfig;
+ this._sortedActionsConfig.sort((a: ActionConfig, b: ActionConfig) =>
+ a.order && b.order ? a.order - b.order : 0,
+ );
+ return this._sortedActionsConfig;
+ }
+}
diff --git a/src/app/datasets/datafiles/datafiles.component.html b/src/app/datasets/datafiles/datafiles.component.html
index 39877b4ed..8eb776fa3 100644
--- a/src/app/datasets/datafiles/datafiles.component.html
+++ b/src/app/datasets/datafiles/datafiles.component.html
@@ -38,8 +38,11 @@
{{ count }} datafiles.
-
No datafiles linked to this dataset
+ No files associated to this dataset
+
+
+
{
let component: DatafilesComponent;
let fixture: ComponentFixture;
- const getConfig = () => ({});
+ const getConfig = () => ({
+ datafilesActionsEnabled: true,
+ datafilesActions: [
+ {
+ id: "eed8efec-4354-11ef-a3b5-d75573a5d37f",
+ order: 4,
+ label: "Download All",
+ files: "all",
+ mat_icon: "download",
+ url: "",
+ target: "_blank",
+ enabled: "#SizeLimit",
+ authorization: ["#datasetAccess", "#datasetPublic"],
+ },
+ {
+ id: "3072fafc-4363-11ef-b9f9-ebf568222d26",
+ order: 3,
+ label: "Download Selected",
+ files: "selected",
+ mat_icon: "download",
+ url: "",
+ target: "_blank",
+ enabled: "#Selected && #SizeLimit",
+ authorization: ["#datasetAccess", "#datasetPublic"],
+ },
+ {
+ id: "4f974f0e-4364-11ef-9c63-03d19f813f4e",
+ order: 2,
+ label: "Notebook All",
+ files: "all",
+ icon: "/assets/icons/jupyter_logo.png",
+ url: "",
+ target: "_blank",
+ authorization: ["#datasetAccess", "#datasetPublic"],
+ },
+ {
+ id: "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd",
+ order: 1,
+ label: "Notebook Selected",
+ files: "selected",
+ icon: "/assets/icons/jupyter_logo.png",
+ url: "",
+ target: "_blank",
+ enabled: "#Selected",
+ authorization: ["#datasetAccess", "#datasetPublic"],
+ },
+ ],
+ });
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
@@ -44,6 +96,10 @@ describe("DatafilesComponent", () => {
{ provide: MatDialogRef, useClass: MockMatDialogRef },
{ provide: AppConfigService, useValue: { getConfig } },
{ provide: UserApi, useClass: MockUserApi },
+ {
+ provide: DatafilesActionsComponent,
+ useClass: MockDatafilesActionsComponent,
+ },
],
},
});
diff --git a/src/app/datasets/datafiles/datafiles.component.ts b/src/app/datasets/datafiles/datafiles.component.ts
index ff8846583..136e192bd 100644
--- a/src/app/datasets/datafiles/datafiles.component.ts
+++ b/src/app/datasets/datafiles/datafiles.component.ts
@@ -30,17 +30,8 @@ import { PublicDownloadDialogComponent } from "datasets/public-download-dialog/p
import { submitJobAction } from "state-management/actions/jobs.actions";
import { AppConfigService } from "app-config.service";
import { NgForm } from "@angular/forms";
-
-export interface File {
- path: string;
- size: number;
- time: string;
- chk: string;
- uid: string;
- gid: string;
- perm: string;
- selected: boolean;
-}
+import { DataFiles_File } from "./datafiles.interfaces";
+import { ActionDataset } from "datasets/datafiles-actions/datafiles-action.interfaces";
@Component({
selector: "datafiles",
@@ -73,6 +64,7 @@ export class DatafilesComponent
files: Array = [];
sourcefolder = "";
datasetPid = "";
+ actionDataset: ActionDataset;
count = 0;
pageSize = 25;
@@ -111,7 +103,7 @@ export class DatafilesComponent
dateFormat: "yyyy-MM-dd HH:mm",
},
];
- tableData: File[] = [];
+ tableData: DataFiles_File[] = [];
constructor(
public appConfigService: AppConfigService,
@@ -160,6 +152,19 @@ export class DatafilesComponent
updateSelectionStatus() {
this.areAllSelected = this.getAreAllSelected();
this.isNoneSelected = this.getIsNoneSelected();
+ this.updateSelectedInFiles();
+ }
+
+ updateSelectedInFiles() {
+ const selected = this.tableData
+ .filter((item) => item.selected)
+ .map((item) => item.path);
+ const files = this.files.map((item) => {
+ item.selected = selected.includes(item.path);
+ return item;
+ });
+ console.log(files);
+ this.files = [...files];
}
onSelectOne(checkboxEvent: CheckboxEvent) {
@@ -205,13 +210,14 @@ export class DatafilesComponent
if (dataset) {
this.sourcefolder = dataset.sourceFolder;
this.datasetPid = dataset.pid;
+ this.actionDataset = dataset;
}
}),
);
this.subscriptions.push(
this.datablocks$.subscribe((datablocks) => {
if (datablocks) {
- const files: File[] = [];
+ const files: DataFiles_File[] = [];
datablocks.forEach((block) => {
block.dataFileList.map((file) => {
this.totalFileSize += file.size;
diff --git a/src/app/datasets/datafiles/datafiles.interfaces.ts b/src/app/datasets/datafiles/datafiles.interfaces.ts
new file mode 100644
index 000000000..a781da1ee
--- /dev/null
+++ b/src/app/datasets/datafiles/datafiles.interfaces.ts
@@ -0,0 +1,11 @@
+export interface DataFiles_File {
+ path: string;
+ size: number;
+ time: string;
+ chk: string;
+ uid: string;
+ gid: string;
+ perm: string;
+ selected: boolean;
+ hash: string;
+}
diff --git a/src/app/datasets/datasets.module.ts b/src/app/datasets/datasets.module.ts
index 72e36d577..72c17a0bf 100644
--- a/src/app/datasets/datasets.module.ts
+++ b/src/app/datasets/datasets.module.ts
@@ -82,6 +82,8 @@ import { instrumentsReducer } from "state-management/reducers/instruments.reduce
import { InstrumentEffects } from "state-management/effects/instruments.effects";
import { RelatedDatasetsComponent } from "./related-datasets/related-datasets.component";
import { FullTextSearchBarComponent } from "./dashboard/full-text-search/full-text-search-bar.component";
+import { DatafilesActionsComponent } from "./datafiles-actions/datafiles-actions.component";
+import { DatafilesActionComponent } from "./datafiles-actions/datafiles-action.component";
@NgModule({
imports: [
@@ -161,6 +163,8 @@ import { FullTextSearchBarComponent } from "./dashboard/full-text-search/full-te
DatasetFileUploaderComponent,
AdminTabComponent,
RelatedDatasetsComponent,
+ DatafilesActionsComponent,
+ DatafilesActionComponent,
],
providers: [
ArchivingService,
diff --git a/src/app/shared/MockStubs.ts b/src/app/shared/MockStubs.ts
index edaa2a986..3e1dc2776 100644
--- a/src/app/shared/MockStubs.ts
+++ b/src/app/shared/MockStubs.ts
@@ -6,6 +6,11 @@ import { AppConfig } from "app-config.module";
import { SciCatDataSource } from "./services/scicat.datasource";
import { LoopBackAuth } from "./sdk";
import { Injectable } from "@angular/core";
+import {
+ ActionConfig,
+ ActionDataset,
+} from "datasets/datafiles-actions/datafiles-action.interfaces";
+import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces";
export class MockUserApi {
getCurrentId() {
@@ -200,3 +205,61 @@ export class MockLoopBackAuth extends LoopBackAuth {
getAccessToken = () => ({ id: "test" });
getAccessTokenId = () => "test";
}
+
+export class MockDatafilesActionsComponent {
+ actionsConfig: ActionConfig[];
+ dataset: ActionDataset;
+ files: DataFiles_File[];
+}
+
+export class MockHtmlElement {
+ id = "";
+ tag = "HTML";
+ innerHTML = "";
+ value = "";
+ name = "";
+ disabled = false;
+ style: unknown = { display: "block", backgroundColor: "red" };
+ children: MockHtmlElement[] = [];
+
+ constructor(tag = "", id = "") {
+ this.id = id;
+ this.tag = tag;
+ }
+ createElement(t: string, id = "") {
+ return new MockHtmlElement(t, id);
+ }
+ appendChild(x: MockHtmlElement) {
+ this.children.push(x);
+ return x;
+ }
+ clear() {
+ this.children = [];
+ }
+ querySelector(sel: string) {
+ // too hard to implement correctly, so just hack something
+ const list = this.getElementsByTagName(sel);
+ return list.length > 0 ? list[0] : this.children[0];
+ }
+ querySelectorAll(sel: string) {
+ // too hard to implement correctly, so just return all children
+ return this.children;
+ }
+ getElementById(id: string): any {
+ // if not found, just CREATE one!!
+ return (
+ this.children.find((x) => x.id == id) ||
+ this.appendChild(this.createElement("", id))
+ );
+ }
+ getElementsByClassName(classname: string): any[] {
+ return this.children.filter((x: any) => x.classList.contains(classname));
+ }
+ getElementsByName(name: string): any[] {
+ return this.children.filter((x: any) => x.name == name);
+ }
+ getElementsByTagName(tag: string): any[] {
+ return this.children.filter((x: any) => x.tag == tag.toUpperCase());
+ }
+ submit() {}
+}
diff --git a/src/assets/config.json b/src/assets/config.json
index c32cf212b..5d70653dc 100644
--- a/src/assets/config.json
+++ b/src/assets/config.json
@@ -9,8 +9,8 @@
"editPublishedData": false,
"addSampleEnabled": true,
"externalAuthEndpoint": "/auth/msad",
- "facility": "ESS",
- "siteIcon": "esslogo-white.png",
+ "facility": "Local",
+ "siteIcon": "site-header-logo.png",
"loginFacilityLabel": "ESS",
"loginLdapLabel": "Ldap",
"loginLocalLabel": "Local",
@@ -111,8 +111,8 @@
"maxDirectDownloadSize": 5000000000,
"metadataPreviewEnabled": true,
"metadataStructure": "",
- "multipleDownloadAction": "http://localhost:3012/zip",
- "multipleDownloadEnabled": true,
+ "multipleDownloadAction": "http://localhost:3012/zip",
+ "multipleDownloadEnabled": true,
"oAuth2Endpoints": [
{
"authURL": "api/v3/auth/oidc",
@@ -131,5 +131,51 @@
"shoppingCartEnabled": true,
"shoppingCartOnHeader": true,
"tableSciDataEnabled": true,
- "datasetDetailsShowMissingProposalId": false
+ "datasetDetailsShowMissingProposalId": false,
+ "datafilesActionsEnabled" : true,
+ "datafilesActions" : [
+ {
+ "id" : "eed8efec-4354-11ef-a3b5-d75573a5d37f",
+ "order" : 4,
+ "label" : "Download All",
+ "files" : "all",
+ "mat_icon" : "download",
+ "url" : "",
+ "target" : "_blank",
+ "enabled" : "#SizeLimit",
+ "authorization" : ["#datasetAccess", "#datasetPublic" ]
+ },
+ {
+ "id" : "3072fafc-4363-11ef-b9f9-ebf568222d26",
+ "order" : 3,
+ "label" : "Download Selected",
+ "files" : "selected",
+ "mat_icon" : "download",
+ "url" : "",
+ "target" : "_blank",
+ "enabled" : "#Selected && #SizeLimit",
+ "authorization" : ["#datasetAccess", "#datasetPublic" ]
+ },
+ {
+ "id" : "4f974f0e-4364-11ef-9c63-03d19f813f4e",
+ "order" : 2,
+ "label" : "Notebook All",
+ "files" : "all",
+ "icon" : "/assets/icons/jupyter_logo.png",
+ "url" : "",
+ "target" : "_blank",
+ "authorization" : ["#datasetAccess", "#datasetPublic" ]
+ },
+ {
+ "id" : "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd",
+ "order" : 1,
+ "label" : "Notebook Selected",
+ "files" : "selected",
+ "icon" : "/assets/icons/jupyter_logo.png",
+ "url" : "",
+ "target" : "_blank",
+ "enabled" : "#Selected",
+ "authorization" : ["#datasetAccess", "#datasetPublic" ]
+ }
+ ]
}
diff --git a/src/assets/icons/jupyter_logo.png b/src/assets/icons/jupyter_logo.png
new file mode 100644
index 000000000..58623ebfb
Binary files /dev/null and b/src/assets/icons/jupyter_logo.png differ