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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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.
+
+
+
+
+ Download Report
+
+
+
+
+
+
+
+
+
+
+ {{ stages[stage].previous.text }}
+
+
+ {{ stages[stage].next.text }}
+
+
+
+
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) => {