Merge pull request #7 from jhuapl-lglenden/develop-updates

Updated UI.
This commit is contained in:
Laura Glendenning
2020-10-07 12:54:18 -04:00
committed by GitHub
66 changed files with 2343 additions and 1580 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "pine",
"version": "1.0.0",
"version": "1.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "pine",
"version": "1.0.0",
"version": "1.1.0",
"scripts": {
"ng": "ng",
"start": "ng serve --progress=false",

View File

@@ -1,47 +1,24 @@
/*(C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC. */
.mat-toolbar .mat-primary {
position: sticky;
top: 0;
height: 64px;
#top-div {
height: 100%;
width: 100%;
}
#app-container {
.app-toolbar {
position: fixed;
top: 0px;
width: 100%;
}
#toolbar {
border-top-left-radius: 15px;
border-top-right-radius: 15px;
.logo-img {
width: 30px;
margin-right: 8px;
}
#sidenav::after {
content: "";
opacity: 0.3;
top: 0;
left: 0;
bottom: 0;
right: 0;
position: absolute;
z-index: -1;
background-image: url("/assets/pine_small.png");
background-repeat: no-repeat;
background-position: center bottom;
}
#content {
padding: 5px;
}
:host::ng-deep.full-content {
width: 100%;
min-height: calc(100vh - 78px);
}
#status-bar {
position: absolute;
bottom: 0px;
.page-content {
position: fixed;
top: 64px;
left: 0px;
right: 0px;
z-index: 100;
}
bottom: 0px;
}

View File

@@ -1,27 +1,29 @@
<!-- (C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC.-->
<mat-sidenav-container id="app-container" autosize fullscreen>
<mat-sidenav *ngIf="ready && !backendError && getLoggedIn()"
id="sidenav" class="bordered background-primary-color-lighter"
fixedInViewport="false" [attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
opened="true" mode="side">
<app-navigation></app-navigation>
</mat-sidenav>
<mat-sidenav-content id="content">
<mat-toolbar color="primary" class="bordered" id="toolbar">
<mat-toolbar-row>
<span>{{appConfig.appName}}</span>
<span class="spacer"></span>
<p class="login-text" *ngIf="getLoggedIn()">{{ getLoggedInUserString() }}</p>
</mat-toolbar-row>
</mat-toolbar>
<div id="outlet" class="bordered">
<div *ngIf="!ready">Connecting to backend...</div>
<div *ngIf="backendError">
<h1>Backend Error</h1>
<p>{{backendErrorMessage}}</p>
<div fxLayout="column" id="top-div">
<mat-toolbar class="app-toolbar" color="primary" fxLayout="row">
<div fxLayout="row" style="cursor: pointer" [routerLink]=" ['/collection'] " routerLinkActive="active">
<img class="logo-img" src="assets/tree_icon_sketch.png" alt="PINE Logo"/>
<h1>{{title}}</h1>
</div>
<router-outlet *ngIf="!backendError && ready"></router-outlet>
<ng-container *ngIf="ready && !backendError && getLoggedIn()">
<span fxFlex="8px"></span>
<app-toolbar-nav></app-toolbar-nav>
<span fxFlex></span>
<button class="user-menu-btn" mat-flat-button color="primary" [matMenuTriggerFor]="userMenu" aria-label="User Profile">
<span id="toolbar-user-name">{{ getLoggedInUserString() }}</span>
<mat-icon>keyboard_arrow_down</mat-icon>
</button>
<mat-menu #userMenu="matMenu" yPosition="below" overlapTrigger="false">
<app-user-card></app-user-card>
</mat-menu>
</ng-container>
</mat-toolbar>
<div class="page-content background-grey">
<router-outlet></router-outlet>
</div>
</mat-sidenav-content>
</mat-sidenav-container>
<app-status-bar id="status-bar" [visible]="false"></app-status-bar>
</div>

View File

@@ -33,6 +33,8 @@ export class AppComponent implements OnInit, AfterViewInit {
public backendErrorMessage = "";
public title: string = 'PINE';
@ViewChild(StatusBarComponent)
public statusBar: StatusBarComponent;

View File

@@ -17,11 +17,11 @@ export class AppConfig {
public appLongName = "PMed Interface for NLP Experimentation";
public appLogoAsset = "pine_logo.png";
public appLogoAsset = "tree_icon_sketch.png";
public loginPage = `/${PATHS.user.login}`;
public landingPage = `/${PATHS.home}`;
public landingPage = `/${PATHS.collection.view}`;
constructor(private http: HttpClient) { }

View File

@@ -9,6 +9,7 @@ import { AppComponent } from "./app.component";
import { LayoutModule } from "@angular/cdk/layout";
import { HttpClientModule } from "@angular/common/http";
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { FlexLayoutModule } from '@angular/flex-layout';
import {
MatAutocompleteModule,
@@ -66,7 +67,6 @@ import { SettingsService } from "./service/settings/settings.service";
import { AuthGuard, AdminAuthGuard } from "./service/auth/auth-guard.service";
import { LoginComponent } from "./component/login/login.component";
import { HomeComponent } from "./component/home/home.component";
import { AddCollectionComponent } from "./component/add-collection/add-collection.component";
import { CollectionDetailsComponent, AddLabelDialog, AddViewerDialog, AddAnnotatorDialog} from "./component/collection-details/collection-details.component";
import { AddDocumentComponent } from "./component/add-document/add-document.component";
@@ -102,6 +102,10 @@ import { ImageCollectionUploaderComponent, ImageCollectionUploaderDialog } from
import { StatusBarComponent } from './component/status-bar/status-bar.component';
import { StatusBarService } from "./service/status-bar/status-bar.service";
import { AboutComponent } from './component/about/about.component';
import { ToolbarComponent } from './component/toolbar';
import { ToolbarNavComponent } from './component/toolbar/toolbar-nav/toolbar-nav.component';
import { ToolbarNavButtonComponent } from './component/toolbar/toolbar-nav-button/toolbar-nav-button.component';
import { UserCardComponent } from './component/user-card/user-card.component';
export function initializeApp(appConfig: AppConfig) {
return () => appConfig.load();
@@ -111,7 +115,6 @@ export function initializeApp(appConfig: AppConfig) {
declarations: [
AppComponent,
LoginComponent,
HomeComponent,
AddCollectionComponent,
CollectionDetailsComponent,
AddDocumentComponent,
@@ -149,12 +152,17 @@ export function initializeApp(appConfig: AppConfig) {
ImageCollectionUploaderComponent,
ImageCollectionUploaderDialog,
StatusBarComponent,
AboutComponent
AboutComponent,
ToolbarComponent,
ToolbarNavComponent,
ToolbarNavButtonComponent,
UserCardComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
ReactiveFormsModule,
FlexLayoutModule,
LayoutModule,
MatAutocompleteModule,
MatBadgeModule,
@@ -228,7 +236,9 @@ export function initializeApp(appConfig: AppConfig) {
AddViewerDialog,
ImageChooserDialog,
ImageCollectionUploaderDialog,
AboutComponent
AboutComponent,
AddCollectionComponent,
AddDocumentComponent
]
})

View File

@@ -1,15 +1,12 @@
// (C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC.
export const PATHS = {
home: "home",
collection: {
add: "collection/add",
details: "collection/details",
view: "collections"
view: "collection"
},
document: {
add: "document/add",
annotate: "annotate"
annotate: "collection/annotate"
},
user: {
account: "account",

View File

@@ -6,7 +6,6 @@ import { PATHS, PARAMS } from "./app.paths";
import { AuthGuard, AdminAuthGuard } from "./service/auth/auth-guard.service";
import { LoginComponent } from "./component/login/login.component";
import { HomeComponent } from "./component/home/home.component";
import { AddCollectionComponent } from "./component/add-collection/add-collection.component";
import { CollectionDetailsComponent } from "./component/collection-details/collection-details.component";
import { AddDocumentComponent } from "./component/add-document/add-document.component";
@@ -20,18 +19,12 @@ import { AdminDataComponent } from "./component/admin-data/admin-data.component"
import { OAuthAuthorizeComponent } from "./service/auth/modules/oauth-authorize.component";
const appRoutes: Routes = [
{path: PATHS.user.login, component: LoginComponent,
data: { subtitle: LoginComponent.SUBTITLE }},
{path: PATHS.home, component: HomeComponent, canActivate: [AuthGuard],
data: {}},
{path: "", redirectTo: PATHS.collection.view, pathMatch: 'full'},
{path: PATHS.user.login, component: LoginComponent, data: { subtitle: LoginComponent.SUBTITLE }},
{path: `${PATHS.document.annotate}/:${PARAMS.document.annotate.document_id}`, component: AnnotateComponent, canActivate: [AuthGuard],
data: { subtitle: AnnotateComponent.SUBTITLE}},
{path: PATHS.collection.add, component: AddCollectionComponent, canActivate: [AuthGuard],
data: { subtitle: AddCollectionComponent.SUBTITLE }},
{path: `${PATHS.collection.details}/:${PARAMS.collection.details.collection_id}`, component: CollectionDetailsComponent, canActivate: [AuthGuard],
data: { subtitle: CollectionDetailsComponent.SUBTITLE }},
{path: `${PATHS.document.add}/:${PARAMS.document.add.collection_id}`, component: AddDocumentComponent, canActivate: [AuthGuard],
data: { subtitle: AddDocumentComponent.SUBTITLE }},
{path: PATHS.collection.view, component: ViewCollectionsComponent, canActivate: [AuthGuard],
data: { subtitle: ViewCollectionsComponent.SUBTITLE }},
{path: PATHS.user.account, component: AccountComponent, canActivate: [AuthGuard],
@@ -46,7 +39,7 @@ const appRoutes: Routes = [
data: { subtitle: AdminDataComponent.SUBTITLE }},
{path: PATHS.user.oauth.authorize, component: OAuthAuthorizeComponent,
data: { subtitle: OAuthAuthorizeComponent.SUBTITLE }},
{path: "**", redirectTo: PATHS.home}
{path: "**", redirectTo: PATHS.collection.view}
];
export const routing = RouterModule.forRoot(appRoutes);

View File

@@ -1,17 +1,17 @@
/*(C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC. */
#content {
background-image: url("/assets/pine_small.png");
background-image: url("/assets/tree_icon_sketch.png");
background-repeat: no-repeat;
background-position: center bottom;
background-size: 100% 100%;
background-size: contain;
padding-top: 10px;
padding-bottom: 10px;
}
#content-card {
background-color: #FFFFFF;
opacity: 0.95;
opacity: 0.85;
}
#version-table mat-cell, #version-table .mat-cell,

View File

@@ -1,4 +1,23 @@
/*(C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC. */
#accordian {
margin: 10px;
.account-container {
height: 100%;
width: 100%;
position: relative;
}
.account-container > mat-toolbar {
position: absolute;
top: 0px;
width: 100%;
}
.account-container > .account-content {
position: absolute;
top: 64px;
bottom: 0px;
right: 0px;
left: 0px;
padding: 20px;
overflow-y: auto;
}

View File

@@ -1,113 +1,111 @@
<!-- (C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC.-->
<mat-card>
<mat-card-header>
<mat-card-title>
<h1>Manage Account</h1>
</mat-card-title>
</mat-card-header>
<mat-divider></mat-divider>
<div *ngIf="loading">Loading...</div>
<mat-accordion *ngIf="!loading" id="accordian" multi="true">
<div class="account-container">
<mat-toolbar>
<span>Manage Account</span>
</mat-toolbar>
<div class="account-content">
<div *ngIf="loading">Loading...</div>
<mat-accordion *ngIf="!loading" id="accordian" multi="true">
<mat-expansion-panel expanded="true">
<mat-expansion-panel-header>
<mat-panel-title>Change Account Details</mat-panel-title>
</mat-expansion-panel-header>
<form [formGroup]="detailsForm" (ngSubmit)="changeDetails()">
<mat-form-field class="form-field" appearance="standard">
<mat-label>Email / Username</mat-label>
<input matInput formControlName="email" type="text"
class="form-control" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>First Name</mat-label>
<input matInput required formControlName="first_name" type="text"
class="form-control"
[ngClass]="{ 'is-invalid': (d.first_name.dirty || d.first_name.touched || submittedDetails) && d.first_name.errors }" />
<mat-error *ngIf="(d.first_name.dirty || d.first_name.touched || submittedDetails) && d.first_name.errors">
<div *ngIf="d.first_name.errors.required">First name is required.</div>
</mat-error>
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Last Name</mat-label>
<input matInput required formControlName="last_name" type="text"
class="form-control"
[ngClass]="{ 'is-invalid': (d.last_name.dirty || d.last_name.touched || submittedDetails) && d.last_name.errors }" />
<mat-error *ngIf="(d.last_name.dirty || d.last_name.touched || submittedDetails) && d.last_name.errors">
<div *ngIf="d.last_name.errors.required">Last name is required.</div>
</mat-error>
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Description</mat-label>
<textarea matInput formControlName="description" cdkTextareaAutosize
class="form-control"
[ngClass]="{ 'is-dirty': (d.description.dirty), 'is-invalid': (d.description.dirty || d.description.touched || submittedDetails) && d.description.errors }">
<mat-expansion-panel expanded="true">
<mat-expansion-panel-header>
<mat-panel-title>Change Account Details</mat-panel-title>
</mat-expansion-panel-header>
<form [formGroup]="detailsForm" (ngSubmit)="changeDetails()">
<mat-form-field class="form-field" appearance="standard">
<mat-label>Email / Username</mat-label>
<input matInput formControlName="email" type="text" class="form-control" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>First Name</mat-label>
<input matInput required formControlName="first_name" type="text" class="form-control"
[ngClass]="{ 'is-invalid': (d.first_name.dirty || d.first_name.touched || submittedDetails) && d.first_name.errors }" />
<mat-error
*ngIf="(d.first_name.dirty || d.first_name.touched || submittedDetails) && d.first_name.errors">
<div *ngIf="d.first_name.errors.required">First name is required.</div>
</mat-error>
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Last Name</mat-label>
<input matInput required formControlName="last_name" type="text" class="form-control"
[ngClass]="{ 'is-invalid': (d.last_name.dirty || d.last_name.touched || submittedDetails) && d.last_name.errors }" />
<mat-error
*ngIf="(d.last_name.dirty || d.last_name.touched || submittedDetails) && d.last_name.errors">
<div *ngIf="d.last_name.errors.required">Last name is required.</div>
</mat-error>
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Description</mat-label>
<textarea matInput formControlName="description" cdkTextareaAutosize class="form-control"
[ngClass]="{ 'is-dirty': (d.description.dirty), 'is-invalid': (d.description.dirty || d.description.touched || submittedDetails) && d.description.errors }">
</textarea>
</mat-form-field>
</mat-form-field>
<mat-error *ngIf="detailsHadError">
{{ detailsError }}
</mat-error>
<mat-error *ngIf="detailsHadError">
{{ detailsError }}
</mat-error>
<mat-action-row>
<button mat-raised-button [disabled]="!detailsForm.dirty">Save</button>
</mat-action-row>
</form>
</mat-expansion-panel>
<mat-action-row>
<button mat-raised-button [disabled]="!detailsForm.dirty">Save</button>
</mat-action-row>
</form>
</mat-expansion-panel>
<br />
<br />
<mat-expansion-panel expanded="true">
<mat-expansion-panel-header>
<mat-panel-title>Change Password</mat-panel-title>
</mat-expansion-panel-header>
<form [formGroup]="passwordForm" (ngSubmit)="changePassword()">
<mat-form-field class="form-field" appearance="standard">
<mat-label>Current Password</mat-label>
<input matInput required formControlName="current_password" type="password"
class="form-control"
[ngClass]="{ 'is-invalid': (p.current_password.dirty || p.current_password.touched || submittedPassword) && p.current_password.errors }" />
<mat-error *ngIf="(p.current_password.dirty || p.current_password.touched || submittedPassword) && p.current_password.errors">
<div *ngIf="p.current_password.errors.required">Current password is required.</div>
</mat-error>
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>New Password</mat-label>
<input matInput required formControlName="new_password" type="password"
class="form-control"
[ngClass]="{ 'is-invalid': (p.new_password.dirty || p.new_password.touched || submittedPassword) && p.new_password.errors }" />
<mat-error *ngIf="(p.new_password.dirty || p.new_password.touched || submittedPassword) && p.new_password.errors">
<div *ngIf="p.new_password.errors.required">New password is required.</div>
</mat-error>
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Confirm New Password</mat-label>
<input matInput required formControlName="new_password_confirm" type="password"
class="form-control"
[ngClass]="{ 'is-invalid': (p.new_password_confirm.dirty || p.new_password_confirm.touched || submittedPassword) && p.new_password_confirm.errors }" />
<mat-error *ngIf="(p.new_password_confirm.dirty || p.new_password_confirm.touched || submittedPassword) && p.new_password_confirm.errors">
<div *ngIf="p.new_password_confirm.errors.required">New password is required.</div>
<div *ngIf="p.new_password_confirm.errors.nonmatching">Passwords do not match.</div>
</mat-error>
</mat-form-field>
<mat-expansion-panel expanded="true">
<mat-expansion-panel-header>
<mat-panel-title>Change Password</mat-panel-title>
</mat-expansion-panel-header>
<form [formGroup]="passwordForm" (ngSubmit)="changePassword()">
<mat-error *ngIf="passwordHadError">
{{ passwordError }}
</mat-error>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Current Password</mat-label>
<input matInput required formControlName="current_password" type="password" class="form-control"
[ngClass]="{ 'is-invalid': (p.current_password.dirty || p.current_password.touched || submittedPassword) && p.current_password.errors }" />
<mat-error
*ngIf="(p.current_password.dirty || p.current_password.touched || submittedPassword) && p.current_password.errors">
<div *ngIf="p.current_password.errors.required">Current password is required.</div>
</mat-error>
</mat-form-field>
<mat-action-row>
<button mat-raised-button [disabled]="!passwordForm.dirty">Save</button>
</mat-action-row>
</form>
</mat-expansion-panel>
<mat-form-field class="form-field" appearance="standard">
<mat-label>New Password</mat-label>
<input matInput required formControlName="new_password" type="password" class="form-control"
[ngClass]="{ 'is-invalid': (p.new_password.dirty || p.new_password.touched || submittedPassword) && p.new_password.errors }" />
<mat-error
*ngIf="(p.new_password.dirty || p.new_password.touched || submittedPassword) && p.new_password.errors">
<div *ngIf="p.new_password.errors.required">New password is required.</div>
</mat-error>
</mat-form-field>
</mat-accordion>
</mat-card>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Confirm New Password</mat-label>
<input matInput required formControlName="new_password_confirm" type="password"
class="form-control"
[ngClass]="{ 'is-invalid': (p.new_password_confirm.dirty || p.new_password_confirm.touched || submittedPassword) && p.new_password_confirm.errors }" />
<mat-error
*ngIf="(p.new_password_confirm.dirty || p.new_password_confirm.touched || submittedPassword) && p.new_password_confirm.errors">
<div *ngIf="p.new_password_confirm.errors.required">New password is required.</div>
<div *ngIf="p.new_password_confirm.errors.nonmatching">Passwords do not match.</div>
</mat-error>
</mat-form-field>
<mat-error *ngIf="passwordHadError">
{{ passwordError }}
</mat-error>
<mat-action-row>
<button mat-raised-button [disabled]="!passwordForm.dirty">Save</button>
</mat-action-row>
</form>
</mat-expansion-panel>
</mat-accordion>
</div>

View File

@@ -1,13 +1,5 @@
/*(C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC. */
:host {
display: flex;
align-items: center;
justify-content: center;
padding-top: 1%;
padding-bottom: 1%
}
.form-card {
width: 100%;
}

View File

@@ -1,332 +1,295 @@
<!-- (C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC.-->
<mat-card class="form-card">
<mat-card-header>
<mat-card-title>
<h1>Add Collection</h1>
</mat-card-title>
</mat-card-header>
<mat-divider></mat-divider>
<h2 mat-dialog-title>Add Document Collection</h2>
<form *ngIf="!loading" [formGroup]="createForm" (ngSubmit)="create()">
<div *ngIf="loading">Loading...</div>
<form *ngIf="!loading" [formGroup]="createForm" (ngSubmit)="create()">
<mat-card-content id="content">
<div mat-dialog-content id="content">
<mat-form-field class="form-field" appearance="standard">
<mat-label>Collection Title</mat-label>
<input matInput required formControlName="metadata_title" type="text" placeholder="title"
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_title.dirty || f.metadata_title.touched || submitted) && f.metadata_title.errors }" />
<mat-error
*ngIf="(f.metadata_title.dirty || f.metadata_title.touched || submitted) && f.metadata_title.errors">
<div *ngIf="f.metadata_title.errors.required">Title is required.</div>
</mat-error>
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Collection Title</mat-label>
<input matInput required formControlName="metadata_title" type="text"
placeholder="title"
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_title.dirty || f.metadata_title.touched || submitted) && f.metadata_title.errors }" />
<mat-error *ngIf="(f.metadata_title.dirty || f.metadata_title.touched || submitted) && f.metadata_title.errors">
<div *ngIf="f.metadata_title.errors.required">Title is required.</div>
</mat-error>
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Description</mat-label>
<textarea matInput required formControlName="metadata_description" type="text"
cdkTextareaAutosize
#autosize="cdkTextareaAutosize"
cdkAutosizeMinRows="2"
cdkAutosizeMaxRows="10"
placeholder="Enter description..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_description.dirty || f.metadata_description.touched || submitted) && f.metadata_description.errors }">
<mat-form-field class="form-field" appearance="standard">
<mat-label>Description</mat-label>
<textarea matInput required formControlName="metadata_description" type="text" cdkTextareaAutosize
#autosize="cdkTextareaAutosize" cdkAutosizeMinRows="2" cdkAutosizeMaxRows="10"
placeholder="Enter description..." class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_description.dirty || f.metadata_description.touched || submitted) && f.metadata_description.errors }">
</textarea>
<mat-error *ngIf="(f.metadata_description.dirty || f.metadata_description.touched || submitted) && f.metadata_description.errors">
<div *ngIf="f.metadata_description.errors.required">Description is required.</div>
</mat-error>
</mat-form-field>
<mat-error
*ngIf="(f.metadata_description.dirty || f.metadata_description.touched || submitted) && f.metadata_description.errors">
<div *ngIf="f.metadata_description.errors.required">Description is required.</div>
</mat-error>
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Creator</mat-label>
<input matInput required formControlName="creator_name" type="text"
class="form-control" />
</mat-form-field>
<input matInput hidden required type="text"
formControlName="creator_id"
class="form-control" />
<mat-form-field class="form-field" appearance="standard">
<mat-label>Creator</mat-label>
<input matInput required formControlName="creator_name" type="text" class="form-control" />
</mat-form-field>
<input matInput hidden required type="text" formControlName="creator_id" class="form-control" />
<app-user-chooser #viewers
[formFieldClass]="'form-field'"
[formFieldAppearance]="'standard'"
[requireLoggedInUser]="true"
[label]="'Viewers'"
(userAdded)="viewersOrAnnotatorsChanged()"
(userRemoved)="viewersOrAnnotatorsChanged()">
</app-user-chooser>
<app-user-chooser #viewers [formFieldClass]="'form-field'" [formFieldAppearance]="'standard'"
[requireLoggedInUser]="true" [label]="'Viewers'" (userAdded)="viewersOrAnnotatorsChanged()"
(userRemoved)="viewersOrAnnotatorsChanged()">
</app-user-chooser>
<app-user-chooser #annotators
[formFieldClass]="'form-field'"
[formFieldAppearance]="'standard'"
[requireLoggedInUser]="true"
[label]="'Annotators'"
(userAdded)="viewersOrAnnotatorsChanged()"
(userRemoved)="viewersOrAnnotatorsChanged()">
</app-user-chooser>
<app-user-chooser #annotators [formFieldClass]="'form-field'" [formFieldAppearance]="'standard'"
[requireLoggedInUser]="true" [label]="'Annotators'" (userAdded)="viewersOrAnnotatorsChanged()"
(userRemoved)="viewersOrAnnotatorsChanged()">
</app-user-chooser>
<app-label-chooser #labels
[formFieldClass]="'form-field'"
[formFieldAppearance]="'standard'"
[label]="'Labels *'"
(labelAdded)="labelAdded()">
</app-label-chooser>
<app-label-chooser #labels [formFieldClass]="'form-field'" [formFieldAppearance]="'standard'"
[label]="'Labels *'" (labelAdded)="labelAdded()">
</app-label-chooser>
<mat-accordion multi>
<mat-expansion-panel expanded>
<mat-expansion-panel-header>
<mat-panel-title>Collection Documents</mat-panel-title>
<mat-panel-description>Add documents to this collection.</mat-panel-description>
</mat-expansion-panel-header>
<mat-form-field class="form-field" appearance="standard">
<mat-label>CSV File with Documents</mat-label>
<div style="display: flex; flex-direction: row;">
<input matInput type="text" readonly
formControlName="csv_file" />
<label for="file_upload" class="mat-raised-button">
Choose
</label>
</div>
<input hidden type="file" accept=".csv,text/csv"
id="file_upload" name="file_upload"
(change)="handleFileInput($event.target.files)" />
</mat-form-field>
<mat-accordion multi>
<mat-expansion-panel expanded>
<mat-expansion-panel-header>
<mat-panel-title>Collection Documents</mat-panel-title>
<mat-panel-description>Add documents to this collection.</mat-panel-description>
</mat-expansion-panel-header>
<mat-form-field class="form-field" appearance="standard">
<mat-label>CSV File with Documents</mat-label>
<div style="display: flex; flex-direction: row;">
<input matInput type="text" readonly formControlName="csv_file" />
<label for="file_upload" class="mat-raised-button">
Choose
</label>
</div>
<input hidden type="file" accept=".csv,text/csv" id="file_upload" name="file_upload"
(change)="handleFileInput($event.target.files)" />
</mat-form-field>
<mat-checkbox *ngIf="hasCsvFile" matInput formControlName="csv_has_header">
CSV has header row
</mat-checkbox>
<mat-checkbox *ngIf="hasCsvFile" matInput formControlName="csv_has_header">
CSV has header row
</mat-checkbox>
<mat-form-field *ngIf="hasCsvFile && f.csv_has_header.value" class="form-field" appearance="standard">
<mat-label>CSV Text Column</mat-label>
<mat-select matInput formControlName="csv_text_col"
placeholder="CSV Text Column"
class="form-control"
selectionChange
[ngClass]="{ 'is-invalid': (f.csv_text_col.dirty || f.csv_text_col.touched || submitted) && f.csv_text_col.errors }">
<mat-option *ngFor="let header of csvHeader; index as i" [value]="i">
{{ header }}
</mat-option>
</mat-select>
</mat-form-field>
</mat-expansion-panel>
<mat-form-field *ngIf="hasCsvFile && f.csv_has_header.value" class="form-field" appearance="standard">
<mat-label>CSV Text Column</mat-label>
<mat-select matInput formControlName="csv_text_col" placeholder="CSV Text Column"
class="form-control" selectionChange
[ngClass]="{ 'is-invalid': (f.csv_text_col.dirty || f.csv_text_col.touched || submitted) && f.csv_text_col.errors }">
<mat-option *ngFor="let header of csvHeader; index as i" [value]="i">
{{ header }}
</mat-option>
</mat-select>
</mat-form-field>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Collection Images</mat-panel-title>
<mat-panel-description>Add images to this collection.</mat-panel-description>
</mat-expansion-panel-header>
<app-image-collection-uploader></app-image-collection-uploader>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Collection Images</mat-panel-title>
<mat-panel-description>Add images to this collection.</mat-panel-description>
</mat-expansion-panel-header>
<app-image-collection-uploader></app-image-collection-uploader>
</mat-expansion-panel>
<mat-expansion-panel expanded>
<mat-expansion-panel-header>
<mat-panel-title>Classifier</mat-panel-title>
<mat-panel-description>Customize classifier for this collection.</mat-panel-description>
</mat-expansion-panel-header>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Pipeline</mat-label>
<mat-select required formControlName="pipeline_id"
placeholder="pipeline"
class="form-control"
selectionChange
[ngClass]="{ 'is-invalid': (f.pipeline_id.dirty || f.pipeline_id.touched || submitted) && f.pipeline_id.errors }">
<mat-option *ngFor="let pipeline of pipelines" [value]="pipeline._id">
{{ pipeline.title }}
</mat-option>
</mat-select>
<mat-error *ngIf="(f.pipeline_id.dirty || f.pipeline_id.touched || submitted) && f.pipeline_id.errors">
<div *ngIf="f.pipeline_id.errors.required">Pipeline is required.</div>
</mat-error>
</mat-form-field>
<div *ngIf="f.pipeline_id.value"><b>Description:</b> {{ pipelineDescription(f.pipeline_id.value) }}</div>
<div class="form-field">
<mat-expansion-panel expanded>
<mat-expansion-panel-header>
<mat-panel-title>Classifier</mat-panel-title>
<mat-panel-description>Customize classifier for this collection.</mat-panel-description>
</mat-expansion-panel-header>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Pipeline</mat-label>
<mat-select required formControlName="pipeline_id" placeholder="pipeline" class="form-control"
selectionChange
[ngClass]="{ 'is-invalid': (f.pipeline_id.dirty || f.pipeline_id.touched || submitted) && f.pipeline_id.errors }">
<mat-option *ngFor="let pipeline of pipelines" [value]="pipeline._id">
{{ pipeline.title }}
</mat-option>
</mat-select>
<mat-error
*ngIf="(f.pipeline_id.dirty || f.pipeline_id.touched || submitted) && f.pipeline_id.errors">
<div *ngIf="f.pipeline_id.errors.required">Pipeline is required.</div>
</mat-error>
</mat-form-field>
<div *ngIf="f.pipeline_id.value"><b>Description:</b> {{ pipelineDescription(f.pipeline_id.value) }}
</div>
<div class="form-field">
<mat-form-field appearance="standard">
<mat-label>Train Model Every</mat-label>
<input matInput required formControlName="train_every" type="number"
min="1"
placeholder="train every"
class="form-control"
[ngClass]="{ 'is-invalid': (f.train_every.dirty || f.train_every.touched || submitted) && f.train_every.errors }" />
<input matInput required formControlName="train_every" type="number" min="1"
placeholder="train every" class="form-control"
[ngClass]="{ 'is-invalid': (f.train_every.dirty || f.train_every.touched || submitted) && f.train_every.errors }" />
<mat-hint>Documents</mat-hint>
<mat-error *ngIf="(f.train_every.dirty || f.train_every.touched || submitted) && f.train_every.errors">
<mat-error
*ngIf="(f.train_every.dirty || f.train_every.touched || submitted) && f.train_every.errors">
<div *ngIf="f.train_every.errors.required">Value is required.</div>
<div *ngIf="f.train_every.errors.min">Minimum allowed value is {{f.train_every.errors.min["min"]}}.</div>
<div *ngIf="f.train_every.errors.min">Minimum allowed value is
{{f.train_every.errors.min["min"]}}.</div>
</mat-error>
</mat-form-field>
<div class="space"></div>
<mat-form-field appearance="standard">
<mat-label>Document Overlap</mat-label>
<input matInput required formControlName="overlap" type="number"
min="0" max="1"
placeholder="overlap"
class="form-control"
[ngClass]="{ 'is-invalid': (f.overlap.dirty || f.overlap.touched || submitted) && f.overlap.errors }" />
<input matInput required formControlName="overlap" type="number" min="0" max="1"
placeholder="overlap" class="form-control"
[ngClass]="{ 'is-invalid': (f.overlap.dirty || f.overlap.touched || submitted) && f.overlap.errors }" />
<mat-error *ngIf="(f.overlap.dirty || f.overlap.touched || submitted) && f.overlap.errors">
<div *ngIf="f.overlap.errors.required">Overlap is required.</div>
<div *ngIf="f.overlap.errors.max">Maximum allowed value is {{f.overlap.errors.max["max"]}}.</div>
<div *ngIf="f.overlap.errors.min">Minimum allowed value is {{f.overlap.errors.min["min"]}}.</div>
<div *ngIf="f.overlap.errors.max">Maximum allowed value is
{{f.overlap.errors.max["max"]}}.</div>
<div *ngIf="f.overlap.errors.min">Minimum allowed value is
{{f.overlap.errors.min["min"]}}.</div>
</mat-error>
</mat-form-field>
</div>
</div>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Classifier Parameters</mat-label>
<textarea matInput formControlName="classifier_parameters" type="text"
cdkTextareaAutosize
#autosize="cdkTextareaAutosize"
cdkAutosizeMinRows="2"
cdkAutosizeMaxRows="20"
placeholder="Enter parameters in JSON format"
class="form-control"
[ngClass]="{ 'is-invalid': (f.classifier_parameters.dirty || f.classifier_parameters.touched || submitted) && f.classifier_parameters.errors }">
<mat-form-field class="form-field" appearance="standard">
<mat-label>Classifier Parameters</mat-label>
<textarea matInput formControlName="classifier_parameters" type="text" cdkTextareaAutosize
#autosize="cdkTextareaAutosize" cdkAutosizeMinRows="2" cdkAutosizeMaxRows="20"
placeholder="Enter parameters in JSON format" class="form-control"
[ngClass]="{ 'is-invalid': (f.classifier_parameters.dirty || f.classifier_parameters.touched || submitted) && f.classifier_parameters.errors }">
</textarea>
<mat-error *ngIf="(f.classifier_parameters.dirty || f.classifier_parameters.touched || submitted) && f.classifier_parameters.errors">
<div *ngIf="f.classifier_parameters.errors.invalid_json">JSON format is invalid: {{f.classifier_parameters.errors.invalid_json["error"]}}.</div>
</mat-error>
</mat-form-field>
<mat-error
*ngIf="(f.classifier_parameters.dirty || f.classifier_parameters.touched || submitted) && f.classifier_parameters.errors">
<div *ngIf="f.classifier_parameters.errors.invalid_json">JSON format is invalid:
{{f.classifier_parameters.errors.invalid_json["error"]}}.</div>
</mat-error>
</mat-form-field>
</mat-expansion-panel>
</mat-expansion-panel>
<mat-expansion-panel expanded>
<mat-expansion-panel-header>
<mat-panel-title>Configuration</mat-panel-title>
<mat-panel-description>Configure collection settings</mat-panel-description>
</mat-expansion-panel-header>
<mat-expansion-panel expanded>
<mat-expansion-panel-header>
<mat-panel-title>Configuration</mat-panel-title>
<mat-panel-description>Configure collection settings</mat-panel-description>
</mat-expansion-panel-header>
<mat-checkbox [checked]="configAllowOverlappingNerAnnotations" (change)="configAllowOverlappingNerAnnotations = !configAllowOverlappingNerAnnotations">
Allow overlapping NER annotations
</mat-checkbox>
</mat-expansion-panel>
<mat-checkbox [checked]="configAllowOverlappingNerAnnotations"
(change)="configAllowOverlappingNerAnnotations = !configAllowOverlappingNerAnnotations">
Allow overlapping NER annotations
</mat-checkbox>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Metadata</mat-panel-title>
<mat-panel-description>Enter additional metadata</mat-panel-description>
</mat-expansion-panel-header>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Metadata</mat-panel-title>
<mat-panel-description>Enter additional metadata</mat-panel-description>
</mat-expansion-panel-header>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Subject</mat-label>
<input matInput formControlName="metadata_subject" type="text"
placeholder="Enter subject..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_subject.dirty || f.metadata_subject.touched || submitted) && f.metadata_subject.errors }" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Subject</mat-label>
<input matInput formControlName="metadata_subject" type="text" placeholder="Enter subject..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_subject.dirty || f.metadata_subject.touched || submitted) && f.metadata_subject.errors }" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Publisher</mat-label>
<input matInput formControlName="metadata_publisher" type="text"
placeholder="Enter publisher..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_publisher.dirty || f.metadata_publisher.touched || submitted) && f.metadata_publisher.errors }" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Publisher</mat-label>
<input matInput formControlName="metadata_publisher" type="text" placeholder="Enter publisher..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_publisher.dirty || f.metadata_publisher.touched || submitted) && f.metadata_publisher.errors }" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Contributor</mat-label>
<input matInput formControlName="metadata_contributor" type="text"
placeholder="Enter contributor..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_contributor.dirty || f.metadata_contributor.touched || submitted) && f.metadata_contributor.errors }" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Contributor</mat-label>
<input matInput formControlName="metadata_contributor" type="text"
placeholder="Enter contributor..." class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_contributor.dirty || f.metadata_contributor.touched || submitted) && f.metadata_contributor.errors }" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Date</mat-label>
<input matInput formControlName="metadata_date" [matDatepicker]="datePicker"
placeholder="Choose a date..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_date.dirty || f.metadata_date.touched || submitted) && f.metadata_date.errors }">
<mat-datepicker-toggle matSuffix [for]="datePicker"></mat-datepicker-toggle>
<mat-datepicker #datePicker></mat-datepicker>
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Type</mat-label>
<input matInput formControlName="metadata_type" type="text"
placeholder="Enter type..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_type.dirty || f.metadata_type.touched || submitted) && f.metadata_type.errors }" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Format</mat-label>
<input matInput formControlName="metadata_format" type="text"
placeholder="Enter format..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_format.dirty || f.metadata_format.touched || submitted) && f.metadata_format.errors }" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Identifier</mat-label>
<input matInput formControlName="metadata_identifier" type="text"
placeholder="Enter identifier..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_identifier.dirty || f.metadata_identifier.touched || submitted) && f.metadata_identifier.errors }" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Source</mat-label>
<input matInput formControlName="metadata_source" type="text"
placeholder="Enter source..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_source.dirty || f.metadata_source.touched || submitted) && f.metadata_source.errors }" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Language</mat-label>
<input matInput formControlName="metadata_language" type="text"
placeholder="Enter language..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_language.dirty || f.metadata_language.touched || submitted) && f.metadata_language.errors }" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Relation</mat-label>
<input matInput formControlName="metadata_relation" type="text"
placeholder="Enter relation..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_relation.dirty || f.metadata_relation.touched || submitted) && f.metadata_relation.errors }" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Coverage</mat-label>
<input matInput formControlName="metadata_coverage" type="text"
placeholder="Enter coverage..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_coverage.dirty || f.metadata_coverage.touched || submitted) && f.metadata_coverage.errors }" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Rights</mat-label>
<input matInput formControlName="metadata_rights" type="text"
placeholder="Enter rights..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_rights.dirty || f.metadata_rights.touched || submitted) && f.metadata_rights.errors }" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Date</mat-label>
<input matInput formControlName="metadata_date" [matDatepicker]="datePicker"
placeholder="Choose a date..." class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_date.dirty || f.metadata_date.touched || submitted) && f.metadata_date.errors }">
<mat-datepicker-toggle matSuffix [for]="datePicker"></mat-datepicker-toggle>
<mat-datepicker #datePicker></mat-datepicker>
</mat-form-field>
</mat-expansion-panel>
</mat-accordion>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Type</mat-label>
<input matInput formControlName="metadata_type" type="text" placeholder="Enter type..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_type.dirty || f.metadata_type.touched || submitted) && f.metadata_type.errors }" />
</mat-form-field>
<mat-error *ngIf="hadError">
{{ errorMessage }}
</mat-error>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Format</mat-label>
<input matInput formControlName="metadata_format" type="text" placeholder="Enter format..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_format.dirty || f.metadata_format.touched || submitted) && f.metadata_format.errors }" />
</mat-form-field>
<mat-card *ngIf="submitting" class="submitting">
<mat-card-title>
<h2 *ngIf="submittingPercent < 100">Uploading Files...</h2>
<h2 *ngIf="submittingPercent >= 100">Processing...</h2>
</mat-card-title>
<mat-card-content>
<mat-progress-spinner [mode]="submittingPercent < 100 ? 'determinate' : 'indeterminate'"
[value]="submittingPercent"></mat-progress-spinner>
</mat-card-content>
</mat-card>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Identifier</mat-label>
<input matInput formControlName="metadata_identifier" type="text" placeholder="Enter identifier..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_identifier.dirty || f.metadata_identifier.touched || submitted) && f.metadata_identifier.errors }" />
</mat-form-field>
</mat-card-content>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Source</mat-label>
<input matInput formControlName="metadata_source" type="text" placeholder="Enter source..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_source.dirty || f.metadata_source.touched || submitted) && f.metadata_source.errors }" />
</mat-form-field>
<mat-card-actions>
<button mat-raised-button [disabled]="loading"><span class="material-icons">save</span>Save</button>
<button mat-raised-button [routerLink]="[appConfig.landingPage]" type="button"><span class="material-icons">cancel</span>Cancel</button>
</mat-card-actions>
</form>
</mat-card>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Language</mat-label>
<input matInput formControlName="metadata_language" type="text" placeholder="Enter language..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_language.dirty || f.metadata_language.touched || submitted) && f.metadata_language.errors }" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Relation</mat-label>
<input matInput formControlName="metadata_relation" type="text" placeholder="Enter relation..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_relation.dirty || f.metadata_relation.touched || submitted) && f.metadata_relation.errors }" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Coverage</mat-label>
<input matInput formControlName="metadata_coverage" type="text" placeholder="Enter coverage..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_coverage.dirty || f.metadata_coverage.touched || submitted) && f.metadata_coverage.errors }" />
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Rights</mat-label>
<input matInput formControlName="metadata_rights" type="text" placeholder="Enter rights..."
class="form-control"
[ngClass]="{ 'is-invalid': (f.metadata_rights.dirty || f.metadata_rights.touched || submitted) && f.metadata_rights.errors }" />
</mat-form-field>
</mat-expansion-panel>
</mat-accordion>
<mat-error *ngIf="hadError">
{{ errorMessage }}
</mat-error>
<mat-card *ngIf="submitting" class="submitting">
<mat-card-title>
<h2 *ngIf="submittingPercent < 100">Uploading Files...</h2>
<h2 *ngIf="submittingPercent >= 100">Processing...</h2>
</mat-card-title>
<mat-card-content>
<mat-progress-spinner [mode]="submittingPercent < 100 ? 'determinate' : 'indeterminate'"
[value]="submittingPercent"></mat-progress-spinner>
</mat-card-content>
</mat-card>
</div>
<div mat-dialog-actions>
<button mat-raised-button type="button" [mat-dialog-close]="false">
<mat-icon>cancel</mat-icon>Cancel
</button>
<span fxFlex></span>
<mat-spinner *ngIf="loading" [diameter]="28"></mat-spinner>
<span fxFlex="10px"></span>
<button mat-raised-button color="primary" [disabled]="loading">
<mat-icon>save</mat-icon>Save
</button>
</div>
</form>

View File

@@ -23,6 +23,7 @@ import { PATHS } from "../../app.paths";
import { Collection, CONFIG_ALLOW_OVERLAPPING_NER_ANNOTATIONS } from "../../model/collection";
import { Pipeline } from "../../model/pipeline";
import { CreatedObject } from "../../model/created";
import { MatDialogRef } from '@angular/material';
@Component({
selector: "app-add-collection",
@@ -59,7 +60,8 @@ export class AddCollectionComponent implements OnInit {
private formBuilder: FormBuilder,
private router: Router,
private event: EventService,
private pipeline: PipelineService) {
private pipeline: PipelineService,
public dialogRef: MatDialogRef<AddCollectionComponent>) {
}
ngOnInit() {
@@ -190,6 +192,8 @@ export class AddCollectionComponent implements OnInit {
return;
}
this.loading = true;
const collection = <Collection>{};
collection.creator_id = this.f.creator_id.value;
collection.annotators = this.annotators.getChosenUserIds();
@@ -259,7 +263,7 @@ export class AddCollectionComponent implements OnInit {
this.collectionRepository.getCollectionDetails(collectionId).pipe(take(1)).subscribe((collection: Collection) => {
this.event.showUserMessage.emit("Successfully added collection with ID " + collectionId);
this.event.collectionAddedOrArchived.emit(collection);
this.router.navigate([PATHS.collection.details, collectionId]);
this.dialogRef.close(collection);
}, (error: HttpErrorResponse) => {
this.errorMessage = "Error: " + error.error;
this.hadError = true;
@@ -268,11 +272,14 @@ export class AddCollectionComponent implements OnInit {
default:
break;
}
this.loading = false;
}, (error: HttpErrorResponse) => {
this.loading = false;
this.errorMessage = "Error: " + error.error;
this.hadError = true;
}, () => {
subs.unsubscribe();
this.loading = false;
this.submitting = false;
this.createForm.enable();
});

View File

@@ -1,23 +1,21 @@
/*(C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC. */
:host {
display: flex;
flex-direction: column;
}
mat-dialog-content {
border-bottom: 1px solid rgba(0,0,0,.12);
}
.form-card {
width: 100%;
}
.content {
padding-top: 3%;
}
.file-upload-display {
float: left;
}
.file-upload-button {
float: right;
}
:host {
display: flex;
align-items: center;
justify-content: center;
}
}

View File

@@ -1,62 +1,53 @@
<!-- (C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC.-->
<mat-card class="form-card">
<mat-card-header>
<mat-card-title>
<h1>Add Document to Collection</h1>
</mat-card-title>
</mat-card-header>
<mat-divider></mat-divider>
<h2 mat-dialog-title>Add Document to Collection</h2>
<form [formGroup]="createForm" (ngSubmit)="create()">
<mat-card-content class="content">
<mat-dialog-content>
<form [formGroup]="createForm">
<mat-form-field class="form-field" appearance="standard">
<mat-label>Creator</mat-label>
<input matInput required formControlName="creator_name" type="text" class="form-control" />
</mat-form-field>
<input matInput hidden required type="text" formControlName="creator_id" class="form-control" />
<mat-form-field class="form-field" appearance="standard">
<mat-label>Creator</mat-label>
<input matInput required formControlName="creator_name" type="text"
class="form-control" />
</mat-form-field>
<input matInput hidden required type="text"
formControlName="creator_id"
class="form-control"/>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Collection ID</mat-label>
<input matInput required type="text" formControlName="collection_id" class="form-control">
</mat-form-field>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Collection ID</mat-label>
<input matInput required type="text"
formControlName="collection_id"
class="form-control">
</mat-form-field>
<app-image-chooser [collectionId]="collection_id"></app-image-chooser>
<app-image-chooser [collectionId]="collection_id"></app-image-chooser>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Document text</mat-label>
<textarea matInput required readonly type="text" formControlName="text" cdkTextareaAutosize
#autosize="cdkTextareaAutosize" cdkAutosizeMinRows="5" cdkAutosizeMaxRows="30"></textarea>
<mat-form-field class="form-field" appearance="standard">
<mat-label>Document text</mat-label>
<textarea matInput required readonly type="text"
formControlName="text"
cdkTextareaAutosize
#autosize="cdkTextareaAutosize"
cdkAutosizeMinRows="5"
cdkAutosizeMaxRows="30"></textarea>
<input hidden type="file" accept=".txt,text/plain" #textFile
[id]="uuidv4 + '-file_upload'" name="file_upload"
(change)="handleFileInput($event.target.files)" />
<label [for]="textFile.id" class="mat-raised-button">
Click to choose text file for document
</label>
<mat-error *ngIf="(f.text.dirty || f.text.touched || submitted) && f.text.errors">
<div *ngIf="f.text.errors['required']">Document text is required.</div>
</mat-error>
</mat-form-field>
</mat-card-content>
<input hidden type="file" accept=".txt,text/plain" #textFile [id]="uuidv4 + '-file_upload'"
name="file_upload" (change)="handleFileInput($event.target.files)" />
<label [for]="textFile.id" class="mat-raised-button">
Click to choose text file for document
</label>
<mat-error *ngIf="(f.text.dirty || f.text.touched || submitted) && f.text.errors">
<div *ngIf="f.text.errors['required']">Document text is required.</div>
</mat-error>
</mat-form-field>
<mat-error *ngIf="hadError">
{{ errorMessage }}
</mat-error>
<mat-card-actions>
<button mat-raised-button [disabled]="loading"><span class="material-icons">save</span>Save</button>
<button mat-raised-button [routerLink]="['/' + PATHS.collection.details, collection_id]"><span class="material-icons">cancel</span>Cancel</button>
</mat-card-actions>
</form>
</mat-card>
</mat-dialog-content>
<div mat-dialog-actions>
<button mat-raised-button [mat-dialog-close]="false">
<mat-icon>cancel</mat-icon>
<span>&nbsp;Cancel</span>
</button>
<span fxFlex></span>
<mat-spinner *ngIf="loading" [diameter]="28"></mat-spinner>
<span fxFlex="10px"></span>
<button mat-raised-button color="primary" [disabled]="loading" (click)="create()">
<mat-icon>save</mat-icon>
<span>&nbsp;Save</span>
</button>
</div>

View File

@@ -1,8 +1,6 @@
/*(C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC. */
import { Component, OnInit, AfterViewInit, ViewChild } from "@angular/core";
import { Component, OnInit, AfterViewInit, ViewChild, Inject } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { Router } from "@angular/router";
import { HttpErrorResponse } from "@angular/common/http";
import { take } from "rxjs/operators";
@@ -11,7 +9,6 @@ import { PATHS } from "../../app.paths";
import { AuthService } from "../../service/auth/auth.service";
import { DocumentRepositoryService } from "../../service/document-repository/document-repository.service";
import { CollectionRepositoryService } from "../../service/collection-repository/collection-repository.service";
import { EventService } from "../../service/event/event.service";
import { Document } from "../../model/document";
@@ -19,6 +16,12 @@ import { CreatedObject } from "../../model/created";
import { ImageChooserComponent } from "../image-chooser/image-chooser.component";
import { uuidv4 } from "../util";
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { Collection } from 'src/app/model/collection';
export class AddDocumentDialogData {
collection: Collection;
}
@Component({
selector: "app-add-document",
@@ -44,13 +47,12 @@ export class AddDocumentComponent implements OnInit, AfterViewInit {
@ViewChild(ImageChooserComponent)
public imageChooser: ImageChooserComponent;
constructor(private route: ActivatedRoute,
private router: Router,
private auth: AuthService,
constructor(private auth: AuthService,
private event: EventService,
private formBuilder: FormBuilder,
private documentRepository: DocumentRepositoryService,
private collectionRepository: CollectionRepositoryService) {
public dialogRef: MatDialogRef<AddDocumentComponent>,
@Inject(MAT_DIALOG_DATA) public data: AddDocumentDialogData) {
this.createForm = this.formBuilder.group({
creator_name: [{value: this.auth.loggedInUser.display_name, disabled: true},
[Validators.required]],
@@ -64,11 +66,11 @@ export class AddDocumentComponent implements OnInit, AfterViewInit {
ngOnInit() {
this.loading = true;
this.route.paramMap.subscribe(params => {
this.collection_id = params.get("collection_id");
this.f.collection_id.setValue(this.collection_id);
if(this.data.collection) {
this.collection_id = this.data.collection._id;
this.f.collection_id.setValue(this.data.collection._id);
this.loading = false;
});
}
}
ngAfterViewInit() {
@@ -95,6 +97,8 @@ export class AddDocumentComponent implements OnInit, AfterViewInit {
return;
}
this.loading = true;
const document = <Document>{};
document.creator_id = this.f.creator_id.value;
document.collection_id = this.f.collection_id.value;
@@ -108,17 +112,20 @@ export class AddDocumentComponent implements OnInit, AfterViewInit {
console.log(document);
this.documentRepository.postDocument(document).subscribe(
(createdDocument: CreatedObject) => {
this.loading = false;
const documentId = createdDocument._id;
this.event.showUserMessage.emit("Successfully added document with ID " + documentId);
this.event.documentAddedById.emit({collectionId: this.collection_id, documentId: documentId});
this.router.navigate([`/${PATHS.document.annotate}`, documentId]);
this.dialogRef.close(true);
}, (error: HttpErrorResponse) => {
this.loading = false;
console.error(error);
this.errorMessage = "Error: " + JSON.stringify(error["error"]);
this.hadError = true;
}
);
}, (error) => {
this.loading = false;
console.error(error);
this.errorMessage = "Error uploading image: " + JSON.stringify(error["error"]);
this.hadError = true;

View File

@@ -1,5 +1,123 @@
/*(C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC. */
:host {
display: block;
height: 100%;
width: 100%;
overflow-y: auto;
}
.page-container {
height: 100%;
width: 100%;
position: relative;
}
.title-toolbar {
position: absolute;
top: 0px;
width: 100%;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.page-content {
position: absolute;
top: 64px;
right: 0px;
bottom: 0px;
left: 0px;
overflow-y: auto;
}
/* annotation tab */
.filter-bar {
position: absolute;
top: 0px;
height: 58px;
padding: 0px 16px;
width: 100%;
display: flex;
align-items: center;
flex-direction: row;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.annotate-area {
position: absolute;
top: 58px;
bottom: 58px;
right: 0px;
left: 0px;
display: flex;
flex-direction: row;
overflow-y: hidden;
}
.annotate-table-container {
position: relative;
}
.action-bar {
position: absolute;
bottom: 0px;
height: 58px;
padding: 0px 16px;
width: 100%;
display: flex;
align-items: center;
flex-direction: row;
border-top: 1px solid rgba(0, 0, 0, 0.12);
background-color: whitesmoke;
}
.annotate-doc-container {
position: relative;
border-left: 1px solid rgba(0, 0, 0, 0.12);
}
.annotate-doc-toolbar .mat-title {
margin-bottom: 0px;
}
.doc-labeling-container {
padding: 28px;
}
.annotate-table-wrapper {
position: absolute;
top: 56px;
right: 0;
left: 0;
bottom: 0;
overflow-y: auto;
}
.annotate-doc-toolbar {
display: flex;
align-items: center;
padding: 8px 16px;
height: 56px;
position: absolute;
top: 0px;
width: 100%;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.title-tabs {
position: relative;
height: 100%;
}
::ng-deep .title-tabs mat-tab-header {
position: absolute;
width: 100%;
height: 100%;
bottom: 0px;
border-bottom: 0;
}
::ng-deep .title-tabs .mat-tab-labels, ::ng-deep .title-tabs .mat-tab-label {
height: 100%;
}
#container {
}
@@ -7,7 +125,7 @@
.image-container {
position: relative;
width: 100%;
height: 400px;
height: 100%;
background-color: lightgray;
}
@@ -35,6 +153,13 @@
}
#doc {
position: absolute;
top: 56px;
right: 0;
left: 0;
bottom: 0;
padding: 10px 22px;
overflow-y: auto;
line-height: 1.5;
-webkit-user-select: none;
-moz-user-select: none;
@@ -44,11 +169,11 @@
.monospace {
font-family: monospace;
font-size: 16pt;
font-size: 14pt;
}
.nonmonospace {
font-size: 18pt;
font-size: 16pt;
}
.word:focus {
@@ -108,6 +233,10 @@
}
.doc-label-list {
align-items: center;
}
#container mat-card-actions {
padding-left: 9px;
padding-top: 15px;

View File

@@ -1,178 +1,184 @@
<!-- (C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC.-->
<app-loading></app-loading>
<div fxFlexFill class="page-container" fxLayout="column">
<mat-toolbar class="title-toolbar">
<button class="doc-back-button" mat-icon-button matTooltip="Go back to collection details" (click)="backToCollectionDetails()">
<mat-icon>keyboard_arrow_left</mat-icon>
</button>
<span class="page-title">Document {{doc?._id}}</span>
<span fxFlex="22px"></span>
<mat-tab-group *ngIf="collection" class="title-tabs" fxFlex mat-stretch-tabs [(selectedIndex)]="tabIndex"
(selectedIndexChange)="updateTabInUrl()">
<mat-tab class="tab-annotations" label="Annotations"></mat-tab>
<mat-tab class="tab-image" label="Image"></mat-tab>
<mat-tab class="tab-details" label="Details"></mat-tab>
</mat-tab-group>
</mat-toolbar>
<mat-card *ngIf="!loading.loading && !loading.error" id="container">
<div class="page-content">
<mat-accordion multi="true">
<app-loading></app-loading>
<section *ngIf="tabIndex == 0 && !loading.loading && !loading.error" #annotateSection>
<div class="filter-bar">
<button mat-icon-button (click)="showList = !showList">
<mat-icon>list</mat-icon>
</button>
<app-document-details [expanded]="false"
[document]="doc"
[collection]="collection"
(imageUrlChanged)="imageChanged($event)">
</app-document-details>
<span fxFlex="22px"></span>
<mat-error *ngIf="!canAnnotate" id="cantAnnotate">
<h3>Note: you do not have authority to change or add annotations for this document.</h3>
</mat-error>
<mat-expansion-panel id="myDocAnnotations" expanded="true">
<mat-expansion-panel-header>
<mat-panel-title>
<h2>Document Labeling</h2>
</mat-panel-title>
<mat-panel-description>
<h3>Click labels to annotate entire document</h3>
</mat-panel-description>
</mat-expansion-panel-header>
<mat-chip-list>
<mat-checkbox *ngFor="let annotation of myDocAnnotations;" [(ngModel)]="annotation.checked">
<mat-chip [style.background-color]="annotation.label.color" class="shadowed cursor-pointer">{{annotation.label.name}}</mat-chip>
</mat-checkbox>
</mat-chip-list>
</mat-expansion-panel>
<mat-expansion-panel id="myDocImage" expanded="true" *ngIf="doc.metadata && doc.metadata['imageUrl']">
<mat-expansion-panel-header>
<mat-panel-title>
<h2>Image</h2>
</mat-panel-title>
<mat-panel-description>
<h3>View document image</h3>
</mat-panel-description>
</mat-expansion-panel-header>
<div #imageContainer class="image-container">
<div style="position: absolute; top: 0px; bottom: 0; left: 0; right: 0;">
<button class="full-screen-btn" mat-raised-button (click)="toggleImageFullscreen()">{{ isImageFullscreen() ? 'Close' : 'Open' }} Full Screen</button>
<app-image-explorer [imageUrl]="doc.metadata['imageUrl']" [documentId]="doc._id" [collectionId]="collection._id"></app-image-explorer>
<span *ngIf="others.length === 0">No annotations from other users.</span>
<div *ngIf="others.length > 0" id="others">
<mat-form-field fxFlex="180px" floatLabel="never">
<mat-label>Show Annotations:</mat-label>
<mat-select id="othersAnnotations" value="" #othersSelect>
<mat-option value="" (click)="showAnnotationsOf(othersSelect, null)">
Mine
</mat-option>
<mat-option *ngFor="let other of others" [value]="other"
(click)="showAnnotationsOf(othersSelect, other)">
{{ auth.getUserDisplayName(other) }}</mat-option>
</mat-select>
</mat-form-field>
<span fxFlex="10px"></span>
<mat-chip-list
*ngIf="othersSelect.value && othersDocAnnotations.hasOwnProperty(othersSelect.value) && othersDocAnnotations[othersSelect.value].length > 0">
<mat-chip *ngFor="let label of othersDocAnnotations[othersSelect.value]"
[style.background-color]="getColorFor(label)">
{{label}}
</mat-chip>
</mat-chip-list>
<span
*ngIf="othersSelect.value && (!othersDocAnnotations.hasOwnProperty(othersSelect.value) || othersDocAnnotations[othersSelect.value].length === 0)">No
labels for this document.</span>
</div>
<span fxFlex></span>
<div>
<span>
<b>
Document Overall Agreement:
</b>
<span *ngIf="ann_agreement != undefined">{{ann_agreement | percent:'1.2-2'}}</span>
<span *ngIf="ann_agreement == undefined">N/A</span>
</span>
</div>
</div>
</mat-expansion-panel>
<mat-expansion-panel expanded="true">
<mat-expansion-panel-header>
<mat-panel-title>
<h2>Others' Labels and Annotations</h2>
</mat-panel-title>
<mat-panel-description>
<h3>See how others have labeled and annotated this document</h3>
</mat-panel-description>
</mat-expansion-panel-header>
<span *ngIf="others.length === 0">No annotations from other users.</span>
<div *ngIf="others.length > 0" id="others">
<table>
<tr>
<td>
<div>
<span>
<b>
Document Overall Agreement:
</b>
{{ann_agreement | percent:'1.2-2'}}
</span>
</div>
</td>
</tr>
<tr>
<td>
<div>
<mat-form-field>
<mat-label>Show Annotations:</mat-label>
<mat-select id="othersAnnotations" value="" #othersSelect>
<mat-option value="" (click)="showAnnotationsOf(othersSelect, null)">Mine
</mat-option>
<mat-option *ngFor="let other of others" [value]="other"
(click)="showAnnotationsOf(othersSelect, other)">
{{ auth.getUserDisplayName(other) }}</mat-option>
</mat-select>
</mat-form-field>
<mat-chip-list
*ngIf="othersSelect.value && othersDocAnnotations.hasOwnProperty(othersSelect.value) && othersDocAnnotations[othersSelect.value].length > 0">
<mat-chip *ngFor="let label of othersDocAnnotations[othersSelect.value]"
[style.background-color]="getColorFor(label)">
{{label}}
</mat-chip>
</mat-chip-list>
<span
*ngIf="othersSelect.value && (!othersDocAnnotations.hasOwnProperty(othersSelect.value) || othersDocAnnotations[othersSelect.value].length === 0)">No
labels for this document.</span>
</div>
</td>
</tr>
</table>
<div class="annotate-area">
<div *ngIf="showList" class="annotate-table-container" fxFlex="30%">
<app-ner-annotation-table [labels]="availableLabels" [data]="nerData"
(remove)="removeAnnotation($event)">
</app-ner-annotation-table>
</div>
<div class="annotate-doc-container" fxFlex>
<div class="annotate-doc-toolbar" fxLayout="row">
<span class="mat-title">NER Annotations</span>
<span fxFlex></span>
<span *ngIf="showingAnnotationsFor === null">Click to select text; right-click to annotate
selection</span>
<span *ngIf="showingAnnotationsFor !== null">Showing
{{ auth.getUserDisplayName(showingAnnotationsFor) }}'s
annotations in read-only mode</span>
<span fxFlex="10px"></span>
<mat-menu #settingsMenu="matMenu" id="settings">
<button>
<mat-checkbox matMenuItem [(ngModel)]="settingMonospace"
(click)="$event.stopPropagation()" class="mat-menu-item">
Monospace font
</mat-checkbox>
</button>
</mat-menu>
<button mat-icon-button [matMenuTriggerFor]="settingsMenu" id="settingsButton"
matTooltip="Document/annotation settings">
<mat-icon>settings</mat-icon>
</button>
</div>
<div #docElem id="doc" class="cursor-pointer">
<!-- set word-start and word-end to help with testing -->
<span #wordsList class="word" *ngFor="let word of nerData.words" [id]="word.id"
[attr.word-start]="word.start" [attr.word-end]="word.end"
[matTooltip]="getWordTooltip(word)" (mousedown)="mousedown($event, word)"
(mouseover)="mouseover($event, word)" (mouseout)="mouseout($event, word)"
(mouseup)="mouseup($event, word)" (click)="click($event, word)"
(contextmenu)="contextMenu($event, word)">{{ word.text }}</span>
</div>
<div *ngIf="!allowOverlappingNerAnnotations"> (Note: overlapping annotations are not allowed for
this
collection.)
</div>
<div #popoverTemplate id="popoverTemplate" class="popover" hidden>
<mat-chip-list>
<mat-chip *ngFor="let label of availableLabels" [style.background-color]="label.color"
class="shadowed cursor-pointer doc-label-chip">{{label.name}}</mat-chip>
</mat-chip-list>
<div style="padding: 2px">
<button mat-raised-button color="warn">
Remove / Reset
</button>
</div>
</div>
</div>
</div>
</mat-expansion-panel>
<mat-expansion-panel expanded="true">
<mat-expansion-panel-header>
<mat-panel-title>
<h1>NER Annotations</h1>
</mat-panel-title>
<mat-panel-description>
<h3 *ngIf="showingAnnotationsFor === null">Click to select text; right-click to annotate selection
</h3>
<h3 *ngIf="showingAnnotationsFor !== null">Showing
{{ auth.getUserDisplayName(showingAnnotationsFor) }}'s
annotations in read-only mode</h3>
</mat-panel-description>
</mat-expansion-panel-header>
<mat-menu #settingsMenu="matMenu" id="settings">
<button>
<mat-checkbox matMenuItem [(ngModel)]="settingMonospace" (click)="$event.stopPropagation()"
class="mat-menu-item">
Monospace font
</mat-checkbox>
<div class="action-bar" fxLayout="row">
<button mat-raised-button [routerLink]="['/' + PATHS.collection.details, doc.collection_id]">
<span class="material-icons">cancel</span>Cancel
</button>
<app-error></app-error>
<span fxFlex></span>
<button class="annotate-button" mat-raised-button (click)="save(false)" [disabled]="!canCurrentlyAnnotate">
<span class="material-icons">save</span>Save
</button>
<span fxFlex="10px"></span>
<button mat-raised-button (click)="save(true)" [disabled]="!canCurrentlyAnnotate">
<span class="material-icons">save</span><span class="material-icons">navigate_next</span>
Save and Advance to Next Document
</button>
</mat-menu>
<button mat-icon-button [matMenuTriggerFor]="settingsMenu" id="settingsButton"
matTooltip="Document/annotation settings">
<mat-icon>settings</mat-icon>
</button>
<div #docElem id="doc" class="cursor-pointer">
<!-- set word-start and word-end to help with testing -->
<span #wordsList class="word" *ngFor="let word of nerData.words" [id]="word.id"
[attr.word-start]="word.start" [attr.word-end]="word.end"
[matTooltip]="getWordTooltip(word)" (mousedown)="mousedown($event, word)"
(mouseover)="mouseover($event, word)" (mouseout)="mouseout($event, word)"
(mouseup)="mouseup($event, word)" (click)="click($event, word)"
(contextmenu)="contextMenu($event, word)">{{ word.text }}</span>
</div>
</mat-expansion-panel>
</section>
</mat-accordion>
<section *ngIf="tabIndex == 1 && !loading.loading && !loading.error" #imageSection fxFlexFill>
<div *ngIf="doc.metadata && doc.metadata['imageUrl']" id="myDocImage" class="image-container">
<div style="position: absolute; top: 0px; bottom: 0; left: 0; right: 0;">
<button class="full-screen-btn" mat-raised-button
(click)="toggleImageFullscreen()">{{ isImageFullscreen() ? 'Close' : 'Open' }} Full
Screen</button>
<app-image-explorer [imageUrl]="doc.metadata['imageUrl']" [documentId]="doc._id"
[collectionId]="collection._id"></app-image-explorer>
</div>
</div>
</section>
<section *ngIf="tabIndex == 2 && !loading.loading && !loading.error" #detailSection>
<div class="doc-labeling-container">
<div fxLayout="row">
<h2 class="mat-title">Document Labeling</h2>
<mat-error *ngIf="!canAnnotate" id="cantAnnotate">
<h3>Note: you do not have authority to change or add annotations for this document.</h3>
</mat-error>
</div>
<div class="doc-label-list" fxLayout="row">
<mat-chip-list fxFlex>
<mat-checkbox *ngFor="let annotation of myDocAnnotations;" [(ngModel)]="annotation.checked">
<mat-chip [style.background-color]="annotation.label.color" class="shadowed cursor-pointer">
{{annotation.label.name}}</mat-chip>
</mat-checkbox>
</mat-chip-list>
<button class="btn-short annotate-button" mat-raised-button color="primary" (click)="save(false)"
[disabled]="!canCurrentlyAnnotate">
<span class="material-icons">save</span>Save
</button>
</div>
</div>
<mat-divider></mat-divider>
<app-document-details expanded="true" [document]="doc" [collection]="collection"
(imageUrlChanged)="imageChanged($event)">
</app-document-details>
</section>
<div *ngIf="!allowOverlappingNerAnnotations"> (Note: overlapping annotations are not allowed for this collection.)
</div>
<mat-card-actions>
<button mat-raised-button (click)="save(false)" [disabled]="!canCurrentlyAnnotate">
<span class="material-icons">save</span>Save
</button>
<button mat-raised-button (click)="save(true)" [disabled]="!canCurrentlyAnnotate">
<span class="material-icons">save</span><span class="material-icons">navigate_next</span>Save and Advance to
Next
Document
</button>
<button mat-raised-button [routerLink]="['/' + PATHS.collection.details, doc.collection_id]">
<span class="material-icons">cancel</span>Cancel
</button>
<app-error></app-error>
</mat-card-actions>
<div style="margin-top: 16px">
<app-ner-annotation-table [labels]="availableLabels" [data]="nerData" (remove)="removeAnnotation($event)">
</app-ner-annotation-table>
</div>
<div #popoverTemplate id="popoverTemplate" class="popover" hidden>
<mat-chip-list>
<mat-chip *ngFor="let label of availableLabels" [style.background-color]="label.color"
class="shadowed cursor-pointer">{{label.name}}</mat-chip>
</mat-chip-list>
<div style="padding: 2px">
<button mat-raised-button color="warn">
Remove / Reset
</button>
</div>
</div>
</mat-card>
</div>

View File

@@ -38,6 +38,7 @@ import { PATHS, PARAMS } from "../../app.paths";
import { AnnotatedService } from 'src/app/service/annotated/annotated.service';
import { IaaReportingService } from 'src/app/service/iaa-reporting/iaa-reporting.service';
import { IAAReport } from 'src/app/model/iaareport';
import * as _ from 'lodash';
class DocAnnotation {
constructor(public label: DocLabel, public checked: boolean) {
@@ -69,6 +70,8 @@ export class AnnotateComponent implements OnInit, AfterViewInit {
private selectionChangeTimer;
private mouseDown: LeftMouseDown;
public tabIndex = 0;
public canAnnotate = false;
public canCurrentlyAnnotate = false; // for showing others' annotations
public allowOverlappingNerAnnotations = true;
@@ -90,6 +93,8 @@ export class AnnotateComponent implements OnInit, AfterViewInit {
private mouseOutState = false;
private recentWord = null;
public showList: boolean = true;
public ann_agreement: number
@ViewChildren("wordsList")
public wordsList: QueryList<any>;
@@ -116,6 +121,11 @@ export class AnnotateComponent implements OnInit, AfterViewInit {
}
ngOnInit() {
this.route.queryParams.subscribe(params => {
if(params.tab != undefined) {
this.tabIndex = params.tab;
}
});
}
ngAfterViewInit() {
@@ -195,6 +205,21 @@ export class AnnotateComponent implements OnInit, AfterViewInit {
});
}
public updateTabInUrl() {
let param = { tab: this.tabIndex };
this.router.navigate([], {
relativeTo: this.route,
queryParams: param,
queryParamsHandling: 'merge'
});
}
public backToCollectionDetails() {
if(this.collection) {
this.router.navigate(['collection', 'details', this.collection._id]);
}
}
public imageChanged(imageUrl: string) {
if(!this.doc.metadata) {
this.doc.metadata = {};

View File

@@ -1,4 +1,74 @@
/*(C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC. */
.page-container {
height: 100%;
width: 100%;
position: relative;
}
.title-toolbar {
position: absolute;
top: 0px;
width: 100%;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.page-content {
position: absolute;
top: 64px;
right: 0px;
bottom: 0px;
left: 0px;
overflow-y: auto;
}
.title-tabs {
position: relative;
height: 100%;
}
::ng-deep .title-tabs mat-tab-header {
position: absolute;
width: 100%;
height: 100%;
bottom: 0px;
border-bottom: 0;
}
::ng-deep .title-tabs .mat-tab-labels, ::ng-deep .title-tabs .mat-tab-label {
height: 100%;
}
.document-action-row {
position: absolute;
top: 0px;
height: 58px;
width: 100%;
flex-direction: row;
padding: 0px 16px;
display: flex;
align-items: center;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.doc-list-table {
position: absolute;
top: 58px;
left: 0;
right: 0;
bottom: 0;
overflow-y: auto;
}
.doc-list-table table {
width: 100%;
}
.detail-btn-area {
position: absolute;
top: 10px;
right: 10px;
}
.spacer {
flex: 1 1 auto;
@@ -6,33 +76,4 @@
.add-doc-btn {
padding: 0 50px;
}
.card mat-card-content {
padding-top: 15px;
}
tr.trow {
background-color: #f7ffff ;
transition: .2s;
border-radius: 6px;
box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.26);
border: 1px solid #f7ffff;
margin: 10px;
}
tr.trow:hover {
background-color: #eafdfd;
transition: .2s;
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.26);
border: 1px solid #eafdfd;
}
tr.trow:active {
background-color: #e7fdfd ;
transition: .2s;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);
border: 1px solid #ddffff;
}
}

View File

@@ -1,214 +1,225 @@
<!-- (C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC.-->
<mat-card class="card">
<div fxFlexFill class="page-container" fxLayout="column">
<mat-toolbar class="title-toolbar">
<button mat-icon-button matTooltip="Go back to collection list" (click)="backToCollectionList()">
<mat-icon>keyboard_arrow_left</mat-icon>
</button>
<span class="page-title">{{collection?.getTitle()}}</span>
<span fxFlex="22px"></span>
<mat-tab-group *ngIf="collection" class="title-tabs" fxFlex mat-stretch-tabs [(selectedIndex)]="tabIndex"
(selectedIndexChange)="updateTabInUrl()">
<mat-tab label="Documents"></mat-tab>
<mat-tab label="Details"></mat-tab>
</mat-tab-group>
</mat-toolbar>
<mat-card-header>
<mat-card-title>
<h1><span *ngIf="collection?.archived" class="material-icons">archive</span><span
*ngIf="collection?.archived">Archived </span>Collection Details</h1>
</mat-card-title>
</mat-card-header>
<div class="page-content">
<section *ngIf="tabIndex == 1" #detailsSection>
<div *ngIf="loading">Loading...</div>
<div class="document-action-row">
<span fxFlex></span>
<button *ngIf="!collection?.archived" mat-stroked-button (click)="archiveCollection()"
[disabled]="!canArchive">
<mat-icon>archive</mat-icon>Archive
</button>
<button *ngIf="collection?.archived" mat-stroked-button (click)="unarchiveCollection()"
[disabled]="!canArchive">
<mat-icon>unarchive</mat-icon> Unarchive
</button>
<span fxFlex="10px"></span>
<button *ngIf="can_add_images" class="add-doc-btn" mat-stroked-button (click)="uploadImages()">
<mat-icon>cloud_upload</mat-icon> Upload Images
</button>
<span fxFlex="10px"></span>
<button mat-stroked-button (click)="downloadData()">
<mat-icon>cloud_download</mat-icon> Download Data
</button>
</div>
<mat-divider></mat-divider>
<div class="doc-list-table">
<table id="metadata-table" *ngIf="!loading">
<tr style="vertical-align: top;">
<td style="padding-top: 25px">
<tr class="space-under">
<td><b>Collection title:</b></td>
<td>{{ collection?.getTitle() }}</td>
</tr>
<tr class="space-under">
<td> <b>Collection ID:</b> </td>
<td>{{collection?._id}}</td>
</tr>
<tr class="space-under">
<td><b>Creation Date:</b></td>
<td>{{collection?._created}}</td>
</tr>
<tr class="space-under">
<td><b>Last Updated:</b></td>
<td>{{collection?._updated}}</td>
</tr>
<tr class="space-under">
<td><b>Creator:</b></td>
<td colspan="3">{{auth.getUserDisplayName(collection?.creator_id)}}</td>
</tr>
<tr class="space-under">
<td><b>Viewers:</b></td>
<td colspan="3">
<mat-chip-list>
<mat-chip *ngFor="let viewer of collection?.viewers">
{{auth.getUserDisplayName(viewer)}}
</mat-chip>
<mat-chip *ngIf="can_add_users" (click)="openAddViewerDialog()">+</mat-chip>
</mat-chip-list>
</td>
</tr>
<tr class="space-under">
<td><b>Annotators:</b></td>
<td colspan="3">
<mat-chip-list>
<mat-chip *ngFor="let annotator of collection?.annotators">
{{auth.getUserDisplayName(annotator)}}</mat-chip>
<mat-chip *ngIf="can_add_users" (click)="openAddAnnotatorDialog()">+</mat-chip>
</mat-chip-list>
</td>
</tr>
<tr class="space-under">
<td><b>Labels:</b></td>
<td colspan="3">
<mat-chip-list>
<mat-chip *ngFor="let label of collection?.labels">{{label}}</mat-chip>
<mat-chip *ngIf="can_add_users" (click)="openAddLabelDialog()">+</mat-chip>
</mat-chip-list>
</td>
</tr>
<tr class="space-under">
<td><b>Additional:</b></td>
<td colspan="3">
<table>
<tr *ngFor="let item of getAdditionalMetadata() | keyvalue">
<td><b>{{item.key}}</b></td>
<td>{{item.value}}</td>
</tr>
</table>
</td>
</tr>
<tr class="space-under">
<td><b>Configuration:</b></td>
<td colspan="3">
<table>
<tr *ngFor="let item of collection.configuration | keyvalue">
<td><b>{{item.key}}</b></td>
<td>{{item.value}}</td>
</tr>
</table>
</td>
</tr>
<tr>
<td><b>Classifier:</b></td>
<td colspan="3">
<table *ngIf="classifier">
<tr>
<td *ngIf="pipeline">
<b>{{ pipeline.title }}</b><br />{{ pipeline.description }}
</td>
<td>
<table>
<tr *ngFor="let item of classifier.parameters | keyvalue">
<td><b>{{item.key}}</b></td>
<td>{{item.value}}</td>
</tr>
</table>
</td>
</tr>
</table>
<ng-container *ngIf="!classifier">
<b>ERROR: No classifier configured for collection.</b>
</ng-container>
</td>
</tr>
</td>
<div *ngIf="loading">Loading...</div>
<mat-card-content>
</tr>
<tr *ngIf="iaa_report">
<td>
<table id="metadata-table" *ngIf="!loading">
<tr style="vertical-align: top;">
<td style="padding-top: 25px">
<tr class="space-under">
<td><b>Collection title:</b></td>
<td>{{ collection?.getTitle() }}</td>
</tr>
<tr class="space-under">
<td> <b>Collection ID:</b> </td>
<td>{{collection?._id}}</td>
</tr>
<tr class="space-under">
<td><b>Creation Date:</b></td>
<td>{{collection?._created}}</td>
</tr>
<tr class="space-under">
<td><b>Last Updated:</b></td>
<td>{{collection?._updated}}</td>
</tr>
<tr class="space-under">
<td><b>Creator:</b></td>
<td colspan="3">{{auth.getUserDisplayName(collection?.creator_id)}}</td>
</tr>
<tr class="space-under">
<td><b>Viewers:</b></td>
<td colspan="3">
<mat-chip-list>
<mat-chip *ngFor="let viewer of collection?.viewers">{{auth.getUserDisplayName(viewer)}}</mat-chip>
<mat-chip *ngIf="can_add_users" (click)="openAddViewerDialog()">+</mat-chip>
</mat-chip-list>
</td>
</tr>
<tr class="space-under">
<td><b>Annotators:</b></td>
<td colspan="3">
<mat-chip-list>
<mat-chip *ngFor="let annotator of collection?.annotators">{{auth.getUserDisplayName(annotator)}}</mat-chip>
<mat-chip *ngIf="can_add_users" (click)="openAddAnnotatorDialog()">+</mat-chip>
</mat-chip-list>
</td>
</tr>
<tr class="space-under">
<td><b>Labels:</b></td>
<td colspan="3">
<mat-chip-list>
<mat-chip *ngFor="let label of collection?.labels">{{label}}</mat-chip>
<mat-chip *ngIf="can_add_users" (click)="openAddLabelDialog()">+</mat-chip>
</mat-chip-list>
</td>
</tr>
<tr class="space-under">
<td><b>Additional:</b></td>
<td colspan="3">
<table>
<tr *ngFor="let item of getAdditionalMetadata() | keyvalue">
<td><b>{{item.key}}</b></td>
<td>{{item.value}}</td>
</tr>
</table>
</td>
</tr>
<tr class="space-under">
<td><b>Configuration:</b></td>
<td colspan="3">
<table>
<tr *ngFor="let item of collection.configuration | keyvalue">
<td><b>{{item.key}}</b></td>
<td>{{item.value}}</td>
</tr>
</table>
</td>
</tr>
<tr>
<td><b>Classifier:</b></td>
<td colspan="3">
<table *ngIf="classifier">
<tr>
<td *ngIf="pipeline">
<b>{{ pipeline.title }}</b><br />{{ pipeline.description }}
</td>
<td>
<table>
<tr *ngFor="let item of classifier.parameters | keyvalue">
<td><b>{{item.key}}</b></td>
<td>{{item.value}}</td>
</tr>
<app-collection-iaa-report [iaa_report]="iaa_report">
</app-collection-iaa-report>
</td>
</tr>
<tr>
<td>
<app-metrics [metrics]="metrics">
</app-metrics>
</td>
</tr>
</table>
</td>
</tr>
</table>
<ng-container *ngIf="!classifier">
<b>ERROR: No classifier configured for collection.</b>
</ng-container>
</td>
</tr>
</td>
</div>
</section>
</tr>
<tr *ngIf="iaa_report" >
<td>
<section *ngIf="tabIndex == 0" #documentsSection>
<div fxLayout="column">
<div class="document-action-row" fxLayout="row">
<mat-form-field fxFlex="280px" floatLabel="never">
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
</mat-form-field>
<span fxFlex></span>
<button *ngIf="can_add_documents" mat-stroked-button (click)="addDocument()">
<mat-icon>add</mat-icon> Add Document
</button>
<span fxFlex="10px"></span>
<button mat-stroked-button [routerLink]="['/' + PATHS.document.annotate, nextDocId]"
[disabled]="nextDocId == null">
<mat-icon>navigate_next</mat-icon> Next Document to Annotate
</button>
</div>
<app-collection-iaa-report [iaa_report]="iaa_report">
<div class="doc-list-table">
<table mat-table class="table-selectable" [dataSource]="dataSource" matSort matSortActive="id"
[hidden]="loading">
</app-collection-iaa-report>
</td>
</tr>
<tr>
<td>
<app-metrics [metrics]="metrics">
</app-metrics>
</td>
</tr>
</table>
</mat-card-content>
<mat-card-actions>
<span [matTooltipDisabled]="canArchive" matTooltip="Only the collection's creator can unarchive it."><button
*ngIf="collection?.archived" class="add-doc-btn" mat-raised-button (click)="unarchiveCollection()"
[disabled]="!canArchive"><mat-icon>unarchive</mat-icon> Unarchive Collection</button></span>
<button *ngIf="can_add_documents" class="add-doc-btn" mat-raised-button [routerLink]="['/' + PATHS.document.add, collection?._id]">
<mat-icon>add</mat-icon> Add Document
</button>
<button *ngIf="can_add_images" class="add-doc-btn" mat-raised-button (click)="uploadImages()">
<mat-icon>cloud_upload</mat-icon> Upload Images
</button>
<button class="add-doc-btn" mat-raised-button [routerLink]="['/' + PATHS.document.annotate, nextDocId]"
[disabled]="nextDocId == null"><mat-icon>navigate_next</mat-icon> Next Document to Annotate</button>
<span [matTooltipDisabled]="canArchive" matTooltip="Only the collection's creator can archive it."><button
*ngIf="!collection?.archived" class="add-doc-btn" mat-raised-button (click)="archiveCollection()"
[disabled]="!canArchive"><mat-icon>archive</mat-icon> Archive Collection</button></span>
<button class="add-doc-btn" mat-raised-button (click)="downloadData()">
<mat-icon>cloud_download</mat-icon> Download Data
</button>
</mat-card-actions>
</mat-card>
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header> ID </th>
<td mat-cell *matCellDef="let document"> {{ document.id }} </td>
</ng-container>
<mat-card class="card">
<mat-card-header>
<mat-card-title>
<h1>Documents in Collection</h1>
<ng-container matColumnDef="creator">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Creator </th>
<td mat-cell *matCellDef="let document"> {{ document.creator }} </td>
</ng-container>
</mat-card-title>
</mat-card-header>
<ng-container matColumnDef="last_updated">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Last Updated </th>
<td mat-cell *matCellDef="let document"> {{ document.last_updated }} </td>
</ng-container>
<mat-divider></mat-divider>
<ng-container matColumnDef="annotated">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Annotated </th>
<td mat-cell *matCellDef="let document">
<mat-icon *ngIf="document.annotated" style="color: green">done</mat-icon>
<mat-icon *ngIf="!document.annotated" style="color: red">clear</mat-icon>
</td>
</ng-container>
<mat-card-content>
<mat-form-field>
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
</mat-form-field>
<table mat-table [dataSource]="dataSource" matSort matSortActive="id" style="width: 100%;border-spacing: 0 5px;" [hidden]="loading">
<ng-container matColumnDef="agreement">
<th mat-header-cell *matHeaderCellDef> Annotation <br>Agreement </th>
<td mat-cell *matCellDef="let document">
<span
*ngIf="document.ann_agreement != 'null'">{{document.ann_agreement | percent:'1.2-2'}}</span>
<span *ngIf="document.ann_agreement == 'null'">N/A</span>
</td>
</ng-container>
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header> ID </th>
<td mat-cell *matCellDef="let document"> {{ document.id }} </td>
</ng-container>
<ng-container matColumnDef="text_start">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Text </th>
<td mat-cell *matCellDef="let document"> {{ document.text_start }} </td>
</ng-container>
<ng-container matColumnDef="creator">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Creator </th>
<td mat-cell *matCellDef="let document"> {{ document.creator }} </td>
</ng-container>
<ng-container matColumnDef="last_updated">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Last Updated </th>
<td mat-cell *matCellDef="let document"> {{ document.last_updated }} </td>
</ng-container>
<ng-container matColumnDef="annotated">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Annotated </th>
<td mat-cell *matCellDef="let document">
<mat-icon *ngIf="document.annotated" style="color: green">done</mat-icon>
<mat-icon *ngIf="!document.annotated" style="color: red">clear</mat-icon>
</td>
</ng-container>
<ng-container matColumnDef="agreement">
<th mat-header-cell *matHeaderCellDef> Annotation <br>Agreement </th>
<td mat-cell *matCellDef="let document">
<span *ngIf="document.ann_agreement != 'null'">{{document.ann_agreement | percent:'1.2-2'}}</span>
<span *ngIf="document.ann_agreement == 'null'">N/A</span>
</td>
</ng-container>
<ng-container matColumnDef="text_start">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Text </th>
<td mat-cell *matCellDef="let document"> {{ document.text_start }} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row class="trow" *matRowDef="let row; columns: displayedColumns;"
[routerLink]="['/' + PATHS.document.annotate, row.id ]"></tr>
</table>
<mat-paginator [pageSize]="20" [pageSizeOptions]="[5, 10, 20, 50, 100]" showFirstLastButtons></mat-paginator>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button [routerLink]="['/' + PATHS.collection.view]">Go back</button>
</mat-card-actions>
</mat-card>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row class="trow" *matRowDef="let row; columns: displayedColumns;"
[routerLink]="['/' + PATHS.document.annotate, row.id ]"></tr>
</table>
</div>
</div>
</section>
</div>
</div>

View File

@@ -1,9 +1,11 @@
/*(C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC. */
import { Component, OnInit, ViewChild, AfterViewInit, Inject } from "@angular/core";
import { MatPaginator, MatTableDataSource, MatSort, MatDialog, MatDialogRef, MAT_DIALOG_DATA } from "@angular/material";
import { ActivatedRoute } from "@angular/router";
import { ActivatedRoute, Router } from "@angular/router";
import { HttpErrorResponse } from "@angular/common/http";
import * as _ from "lodash";
import * as moment from "moment";
import { take } from "rxjs/operators";
import { PATHS, PARAMS } from "../../app.paths";
@@ -17,28 +19,29 @@ import { EventService } from "../../service/event/event.service";
import { PipelineService } from "../../service/pipeline/pipeline.service";
import { MetricsService } from "../../service/metrics/metrics.service";
import { Annotation } from "../../model/annotation";
import { Annotation } from "../../model/annotation";
import { Document } from "../../model/document";
import { Classifier } from "../../model/classifier";
import { Collection, DownloadCollectionData, METADATA_TITLE } from "../../model/collection";
import { Pipeline } from "../../model/pipeline";
import {DocumentMenuItem} from "../nav-collection-menu/nav-collection-menu.component";
import { DocumentMenuItem } from "../nav-collection-menu/nav-collection-menu.component";
import { Metric } from "../../model/metrics";
import { IaaReportingService } from '../../service/iaa-reporting/iaa-reporting.service';
import { IAAReport } from '../../model/iaareport';
import { DownloadCollectionDataDialogComponent } from '../download-collection-data.dialog/download-collection-data.dialog.component';
import { ImageCollectionUploaderComponent, dialog } from "../image-collection-uploader/image-collection-uploader.component";
import { AddDocumentComponent, AddDocumentDialogData } from '../add-document/add-document.component';
export interface AnnotatorDialogData {
annotator: string;
annotator: string;
}
export interface ViewerDialogData {
viewer: string;
viewer: string;
}
export interface LabelDialogData {
label: string;
label: string;
}
export interface DocumentRow {
@@ -47,7 +50,8 @@ export interface DocumentRow {
last_updated: Date;
text_start: string;
annotated: boolean;
ann_agreement: number
ann_agreement: number;
created: Date;
}
@Component({
@@ -61,6 +65,8 @@ export class CollectionDetailsComponent implements OnInit {
public readonly PATHS = PATHS;
public tabIndex: number = 0;
public loading = false;
public canArchive = false;
@@ -70,9 +76,10 @@ export class CollectionDetailsComponent implements OnInit {
public pipeline: Pipeline;
documents: DocumentRow[];
dataSource = new MatTableDataSource<DocumentRow>();
oldestDocId: string = null;
nextDocId: string = null;
metrics: object;
iaa_report : IAAReport;
iaa_report: IAAReport;
private new_annotator: string = null;
private new_viewer: string = null;
private new_label: string = null;
@@ -84,22 +91,30 @@ export class CollectionDetailsComponent implements OnInit {
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
constructor(private route: ActivatedRoute,
private annotationService: AnnotationService,
private collectionService: CollectionRepositoryService,
private documentsService: DocumentRepositoryService,
private pipelineService: PipelineService,
private metricsService: MetricsService,
private events: EventService,
public auth: AuthService,
private iaa_reports : IaaReportingService,
private dialog: MatDialog) {
constructor(private router: Router,
private route: ActivatedRoute,
private annotationService: AnnotationService,
private collectionService: CollectionRepositoryService,
private documentsService: DocumentRepositoryService,
private pipelineService: PipelineService,
private metricsService: MetricsService,
private events: EventService,
public auth: AuthService,
private iaa_reports: IaaReportingService,
private dialog: MatDialog) {
this.can_add_images = this.can_add_documents = false;
}
ngOnInit() {
this.loading = true;
this.dataSource.paginator = this.paginator;
this.route.queryParams.subscribe(params => {
if(params.tab != undefined) {
this.tabIndex = params.tab;
}
});
this.route.paramMap.subscribe(params => {
const colId = params.get(PARAMS.collection.details.collection_id);
this.collectionService.getCanAddDocumentsOrImages(colId).pipe(take(1)).subscribe((val: boolean) => {
@@ -112,62 +127,90 @@ export class CollectionDetailsComponent implements OnInit {
this.collection = collection;
this.can_add_users = collection.creator_id === this.auth.loggedInUser.id;
this.canArchive = true;//collection.creator_id === this.auth.getLocalLoggedInUser().id;
if(this.documents) {
if (this.documents) {
this.documents.length = 0;
}
this.iaa_reports.getIIAReportByCollection(colId).subscribe((iaa_report: any)=>{
this.iaa_reports.getIIAReportByCollection(colId).subscribe((iaa_report: any) => {
this.iaa_report = iaa_report[0]
})
this.documentsService.getDocumentsByCollectionIDPaginated(colId, true).subscribe((document: Document) => {
tempDocuments.push(<DocumentRow>{
tempDocuments.push(<DocumentRow>{
id: document._id,
creator: this.auth.getUserDisplayName(document.creator_id),
last_updated: document._updated,
created: new Date(document._created),
text_start: document.getTextPreview(),
annotated: document.has_annotated ? document.has_annotated[this.auth.loggedInUser.id] : undefined,
ann_agreement: this.iaa_report ? this.iaa_report.per_doc_agreement[this.iaa_report.per_doc_agreement.findIndex((doc: any)=> doc.doc_id == document._id)]["avg"] : null
});
}, (error) => {},
() => {
this.documents = tempDocuments;
this.dataSource.data = this.documents;
this.dataSource.sort = this.sort
this.pipelineService.getClassifierForCollection(colId).subscribe((classifier: Classifier) => {
this.classifier = classifier;
this.nextDocId = null;
this.metricsService.getMetricForClassifier(this.classifier._id).toPromise().then((metrics)=>{
this.metrics = metrics
})
this.pipelineService.getPipeline(classifier.pipeline_id).subscribe((pipeline: Pipeline) => {
this.pipeline = pipeline;
this.pipelineService.getNextDocumentIdForClassifier(classifier._id).subscribe((docId: string) => {
this.nextDocId = docId;
this.loading = false;
}, (error: HttpErrorResponse) => {
console.error("Error getting next document ID for collection", error);
this.loading = false;
},
() => {
// this.metricsService.getMetricForClassifier()
}
);
});
}, (error) => {
console.error("Error getting classifier for collection", error);
this.classifier = null;
this.nextDocId = null;
this.loading = false;
ann_agreement: this.iaa_report ? this.iaa_report.per_doc_agreement[this.iaa_report.per_doc_agreement.findIndex((doc: any) => doc.doc_id == document._id)]["avg"] : null
});
});
}, (error) => { },
() => {
this.documents = tempDocuments;
this.dataSource.data = this.documents;
this.dataSource.sort = this.sort
this.pipelineService.getClassifierForCollection(colId).subscribe((classifier: Classifier) => {
this.classifier = classifier;
this.nextDocId = null;
this.metricsService.getMetricForClassifier(this.classifier._id).toPromise().then((metrics) => {
this.metrics = metrics
}, (err) => {
console.error(err);
});
this.pipelineService.getPipeline(classifier.pipeline_id).subscribe((pipeline: Pipeline) => {
this.pipeline = pipeline;
this.pipelineService.getNextDocumentIdForClassifier(classifier._id).subscribe((docId: string) => {
this.nextDocId = docId;
this.loading = false;
}, (error: HttpErrorResponse) => {
console.error("Error getting next document ID for collection", error);
this.loading = false;
},
() => {
// this.metricsService.getMetricForClassifier()
}
);
});
}, (error) => {
console.error("Error getting classifier for collection", error);
this.classifier = null;
this.nextDocId = null;
this.loading = false;
});
});
});
});
}
public updateTabInUrl() {
let param = { tab: this.tabIndex };
this.router.navigate([], {
relativeTo: this.route,
queryParams: param,
queryParamsHandling: 'merge'
});
}
public addDocument() {
if (this.collection) {
let dialogData: AddDocumentDialogData = {
collection: this.collection
};
const dialogRef = this.dialog.open(AddDocumentComponent, {
width: '450px',
data: dialogData
});
}
}
public backToCollectionList() {
this.router.navigate(['collections']);
}
public getAdditionalMetadata() {
const additional = {};
for(const key in this.collection.metadata) {
if(key.toLowerCase() !== METADATA_TITLE && this.collection.metadata[key]) {
for (const key in this.collection.metadata) {
if (key.toLowerCase() !== METADATA_TITLE && this.collection.metadata[key]) {
additional[key] = this.collection.metadata[key];
}
}
@@ -185,7 +228,7 @@ export class CollectionDetailsComponent implements OnInit {
this.events.showUserMessage.emit("Collection successfully archived.");
}, (error: HttpErrorResponse) => {
this.events.showUserMessage.emit("ERROR: Collection was not successfully archived:\n" +
`Error ${error.status}: ${error.message}`);
`Error ${error.status}: ${error.message}`);
});
}
@@ -196,7 +239,7 @@ export class CollectionDetailsComponent implements OnInit {
this.events.showUserMessage.emit("Collection successfully unarchived.");
}, (error: HttpErrorResponse) => {
this.events.showUserMessage.emit("ERROR: Collection was not successfully unarchived:\n" +
`Error ${error.status}: ${error.message}`);
`Error ${error.status}: ${error.message}`);
});
}
@@ -204,10 +247,10 @@ export class CollectionDetailsComponent implements OnInit {
dialog(this.dialog)
.pipe(take(1))
.subscribe((uploader: ImageCollectionUploaderComponent) => {
if(uploader) {
if (uploader) {
uploader.upload(this.collection._id, true)
.pipe(take(1))
.subscribe(_ => {},
.subscribe(_ => { },
(error) => {
console.error(error);
this.events.showUserMessage.emit("Unable to upload images.");
@@ -217,75 +260,78 @@ export class CollectionDetailsComponent implements OnInit {
}
public openAddAnnotatorDialog() {
const dialogRef = this.dialog.open(AddAnnotatorDialog, {
width: '250px',
data: {new_annotator: this.new_annotator}
});
dialogRef.afterClosed().subscribe(result => {
this.new_annotator = result;
if(this.new_annotator){
this.new_annotator = this.new_annotator.replace(/[|&;$%@"<>()+,]/g, "");
this.collectionService.addAnnotatorToCollection(this.collection._id, this.new_annotator).subscribe(res => {;
this.events.showUserMessage.emit("New Annotator Added to Collection");
this.collection.annotators.push(this.new_annotator);
if (!this.collection.viewers.includes(this.new_annotator)){
this.collection.viewers.push(this.new_annotator);
}
}, (error: HttpErrorResponse) => {
this.events.showUserMessage.emit("ERROR: Annotator was not successfully added:\n" +
`Error ${error.status}: ${error.message}`);
const dialogRef = this.dialog.open(AddAnnotatorDialog, {
width: '250px',
data: { new_annotator: this.new_annotator }
});
dialogRef.afterClosed().subscribe(result => {
this.new_annotator = result;
if (this.new_annotator) {
this.new_annotator = this.new_annotator.replace(/[|&;$%@"<>()+,]/g, "");
this.collectionService.addAnnotatorToCollection(this.collection._id, this.new_annotator).subscribe(res => {
;
this.events.showUserMessage.emit("New Annotator Added to Collection");
this.collection.annotators.push(this.new_annotator);
if (!this.collection.viewers.includes(this.new_annotator)) {
this.collection.viewers.push(this.new_annotator);
}
}, (error: HttpErrorResponse) => {
this.events.showUserMessage.emit("ERROR: Annotator was not successfully added:\n" +
`Error ${error.status}: ${error.message}`);
});
}
});
}
});
}
public openAddViewerDialog() {
const dialogRef = this.dialog.open(AddViewerDialog, {
width: '250px',
data: {new_viewer: this.new_viewer}
});
dialogRef.afterClosed().subscribe(result => {
this.new_viewer = result;
if(this.new_viewer){
this.new_viewer = this.new_viewer.replace(/[|&;$%@"<>()+,]/g, "");
this.collectionService.addViewerToCollection(this.collection._id, this.new_viewer).subscribe(res => {;
this.events.showUserMessage.emit("New Viewer Added to Collection");
this.collection.viewers.push(this.new_viewer);
}, (error: HttpErrorResponse) => {
this.events.showUserMessage.emit("ERROR: Viewer was not successfully added:\n" +
`Error ${error.status}: ${error.message}`);
const dialogRef = this.dialog.open(AddViewerDialog, {
width: '250px',
data: { new_viewer: this.new_viewer }
});
dialogRef.afterClosed().subscribe(result => {
this.new_viewer = result;
if (this.new_viewer) {
this.new_viewer = this.new_viewer.replace(/[|&;$%@"<>()+,]/g, "");
this.collectionService.addViewerToCollection(this.collection._id, this.new_viewer).subscribe(res => {
;
this.events.showUserMessage.emit("New Viewer Added to Collection");
this.collection.viewers.push(this.new_viewer);
}, (error: HttpErrorResponse) => {
this.events.showUserMessage.emit("ERROR: Viewer was not successfully added:\n" +
`Error ${error.status}: ${error.message}`);
});
}
});
}
});
}
public openAddLabelDialog() {
const dialogRef = this.dialog.open(AddLabelDialog, {
width: '250px',
data: {new_label: this.new_label}
});
dialogRef.afterClosed().subscribe(result => {
this.new_label = result;
if(this.new_label){
this.new_label = this.new_label.replace(/[|&;$%@"<>()+,]/g, "");
this.collectionService.addLabelToCollection(this.collection._id, this.new_label).subscribe(res => {;
this.events.showUserMessage.emit("New Label Added to Collection");
this.collection.labels.push(this.new_label);
}, (error: HttpErrorResponse) => {
this.events.showUserMessage.emit("ERROR: Label was not successfully added:\n" +
`Error ${error.status}: ${error.message}`);
const dialogRef = this.dialog.open(AddLabelDialog, {
width: '250px',
data: { new_label: this.new_label }
});
dialogRef.afterClosed().subscribe(result => {
this.new_label = result;
if (this.new_label) {
this.new_label = this.new_label.replace(/[|&;$%@"<>()+,]/g, "");
this.collectionService.addLabelToCollection(this.collection._id, this.new_label).subscribe(res => {
;
this.events.showUserMessage.emit("New Label Added to Collection");
this.collection.labels.push(this.new_label);
}, (error: HttpErrorResponse) => {
this.events.showUserMessage.emit("ERROR: Label was not successfully added:\n" +
`Error ${error.status}: ${error.message}`);
});
}
});
}
});
}
public downloadData() {
DownloadCollectionDataDialogComponent.show(this.dialog, this.collection).subscribe(
(value: DownloadCollectionData) => {
if(value) {
if (value) {
this.collectionService.downloadData(this.collection._id, value).subscribe(
(response) => {
BackendService.downloadFile(response, "collection_" + this.collection._id + ".json");
@@ -300,49 +346,49 @@ export class CollectionDetailsComponent implements OnInit {
}
@Component({
selector: 'add-annotator-dialog',
templateUrl: './collection-details-add-annotator-modal.component.html',
selector: 'add-annotator-dialog',
templateUrl: './collection-details-add-annotator-modal.component.html',
})
export class AddAnnotatorDialog {
constructor(
public dialogRef: MatDialogRef<AddAnnotatorDialog>,
@Inject(MAT_DIALOG_DATA) public data: AnnotatorDialogData) {}
constructor(
public dialogRef: MatDialogRef<AddAnnotatorDialog>,
@Inject(MAT_DIALOG_DATA) public data: AnnotatorDialogData) { }
onNoClick() {
this.dialogRef.close();
}
onNoClick() {
this.dialogRef.close();
}
}
@Component({
selector: 'add-viewer-dialog',
templateUrl: './collection-details-add-viewer-modal.component.html',
selector: 'add-viewer-dialog',
templateUrl: './collection-details-add-viewer-modal.component.html',
})
export class AddViewerDialog {
constructor(
public dialogRef: MatDialogRef<AddViewerDialog>,
@Inject(MAT_DIALOG_DATA) public data: ViewerDialogData) {}
constructor(
public dialogRef: MatDialogRef<AddViewerDialog>,
@Inject(MAT_DIALOG_DATA) public data: ViewerDialogData) { }
onNoClick() {
this.dialogRef.close();
}
onNoClick() {
this.dialogRef.close();
}
}
@Component({
selector: 'add-label-dialog',
templateUrl: './collection-details-add-label-modal.component.html',
selector: 'add-label-dialog',
templateUrl: './collection-details-add-label-modal.component.html',
})
export class AddLabelDialog {
constructor(
public dialogRef: MatDialogRef<AddLabelDialog>,
@Inject(MAT_DIALOG_DATA) public data: LabelDialogData) {}
constructor(
public dialogRef: MatDialogRef<AddLabelDialog>,
@Inject(MAT_DIALOG_DATA) public data: LabelDialogData) { }
onNoClick() {
this.dialogRef.close();
}
onNoClick() {
this.dialogRef.close();
}
}

View File

@@ -3,3 +3,7 @@
.metadata-table {
width: 100%;
}
.detail-container {
padding: 28px;
}

View File

@@ -1,58 +1,56 @@
<!-- (C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC.-->
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
<h2>Document Details</h2>
</mat-panel-title>
<mat-panel-description>
<h3>Show document details and metadata</h3>
</mat-panel-description>
</mat-expansion-panel-header>
<table class="metadata-table">
<tr class="space-under">
<td><b>Document ID:</b></td>
<td>{{document?._id}}</td>
</tr>
<tr class="space-under">
<td><b>Creation Date:</b></td>
<td>{{document?._created}}</td>
</tr>
<tr class="space-under">
<td><b>Last Updated:</b></td>
<td>{{document?._updated}}</td>
</tr>
<tr class="space-under">
<td><b>Creator:</b></td>
<td>{{auth.getUserDisplayName(document?.creator_id)}}</td>
</tr>
<tr class="space-under">
<td><b>Metadata:</b></td>
<td>
<table>
<ng-container *ngIf="document && document?.metadata">
<tr *ngFor="let item of document?.metadata | keyvalue">
<td><b>{{item.key}}</b></td>
<td *ngIf="item.key !== 'imageUrl'">{{item.value}}</td>
<td *ngIf="item.key === 'imageUrl'">
<a [href]="collections.collectionImageUrl(collection._id, item.value)" target="_blank">
{{item.value}}
<span *ngIf="item.value !== collections.collectionImageUrl(collection._id, item.value)">({{collections.collectionImageUrl(collection._id, item.value)}})</span>
</a>
<div><button *ngIf="canUpdateMetadata" mat-button mat-raised-button (click)="updateImage()">Update document image</button></div>
</td>
</tr>
</ng-container>
<tr *ngIf="canUpdateMetadata && (!document || !document.metadata || !document.metadata.hasOwnProperty('imageUrl'))">
<td><b>imageUrl</b></td>
<td><button mat-button mat-raised-button (click)="updateImage()">Update document image</button></td>
</tr>
</table>
</td>
</tr>
<tr>
<td><b>Collection:</b></td>
<td *ngIf="!collection">Loading...</td>
<td *ngIf="collection">{{ collection.hasTitle() ? collection.getTitle() + " (" : "" }}<a href="#" [routerLink]="['/' + PATHS.collection.details, document?.collection_id]">{{document?.collection_id}}</a>{{ collection.hasTitle() ? ")" : "" }}</td>
</tr>
</table>
</mat-expansion-panel>
<div class="detail-container">
<table class="metadata-table">
<tr class="space-under">
<td><b>Document ID:</b></td>
<td>{{document?._id}}</td>
</tr>
<tr class="space-under">
<td><b>Creation Date:</b></td>
<td>{{document?._created}}</td>
</tr>
<tr class="space-under">
<td><b>Last Updated:</b></td>
<td>{{document?._updated}}</td>
</tr>
<tr class="space-under">
<td><b>Creator:</b></td>
<td>{{auth.getUserDisplayName(document?.creator_id)}}</td>
</tr>
<tr class="space-under">
<td><b>Metadata:</b></td>
<td>
<table>
<ng-container *ngIf="document && document?.metadata">
<tr *ngFor="let item of document?.metadata | keyvalue">
<td><b>{{item.key}}</b></td>
<td *ngIf="item.key !== 'imageUrl'">{{item.value}}</td>
<td *ngIf="item.key === 'imageUrl'">
<a [href]="collections.collectionImageUrl(collection._id, item.value)" target="_blank">
{{item.value}}
<span
*ngIf="item.value !== collections.collectionImageUrl(collection._id, item.value)">({{collections.collectionImageUrl(collection._id, item.value)}})</span>
</a>
<div><button *ngIf="canUpdateMetadata" mat-button mat-raised-button
(click)="updateImage()">Update document image</button></div>
</td>
</tr>
</ng-container>
<tr
*ngIf="canUpdateMetadata && (!document || !document.metadata || !document.metadata.hasOwnProperty('imageUrl'))">
<td><b>imageUrl</b></td>
<td><button mat-button mat-raised-button (click)="updateImage()">Update document
image</button></td>
</tr>
</table>
</td>
</tr>
<tr>
<td><b>Collection:</b></td>
<td *ngIf="!collection">Loading...</td>
<td *ngIf="collection">{{ collection.hasTitle() ? collection.getTitle() + " (" : "" }}<a href="#"
[routerLink]="['/' + PATHS.collection.details, document?.collection_id]">{{document?.collection_id}}</a>{{ collection.hasTitle() ? ")" : "" }}
</td>
</tr>
</table>
</div>

View File

@@ -6,7 +6,14 @@
<app-image-chooser [collectionId]="data.collectionId" [startingUrl]="data.startingUrl"></app-image-chooser>
</mat-dialog-content>
<mat-dialog-actions class="buttons" align="end">
<button mat-button mat-raised-button (click)="closeDialog(true)" cdkFocusInitial>Save</button>
<button mat-button mat-raised-button (click)="closeDialog(false)">Cancel</button>
<mat-dialog-actions class="buttons">
<button mat-button mat-raised-button (click)="closeDialog(false)">
<mat-icon>cancel</mat-icon>
<span>&nbsp;Cancel</span>
</button>
<span fxFlex></span>
<button mat-button mat-raised-button color="primary" (click)="closeDialog(true)" cdkFocusInitial>
<mat-icon>save</mat-icon>
<span>&nbsp;Save</span>
</button>
</mat-dialog-actions>

View File

@@ -6,7 +6,14 @@
<app-image-collection-uploader></app-image-collection-uploader>
</mat-dialog-content>
<mat-dialog-actions class="buttons" align="end">
<button mat-button mat-raised-button (click)="closeDialog(true)" cdkFocusInitial>Save</button>
<button mat-button mat-raised-button (click)="closeDialog(false)">Cancel</button>
<mat-dialog-actions class="buttons">
<button mat-button mat-raised-button (click)="closeDialog(false)">
<mat-icon>cancel</mat-icon>
<span>&nbsp;Cancel</span>
</button>
<span fxFlex></span>
<button mat-button mat-raised-button color="primary" (click)="closeDialog(true)" cdkFocusInitial>
<mat-icon>save</mat-icon>
<span>&nbsp;Save</span>
</button>
</mat-dialog-actions>

View File

@@ -5,14 +5,16 @@
align-items: center;
justify-content: center;
padding-top: 1%;
padding-bottom: 1%
padding-bottom: 1%;
height: 100%;
}
#login-card {
width: 350px;
display: flex;
flex-direction: column;
justify-content: center;
justify-content: center;
margin-top: -64px;
}
#logo, #name, #long-name, #buttons {

View File

@@ -1,195 +1,189 @@
<!-- (C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC.-->
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
<h3>Classifier Performance Metrics</h3>
</mat-panel-title>
</mat-expansion-panel-header>
<div *ngIf="!latest_avg_metrics">
Classifier doesn't have enough data for training. Please proceed to annotate more documents.
</div>
<table *ngIf="latest_avg_metrics">
<tr>
<h3>Classifier Performance Metrics</h3>
<div *ngIf="!latest_avg_metrics">
Classifier doesn't have enough data for training. Please proceed to annotate more documents.
</div>
<table *ngIf="latest_avg_metrics">
<tr>
<tr>
<tr>
<td>
<i class="material-icons info" matTooltip="Latest average performance metrics">info</i><b>Classifier
Performance Summary:</b>
<i class="material-icons info" matTooltip="Latest average performance metrics">info</i><b>Classifier
Performance Summary:</b>
</td>
</tr>
<tr>
</tr>
<tr>
<td>
<tr>
<tr>
<td>
<i class="material-icons info"
matTooltip="The number of tokens (words) the model predicted correctly to have a certain label. The higher the better.">info</i>True
Positives:
<i class="material-icons info"
matTooltip="The number of tokens (words) the model predicted correctly to have a certain label. The higher the better.">info</i>True
Positives:
</td>
<td>
{{latest_avg_metrics?.Totals.TP}}
{{latest_avg_metrics?.Totals.TP}}
</td>
</tr>
</tr>
<tr>
<tr>
<td>
<i class="material-icons info"
matTooltip="The number of tokens (words) the model predicted correctly not to have a certain label. The higher the better.">info</i>True
Negatives:
<i class="material-icons info"
matTooltip="The number of tokens (words) the model predicted correctly not to have a certain label. The higher the better.">info</i>True
Negatives:
</td>
<td>
{{latest_avg_metrics?.Totals.TN}}
{{latest_avg_metrics?.Totals.TN}}
</td>
</tr>
<tr>
</tr>
<tr>
<td>
<i class="material-icons info"
matTooltip="The number of tokens (words) the model predcited to not have a certain label when it should've had it. The lower the better.">info</i>False
Negatives:
<i class="material-icons info"
matTooltip="The number of tokens (words) the model predcited to not have a certain label when it should've had it. The lower the better.">info</i>False
Negatives:
</td>
<td>
{{latest_avg_metrics?.Totals.FN}}
{{latest_avg_metrics?.Totals.FN}}
</td>
</tr>
<tr>
</tr>
<tr>
<td>
<i class="material-icons info"
matTooltip="The number of tokens (words) the model predicted to have a certain label when it should not have had it. The lower the better">info</i>False
Positives:
<i class="material-icons info"
matTooltip="The number of tokens (words) the model predicted to have a certain label when it should not have had it. The lower the better">info</i>False
Positives:
</td>
<td>
{{latest_avg_metrics?.Totals.FP}}
{{latest_avg_metrics?.Totals.FP}}
</td>
</tr>
<tr>
</tr>
<tr>
<td>
<i class="material-icons info"
matTooltip="The harmonic mean of precision and recall. Number between 0 and 1, 1 being the best value.">info</i>F1:
<i class="material-icons info"
matTooltip="The harmonic mean of precision and recall. Number between 0 and 1, 1 being the best value.">info</i>F1:
</td>
<td>
{{latest_avg_metrics?.Totals.f1}}
{{latest_avg_metrics?.Totals.f1}}
</td>
</tr>
<tr>
</tr>
<tr>
<td>
<i class="material-icons info"
matTooltip="The fraction of label predictions our model got correct out of all the predicitons. The higher the better">info</i>Accuracy:
<i class="material-icons info"
matTooltip="The fraction of label predictions our model got correct out of all the predicitons. The higher the better">info</i>Accuracy:
</td>
<td>
{{latest_avg_metrics?.Totals.acc | percent:'1.2-2'}}
{{latest_avg_metrics?.Totals.acc | percent:'1.2-2'}}
</td>
</tr>
</tr>
<tr>
<tr>
<td>
<i class="material-icons info"
matTooltip="The fraction of tokens (words) our model labeled correctly out of all the cases that were labeled by the model">info</i>Precision:
<i class="material-icons info"
matTooltip="The fraction of tokens (words) our model labeled correctly out of all the cases that were labeled by the model">info</i>Precision:
</td>
<td>
{{latest_avg_metrics?.Totals.precision | percent:'1.2-2'}}
{{latest_avg_metrics?.Totals.precision | percent:'1.2-2'}}
</td>
</tr>
<tr>
</tr>
<tr>
<td>
<i class="material-icons info"
matTooltip="The fraction of cases our model labeled correctly out of all the cases that were correct labels">info</i>Recall:
<i class="material-icons info"
matTooltip="The fraction of cases our model labeled correctly out of all the cases that were correct labels">info</i>Recall:
</td>
<td>
{{latest_avg_metrics?.Totals.recall | percent:'1.2-2'}}
{{latest_avg_metrics?.Totals.recall | percent:'1.2-2'}}
</td>
</tr>
</td>
</tr>
</td>
</tr>
</tr>
<tr>
<tr>
<td>
<i class="material-icons info"
matTooltip="Average metrics up to the evaluation made at this time">info</i><b>Viewing Data for Epoch: </b>
<i class="material-icons info"
matTooltip="Average metrics up to the evaluation made at this time">info</i><b>Viewing Data for Epoch:
</b>
<button mat-button class="drop-down" [matMenuTriggerFor]="epochMenu"
[disabled]="dateDisabled">{{sortedMetrics?.length - epoch}} -
{{sortedMetrics && sortedMetrics[epoch]._updated}} <span
class="material-icons">arrow_drop_down</span></button>
<mat-menu #epochMenu="matMenu">
<button *ngFor="let epoch of sortedMetrics; let i = index" mat-menu-item
(click)="selectEpoch(i)">{{sortedMetrics?.length-i}} -
{{epoch._updated | date}}</button>
</mat-menu>
<button mat-button class="drop-down" [matMenuTriggerFor]="epochMenu"
[disabled]="dateDisabled">{{sortedMetrics?.length - epoch}} -
{{sortedMetrics && sortedMetrics[epoch]._updated}} <span
class="material-icons">arrow_drop_down</span></button>
<mat-menu #epochMenu="matMenu">
<button *ngFor="let epoch of sortedMetrics; let i = index" mat-menu-item
(click)="selectEpoch(i)">{{sortedMetrics?.length-i}} -
{{epoch._updated | date}}</button>
</mat-menu>
</td>
</tr>
</tr>
<tr>
<tr>
<td>
<i class="material-icons info"
matTooltip="Data for which the evaluation metrics will be displayed. Select Overall for average of the whole model or select a specific label for averages metrics over that label.">info</i><b>Viewing
Data for label: </b>
<button mat-button class="drop-down" [matMenuTriggerFor]="labelMenu">{{label}}<span
class="material-icons">arrow_drop_down</span></button>
<mat-menu #labelMenu="matMenu">
<button *ngFor="let label of sortedMetrics[epoch].metric_averages | keyvalue" mat-menu-item
(click)="selectLabel(label.key)">{{label.key === 'Totals' ? 'Overall' : label.key }} </button>
</mat-menu>
<i class="material-icons info"
matTooltip="Data for which the evaluation metrics will be displayed. Select Overall for average of the whole model or select a specific label for averages metrics over that label.">info</i><b>Viewing
Data for label: </b>
<button mat-button class="drop-down" [matMenuTriggerFor]="labelMenu">{{label}}<span
class="material-icons">arrow_drop_down</span></button>
<mat-menu #labelMenu="matMenu">
<button *ngFor="let label of sortedMetrics[epoch].metric_averages | keyvalue" mat-menu-item
(click)="selectLabel(label.key)">{{label.key === 'Totals' ? 'Overall' : label.key }} </button>
</mat-menu>
</td>
</tr>
</tr>
<tr>
<tr>
<td>
<mat-tab-group (selectedTabChange)="changedTabs($event)" style="min-width:800px">
<mat-tab style="min-width:800px">
<ng-template mat-tab-label>
<i class="material-icons info"
matTooltip="The matrix showcases the amount of tokens the model predicted as positive and negative versus the actual values">info</i>Confusion
Matrix
</ng-template>
<app-conf-matrix [data]="confMatrixData">
</app-conf-matrix>
</mat-tab>
<mat-tab style="position:relative; height: 100%; min-width: 800px;">
<ng-template mat-tab-label>
<i class="material-icons info"
matTooltip="The venn diagram represents the amount of presition and recall. The blue circle is the amount of False Negatives and the orange circle is the amount of False Positives while the intersection is the amount of True Positives. The larger the intersection the higher precision and recall; a larger amount of False Negatives relative to False Positives represents high precision and low recall, while a larger amount of False Positives relative to False Negatives represent a high recall and low precision. The larger the intersection the better.">info</i>Recall
and Precision
</ng-template>
<mat-grid-list cols="4" rowHeight="1:3">
<mat-grid-tile colspan="3">
<div style="max-height: 100%; max-width: 100%; overflow: auto">
<app-venn-diag [data]="vennDiagramData"></app-venn-diag>
</div>
</mat-grid-tile>
<mat-grid-tile>
<mat-tab-group (selectedTabChange)="changedTabs($event)" style="min-width:800px">
<mat-tab style="min-width:800px">
<ng-template mat-tab-label>
<i class="material-icons info"
matTooltip="The matrix showcases the amount of tokens the model predicted as positive and negative versus the actual values">info</i>Confusion
Matrix
</ng-template>
<app-conf-matrix [data]="confMatrixData">
</app-conf-matrix>
</mat-tab>
<mat-tab style="position:relative; height: 100%; min-width: 800px;">
<ng-template mat-tab-label>
<i class="material-icons info"
matTooltip="The venn diagram represents the amount of presition and recall. The blue circle is the amount of False Negatives and the orange circle is the amount of False Positives while the intersection is the amount of True Positives. The larger the intersection the higher precision and recall; a larger amount of False Negatives relative to False Positives represents high precision and low recall, while a larger amount of False Positives relative to False Negatives represent a high recall and low precision. The larger the intersection the better.">info</i>Recall
and Precision
</ng-template>
<mat-grid-list cols="4" rowHeight="1:3">
<mat-grid-tile colspan="3">
<div style="max-height: 100%; max-width: 100%; overflow: auto">
<app-venn-diag [data]="vennDiagramData"></app-venn-diag>
</div>
</mat-grid-tile>
<mat-grid-tile>
<h3>
Recall:
{{sortedMetrics[epoch].metric_averages && sortedMetrics[epoch].metric_averages[getCorrectLabel(label)].recall | percent:'1.2-2'}}
<br>
Precision:
{{sortedMetrics[epoch].metric_averages && sortedMetrics[epoch].metric_averages[getCorrectLabel(label)].precision | percent:'1.2-2'}}
</h3>
</mat-grid-tile>
</mat-grid-list>
<h3>
Recall:
{{sortedMetrics[epoch].metric_averages && sortedMetrics[epoch].metric_averages[getCorrectLabel(label)].recall | percent:'1.2-2'}}
<br>
Precision:
{{sortedMetrics[epoch].metric_averages && sortedMetrics[epoch].metric_averages[getCorrectLabel(label)].precision | percent:'1.2-2'}}
</h3>
</mat-grid-tile>
</mat-grid-list>
</mat-tab>
<mat-tab style="min-width:800px">
<ng-template mat-tab-label>
<i class="material-icons info"
matTooltip="Averages, Precision and Recall measured at each epoch.">info</i>Historic
Data
</ng-template>
<app-metrics-history [label]="getCorrectLabel(label)" [data]="sortedMetrics"></app-metrics-history>
</mat-tab>
</mat-tab-group>
</mat-tab>
<mat-tab style="min-width:800px">
<ng-template mat-tab-label>
<i class="material-icons info"
matTooltip="Averages, Precision and Recall measured at each epoch.">info</i>Historic
Data
</ng-template>
<app-metrics-history [label]="getCorrectLabel(label)" [data]="sortedMetrics"></app-metrics-history>
</mat-tab>
</mat-tab-group>
</td>
</tr>
</table>
</mat-expansion-panel>
</mat-accordion>
</tr>
</table>

View File

@@ -1,7 +1,46 @@
/*(C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC. */
:host {
width: 100%;
width: 100%;
display: block;
height: 100%;
}
.delete-icon {
margin-bottom: 4px;
}
.delete-cell {
padding-left: 0px !important;
width: 40px;
}
.annotate-table-wrapper {
position: absolute;
top: 56px;
right: 0;
left: 0;
bottom: 0;
overflow-y: auto;
}
.annotate-doc-toolbar {
display: flex;
align-items: center;
padding: 8px 16px;
height: 56px;
position: absolute;
top: 0px;
width: 100%;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.annotate-doc-toolbar .mat-title {
margin-bottom: 0px;
}
.table-container {
}
#table {
@@ -19,11 +58,6 @@
padding-right: 10px;
}
#table .mat-column-text {
padding-left: 0px;
width: 100%;
}
#table .mat-column-actions {
padding-right: 0px;
}

View File

@@ -1,11 +1,13 @@
<!-- (C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC.-->
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<h1>My Annotations</h1>
<div class="annotate-doc-toolbar" fxLayout="row">
<span class="mat-title">Annotation List</span>
<span fxFlex></span>
<mat-form-field id="filter" floatLabel="never">
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
</mat-form-field>
</mat-panel-title>
</mat-expansion-panel-header>
</div>
<div class="annotate-table-wrapper">
<table mat-table id="table" [dataSource]="dataSource" matSort>
<ng-container matColumnDef="text">
@@ -16,7 +18,8 @@
<ng-container matColumnDef="label">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Label </th>
<td mat-cell *matCellDef="let annotation">
<mat-chip [style.background-color]="getLabelColor(annotation.label)" class="shadowed">{{annotation.label}}</mat-chip>
<mat-chip [style.background-color]="getLabelColor(annotation.label)" class="shadowed">
{{annotation.label}}</mat-chip>
</td>
</ng-container>
@@ -31,9 +34,11 @@
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> Actions </th>
<td mat-cell *matCellDef="let annotation">
<button mat-raised-button (click)="removeAnnotation(annotation)">Remove</button>
<th class="delete-cell" mat-header-cell *matHeaderCellDef></th>
<td class="delete-cell" mat-cell *matCellDef="let annotation">
<button mat-icon-button color="warn" (click)="removeAnnotation(annotation)">
<mat-icon class="delete-icon">delete_outline</mat-icon>
</button>
</td>
</ng-container>
@@ -41,16 +46,4 @@
<tr mat-row class="trow" *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<div id="paginator">
<mat-form-field id="filter">
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
</mat-form-field>
<div class="spacer"></div>
<mat-paginator [pageSizeOptions]="[5, 10, 20, 50]" showFirstLastButtons></mat-paginator>
</div>
</mat-expansion-panel>
</div>

View File

@@ -0,0 +1 @@
export * from './toolbar/toolbar.component';

View File

@@ -0,0 +1,12 @@
<a mat-button [routerLink]="link" routerLinkActive="selected">
<div fxLayout="column" class="btn-container">
<div fxFlex="6px"></div>
<div fxFlex class="btn-content">
<mat-icon *ngIf="icon" class="menu-icon">{{icon}}</mat-icon>
<span class="menu-text mat-h3">
<ng-content></ng-content>
</span>
</div>
<div fxFlex="6px" class="bottom-bar"></div>
</div>
</a>

View File

@@ -0,0 +1,24 @@
a {
display: flex;
height: 100%;
align-items: center;
border-radius: 0;
padding: 0px;
}
a.selected {
background-color: rgba(0, 0, 0, 0.1);
}
a.selected .bottom-bar {
background-color: #232941;
}
.btn-container {
line-height: 52px;
}
fa-icon {
margin-right: 8px;
}
.btn-content {
padding: 0 16px;
}

View File

@@ -0,0 +1,30 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ToolbarNavButtonComponent } from './toolbar-nav-button.component';
describe('ToolbarNavButtonComponent', () => {
let component: ToolbarNavButtonComponent;
let fixture: ComponentFixture<ToolbarNavButtonComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
ToolbarNavButtonComponent
],
schemas: [
NO_ERRORS_SCHEMA
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ToolbarNavButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,11 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-toolbar-nav-button',
templateUrl: './toolbar-nav-button.component.html',
styleUrls: ['./toolbar-nav-button.component.scss']
})
export class ToolbarNavButtonComponent {
@Input() icon;
@Input() link;
}

View File

@@ -0,0 +1,6 @@
<ng-container>
<app-toolbar-nav-button class="doc-collections-btn" [icon]="eco" link="/collection">Document Collections</app-toolbar-nav-button>
<button class="info-btn" mat-icon-button matTooltip="About" (click)="doAbout()">
<mat-icon>info</mat-icon>
</button>
</ng-container>

View File

@@ -0,0 +1,11 @@
:host {
flex-direction: row;
box-sizing: border-box;
display: flex;
height: 100%;
margin-left: 10px;
}
.info-btn {
margin-top: 10px;
}

View File

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

View File

@@ -0,0 +1,21 @@
import { Component, OnInit } from '@angular/core';
import { show } from "../../about/about.component";
import { MatDialog } from '@angular/material';
@Component({
selector: 'app-toolbar-nav',
templateUrl: './toolbar-nav.component.html',
styleUrls: ['./toolbar-nav.component.scss']
})
export class ToolbarNavComponent implements OnInit {
constructor(private dialog: MatDialog) { }
ngOnInit() {
}
public doAbout() {
show(this.dialog);
}
}

View File

@@ -0,0 +1,23 @@
<mat-toolbar class="app-toolbar" color="primary" fxFlex="nogrow" fxLayout="row">
<div></div>
<div fxLayout="row" style="cursor: pointer" [routerLink]=" ['/home'] " routerLinkActive="active">
<img class="logo-img" src="assets/PMAP_data_catalog_logo_crop@4x.png" alt="PMAP Data Catalog Logo"/>
<h1>{{title}}</h1>
</div>
<ng-container *ngIf="user">
<app-toolbar-nav></app-toolbar-nav>
<span fxFlex></span>
<button mat-flat-button color="primary" [matMenuTriggerFor]="userMenu" aria-label="User Profile">
<mat-icon class="account-icon">account_circle</mat-icon>
<span id="toolbar-user-name">{{user.details.givenName}} {{user.details.sn}}</span>
<mat-icon>keyboard_arrow_down</mat-icon>
</button>
<mat-menu #userMenu="matMenu" yPosition="below" overlapTrigger="false">
<!-- <app-user-card></app-user-card> -->
</mat-menu>
</ng-container>
</mat-toolbar>

View File

@@ -0,0 +1,21 @@
.app-toolbar {
padding-right: 4px;
}
.app-icon {
margin-right: 10px;
}
app-toolbar-nav {
margin-left: 24px;
}
.account-icon {
margin-right: 4px;
}
.logo-img {
height: 36px;
margin-right: 12px;
margin-top: -4px;
}

View File

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

View File

@@ -0,0 +1,27 @@
import { Component, OnInit } from '@angular/core';
import { AuthService } from 'src/app/service/auth/auth.service';
@Component({
selector: 'app-toolbar',
templateUrl: './toolbar.component.html',
styleUrls: ['./toolbar.component.scss']
})
export class ToolbarComponent implements OnInit {
title = "PMAP Data Catalog"
//user: AuthUser;
//faBookOpen = faBookOpen;
constructor(private authService: AuthService) { }
ngOnInit() {
/* this.authService.currentUser$.subscribe((user) =>{
this.user = user;
}); */
}
logout(): void {
this.authService.logout();
}
}

View File

@@ -0,0 +1,27 @@
<div *ngIf="user" class="user-container">
<div fxLayout="row" class="user-details">
<mat-icon class="user-icon">person</mat-icon>
<span fxFlex>
<div class="mat-body-strong">{{user.display_name}}</div>
<div class="mat-body">{{user.username}}</div>
</span>
</div>
<mat-divider></mat-divider>
<div fxLayout="column" class="action-buttons">
<button class="user-btn" *ngIf="user.is_admin" mat-raised-button [routerLink]="['/' + PATHS.admin.dashboard]" routerLinkActive="active">
<mat-icon>settings_applications</mat-icon>
<span fxFlex>Admin Dashboard</span>
</button>
<button class="user-btn" mat-raised-button [routerLink]="['/' + PATHS.user.account]" routerLinkActive="active">
<mat-icon>account_circle</mat-icon>
<span fxFlex>Manage Account</span>
</button>
<button class="user-btn logout-btn" color="accent" mat-raised-button (click)="logout()">
<mat-icon>exit_to_app</mat-icon>
<span fxFlex>Logout</span>
</button>
</div>
</div>

View File

@@ -0,0 +1,29 @@
.user-container {
min-width: 200px;
max-width: 280px;
padding: 10px;
}
.user-icon {
font-size: 40px;
height: 40px;
width: 40px;
margin-top: auto;
margin-bottom: auto;
margin-right: 6px
}
mat-divider {
margin-top: 10px;
}
.action-buttons button {
margin-top: 10px;
}
.user-btn {
width: 100%;
}
.user-btn mat-icon {
margin: auto;
}

View File

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

View File

@@ -0,0 +1,26 @@
import { Component, OnInit } from '@angular/core';
import { User, UserDetails } from 'src/app/model/user';
import { AuthService } from 'src/app/service/auth/auth.service';
import { PATHS } from "../../app.paths";
@Component({
selector: 'app-user-card',
templateUrl: './user-card.component.html',
styleUrls: ['./user-card.component.scss']
})
export class UserCardComponent implements OnInit {
public readonly PATHS = PATHS;
public user: User;
constructor(private authService: AuthService) { }
ngOnInit() {
this.user = this.authService.loggedInUser;
}
logout(): void {
this.authService.logout();
}
}

View File

@@ -1,42 +1,45 @@
/*(C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC. */
#header {
width: 100%;
.list-container {
height: 100%;
width: 100%;
position: relative;
}
/deep/ #header > .mat-card-header-text {
width: 100%;
.filter-bar {
position: absolute;
top: 64px;
height: 58px;
padding: 0px 16px;
width: 100%;
display: flex;
align-items: center;
flex-direction: row;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
#title {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
::ng-deep .archive-toggle .mat-button-toggle-label-content {
line-height: 32px !important;
}
.add-doc-btn {
padding: 0 50px;
.keyword-input {
width: 280px;
}
tr.trow {
background-color: #f7ffff;
transition: .2s;
border-radius: 6px;
box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.26);
border: 1px solid #f7ffff;
margin: 10px;
.list-container > mat-toolbar {
position: absolute;
top: 0px;
width: 100%;
}
tr.trow:hover {
background-color: #eafdfd;
transition: .2s;
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.26);
border: 1px solid #eafdfd;
.list-container > .table-container {
position: absolute;
top: 122px;
bottom: 0px;
right: 0px;
left: 0px;
overflow-y: auto;
}
tr.trow:active {
background-color: #e7fdfd;
transition: .2s;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);
border: 1px solid #ddffff;
.list-container > .table-container table {
width: 100%;
}

View File

@@ -1,52 +1,54 @@
<!-- (C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC.-->
<app-loading></app-loading>
<mat-card *ngIf="!loading.loading && !loading.error">
<mat-card-header id="header">
<mat-card-title id="title">
<h1>My Collections</h1>
<span class="spacer"></span>
<mat-radio-group [value]="active ? 'active' : 'archived'" (change)="archiveChanged($event)">
<mat-radio-button value="active">Active</mat-radio-button>
&nbsp;&nbsp;&nbsp;&nbsp;
<mat-radio-button value="archived">Archived</mat-radio-button>
</mat-radio-group>
</mat-card-title>
</mat-card-header>
<!-- <app-loading></app-loading> -->
<mat-divider></mat-divider>
<div class="list-container" fxFlexFill>
<mat-toolbar>
<span>Document Collections</span>
<span *ngIf="loading || error" fxFlex="10px"></span>
<mat-spinner *ngIf="loading" diameter="26"></mat-spinner>
<mat-error class="mat-body" *ngIf="!loading && error">{{error}}</mat-error>
<span fxFlex></span>
<button mat-stroked-button (click)="openCreateCollectionDialog()">
<mat-icon>add</mat-icon>Add Document Collection
</button>
</mat-toolbar>
<mat-card-content style="padding-top: 2%; width: 100%">
<mat-form-field>
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
</mat-form-field>
<table mat-table [dataSource]="dataSource" style="width: 100%; border-spacing: 0 5px;">
<div class="filter-bar">
<mat-button-toggle-group [value]="active ? 'active' : 'archived'" class="archive-toggle"
aria-label="Archive Toggle" (change)="archiveChanged($event)">
<mat-button-toggle value="active">Active</mat-button-toggle>
<mat-button-toggle value="archive">Archived</mat-button-toggle>
</mat-button-toggle-group>
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef>Title</th>
<td mat-cell *matCellDef="let collection">{{ collection.title }}</td>
</ng-container>
<span fxFlex="32px"></span>
<ng-container matColumnDef="creator">
<th mat-header-cell *matHeaderCellDef>Creator</th>
<td mat-cell *matCellDef="let collection">{{ collection.creator }}</td>
</ng-container>
<mat-form-field class="keyword-input" floatLabel="never">
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
</mat-form-field>
</div>
<ng-container matColumnDef="last_updated">
<th mat-header-cell *matHeaderCellDef>Last Updated</th>
<td mat-cell *matCellDef="let collection">{{ collection.last_updated }}</td>
</ng-container>
<div class="table-container">
<table mat-table class="table-selectable" [dataSource]="dataSource">
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row class="trow spacing" *matRowDef="let row; columns: displayedColumns;" [routerLink]="['/' + PATHS.collection.details, row.id]" ></tr>
</table>
<mat-paginator [pageSize]="20" [pageSizeOptions]="[5, 10, 20, 150, 00]" showFirstLastButtons></mat-paginator>
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef>Title</th>
<td mat-cell *matCellDef="let collection">{{ collection.title }}</td>
</ng-container>
<ng-container matColumnDef="creator">
<th mat-header-cell *matHeaderCellDef>Creator</th>
<td mat-cell *matCellDef="let collection">{{ collection.creator }}</td>
</ng-container>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button [routerLink]="[appConfig.landingPage]"><span class="material-icons">home</span>Home</button>
<ng-container matColumnDef="last_updated">
<th mat-header-cell *matHeaderCellDef>Last Updated</th>
<td mat-cell *matCellDef="let collection">{{ collection.last_updated }}</td>
</ng-container>
</mat-card-actions>
</mat-card>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row class="trow spacing" *matRowDef="let row; columns: displayedColumns;"
[routerLink]="['/' + PATHS.collection.details, row.id]"></tr>
</table>
</div>
</div>

View File

@@ -1,17 +1,16 @@
/*(C) 2019 The Johns Hopkins University Applied Physics Laboratory LLC. */
import { Component, OnInit, ViewChild } from "@angular/core";
import { MatPaginator, MatTableDataSource } from "@angular/material";
import { MatPaginator, MatTableDataSource, MatDialog } from "@angular/material";
import { PATHS } from "../../app.paths";
import { AppConfig } from "../../app.config";
import { LoadingComponent } from "../loading/loading.component";
import { CollectionRepositoryService } from "../../service/collection-repository/collection-repository.service";
import { AuthService } from "../../service/auth/auth.service";
import { Collection } from "../../model/collection";
import { AddCollectionComponent } from '../add-collection/add-collection.component';
export interface CollectionRow {
id: string;
@@ -31,8 +30,8 @@ export class ViewCollectionsComponent implements OnInit {
public readonly PATHS = PATHS;
@ViewChild(LoadingComponent)
public loading: LoadingComponent;
public loading: boolean;
public error: string;
public active = true;
@@ -47,15 +46,27 @@ export class ViewCollectionsComponent implements OnInit {
constructor(public collectionsService: CollectionRepositoryService,
public appConfig: AppConfig,
private auth: AuthService) { }
private auth: AuthService,
private dialogService: MatDialog) { }
ngOnInit() {
this.reload();
}
public openCreateCollectionDialog() {
let dialogRef = this.dialogService.open(AddCollectionComponent, {
width: '520px'
});
dialogRef.afterClosed().subscribe((result) => {
if (result) {
this.reload();
}
});
}
private reload() {
this.loading.loading = true;
this.loading.clearError();
this.loading = true;
this.error = undefined;
const response = this.active ? this.collectionsService.getMyUnarchivedCollectionsPaginated() :
this.collectionsService.getMyArchivedCollectionsPaginated();
const temp = [];
@@ -67,14 +78,14 @@ export class ViewCollectionsComponent implements OnInit {
last_updated: collection._updated,
});
}, (error) => {
this.loading.setError(`Error ${error.status}: ${error.message}`);
this.loading.loading = false;
this.error = `Error ${error.status}: ${error.message}`;
this.loading = false;
}, () => {
temp.sort((a: CollectionRow, b: CollectionRow) => a.title.localeCompare(b.title));
this.collections = temp;
this.dataSource.data = this.collections;
this.dataSource.paginator = this.paginator;
this.loading.loading = false;
this.loading = false;
});
}

View File

@@ -25,6 +25,7 @@ export class Document extends ModelObject implements DBDocument {
public text: string;
public metadata: {[key: string]: any};
public has_annotated: {[user_id: string]: boolean};
public _created: any;
public getTextPreview(n: number = Document.DEFAULT_PREVIEW_LENGTH) {
return `${this.text.slice(0, n)}...`;

View File

@@ -4,28 +4,34 @@ import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterSt
import { AuthService } from "./auth.service";
import { AppConfig } from "../../app.config";
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
constructor(private authService: AuthService,
private router: Router,
private appConfig: AppConfig) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
private router: Router,
private appConfig: AppConfig) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
if (this.authService.isAuthenticated()) {
return true;
} else {
if (state.url === "/" || "") {
this.router.navigate([this.appConfig.loginPage], { queryParams: { returnUrl: this.appConfig.landingPage } });
} else {
this.router.navigate([this.appConfig.loginPage], { queryParams: { returnUrl: state.url} });
}
return this.authService.isAuthenticatedObs().pipe(map((isAuth) => {
if (!isAuth) {
if (state.url === "/" || "") {
this.router.navigate([this.appConfig.loginPage], { queryParams: { returnUrl: this.appConfig.landingPage } });
} else {
this.router.navigate([this.appConfig.loginPage], { queryParams: { returnUrl: state.url } });
}
}
return isAuth;
}));
}
return false;
}
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
return this.canActivate(route, state);
}
}
@@ -34,24 +40,24 @@ export class AuthGuard implements CanActivate, CanActivateChild {
export class AdminAuthGuard implements CanActivate, CanActivateChild {
constructor(private authService: AuthService,
private router: Router,
private appConfig: AppConfig) { }
private router: Router,
private appConfig: AppConfig) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
if(this.authService.flat) {
if (this.authService.flat) {
return false;
} else if(this.authService.isAuthenticated()) {
} else if (this.authService.isAuthenticated()) {
return this.authService.loggedInUser.is_admin;
} else {
if (state.url === "/" || "") {
this.router.navigate([this.appConfig.loginPage], { queryParams: { returnUrl: this.appConfig.landingPage } });
} else {
this.router.navigate([this.appConfig.loginPage], { queryParams: { returnUrl: state.url} });
this.router.navigate([this.appConfig.loginPage], { queryParams: { returnUrl: state.url } });
}
}
return false;
}
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
return this.canActivate(route, state);
}

View File

@@ -1,7 +1,7 @@
import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
import { Observable, forkJoin } from "rxjs";
import { Observable, forkJoin, of } from "rxjs";
import { BackendService } from "../backend/backend.service";
@@ -16,40 +16,41 @@ import { EveAuthModule } from "./modules/eve-auth-module";
import { LoginForm } from "../../model/login";
import { User, UserDetails } from "../../model/user";
import { map, catchError } from 'rxjs/operators';
@Injectable()
export class AuthService {
public module: AuthModule = null;
public moduleName: string = null;
public loggedInUser: User = null;
public flat: boolean;
public canManageUsers: boolean;
constructor(private backend: BackendService,
private router: Router,
private appConfig: AppConfig) {
private router: Router,
private appConfig: AppConfig) {
}
public instantiate(): Observable<string> {
return new Observable((observer) => {
forkJoin([this.getModule(),
this.getFlat(),
this.getBackendLoggedInUser(),
this.getCanManageUsers()]).subscribe(([module,
flat,
user,
canManageUsers]) => {
this.getFlat(),
this.getBackendLoggedInUser(),
this.getCanManageUsers()]).subscribe(([module,
flat,
user,
canManageUsers]) => {
this.moduleName = module;
this.flat = flat;
this.canManageUsers = canManageUsers;
this.loggedInUser = user;
if(module === VegasAuthModule.NAME) {
if (module === VegasAuthModule.NAME) {
this.module = new VegasAuthModule(this.backend, this, this.router);
observer.next(module);
observer.complete();
} else if(module === EveAuthModule.NAME) {
} else if (module === EveAuthModule.NAME) {
this.module = new EveAuthModule(this.backend, this, this.router);
(<EveAuthModule>this.module).reloadAllUsers().subscribe(() => {
observer.next(module);
@@ -68,7 +69,7 @@ export class AuthService {
});
});
}
public updateLoggedInUser(): Observable<any> {
return new Observable<any>((observer) => {
this.getBackendLoggedInUser().subscribe((user: User) => {
@@ -81,7 +82,7 @@ export class AuthService {
});
});
}
private getBackendLoggedInUser(): Observable<User> {
return this.backend.get<User>("/auth/logged_in_user");
}
@@ -102,14 +103,25 @@ export class AuthService {
return this.backend.get("/auth/logged_in_user_details");
}
// public getAllUsers(): Observable<User[]> {
// return this.canManageUsers ? this.module.getAllUsers() : null;
// }
// public getAllUsers(): Observable<User[]> {
// return this.canManageUsers ? this.module.getAllUsers() : null;
// }
public isAuthenticated(): boolean {
return this.loggedInUser != null;
}
public isAuthenticatedObs(): Observable<boolean> {
return this.getBackendLoggedInUser().pipe(
catchError((err) => {
return of(false);
}),
map((user: User) => {
this.loggedInUser = user;
return user ? true : false;
}));
}
public getLoginForm(): Observable<LoginForm> {
return this.backend.get<LoginForm>("/auth/login_form");
}

View File

@@ -31,7 +31,7 @@ export class OAuthAuthorizeComponent implements OnInit {
ngOnInit(): void {
this.route.queryParams.subscribe((params) => {
this.route.fragment.subscribe((fragment) => {
let return_to = PATHS.home;
let return_to = PATHS.collection.view;
if(params["return_to"]) {
return_to = params["return_to"];
}

View File

@@ -71,6 +71,11 @@ export class BackendService {
{...options, ...{withCredentials: true, headers: this.IEheaders }});
}
public patch<T>(path: string, data: object = {}, options = {}): Observable<T> {
return this.http.patch<T>(AppConfig.settings.backend.host + path, data,
{...options, ...{withCredentials: true, headers: this.IEheaders }});
}
public postForm<T>(path: string, form: FormData, options = {}): Observable<T> {
// setting a header to "Content-Type": "multipart/form-data" makes this not work...
return this.http.post<T>(AppConfig.settings.backend.host + path, form,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

View File

@@ -3,6 +3,29 @@
@import '~roboto-fontface/css/roboto/roboto-fontface.css';
@import '~tippy.js/dist/themes/light-border.css';
/* make Flex work. */
html,
body,
app-root {
height: 100%;
width: 100%;
margin: 0px;
display: flex;
font-family: Roboto, "Helvetica Neue", sans-serif;
}
body {
margin: 0px !important;
}
.btn-short {
max-height: 36px;
}
.mat-form-field-infix {
width: auto !important;
}
#loading {
font-weight: bold;
}

View File

@@ -2,8 +2,144 @@
@include mat-core();
$custom-theme-primary: mat-palette($mat-blue-grey);
$custom-theme-accent: mat-palette($mat-cyan);
/* Palettes */
$md-whaleblue: (
50 : #e5e5e8,
100 : #bdbfc6,
200 : #9194a0,
300 : #65697a,
400 : #44495e,
500 : #232941,
600 : #1f243b,
700 : #1a1f32,
800 : #15192a,
900 : #0c0f1c,
A100 : #5d78ff,
A200 : #2a4eff,
A400 : #0029f6,
A700 : #0025dd,
contrast: (
50 : #000000,
100 : #000000,
200 : #000000,
300 : #ffffff,
400 : #ffffff,
500 : #ffffff,
600 : #ffffff,
700 : #ffffff,
800 : #ffffff,
900 : #ffffff,
A100 : #ffffff,
A200 : #ffffff,
A400 : #ffffff,
A700 : #ffffff,
)
);
$md-deeppine: (
50 : #e5ebea,
100 : #bfcccb,
200 : #94aba8,
300 : #698985,
400 : #496f6b,
500 : #295651,
600 : #244f4a,
700 : #1f4540,
800 : #193c37,
900 : #0f2b27,
A100 : #6affe5,
A200 : #37ffdc,
A400 : #04ffd3,
A700 : #00eac1,
contrast: (
50 : #000000,
100 : #000000,
200 : #000000,
300 : #ffffff,
400 : #ffffff,
500 : #ffffff,
600 : #ffffff,
700 : #ffffff,
800 : #ffffff,
900 : #ffffff,
A100 : #000000,
A200 : #000000,
A400 : #000000,
A700 : #000000,
)
);
$md-jade: (
50 : #e9f2ec,
100 : #c8dfcf,
200 : #a4c9b0,
300 : #80b390,
400 : #64a378,
500 : #499360,
600 : #428b58,
700 : #39804e,
800 : #317644,
900 : #216433,
A100 : #a5ffbb,
A200 : #72ff95,
A400 : #3fff6f,
A700 : #25ff5c,
contrast: (
50 : #000000,
100 : #000000,
200 : #000000,
300 : #000000,
400 : #000000,
500 : #ffffff,
600 : #ffffff,
700 : #ffffff,
800 : #ffffff,
900 : #ffffff,
A100 : #000000,
A200 : #000000,
A400 : #000000,
A700 : #000000,
)
);
$md-summergreen: (
50 : #f3f7f4,
100 : #e0eae4,
200 : #cbddd2,
300 : #b6cfbf,
400 : #a7c4b2,
500 : #97baa4,
600 : #8fb39c,
700 : #84ab92,
800 : #7aa389,
900 : #699478,
A100 : #ffffff,
A200 : #d8ffe5,
A400 : #a5ffc4,
A700 : #8bffb4,
contrast: (
50 : #000000,
100 : #000000,
200 : #000000,
300 : #000000,
400 : #000000,
500 : #000000,
600 : #000000,
700 : #000000,
800 : #000000,
900 : #000000,
A100 : #000000,
A200 : #000000,
A400 : #000000,
A700 : #000000,
)
);
/* Theme */
$custom-theme-primary: mat-palette($md-deeppine, 500);
$custom-theme-accent: mat-palette($md-whaleblue, 500);
$custom-theme-warn: mat-palette($mat-red);
$custom-theme: mat-light-theme($custom-theme-primary, $custom-theme-accent, $custom-theme-warn);
@@ -16,4 +152,9 @@ $custom-theme: mat-light-theme($custom-theme-primary, $custom-theme-accent, $cus
background-color: mat-color($custom-theme-accent, lighter) !important;
}
.table-selectable tr.trow:hover {
background-color: mat-color($custom-theme-primary, lighter);
cursor: pointer;
}
@include angular-material-theme($custom-theme);

View File

@@ -4,13 +4,13 @@ describe("Sanity Tests", function() {
it("Checks Page Loads and is Eve", function() {
cy.visit("/");
cy.contains("Username or email")
cy.get(".login-field")
.should("be.visible");
});
it("Checks Invalid Username Eve Login", function() {
cy.visit("/");
cy.contains("Username or email")
cy.get(".login-field")
.should("be.visible");
cy.get("input[name='username']")
.should("be.visible")
@@ -30,7 +30,7 @@ describe("Sanity Tests", function() {
it("Checks Invalid Password Eve Login", function() {
cy.visit("/");
cy.contains("Username or email")
cy.get(".login-field")
.should("be.visible");
cy.get("input[name='username']")
.should("be.visible")
@@ -52,7 +52,7 @@ describe("Sanity Tests", function() {
cy.fixture("users.json").then((users) => {
var user = users.find((u) => u["_id"] == Cypress.env("LOGIN_USER") || u["email"] == Cypress.env("LOGIN_USER"));
cy.visit("/");
cy.contains("Username or email")
cy.get(".login-field")
.should("be.visible");
cy.get("input[name='username']")
.should("be.visible")
@@ -78,8 +78,8 @@ describe("Sanity Tests", function() {
it("Checks About Dialog With Eve", function() {
cy.pine_login_eve();
cy.visit("/");
cy.get("#nav-list")
.contains("About")
cy.get(".app-toolbar")
.get(".info-btn")
.should("be.visible")
.click();
cy.get("app-about")
@@ -95,8 +95,8 @@ describe("Sanity Tests", function() {
.find("button").contains("Close")
.should("be.visible")
.click();
cy.get("#nav-list")
.contains("About")
cy.get(".app-toolbar")
.get(".info-btn")
.should("be.visible");
cy.pine_logout();
});

View File

@@ -10,7 +10,7 @@ function verifyCollectionDetails(collection = {
viewers: array,
annotators: array
}) {
cy.contains("Collection Details")
cy.contains(collection.metadata.title)
.should("be.visible");
return cy.get("#metadata-table", {timeout: 20 * 1000})
.should("be.visible")
@@ -94,7 +94,7 @@ function verifyCollectionDetails(collection = {
}
});
for(const button of ["Add Document", "Upload Images", "Next Document to Annotate", "Archive Collection", "Download Data"]) {
for(const button of ["Upload Images", "Archive", "Download Data"]) {
cy.get("button")
.contains(button)
.parent("button")
@@ -109,36 +109,16 @@ function verifyCollectionDetails(collection = {
describe("Collections Tests", function() {
it("Checks Collections are Visible in Navigation Menu with Eve", function() {
cy.pine_login_eve();
cy.get("#pineNavMyCollections")
.should("be.visible")
.click();
cy.get("#pineNavViewAllCollections")
.should("be.visible");
cy.fixture("collections.json").then((collections) => {
for(const data of collections) {
const collection = data.collection;
cy.containsElemExactly("button", collection.metadata.title, true)
.should("be.visible");
}
});
cy.get("#pineNavMyCollections")
.should("be.visible")
.click({force: true}); // close menu
cy.pine_logout();
});
it("Checks Collections are Visible in My Collections Page with Eve", function() {
cy.pine_login_eve();
cy.contains("View My Collections")
cy.get(".doc-collections-btn")
.should("be.visible")
.click();
cy.fixture("collections.json").then((collections) => {
for(const data of collections) {
const collection = data.collection;
cy.containsElemExactly("td", collection.metadata.title)
.should("be.visible");
.should("be.visible");
}
});
cy.pine_logout();
@@ -149,13 +129,16 @@ describe("Collections Tests", function() {
cy.fixture("collections.json").then((collections) => {
for(const data of collections) {
const collection = data.collection;
cy.visit("/home");
cy.contains("View My Collections")
cy.get(".doc-collections-btn")
.should("be.visible")
.click();
cy.containsElemExactly("td", collection.metadata.title)
.should("be.visible")
.click();
cy.get(".title-tabs")
.contains("Details")
.should("be.visible")
.click();
verifyCollectionDetails(collection);
}
});
@@ -165,8 +148,7 @@ describe("Collections Tests", function() {
it("Checks Adding and Archiving of a Collection with Eve", function() {
cy.pine_login_eve();
cy.visit("/")
cy.get("#nav-list")
.contains("Add Collection")
cy.contains("Add Document Collection")
.should("be.visible")
.click();
cy.get("app-add-collection")
@@ -273,63 +255,43 @@ describe("Collections Tests", function() {
cy.get("@form")
.submit();
// wait to get redirected to details page
cy.location().should((loc) => {
expect(loc.pathname).to.include("collection/details");
});
verifyCollectionDetails(collection).then(collection_id => collection._id = collection_id);
cy.location().should((loc) => {
expect(loc.pathname).to.eq(`/collection/details/${collection._id}`);
});
// make sure title is now listed in collection details page and in the nav menu
cy.get("#pineNavMyCollections")
.should("be.visible")
.click();
cy.containsElemExactly("button", collection.metadata.title, true)
.should("be.visible");
cy.get("#pineNavMyCollections")
.should("be.visible")
.click({force: true}); // close menu
cy.visit("/");
cy.contains("View My Collections")
cy.get(".doc-collections-btn")
.should("be.visible")
.click();
cy.containsElemExactly("td", collection.metadata.title)
.should("be.visible")
.click();
cy.get(".title-tabs")
.contains("Details")
.should("be.visible")
.click();
verifyCollectionDetails(collection).then(collection_id => collection._id = collection_id);
cy.location().should((loc) => {
expect(loc.pathname).to.eq(`/collection/details/${collection._id}`);
});
// archive collection
cy.get("button")
.contains("Unarchive Collection")
.contains("Unarchive")
.should("not.exist");
cy.get("button")
.contains("Archive Collection")
.parent("button")
.contains("Archive")
.click();
cy.wait(500);
cy.get("button")
.contains("Unarchive Collection")
.contains("Unarchive")
.should("exist");
// make sure title is no longer listed
cy.get("#pineNavMyCollections")
.should("be.visible")
.click();
cy.containsElemExactly("button", collection.metadata.title, true)
.should("not.exist");
cy.get("#pineNavMyCollections")
.should("be.visible")
.click({force: true}); // close menu
cy.visit("/");
cy.contains("View My Collections")
cy.get(".doc-collections-btn")
.should("be.visible")
.click();
cy.containsElemExactly("td", collection.metadata.title)
.should("not.exist");
// wait for snackbar
cy.wait(3000);
cy.pine_logout();
});

View File

@@ -8,11 +8,14 @@ const document_fragments = [
];
function goToCollection(collection_title) {
cy.visit("/");
cy.contains("View My Collections")
cy.get(".doc-collections-btn")
.should("be.visible")
.click();
return cy.contains(collection_title)
cy.contains(collection_title)
.should("be.visible")
.click();
return cy.get(".title-tabs")
.contains("Details")
.should("be.visible")
.click().then(_ => {
cy.contains("Collection title", {timeout: 20 * 1000})
@@ -36,16 +39,20 @@ function goToDocument(collection_title, document_fragment) {
return goToCollection(collection_title).then(collection_id => {
expect(collection_id).to.be.a("string");
expect(collection_id.length).to.be.at.least(1);
cy.contains("Documents in Collection")
.parents("mat-card")
cy.get(".title-tabs")
.contains("Documents")
.should("be.visible")
.click();
cy.get(".doc-list-table")
.find("table.mat-table")
.then(table => {
cy.wrap(table).find("td")
.contains(document_fragment)
.click();
.click({ force: true });
});
var document_id;
cy.contains("Document Details")
cy.get(".title-tabs")
.contains("Details")
.should("be.visible")
.click().then(deets => {
cy.wrap(deets).get("table")
@@ -63,6 +70,7 @@ function goToDocument(collection_title, document_fragment) {
cy.wrap(deets).get("table")
.get("tr")
.contains("Collection")
.scrollIntoView()
.should("be.visible")
.parents("tr")
.children("td").eq(1)
@@ -73,7 +81,7 @@ function goToDocument(collection_title, document_fragment) {
});
});
cy.location().should((loc) => {
expect(loc.pathname).to.eq(`/annotate/${document_id}`);
expect(loc.pathname).to.eq(`/collection/annotate/${document_id}`);
});
return document_id;
});
@@ -117,7 +125,7 @@ function annotateWord(wordSubject, label) {
// first column: text, should be the same as the word we clicked
cy.wrap($tds[0]).should("have.text", $word.text());
// second column: label, should be the same as the label we clicked
cy.wrap($tds[1]).should("have.text", $label.text());
cy.wrap($tds[1]).invoke("text").then((text) => { expect(text.trim()).to.equal($label.text().trim()) });
// third column: start, should match the class of the word we clicked
cy.wrap($tds[2]).should("have.text", $word.attr("word-start"));
// fourth column: end, should match the class of the word we clicked
@@ -154,10 +162,7 @@ function unannotateWord(wordSubject) {
}
function save() {
return cy.get("@doc")
.parents("mat-card")
.find("button")
.contains("Save")
return cy.get(".annotate-button")
.click().then(_ => {
cy.get("snack-bar-container")
.should("be.visible")
@@ -169,31 +174,16 @@ function save() {
describe("Documents Tests", function() {
it("Checks Documents are Visible in Navigation Menu with Eve", function() {
cy.pine_login_eve();
cy.get("#pineNavMyCollections")
.should("be.visible")
.click();
cy.contains(collection_title)
.should("be.visible")
.click();
for(const frag of document_fragments) {
cy.contains(frag)
.should("exist", {timeout: 20 * 1000});
}
cy.get("#pineNavMyCollections")
.should("be.visible")
.click({force: true}); // close menu
cy.pine_logout();
});
it("Checks Documents are Visible in Collection Details Page with Eve", function() {
cy.pine_login_eve();
goToCollection(collection_title);
cy.contains("Collection Details")
cy.contains(collection_title)
.should("be.visible");
cy.contains("Documents in Collection")
.parents("mat-card")
cy.get(".title-tabs")
.contains("Documents")
.should("be.visible")
.click();
cy.get(".doc-list-table")
.find("table.mat-table")
.then(table => {
for(const frag of document_fragments) {
@@ -209,23 +199,21 @@ describe("Documents Tests", function() {
cy.pine_login_eve();
const frag = document_fragments[0];
goToDocument(collection_title, frag);
cy.get("mat-panel-title")
.contains("Document Details")
.should("exist");
cy.get("mat-panel-title")
cy.get(".mat-title")
.contains("Document Labeling")
.should("exist");
cy.get("mat-panel-title")
cy.get(".title-tabs")
.contains("Image")
.should("exist");
cy.get("mat-panel-title")
.contains("Others' Labels and Annotations")
.should("exist");
cy.get("mat-panel-title")
cy.get(".title-tabs")
.contains("Annotations")
.should("exist")
.click();
cy.get(".mat-title")
.contains("NER Annotations")
.should("exist");
cy.get("mat-panel-title")
.contains("My Annotations")
cy.get(".mat-title")
.contains("Annotation List")
.should("exist");
cy.get("#doc")
.should(($doc) => {
@@ -237,18 +225,18 @@ describe("Documents Tests", function() {
it("Annotates an Unannotated Document with Eve", function() {
cy.pine_login_eve();
goToCollection(collection_title);
cy.get(".title-tabs")
.contains("Documents")
.should("be.visible")
.click();
// find the first unannotated document
cy.contains("Documents in Collection")
.parents("mat-card")
.find("mat-select")
.set_mat_select_value("100")
.parents("mat-card")
cy.get(".doc-list-table")
.find("table.mat-table")
.find("mat-icon")
.contains("clear")
.first()
.should("exist")
.click();
.click({ force: true });
cy.get("app-ner-annotation-table")
.find("table")
@@ -269,12 +257,19 @@ describe("Documents Tests", function() {
.should("not.have.class", "select")
.should("not.have.class", "annotation")
.as("firstWord");
cy.contains("Document Labeling")
.parents("mat-card")
cy.get(".title-tabs")
.contains("Details")
.should("be.visible")
.click();
cy.get(".doc-labeling-container")
.find("mat-chip")
.first()
.then($firstLabel => {
annotateWord(cy.get("@firstWord"), $firstLabel.text());
cy.get(".title-tabs")
.contains("Annotations")
.should("be.visible")
.click();
annotateWord(cy.get("@firstWord"), $firstLabel.text().trim());
});
save();

View File

@@ -100,17 +100,19 @@ Cypress.Commands.add("pine_login_eve", {}, () => {
cy.request("POST", Cypress.env("API_URL") + "/auth/login",
{"username": Cypress.env("LOGIN_USER"), "password": user["password"]});
cy.visit("/login?checkBackend");
cy.get("#toolbar")
cy.get(".app-toolbar")
.contains(user["_id"])
.should("be.visible");
});
});
Cypress.Commands.add("pine_logout", {}, () => {
cy.get("#pineNavLogout")
cy.get(".user-menu-btn")
.click()
.get(".logout-btn")
.should("be.visible")
.click();
// the following check should change if auth module isn't eve
cy.contains("Username or email")
cy.get(".login-field")
.should("be.visible");
});