first import of ui (angular)

This commit is contained in:
Michele Artini 2023-09-27 11:48:21 +02:00
parent ab3964043d
commit 47257cbb42
345 changed files with 16829 additions and 0 deletions

View File

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

42
dnet-app/frontends/is/.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

View File

@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

View File

@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "pwa-chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

View File

@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

View File

@ -0,0 +1,27 @@
# DnetIsApplication
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.1.1.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

View File

@ -0,0 +1,102 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"dnet-is-application": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/dnet-is-application",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.css"
],
"scripts": [
]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "dnet-is-application:build:production"
},
"development": {
"browserTarget": "dnet-is-application:build:development",
"proxyConfig": "src/proxy.conf.json"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "dnet-is-application:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.css"
],
"scripts": []
}
}
}
}
}
}

11986
dnet-app/frontends/is/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
{
"name": "dnet-is-application",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^15.1.0",
"@angular/cdk": "^15.1.1",
"@angular/common": "^15.1.0",
"@angular/compiler": "^15.1.0",
"@angular/core": "^15.1.0",
"@angular/forms": "^15.1.0",
"@angular/material": "^15.1.1",
"@angular/platform-browser": "^15.1.0",
"@angular/platform-browser-dynamic": "^15.1.0",
"@angular/router": "^15.1.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.12.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.1.1",
"@angular/cli": "~15.1.1",
"@angular/compiler-cli": "^15.1.0",
"@types/jasmine": "~4.3.0",
"jasmine-core": "~4.5.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"typescript": "~4.9.4"
}
}

View File

@ -0,0 +1,39 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { InfoComponent } from './info/info.component';
import { ProtocolsComponent } from './protocols/protocols.component';
import { WfHistoryComponent } from './wf-history/wf-history.component';
import { ResourcesComponent } from './resources/resources.component';
import { VocabulariesComponent, VocabularyEditorComponent } from './vocabularies/vocabularies.component';
import { ContextViewerComponent, ContextsComponent } from './contexts/contexts.component';
import { DsmSearchComponent, DsmResultsComponent, DsmApiComponent } from './dsm/dsm.component';
import { MdstoreInspectorComponent, MdstoresComponent } from './mdstores/mdstores.component';
import { CleanerTesterComponent } from './cleaner-tester/cleaner-tester.component';
import { EmailsComponent } from './emails/emails.component';
import { WfConfsComponent } from './wf-confs/wf-confs.component';
const routes: Routes = [
{ path: "", redirectTo: 'info', pathMatch: 'full' },
{ path: "info", component: InfoComponent },
{ path: "resources/:type", component: ResourcesComponent },
{ path: "adv_resources/context", component: ContextsComponent },
{ path: "adv_resources/vocabulary", component: VocabulariesComponent },
{ path: "adv_resources/protocol", component: ProtocolsComponent },
{ path: "adv_resources/email", component: EmailsComponent },
{ path: "wfs/:section", component: WfConfsComponent },
{ path: "wfs/conf/:conf", component: WfConfsComponent },
{ path: "wf_history", component: WfHistoryComponent },
{ path: "ctx_viewer", component: ContextViewerComponent },
{ path: "voc_editor", component: VocabularyEditorComponent },
{ path: "dsm/search", component: DsmSearchComponent },
{ path: "dsm/results/:page/:size", component: DsmResultsComponent },
{ path: "mdstores", component: MdstoresComponent },
{ path: "mdrecords/:versionId/:limit", component: MdstoreInspectorComponent },
{ path: "cleaner", component: CleanerTesterComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@ -0,0 +1,17 @@
.sidenav-container {
height: 100%;
}
.sidenav {
width: 350px;
}
.sidenav .mat-toolbar {
background: inherit;
}
.mat-toolbar.mat-primary {
position: sticky;
top: 0;
z-index: 1;
}

View File

@ -0,0 +1,23 @@
<div *ngIf="spinnerService.visibility | async" class="spinner">
<mat-progress-spinner mode="indeterminate" [strokeWidth]="8" [diameter]="80" color="primary"></mat-progress-spinner>
</div>
<mat-sidenav-container class="sidenav-container">
<mat-sidenav #drawer class="sidenav mat-elevation-z8" fixedInViewport
[attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'" [mode]="(isHandset$ | async) ? 'over' : 'side'"
[opened]="(isHandset$ | async) === false">
<app-main-menu-panels></app-main-menu-panels>
</mat-sidenav>
<mat-sidenav-content>
<mat-toolbar color="primary">
<button type="button" aria-label="Toggle sidenav" mat-icon-button (click)="drawer.toggle()">
<mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
</button>
<span>{{title}}</span>
</mat-toolbar>
<div style="padding: 20px;">
<!-- The routed views render in the <router-outlet>-->
<router-outlet></router-outlet>
</div>
</mat-sidenav-content>
</mat-sidenav-container>

View File

@ -0,0 +1,23 @@
import { Component, inject } from '@angular/core';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { SpinnerService } from './common/spinner.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'D-NET Information Service Application';
isHandset$: Observable<boolean> = this.breakpointObserver.observe(Breakpoints.Handset)
.pipe(
map(result => result.matches),
shareReplay()
);
constructor(private breakpointObserver: BreakpointObserver, public spinnerService: SpinnerService) { }
}

View File

@ -0,0 +1,120 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FilterPipe } from './common/filter.pipe';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { InfoComponent } from './info/info.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { LayoutModule } from '@angular/cdk/layout';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatIconModule } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatTreeModule } from '@angular/material/tree';
import { MatBadgeModule } from '@angular/material/badge';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'
import { MatSelectModule } from '@angular/material/select'
import { MatTableModule } from '@angular/material/table';
import { ProtocolsComponent } from './protocols/protocols.component';
import { MainMenuPanelsComponent } from './main-menu-panels/main-menu-panels.component';
import { MatExpansionModule } from '@angular/material/expansion';
import { WfHistoryDialog, WfHistoryComponent } from './wf-history/wf-history.component';
import { MatDialogModule } from '@angular/material/dialog';
import { MatSortModule } from '@angular/material/sort';
import { ResourcesComponent, ResContentDialog, ResCreateNewDialog, ResMetadataDialog } from './resources/resources.component'
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { ContextsComponent, ContextViewerComponent, ContextParamsDialog } from './contexts/contexts.component';
import { VocabulariesComponent, VocabularyEditorComponent, VocDialog, VocTermDialog } from './vocabularies/vocabularies.component';
import { DsmSearchComponent, DsmResultsComponent, DsmApiComponent, DsmAddApiDialog, DsmBrowseDialog } from './dsm/dsm.component';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { SpinnerHttpInterceptor } from './common/spinner.service';
import { MdstoresComponent, MdstoreInspectorComponent, MDStoreVersionsDialog, AddMDStoreDialog } from './mdstores/mdstores.component';
import { CleanerTesterComponent } from './cleaner-tester/cleaner-tester.component';
import { EmailDialog, EmailsComponent } from './emails/emails.component';
import { WfConfsComponent, WfConfDialog, WfConfSingle } from './wf-confs/wf-confs.component';
import { MatTabsModule } from '@angular/material/tabs';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatStepperModule } from '@angular/material/stepper';
@NgModule({
declarations: [
AppComponent,
FilterPipe,
MainMenuPanelsComponent,
InfoComponent,
ProtocolsComponent,
WfHistoryComponent,
WfHistoryDialog,
ResourcesComponent,
ResContentDialog,
ResCreateNewDialog,
ResMetadataDialog,
ContextsComponent,
ContextViewerComponent,
ContextParamsDialog,
VocabulariesComponent,
VocabularyEditorComponent,
VocDialog,
VocTermDialog,
DsmSearchComponent,
DsmResultsComponent,
DsmApiComponent,
DsmAddApiDialog,
DsmBrowseDialog,
MdstoresComponent,
MdstoreInspectorComponent,
MDStoreVersionsDialog,
AddMDStoreDialog,
CleanerTesterComponent,
EmailsComponent,
EmailDialog,
WfConfsComponent,
WfConfDialog,
WfConfSingle
],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
BrowserModule,
AppRoutingModule,
FormsModule,
HttpClientModule,
LayoutModule,
MatToolbarModule,
MatButtonModule,
MatSidenavModule,
MatIconModule,
MatListModule,
MatTreeModule,
MatBadgeModule,
MatCardModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatCheckboxModule,
MatTableModule,
MatExpansionModule,
MatDialogModule,
MatSortModule,
ReactiveFormsModule,
MatSnackBarModule,
MatPaginatorModule,
MatProgressSpinnerModule,
MatTabsModule,
MatStepperModule
],
providers: [{
provide: HTTP_INTERCEPTORS,
useClass: SpinnerHttpInterceptor,
multi: true
}],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@ -0,0 +1,25 @@
<h2>Cleaner Tester</h2>
<form [formGroup]="cleanForm" (ngSubmit)="clean()">
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Cleaning rule</mat-label>
<mat-select matInput formControlName="rule">
<mat-option *ngFor="let i of rules" [value]="i.name">{{i.name}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 50%;">
<mat-label>Input Record</mat-label>
<textarea matInput rows="10" formControlName="xmlIn"></textarea>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 50%;">
<mat-label>Output Record</mat-label>
<textarea matInput rows="10" formControlName="xmlOut" readonly></textarea>
</mat-form-field>
<button mat-stroked-button color="primary" type="submit" [disabled]="!cleanForm.valid">Submit</button>
<mat-error *ngIf="cleanForm.errors?.['serverError']">
{{ cleanForm.errors?.['serverError'] }}
</mat-error>
</form>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CleanerTesterComponent } from './cleaner-tester.component';
describe('CleanerTesterComponent', () => {
let component: CleanerTesterComponent;
let fixture: ComponentFixture<CleanerTesterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ CleanerTesterComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(CleanerTesterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,32 @@
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute } from '@angular/router';
import { ResourceType, SimpleResource } from '../common/is.model';
import { ISService } from '../common/is.service';
@Component({
selector: 'app-cleaner-tester',
templateUrl: './cleaner-tester.component.html',
styleUrls: ['./cleaner-tester.component.css']
})
export class CleanerTesterComponent {
rules: SimpleResource[] = [];
xmlOutput: string = ''
cleanForm = new FormGroup({
xmlIn: new FormControl('', [Validators.required]),
rule: new FormControl('', [Validators.required]),
xmlOut: new FormControl('')
});
constructor(public service: ISService, public route: ActivatedRoute, public dialog: MatDialog) {
this.service.loadSimpleResources("cleaning_rule", (data: SimpleResource[]) => this.rules = data);
}
clean() {
this.service.testCleaning(this.cleanForm.get("rule")?.value!, this.cleanForm.get("xmlIn")?.value!, (data: string) => this.cleanForm.get("xmlOut")?.setValue(data), this.cleanForm);
}
}

View File

@ -0,0 +1,25 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'searchFilter' })
export class FilterPipe implements PipeTransform {
/**
* Pipe filters the list of elements based on the search text provided
*
* @param items list of elements to search in
* @param searchText search string
* @returns list of elements filtered by search text or []
*/
transform(items: any[], searchText: string): any[] {
if (!items) return [];
if (!searchText) return items;
searchText = searchText.trim().toLocaleLowerCase();
return items.filter(obj => {
return Object.keys(obj).reduce((acc, curr) => {
return acc || JSON.stringify(obj[curr]).toLowerCase().includes(searchText);
}, false);
});
}
}

View File

@ -0,0 +1,249 @@
export interface ResourceType {
id: string
name: string
contentType: string
count: number
simple: boolean
}
export interface KeyValue {
k: string;
v: any;
}
export interface BrowseTerm {
term: string,
name: string,
total: number
}
export interface Module {
group: string;
name: string;
versions: string[];
files: string[];
}
export interface ProtocolParam {
name: string
label: string
type: string
optional: boolean
hasSelFunction: boolean
}
export interface Protocol {
id: string
params: ProtocolParam[]
}
export interface WfHistoryEntry {
processId: string,
name: string,
family: string,
status: string,
startDate: string,
endDate: string,
dsId?: string,
dsName?: string,
dsApi?: string,
details: Map<string, string>
}
export interface SimpleResource {
id: string,
name: string,
type: string,
subtype?: string,
description?: string,
creationDate?: string,
modificationDate?: string
}
export interface ContextParam {
name: string,
value: string
}
export interface Context {
id: string,
label: string,
parameters: ContextParam[],
nChilds: number,
type: string
}
export interface ContextNode {
id: string,
label: string,
parameters: ContextParam[],
nChilds: number,
claim: boolean,
parent: string,
populated?: boolean,
childs?: ContextNode[]
}
export interface Vocabulary {
id: string,
name: string,
description?: string
}
export interface VocabularyTermSynonym {
term: string,
encoding: string
}
export interface VocabularyTerm {
code: string,
vocabulary: string,
name: string,
encoding: string,
synonyms: VocabularyTermSynonym[]
}
export interface Api {
id: string,
protocol: string,
compliance: string,
active: boolean,
aggrDate: string,
aggrTotal: number
}
export interface ApiParam {
param: string,
value: string
}
export interface ApiInsert {
id: string,
protocol: string,
datasource: string,
contentdescription: string,
removable: boolean,
compatibility: string,
metadataIdentifierPath: string,
baseurl: string,
apiParams: ApiParam[]
};
export interface Organization {
name: string,
country: string
}
export interface Datasource {
id: string,
name: string,
otherName?: string,
nsprefix: string,
websiteUrl?: string,
type: string,
consenttermsofuse?: boolean,
fulltextdownload?: boolean,
collectedFrom: string,
organizations: Organization[],
apis: Api[]
}
export interface Page<T> {
content: T[],
totalPages: number,
totalElements: number,
size: number,
number: number
}
export interface DsmConf {
compatibilityLevels: string[],
contentDescTypes: string[],
protocols: Protocol[]
}
export interface MDStore {
id: string,
format: string,
layout: string,
type: string,
interpretation: string,
datasourceName: string,
datasourceId: string,
apiId: string,
currentVersion: string,
creationDate: string,
lastUpdate: string,
size: number,
numberOfVersions: number,
params: any
}
export interface MDStoreVersion {
id: string,
mdstore: string,
writing: boolean,
readCount: number,
lastUpdate: string,
size: number,
params: any
}
export interface MDStoreRecord {
id: string,
originalId: string,
encoding: string,
body: string,
dateOfCollection: string,
dateOfTransformation: string,
provenance: any
}
export interface EmailTemplate {
id: string,
description: string,
subject: string,
message: string
}
export interface WfSection {
id: string,
name: string
}
export interface WfConf {
id: string,
name: string,
section?: string,
details: Map<string, string>,
priority: number,
dsId?: string,
dsName?: string,
apiId?: string,
enabled: boolean,
configured: boolean,
schedulingEnabled: boolean,
cronExpression?: string,
cronMinInterval?: number,
workflow: string,
destroyWf?: string,
systemParams: Map<string, string>,
userParams: Map<string, string>
}
export interface WfParam {
name: string,
description?: string,
type?: string,
defaultValue?: any,
required: boolean
}
export interface WfSubscription {
// TODO
}
export interface WfProcessStatus {
// TODO
}

View File

@ -0,0 +1,333 @@
import { Injectable, OnInit } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Page, DsmConf, ResourceType, Protocol, WfHistoryEntry, SimpleResource, Context, ContextNode, Vocabulary, VocabularyTerm, KeyValue, BrowseTerm, Datasource, MDStore, MDStoreVersion, MDStoreRecord, EmailTemplate, WfConf, WfSubscription, WfProcessStatus, WfSection } from './is.model';
import { FormGroup } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { firstValueFrom, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ISService {
constructor(public client: HttpClient, public snackBar: MatSnackBar) {
}
loadResourceTypes(onSuccess: Function) {
this.httpGet<ResourceType[]>("/ajax/resourceTypes", onSuccess)
}
loadResourceType(id: string, onSuccess: Function) {
this.httpGet<ResourceType>("/ajax/resourceTypes/" + encodeURIComponent(id), onSuccess);
}
loadInfo(onSuccess: Function): void {
this.httpGet<any[]>("/ajax/info/", onSuccess);
}
loadProtocols(onSuccess: Function): void {
this.httpGet<Protocol[]>("/ajax/protocols/", onSuccess);
}
loadSimpleResources(type: string, onSuccess: Function): void {
this.httpGet<SimpleResource[]>("/ajax/resources/" + encodeURIComponent(type), onSuccess);
}
loadSimpleResourceContent(id: any, onSuccess: Function): void {
const headers = new HttpHeaders().set('Content-Type', 'text/plain; charset=utf-8');
this.httpGetWithOptions<string>("/ajax/resources/" + encodeURIComponent(id) + '/content', {
headers, responseType: 'text' as 'json'
}, onSuccess);
}
saveSimpleResourceMedatata(res: SimpleResource, onSuccess: Function, relatedForm?: FormGroup): void {
this.httpPost('/ajax/resources/' + encodeURIComponent(res.id) + '/metadata', res, onSuccess, relatedForm);
}
saveSimpleResourceContent(id: string, content: string, onSuccess: Function, relatedForm?: FormGroup): void {
const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
let body = new HttpParams().set('content', content);
this.httpPostWithOptions('/ajax/resources/' + encodeURIComponent(id) + '/content', body, { headers: headers }, onSuccess, relatedForm);
}
addSimpleResource(name: string, type: string, subtype: string, description: string, content: string, onSuccess: Function, relatedForm?: FormGroup): void {
const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
let body = new HttpParams()
.set('name', name)
.set('type', type)
.set('subtype', subtype)
.set('description', description)
.set('content', content);
this.httpPostWithOptions('/ajax/resources/', body, { headers: headers }, onSuccess, relatedForm);
}
deleteSimpleResource(resourceId: string, onSuccess: Function): void {
this.httpDelete('/ajax/resources/' + encodeURIComponent(resourceId), onSuccess);
}
loadWfHistory(total: number, from: number, to: number, onSuccess: Function): void {
let params = new HttpParams();
if (total && total > 0) { params = params.append('total', total); }
if (from && from > 0) { params = params.append('from', from); }
if (to && to > 0) { params = params.append('to', to); }
this.httpGetWithOptions<WfHistoryEntry[]>('/ajax/wf_history/', { params: params }, onSuccess);
}
loadWfHistoryForConf(wfConfId: string, onSuccess: Function): void {
this.httpGet<WfHistoryEntry[]>('/ajax/wf_history/byConf/' + encodeURIComponent(wfConfId), onSuccess);
}
loadContexts(onSuccess: Function): void {
this.httpGet<Context[]>('./ajax/contexts/', onSuccess);
}
loadContext(ctxId: string, onSuccess: Function): void {
this.httpGet<Context>('./ajax/contexts/' + encodeURIComponent(ctxId), onSuccess);
}
loadContextCategories(ctxId: string, onSuccess: Function): void {
this.httpGet<ContextNode[]>('./ajax/contexts/' + encodeURIComponent(ctxId) + '/categories', onSuccess);
}
loadContextConcepts(level: number, nodeId: string, onSuccess: Function): void {
this.httpGet<ContextNode[]>('./ajax/contexts/' + encodeURIComponent(level) + '/' + encodeURIComponent(nodeId) + '/concepts', onSuccess);
}
loadVocabularies(onSuccess: Function): void {
this.httpGet<Vocabulary[]>('./ajax/vocs/', onSuccess);
}
loadVocabulary(vocId: string, onSuccess: Function): void {
this.httpGet<Vocabulary>('./ajax/vocs/' + encodeURIComponent(vocId), onSuccess);
}
loadVocabularyTerms(vocId: string, onSuccess: Function): void {
this.httpGet<VocabularyTerm[]>('./ajax/vocs/' + encodeURIComponent(vocId) + '/terms', onSuccess);
}
saveVocabulary(voc: Vocabulary, onSuccess: Function, relatedForm?: FormGroup): void {
this.httpPost('./ajax/vocs/', voc, onSuccess, relatedForm);
}
saveVocabularyTerm(vocId: string, term: VocabularyTerm, onSuccess: Function, relatedForm?: FormGroup): void {
this.httpPost('./ajax/vocs/' + encodeURIComponent(vocId) + '/terms', term, onSuccess, relatedForm);
}
deleteVocabulary(vocId: string, onSuccess: Function): void {
this.httpDelete('./ajax/vocs/' + encodeURIComponent(vocId), onSuccess);
}
deleteVocabularyTerm(vocId: string, termCode: string, onSuccess: Function): void {
this.httpDelete('./ajax/vocs/'
+ encodeURIComponent(vocId)
+ '/terms/'
+ encodeURIComponent(termCode)
, onSuccess);
}
dsmConf(onSuccess: Function) {
this.httpGet<DsmConf>('./ajax/dsm/conf', onSuccess);
}
dsmBrowsableFields(onSuccess: Function) {
this.httpGet<KeyValue[]>('./ajax/dsm/browsableFields', onSuccess);
}
dsmBrowse(field: string, onSuccess: Function) {
this.httpGet<BrowseTerm[]>('./ajax/dsm/browse/' + encodeURIComponent(field), onSuccess);
}
dsmSearchByField(field: string, value: string, page: number, pageSize: number, onSuccess: Function) {
this.httpGet<Page<Datasource>>('./ajax/dsm/searchByField/' + encodeURIComponent(field) + '/' + page + '/' + pageSize + '?value=' + encodeURIComponent(value), onSuccess);
}
dsmSearch(value: string, page: number, pageSize: number, onSuccess: Function) {
this.httpGet<Page<Datasource>>('./ajax/dsm/search/' + page + '/' + pageSize + '?value=' + encodeURIComponent(value), onSuccess);
}
loadMDStores(onSuccess: Function): void {
this.httpGet<MDStore[]>("/ajax/mdstores/", onSuccess);
}
loadMDStore(mdId: string, onSuccess: Function): void {
this.httpGet<MDStore>('./ajax/mdstores/mdstore/' + encodeURIComponent(mdId), onSuccess);
}
addMDStore(format: string, layout: string, interpretation: string, type: string, dsName: string, dsId: string, apiId: string, onSuccess: Function, relatedForm?: FormGroup) {
const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
let body = new HttpParams()
.set('dsName', dsName)
.set('dsId', dsId)
.set('apiId', apiId);
this.httpPostWithOptions('/ajax/mdstores/new/'
+ encodeURIComponent(format)
+ '/'
+ encodeURIComponent(layout)
+ '/'
+ encodeURIComponent(interpretation)
+ '/'
+ encodeURIComponent(type),
body, { headers: headers }, onSuccess, relatedForm);
}
deleteMDStore(mdId: string, onSuccess: Function): void {
this.httpDelete('./ajax/mdstores/mdstore/' + encodeURIComponent(mdId), onSuccess);
}
prepareNewMDStoreVersion(mdId: string, onSuccess: Function): void {
this.httpGet<MDStoreVersion>('./ajax/mdstores/mdstore/' + encodeURIComponent(mdId) + '/newVersion', onSuccess);
}
commitMDStoreVersion(versionId: string, size: number, onSuccess: Function) {
this.httpGet<any>('./ajax/mdstores/version/' + encodeURIComponent(versionId) + '/commit/' + size, onSuccess);
}
abortMDStoreVersion(versionId: string, onSuccess: Function) {
this.httpGet<any>('./ajax/mdstores/version/' + encodeURIComponent(versionId) + '/abort', onSuccess);
}
deleteMDStoreVersion(versionId: string, onSuccess: Function) {
this.httpDelete('./ajax/mdstores/version/' + encodeURIComponent(versionId), onSuccess);
}
resetReadingMDStoreVersion(versionId: string, onSuccess: Function) {
this.httpGet<any>('./ajax/mdstores/version/' + encodeURIComponent(versionId) + '/resetReading', onSuccess);
}
loadMDStoreVersions(mdId: string, onSuccess: Function): void {
this.httpGet<MDStoreVersion[]>('./ajax/mdstores/mdstore/' + encodeURIComponent(mdId) + '/versions', onSuccess);
}
loadMDStoreVersion(versionId: string, onSuccess: Function): void {
this.httpGet<MDStoreVersion>('./ajax/mdstores/version/' + encodeURIComponent(versionId), onSuccess);
}
loadMDStoreVersionRecords(versionId: string, limit: number, onSuccess: Function): void {
this.httpGet<MDStoreRecord[]>('./ajax/mdstores/version/' + encodeURIComponent(versionId) + '/content/' + limit, onSuccess);
}
testCleaning(rule: string, xml: string, onSuccess: Function, relatedForm?: FormGroup): void {
var headers = new HttpHeaders().set('Content-Type', 'text/plain; charset=utf-8');
this.httpPostWithOptions('./ajax/mapping/clean?rule=' + encodeURIComponent(rule), xml, { headers, responseType: 'text' as 'json' }, onSuccess, relatedForm);
}
loadEmailTemplates(onSuccess: Function): void {
this.httpGet<EmailTemplate[]>('./ajax/templates/email/', onSuccess);
}
saveEmailTemplate(email: EmailTemplate, onSuccess: Function, relatedForm?: FormGroup): void {
this.httpPost('./ajax/templates/email/', email, onSuccess, relatedForm);
}
deleteEmailTemplate(id: string, onSuccess: Function): void {
this.httpDelete('./ajax/templates/email/' + encodeURIComponent(id), onSuccess);
}
loadWfSections(onSuccess: Function) {
this.httpGet<WfSection[]>("/ajax/wfs/sections", onSuccess)
}
loadWfConfigurations(sectionId: string, onSuccess: Function): void {
this.httpGet<KeyValue[]>('./ajax/wfs/sections/' + encodeURIComponent(sectionId), onSuccess);
}
loadWfConfiguration(id: string, onSuccess: Function): void {
this.httpGet<WfConf>('./ajax/wfs/conf/' + encodeURIComponent(id), onSuccess);
}
saveWfConfiguration(conf: WfConf, onSuccess: Function, relatedForm?: FormGroup): void {
this.httpPost('./ajax/wfs/conf', conf, onSuccess, relatedForm);
}
deleteWfConfiguration(id: string, onSuccess: Function): void {
this.httpDelete('./ajax/wfs/conf/' + encodeURIComponent(id), onSuccess);
}
startWfConfiguration(id: string, onSuccess: Function): void {
this.httpGet<WfProcessStatus>('./ajax/wfs/conf/' + encodeURIComponent(id) + '/start', onSuccess);
}
startDestroyWfConfiguration(id: string, onSuccess: Function): void {
this.httpGet<WfProcessStatus>('./ajax/wfs/conf/' + encodeURIComponent(id) + '/destroy', onSuccess);
}
findProcess(id: string, onSuccess: Function): void {
this.httpGet<WfProcessStatus>('./ajax/wfs/process/' + encodeURIComponent(id), onSuccess);
}
killProcess(id: string, onSuccess: Function): void {
this.httpDelete('./ajax/wfs/process/' + encodeURIComponent(id), onSuccess);
}
findWfSubscriptions(id: string, onSuccess: Function): void {
this.httpGet<WfSubscription[]>('./ajax/wfs/conf/' + encodeURIComponent(id) + '/subscriptions', onSuccess);
}
saveWfSubscriptions(id: string, subscriptions: WfSubscription[], onSuccess: Function, relatedForm?: FormGroup): void {
this.httpPost('./ajax/wfs/conf/' + encodeURIComponent(id) + '/subscriptions', subscriptions, onSuccess, relatedForm);
}
private httpGet<T>(url: string, onSuccess: Function) {
this.client.get<T>(url).subscribe({
next: data => onSuccess(data),
error: error => this.showError(error)
});
}
private httpGetWithOptions<T>(url: string, options: any, onSuccess: Function) {
this.client.get<T>(url, options).subscribe({
next: data => onSuccess(data),
error: error => this.showError(error)
});
}
private httpDelete(url: string, onSuccess: Function) {
this.client.delete<void>(url).subscribe({
next: data => onSuccess(data),
error: error => this.showError(error)
});
}
private httpPost(url: string, body: any, onSuccess: Function, relatedForm?: FormGroup) {
this.client.post<void>(url, body).subscribe({
next: data => onSuccess(data),
error: error => this.showError(error, relatedForm)
});
}
private httpPostWithOptions(url: string, body: any, options: any, onSuccess: Function, relatedForm?: FormGroup) {
this.client.post<void>(url, body, options).subscribe({
next: data => onSuccess(data),
error: error => this.showError(error, relatedForm)
});
}
private showError(error: any, form?: FormGroup) {
console.log(error);
const msg = this.errorMessage(error);
if (form) {
form.setErrors({ serverError: msg })
} else if (this.snackBar) {
this.snackBar.open(msg, 'ERROR', {
duration: 5000,
});
} else {
alert(msg);
}
}
private errorMessage(error: any) {
if (error.error && error.error.message) {
return error.error.message;
} else if (error.message) {
return error.message;
} else {
return 'Generic server side error';
}
}
}

View File

@ -0,0 +1,38 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { Observable } from 'rxjs';
import { HttpInterceptor, HttpResponse } from '@angular/common/http';
import { HttpRequest } from '@angular/common/http';
import { HttpHandler } from '@angular/common/http';
import { HttpEvent } from '@angular/common/http';
import { tap } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class SpinnerService {
visibility: BehaviorSubject<boolean>;
constructor() { this.visibility = new BehaviorSubject(false); }
show() { this.visibility.next(true); }
hide() { this.visibility.next(false); }
}
@Injectable()
export class SpinnerHttpInterceptor implements HttpInterceptor {
constructor(private spinnerService: SpinnerService) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
this.spinnerService.show();
return next.handle(req)
.pipe(tap((event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
this.spinnerService.hide();
}
}, (error) => {
this.spinnerService.hide();
}));
}
}

View File

@ -0,0 +1,16 @@
<h1 mat-dialog-title>Parameters</h1>
<div mat-dialog-content>
<p *ngIf="!data || data.length == 0">No parameters</p>
<div *ngFor="let p of data">
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%; font-size: 0.8em;">
<mat-label>{{p.name}}</mat-label>
<input matInput required readonly value="{{p.value}}" />
</mat-form-field>
</div>
</div>
<div mat-dialog-actions>
<button mat-stroked-button color="primary" mat-dialog-close>Close</button>
</div>

View File

@ -0,0 +1,57 @@
<h2>Context Viewer</h2>
<p>
<b>Context ID: </b>{{context?.id}}<br />
<b>Label: </b>{{context?.label}}<br />
<b>Type: </b>{{context?.type}}<br />
</p>
<p>
<a mat-stroked-button color="primary" [routerLink]="['/adv_resources/context']">
<mat-icon fontIcon="home"></mat-icon>
Return to contexts list
</a>
<button mat-stroked-button color="primary" (click)="showParamsDialog(context?.parameters)">
<mat-icon fontIcon="info"></mat-icon>
Global parameters
</button>
<a mat-stroked-button color="link" href="/api/contexts/{{context?.id}}" target="_blank">
<mat-icon fontIcon="download"></mat-icon>
Download
</a>
</p>
<ul>
<li *ngFor="let cat of categories" class="context-node">
<a (click)="populateNode(0, cat)" *ngIf="!(cat.populated || cat.nChilds == 0)"
style="margin-right: 0.5em;">[+{{cat.nChilds}}]</a>
<a title="{{cat.label}}" (click)="showParamsDialog(cat.parameters)" style="margin-right: 0.5em;">{{cat.id}}</a>
<span class="badge-label badge-info" *ngIf="cat.claim">claim</span>
<ul *ngIf="cat.childs && cat.childs.length > 0">
<li *ngFor="let c0 of cat.childs" class="context-node">
<a (click)="populateNode(1, c0)" *ngIf="!(c0.populated || c0.nChilds == 0)"
style="margin-right: 0.5em;">[+{{c0.nChilds}}]</a>
<a title="{{c0.label}" (click)="showParamsDialog(c0.parameters)" style="margin-right: 0.5em;">{{c0.id}}</a>
<span class="badge-label badge-info" *ngIf="c0.claim">claim</span>
<ul *ngIf="c0.childs && c0.childs.length > 0">
<li *ngFor="let c1 of c0.childs" class="context-node">
<a (click)="populateNode(2, c1)" *ngIf="!(c1.populated || c1.nChilds == 0)"
style="margin-right: 0.5em;">[+{{c1.nChilds}}]</a>
<a title="{{c1.label}}" (click)="showParamsDialog(c1.parameters)" style="margin-right: 0.5em;">{{c1.id}}</a>
<span class="badge-label badge-info" *ngIf="c1.claim">claim</span>
<ul *ngIf="c1.childs && c1.childs.length > 0">
<li *ngFor="let c2 of c1.childs" class="context-node">
<a title="{{c2.label}}" (click)="showParamsDialog(c2.parameters)"
style="margin-right: 0.5em;">{{c2.id}}</a>
<span class="badge-label badge-info" *ngIf="c2.claim">claim</span>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>

View File

@ -0,0 +1,11 @@
.context-node {
padding-top: 1em;
}
.context-node a {
text-decoration: none;
}
.context-node a:hover {
text-decoration: underline;
}

View File

@ -0,0 +1,44 @@
<h2>Contexts</h2>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%; margin-top: 10px;">
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event)" placeholder="Filter..." #input />
</mat-form-field>
<table mat-table [dataSource]="contextsDatasource" matSort class="mat-elevation-z8">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef style="width: 20%;" mat-sort-header
sortActionDescription="Sort by Context ID"> Id </th>
<td mat-cell *matCellDef="let element">
<a [routerLink]="['/ctx_viewer']" [queryParams]="{id: element.id}">{{element.id}}</a>
</td>
</ng-container>
<ng-container matColumnDef="label">
<th mat-header-cell *matHeaderCellDef style="width: 40%;" mat-sort-header
sortActionDescription="Sort by Context Label"> Label </th>
<td mat-cell *matCellDef="let element"> {{element.label}} </td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef style="width: 10%;" mat-sort-header
sortActionDescription="Sort by Context Type"> Type </th>
<td mat-cell *matCellDef="let element"> {{element.type}} </td>
</ng-container>
<ng-container matColumnDef="buttons">
<th mat-header-cell *matHeaderCellDef style="text-align: right;">Parameters</th>
<td mat-cell *matCellDef="let element" class="table-buttons">
<button mat-stroked-button color="primary" (click)="showParamsDialog(element)">show</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="colums"></tr>
<tr mat-row *matRowDef="let row; columns: colums;"></tr>
<!-- Row shown when there is no matching data. -->
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" colspan="4" style="padding: 0 16px;">No data matching the filter "{{input.value}}"</td>
</tr>
</table>

View File

@ -0,0 +1,101 @@
import { Component, Inject, AfterViewInit, OnInit, ViewChild } from '@angular/core';
import { ISService } from '../common/is.service';
import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
import { Context, ContextParam, ContextNode } from '../common/is.model';
import { ActivatedRoute, Params } from '@angular/router';
import { MatDialog, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'app-contexts',
templateUrl: './contexts.component.html',
styleUrls: ['./contexts.component.css']
})
export class ContextsComponent implements AfterViewInit, OnInit {
contextsDatasource: MatTableDataSource<Context> = new MatTableDataSource<Context>([]);
colums: string[] = ['id', 'label', 'type', 'buttons'];
@ViewChild(MatSort) sort: MatSort | undefined
constructor(public service: ISService, public route: ActivatedRoute, public dialog: MatDialog) {
}
ngOnInit() {
this.reload();
}
ngAfterViewInit() {
if (this.sort) this.contextsDatasource.sort = this.sort;
}
reload() {
this.service.loadContexts((data: Context[]) => this.contextsDatasource.data = data);
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value.trim().toLowerCase();
this.contextsDatasource.filter = filterValue;
}
showParamsDialog(context: Context): void {
const dialogRef = this.dialog.open(ContextParamsDialog, {
data: context.parameters,
width: '80%'
});
}
}
@Component({
selector: 'context-dialog',
templateUrl: 'context-params-dialog.html',
styleUrls: ['./contexts.component.css']
})
export class ContextParamsDialog {
constructor(public dialogRef: MatDialogRef<ContextParamsDialog>, @Inject(MAT_DIALOG_DATA) public data: any, public service: ISService) {
}
onNoClick(): void {
this.dialogRef.close();
}
}
@Component({
selector: 'app-context-editor',
templateUrl: './context-viewer.component.html',
styleUrls: ['./contexts.component.css']
})
export class ContextViewerComponent implements OnInit {
context?: Context
categories: ContextNode[] = [];
constructor(public service: ISService, public route: ActivatedRoute, public dialog: MatDialog) {
}
ngOnInit() {
this.route.queryParams.subscribe((params) => {
this.service.loadContext(params['id'], (data: Context) => this.context = data);
this.service.loadContextCategories(params['id'], (data: ContextNode[]) => this.categories = data);
});
}
populateNode(level: number, node: ContextNode): void {
this.service.loadContextConcepts(level, node.id, (data: ContextNode[]) => {
node.populated = true;
node.childs = data
});
}
showParamsDialog(params: ContextParam[] | undefined): void {
if (params) {
this.dialog.open(ContextParamsDialog, {
data: params,
width: '80%'
});
}
}
}

View File

@ -0,0 +1,142 @@
<form [formGroup]="addApiForm" (ngSubmit)="onSubmit()">
<h1 mat-dialog-title>Add a new API</h1>
<div mat-dialog-content>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Protocol</mat-label>
<mat-select matInput formControlName="protocol" (selectionChange)="onChangeProtocol($event)">
<mat-option *ngFor="let i of protocols" [value]="i">{{i}}</mat-option>
</mat-select>
<mat-error *ngIf="addApiForm.get('protocol')?.invalid">This field is <strong>required</strong></mat-error>
</mat-form-field>
<mat-card style="margin-top: 10px;" *ngIf="selectedProtocol">
<mat-card-header>
<mat-card-title>API Description</mat-card-title>
</mat-card-header>
<mat-card-content style="padding-top: 1em;">
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Datasource (ID: {{data.dsId}})</mat-label>
<input matInput readonly value="{{data.dsName}}" />
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 30%;">
<mat-label>Api ID (prefix)</mat-label>
<input matInput readonly value="{{apiPrefix}}" />
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 70%;">
<mat-label>Api ID (suffix)</mat-label>
<input matInput formControlName="apiIdSuffix" placeholder="example: {{selectedProtocol}}_01" />
<mat-error *ngIf="addApiForm.get('apiIdSuffix')?.invalid">This field is
<strong>required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 50%;">
<mat-label>Compatibility level</mat-label>
<mat-select matInput formControlName="compatibility">
<mat-option *ngFor="let i of compatibilityLevels" [value]="i">{{i}}</mat-option>
</mat-select>
<mat-error *ngIf="addApiForm.get('compatibility')?.invalid">This field is
<strong>required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 50%;">
<mat-label>Content description</mat-label>
<mat-select matInput formControlName="contentdescription">
<mat-option *ngFor="let i of contentDescTypes" [value]="i">{{i}}</mat-option>
</mat-select>
<mat-error *ngIf="addApiForm.get('contentdescription')?.invalid">This field is
<strong>required</strong></mat-error>
</mat-form-field>
</mat-card-content>
</mat-card>
<mat-card style="margin-top: 10px;" *ngIf="selectedProtocol">
<mat-card-header>
<mat-card-title>Configuration Parameters</mat-card-title>
</mat-card-header>
<mat-card-content style="padding-top: 1em;">
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Base URL</mat-label>
<input matInput formControlName="baseurl" />
<mat-error *ngIf="addApiForm.get('baseurl')?.invalid">
Invalid URL
<span *ngIf="addApiForm.get('baseurl')?.hasError('required')">: This field is
<strong>required</strong></span>
</mat-error>
</mat-form-field>
<ng-container *ngFor="let p of selProtParams">
<ng-container *ngIf="!p.hasSelFunction">
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;" *ngIf="p.type != 'BOOLEAN'">
<mat-label>
<b>{{selectedProtocol}}</b> - {{p.label}}
<b *ngIf="p.type == 'LIST'">(Comma separeted values)</b>
<b *ngIf="p.type == 'INT'">(Numeric)</b>
</mat-label>
<input matInput [formControlName]="paramPrefix + p.name" />
<mat-error *ngIf="addApiForm.get(paramPrefix + p.name)?.invalid">Invalid value</mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;" *ngIf="p.type == 'BOOLEAN'">
<mat-label><b>{{selectedProtocol}}</b> - {{p.label}} <b>(true/false)</b></mat-label>
<mat-select matInput [formControlName]="paramPrefix + p.name">
<mat-option></mat-option>
<mat-option value="true">true</mat-option>
<mat-option value="false">false</mat-option>
</mat-select>
<mat-error *ngIf="addApiForm.get(paramPrefix + p.name)?.invalid">Invalid value</mat-error>
</mat-form-field>
</ng-container>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;" *ngIf="p.hasSelFunction">
<mat-label><b>{{selectedProtocol}}</b> - {{p.label}} <b *ngIf="p.type == 'LIST'">(multiple
choice)</b></mat-label>
<mat-select matInput [formControlName]="paramPrefix + p.name" [multiple]="p.type == 'LIST'">
<mat-option *ngIf="p.type != 'LIST'"></mat-option>
<mat-option value="0">0 - TODO</mat-option>
<mat-option value="1">1 - TODO</mat-option>
<mat-option value="2">2 - TODO</mat-option>
<mat-option value="3">3 - TODO</mat-option>
</mat-select>
<mat-error *ngIf="addApiForm.get(paramPrefix + p.name)?.invalid">Invalid value</mat-error>
</mat-form-field>
</ng-container>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>XPath for Metatadata Identifier</mat-label>
<input matInput formControlName="metadataIdentifierPath" />
<mat-error *ngIf="addApiForm.get('metadataIdentifierPath')?.invalid">This field is
<strong>required</strong></mat-error>
</mat-form-field>
</mat-card-content>
</mat-card>
</div>
<div mat-dialog-actions>
<button mat-stroked-button color="primary" type="submit" [disabled]="!addApiForm.valid">Submit</button>
<button mat-stroked-button color="primary" mat-dialog-close>Close</button>
<mat-error *ngIf="addApiForm.errors?.['serverError']">
{{ addApiForm.errors?.['serverError'] }}
</mat-error>
</div>
</form>
<!--
<form-textfield-static label="Datasource" value="dsName"></form-textfield-static>
<form-textfield-with-prefix label="API Id" prefix="{{prefix}}" value="api.id"></form-textfield-with-prefix>
<form-select label="Compatibility level" terms="compatibilityLevels" value="api.compliance"></form-select>
<form-select label="Content description" terms="contentDescTypes" value="api.contentdescription"></form-select>
<form-select label="Protocol" terms="protocols" value="api.protocol"></form-select>
<form-textfield label="BaseURL" value="api.baseUrl" regex="^(http|https|ftp|file|sftp|jar|mongodb):\/\/"></form-textfield>
<div ng-show="api.protocol" ng-repeat="p in selProtParams">
<form-textfield label="Parameter: {{p.label}}" value="api.apiParams[p.name]" type="{{p.type}}" optional="{{p.optional}}"></form-textfield>
</div>
{{api}}
<br />
<button type="submit" class="btn btn-sm btn-primary">Add API</button>
-->

View File

@ -0,0 +1 @@
<p>dsm api page</p>

View File

@ -0,0 +1,39 @@
<h1 mat-dialog-title>{{data.label}}</h1>
<div mat-dialog-content>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%; margin-top: 10px;">
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event)" placeholder="Filter..." #input />
</mat-form-field>
<table mat-table [dataSource]="datasource" matSort class="mat-elevation-z8">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef style="width: 70%;" mat-sort-header sortActionDescription="Sort by Term">
Name</th>
<td mat-cell *matCellDef="let element">
<a [routerLink]="['/dsm/results/0/50']" [queryParams]="{field: data.field, value: element.term}"
mat-dialog-close>{{element.name}}</a>
</td>
</ng-container>
<ng-container matColumnDef="total">
<th mat-header-cell *matHeaderCellDef style="width: 30%;" mat-sort-header sortActionDescription="Sort by Total"> #
datasources </th>
<td mat-cell *matCellDef="let element"> {{element.total}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="colums"></tr>
<tr mat-row *matRowDef="let row; columns: colums;"></tr>
<!-- Row shown when there is no matching data. -->
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" colspan="2" style="padding: 0 16px;">No data matching the filter "{{input.value}}"</td>
</tr>
</table>
</div>
<div mat-dialog-actions>
<button mat-stroked-button color="primary" mat-dialog-close>Close</button>
</div>

View File

@ -0,0 +1,99 @@
<h2>Datasource Manager: Results</h2>
<p>
<span *ngIf="field">Searching for <b>{{field}}</b> = <i>"{{value}}"</i></span>
<span *ngIf="!field && value">Searching for <i>"{{value}}"</i></span>
<span *ngIf="!field && !value">Returning all the datasources</span>
</p>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%; margin-top: 10px;">
<mat-label><b>Filter in current page</b> (Total: {{(results | searchFilter: filterText).length}})</mat-label>
<input matInput [(ngModel)]="filterText" placeholder="Filter..." autofocus />
</mat-form-field>
<mat-paginator (page)="changePage($event)" [pageIndex]="currPage" [pageSize]="pageSize" [length]="nResults"
[pageSize]="pageSize" [pageSizeOptions]="[10, 25, 50, 100]" aria-label="Select page">
</mat-paginator>
<mat-card *ngFor="let r of results | searchFilter: filterText" style="margin-top: 10px;">
<mat-card-header>
<mat-card-title>{{r.name}}</mat-card-title>
<mat-card-subtitle *ngIf="r.otherName && r.name != r.otherName">{{r.otherName}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<table class="dsm-result-table">
<tr>
<th>Id</th>
<td>{{r.id}}</td>
</tr>
<tr>
<th>Type</th>
<td>{{r.type}}</td>
</tr>
<tr>
<th>Namespace Prefix</th>
<td>{{r.nsprefix}}</td>
</tr>
<tr>
<th>Collected From</th>
<td>{{r.collectedFrom}}</td>
</tr>
<tr *ngIf="r.websiteUrl">
<th>URL</th>
<td><a href="{{r.websiteUrl}}" target="_blank">{{r.websiteUrl}}</a></td>
</tr>
<tr *ngIf="r.organizations && r.organizations.length > 0">
<th>Organization(s)</th>
<td>
<span *ngFor="let o of r.organizations">
{{o.name}}
<img src="assets/images/flags/{{o.country}}.gif" title="{{o.country}}" alt="{{o.country}}"
*ngIf="o.country" />
<br />
</span>
</td>
</tr>
<tr>
<th>APIs</th>
<td>
<ng-container *ngFor="let a of r.apis">
<p *ngIf="a.id" style="border-bottom: 1px solid lightgrey;">
<a [routerLink]="['/dsm/api']" [queryParams]="{id: a.id}" style="margin-right: 0.5em;">{{a.id}}</a>
<span class="badge-label badge-default" title="protocol">{{a.protocol}}</span>
<span class="badge-label badge-info" title="compliance">{{a.compliance}}</span>
<span class="badge-label badge-success" *ngIf="a.active">active</span>
<span class="badge-label badge-failure" *ngIf="!a.active">not active</span>
<span *ngIf="a.aggrDate">
<br />
<b>Last aggregation:</b>
{{a.aggrDate}}
<b>(total: {{a.aggrTotal}})</b>
</span>
</p>
</ng-container>
<button mat-raised-button color="primary" (click)="openAddApiDialog(r.id, r.name)">add api</button>
</td>
</tr>
<tr *ngIf="r.consenttermsofuse">
<th>Consent Terms of Use</th>
<td>YES</td>
</tr>
<tr *ngIf="r.fulltextdownload">
<th>Fulltext Download</th>
<td>YES</td>
</tr>
</table>
</mat-card-content>
</mat-card>
<mat-paginator (page)="changePage($event)" [pageIndex]="currPage" [pageSize]="pageSize" [length]="nResults"
[pageSize]="pageSize" [pageSizeOptions]="[10, 25, 50, 100]" aria-label="Select page">
</mat-paginator>

View File

@ -0,0 +1,17 @@
<h2>Datasource Manager: Search</h2>
<form (ngSubmit)="search()">
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Search...</mat-label>
<input matInput [(ngModel)]="searchText" name="searchText" autofocus />
</mat-form-field>
</form>
<div>
<h4>Or browse using:</h4>
<ul>
<li *ngFor="let f of browsableFields">
<a (click)="browseField(f.k, f.v)">{{f.v}}</a>
</li>
</ul>
</div>

View File

@ -0,0 +1,21 @@
.dsm-result-table {
margin-top: 1em;
border-collapse: collapse;
}
.dsm-result-table tr:not(:last-child) {
border-bottom: 1pt solid lightgrey;
}
.dsm-result-table th,
.dsm-result-table td {
text-align: left;
font-size: 0.9em;
vertical-align: top;
}
.dsm-result-table td button {
font-size: 0.8em !important;
padding: 0 !important;
height: 2.5em !important;
}

View File

@ -0,0 +1,251 @@
import { Component, Inject, OnInit, ViewChild } from '@angular/core';
import { Page, BrowseTerm, Datasource, KeyValue, DsmConf, ProtocolParam, Api, ApiInsert } from '../common/is.model';
import { ISService } from '../common/is.service';
import { ActivatedRoute, Params } from '@angular/router';
import { MatDialog, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
import { combineLatest } from 'rxjs';
import { Router } from '@angular/router';
import { PageEvent } from '@angular/material/paginator';
import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { MatSelectChange } from '@angular/material/select';
@Component({
selector: 'app-dsm-search',
templateUrl: './dsm-search.component.html',
styleUrls: ['./dsm.component.css']
})
export class DsmSearchComponent implements OnInit {
searchText: string = '';
browsableFields: KeyValue[] = [];
constructor(public service: ISService, public route: ActivatedRoute, public router: Router, public dialog: MatDialog) {
}
ngOnInit() {
this.service.dsmBrowsableFields((data: KeyValue[]) => this.browsableFields = data);
}
search() {
this.router.navigate(['/dsm/results/0/50'], {
queryParams: { value: this.searchText }
});
}
browseField(field: string, label: string) {
const dialogRef = this.dialog.open(DsmBrowseDialog, {
data: { field: field, label: label },
width: '80%'
});
}
}
@Component({
selector: 'app-dsm-results',
templateUrl: './dsm-results.component.html',
styleUrls: ['./dsm.component.css']
})
export class DsmResultsComponent implements OnInit {
filterText: string = '';
results: Datasource[] = [];
nResults: number = 0;
currPage: number = 0;
nPages: number = 0;
pageSize: number = 0;
field?: string;
value: string = '';
constructor(public service: ISService, public route: ActivatedRoute, public router: Router, public dialog: MatDialog) {
}
ngOnInit(): void {
combineLatest([this.route.params, this.route.queryParams],
(params: Params, queryParams: Params) => ({ params, queryParams })
).subscribe((res: { params: Params; queryParams: Params }) => {
const { params, queryParams } = res;
this.currPage = parseInt(params['page']);
this.pageSize = parseInt(params['size']);
this.field = queryParams['field'];
this.value = queryParams['value'];
this.reload();
});
}
reload() {
if (this.field) {
this.service.dsmSearchByField(this.field, this.value, this.currPage, this.pageSize, (page: Page<Datasource>) => {
this.results = page.content;
this.nResults = page.totalElements;
this.nPages = page.totalPages;
});
} else {
this.service.dsmSearch(this.value, this.currPage, this.pageSize, (page: Page<Datasource>) => {
this.results = page.content;
this.nResults = page.totalElements;
this.nPages = page.totalPages;
});
}
}
changePage(event: PageEvent) {
let path = '/dsm/results/' + event.pageIndex + '/' + event.pageSize;
let qp = this.field ?
{ field: this.field, value: this.value } :
{ value: this.value };
this.router.navigate([path], {
queryParams: qp
});
}
openAddApiDialog(dsId: string, dsName: string) {
const dialogRef = this.dialog.open(DsmAddApiDialog, {
data: { dsId: dsId, dsName: dsName },
width: '80%'
});
dialogRef.afterClosed().subscribe(result => {
if (result) this.reload();
});
}
}
@Component({
selector: 'app-dsm-api',
templateUrl: './dsm-api.component.html',
styleUrls: ['./dsm.component.css']
})
export class DsmApiComponent {
}
@Component({
selector: 'dsm-browse-dialog',
templateUrl: 'dsm-browse-dialog.html',
styleUrls: ['./dsm.component.css']
})
export class DsmBrowseDialog implements OnInit {
datasource: MatTableDataSource<BrowseTerm> = new MatTableDataSource<BrowseTerm>([]);
colums: string[] = ['name', 'total'];
@ViewChild(MatSort) sort: MatSort | undefined
constructor(public dialogRef: MatDialogRef<DsmBrowseDialog>, @Inject(MAT_DIALOG_DATA) public data: any, public service: ISService) {
}
ngOnInit(): void {
this.service.dsmBrowse(this.data.field, (res: BrowseTerm[]) => this.datasource.data = res);
}
ngAfterViewInit() {
if (this.sort) this.datasource.sort = this.sort;
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value.trim().toLowerCase();
this.datasource.filter = filterValue;
}
onNoClick(): void {
this.dialogRef.close();
}
}
@Component({
selector: 'dsm-add-api-dialog',
templateUrl: './dsm-add-api.dialog.html',
styleUrls: ['./dsm.component.css']
})
export class DsmAddApiDialog {
selectedProtocol: string = '';
apiPrefix: string = '';
paramPrefix: string = '__PARAM_';
protocols: string[] = [];
compatibilityLevels: string[] = [];
contentDescTypes: string[] = [];
protocolsMap: any = {};
selProtParams: ProtocolParam[] = [];
addApiForm: FormGroup = new FormGroup({
apiIdSuffix: new FormControl('', [Validators.required]),
compatibility: new FormControl('', [Validators.required]),
contentdescription: new FormControl('', [Validators.required]),
protocol: new FormControl('', [Validators.required]),
baseurl: new FormControl('', [Validators.required, Validators.pattern('^(http|https|ftp|file|sftp|jar|mongodb):\/\/')]),
metadataIdentifierPath: new FormControl('', [Validators.required])
});
constructor(public dialogRef: MatDialogRef<DsmAddApiDialog>, @Inject(MAT_DIALOG_DATA) public data: any, public service: ISService) {
this.apiPrefix = 'api_________::' + data.dsId + '::';
this.service.dsmConf((conf: DsmConf) => {
this.compatibilityLevels = conf.compatibilityLevels;
this.contentDescTypes = conf.contentDescTypes;
conf.protocols.forEach((p) => {
this.protocols.push(p.id);
this.protocolsMap[p.id] = p.params;
});
});
}
onChangeProtocol(e: MatSelectChange): void {
this.selectedProtocol = e.value;
Object.keys(this.addApiForm.controls).forEach(k => {
if (k.startsWith(this.paramPrefix)) {
this.addApiForm.removeControl(k);
}
});
if (this.protocolsMap[e.value]) {
this.selProtParams = this.protocolsMap[e.value];
this.selProtParams.forEach(p => {
let validations: ValidatorFn[] = [];
if (!p.optional) { validations.push(Validators.required); }
if (p.type == 'INT') { validations.push(Validators.pattern('^[0-9]*$')); }
this.addApiForm.addControl(this.paramPrefix + p.name, new FormControl('', validations));
});
} else {
this.selProtParams = [];
}
}
onSubmit(): void {
let api: ApiInsert = {
id: this.apiPrefix + this.addApiForm.get('apiIdSuffix')?.value,
protocol: this.selectedProtocol,
datasource: this.data.dsId,
contentdescription: this.addApiForm.get('contentdescription')?.value,
removable: true,
compatibility: this.addApiForm.get('compatibility')?.value,
metadataIdentifierPath: this.addApiForm.get('metadataIdentifierPath')?.value,
baseurl: this.addApiForm.get('baseurl')?.value,
apiParams: []
};
Object.keys(this.addApiForm.controls).forEach(k => {
if (k.startsWith(this.paramPrefix)) {
let val = this.addApiForm.get(k)?.value;
if (val) {
api.apiParams.push({
param: k.substring(this.paramPrefix.length),
value: (Array.isArray(val)) ? val.join() : val
});
}
}
});
console.log(api);
//this.service.dsmAddApi(api, (data: void) => this.dialogRef.close(1), this.metadataForm);
}
onNoClick(): void {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,41 @@
<form [formGroup]="emailForm" (ngSubmit)="onSubmit()">
<h1 mat-dialog-title *ngIf="emailForm.get('id')?.value">Edit Email Template</h1>
<h1 mat-dialog-title *ngIf="!emailForm.get('id')?.value">New Email Template</h1>
<div mat-dialog-content>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;" *ngIf="emailForm.get('id')?.value">
<mat-label>ID</mat-label>
<input matInput formControlName="id" readonly="readonly" />
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Description</mat-label>
<input matInput formControlName="description" />
<mat-error *ngIf="emailForm.get('description')?.invalid">This field is <strong>required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Email: subject</mat-label>
<input matInput formControlName="subject" />
<mat-error *ngIf="emailForm.get('subject')?.invalid">This field is <strong>required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Email: message</mat-label>
<textarea matInput formControlName="message" required rows="16" style="font-size: 0.8em;"></textarea>
<mat-error *ngIf="emailForm.get('message')?.invalid">This field is <strong>required</strong></mat-error>
</mat-form-field>
</div>
<div mat-dialog-actions>
<button mat-stroked-button color="primary" type="submit" [disabled]="!emailForm.valid">Submit</button>
<button mat-stroked-button color="primary" mat-dialog-close>Close</button>
<mat-error *ngIf="emailForm.errors?.['serverError']">
{{ emailForm.errors?.['serverError'] }}
</mat-error>
</div>
</form>

View File

@ -0,0 +1,42 @@
<h2>Email Templates</h2>
<button mat-stroked-button color="primary" (click)="openAddEmailTemplateDialog()">
<mat-icon fontIcon="add"></mat-icon>
create a new template
</button>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%; margin-top: 10px;">
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event)" placeholder="Filter..." #input />
</mat-form-field>
<table mat-table [dataSource]="emailsDatasource" matSort class="mat-elevation-z8">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef style="width: 25%;" mat-sort-header sortActionDescription="Sort by ID"> Id
</th>
<td mat-cell *matCellDef="let element">
<a (click)="openEditEmailTemplateDialog(element)">{{element.id}}</a>
</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by Description"> Description </th>
<td mat-cell *matCellDef="let element"> {{element.description}} </td>
</ng-container>
<ng-container matColumnDef="buttons">
<th mat-header-cell *matHeaderCellDef style="text-align: right;" style="width: 20%"></th>
<td mat-cell *matCellDef="let element" class="table-buttons">
<button mat-stroked-button color="warn" (click)="deleteEmailTemplate(element)">delete</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="colums"></tr>
<tr mat-row *matRowDef="let row; columns: colums;"></tr>
<!-- Row shown when there is no matching data. -->
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" colspan="4" style="padding: 0 16px;">No data matching the filter "{{input.value}}"</td>
</tr>
</table>

View File

@ -0,0 +1,98 @@
import { AfterViewInit, Component, Inject, OnInit, ViewChild } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute } from '@angular/router';
import { EmailTemplate } from '../common/is.model';
import { ISService } from '../common/is.service';
@Component({
selector: 'app-emails',
templateUrl: './emails.component.html',
styleUrls: ['./emails.component.css']
})
export class EmailsComponent implements OnInit, AfterViewInit {
emailsDatasource: MatTableDataSource<EmailTemplate> = new MatTableDataSource<EmailTemplate>([]);
colums: string[] = ['id', 'description', 'buttons'];
@ViewChild(MatSort) sort: MatSort | undefined
searchText: string = '';
constructor(public service: ISService, public route: ActivatedRoute, public dialog: MatDialog) { }
ngOnInit() { this.reload() }
ngAfterViewInit() { if (this.sort) this.emailsDatasource.sort = this.sort; }
reload() { this.service.loadEmailTemplates((data: EmailTemplate[]) => this.emailsDatasource.data = data); }
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value.trim().toLowerCase();
this.emailsDatasource.filter = filterValue;
}
openAddEmailTemplateDialog(): void {
const dialogRef = this.dialog.open(EmailDialog, {
data: {
id: '',
description: '',
subject: '',
message: ''
},
width: '80%'
});
dialogRef.afterClosed().subscribe(result => {
if (result) this.reload();
});
}
openEditEmailTemplateDialog(email: EmailTemplate): void {
const dialogRef = this.dialog.open(EmailDialog, {
data: email,
width: '80%'
});
dialogRef.afterClosed().subscribe(result => {
if (result) this.reload();
});
}
deleteEmailTemplate(email: EmailTemplate) {
if (confirm('Are you sure?')) {
this.service.deleteEmailTemplate(email.id, (data: void) => this.reload());
}
}
}
@Component({
selector: 'email-dialog',
templateUrl: './email-dialog.html',
styleUrls: ['./emails.component.css']
})
export class EmailDialog {
emailForm = new FormGroup({
id: new FormControl(''),
description: new FormControl('', [Validators.required]),
subject: new FormControl('', [Validators.required]),
message: new FormControl('', [Validators.required])
});
constructor(public dialogRef: MatDialogRef<EmailDialog>, @Inject(MAT_DIALOG_DATA) public data: any, public service: ISService) {
this.emailForm.get('id')?.setValue(data.id);
this.emailForm.get('description')?.setValue(data.description);
this.emailForm.get('subject')?.setValue(data.subject);
this.emailForm.get('message')?.setValue(data.message);
}
onSubmit(): void {
const email = Object.assign({}, this.data, this.emailForm.value);
this.service.saveEmailTemplate(email, (data: void) => this.dialogRef.close(1), this.emailForm);
}
onNoClick(): void {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,66 @@
<h2>Container Info</h2>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event)" placeholder="Filter..." #input />
</mat-form-field>
<div *ngFor="let section of kvDatasources">
<div style="margin-top: 2em;" *ngIf="section.datasource.filteredData.length > 0">
<h3>{{section.name}}</h3>
<table mat-table [dataSource]="section.datasource" class="mat-elevation-z8">
<ng-container matColumnDef="k">
<th mat-header-cell *matHeaderCellDef> Property </th>
<td mat-cell *matCellDef="let element" style="width: 30%"> {{element.k}} </td>
</ng-container>
<ng-container matColumnDef="v">
<th mat-header-cell *matHeaderCellDef> Value </th>
<td mat-cell *matCellDef="let element" style="width: 70%"> {{element.v}} </td>
</ng-container>
<tr mat-row *matRowDef="let row; columns: displayedKVColumns;"></tr>
<!-- Row shown when there is no matching data. -->
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" colspan="2" style="padding: 0 16px;">No data matching the filter "{{input.value}}"</td>
</tr>
</table>
</div>
</div>
<div style="margin-top: 3em;" *ngIf="moduleDatasource.filteredData.length > 0">
<h3>Modules</h3>
<table mat-table [dataSource]="moduleDatasource" class="mat-elevation-z8">
<ng-container matColumnDef="group">
<th mat-header-cell *matHeaderCellDef style="width: 15%;"> Group </th>
<td mat-cell *matCellDef="let element"> {{element.group}} </td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef style="width: 15%;"> Name </th>
<td mat-cell *matCellDef="let element"> {{element.name}} </td>
</ng-container>
<ng-container matColumnDef="versions">
<th mat-header-cell *matHeaderCellDef style="width: 10%;"> Versions </th>
<td mat-cell *matCellDef="let element"> <span *ngFor="let v of element.versions">{{v}}<br /></span> </td>
</ng-container>
<ng-container matColumnDef="files">
<th mat-header-cell *matHeaderCellDef style="width: 60%;"> Files </th>
<td mat-cell *matCellDef="let element"> <span *ngFor="let f of element.files">{{f}}<br /></span> </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedModuleColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedModuleColumns;"></tr>
<!-- Row shown when there is no matching data. -->
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" colspan="4" style="padding: 0 16px;">No data matching the filter "{{input.value}}"</td>
</tr>
</table>
</div>

View File

@ -0,0 +1,45 @@
import { Component } from '@angular/core';
import { ISService } from '../common/is.service';
import { MatTableDataSource } from '@angular/material/table';
import { Module } from '../common/is.model';
export interface KeyValueDatasource {
name: string;
datasource: MatTableDataSource<any>;
}
@Component({
selector: 'app-info',
templateUrl: './info.component.html',
styleUrls: ['./info.component.css']
})
export class InfoComponent {
kvDatasources: KeyValueDatasource[] = [];
moduleDatasource: MatTableDataSource<Module> = new MatTableDataSource<Module>([]);
displayedKVColumns: string[] = ['k', 'v'];
displayedModuleColumns: string[] = ['group', 'name', 'versions', 'files'];
constructor(public service: ISService) {
this.service.loadInfo((data: any[]) => {
data.forEach(section => {
if (section['name'] == 'Modules') {
this.moduleDatasource.data = section['data'];
} else {
this.kvDatasources.push({
name: section['name'],
datasource: new MatTableDataSource(section['data'])
});
}
})
});
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value.trim().toLowerCase();
this.kvDatasources.forEach(s => s.datasource.filter = filterValue)
this.moduleDatasource.filter = filterValue;
}
}

View File

@ -0,0 +1,50 @@
.menu-count {
padding-top: 0.1em;
padding-bottom: 0.1em;
padding-left: 0.4em;
padding-right: 0.4em;
border-radius: 1em;
color: #fff;
background-color: cornflowerblue;
text-align: center;
}
.collapse-buttons {
text-align: right;
}
.collapse-buttons button {
font-size: 0.6em;
}
.mat-expansion-panel-spacing {
margin: 0;
}
.menu-item {
padding-top: 0.3em;
padding-bottom: 0.3em;
padding-left: 7%;
padding-right: 3%;
font-size: 0.9em;
width: 90%;
color: #696969;
text-decoration: none !important;
display: inline-block;
}
.menu-item:hover {
color: #999;
background-color: #eaeaea;
}
.menu-count {
padding-left: 0.5em;
padding-right: 0.5em;
border-radius: 1em;
font-size: 0.7em;
color: #fff;
text-align: center;
float: right;
width: 2em;
}

View File

@ -0,0 +1,92 @@
<div class="collapse-buttons">
<button mat-button color="primary" (click)="accordion.openAll()">Expand All</button>
<button mat-button color="primary" (click)="accordion.closeAll()">Collapse All</button>
</div>
<mat-accordion multi>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Home</mat-panel-title>
</mat-expansion-panel-header>
<a class="menu-item" routerLink="">Home</a>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Datasources</mat-panel-title>
</mat-expansion-panel-header>
<div>
<a class="menu-item" routerLink="dsm/search">Search</a>
</div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Simple Resources</mat-panel-title>
</mat-expansion-panel-header>
<div>
<ng-container *ngFor="let r of resTypes">
<div *ngIf="r.simple">
<a class="menu-item" [routerLink]="['resources/' + r.id]">
{{r.name}}
<span class="menu-count">{{r.count}}</span>
</a>
</div>
</ng-container>
</div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Advanced Resources</mat-panel-title>
</mat-expansion-panel-header>
<div>
<ng-container *ngFor="let r of resTypes">
<div *ngIf="!r.simple">
<a class="menu-item" [routerLink]="['adv_resources/' + r.id]">
{{r.name}}
<span class="menu-count">{{r.count}}</span>
</a>
</div>
</ng-container>
</div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Workflows</mat-panel-title>
</mat-expansion-panel-header>
<div>
<div *ngFor="let s of wfSections">
<a class="menu-item" [routerLink]="['wfs', s.id]">{{s.name}}</a>
</div>
</div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Tools</mat-panel-title>
</mat-expansion-panel-header>
<div>
<a class="menu-item" routerLink="mdstores">MDStore Inspector</a>
<a class="menu-item" routerLink="cleaner">Cleaner Tester</a>
</div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Logs</mat-panel-title>
</mat-expansion-panel-header>
<div>
<a class="menu-item" routerLink="wf_history">Workflow History</a>
</div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Info</mat-panel-title>
</mat-expansion-panel-header>
<div>
<a class="menu-item" routerLink="info">Container Info</a>
</div>
</mat-expansion-panel>
</mat-accordion>

View File

@ -0,0 +1,24 @@
import { Component, ViewChild } from '@angular/core';
import { KeyValue, ResourceType, WfSection } from '../common/is.model';
import { ISService } from '../common/is.service';
import { MatAccordion } from '@angular/material/expansion';
@Component({
selector: 'app-main-menu-panels',
templateUrl: './main-menu-panels.component.html',
styleUrls: ['./main-menu-panels.component.css']
})
export class MainMenuPanelsComponent {
@ViewChild(MatAccordion)
accordion!: MatAccordion;
resTypes: ResourceType[] = [];
wfSections: WfSection[] = [];
constructor(public service: ISService) {
service.loadResourceTypes((data: ResourceType[]) => this.resTypes = data);
service.loadWfSections((data: WfSection[]) => this.wfSections = data);
}
}

View File

@ -0,0 +1,61 @@
<form [formGroup]="newMdstoreForm" (ngSubmit)="onSubmit()">
<h1 mat-dialog-title>New MDStore</h1>
<div mat-dialog-content>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Format</mat-label>
<input matInput formControlName="format" />
<mat-error *ngIf="newMdstoreForm.get('format')?.invalid">This field is <strong>required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Layout</mat-label>
<input matInput formControlName="layout" />
<mat-error *ngIf="newMdstoreForm.get('layout')?.invalid">This field is <strong>required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Interpretation</mat-label>
<input matInput formControlName="interpretation" />
<mat-error *ngIf="newMdstoreForm.get('interpretation')?.invalid">This field is
<strong>required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Type</mat-label>
<mat-select matInput formControlName="type">
<mat-option value="HDFS">hdfs</mat-option>
<mat-option value="MOCK">mock</mat-option>
</mat-select>
<mat-error *ngIf="newMdstoreForm.get('type')?.invalid">This field is
<strong>required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Datasource Name</mat-label>
<input matInput formControlName="dsName" />
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Datasource ID</mat-label>
<input matInput formControlName="dsId" />
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>API ID</mat-label>
<input matInput formControlName="apiId" />
</mat-form-field>
</div>
<div mat-dialog-actions>
<button mat-stroked-button color="primary" type="submit" [disabled]="!newMdstoreForm.valid">Submit</button>
<button mat-stroked-button color="primary" mat-dialog-close>Close</button>
<mat-error *ngIf="newMdstoreForm.errors?.['serverError']">
{{ newMdstoreForm.errors?.['serverError'] }}
</mat-error>
</div>
</form>

View File

@ -0,0 +1,95 @@
<h2>Metadata Store Inspector</h2>
<p>
<a mat-stroked-button color="primary" [routerLink]="['/mdstores']">
<mat-icon fontIcon="home"></mat-icon>
return to mdstore list
</a>
</p>
<table class="mdstore-table">
<tr>
<th rowspan="4" class="col-xs-1" style="width: 10em">MdStore</th>
<th style="width: 20em">ID</th>
<td>{{mdstore?.id}}</td>
</tr>
<tr>
<th>Type</th>
<td>{{mdstore?.type}}</td>
</tr>
<tr>
<th>Format / Layout / Interpretation</th>
<td>{{mdstore?.format}} / {{mdstore?.layout}} / {{mdstore?.interpretation}}</td>
</tr>
<tr>
<th>Related Datasource</th>
<td>{{mdstore?.datasourceName}}</td>
</tr>
<tr>
<th [attr.rowspan]="(version?.params | keyvalue).length + 3">Version</th>
<th>ID</th>
<td>
<span *ngIf="version?.id == mdstore?.currentVersion" class="badge-label badge-success">current</span>
<span *ngIf="version?.writing && (version?.id != mdstore?.currentVersion)"
class="badge-label badge-warning">writing</span>
<span *ngIf="!version?.writing && (version?.id != mdstore?.currentVersion)"
class=" badge-label badge-failure">expired</span>
{{version?.id}}
</td>
</tr>
<tr *ngFor="let e of version?.params | keyvalue">
<th>Parameter {{e.key}}</th>
<td>{{e.value}}</td>
</tr>
<tr>
<th>Last Update</th>
<td>{{version?.lastUpdate}}</td>
</tr>
<tr>
<th>Size</th>
<td>{{version?.size}}</td>
</tr>
</table>
<h3 *ngIf="records.length > 0" class="muted" style="margin-top: 2em; text-align: center;">The display is limited to the
first {{limit}}
records</h3>
<mat-card *ngFor="let rec of records" style="margin-top: 1em;">
<mat-card-header>
<mat-card-title *ngIf="!rec.id">the record is unreadable</mat-card-title>
<mat-card-title *ngIf="rec.id">{{rec.id}}</mat-card-title>
</mat-card-header>
<mat-card-content>
<br />
<p *ngIf="!rec.id">Invalid record format</p>
<table class="mdstore-table" *ngIf="rec.id">
<tr>
<th style="width: 15em;">Original Id</th>
<td>{{rec.originalId}}</td>
</tr>
<tr>
<th>Collected on</th>
<td>{{rec.dateOfCollection | date:'medium'}}</td>
</tr>
<tr>
<th>Transformed on</th>
<td>{{rec.dateOfTransformation | date:'medium'}}</td>
</tr>
<tr>
<th>Provenance</th>
<td class="small">
<span *ngIf="rec.provenance.datasourceName"><b>Datasource Name</b>:
{{rec.provenance.datasourceName}}<br /></span>
<span *ngIf="rec.provenance.datasourceId"><b>Datasource ID</b>: {{rec.provenance.datasourceId}}<br /></span>
<span *ngIf="rec.provenance.nsPrefix"><b>Prefix</b>: {{rec.provenance.nsPrefix}}<br /></span>
</td>
</tr>
<tr>
<th>Format</th>
<td>{{rec.encoding}}</td>
</tr>
</table>
<pre class="small">{{rec.body}}</pre>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,48 @@
<h1 mat-dialog-title>MDStore Versions</h1>
<div mat-dialog-content>
<div style="text-align: right;">
<button mat-stroked-button color="primary" (click)="reload()">refresh</button>
</div>
<table class="mdstore-table small">
<thead>
<tr>
<th style="width: 50%">ID</th>
<th style="width: 20%; text-align: center;">Read Count</th>
<th style="width: 20%; text-align: center;">Last Update</th>
<th style="width: 10%; text-align: right;">Size</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let v of versions" [ngClass]="{'active-row': v.id == data.currentVersion}">
<td title="{{v.id}}">
<mat-icon fontIcon="edit" *ngIf="v.writing" title="writing..."></mat-icon>
<b>{{v.id}}</b>
<br />
<span class="small" *ngFor="let e of v.params | keyvalue">
<b>{{e.key}}</b>:
{{e.value}}<br />
</span>
<button mat-stroked-button color="primary" (click)="openInspectorPage(v)">inspect</button>
<button mat-stroked-button color="basic" *ngIf="v.writing" (click)="commitVersion(v)">commit</button>
<button mat-stroked-button color="warn" *ngIf="v.writing" (click)="abortVersion(v)">abort</button>
<button mat-stroked-button color="warn" *ngIf="!v.writing && v.readCount == 0 && v.id != data.currentVersion"
(click)="deleteVersion(v)">delete</button>
</td>
<td style="text-align: center;">
{{v.readCount}}
<button mat-stroked-button color="primary" (click)="resetReading(v)"
[disabled]="v.readCount == 0">reset</button>
</td>
<td style="text-align: center;" title="{{v.lastUpdate}}">{{v.lastUpdate | date:"MMM dd,
yyyy 'at' HH:mm"}}</td>
<td style="text-align: right;">{{v.size}}</td>
</tr>
</tbody>
</table>
</div>
<div mat-dialog-actions>
<button mat-stroked-button color="primary" mat-dialog-close>Close</button>
</div>

View File

@ -0,0 +1,34 @@
.mdstore-table {
border-collapse: collapse;
border-bottom: 1pt solid lightgrey;
}
.mdstore-table tr {
border-top: 1pt solid lightgrey;
}
.mdstore-table th,
.mdstore-table td {
text-align: left;
font-size: 0.9em;
vertical-align: top;
padding-left: 1em;
padding-right: 1em;
}
.mdstore-table td button,
.mdstore-table td a.mdc-button {
font-size: 0.8em !important;
padding: 0 !important;
height: 2.5em !important;
}
.mdstore-table tr.active-row {
background-color: #daffda;
}
.mdstore-table mat-icon {
width: 1em;
height: 1em;
font-size: 1em;
}

View File

@ -0,0 +1,73 @@
<h2>Metadata Stores</h2>
<p>
<button mat-stroked-button color="primary" (click)="openAddMdstoreDialog()">
<mat-icon fontIcon="add"></mat-icon>
create a new mdstore
</button>
<button mat-stroked-button color="primary" (click)="reload()">
<mat-icon fontIcon="refresh"></mat-icon>
refresh
</button>
</p>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%; margin-top: 10px;">
<mat-label><b>Filter</b> (Total: {{(mdstores | searchFilter: searchText).length}})</mat-label>
<input matInput [(ngModel)]="searchText" placeholder="Filter..." autofocus />
</mat-form-field>
<mat-card *ngFor="let md of mdstores | searchFilter: searchText" style="margin-top: 10px;">
<mat-card-header>
<mat-card-title>{{md.id}}</mat-card-title>
<mat-card-subtitle>{{md.datasourceName}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content style="padding-top: 1em; padding-bottom: 1em;">
<table class="mdstore-table">
<tr>
<th style="width: 30%">Format / Layout / Interpretation</th>
<td style="width: 70%">{{md.format}} / {{md.layout}} / {{md.interpretation}}</td>
</tr>
<tr>
<th>Datasource</th>
<td>
<b>Name:</b> {{md.datasourceName}}<br />
<b>ID:</b> {{md.datasourceId}}<br />
<b>API:</b> {{md.apiId}}
</td>
</tr>
<tr>
<th>Creation Date</th>
<td>{{md.creationDate}}</td>
</tr>
<tr>
<th>Last Update</th>
<td>{{md.lastUpdate}}</td>
</tr>
<tr>
<th>Size</th>
<td>{{md.size}}</td>
</tr>
<tr>
<th>Type</th>
<td>{{md.type}}</td>
</tr>
<tr *ngFor="let e of md.params | keyvalue">
<th>Parameter {{e.key}}</th>
<td>{{e.value}}</td>
</tr>
<tr>
<th>Versions</th>
<td>
<a (click)="openVersionsDialog(md)">{{md.numberOfVersions}} version(s)</a>
/
<a (click)="createNewVersion(md)">prepare new version</a>
</td>
</tr>
</table>
</mat-card-content>
<mat-card-actions>
<a [routerLink]="['/mdrecords/' + md.currentVersion + '/50']" mat-stroked-button color="primary">inspect</a>
<button mat-stroked-button color="warn" (click)="deleteMdstore(md)">delete</button>
<button mat-stroked-button color="info">zeppelin</button>
</mat-card-actions>
</mat-card>

View File

@ -0,0 +1,179 @@
import { Component, Inject, OnInit } from '@angular/core';
import { ISService } from '../common/is.service';
import { ActivatedRoute, Router } from '@angular/router';
import { MatDialog, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MDStore, MDStoreRecord, MDStoreVersion } from '../common/is.model';
import { FormControl, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-mdstores',
templateUrl: './mdstores.component.html',
styleUrls: ['./mdstores.component.css']
})
export class MdstoresComponent implements OnInit {
mdstores: MDStore[] = [];
searchText: string = '';
constructor(public service: ISService, public route: ActivatedRoute, public dialog: MatDialog) { }
ngOnInit() { this.reload() }
reload() { this.service.loadMDStores((data: MDStore[]) => this.mdstores = data); }
openVersionsDialog(md: MDStore): void {
const dialogRef = this.dialog.open(MDStoreVersionsDialog, {
data: md,
width: '80%'
});
dialogRef.afterClosed().subscribe(result => {
if (result) this.reload();
});
}
openAddMdstoreDialog(): void {
const dialogRef = this.dialog.open(AddMDStoreDialog, {
data: {},
width: '80%'
});
dialogRef.afterClosed().subscribe(result => {
if (result) this.reload();
});
}
createNewVersion(md: MDStore): void {
this.service.prepareNewMDStoreVersion(md.id, (data: MDStoreVersion) => {
md.numberOfVersions = md.numberOfVersions + 1;
this.openVersionsDialog(md);
});
}
deleteMdstore(md: MDStore) {
if (confirm('Are you sure?')) {
this.service.deleteMDStore(md.id, (data: void) => this.reload());
}
}
}
@Component({
selector: 'app-mdstore-inspector',
templateUrl: './mdstore-inspector.component.html',
styleUrls: ['./mdstores.component.css']
})
export class MdstoreInspectorComponent implements OnInit {
mdstore?: MDStore = undefined;
version?: MDStoreVersion = undefined;
records: MDStoreRecord[] = [];
limit: number = 0;
constructor(public service: ISService, public route: ActivatedRoute, public dialog: MatDialog) {
}
ngOnInit() {
this.route.params.subscribe(params => {
const versionId = params['versionId'];
this.limit = params['limit'];
this.service.loadMDStoreVersion(versionId, (data: MDStoreVersion) => {
this.version = data;
this.service.loadMDStore(this.version.mdstore, (data: MDStore) => {
this.mdstore = data;
this.service.loadMDStoreVersionRecords(versionId, this.limit, (data: MDStoreRecord[]) => {
this.records = data;
});
});
});
});
}
}
@Component({
selector: 'mdstores-versions-dialog',
templateUrl: './mdstores-versions-dialog.html',
styleUrls: ['./mdstores.component.css']
})
export class MDStoreVersionsDialog {
versions: MDStoreVersion[] = [];
constructor(public dialogRef: MatDialogRef<MDStoreVersionsDialog>, @Inject(MAT_DIALOG_DATA) public data: any, public service: ISService, public router: Router) {
this.reload();
}
reload() {
this.service.loadMDStoreVersions(this.data.id, (res: MDStoreVersion[]) => this.versions = res);
}
openInspectorPage(version: MDStoreVersion): void {
const url = this.router.serializeUrl(
this.router.createUrlTree(['/mdrecords/' + encodeURIComponent(version.id) + '/50'])
);
window.open(url, '_blank');
}
resetReading(version: MDStoreVersion): void {
this.service.resetReadingMDStoreVersion(version.id, (data: void) => version.readCount = 0);
}
commitVersion(version: MDStoreVersion): void {
let size: number = parseInt(prompt("New Size", "0") || "0");
if (size >= 0) {
this.service.commitMDStoreVersion(version.id, size, (data: void) => {
this.data.currentVersion = version.id;
this.reload();
});
}
}
abortVersion(version: MDStoreVersion): void {
this.service.abortMDStoreVersion(version.id, (data: void) => this.reload());
}
deleteVersion(version: MDStoreVersion): void {
this.service.deleteMDStoreVersion(version.id, (data: void) => this.reload());
}
onNoClick(): void {
this.dialogRef.close();
}
}
@Component({
selector: 'add-mdstore-dialog',
templateUrl: './add-mdstore-dialog.html',
styleUrls: ['./mdstores.component.css']
})
export class AddMDStoreDialog {
newMdstoreForm = new FormGroup({
format: new FormControl('', [Validators.required]),
layout: new FormControl('', [Validators.required]),
interpretation: new FormControl('', [Validators.required]),
type: new FormControl('', [Validators.required]),
dsName: new FormControl(''),
dsId: new FormControl(''),
apiId: new FormControl(''),
});
constructor(public dialogRef: MatDialogRef<MDStoreVersionsDialog>, @Inject(MAT_DIALOG_DATA) public data: any, public service: ISService) {
}
onSubmit(): void {
let format: string = this.newMdstoreForm.get('format')?.value!;
let layout: string = this.newMdstoreForm.get('layout')?.value!;
let interpretation: string = this.newMdstoreForm.get('interpretation')?.value!;
let type: string = this.newMdstoreForm.get('type')?.value!;
let dsName: string = this.newMdstoreForm.get('dsName')?.value!;
let dsId: string = this.newMdstoreForm.get('dsId')?.value!;
let apiId: string = this.newMdstoreForm.get('apiId')?.value!;
this.service.addMDStore(format, layout, interpretation, type, dsName, dsId, apiId, (data: void) => this.dialogRef.close(1), this.newMdstoreForm);
}
onNoClick(): void {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,45 @@
<h2>Harvesting Protocols</h2>
<div style="margin-top: 3em;" *ngFor="let prot of protDatasources">
<h3>{{prot.protocol}}</h3>
<table mat-table [dataSource]="prot.datasource" class="mat-elevation-z8">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Name </th>
<td mat-cell *matCellDef="let element"> {{element.name}} </td>
</ng-container>
<ng-container matColumnDef="label">
<th mat-header-cell *matHeaderCellDef> Label </th>
<td mat-cell *matCellDef="let element"> {{element.label}} </td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef> Type </th>
<td mat-cell *matCellDef="let element"> {{element.type}} </td>
</ng-container>
<ng-container matColumnDef="optional">
<th mat-header-cell *matHeaderCellDef> Required </th>
<td mat-cell *matCellDef="let element">
<mat-icon fontIcon="check" *ngIf="!element.optional"></mat-icon>
</td>
</ng-container>
<ng-container matColumnDef="hasSelFunction">
<th mat-header-cell *matHeaderCellDef> Has Sel Function </th>
<td mat-cell *matCellDef="let element">
<mat-icon fontIcon="check" *ngIf="element.hasSelFunction"></mat-icon>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="colums"></tr> 
<tr mat-row *matRowDef="let row; columns: colums;"></tr>
<!-- Row shown when there is no matching data. -->
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" colspan="5" style="padding: 0 16px;">No parameters</td>
</tr>
</table>
</div>

View File

@ -0,0 +1,31 @@
import { Component } from '@angular/core';
import { ISService } from '../common/is.service';
import { MatTableDataSource } from '@angular/material/table';
import { Protocol, ProtocolParam } from '../common/is.model';
export interface ProtocolDatasource {
protocol: string;
datasource: MatTableDataSource<ProtocolParam>;
}
@Component({
selector: 'app-protocols',
templateUrl: './protocols.component.html',
styleUrls: ['./protocols.component.css']
})
export class ProtocolsComponent {
protDatasources: ProtocolDatasource[] = [];
colums: string[] = ['name', 'label', 'type', 'optional', 'hasSelFunction'];
constructor(public service: ISService) {
this.service.loadProtocols((data: Protocol[]) =>
data.forEach(p => {
this.protDatasources.push({
protocol: p.id,
datasource: new MatTableDataSource(p.params)
});
})
);
}
}

View File

@ -0,0 +1,27 @@
<form [formGroup]="contentForm" (ngSubmit)="onSubmit()">
<h1 mat-dialog-title>Edit content</h1>
<div mat-dialog-content>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>ID</mat-label>
<input matInput readonly value="{{data.id}}" />
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Content ({{data.contentType}})</mat-label>
<textarea matInput formControlName="content" required rows="16" style="font-size: 0.8em;"></textarea>
<mat-error *ngIf="contentForm.get('content')?.invalid">This field is <strong>required</strong></mat-error>
</mat-form-field>
</div>
<div mat-dialog-actions>
<button mat-stroked-button color="primary" type="submit" [disabled]="!contentForm.valid">Submit</button>
<button mat-stroked-button color="primary" mat-dialog-close>Close</button>
<mat-error *ngIf="contentForm.errors?.['serverError']">
{{ contentForm.errors?.['serverError'] }}
</mat-error>
</div>
</form>

View File

@ -0,0 +1,41 @@
<form [formGroup]="metadataForm" (ngSubmit)="onSubmit()">
<h1 mat-dialog-title>Edit metadata</h1>
<div mat-dialog-content>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>ID</mat-label>
<input matInput readonly value="{{data.id}}" />
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Type</mat-label>
<input matInput readonly value="{{data.type}}" />
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>SubType (optional)</mat-label>
<input matInput formControlName="subtype" />
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Name</mat-label>
<input matInput formControlName="name" />
<mat-error *ngIf="metadataForm.get('name')?.invalid">This field is <strong>required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Description</mat-label>
<textarea matInput formControlName="description" rows="8"></textarea>
</mat-form-field>
</div>
<div mat-dialog-actions>
<button mat-stroked-button color="primary" type="submit" [disabled]="!metadataForm.valid">Submit</button>
<button mat-stroked-button color="primary" mat-dialog-close>Close</button>
<mat-error *ngIf="metadataForm.errors?.['serverError']">
{{ metadataForm.errors?.['serverError'] }}
</mat-error>
</div>
</form>

View File

@ -0,0 +1,38 @@
<form [formGroup]="newResourceForm" (ngSubmit)="onSubmit()">
<h1 mat-dialog-title>New Resource</h1>
<div mat-dialog-content>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Name</mat-label>
<input matInput formControlName="name" />
<mat-error *ngIf="newResourceForm.get('name')?.invalid">This field is <strong>required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>SubType (optional)</mat-label>
<input matInput formControlName="subtype" />
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Content ({{data.contentType}})</mat-label>
<textarea matInput formControlName="content" rows="10" style="font-size: 0.8em;"></textarea>
<mat-error *ngIf="newResourceForm.get('content')?.invalid">This field is <strong>required</strong></mat-error>
</mat-form-field>
</div>
<div mat-dialog-actions>
<button mat-stroked-button color="primary" type="submit" [disabled]="!newResourceForm.valid">Submit</button>
<button mat-stroked-button color="primary" mat-dialog-close>Close</button>
<mat-error *ngIf="newResourceForm.errors?.['serverError']">
{{ newResourceForm.errors?.['serverError'] }}
</mat-error>
</div>
</form>

View File

@ -0,0 +1,47 @@
<h2>{{type?.name}}</h2>
<button mat-stroked-button color="primary" (click)="openNewDialog()">
<mat-icon fontIcon="add"></mat-icon>
create a new resource
</button>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%; margin-top: 10px;">
<mat-label><b>Filter</b> (Total: {{(resources | searchFilter: searchText).length}})</mat-label>
<input matInput [(ngModel)]="searchText" placeholder="Filter..." autofocus />
</mat-form-field>
<mat-card *ngFor="let r of resources | searchFilter: searchText" style="margin-top: 10px;">
<mat-card-header>
<mat-card-title title="{{r.id}}">
{{r.name}}
<span class="badge-label badge-warning" style="font-size: 0.7em;" *ngIf="r.subtype">{{r.subtype}}</span>
<span class="badge-label badge-info" style="font-size: 0.7em;">{{type?.contentType}}</span>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<p>{{r.description}}</p>
<p class="muted small">
<b>Id:</b> {{r.id}}<br />
<b>Creation date:</b> {{r.creationDate}}<br />
<b>Modification date:</b> {{r.modificationDate}}
</p>
</mat-card-content>
<mat-card-actions>
<button mat-stroked-button color="primary" (click)="openMetadataDialog(r)">
<mat-icon fontIcon="edit"></mat-icon>
edit metadata
</button>
<button mat-stroked-button color="primary" (click)="openContentDialog(r)">
<mat-icon fontIcon="edit"></mat-icon>
edit content
</button>
<a href="./api/resources/{{r.id}}/content" mat-stroked-button color="link" target="_blank">
<mat-icon fontIcon="code"></mat-icon>
raw content
</a>
<button mat-stroked-button color="warn" (click)="deleteResource(r)">
<mat-icon fontIcon="delete"></mat-icon>
delete
</button>
</mat-card-actions>
</mat-card>

View File

@ -0,0 +1,170 @@
import { Component, Inject, OnInit } from '@angular/core';
import { ISService } from '../common/is.service';
import { ActivatedRoute } from '@angular/router';
import { MatDialog, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ResourceType, SimpleResource } from '../common/is.model';
import { FormControl, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-resources',
templateUrl: './resources.component.html',
styleUrls: ['./resources.component.css']
})
export class ResourcesComponent implements OnInit {
typeId: string = '';
type?: ResourceType;
resources: SimpleResource[] = [];
searchText: string = '';
constructor(public service: ISService, public route: ActivatedRoute, public dialog: MatDialog) {
}
ngOnInit() {
this.route.params.subscribe(params => {
this.typeId = params['type'];
this.service.loadResourceType(this.typeId, (data: ResourceType) => this.type = data);
this.reload()
});
}
reload() {
if (this.typeId) {
this.service.loadSimpleResources(this.typeId, (data: SimpleResource[]) => this.resources = data);
}
}
openNewDialog(): void {
const dialogRef = this.dialog.open(ResCreateNewDialog, {
data: this.type,
width: '80%'
});
dialogRef.afterClosed().subscribe(result => {
if (result) this.reload();
});
}
openMetadataDialog(r: SimpleResource): void {
const dialogRef = this.dialog.open(ResMetadataDialog, {
data: r,
width: '80%'
});
dialogRef.afterClosed().subscribe(result => {
if (result) this.reload();
});
}
openContentDialog(r: SimpleResource): void {
this.service.loadSimpleResourceContent(r.id, (data: string) => {
const dialogRef = this.dialog.open(ResContentDialog, {
data: {
id: r.id,
contentType: this.type?.contentType,
content: data
},
width: '80%'
});
dialogRef.afterClosed().subscribe(result => {
if (result) this.reload();
});
});
}
deleteResource(r: SimpleResource) {
if (confirm('Are you sure?')) {
this.service.deleteSimpleResource(r.id, (data: void) => this.reload());
}
}
}
@Component({
selector: 'res-content-dialog',
templateUrl: 'content-dialog.html',
styleUrls: ['resources.component.css']
})
export class ResContentDialog {
contentFormControl = new FormControl('');
contentForm = new FormGroup({
content: this.contentFormControl
});
constructor(public dialogRef: MatDialogRef<ResContentDialog>, @Inject(MAT_DIALOG_DATA) public data: any, public service: ISService) {
this.contentFormControl.setValue(data.content);
}
onSubmit(): void {
let content = this.contentFormControl.value;
if (content) {
this.service.saveSimpleResourceContent(this.data.id, content, (data: void) => this.dialogRef.close(1), this.contentForm)
}
}
onNoClick(): void {
this.dialogRef.close();
}
}
@Component({
selector: 'res-metadata-dialog',
templateUrl: 'metadata-dialog.html',
styleUrls: ['resources.component.css']
})
export class ResMetadataDialog {
metadataForm = new FormGroup({
name: new FormControl('', [Validators.required]),
subtype: new FormControl(''),
description: new FormControl('')
});
constructor(public dialogRef: MatDialogRef<ResMetadataDialog>, @Inject(MAT_DIALOG_DATA) public data: any, public service: ISService) {
this.metadataForm.get('name')?.setValue(data.name);
if (data.subtype) {
this.metadataForm.get('subtype')?.setValue(data.subtype);
}
if (data.description) {
this.metadataForm.get('description')?.setValue(data.description);
}
}
onSubmit(): void {
const res = Object.assign({}, this.data, this.metadataForm.value);
this.service.saveSimpleResourceMedatata(res, (data: void) => this.dialogRef.close(1), this.metadataForm);
}
onNoClick(): void {
this.dialogRef.close();
}
}
@Component({
selector: 'res-new-dialog',
templateUrl: 'new-dialog.html',
styleUrls: ['resources.component.css']
})
export class ResCreateNewDialog {
newResourceForm = new FormGroup({
name: new FormControl('', [Validators.required]),
subtype: new FormControl(''),
description: new FormControl(''),
content: new FormControl('', [Validators.required])
});
constructor(public dialogRef: MatDialogRef<ResCreateNewDialog>, @Inject(MAT_DIALOG_DATA) public data: any, public service: ISService) { }
onSubmit(): void {
let name: string = this.newResourceForm.get('name')?.value!;
let type: string = this.data.id!;
let subtype: string = this.newResourceForm.get('subtype')?.value!;
let description: string = this.newResourceForm.get('description')?.value!;
let content: string = this.newResourceForm.get('content')?.value!;
this.service.addSimpleResource(name, type, subtype, description, content, (data: void) => this.dialogRef.close(1), this.newResourceForm);
}
onNoClick(): void {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,32 @@
<form [formGroup]="vocForm" (ngSubmit)="onSubmit()">
<h1 mat-dialog-title *ngIf="data.id">Edit vocabulary</h1>
<h1 mat-dialog-title *ngIf="!data.id">New vocabulary</h1>
<div mat-dialog-content>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>ID</mat-label>
<input matInput required formControlName="id" [readonly]="data.id.length > 0" />
<mat-error *ngIf="vocForm.get('id')?.invalid">This field is <strong>required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Name</mat-label>
<input matInput formControlName="name" required />
<mat-error *ngIf="vocForm.get('name')?.invalid">This field is <strong>required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Description</mat-label>
<textarea matInput formControlName="description" rows="4"></textarea>
</mat-form-field>
</div>
<div mat-dialog-actions>
<button mat-stroked-button color="primary" type="submit" [disabled]="!vocForm.valid">Submit</button>
<button mat-stroked-button color="primary" mat-dialog-close>Close</button>
<mat-error *ngIf="vocForm.errors?.['serverError']">
{{ vocForm.errors?.['serverError'] }}
</mat-error>
</div>
</form>

View File

@ -0,0 +1,131 @@
<form [formGroup]="termForm" (ngSubmit)="onSubmit()">
<h1 mat-dialog-title *ngIf="data.code">Edit term</h1>
<h1 mat-dialog-title *ngIf="!data.code">New term</h1>
<div mat-dialog-content>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Code</mat-label>
<input matInput formControlName="code" required />
<mat-error *ngIf="termForm.get('id')?.invalid">This field is <strong>required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Name</mat-label>
<input matInput formControlName="name" required />
<mat-error *ngIf="termForm.get('name')?.invalid">This field is <strong>required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Encoding</mat-label>
<input matInput formControlName="encoding" />
</mat-form-field>
<table mat-table [dataSource]="synonymsDatasource" class="mat-elevation-z8">
<ng-container matColumnDef="term">
<th mat-header-cell *matHeaderCellDef>Term</th>
<td mat-cell *matCellDef="let element">{{element.term}}</td>
<td mat-footer-cell *matFooterCellDef>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>New term</mat-label>
<input matInput formControlName="tmpSynonymTerm" />
</mat-form-field>
</td>
</ng-container>
<ng-container matColumnDef="encoding">
<th mat-header-cell *matHeaderCellDef style="width: 20%;">Encoding</th>
<td mat-cell *matCellDef="let element">{{element.encoding}}</td>
<td mat-footer-cell *matFooterCellDef>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>New encoding</mat-label>
<input matInput formControlName="tmpSynonymEncoding" />
</mat-form-field>
</td>
</ng-container>
<ng-container matColumnDef="buttons">
<th mat-header-cell *matHeaderCellDef style="text-align: right; width: 8em;"></th>
<td mat-cell *matCellDef="let index = index" class="table-buttons">
<button mat-stroked-button color="warn" type="button" (click)="removeSynonym(index)">
delete
</button>
</td>
<td mat-footer-cell *matFooterCellDef class="table-buttons">
<button mat-stroked-button color="primary" type="button" (click)="addSynonym()"
[disabled]="!isNewSynonymValid()">
add
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="synonymColums"></tr>
<tr mat-row *matRowDef="let row; columns: synonymColums;"></tr>
<!-- Row shown when there is no matching data. -->
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" colspan="3" style="padding: 0 16px;">No synonyms</td>
</tr>
<tr mat-footer-row *matFooterRowDef="synonymColums"></tr>
</table>
<!--
<table style="border: 1px solid gray; font-size: 0.9em;">
<thead>
<tr>
<th style="text-align: left;">Synonym</th>
<th style="width: 20%; text-align: left;">Encoding</th>
<th style="width: 8em"></th>
</tr>
</thead>
<tbody>
<tr *ngIf="synonyms.length == 0">
<td colspan="3">0 synonym(s)</td>
</tr>
<tr *ngFor="let s of synonyms; index as i">
<td>{{s.term}}</td>
<td>{{s.encoding}}</td>
<td class="table-buttons">
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>New term</mat-label>
<input matInput formControlName="tmpSynonymTerm" />
</mat-form-field>
</td>
<td>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>New encoding</mat-label>
<input matInput formControlName="tmpSynonymEncoding" />
</mat-form-field>
</td>
<td class="table-buttons">
<button mat-stroked-button
color="primary"
type="button"
(click)="addSynonym()"
[disabled]="!isNewSynonymValid()">
add
</button>
</td>
</tr>
</tfoot>
</table>
-->
</div>
<div mat-dialog-actions>
<button mat-stroked-button color="primary" type="submit" [disabled]="!termForm.valid">Submit</button>
<button mat-stroked-button color="primary" mat-dialog-close>Close</button>
<mat-error *ngIf="termForm.errors?.['serverError']">
{{ termForm.errors?.['serverError'] }}
</mat-error>
</div>
</form>

View File

@ -0,0 +1,50 @@
<h2>Vocabularies</h2>
<button mat-stroked-button color="primary" (click)="newVocabularyDialog()">
<mat-icon fontIcon="add"></mat-icon>
create a new vocabulary
</button>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%; margin-top: 10px;">
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event)" placeholder="Filter..." #input />
</mat-form-field>
<table mat-table [dataSource]="vocsDatasource" matSort class="mat-elevation-z8">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef style="width: 20%;" mat-sort-header sortActionDescription="Sort by ID"> Id
</th>
<td mat-cell *matCellDef="let element">
<a [routerLink]="['/voc_editor']" [queryParams]="{id: element.id}">{{element.id}}</a>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef style="width: 20%;" mat-sort-header sortActionDescription="Sort by Name"> Name
</th>
<td mat-cell *matCellDef="let element"> {{element.name}} </td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef style="width: 40%;" mat-sort-header
sortActionDescription="Sort by Description"> Description </th>
<td mat-cell *matCellDef="let element"> {{element.description}} </td>
</ng-container>
<ng-container matColumnDef="buttons">
<th mat-header-cell *matHeaderCellDef style="text-align: right;"></th>
<td mat-cell *matCellDef="let element" class="table-buttons">
<button mat-stroked-button color="primary" (click)="editVocabularyDialog(element)">edit</button>
<button mat-stroked-button color="warn" (click)="deleteVocabulary(element)">delete</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="colums"></tr>
<tr mat-row *matRowDef="let row; columns: colums;"></tr>
<!-- Row shown when there is no matching data. -->
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" colspan="4" style="padding: 0 16px;">No data matching the filter "{{input.value}}"</td>
</tr>
</table>

View File

@ -0,0 +1,243 @@
import { Component, Inject, AfterViewInit, OnInit, ViewChild } from '@angular/core';
import { ISService } from '../common/is.service';
import { MatTable, MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
import { Vocabulary, VocabularyTerm, VocabularyTermSynonym } from '../common/is.model';
import { ActivatedRoute } from '@angular/router';
import { MatDialog, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormControl, FormGroup } from '@angular/forms';
@Component({
selector: 'app-vocabularies',
templateUrl: './vocabularies.component.html',
styleUrls: ['./vocabularies.component.css']
})
export class VocabulariesComponent implements OnInit, AfterViewInit {
vocsDatasource: MatTableDataSource<Vocabulary> = new MatTableDataSource<Vocabulary>([]);
colums: string[] = ['id', 'name', 'description', 'buttons'];
@ViewChild(MatSort) sort: MatSort | undefined
constructor(public service: ISService, public route: ActivatedRoute, public dialog: MatDialog) {
}
ngOnInit() {
this.reload();
}
ngAfterViewInit() {
if (this.sort) this.vocsDatasource.sort = this.sort;
}
reload() {
this.service.loadVocabularies((data: Vocabulary[]) => this.vocsDatasource.data = data);
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value.trim().toLowerCase();
this.vocsDatasource.filter = filterValue;
}
newVocabularyDialog(): void {
const dialogRef = this.dialog.open(VocDialog, {
data: { id: '', name: '', description: '' },
width: '80%'
});
dialogRef.afterClosed().subscribe(result => {
if (result) this.reload();
});
}
editVocabularyDialog(vocabulary: Vocabulary): void {
const dialogRef = this.dialog.open(VocDialog, {
data: vocabulary,
width: '80%'
});
dialogRef.afterClosed().subscribe(result => {
if (result) this.reload();
});
}
deleteVocabulary(vocabulary: Vocabulary): void {
if (confirm("Are you sure?")) {
this.service.deleteVocabulary(vocabulary.id, (data: void) => this.reload());
}
}
}
@Component({
selector: 'app-vocabulary-editor',
templateUrl: './vocabulary-editor.component.html',
styleUrls: ['./vocabularies.component.css']
})
export class VocabularyEditorComponent implements OnInit, AfterViewInit {
voc?: Vocabulary
termsDatasource: MatTableDataSource<VocabularyTerm> = new MatTableDataSource<VocabularyTerm>([]);
colums: string[] = ['code', 'name', 'encoding', 'synonyms', 'buttons'];
@ViewChild(MatSort) sort: MatSort | undefined
constructor(public service: ISService, public route: ActivatedRoute, public dialog: MatDialog) {
}
ngOnInit() {
this.reload();
}
ngAfterViewInit() {
if (this.sort) this.termsDatasource.sort = this.sort;
}
reload() {
this.route.queryParams.subscribe((params) => {
this.service.loadVocabulary(params['id'], (data: Vocabulary) => this.voc = data);
this.service.loadVocabularyTerms(params['id'], (data: VocabularyTerm[]) => this.termsDatasource.data = data);
});
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value.trim().toLowerCase();
this.termsDatasource.filter = filterValue;
}
newVocabularyTermDialog(): void {
if (this.voc?.id) {
const dialogRef = this.dialog.open(VocTermDialog, {
data: { vocabulary: this.voc.id, code: '', name: '', encoding: 'OPENAIRE', synonyms: [] },
width: '80%'
});
dialogRef.afterClosed().subscribe(result => {
if (result) this.reload();
});
}
}
editVocabularyTermDialog(term: VocabularyTerm): void {
const dialogRef = this.dialog.open(VocTermDialog, {
data: term,
width: '80%'
});
dialogRef.afterClosed().subscribe(result => {
if (result) this.reload();
});
}
deleteVocabularyTerm(term: VocabularyTerm): void {
if (confirm("Are you sure?") && this.voc?.id && term.code) {
this.service.deleteVocabularyTerm(this.voc.id, term.code, (data: void) => this.reload());
}
}
}
@Component({
selector: 'voc-dialog',
templateUrl: 'voc-dialog.html',
styleUrls: ['vocabularies.component.css']
})
export class VocDialog {
vocForm = new FormGroup({
id: new FormControl(''),
name: new FormControl(''),
description: new FormControl('')
});
constructor(public dialogRef: MatDialogRef<VocDialog>, @Inject(MAT_DIALOG_DATA) public data: any, public service: ISService) {
this.vocForm.get('id')?.setValue(data.id);
this.vocForm.get('name')?.setValue(data.name);
this.vocForm.get('description')?.setValue(data.description);
}
onSubmit(): void {
const voc = Object.assign({}, this.data, this.vocForm.value);
this.service.saveVocabulary(voc, (data: void) => this.dialogRef.close(1), this.vocForm);
}
onNoClick(): void {
this.dialogRef.close();
}
}
@Component({
selector: 'voc-term-dialog',
templateUrl: 'voc-term-dialog.html',
styleUrls: ['vocabularies.component.css']
})
export class VocTermDialog {
termForm = new FormGroup({
code: new FormControl(''),
name: new FormControl(''),
encoding: new FormControl('OPENAIRE'),
tmpSynonymTerm: new FormControl(''),
tmpSynonymEncoding: new FormControl('OPENAIRE')
});
synonymsDatasource: MatTableDataSource<VocabularyTermSynonym> = new MatTableDataSource<VocabularyTermSynonym>([]);
synonymColums: string[] = ['term', 'encoding', 'buttons'];
@ViewChild(MatTable) synonymsTable: MatTable<VocabularyTermSynonym> | undefined;
constructor(public dialogRef: MatDialogRef<VocDialog>, @Inject(MAT_DIALOG_DATA) public data: any, public service: ISService) {
this.termForm.get('code')?.setValue(data.code);
this.termForm.get('name')?.setValue(data.name);
this.termForm.get('encoding')?.setValue(data.encoding);
this.synonymsDatasource.data = data.synonyms;
}
onSubmit(): void {
console.log('SUBMIT');
let oldCode = this.data.code;
let voc = this.data.vocabulary;
let term: VocabularyTerm = {
code: this.termForm.get('code')?.value!,
name: this.termForm.get('name')?.value!,
encoding: this.termForm.get('encoding')?.value!,
synonyms: this.synonymsDatasource.data,
vocabulary: voc
}
this.service.saveVocabularyTerm(voc, term, (data: void) => {
if (oldCode && oldCode != term.code) {
this.service.deleteVocabularyTerm(voc, oldCode, (data: void) => this.dialogRef.close(1))
} else { this.dialogRef.close(1) }
}, this.termForm);
}
removeSynonym(pos: number) {
this.synonymsDatasource.data.splice(pos, 1);
this.synonymsTable?.renderRows();
}
isNewSynonymValid(): boolean {
if (!this.termForm.get('tmpSynonymTerm')) return false;
if (!this.termForm.get('tmpSynonymEncoding')) return false;
if (this.termForm.get('tmpSynonymTerm')?.value?.trim().length == 0) return false;
if (this.termForm.get('tmpSynonymEncoding')?.value?.trim().length == 0) return false;
return true;
}
addSynonym() {
this.synonymsDatasource.data.push({
term: this.termForm.get('tmpSynonymTerm')?.value!,
encoding: this.termForm.get('tmpSynonymEncoding')?.value!
});
this.termForm.get('tmpSynonymTerm')?.setValue('');
this.termForm.get('tmpSynonymEncoding')?.setValue('OPENAIRE');
this.synonymsTable?.renderRows();
}
onNoClick(): void {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,75 @@
<h2>Vocabulary Editor</h2>
<p>
<b>Vocabulary ID: </b>{{voc?.id}}<br />
<b>Vocabulary Name: </b>{{voc?.name}}<br />
<b>Description: </b>{{voc?.description}}
</p>
<p>
<a mat-stroked-button color="primary" [routerLink]="['/adv_resources/vocabulary']">
<mat-icon fontIcon="home"></mat-icon>
return to vocabulary list
</a>
<button mat-stroked-button color="primary" (click)="newVocabularyTermDialog()">
<mat-icon fontIcon="add"></mat-icon>
create a new term
</button>
<a mat-stroked-button color="link" href="/api/vocs/{{voc?.id}}/terms" target="_blank">
<mat-icon fontIcon="download"></mat-icon>
Download
</a>
</p>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%; margin-top: 10px;">
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event)" placeholder="Filter..." #input />
</mat-form-field>
<table mat-table [dataSource]="termsDatasource" matSort class="mat-elevation-z8">
<ng-container matColumnDef="code">
<th mat-header-cell *matHeaderCellDef style="width: 15%;" mat-sort-header sortActionDescription="Sort by Code"> Code
</th>
<td mat-cell *matCellDef="let element"><b>{{element.code}}</b></td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef style="width: 15%;" mat-sort-header sortActionDescription="Sort by Name"> Name
</th>
<td mat-cell *matCellDef="let element"> {{element.name}} </td>
</ng-container>
<ng-container matColumnDef="encoding">
<th mat-header-cell *matHeaderCellDef style="width: 10%;" mat-sort-header sortActionDescription="Sort by Encoding">
Encoding </th>
<td mat-cell *matCellDef="let element">{{element.encoding}}</td>
</ng-container>
<ng-container matColumnDef="synonyms">
<th mat-header-cell *matHeaderCellDef style="width: 40%;" mat-sort-header sortActionDescription="Sort by Synonyms">
Description </th>
<td mat-cell *matCellDef="let element" style="font-size: 0,7em;">
<span class="muted" *ngIf="element.synonyms.length == 0">0 synonym(s)</span>
<span *ngFor="let s of element.synonyms" class="badge-label badge-info">
{{s.term}}
</span>
</td>
</ng-container>
<ng-container matColumnDef="buttons">
<th mat-header-cell *matHeaderCellDef style="text-align: right;"></th>
<td mat-cell *matCellDef="let element" class="table-buttons">
<button mat-stroked-button color="primary" (click)="editVocabularyTermDialog(element)">edit</button>
<button mat-stroked-button color="warn" (click)="deleteVocabularyTerm(element)">delete</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="colums"></tr>
<tr mat-row *matRowDef="let row; columns: colums;"></tr>
<!-- Row shown when there is no matching data. -->
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" colspan="5" style="padding: 0 16px;">No data matching the filter "{{input.value}}"</td>
</tr>
</table>

View File

@ -0,0 +1,104 @@
<h1 mat-dialog-title>Workflow Configuration</h1>
<div mat-dialog-content>
<mat-vertical-stepper [linear]="true" #stepper>
<mat-step [stepControl]="wfConfFormStep1">
<form [formGroup]="wfConfFormStep1">
<ng-template matStepLabel>Choose Workflow</ng-template>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Workflow</mat-label>
<mat-select matInput formControlName="workflow" (selectionChange)="onChangeWfTemplate($event)">
<mat-option *ngFor="let i of wfTemplates" [value]="i.id">{{i.name}} ({{i.subtype}})</mat-option>
</mat-select>
<mat-error *ngIf="wfConfFormStep1.get('workflow')?.invalid">This field is
<strong>required</strong></mat-error>
</mat-form-field>
<div>
<button mat-stroked-button color="primary" matStepperNext>Next</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="wfConfFormStep2" label="General">
<form [formGroup]="wfConfFormStep2">
<section>
<mat-checkbox formControlName="enabled">enabled</mat-checkbox>
</section>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;" *ngIf="data.id">
<mat-label>ID</mat-label>
<input matInput readonly value="{{data.id}}" />
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Name</mat-label>
<input matInput formControlName="name" />
<mat-error *ngIf="wfConfFormStep2.get('name')?.invalid">This field is <strong>required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 40%;">
<mat-label>Priority</mat-label>
<input matInput formControlName="priority" type="number" step="1" min="1" max="100" />
<mat-error *ngIf="wfConfFormStep2.get('priority')?.invalid">Numeric value (range: 1-100)</mat-error>
</mat-form-field>
<div>
<button mat-stroked-button color="primary" matStepperPrevious>Back</button>
<button mat-stroked-button color="primary" matStepperNext>Next</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="wfConfFormStep3" label="Worflow Parameters">
<form [formGroup]="wfConfFormStep3">
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;" *ngFor="let p of wfParameters">
<mat-label>{{p.name}}</mat-label>
<input matInput [formControlName]="p.name" *ngIf="p.type != 'boolean'" />
<mat-select matInput [formControlName]="p.name" *ngIf="p.type == 'boolean'">
<mat-option></mat-option>
<mat-option value="true">true</mat-option>
<mat-option value="false">false</mat-option>
</mat-select>
<mat-error *ngIf="wfConfFormStep3.get(p.name)?.invalid">Invalid value</mat-error>
</mat-form-field>
<div>
<button mat-stroked-button color="primary" matStepperPrevious>Back</button>
<button mat-stroked-button color="primary" matStepperNext>Next</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="wfConfFormStep4" label="Scheduling">
<form [formGroup]="wfConfFormStep4">
<section>
<mat-checkbox formControlName="schedulingEnabled">Enabled</mat-checkbox>
</section>
<div *ngIf="wfConfFormStep4.get('schedulingEnabled')?.value">
<mat-form-field appearance="fill" floatLabel="always" style="width: 50%;">
<mat-label>Cron Expression</mat-label>
<input matInput formControlName="cronExpression" />
<mat-error *ngIf="wfConfFormStep4.get('cronExpression')?.invalid">This field is
<strong>required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 50%;">
<mat-label>Min Interval (in minutes)</mat-label>
<input matInput formControlName="cronMinInterval" type="number" step="10" min="10" max="10080" />
<mat-error *ngIf="wfConfFormStep4.get('cronMinInterval')?.invalid">This field is
<strong>required</strong></mat-error>
</mat-form-field>
</div>
<div>
<button mat-stroked-button color="primary" matStepperPrevious>Back</button>
<button mat-stroked-button color="primary" matStepperNext>Next</button>
</div>
</form>
</mat-step>
<mat-step>
<ng-template matStepLabel>Done</ng-template>
<p>You are now done.</p>
<div>
<form [formGroup]="wfConfFormFinal" (ngSubmit)="onSubmit()">
<button mat-stroked-button color="primary" type="submit">Save</button>
<mat-error *ngIf="wfConfFormFinal.errors?.['serverError']">
{{ wfConfFormFinal.errors?.['serverError'] }}
</mat-error>
</form>
</div>
</mat-step>
</mat-vertical-stepper>
</div>

View File

@ -0,0 +1,75 @@
<mat-card *ngIf="conf" style="margin-top: 0.4em;">
<mat-card-header>
<mat-card-title>{{conf.name}}</mat-card-title>
<mat-card-subtitle *ngIf="conf.dsName"><b>Datasource Name:</b> {{conf.dsName}}</mat-card-subtitle>
<mat-card-subtitle *ngIf="conf.dsId"><b>Datasource ID:</b> {{conf.dsId}}</mat-card-subtitle>
<mat-card-subtitle *ngIf="conf.apiId"><b>Datasource API:</b> {{conf.apiId}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content style="padding-top: 1em;">
<button mat-stroked-button color="primary" (click)="launchWfConf()">
<mat-icon fontIcon="play_circle"></mat-icon>
launch
</button>
<button mat-stroked-button color="primary" (click)="editConf()">
<mat-icon fontIcon="edit"></mat-icon>
configure
</button>
<a href="./api/resources/{{conf.workflow}}/content" mat-stroked-button color="link" target="_blank">
<mat-icon fontIcon="code"></mat-icon>
raw workflow
</a>
<button mat-stroked-button color="warn" (click)="deleteConf()">
<mat-icon fontIcon="delete"></mat-icon>
delete
</button>
<mat-divider style="margin-top: 1em; margin-bottom: 1em;"></mat-divider>
<table mat-table [dataSource]="historyDatasource" matSort>
<ng-container matColumnDef="processId">
<th mat-header-cell *matHeaderCellDef style="width: 15%;" mat-sort-header
sortActionDescription="Sort by Process ID"> Process Id </th>
<td mat-cell *matCellDef="let element">
<a (click)="openWfHistoryDialog(element)">{{element.processId}}</a>
</td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef style="width: 10%;" mat-sort-header
sortActionDescription="Sort by Status">
Status </th>
<td mat-cell *matCellDef="let element"><span class="badge-label"
[ngClass]="{'badge-success' : element.status === 'success', 'badge-failure' : element.status === 'failure'}">{{element.status}}</span>
</td>
</ng-container>
<ng-container matColumnDef="startDate">
<th mat-header-cell *matHeaderCellDef style="width: 15%;" mat-sort-header
sortActionDescription="Sort by Start Date"> Start Date </th>
<td mat-cell *matCellDef="let element"> {{element.startDate}} </td>
</ng-container>
<ng-container matColumnDef="endDate">
<th mat-header-cell *matHeaderCellDef style="width: 15%;" mat-sort-header
sortActionDescription="Sort by End Date">
End Date </th>
<td mat-cell *matCellDef="let element"> {{element.endDate}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="colums"></tr>
<tr mat-row *matRowDef="let row; columns: colums;"></tr>
<!-- Row shown when there is no matching data. -->
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" colspan="4" style="padding: 0 16px;">No execution in history"</td>
</tr>
</table>
</mat-card-content>
<!-- <pre>{{conf | json}}</pre> -->
</mat-card>
<div *ngIf="!conf" style="margin-top: 2em;">
Workflow Configuration does not exist
</div>

View File

@ -0,0 +1,15 @@
<h2>{{section?.name}}: Configured Workflows</h2>
<button mat-stroked-button color="primary" (click)="openAddWfConfDialog()">
<mat-icon fontIcon="add"></mat-icon>
configure a new workflow
</button>
<nav mat-tab-nav-bar *ngIf="confs.length > 0" style="margin-top: 1em;">
<a mat-tab-link *ngFor="let c of confs" [routerLink]="['/wfs/conf', c.k]" routerLinkActive #rla="routerLinkActive"
[active]="rla.isActive">
{{c.v}}
</a>
</nav>
<wf-conf-single [conf]="conf"></wf-conf-single>

View File

@ -0,0 +1,234 @@
import { JsonPipe } from '@angular/common';
import { Component, Inject, Input, OnChanges, OnInit, SecurityContext, SimpleChanges } from '@angular/core';
import { FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatSelectChange } from '@angular/material/select';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute, Router } from '@angular/router';
import { KeyValue, SimpleResource, WfConf, WfHistoryEntry, WfParam, WfProcessStatus, WfSection } from '../common/is.model';
import { ISService } from '../common/is.service';
import { ResMetadataDialog } from '../resources/resources.component';
import { MatTableDataSource } from '@angular/material/table';
import { WfHistoryDialog } from '../wf-history/wf-history.component';
@Component({
selector: 'app-wf-confs',
templateUrl: './wf-confs.component.html',
styleUrls: ['./wf-confs.component.css']
})
export class WfConfsComponent implements OnInit {
section?: WfSection;
confs: KeyValue[] = [];
conf?: WfConf;
constructor(public service: ISService, public route: ActivatedRoute, public router: Router, public dialog: MatDialog, public snackBar: MatSnackBar) {
}
ngOnInit() {
this.route.params.subscribe(params => {
let sectionId = params['section'];
let confId = params['conf'];
if (confId) {
this.service.loadWfConfiguration(confId, (conf: WfConf) => {
this.conf = conf;
if (conf.section) {
this.service.loadWfSections((data: WfSection[]) => this.section = data.find(s => s.id == conf?.section));
this.service.loadWfConfigurations(conf.section, (data: KeyValue[]) => this.confs = data);
}
});
} else if (sectionId) {
this.service.loadWfSections((data: WfSection[]) => this.section = data.find(s => s.id == sectionId));
this.service.loadWfConfigurations(sectionId, (data: KeyValue[]) => {
this.confs = data;
if (data.length > 0) {
this.router.navigate(['/wfs/conf', data[0].k]);
}
});
} else {
console.log("One of the following parameters is missing: sectionId or confId");
}
});
}
openAddWfConfDialog(): void {
const dialogRef = this.dialog.open(WfConfDialog, {
data: {
id: '',
name: '',
section: this.section?.id,
enabled: true,
priority: 75,
workflow: '',
schedulingEnabled: false,
cronExpression: '0 30 12 1/1 * ?',
cronMinInterval: 9600,
details: new Map,
configured: true,
systemParams: new Map,
userParams: new Map
},
width: '80%'
});
dialogRef.afterClosed().subscribe(result => {
if (result) this.router.navigate(['/wfs/conf', result.id]);;
});
}
}
@Component({
selector: 'wf-conf-dialog',
templateUrl: 'wf-conf-dialog.html',
styleUrls: ['./wf-confs.component.css']
})
export class WfConfDialog implements OnInit {
wfTemplates: SimpleResource[] = [];
wfParameters: WfParam[] = [];
wfConfFormStep1 = new FormGroup({
workflow: new FormControl('', [Validators.required]),
});
wfConfFormStep2 = new FormGroup({
name: new FormControl('', [Validators.required]),
//details: Map<string, string>,
enabled: new FormControl(true, [Validators.required]),
priority: new FormControl(75, [Validators.required, Validators.min(1), Validators.max(100)]),
});
wfConfFormStep3 = new FormGroup({
//systemParams: Map<string, string>,
//userParams: Map<string, string>
});
wfConfFormStep4 = new FormGroup({
schedulingEnabled: new FormControl(false, [Validators.required]),
cronExpression: new FormControl("", [Validators.required]),
cronMinInterval: new FormControl("", [Validators.required]),
});
wfConfFormFinal = new FormGroup({});
constructor(public dialogRef: MatDialogRef<ResMetadataDialog>, @Inject(MAT_DIALOG_DATA) public data: any, public service: ISService) {
this.wfConfFormStep1.get('workflow')?.setValue(data.workflow);
this.wfConfFormStep2.get('name')?.setValue(data.name);
this.wfConfFormStep2.get('enabled')?.setValue(data.enabled);
this.wfConfFormStep2.get('priority')?.setValue(data.priority);
//details
//systemParams,
//userParams
this.wfConfFormStep4.get('schedulingEnabled')?.setValue(data.schedulingEnabled);
this.wfConfFormStep4.get('cronExpression')?.setValue(data.cronExpression);
this.wfConfFormStep4.get('cronMinInterval')?.setValue(data.cronMinInterval);
}
ngOnInit(): void {
if (this.data.id && this.data.workflow) {
this.prepareWfParameters(this.data.workflow);
}
this.service.loadSimpleResources("wf_template", (data: SimpleResource[]) => this.wfTemplates = data);
}
onChangeWfTemplate(e: MatSelectChange): void {
this.prepareWfParameters(e.value);
}
prepareWfParameters(wfTemplateId: string): void {
this.service.loadSimpleResourceContent(wfTemplateId, (data: any) => {
console.log(data);
if (data.parameters) {
this.wfParameters = data.parameters;
} else {
this.wfParameters = JSON.parse(data).parameters;
}
this.wfConfFormStep3.controls = {};
this.wfParameters.forEach(p => {
let validations: ValidatorFn[] = [];
if (p.required) { validations.push(Validators.required); }
if (p.type == 'number') { validations.push(Validators.pattern('^[0-9]*$')); }
this.wfConfFormStep3.addControl(p.name, new FormControl(this.data.userParams[p.name], validations));
})
});
}
onSubmit(): void {
const conf = Object.assign({}, this.data, this.wfConfFormStep1.value, this.wfConfFormStep2.value, this.wfConfFormStep4.value);
conf.details = {};
conf.systemParams = {};
conf.userParams = this.wfConfFormStep3.value;
this.service.saveWfConfiguration(conf, (data: void) => this.dialogRef.close(data), this.wfConfFormFinal);
}
onNoClick(): void {
this.dialogRef.close();
}
}
@Component({
selector: 'wf-conf-single',
templateUrl: 'wf-conf-single.html',
styleUrls: ['./wf-confs.component.css']
})
export class WfConfSingle implements OnInit, OnChanges {
@Input() conf?: WfConf;
prevConfId = '';
historyDatasource: MatTableDataSource<WfHistoryEntry> = new MatTableDataSource<WfHistoryEntry>([]);
colums: string[] = ['processId', 'status', 'startDate', 'endDate'];
constructor(public service: ISService, public dialog: MatDialog, public snackBar: MatSnackBar) { }
ngOnInit(): void {
if (this.conf) {
this.service.loadWfHistoryForConf(this.conf?.id, (data: WfHistoryEntry[]) => this.historyDatasource.data = data);
}
}
ngOnChanges(changes: SimpleChanges): void {
if (this.conf && this.conf.id != this.prevConfId) {
this.prevConfId = this.conf.id;
this.service.loadWfHistoryForConf(this.conf?.id, (data: WfHistoryEntry[]) => this.historyDatasource.data = data);
}
}
launchWfConf() {
if (this.conf?.id && this.conf?.workflow) {
this.service.startWfConfiguration(this.conf?.id, (data: WfProcessStatus) => this.snackBar.open('Workflow launched !!!', 'INFO', { duration: 5000 }));
}
}
editConf() {
const dialogRef = this.dialog.open(WfConfDialog, {
data: this.conf,
width: '80%'
});
}
deleteConf() {
if (this.conf?.destroyWf) {
if (this.conf?.id && this.conf?.workflow) {
this.service.startDestroyWfConfiguration(this.conf?.id, (data: WfProcessStatus) => this.snackBar.open('Destroy Workflow launched, PLEASE WAIT !!!', 'INFO', { duration: 5000 }));
}
} else if (this.conf?.id) {
this.service.deleteWfConfiguration(this.conf?.id, (data: void) => {
this.snackBar.open('Configuration deleted !!!', 'INFO', { duration: 5000 });
this.conf = undefined;
});
}
}
openWfHistoryDialog(wf: WfHistoryEntry): void {
const wfDialogRef = this.dialog.open(WfHistoryDialog, {
data: wf
});
}
}

View File

@ -0,0 +1,75 @@
<h2>Workflow History</h2>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event)" placeholder="Filter..." #input />
</mat-form-field>
<p>
<span *ngIf="from < 0 && to < 0"><b>Recent workflows</b> (max {{total}})</span>
<span *ngIf="from >= 0 && to >= 0"><b>Workflows from </b>{{from | date:"yyyy-MM-dd HH:mm:ss"}} <b>to</b> {{to |
date:"yyyy-MM-dd HH:mm:ss"}}</span>
<span *ngIf="from >= 0 && to < 0"><b>Workflows from </b>{{from | date:"yyyy-MM-dd HH:mm:ss"}} <b>to</b>
<i>undefined</i></span>
<span *ngIf="from < 0 && to >= 0"><b>Workflows from </b><i>undefined</i> <b>to</b> {{to | date:"yyyy-MM-dd
HH:mm:ss"}}</span>
<br />
<span><b>Count :</b> {{historyDatasource.filteredData.length}}</span>
</p>
<table mat-table [dataSource]="historyDatasource" matSort>
<ng-container matColumnDef="processId">
<th mat-header-cell *matHeaderCellDef style="width: 15%;" mat-sort-header
sortActionDescription="Sort by Process ID"> Process Id </th>
<td mat-cell *matCellDef="let element">
<a (click)="openWfDialog(element)">{{element.processId}}</a>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef style="width: 15%;" mat-sort-header sortActionDescription="Sort by WF Name">
Workflow Name </th>
<td mat-cell *matCellDef="let element"> {{element.name}} </td>
</ng-container>
<ng-container matColumnDef="family">
<th mat-header-cell *matHeaderCellDef style="width: 10%;" mat-sort-header sortActionDescription="Sort by WF Family">
Workflow Family </th>
<td mat-cell *matCellDef="let element"> {{element.family}} </td>
</ng-container>
<ng-container matColumnDef="dsName">
<th mat-header-cell *matHeaderCellDef style="width: 20%;" mat-sort-header
sortActionDescription="Sort by Datasource"> Datasource </th>
<td mat-cell *matCellDef="let element"> {{element.dsName}} </td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef style="width: 10%;" mat-sort-header sortActionDescription="Sort by Status">
Status </th>
<td mat-cell *matCellDef="let element"><span class="badge-label"
[ngClass]="{'badge-success' : element.status === 'success', 'badge-failure' : element.status === 'failure'}">{{element.status}}</span>
</td>
</ng-container>
<ng-container matColumnDef="startDate">
<th mat-header-cell *matHeaderCellDef style="width: 15%;" mat-sort-header
sortActionDescription="Sort by Start Date"> Start Date </th>
<td mat-cell *matCellDef="let element"> {{element.startDate}} </td>
</ng-container>
<ng-container matColumnDef="endDate">
<th mat-header-cell *matHeaderCellDef style="width: 15%;" mat-sort-header sortActionDescription="Sort by End Date">
End Date </th>
<td mat-cell *matCellDef="let element"> {{element.endDate}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="colums"></tr>
<tr mat-row *matRowDef="let row; columns: colums;"></tr>
<!-- Row shown when there is no matching data. -->
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" colspan="7" style="padding: 0 16px;">No data matching the filter "{{input.value}}"</td>
</tr>
</table>

View File

@ -0,0 +1,155 @@
import { Component, Inject, AfterViewInit, OnInit, ViewChild } from '@angular/core';
import { ISService } from '../common/is.service';
import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
import { WfHistoryEntry, KeyValue } from '../common/is.model';
import { ActivatedRoute, Params } from '@angular/router';
import { combineLatest } from 'rxjs';
import { MatDialog, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'app-wf-history',
templateUrl: './wf-history.component.html',
styleUrls: ['./wf-history.component.css']
})
export class WfHistoryComponent implements AfterViewInit, OnInit {
historyDatasource: MatTableDataSource<WfHistoryEntry> = new MatTableDataSource<WfHistoryEntry>([]);
colums: string[] = ['processId', 'name', 'family', 'dsName', 'status', 'startDate', 'endDate'];
total: number = 100
from: number = -1
to: number = -1
constructor(public service: ISService, public route: ActivatedRoute, public dialog: MatDialog) {
}
ngOnInit() {
combineLatest([this.route.params, this.route.queryParams],
(params: Params, queryParams: Params) => ({ params, queryParams })
).subscribe((res: { params: Params; queryParams: Params }) => {
const { params, queryParams } = res;
let totalP = queryParams['total'];
let fromP = queryParams['from'];
let toP = queryParams['to'];
if (totalP) { this.total = parseInt(totalP); }
if (fromP) { this.from = parseInt(fromP); }
if (toP) { this.to = parseInt(toP); }
this.service.loadWfHistory(this.total, this.from, this.to, (data: WfHistoryEntry[]) => this.historyDatasource.data = data);
});
}
@ViewChild(MatSort) sort: MatSort | undefined
ngAfterViewInit() {
if (this.sort) this.historyDatasource.sort = this.sort;
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value.trim().toLowerCase();
this.historyDatasource.filter = filterValue;
}
openWfDialog(wf: WfHistoryEntry): void {
const wfDialogRef = this.dialog.open(WfHistoryDialog, {
data: wf
});
}
}
@Component({
selector: 'wf-history.dialog',
templateUrl: 'wf-history.dialog.html',
styleUrls: ['wf-history.component.css']
})
export class WfHistoryDialog {
startDate: string = '';
endDate: string = '';
duration: string = '';
wfDatasource: MatTableDataSource<KeyValue> = new MatTableDataSource<KeyValue>([]);
colums: string[] = ['k', 'v'];
constructor(
public dialogRef: MatDialogRef<WfHistoryDialog>,
@Inject(MAT_DIALOG_DATA) public data: WfHistoryEntry,
) {
let list: KeyValue[] = [];
let map = new Map(Object.entries(data.details));
for (let [key, value] of map) {
list.push({ k: key, v: value });
}
this.wfDatasource.data = list;
this.startDate = data.startDate;
this.endDate = data.endDate;
this.duration = this.calculateDateDiff(
parseInt(map.get('system:startDate')),
parseInt(map.get('system:endDate'))
);
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value.trim().toLowerCase();
this.wfDatasource.filter = filterValue;
}
onNoClick(): void {
this.dialogRef.close();
}
calculateDateDiff(start: number, end: number): string {
if (start <= 0 || end <= 0) {
return '-';
}
var seconds = 0;
var minutes = 0;
var hours = 0;
var days = 0;
if (end > start) {
seconds = Math.round((end - start) / 1000);
if (seconds > 60) {
minutes = Math.floor(seconds / 60);
seconds = seconds % 60;
if (minutes > 60) {
hours = Math.floor(minutes / 60);
minutes = minutes % 60;
if (hours > 24) {
days = Math.floor(hours / 24);
hours = hours % 24;
}
}
}
}
var res = '';
if (days > 0) {
if (res) { res += ', '; }
res += days + " day(s)"
}
if (hours > 0) {
if (res) { res += ', '; }
res += hours + " hour(s)"
}
if (minutes > 0) {
if (res) { res += ', '; }
res += minutes + " minute(s)"
}
if (seconds > 0) {
if (res) { res += ', '; }
res += seconds + " second(s)"
}
if (!res) {
res = '0 seconds';
}
return res;
}
}

View File

@ -0,0 +1,41 @@
<h1 mat-dialog-title>Details</h1>
<div mat-dialog-content>
<p style="font-size: 0.8em;">
<b>Started at: </b>{{startDate}}<br />
<b>Finished at: </b>{{endDate}}<br />
<b>Duration: </b>{{duration}}<br />
</p>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event)" placeholder="Filter..." #filterParams />
</mat-form-field>
<table mat-table [dataSource]="wfDatasource" class="mat-elevation-z8">
<ng-container matColumnDef="k">
<th mat-header-cell *matHeaderCellDef>k</th>
<td mat-cell *matCellDef="let element" style="width: 30%;">{{element.k}}</td>
</ng-container>
<ng-container matColumnDef="v">
<th mat-header-cell *matHeaderCellDef>v</th>
<td mat-cell *matCellDef="let element" style="width: 70%;"> {{element.v}} </td>
</ng-container>
<!-- <tr mat-header-row *matHeaderRowDef="colums"></tr> -->
<tr mat-row *matRowDef="let row; columns: colums;"></tr>
<!-- Row shown when there is no matching data. -->
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" colspan="2" style="padding: 0 16px;">No data matching the filter "{{filterParams.value}}"
</td>
</tr>
</table>
</div>
<div mat-dialog-actions>
<button mat-stroked-button color="primary" mat-dialog-close>Close</button>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 947 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

Some files were not shown because too many files have changed in this diff Show More