Skip to content

Commit

Permalink
NAS-130469 / 24.10 / Various app fixes (#10407)
Browse files Browse the repository at this point in the history
* NAS-130469: Various app fixes

* NAS-130469: Various app fixes

* NAS-130469: Various app fixes
  • Loading branch information
undsoft authored Aug 7, 2024
1 parent 26b9c1a commit b843a33
Show file tree
Hide file tree
Showing 135 changed files with 561 additions and 718 deletions.
2 changes: 1 addition & 1 deletion src/app/interfaces/app-details-route-params.interface.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export interface AppDetailsRouteParams { appId: string; catalog: string; train: string }
export interface AppDetailsRouteParams { appId: string; train: string }
3 changes: 1 addition & 2 deletions src/app/modules/dialog/dialog.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { UntilDestroy } from '@ngneat/until-destroy';
import { TranslateService } from '@ngx-translate/core';
import { Observable, of } from 'rxjs';
import { JobProgressDialogRef } from 'app/classes/job-progress-dialog-ref.class';
import { ApiJobMethod, ApiJobResponse } from 'app/interfaces/api/api-job-directory.interface';
import {
ConfirmOptions,
ConfirmOptionsWithSecondaryCheckbox,
Expand Down Expand Up @@ -148,7 +147,7 @@ export class DialogService {
*
* If you need more control over JobProgressDialogComponent, use it directly.
*/
jobDialog<M extends ApiJobMethod, R extends ApiJobResponse<M>>(
jobDialog<R>(
job$: Observable<Job<R>>,
{ title, description, canMinimize }: {
title?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class JobsPanelComponent {

const title = job.description ? job.description : job.method;

this.dialog.jobDialog<ApiJobMethod, ApiJobResponse<ApiJobMethod>>(
this.dialog.jobDialog(
this.store$.select(selectJob(job.id)) as Observable<Job<ApiJobResponse<ApiJobMethod>>>,
{
title,
Expand Down
4 changes: 2 additions & 2 deletions src/app/pages/apps/apps-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const routes: Routes = [
component: InstalledAppsComponent,
},
{
path: ':catalog/:train/:appId',
path: ':train/:appId',
component: AppRouterOutletComponent,
data: { breadcrumb: null },
children: [
Expand Down Expand Up @@ -87,7 +87,7 @@ const routes: Routes = [
component: CategoryViewComponent,
},
{
path: ':catalog/:train/:appId',
path: ':train/:appId',
component: AppRouterOutletComponent,
resolve: { breadcrumb: appNameResolver },
children: [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatButtonHarness } from '@angular/material/button/testing';
import { Router } from '@angular/router';
import { createRoutingFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { MockComponents, MockModule } from 'ng-mocks';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { of } from 'rxjs';
import { mockCall, mockJob, mockWebSocket } from 'app/core/testing/utils/mock-websocket.utils';
import { AvailableApp } from 'app/interfaces/available-app.interface';
import { CatalogApp } from 'app/interfaces/catalog.interface';
import { App } from 'app/interfaces/chart-release.interface';
import { PageHeaderModule } from 'app/modules/page-header/page-header.module';
import { OrNotAvailablePipe } from 'app/modules/pipes/or-not-available/or-not-available.pipe';
import { AppCardLogoComponent } from 'app/pages/apps/components/app-card-logo/app-card-logo.component';
import {
AppAvailableInfoCardComponent,
} from 'app/pages/apps/components/app-detail-view/app-available-info-card/app-available-info-card.component';
import { AppDetailViewComponent } from 'app/pages/apps/components/app-detail-view/app-detail-view.component';
import {
AppDetailsHeaderComponent,
} from 'app/pages/apps/components/app-detail-view/app-details-header/app-details-header.component';
import {
AppDetailsSimilarComponent,
} from 'app/pages/apps/components/app-detail-view/app-details-similar/app-details-similar.component';
import {
AppResourcesCardComponent,
} from 'app/pages/apps/components/app-detail-view/app-resources-card/app-resources-card.component';
import { AppsStore } from 'app/pages/apps/store/apps-store.service';
import { DockerStore } from 'app/pages/apps/store/docker.service';
import { InstalledAppsStore } from 'app/pages/apps/store/installed-apps-store.service';
import { AuthService } from 'app/services/auth/auth.service';

const appsResponse = [{
name: 'webdav',
catalog: 'TRUENAS',
train: 'community',
description: 'webdav',
app_readme: '<h1>WebDAV</h1>\n<p> When application ...</p>',
last_update: { $date: 452 },
}] as AvailableApp[];

const existingCatalogApp = {
name: 'webdav',
versions: {
['1.0.9' as string]: {},
},
latest_version: '1.0.9',
} as CatalogApp;

describe('AppDetailViewComponent', () => {
let spectator: Spectator<AppDetailViewComponent>;
let loader: HarnessLoader;

const createComponent = createRoutingFactory({
component: AppDetailViewComponent,
imports: [
NgxSkeletonLoaderModule,
MockModule(PageHeaderModule),
OrNotAvailablePipe,
],
declarations: [
AppDetailsHeaderComponent,
MockComponents(
AppResourcesCardComponent,
AppAvailableInfoCardComponent,
AppCardLogoComponent,
AppDetailsSimilarComponent,
),
],
providers: [
InstalledAppsStore,
mockWebSocket([
mockJob('app.create'),
mockJob('app.update'),
mockCall('catalog.get_app_details', existingCatalogApp),
mockCall('app.query', [{} as App]),
mockCall('service.started', true),
]),
mockProvider(AuthService, {
user$: of({ attributes: { appsAgreement: true } }),
hasRole: () => of(true),
}),
mockProvider(AppsStore, {
availableApps$: of(appsResponse),
isLoading$: of(false),
}),
mockProvider(DockerStore, {
selectedPool$: of('pool'),
}),
],
params: { appId: 'webdav', catalog: 'TRUENAS', train: 'community' },
});

beforeEach(() => {
spectator = createComponent();
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

it('redirect to install app when Install button is pressed', async () => {
const saveButton = await loader.getHarness(MatButtonHarness.with({ text: 'Install' }));
await saveButton.click();
expect(spectator.inject(Router).navigate).toHaveBeenCalledWith(['/apps', 'available', 'community', 'webdav', 'install']);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Gallery, GalleryItem, ImageItem } from 'ng-gallery';
import {
map, filter, BehaviorSubject, tap, switchMap,
} from 'rxjs';
import { appImagePlaceholder, officialCatalog } from 'app/constants/catalog.constants';
import { appImagePlaceholder } from 'app/constants/catalog.constants';
import { AppDetailsRouteParams } from 'app/interfaces/app-details-route-params.interface';
import { AvailableApp } from 'app/interfaces/available-app.interface';
import { AppsStore } from 'app/pages/apps/store/apps-store.service';
Expand All @@ -24,12 +24,10 @@ export class AppDetailViewComponent implements OnInit {
app: AvailableApp;

appId: string;
catalog: string;
train: string;

isLoading$ = new BehaviorSubject<boolean>(true);
readonly imagePlaceholder = appImagePlaceholder;
readonly officialCatalog = officialCatalog;

items: GalleryItem[];

Expand All @@ -55,16 +53,15 @@ export class AppDetailViewComponent implements OnInit {
this.activatedRoute.params
.pipe(
filter((params) => {
return !!(params.appId as string) && !!(params.catalog as string) && !!(params.train as string);
return !!(params.appId as string) && !!(params.train as string);
}),
tap(() => {
this.isLoading$.next(true);
}),
untilDestroyed(this),
)
.subscribe(({ appId, catalog, train }: AppDetailsRouteParams) => {
.subscribe(({ appId, train }: AppDetailsRouteParams) => {
this.appId = appId;
this.catalog = catalog;
this.train = train;
this.loadAppInfo();
});
Expand All @@ -77,7 +74,7 @@ export class AppDetailViewComponent implements OnInit {
switchMap(() => {
return this.applicationsStore.availableApps$.pipe(
map((apps: AvailableApp[]) => apps.find(
(app) => app.name === this.appId && app.catalog === this.catalog && this.train === app.train,
(app) => app.name === this.appId && this.train === app.train,
)),
);
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { Router } from '@angular/router';
import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { MockComponent } from 'ng-mocks';
import { of } from 'rxjs';
import { officialCatalog } from 'app/constants/catalog.constants';
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils';
import { mockCall, mockWebSocket } from 'app/core/testing/utils/mock-websocket.utils';
import { AvailableApp } from 'app/interfaces/available-app.interface';
Expand All @@ -32,7 +31,6 @@ describe('AppDetailsHeaderComponent', () => {
icon_url: 'http://github.com/truenas/icon.png',
name: 'SETI@home',
latest_app_version: '1.0.0',
catalog: officialCatalog,
tags: ['aliens', 'ufo'],
train: 'stable',
home: 'https://www.seti.org',
Expand Down Expand Up @@ -132,7 +130,7 @@ describe('AppDetailsHeaderComponent', () => {
const installButton = await loader.getHarness(MatButtonHarness.with({ text: 'Install' }));
await installButton.click();

expect(spectator.inject(Router).navigate).toHaveBeenCalledWith(['/apps', 'available', 'TRUENAS', 'stable', 'SETI@home', 'install']);
expect(spectator.inject(Router).navigate).toHaveBeenCalledWith(['/apps', 'available', 'stable', 'SETI@home', 'install']);
});

it('shows Install Another Instance and installed badge when app is installed', async () => {
Expand All @@ -145,7 +143,7 @@ describe('AppDetailsHeaderComponent', () => {
expect(installButton).toExist();

await installButton.click();
expect(spectator.inject(Router).navigate).toHaveBeenCalledWith(['/apps', 'available', 'TRUENAS', 'stable', 'SETI@home', 'install']);
expect(spectator.inject(Router).navigate).toHaveBeenCalledWith(['/apps', 'available', 'stable', 'SETI@home', 'install']);

const installedBadge = spectator.query('.installed-badge');
expect(installedBadge).toExist();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export class AppDetailsHeaderComponent {
map((apps) => {
return apps.filter((app) => {
return app.metadata.name === this.app.name
&& app.catalog === this.app.catalog
&& app.catalog_train === this.app.train;
});
}),
Expand Down Expand Up @@ -91,7 +90,7 @@ export class AppDetailsHeaderComponent {
navigateToInstallPage(): void {
this.showAgreementWarning().pipe(untilDestroyed(this)).subscribe({
next: () => {
this.router.navigate(['/apps', 'available', this.app.catalog, this.app.train, this.app.name, 'install']);
this.router.navigate(['/apps', 'available', this.app.train, this.app.name, 'install']);
},
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
<ng-container>
<h2>{{ 'Similar Apps' | translate }}</h2>
<h2>{{ 'Similar Apps' | translate }}</h2>

@if (similarAppsLoading$ | async) {
<div fxLayout="row wrap" fxLayoutGap="16px">
<ngx-skeleton-loader class="similar-app-loader" fxFlex></ngx-skeleton-loader>
<ngx-skeleton-loader class="similar-app-loader" fxFlex></ngx-skeleton-loader>
</div>
} @else if (similarApps.length) {
<div class="similar-apps">
@for (app of similarApps; track trackByAppId($index, app)) {
<ix-app-card
[app]="app"
[routerLink]="['/apps', 'available', app.catalog, app.train, app.name]"
(keyup.enter)="router.navigate(['/apps', 'available', app.catalog, app.train, app.name])"
></ix-app-card>
}
</div>
} @else {
<h4 class="no-similar">{{ 'No similar apps found.' | translate }}</h4>
}

</ng-container>
@if (isLoading()) {
<div fxLayout="row wrap" fxLayoutGap="16px">
<ngx-skeleton-loader class="similar-app-loader" fxFlex></ngx-skeleton-loader>
<ngx-skeleton-loader class="similar-app-loader" fxFlex></ngx-skeleton-loader>
</div>
} @else if (loadingError()) {
<h4 class="no-similar">{{ 'Error when loading similar apps.' | translate }}</h4>
} @else if (similarApps().length) {
<div class="similar-apps">
@for (app of similarApps(); track trackByAppId($index, app)) {
<ix-app-card
[app]="app"
[routerLink]="['/apps', 'available', app.train, app.name]"
(keyup.enter)="router.navigate(['/apps', 'available', app.train, app.name])"
></ix-app-card>
}
</div>
} @else {
<h4 class="no-similar">{{ 'No similar apps found.' | translate }}</h4>
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('AppDetailsSimilarComponent', () => {
],
providers: [
mockProvider(ApplicationsService, {
getAppSimilarApps: jest.fn(() => of(similarApps)),
getSimilarApps: jest.fn(() => of(similarApps)),
}),
],
});
Expand All @@ -43,7 +43,7 @@ describe('AppDetailsSimilarComponent', () => {
});

it('loads and shows similar apps', () => {
expect(spectator.inject(ApplicationsService).getAppSimilarApps).toHaveBeenCalledWith(currentApp);
expect(spectator.inject(ApplicationsService).getSimilarApps).toHaveBeenCalledWith(currentApp);

const appCards = spectator.queryAll(AppCardComponent);
expect(appCards).toHaveLength(2);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {
ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges,
ChangeDetectionStrategy, Component, OnChanges,
input, signal,
} from '@angular/core';
import { Router } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { BehaviorSubject } from 'rxjs';
import { AvailableApp } from 'app/interfaces/available-app.interface';
import { ApplicationsService } from 'app/pages/apps/services/applications.service';

Expand All @@ -15,39 +15,39 @@ import { ApplicationsService } from 'app/pages/apps/services/applications.servic
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppDetailsSimilarComponent implements OnChanges {
@Input() app: AvailableApp;
readonly app = input.required< AvailableApp>();

protected similarAppsLoading$ = new BehaviorSubject<boolean>(false);
protected similarApps: AvailableApp[] = [];
protected isLoading = signal(false);
protected similarApps = signal<AvailableApp[]>([]);
protected loadingError = signal<unknown>(null);

private readonly maxSimilarApps = 6;

constructor(
protected router: Router,
private appService: ApplicationsService,
private cdr: ChangeDetectorRef,
) { }

ngOnChanges(): void {
this.loadSimilarApps();
}

private loadSimilarApps(): void {
this.similarAppsLoading$.next(true);
this.appService.getAppSimilarApps(this.app).pipe(untilDestroyed(this)).subscribe({
this.isLoading.set(true);
this.appService.getSimilarApps(this.app()).pipe(untilDestroyed(this)).subscribe({
next: (apps) => {
this.similarApps = apps.slice(0, this.maxSimilarApps);
this.similarAppsLoading$.next(false);
this.cdr.markForCheck();
this.isLoading.set(false);
this.similarApps.set(apps.slice(0, this.maxSimilarApps));
},
error: () => {
this.similarAppsLoading$.next(false);
this.cdr.markForCheck();
error: (error) => {
this.isLoading.set(false);
console.error(error);
this.loadingError.set(error);
},
});
}

trackByAppId(id: number, app: AvailableApp): string {
trackByAppId(_: number, app: AvailableApp): string {
return `${app.catalog}-${app.train}-${app.name}`;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { MockComponent } from 'ng-mocks';
import { of } from 'rxjs';
import { officialCatalog } from 'app/constants/catalog.constants';
import { AvailableApp } from 'app/interfaces/available-app.interface';
import { App } from 'app/interfaces/chart-release.interface';
import { AppCardLogoComponent } from 'app/pages/apps/components/app-card-logo/app-card-logo.component';
Expand Down Expand Up @@ -30,7 +29,6 @@ describe('AppCardComponent', () => {
icon_url: 'https://www.seti.org/logo.png',
description: 'Use your computer to help SETI@home search for extraterrestrial intelligence.',
latest_version: '1.0.0',
catalog: officialCatalog,
train: 'charts',
installed: true,
} as AvailableApp,
Expand Down
Loading

0 comments on commit b843a33

Please sign in to comment.