Skip to content

Commit

Permalink
Added page to admin dashboard to view all uploads
Browse files Browse the repository at this point in the history
fixes: #2051
  • Loading branch information
hudson-newey committed Nov 8, 2023
1 parent fe80ba4 commit 56dd61b
Show file tree
Hide file tree
Showing 11 changed files with 289 additions and 36 deletions.
10 changes: 10 additions & 0 deletions src/app/components/admin/admin.menus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,14 @@ export const adminThemeMenuItem = menuRoute({
route: adminRoute.add("theme"),
tooltip: () => "View and experiment with website theme",
parent: adminDashboardMenuItem,
predicate: isAdminPredicate,
});

export const adminUploadsMenuItem = menuRoute({
icon: ["fas", "cloud"],
label: "All Recording Uploads",
route: adminRoute.add("all_uploads"),
tooltip: () => "View all uploads",
parent: adminDashboardMenuItem,
predicate: isAdminPredicate,
});
2 changes: 2 additions & 0 deletions src/app/components/admin/admin.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { TagGroupsModule } from "./tag-group/tag-groups.module";
import { TagsModule } from "./tags/tags.module";
import { AdminThemeTemplateComponent } from "./theme-template/theme-template.component";
import { AdminUserListComponent } from "./users/user.component";
import { AllUploadsComponent } from "./all-uploads/all-uploads.component";

const modules = [
AnalysisJobsModule,
Expand All @@ -25,6 +26,7 @@ const components = [
AdminDashboardComponent,
AdminUserListComponent,
AdminThemeTemplateComponent,
AllUploadsComponent,
];
const routes = adminRoute.compileRoutes(getRouteConfigForPage);

Expand Down
112 changes: 112 additions & 0 deletions src/app/components/admin/all-uploads/all-uploads.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { assertPageInfo } from "@test/helpers/pageRoute";
import { Spectator, SpyObject, createRoutingFactory } from "@ngneat/spectator";
import { MockBawApiModule } from "@baw-api/baw-apiMock.module";
import { SharedModule } from "@shared/shared.module";
import { ToastrService } from "ngx-toastr";
import { ConfirmationComponent } from "@components/harvest/components/modal/confirmation.component";
import { LoadingComponent } from "@shared/loading/loading.component";
import { UserLinkComponent } from "@shared/user-link/user-link/user-link.component";
import { Injector } from "@angular/core";
import { SHALLOW_HARVEST } from "@baw-api/ServiceTokens";
import { Harvest } from "@models/Harvest";
import { Project } from "@models/Project";
import { generateProject } from "@test/fakes/Project";
import { of } from "rxjs";
import { User } from "@models/User";
import { generateUser } from "@test/fakes/User";
import { ShallowHarvestsService } from "@baw-api/harvest/harvest.service";
import { generateHarvest } from "@test/fakes/Harvest";
import { AllUploadsComponent } from "./all-uploads.component";

// the functionality that the project names are shown in the harvest list
// and the correct api calls are made is asserted in the harvest list component
// these tests assert that the harvest list component is extended correctly
describe("AllUploadsComponent", () => {
let spectator: Spectator<AllUploadsComponent>;
let fakeHarvest: Harvest;
let fakeHarvestApi: SpyObject<ShallowHarvestsService>;

const createComponent = createRoutingFactory({
declarations: [LoadingComponent, ConfirmationComponent, UserLinkComponent],
component: AllUploadsComponent,
imports: [MockBawApiModule, SharedModule],
mocks: [ToastrService],
});

function setup(): void {
fakeHarvest = new Harvest(generateHarvest({ status: "uploading" }));

spectator = createComponent({ detectChanges: false });

const injector = spectator.inject(Injector);
fakeHarvest["injector"] = injector;

fakeHarvestApi = spectator.inject(SHALLOW_HARVEST.token);
fakeHarvest.addMetadata({
paging: { items: 1, page: 0, total: 1, maxPage: 5 },
});

// since the harvest creator is a resolved model, we need to mock the creator property
const fakeUser: User = new User(generateUser());
spyOnProperty(fakeHarvest, "creator").and.callFake(() => fakeUser);

const mockHarvestProject: Project = new Project(generateProject());
spyOnProperty(fakeHarvest, "project").and.callFake(
() => mockHarvestProject
);

// mock the harvest service filter API to populate the
// list component ngx-datatable
const mockResponse = of([fakeHarvest]);
fakeHarvestApi.filter.and.callFake(() => mockResponse);
fakeHarvestApi.transitionStatus.and.callFake(() => of(fakeHarvest));

spectator.detectChanges();
}

beforeEach(() => setup());

assertPageInfo(AllUploadsComponent, ["Recording Uploads", "All Recording Uploads"]);

it("should create", () => {
expect(spectator.component).toBeInstanceOf(AllUploadsComponent);
});

it("should return 'null' for the project", () => {
expect(spectator.component.project).toBeNull();
});

it("should have the harvest list table", () => {
const datatableElement: HTMLElement =
spectator.query<HTMLElement>("ngx-datatable");
expect(datatableElement).toExist();
});

it("should make the correct api calls", () => {
expect(fakeHarvestApi.transitionStatus).not.toHaveBeenCalled();

// test that the filter object that the filter service was called with did not contain a filter key
expect(fakeHarvestApi.filter).not.toHaveBeenCalledWith(
jasmine.objectContaining({
filter: jasmine.any(Object),
})
);

// assert that projection conditions were still applied to the request body
expect(fakeHarvestApi.filter).toHaveBeenCalledWith(
jasmine.objectContaining({
projection: {
include: [
"id",
"projectId",
"name",
"createdAt",
"creatorId",
"streaming",
"status",
],
},
}),
);
});
});
34 changes: 34 additions & 0 deletions src/app/components/admin/all-uploads/all-uploads.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Component } from "@angular/core";
import { List } from "immutable";
import { ListComponent } from "@components/harvest/pages/list/list.component";
import { ActivatedRoute } from "@angular/router";
import { ShallowHarvestsService } from "@baw-api/harvest/harvest.service";
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
import { adminCategory, adminUploadsMenuItem } from "../admin.menus";
import { adminMenuItemActions } from "../dashboard/dashboard.component";

@Component({
selector: "baw-all-uploads",
templateUrl: "../../harvest/pages/list/list.component.html",
})
class AllUploadsComponent extends ListComponent {
public constructor(
private modal: NgbModal,
private api: ShallowHarvestsService,
private activatedRoute: ActivatedRoute
) {
super(modal, api, activatedRoute);
}

public override get project() {
return null;
}
}

AllUploadsComponent.linkToRoute({
category: adminCategory,
pageRoute: adminUploadsMenuItem,
menus: { actions: List(adminMenuItemActions) },
});

export { AllUploadsComponent };
2 changes: 2 additions & 0 deletions src/app/components/admin/dashboard/dashboard.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
adminDashboardMenuItem,
adminJobStatusMenuItem,
adminThemeMenuItem,
adminUploadsMenuItem,
adminUserListMenuItem,
} from "../admin.menus";
import { adminOrphansMenuItem } from "../orphan/orphans.menus";
Expand All @@ -27,6 +28,7 @@ export const adminMenuItemActions = [
adminTagsMenuItem,
adminThemeMenuItem,
adminUserListMenuItem,
adminUploadsMenuItem,
];

@Component({
Expand Down
4 changes: 0 additions & 4 deletions src/app/components/harvest/harvest.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { FileRowComponent } from "./components/metadata-review/file-row.componen
import { FolderRowComponent } from "./components/metadata-review/folder-row.component";
import { LoadMoreComponent } from "./components/metadata-review/load-more.component";
import { WhitespaceComponent } from "./components/metadata-review/whitespace.component";
import { ConfirmationComponent } from "./components/modal/confirmation.component";
import { CanCloseDialogComponent } from "./components/shared/can-close-dialog.component";
import { EtaComponent } from "./components/shared/eta.component";
import { StatisticGroupComponent } from "./components/shared/statistics/group.component";
Expand Down Expand Up @@ -50,9 +49,6 @@ const internalComponents = [
TitleComponent,
UploadUrlComponent,

// Modals
ConfirmationComponent,

// Widgets
ValidationsWidgetComponent,

Expand Down
44 changes: 39 additions & 5 deletions src/app/components/harvest/pages/list/list.component.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
<h1>Upload History</h1>
<h1>
<ng-container *ngIf="project === null; else showProject">
All Uploads
</ng-container>

<p *ngIf="!canCreateHarvestCapability" class="alert alert-info">
<ng-template #showProject>
Upload History
</ng-template>
</h1>

<!--
If canCreateHarvestCapability is false, the project does not allow uploading audio
However, if there is no project, the canCreateHarvestCapability will be undefined
-->
<p *ngIf="canCreateHarvestCapability === false" class="alert alert-info">
This project does not allow uploading audio,
<a [strongRoute]="contactUs.route">{{ contactUs.label }}</a> to request
permission to upload audio.
Expand Down Expand Up @@ -37,6 +49,28 @@ <h1>Upload History</h1>
</ng-template>
</ngx-datatable-column>

<!-- If the list is not scoped to a project, list all projects with names -->
<ngx-datatable-column *ngIf="project === null" prop="">
<ng-template let-column="column" ngx-datatable-header-template>
Project
</ng-template>
<ng-template let-value="value" ngx-datatable-cell-template>
<!-- Show loading animation while project is unresolved -->
<!-- Without this, ngx-datatable will not trigger change detection when the model is resolved -->
<baw-loading
*ngIf="value.project | isUnresolved; else showProject"
size="sm"
></baw-loading>

<!-- Create project link when site is loaded-->
<ng-template #showProject>
<a [bawUrl]="value.project.viewUrl">
{{ value.project.name }}
</a>
</ng-template>
</ng-template>
</ngx-datatable-column>

<ngx-datatable-column prop="streaming">
<ng-template let-column="column" ngx-datatable-header-template>
Upload Type
Expand All @@ -52,18 +86,18 @@ <h1>Upload History</h1>
</ng-template>
</ngx-datatable-column>

<ngx-datatable-column prop="action">
<ngx-datatable-column prop="action" [sortable]="false">
<ng-template let-column="column" ngx-datatable-header-template>
Action
</ng-template>
<ng-template let-row="row" let-value="value" ngx-datatable-cell-template>
<a class="btn btn-sm btn-primary" [bawUrl]="row.viewUrl">
<a class="btn btn-sm btn-primary me-1" [bawUrl]="row.viewUrl">
{{ asHarvest(row).status !== "complete" ? "Continue" : "View" }}
</a>
<a
name="list-abort-button"
*ngIf="asHarvest(row).isAbortable()"
class="btn btn-sm btn-outline-danger ms-1"
class="btn btn-sm btn-outline-danger"
(click)="abortUpload(abortUploadModal, asHarvest(row))"
>
Abort
Expand Down
64 changes: 58 additions & 6 deletions src/app/components/harvest/pages/list/list.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
tick,
} from "@angular/core/testing";
import { MockBawApiModule } from "@baw-api/baw-apiMock.module";
import { HARVEST } from "@baw-api/ServiceTokens";
import { SHALLOW_HARVEST } from "@baw-api/ServiceTokens";
import { ConfirmationComponent } from "@components/harvest/components/modal/confirmation.component";
import { Harvest } from "@models/Harvest";
import { Project } from "@models/Project";
Expand Down Expand Up @@ -39,7 +39,7 @@ describe("ListComponent", () => {
mocks: [ToastrService],
});

function setup(project: Project, mockHarvest: Harvest) {
function setup(project: Project | null, mockHarvest: Harvest) {
spec = createComponent({
detectChanges: false,
data: {
Expand All @@ -48,17 +48,26 @@ describe("ListComponent", () => {
});

const injector = spec.inject(Injector);
project["injector"] = injector;

if (project) {
project["injector"] = injector;
}

mockHarvest["injector"] = injector;

const mockHarvestApi = spec.inject(HARVEST.token);
spyOnProperty(spec.component, "project").and.callFake(() => project);

const mockHarvestApi = spec.inject(SHALLOW_HARVEST.token);
mockHarvest.addMetadata({
paging: { items: 1, page: 0, total: 1, maxPage: 5 },
});

// since the harvest creator is a resolved model, we need to mock the creator property
spyOnProperty(mockHarvest, "creator").and.callFake(() => defaultUser);

const mockHarvestProject: Project = project ? project : new Project(generateProject());
spyOnProperty(mockHarvest, "project").and.callFake(() => mockHarvestProject);

// inject the NgbModal service so that we can
// dismiss all modals at the end of every test
modalService = spec.inject(NgbModal);
Expand Down Expand Up @@ -166,7 +175,10 @@ describe("ListComponent", () => {
getModalNextButton().click();
tick();

expect(harvestApi.transitionStatus).toHaveBeenCalledWith(defaultHarvest, "complete");
expect(harvestApi.transitionStatus).toHaveBeenCalledWith(
defaultHarvest,
"complete"
);
discardPeriodicTasks();
flush();
}));
Expand Down Expand Up @@ -229,7 +241,6 @@ describe("ListComponent", () => {
],
},
}),
defaultProject
);
});

Expand All @@ -239,4 +250,45 @@ describe("ListComponent", () => {
const harvestApi = setup(defaultProject, defaultHarvest);
expect(harvestApi.filter).toHaveBeenCalledTimes(1);
});

it("should make the correct api calls for a harvest list scoped to a project", () => {
const harvestApi = setup(defaultProject, defaultHarvest);
expect(harvestApi.filter).toHaveBeenCalledWith(
jasmine.objectContaining({
filter: {
projectId: {
eq: defaultProject.id,
},
},
}),
);
});

it("should make the correct api calls for an unscoped harvest list", () => {
// to unscope the harvest list, we return `null` from the project getter
const harvestApi = setup(null, defaultHarvest);
expect(harvestApi.filter).not.toHaveBeenCalledWith(
jasmine.objectContaining({
filter: jasmine.any(Object),
})
);
});

it("should not display the harvest project name in the project column if the harvest list is scoped to a project", () => {
setup(defaultProject, defaultHarvest);
const projectColumnHeader =
getElementByInnerText<HTMLTableCellElement>("Project");
expect(projectColumnHeader).not.toExist();
});

it("should display the harvest project name in the project column if the harvest list is not scoped to a project", () => {
setup(null, defaultHarvest);

const expectedProject: Project = defaultHarvest.project;
const expectedProjectName: string = expectedProject.name;

const projectNameColumnValue: HTMLTableCellElement = getElementByInnerText(expectedProjectName);

expect(projectNameColumnValue).toExist();
});
});
Loading

0 comments on commit 56dd61b

Please sign in to comment.