Skip to content

Commit

Permalink
NAS-129427: Implement App widget
Browse files Browse the repository at this point in the history
  • Loading branch information
denysbutenko committed Jul 4, 2024
1 parent f773ca9 commit c507062
Show file tree
Hide file tree
Showing 94 changed files with 262 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ describe('AppCardLogoComponent', () => {
});
});

it('shows default image', () => {
expect(spectator.query('img')).toHaveAttribute('src', 'assets/images/truenas_scale_ondark_favicon.png');
});

it('shows app logo', () => {
expect(spectator.query(LazyLoadImageDirective).lazyImage).toBe('https://www.seti.org/logo.png');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import {
signal,
} from '@angular/core';
import {
IntersectionObserverHooks,
LAZYLOAD_IMAGE_HOOKS, LazyLoadImageModule, StateChange,
LAZYLOAD_IMAGE_HOOKS, LazyLoadImageModule, ScrollHooks, StateChange,
} from 'ng-lazyload-image';
import { officialCatalog, appImagePlaceholder } from 'app/constants/catalog.constants';
import { appImagePlaceholder } from 'app/constants/catalog.constants';
import { LayoutService } from 'app/services/layout.service';

@Component({
Expand All @@ -17,16 +16,15 @@ import { LayoutService } from 'app/services/layout.service';
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgIf, LazyLoadImageModule],
providers: [{ provide: LAZYLOAD_IMAGE_HOOKS, useClass: IntersectionObserverHooks }],
providers: [{ provide: LAZYLOAD_IMAGE_HOOKS, useClass: ScrollHooks }],
})
export class AppCardLogoComponent {
readonly url = input<string>();
protected wasLogoLoaded = signal(false);

layoutService = inject(LayoutService);
private layoutService = inject(LayoutService);

readonly scrollTarget = this.layoutService.getContentContainer();
readonly officialCatalog = officialCatalog;
readonly appImagePlaceholder = appImagePlaceholder;

onLogoLoaded(event: StateChange): void {
Expand Down
8 changes: 4 additions & 4 deletions src/app/pages/apps/services/applications.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
import { ixChartApp } from 'app/constants/catalog.constants';
import { AppExtraCategory } from 'app/enums/app-extra-category.enum';
import { ChartReleaseStatus } from 'app/enums/chart-release-status.enum';
import { JobState } from 'app/enums/job-state.enum';
import { ServiceName } from 'app/enums/service-name.enum';
import { observeJob } from 'app/helpers/operators/observe-job.operator';
import { ApiEvent } from 'app/interfaces/api-message.interface';
import { UpgradeSummary } from 'app/interfaces/application.interface';
import { AppsFiltersValues } from 'app/interfaces/apps-filters-values.interface';
Expand Down Expand Up @@ -151,11 +151,11 @@ export class ApplicationsService {
switch (app.status) {
case ChartReleaseStatus.Active:
return this.stopApplication(app.name).pipe(
observeJob(),
switchMap(() => this.startApplication(app.name).pipe(observeJob())),
filter((job) => job.state === JobState.Success),
switchMap(() => this.startApplication(app.name)),
);
case ChartReleaseStatus.Stopped:
return this.startApplication(app.name).pipe(observeJob());
return this.startApplication(app.name).pipe();
case ChartReleaseStatus.Deploying:
default:
return EMPTY;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ <h3>{{ 'App' | translate }}</h3>
matTooltipPosition="above"
[attr.aria-label]="'Restart App' | translate"
[matTooltip]="'Restart App' | translate"
[disabled]="appRestarting()"
(click)="onRestartApp(app)"
>
<ix-icon name="mdi-restart"></ix-icon>
Expand All @@ -42,7 +43,7 @@ <h3>{{ 'App' | translate }}</h3>

<div [class]="['container', size()]">
<div class="app-header-row">
<ix-app-card-logo [url]="(application() | async).value?.chart_metadata?.icon"></ix-app-card-logo>
<ix-app-card-logo [url]="(application() | async)?.value?.chart_metadata?.icon"></ix-app-card-logo>

<div class="app-info">
<div class="app-header">
Expand Down Expand Up @@ -72,7 +73,7 @@ <h3 *ixWithLoadingState="application() as app" class="name">
<div class="app-stats">
<div class="app-stats-row">
<div class="slot square">
<div>
<div class="cpu-usage">
<h3 *ixWithLoadingState="stats() as stats">
<span>{{ stats.cpu.toFixed(0) }}</span>
<small>%</small>
Expand Down Expand Up @@ -106,7 +107,7 @@ <h4>{{ 'Network I/O' | translate }}</h4>
</div>
<div class="app-stats-row">
<div class="slot square">
<div>
<div class="memory-usage">
<h3>
<ng-container *ixWithLoadingState="stats() as stats">
<ng-container *appLet="splitMemory(stats.memory | ixFileSize) as memory">
Expand All @@ -122,6 +123,7 @@ <h3>
</div>
<div class="slot rectangle">
<div class="chart-info">
<!-- TODO: Replace network data with disk data when it is available -->
<h4>{{ 'Disk I/O' | translate }}</h4>
<div class="in-out">
<div class="in-out-row">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { MockComponent } from 'ng-mocks';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Observable, of } from 'rxjs';
import { ChartReleaseStatus } from 'app/enums/chart-release-status.enum';
import { ApiEvent } from 'app/interfaces/api-message.interface';
import { ChartScaleResult, ChartScaleQueryParams } from 'app/interfaces/chart-release-event.interface';
import { ChartRelease } from 'app/interfaces/chart-release.interface';
import { Job } from 'app/interfaces/job.interface';
import { DialogService } from 'app/modules/dialog/dialog.service';
import { IxIconHarness } from 'app/modules/ix-icon/ix-icon.harness';
import { IxFileSizePipe } from 'app/modules/pipes/ix-file-size/ix-file-size.pipe';
import { MapValuePipe } from 'app/modules/pipes/map-value/map-value.pipe';
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service';
import { AppCardLogoComponent } from 'app/pages/apps/components/app-card-logo/app-card-logo.component';
import { AppStatusCellComponent } from 'app/pages/apps/components/installed-apps/app-status-cell/app-status-cell.component';
import { AppUpdateCellComponent } from 'app/pages/apps/components/installed-apps/app-update-cell/app-update-cell.component';
Expand All @@ -23,6 +28,7 @@ import { WidgetAppComponent } from './widget-app.component';

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

const app = {
id: 'testapp',
Expand All @@ -34,7 +40,7 @@ describe('WidgetAppComponent', () => {
update_available: true,
container_images_update_available: false,
chart_metadata: {
icon: '',
icon: 'http://localhost/test-app.png',
appVersion: '1.0',
},
catalog: 'truenas',
Expand All @@ -43,7 +49,7 @@ describe('WidgetAppComponent', () => {

const createComponent = createComponentFactory({
component: WidgetAppComponent,
imports: [MapValuePipe, IxFileSizePipe],
imports: [MapValuePipe, IxFileSizePipe, NgxSkeletonLoaderModule],
declarations: [
MockComponent(AppStatusCellComponent),
MockComponent(AppUpdateCellComponent),
Expand All @@ -64,7 +70,9 @@ describe('WidgetAppComponent', () => {
mockProvider(ThemeService, {
currentTheme: () => ({ blue: '#0000FF', orange: '#FFA500' }),
}),
mockProvider(RedirectService),
mockProvider(RedirectService, {
openWindow: jest.fn(),
}),
mockProvider(ApplicationsService, {
restartApplication: jest.fn(() => of(true)),
getInstalledAppsStatusUpdates: jest.fn(() => {
Expand All @@ -76,6 +84,9 @@ describe('WidgetAppComponent', () => {
afterClosed: () => of(true),
})),
}),
mockProvider(SnackbarService, {
success: jest.fn(),
}),
],
});

Expand All @@ -86,27 +97,58 @@ describe('WidgetAppComponent', () => {
settings: { appName: app.name },
},
});
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

it('should generate correct chart data', () => {
const chartData = spectator.component.networkStats();
it('checks status rows', () => {
expect(spectator.query('.app-header .name')).toHaveText('TestApp');
expect(spectator.query(AppStatusCellComponent)).toBeTruthy();
expect(spectator.query(AppUpdateCellComponent)).toBeTruthy();
});

expect(chartData).toHaveLength(60);
expect(chartData[chartData.length - 1]).toEqual([100, 200]);
expect(chartData[chartData.length - 2]).toEqual([0, 0]);
expect(chartData[chartData.length - 3]).toEqual([0, 0]);
it('should split memory correctly', () => {
const result = spectator.component.splitMemory('512 MiB');
expect(result).toEqual([512, 'MiB']);
});

it('should open web portal', () => {
it('checks open web portal', async () => {
const redirectSpy = jest.spyOn(spectator.inject(RedirectService), 'openWindow');

spectator.component.openWebPortal(app);
const portalButton = await loader.getHarness(IxIconHarness.with({ name: 'mdi-web' }));
await portalButton.click();

expect(redirectSpy).toHaveBeenCalledWith('http://test.com');
});

it('should split memory correctly', () => {
const result = spectator.component.splitMemory('512 MiB');
expect(result).toEqual([512, 'MiB']);
it('checks restart app', async () => {
const restartSpy = jest.spyOn(spectator.inject(ApplicationsService), 'restartApplication');
const snackbarSpy = jest.spyOn(spectator.inject(SnackbarService), 'success');

const restartButton = await loader.getHarness(IxIconHarness.with({ name: 'mdi-restart' }));
await restartButton.click();

expect(snackbarSpy).toHaveBeenCalledWith('App is restarting');
expect(restartSpy).toHaveBeenCalledWith(app);
});

it('checks cpu usage', () => {
expect(spectator.query('.cpu-usage h3')).toHaveText('55%');
expect(spectator.query('.cpu-usage strong')).toHaveText('CPU Usage');
});

it('checks memory usage', () => {
expect(spectator.query('.memory-usage h3')).toHaveText('1KiB');
expect(spectator.query('.memory-usage strong')).toHaveText('Memory Usage');
});

it('should generate correct network chart data', () => {
const chartData = spectator.component.networkStats();

expect(chartData).toHaveLength(60);
expect(chartData[chartData.length - 1]).toEqual([100, 200]);
expect(chartData[chartData.length - 2]).toEqual([0, 0]);
expect(chartData[chartData.length - 3]).toEqual([0, 0]);
});

// TODO: Add tests for disk chart data
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@ import {
} from 'rxjs';
import { toLoadingState } from 'app/helpers/operators/to-loading-state.helper';
import { ChartRelease, ChartReleaseStats } from 'app/interfaces/chart-release.interface';
import { DialogService } from 'app/modules/dialog/dialog.service';
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service';
import { ApplicationsService } from 'app/pages/apps/services/applications.service';
import { WidgetResourcesService } from 'app/pages/dashboard/services/widget-resources.service';
import { WidgetComponent } from 'app/pages/dashboard/types/widget-component.interface';
import { SlotSize } from 'app/pages/dashboard/types/widget.interface';
import { WidgetAppSettings } from 'app/pages/dashboard/widgets/apps/widget-app/widget-app.definition';
import { ErrorHandlerService } from 'app/services/error-handler.service';
import { RedirectService } from 'app/services/redirect.service';
import { ThemeService } from 'app/services/theme/theme.service';

Expand Down Expand Up @@ -58,8 +57,9 @@ export class WidgetAppComponent implements WidgetComponent<WidgetAppSettings> {
shareReplay({ bufferSize: 1, refCount: true }),
);
});
appRestarting = signal<boolean>(false);

initialNetworkStats = Array.from({ length: 60 }, () => ([0, 0]));
protected readonly initialNetworkStats = Array.from({ length: 60 }, () => ([0, 0]));
cachedNetworkStats = signal<number[][]>([]);

networkStats = computed(() => {
Expand Down Expand Up @@ -104,18 +104,19 @@ export class WidgetAppComponent implements WidgetComponent<WidgetAppSettings> {
private translate: TranslateService,
private redirect: RedirectService,
private appService: ApplicationsService,
private dialogService: DialogService,
private errorHandler: ErrorHandlerService,
private snackbar: SnackbarService,
) {}

onRestartApp(app: ChartRelease): void {
this.dialogService.jobDialog(
this.appService.restartApplication(app),
{ title: app.name, canMinimize: true },
)
.afterClosed()
.pipe(this.errorHandler.catchError(), untilDestroyed(this))
.subscribe();
this.appRestarting.set(true);
this.snackbar.success(this.translate.instant('App is restarting'));

this.appService.restartApplication(app)
.pipe(untilDestroyed(this))
.subscribe(() => {
this.appRestarting.set(false);
this.snackbar.success(this.translate.instant('App is restarted'));
});
}

openWebPortal(app: ChartRelease): void {
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/af.json
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@
"App": "",
"App Name": "",
"App Version": "",
"App is restarted": "",
"App is restarting": "",
"Appdefaults Auxiliary Parameters": "",
"Append <i>@realm</i> to <i>cn</i> in LDAP queries for both groups and users when User CN is set).": "",
"Append Data": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@
"App": "",
"App Name": "",
"App Version": "",
"App is restarted": "",
"App is restarting": "",
"Appdefaults Auxiliary Parameters": "",
"Append <i>@realm</i> to <i>cn</i> in LDAP queries for both groups and users when User CN is set).": "",
"Append Data": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/ast.json
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@
"App": "",
"App Name": "",
"App Version": "",
"App is restarted": "",
"App is restarting": "",
"Appdefaults Auxiliary Parameters": "",
"Append <i>@realm</i> to <i>cn</i> in LDAP queries for both groups and users when User CN is set).": "",
"Append Data": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/az.json
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@
"App": "",
"App Name": "",
"App Version": "",
"App is restarted": "",
"App is restarting": "",
"Appdefaults Auxiliary Parameters": "",
"Append <i>@realm</i> to <i>cn</i> in LDAP queries for both groups and users when User CN is set).": "",
"Append Data": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/be.json
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@
"App": "",
"App Name": "",
"App Version": "",
"App is restarted": "",
"App is restarting": "",
"Appdefaults Auxiliary Parameters": "",
"Append <i>@realm</i> to <i>cn</i> in LDAP queries for both groups and users when User CN is set).": "",
"Append Data": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/bg.json
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@
"App": "",
"App Name": "",
"App Version": "",
"App is restarted": "",
"App is restarting": "",
"Appdefaults Auxiliary Parameters": "",
"Append <i>@realm</i> to <i>cn</i> in LDAP queries for both groups and users when User CN is set).": "",
"Append Data": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/bn.json
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@
"App": "",
"App Name": "",
"App Version": "",
"App is restarted": "",
"App is restarting": "",
"Appdefaults Auxiliary Parameters": "",
"Append <i>@realm</i> to <i>cn</i> in LDAP queries for both groups and users when User CN is set).": "",
"Append Data": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/br.json
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@
"App": "",
"App Name": "",
"App Version": "",
"App is restarted": "",
"App is restarting": "",
"Appdefaults Auxiliary Parameters": "",
"Append <i>@realm</i> to <i>cn</i> in LDAP queries for both groups and users when User CN is set).": "",
"Append Data": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/bs.json
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@
"App": "",
"App Name": "",
"App Version": "",
"App is restarted": "",
"App is restarting": "",
"Appdefaults Auxiliary Parameters": "",
"Append <i>@realm</i> to <i>cn</i> in LDAP queries for both groups and users when User CN is set).": "",
"Append Data": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@
"App": "",
"App Name": "",
"App Version": "",
"App is restarted": "",
"App is restarting": "",
"Appdefaults Auxiliary Parameters": "",
"Append <i>@realm</i> to <i>cn</i> in LDAP queries for both groups and users when User CN is set).": "",
"Append Data": "",
Expand Down
Loading

0 comments on commit c507062

Please sign in to comment.