diff --git a/.vscode/settings.json b/.vscode/settings.json index 7f87feb7f..98187e1ab 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,5 +22,6 @@ ".vscode", ".tsling.json" ], - "explorer.compactFolders": false + "explorer.compactFolders": false, + "prettier.semi": false } diff --git a/README.md b/README.md index 08f5a88bb..9ac830e44 100644 --- a/README.md +++ b/README.md @@ -93,11 +93,7 @@ $ npm run test:all #### Code Coverage -On the completion of the unit tests, the system will automatically generate a code coverage report. To open this, run the following command: - -```bash -$ npm run code-coverage -``` +On the completion of the unit tests, the system will automatically generate a code coverage report. You can view the report here: `./coverage/workbench-client/index.html` ## To build @@ -109,6 +105,16 @@ $ npm run build Move the generated files from the `/dist` directory to the required location. +## Build Statistics + +When adding a library to the repository, you may wish to view its cost on the system. You can view the build size using the following command: + +```typescript +$ npm run stats +``` + +This will allow you to compare the bundle size impacts before and after the update by switching between checked out commits/branches. + ## Common Problems Check our Wiki pages for help with common problems and using systems custom to our application. diff --git a/angular.json b/angular.json index 709afa47d..022d86c02 100644 --- a/angular.json +++ b/angular.json @@ -34,6 +34,9 @@ "assets": ["src/favicon.ico", "src/assets", "src/manifest.json"], "styles": [ "src/styles.scss", + "node_modules/primeng/resources/themes/nova-light/theme.css", + "node_modules/primeng/resources/primeng.min.css", + "node_modules/primeicons/primeicons.css", "node_modules/snazzy-info-window/dist/snazzy-info-window.css" ], "scripts": [], @@ -131,6 +134,9 @@ "assets": ["src/assets", "src/manifest.json"], "styles": [ "src/styles.scss", + "node_modules/primeng/resources/themes/nova-light/theme.css", + "node_modules/primeng/resources/primeng.min.css", + "node_modules/primeicons/primeicons.css", "node_modules/snazzy-info-window/dist/snazzy-info-window.css" ], "scripts": [] diff --git a/baw-client.code-workspace b/baw-client.code-workspace index a220d64a3..377ed08b4 100644 --- a/baw-client.code-workspace +++ b/baw-client.code-workspace @@ -40,7 +40,8 @@ }, "peacock.color": "#2b7489", "angularKarmaTestExplorer.debugMode": true, - "explorer.compactFolders": false + "explorer.compactFolders": false, + "prettier.semi": false }, "extensions": { "recommendations": [ diff --git a/package-lock.json b/package-lock.json index e62e06361..1d6264903 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6474,10 +6474,9 @@ } }, "filesize": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", - "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==", - "dev": true + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", + "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==" }, "fill-range": { "version": "7.0.1", @@ -10794,6 +10793,16 @@ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", "dev": true }, + "primeicons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-2.0.0.tgz", + "integrity": "sha512-GJTCeMSQU8UU1GqbsaDrg/IH+b/vSinJQl52NVpdJ7sShYLZA8Eq6jLF48Ye3N/dQloGrE07i7XsZvxQ9pNbqg==" + }, + "primeng": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-9.0.5.tgz", + "integrity": "sha512-juugoXZaU7TyGyTSFx0PNjObqM+/RTY/arJ/LwyH/0KaIv0V+Oqfvnx8btJ96kLOZ+h0xLWXLuAbxsTXe9EX6w==" + }, "private": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", @@ -13428,7 +13437,41 @@ "version": "3.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", - "dev": true + "dev": true, + "requires": { + "ts-node": "^8.8.2" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "ts-node": { + "version": "8.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.9.1.tgz", + "integrity": "sha512-yrq6ODsxEFTLz0R3BX2myf0WBCSQh9A+py8PBo1dCzWIOcvisbyH6akNKqDHMgXePF2kir5mm5JXJTH3OUJYOQ==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + } + } }, "ultron": { "version": "1.1.1", diff --git a/package.json b/package.json index 54e121172..4f62263fa 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "version": "3.0.0", "scripts": { "build": "ng build --prod", - "code-coverage": "npm test -- --watch=false && http-server -c-1 -o -p 9875 ./coverage/workbench-client", "e2e": "ng e2e", "int:extract": "ng xi18n --output-path src/locale", "lint": "ng lint", @@ -47,12 +46,15 @@ "@ngx-loading-bar/http-client": "^4.2.0", "@swimlane/ngx-datatable": "^16.0.3", "bootstrap": "^4.4.1", + "filesize": "^6.1.0", "full-icu": "^1.3.1", "humanize-duration": "^3.22.0", "immutable": "^4.0.0-rc.12", "lodash": "^4.17.15", "luxon": "^1.23.0", "ngx-toastr": "^12.0.1", + "primeicons": "^2.0.0", + "primeng": "^9.0.5", "rxjs": "^6.5.5", "snazzy-info-window": "^1.1.1", "tslib": "^1.11.1", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index c3708aa13..f0d80d2a3 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,7 +1,7 @@ import { AgmCoreModule, LazyMapsAPILoaderConfigLiteral, - LAZY_MAPS_API_CONFIG + LAZY_MAPS_API_CONFIG, } from "@agm/core"; import { AgmSnazzyInfoWindowModule } from "@agm/snazzy-info-window"; import { HttpClientModule } from "@angular/common/http"; @@ -23,7 +23,7 @@ import { ErrorModule } from "./component/error/error.module"; import { HomeModule } from "./component/home/home.module"; import { MyAccountModule, - ProfileModule + ProfileModule, } from "./component/profile/profile.module"; import { ProjectsModule } from "./component/projects/projects.module"; import { ReportProblemsModule } from "./component/report-problem/report-problem.module"; @@ -44,7 +44,7 @@ export const appLibraryImports = [ AgmSnazzyInfoWindowModule, FormlyModule.forRoot(formlyRoot), FormlyBootstrapModule, - ToastrModule.forRoot(toastrRoot) + ToastrModule.forRoot(toastrRoot), ]; export const appImports = [ @@ -62,7 +62,7 @@ export const appImports = [ StatisticsModule, // these last two must be last! HomeModule, - ErrorModule + ErrorModule, ]; /** @@ -84,14 +84,14 @@ export class GoogleMapsConfig implements LazyMapsAPILoaderConfigLiteral { AppRoutingModule, HttpClientModule, ...appLibraryImports, - ...appImports + ...appImports, ], providers: [ ...providers, - { provide: LAZY_MAPS_API_CONFIG, useClass: GoogleMapsConfig } + { provide: LAZY_MAPS_API_CONFIG, useClass: GoogleMapsConfig }, ], bootstrap: [AppComponent], entryComponents: [PermissionsShieldComponent], - exports: [] + exports: [], }) export class AppModule {} diff --git a/src/app/component/projects/harvest-complete/harvest-complete.component.html b/src/app/component/projects/harvest-complete/harvest-complete.component.html new file mode 100644 index 000000000..9b7868b2c --- /dev/null +++ b/src/app/component/projects/harvest-complete/harvest-complete.component.html @@ -0,0 +1,36 @@ +

{{ project.name }}

+ + + + + + + + Details + + + Play + + + Visualize + + + + diff --git a/src/app/component/projects/harvest-complete/harvest-complete.component.spec.ts b/src/app/component/projects/harvest-complete/harvest-complete.component.spec.ts new file mode 100644 index 000000000..12f95fd58 --- /dev/null +++ b/src/app/component/projects/harvest-complete/harvest-complete.component.spec.ts @@ -0,0 +1,23 @@ +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; +import { HarvestCompleteComponent } from "./harvest-complete.component"; + +xdescribe("HarvestCompleteComponent", () => { + let component: HarvestCompleteComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [HarvestCompleteComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HarvestCompleteComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/component/projects/harvest-complete/harvest-complete.component.ts b/src/app/component/projects/harvest-complete/harvest-complete.component.ts new file mode 100644 index 000000000..770306790 --- /dev/null +++ b/src/app/component/projects/harvest-complete/harvest-complete.component.ts @@ -0,0 +1,55 @@ +import { Component, Input, OnInit } from "@angular/core"; +import { PagedTableTemplate } from "src/app/helpers/tableTemplate/pagedTableTemplate"; +import { Project } from "src/app/models/Project"; +import { Site } from "src/app/models/Site"; +import { ApiErrorDetails } from "src/app/services/baw-api/api.interceptor.service"; +import { SitesService } from "src/app/services/baw-api/sites.service"; + +@Component({ + selector: "app-project-harvest-complete", + templateUrl: "./harvest-complete.component.html", +}) +export class HarvestCompleteComponent extends PagedTableTemplate + implements OnInit { + @Input() project: Project; + public sites: Site[]; + public error: ApiErrorDetails; + + constructor(api: SitesService) { + super(api, (sites) => + sites.map((site) => ({ + id: site.id, + name: site.name, + actions: site, + })) + ); + } + + ngOnInit(): void { + this.columns = [{ name: "Id" }, { name: "Name" }, { name: "Actions" }]; + this.sortKeys = { + id: "id", + name: "name", + }; + + this.getPageData(this.project); + } + + public detailsPath(site: Site) { + return site.getViewUrl(this.project); + } + + public playPath(site: Site) { + return "/broken_link"; + } + + public visualizePath(site: Site) { + return "/broken_link"; + } +} + +interface TableRow { + id: number; + name: string; + actions: Site; +} diff --git a/src/app/component/projects/harvest-review/harvest-review.component.html b/src/app/component/projects/harvest-review/harvest-review.component.html new file mode 100644 index 000000000..ed841e2db --- /dev/null +++ b/src/app/component/projects/harvest-review/harvest-review.component.html @@ -0,0 +1,47 @@ +
+ + + + Name + Project + Site + Point + Status + + + + + +
+ + {{ rowData.name }} +
+ + + + + + + + + + + + + + +
+
+
diff --git a/src/app/component/projects/harvest-review/harvest-review.component.spec.ts b/src/app/component/projects/harvest-review/harvest-review.component.spec.ts new file mode 100644 index 000000000..fdcec7f2b --- /dev/null +++ b/src/app/component/projects/harvest-review/harvest-review.component.spec.ts @@ -0,0 +1,23 @@ +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; +import { HarvestReviewComponent } from "./harvest-review.component"; + +xdescribe("HarvestReviewComponent", () => { + let component: HarvestReviewComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [HarvestReviewComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HarvestReviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/component/projects/harvest-review/harvest-review.component.ts b/src/app/component/projects/harvest-review/harvest-review.component.ts new file mode 100644 index 000000000..bb8e95738 --- /dev/null +++ b/src/app/component/projects/harvest-review/harvest-review.component.ts @@ -0,0 +1,234 @@ +import { Component, OnInit } from "@angular/core"; +import { TreeNode } from "primeng/api/treenode"; +import { AbstractModel } from "src/app/models/AbstractModel"; +import { Project } from "src/app/models/Project"; +import { Site } from "src/app/models/Site"; +import { Status } from "../../shared/indicator/indicator.component"; + +@Component({ + selector: "app-project-harvest-review", + templateUrl: "./harvest-review.component.html", + styles: [ + ` + .status { + width: 70px; + max-width: 70px; + } + `, + ], +}) +export class HarvestReviewComponent implements OnInit { + public files: TreeNode[] = []; + public columns = 4; + + constructor() {} + + ngOnInit(): void { + const projectPool = [ + new Project({ id: 1, name: "QLD" }), + new Project({ id: 2, name: "NSW" }), + new Project({ id: 3, name: "TAS" }), + new Project({ id: 4, name: "VIC" }), + new Project({ id: 5, name: "SA" }), + new Project({ id: 6, name: "NT" }), + new Project({ id: 7, name: "WA" }), + ]; + const sitePool = [ + new Site({ id: 1, name: "Avaries" }), + new Site({ id: 2, name: "Gympie" }), + new Site({ id: 3, name: "Woondum" }), + ]; + const pointPool = Array.from(Array(10).keys()).map( + (id) => new Site({ id, name: "Point" + id }) + ); + + this.generateRootFolder( + "Creek", + projectPool.slice(0, 3), + sitePool, + pointPool + ); + this.generateRootFolder( + "Woodland", + projectPool.slice(3, 7), + sitePool, + pointPool + ); + this.generateRootFolder( + "Desert", + projectPool.slice(2, 5), + sitePool, + pointPool + ); + } + + public getText(models: Set) { + if (!models) { + return []; + } + + const text = []; + models.forEach((model) => { + text.push(model.name); + }); + return text; + } + + private generateRootFolder( + name: string, + projectPool: Project[], + sitePool: Site[], + pointPool: Site[] + ) { + const { children, status, models } = this.generateFolders( + projectPool, + sitePool, + pointPool + ); + + this.files.push({ + data: { + name, + status, + ...models, + }, + children, + }); + } + + private generateFolders( + projectPool: Project[], + sitePool: Site[], + pointPool: Site[] + ) { + let status: Status = Status.Success; + const children = []; + const models = { + projects: new Set([]), + sites: new Set([]), + points: new Set([]), + }; + + for (let i = 0; i < 30; i++) { + const { + children: fileChildren, + status: fileStatus, + models: fileModels, + } = this.generateFiles( + (hour: number) => `201810_${hour}0000_REC [-27.3866 152.8761].flac`, + projectPool, + sitePool, + pointPool + ); + + const child = { + data: { + name: `201810${i}_AAO [-27.3866 152.8761]`, + status: fileStatus, + ...fileModels, + }, + children: fileChildren, + }; + + // Append to sets + models.projects = union(fileModels.projects, child.data.projects); + models.sites = union(fileModels.sites, child.data.sites); + models.points = union(fileModels.points, child.data.points); + + // Apply worst status + if (fileStatus > status) { + status = fileStatus; + } + + children.push(child); + } + + // Return results + return { + children, + status, + models, + }; + } + + private generateFiles( + nameCallback: (hour: number) => string, + projectPool: Project[], + sitePool: Site[], + pointPool: Site[] + ) { + let status = Status.Success; + const children = []; + const models = { + projects: new Set([]), + sites: new Set([]), + points: new Set([]), + }; + + // Create children + for (let i = 0; i < 24; i++) { + const childStatus = this.getRandomStatus(); + const projects = this.getRandomModel(projectPool, childStatus); + const sites = this.getRandomModel(sitePool, childStatus); + const points = this.getRandomModel(pointPool, childStatus, true); + + children.push({ + data: { + name: nameCallback(i), + status: childStatus, + projects, + sites, + points, + }, + }); + + // Append to sets + models.projects = union(models.projects, projects); + models.sites = union(models.sites, sites); + models.points = union(models.points, points); + + // Apply worst status + if (childStatus > status) { + status = childStatus; + } + } + + // Return results + return { + children, + status, + models, + }; + } + + private getRandomModel( + pool: AbstractModel[], + status: Status, + isPoint?: boolean + ) { + if ((isPoint && status >= Status.Warning) || status === Status.Error) { + return new Set([]); + } else { + return new Set([pool[Math.floor(Math.random() * pool.length)]]); + } + } + + private getRandomStatus() { + const rand = Math.random(); + if (rand > 0.999) { + return Status.Error; + } else if (rand > 0.99) { + return Status.Warning; + } else { + return Status.Success; + } + } +} + +function union(setA: Set, setB: Set) { + const _union = new Set(setA); + for (const elem of setB) { + _union.add(elem); + } + return _union; +} diff --git a/src/app/component/projects/pages/harvest/harvest.component.html b/src/app/component/projects/pages/harvest/harvest.component.html new file mode 100644 index 000000000..55681d48d --- /dev/null +++ b/src/app/component/projects/pages/harvest/harvest.component.html @@ -0,0 +1,256 @@ +
+

+ Project: {{ project.name }} +
+ Harvest +

+ + + +
+ +
+

Basic Details

+ +

+ Harvesting is the name we use for the process of uploading/ingesting + large amounts of audio data (e.g 3TB, or 4000 files at a time) into our + website. For our users, this allows you to submit your recorded + environment audio for analysis and view the results directly through the + website. Currently harvesting performs several actions such as: +

+ +
    +
  • + Converting audio files to formats compatible with our website +
  • +
  • + Basic integrity checks on the data harvested +
  • +
  • + Gathering metadata about the audio (format, channels, bitrate, sample + rate, duration, etc.) +
  • +
  • + Assigning audio to sites and projects +
  • +
+ +

+ If you wish to upload your recorded audio data, press the button at the + bottom right which will guide you through the process. Please be aware + that this will take a long time to complete, however you are not + required to perform it all in one sitting. If you leave this page and + come back, it will automatically detect what stage of harvesting you + were at. +

+
+ + +
+

Credentials

+ +

+ The initial step to harvesting is to upload your audio data to our + server. The credentials you can see below are generated with one-time + details that will expire after [INSERT] hours. Please keep track of the + username, password, and URI until you have finished your upload. To + access the server, open the URI link in a new tab/window and enter your + credentials (username/password) when prompted. The file structure you + use is very important when you upload, and we ask you follow the + following format: +

+ +
+/{{ "{" }}project{{ "}" }}/{{ "{" }}deployment{{ "}" }}/{{ "{" }}site{{
+          "}"
+        }}/[{{ "{" }}memory_card{{ "}" }}/]
+ +

Syntax

+
    +
  • + / represents a folder separator (either on Windows (\) or Linux (/)) +
  • +
  • + The very first / represents the root folder (i.e the folder that we + see when we initially enter the server) +
  • +
  • + The tokens between the square braces ([ and ]) represent optional + folders +
  • +
  • + The tokens between the curly braces ({{ "{" }} and {{ "}" }}) + represent folder names +
  • +
+ +

Token Guide

+
    +
  • + {{ "{" }}project{{ "}" }}: A short name for the project the audio is + being collected for. Ideally should be similar to the project name + user on the workbench +
  • +
  • + {{ "{" }}deployment{{ "}" }}: A human readable date that roughly + represents when the data was collected. Optionally it may include the + deployment dates as a range. +
  • +
  • + {{ "{" }}site{{ "}" }}: A short name that describes the site (the + location on the ground) where the sensor was deployed. Ideally this + name should be similar to site name used on the workbench +
  • +
  • + {{ "{" }}memory_card{{ "}" }}: An optional folder. If the sensor used + more than one memory card, audio should be kept in the same folders + that represent the memory card it was recorded on. +
  • +
+ +

Store of other data

+

+ We generally encourage users to copy in any relevant data. In + particular, leaving notes that describe the data (e.g “left mic failed”, + or “RTC failed, dates invalid”), or copies of their records. Any + metadata is appreciated. +

+

+ Most data should be put in the site folders as usually, most important + information is related to a sensor. +

+ +

Details

+ + +

Current Progress

+ +
    +
  • Uploaded Files: {{ progress }}
  • +
  • + Uploaded Bytes: {{ progress * 31234321 }} ({{ + filesize(progress * 31234321) + }}) +
  • +
+
+ + +
+

Checking Files

+ +

+ Currently our system is running an initial check of the files uploaded. + This is to ensure the audio files are valid, and will attempt to extract + useful metadata. Once checking is complete, you will be able to view a + list of uploaded files, their projects, sites, points, and status + (success, warning, error). +

+

+ The progress bar below shows you the current progress, and will update + automatically without refreshing the page. You can optionally leave this + page and come back at another time, and the site will take you to the + most up-to-date stage of harvest (i.e if checking is complete, you will + arrive on the Review page). +

+ + +

+ +

+
+ + +
+

Review

+ +

+ This is a review of the audio data you have uploaded. Data is + automatically categorized into three different states: Success, Warning, + and Error. Success implies that the previous step was able to + successfully extract the project, site, and point of an audio file. + Warning indicates that some errors may have been experienced attempting + to extract metadata from the audio file, and it may be missing project, + site, or point information. Error means an unrecoverable issue has + occurred and you may need to re-upload the file. +

+ + +
+ + +
+

Harvesting Files

+ +

+ Harvesting the files into the system. This is transferring the files + from the server you uploaded to, into the relevant storage required to + view on the website. The progress bar below shows you the current + progress, and will update automatically without refreshing the page. +

+ + +

+ +

+
+ + +
+

Summary

+ +

+ This is a summary of all the projects, sites, and points that have been + updated. +

+ +
+ +
+ + + +
+ + +
+ + +
+
+
diff --git a/src/app/component/projects/pages/harvest/harvest.component.scss b/src/app/component/projects/pages/harvest/harvest.component.scss new file mode 100644 index 000000000..f47bc9943 --- /dev/null +++ b/src/app/component/projects/pages/harvest/harvest.component.scss @@ -0,0 +1,8 @@ +// Set width of prime ng steps so that they fill screen +.ui-steps ul { + display: flex; + flex-direction: row; +} +.ui-steps .ui-steps-item { + flex: 1; +} diff --git a/src/app/component/projects/pages/harvest/harvest.component.spec.ts b/src/app/component/projects/pages/harvest/harvest.component.spec.ts new file mode 100644 index 000000000..f9f0204d1 --- /dev/null +++ b/src/app/component/projects/pages/harvest/harvest.component.spec.ts @@ -0,0 +1,23 @@ +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; +import { HarvestComponent } from "./harvest.component"; + +xdescribe("HarvestComponent", () => { + let component: HarvestComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [HarvestComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HarvestComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/component/projects/pages/harvest/harvest.component.ts b/src/app/component/projects/pages/harvest/harvest.component.ts new file mode 100644 index 000000000..6552c8177 --- /dev/null +++ b/src/app/component/projects/pages/harvest/harvest.component.ts @@ -0,0 +1,157 @@ +import { Component, OnInit, ViewEncapsulation } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import filesize from "filesize"; +import { List } from "immutable"; +import { MenuItem } from "primeng/api/menuitem"; +import { Observable, Subscription, timer } from "rxjs"; +import { map, startWith, takeWhile } from "rxjs/operators"; +import { PermissionsShieldComponent } from "src/app/component/shared/permissions-shield/permissions-shield.component"; +import { WidgetMenuItem } from "src/app/component/shared/widget/widgetItem"; +import { Page } from "src/app/helpers/page/pageDecorator"; +import { Project } from "src/app/models/Project"; +import { projectResolvers } from "src/app/services/baw-api/projects.service"; +import { ResolvedModel } from "src/app/services/baw-api/resolver-common"; +import { + harvestProjectMenuItem, + projectCategory, + projectMenuItem, +} from "../../projects.menus"; +import { projectMenuItemActions } from "../details/details.component"; + +const projectKey = "project"; + +@Page({ + category: projectCategory, + menus: { + actions: List([projectMenuItem, ...projectMenuItemActions]), + actionsWidget: new WidgetMenuItem(PermissionsShieldComponent, {}), + links: List(), + }, + resolvers: { + [projectKey]: projectResolvers.show, + }, + self: harvestProjectMenuItem, +}) +@Component({ + selector: "app-harvest", + templateUrl: "./harvest.component.html", + styleUrls: ["./harvest.component.scss"], + // tslint:disable-next-line: use-component-view-encapsulation + encapsulation: ViewEncapsulation.None, +}) +export class HarvestComponent implements OnInit { + public failure: boolean; + public filesize = filesize; + public progress: number; + public project: Project; + public stage: number; + public stages: { + previous: { text?: string; disabled?: boolean }; + next: { text?: string; disabled?: boolean }; + timer: { enable: boolean; callback?: () => void }; + }[] = [ + { previous: {}, next: { text: "Start" }, timer: { enable: false } }, + { + previous: { text: "Cancel" }, + next: { text: "Finished Uploading" }, + timer: { enable: true }, + }, + { + previous: {}, + next: {}, + timer: { + enable: true, + callback: () => { + this.nextStage(); + }, + }, + }, + { + previous: { text: "Re-submit Files" }, + next: { text: "Finish Review" }, + timer: { enable: false }, + }, + { + previous: {}, + next: {}, + timer: { + enable: true, + callback: () => { + this.nextStage(); + }, + }, + }, + { previous: {}, next: {}, timer: { enable: false } }, + ]; + public steps = { + start: 0, + credentials: 1, + check: 2, + review: 3, + harvest: 4, + summary: 5, + }; + public harvestSteps: MenuItem[] = [ + { label: "Start" }, + { label: "Credentials" }, + { label: "Check" }, + { label: "Review" }, + { label: "Harvest" }, + { label: "Summary" }, + ]; + private intervalSpeed = 300; + + private subscription: Subscription; + private mockTimer: Observable = timer(0, this.intervalSpeed).pipe( + startWith(0), + map((v) => { + this.progress = v; + return this.progress; + }), + takeWhile(() => this.progress < 100) + ); + + constructor(private route: ActivatedRoute) {} + + ngOnInit(): void { + const resolvedProject: ResolvedModel = this.route.snapshot.data[ + projectKey + ]; + + if (resolvedProject.error) { + this.failure = true; + return; + } + + this.project = resolvedProject.model; + this.stage = 0; + } + + public nextStage() { + this.stage++; + this.harvestObs(); + } + + public previousStage() { + // Review page should go to Credentials + if (this.stage === this.steps.review) { + this.stage = this.steps.credentials; + } else { + this.stage--; + } + + this.harvestObs(); + } + + private harvestObs() { + const mockTimer = this.stages[this.stage].timer; + if (mockTimer.enable) { + this.subscription?.unsubscribe(); + this.subscription = this.mockTimer.subscribe( + () => {}, + () => {}, + mockTimer.callback ? mockTimer.callback : () => {} + ); + } + } +} diff --git a/src/app/component/projects/pill-list/pill-list.component.html b/src/app/component/projects/pill-list/pill-list.component.html new file mode 100644 index 000000000..6266a1a7d --- /dev/null +++ b/src/app/component/projects/pill-list/pill-list.component.html @@ -0,0 +1,13 @@ + + {{ pill }} + + + diff --git a/src/app/component/projects/pill-list/pill-list.component.spec.ts b/src/app/component/projects/pill-list/pill-list.component.spec.ts new file mode 100644 index 000000000..d9fd50787 --- /dev/null +++ b/src/app/component/projects/pill-list/pill-list.component.spec.ts @@ -0,0 +1,23 @@ +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; +import { PillListComponent } from "./pill-list.component"; + +xdescribe("PillListComponent", () => { + let component: PillListComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [PillListComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PillListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/component/projects/pill-list/pill-list.component.ts b/src/app/component/projects/pill-list/pill-list.component.ts new file mode 100644 index 000000000..f4e94ecde --- /dev/null +++ b/src/app/component/projects/pill-list/pill-list.component.ts @@ -0,0 +1,31 @@ +import { Component, Input, OnInit } from "@angular/core"; + +@Component({ + selector: "app-pill-list", + templateUrl: "./pill-list.component.html", +}) +export class PillListComponent implements OnInit { + @Input() text: string[]; + @Input() numPills: number; + public pills: string[]; + public ellipsis: boolean; + public compress: boolean; + + constructor() {} + + ngOnInit() { + this.compress = this.numPills < this.text.length; + this.pills = this.getPills(this.numPills); + } + + public getPills(displayPills: number) { + this.ellipsis = displayPills < this.text.length; + return this.ellipsis ? this.text.slice(0, displayPills) : this.text; + } + + public toggleExpansion() { + this.pills = this.ellipsis + ? this.getPills(this.text.length) + : this.getPills(this.numPills); + } +} diff --git a/src/app/component/projects/projects.menus.ts b/src/app/component/projects/projects.menus.ts index b41957039..3b59d010c 100644 --- a/src/app/component/projects/projects.menus.ts +++ b/src/app/component/projects/projects.menus.ts @@ -1,6 +1,7 @@ import { Category, MenuRoute } from "@interfaces/menusInterfaces"; import { StrongRoute } from "@interfaces/strongRoute"; import { + defaultAudioIcon, defaultDeleteIcon, defaultEditIcon, defaultNewIcon, @@ -99,3 +100,12 @@ export const deleteProjectMenuItem = MenuRoute({ route: projectMenuItem.route.add("delete"), tooltip: () => "Delete this project", }); + +export const harvestProjectMenuItem = MenuRoute({ + icon: defaultAudioIcon, + label: "Harvest Data", + parent: projectMenuItem, + predicate: isProjectOwnerPredicate, + route: projectMenuItem.route.add("harvest"), + tooltip: () => "Upload new audio to this project", +}); diff --git a/src/app/component/projects/projects.module.ts b/src/app/component/projects/projects.module.ts index 9d6f476ef..7469ef64d 100644 --- a/src/app/component/projects/projects.module.ts +++ b/src/app/component/projects/projects.module.ts @@ -4,14 +4,18 @@ import { RouterModule } from "@angular/router"; import { GetRouteConfigForPage } from "@helpers/page/pageRouting"; import { MapModule } from "@shared/map/map.module"; import { SharedModule } from "@shared/shared.module"; +import { HarvestCompleteComponent } from "./harvest-complete/harvest-complete.component"; +import { HarvestReviewComponent } from "./harvest-review/harvest-review.component"; import { AssignComponent } from "./pages/assign/assign.component"; import { DeleteComponent } from "./pages/delete/delete.component"; import { DetailsComponent } from "./pages/details/details.component"; import { EditComponent } from "./pages/edit/edit.component"; +import { HarvestComponent } from "./pages/harvest/harvest.component"; import { ListComponent } from "./pages/list/list.component"; import { NewComponent } from "./pages/new/new.component"; import { PermissionsComponent } from "./pages/permissions/permissions.component"; import { RequestComponent } from "./pages/request/request.component"; +import { PillListComponent } from "./pill-list/pill-list.component"; import { projectsRoute } from "./projects.menus"; import { SiteCardComponent } from "./site-card/site-card.component"; @@ -20,12 +24,16 @@ const components = [ DeleteComponent, DetailsComponent, EditComponent, + HarvestCompleteComponent, + HarvestComponent, + HarvestReviewComponent, ListComponent, NewComponent, PermissionsComponent, RequestComponent, SiteCardComponent, SiteCardComponent, + PillListComponent, ]; const routes = projectsRoute.compileRoutes(GetRouteConfigForPage); diff --git a/src/app/component/shared/indicator/indicator.component.html b/src/app/component/shared/indicator/indicator.component.html new file mode 100644 index 000000000..0e4a9dae9 --- /dev/null +++ b/src/app/component/shared/indicator/indicator.component.html @@ -0,0 +1,19 @@ +
+ +
+ +
+ + +
+ +
+ + +
+ +
+
diff --git a/src/app/component/shared/indicator/indicator.component.scss b/src/app/component/shared/indicator/indicator.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/component/shared/indicator/indicator.component.spec.ts b/src/app/component/shared/indicator/indicator.component.spec.ts new file mode 100644 index 000000000..4e8725cc1 --- /dev/null +++ b/src/app/component/shared/indicator/indicator.component.spec.ts @@ -0,0 +1,23 @@ +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; +import { IndicatorComponent } from "./indicator.component"; + +describe("IndicatorComponent", () => { + let component: IndicatorComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [IndicatorComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(IndicatorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/component/shared/indicator/indicator.component.ts b/src/app/component/shared/indicator/indicator.component.ts new file mode 100644 index 000000000..2c011bad0 --- /dev/null +++ b/src/app/component/shared/indicator/indicator.component.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, Component, Input } from "@angular/core"; + +@Component({ + selector: "app-indicator", + templateUrl: "./indicator.component.html", + styleUrls: ["./indicator.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class IndicatorComponent { + @Input() status: Status = Status.Success; + public Status = Status; +} + +export enum Status { + Success, + Warning, + Error, +} diff --git a/src/app/component/shared/shared.components.ts b/src/app/component/shared/shared.components.ts index 97865c343..96fdd79e6 100644 --- a/src/app/component/shared/shared.components.ts +++ b/src/app/component/shared/shared.components.ts @@ -9,6 +9,8 @@ import { FormlyModule } from "@ngx-formly/core"; import { LoadingBarHttpClientModule } from "@ngx-loading-bar/http-client"; import { NgxDatatableModule } from "@swimlane/ngx-datatable"; import { ToastrModule } from "ngx-toastr"; +import { StepsModule } from "primeng/steps"; +import { TreeTableModule } from "primeng/treetable"; import { DirectivesModule } from "src/app/directives/directives.module"; import { ActionMenuComponent } from "./action-menu/action-menu.component"; import { CardsModule } from "./cards/cards.module"; @@ -25,6 +27,7 @@ import { FormlyHorizontalWrapper } from "./formly/horizontal-wrapper"; import { FormlyImageInput } from "./formly/image-input.component"; import { FormlyTimezoneInput } from "./formly/timezone-input.component"; import { HeaderModule } from "./header/header.module"; +import { IndicatorComponent } from "./indicator/indicator.component"; import { ItemsModule } from "./items/items.module"; import { LoadingComponent } from "./loading/loading.component"; import { MenuModule } from "./menu/menu.module"; @@ -43,6 +46,7 @@ export const sharedComponents = [ FormlyHorizontalWrapper, FormlyImageInput, FormlyTimezoneInput, + IndicatorComponent, LoadingComponent, SecondaryMenuComponent, WIPComponent, @@ -63,6 +67,8 @@ export const sharedModules = [ FormlyBootstrapModule, NgxDatatableModule, ToastrModule, + TreeTableModule, + StepsModule, DirectivesModule, CardsModule, diff --git a/src/app/helpers/tableTemplate/pagedTableTemplate.ts b/src/app/helpers/tableTemplate/pagedTableTemplate.ts index fd683cd82..012c84ccd 100644 --- a/src/app/helpers/tableTemplate/pagedTableTemplate.ts +++ b/src/app/helpers/tableTemplate/pagedTableTemplate.ts @@ -126,12 +126,12 @@ export abstract class PagedTableTemplate this.getPageData(); } - public getPageData() { + public getPageData(...args: AbstractModel[]) { this.loadingData = true; this.rows = []; this.api - .filter(this.filters) + .filter(this.filters, ...args) .pipe(takeUntil(this.unsubscribe)) .subscribe( (models) => {