Skip to content

Commit

Permalink
NAS-131073 / 25.04 / Add apps widgets back (#10687)
Browse files Browse the repository at this point in the history
* NAS-131073: Add apps widgets back

* NAS-131073: Add apps widgets back

* NAS-131073: Add app-version helper
  • Loading branch information
denysbutenko authored Sep 19, 2024
1 parent db283e7 commit 4caf239
Show file tree
Hide file tree
Showing 23 changed files with 153 additions and 48 deletions.
2 changes: 2 additions & 0 deletions src/app/pages/apps/apps.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import { LogsDetailsDialogComponent } from 'app/pages/apps/components/logs-detai
import { SelectPoolDialogComponent } from 'app/pages/apps/components/select-pool-dialog/select-pool-dialog.component';
import { ShellDetailsDialogComponent } from 'app/pages/apps/components/shell-details-dialog/shell-details-dialog.component';
import { CustomFormsModule } from 'app/pages/apps/modules/custom-forms/custom-forms.module';
import { AppVersionPipe } from 'app/pages/dashboard/widgets/apps/common/utils/app-version.pipe';
import { AppCardLogoComponent } from './components/app-card-logo/app-card-logo.component';
import { AppAvailableInfoCardComponent } from './components/app-detail-view/app-available-info-card/app-available-info-card.component';
import { AppDetailViewComponent } from './components/app-detail-view/app-detail-view.component';
Expand Down Expand Up @@ -213,6 +214,7 @@ import { InstalledAppsComponent } from './components/installed-apps/installed-ap
UiSearchDirective,
RequiresRolesDirective,
LetDirective,
AppVersionPipe,
],
})
export class AppsModule { }
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ <h3 mat-card-title>
<div class="label">{{ 'App Version' | translate }}:</div>
<div class="value">
@if (!isCustomApp()) {
{{ app()?.metadata?.app_version | orNotAvailable }}
{{ app()?.metadata?.app_version | appVersion | orNotAvailable }}
} @else if (app().human_version) {
<!-- Show docker image tag as version for custom apps -->
{{ app().human_version?.split(':')?.[1]?.split('_')?.[0] }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { AppRollbackModalComponent } from 'app/pages/apps/components/installed-a
import { AppUpgradeDialogComponent } from 'app/pages/apps/components/installed-apps/app-upgrade-dialog/app-upgrade-dialog.component';
import { ApplicationsService } from 'app/pages/apps/services/applications.service';
import { InstalledAppsStore } from 'app/pages/apps/store/installed-apps-store.service';
import { AppVersionPipe } from 'app/pages/dashboard/widgets/apps/common/utils/app-version.pipe';
import { RedirectService } from 'app/services/redirect.service';
import { WebSocketService } from 'app/services/ws.service';

Expand Down Expand Up @@ -70,6 +71,7 @@ describe('AppInfoCardComponent', () => {
imports: [
CleanLinkPipe,
OrNotAvailablePipe,
AppVersionPipe,
],
declarations: [
MockComponents(
Expand Down Expand Up @@ -133,7 +135,7 @@ describe('AppInfoCardComponent', () => {
},
{
label: 'App Version:',
value: '3.2.1',
value: 'v3.2.1',
},
{
label: 'Source:',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
matTooltipPosition="above"
[name]="hasUpdate() ? 'mdi-alert-circle' : 'mdi-check-circle'"
[matTooltip]="hasUpdate()
? ('{version} is available!' | translate: { version: app().metadata.app_version })
? ('{version} is available!' | translate: { version: app().metadata.app_version | appVersion })
: ('Up to date' | translate)
"
></ix-icon>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
import { App } from 'app/interfaces/app.interface';
import { MapValuePipe } from 'app/modules/pipes/map-value/map-value.pipe';
import { AppUpdateCellComponent } from 'app/pages/apps/components/installed-apps/app-update-cell/app-update-cell.component';
import { AppVersionPipe } from 'app/pages/dashboard/widgets/apps/common/utils/app-version.pipe';

describe('AppUpdateCellComponent', () => {
let spectator: SpectatorHost<AppUpdateCellComponent>;
Expand All @@ -10,6 +11,7 @@ describe('AppUpdateCellComponent', () => {
component: AppUpdateCellComponent,
imports: [
MapValuePipe,
AppVersionPipe,
],
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,22 @@ import { TranslateModule } from '@ngx-translate/core';
import { App } from 'app/interfaces/app.interface';
import { IxIconModule } from 'app/modules/ix-icon/ix-icon.module';
import { MapValuePipe } from 'app/modules/pipes/map-value/map-value.pipe';
import { AppVersionPipe } from 'app/pages/dashboard/widgets/apps/common/utils/app-version.pipe';

@Component({
selector: 'ix-app-update-cell',
templateUrl: './app-update-cell.component.html',
styleUrls: ['./app-update-cell.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TranslateModule, MapValuePipe, MatTooltipModule, IxIconModule],
imports: [TranslateModule, MapValuePipe, MatTooltipModule, IxIconModule, AppVersionPipe],
})
export class AppUpdateCellComponent {
app = input.required<App>();
showIcon = input<boolean>(false);
hasUpdate = computed(() => this.app()?.upgrade_available);

@HostBinding('class') get hostClasses(): string[] {
return ['update', this.showIcon() ? 'has-icon' : 'has-cell'];
}

hasUpdate = computed(() => {
const app = this.app();

return app.upgrade_available;
});
}
3 changes: 2 additions & 1 deletion src/app/pages/apps/services/applications.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,9 @@ export class ApplicationsService {
filter((job) => job.state === JobState.Success),
switchMap(() => this.startApplication(app.name)),
);
case AppState.Crashed:
case AppState.Stopped:
return this.startApplication(app.name).pipe();
return this.startApplication(app.name);
case AppState.Deploying:
default:
return EMPTY;
Expand Down
21 changes: 20 additions & 1 deletion src/app/pages/dashboard/dashboard.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
AsyncPipe, NgClass, NgComponentOutlet, NgTemplateOutlet, PercentPipe, TitleCasePipe,
AsyncPipe, KeyValuePipe, NgClass, NgComponentOutlet, NgTemplateOutlet, PercentPipe, TitleCasePipe,
} from '@angular/common';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
Expand Down Expand Up @@ -42,6 +42,9 @@ import { FormatDateTimePipe } from 'app/modules/pipes/format-date-time/format-da
import { MapValuePipe } from 'app/modules/pipes/map-value/map-value.pipe';
import { NetworkSpeedPipe } from 'app/modules/pipes/network-speed/network-speed.pipe';
import { TestIdModule } from 'app/modules/test-id/test-id.module';
import { AppCardLogoComponent } from 'app/pages/apps/components/app-card-logo/app-card-logo.component';
import { AppStateCellComponent } from 'app/pages/apps/components/installed-apps/app-state-cell/app-state-cell.component';
import { AppUpdateCellComponent } from 'app/pages/apps/components/installed-apps/app-update-cell/app-update-cell.component';
import { DashboardComponent } from 'app/pages/dashboard/components/dashboard/dashboard.component';
import {
WidgetGroupControlsComponent,
Expand All @@ -57,6 +60,12 @@ import { routing } from 'app/pages/dashboard/dashboard.routing';
import { DashboardStore } from 'app/pages/dashboard/services/dashboard.store';
import { WidgetResourcesService } from 'app/pages/dashboard/services/widget-resources.service';
import { widgetComponents } from 'app/pages/dashboard/widgets/all-widgets.constant';
import { AppCardInfoComponent } from 'app/pages/dashboard/widgets/apps/common/app-card-info/app-card-info.component';
import { AppControlsComponent } from 'app/pages/dashboard/widgets/apps/common/app-controls/app-controls.component';
import { AppCpuInfoComponent } from 'app/pages/dashboard/widgets/apps/common/app-cpu-info/app-cpu-info.component';
import { AppMemoryInfoComponent } from 'app/pages/dashboard/widgets/apps/common/app-memory-info/app-memory-info.component';
import { AppNetworkInfoComponent } from 'app/pages/dashboard/widgets/apps/common/app-network-info/app-network-info.component';
import { AppVersionPipe } from 'app/pages/dashboard/widgets/apps/common/utils/app-version.pipe';
import { BackupTaskActionsComponent } from 'app/pages/dashboard/widgets/backup/widget-backup/backup-task-actions/backup-task-actions.component';
import { BackupTaskEmptyComponent } from 'app/pages/dashboard/widgets/backup/widget-backup/backup-task-empty/backup-task-empty.component';
import { BackupTaskTileComponent } from 'app/pages/dashboard/widgets/backup/widget-backup/backup-task-tile/backup-task-tile.component';
Expand Down Expand Up @@ -94,6 +103,11 @@ import { PoolUsageGaugeComponent } from './widgets/storage/widget-pool/common/po
DisksWithZfsErrorsComponent,
PoolStatusComponent,
LastScanErrorsComponent,
AppCardInfoComponent,
AppControlsComponent,
AppCpuInfoComponent,
AppMemoryInfoComponent,
AppNetworkInfoComponent,
...widgetComponents,
],
providers: [
Expand Down Expand Up @@ -154,6 +168,11 @@ import { PoolUsageGaugeComponent } from './widgets/storage/widget-pool/common/po
UiSearchDirective,
RequiresRolesDirective,
Ng2FittextModule,
KeyValuePipe,
AppCardLogoComponent,
AppStateCellComponent,
AppUpdateCellComponent,
AppVersionPipe,
],
})
export class DashboardModule {
Expand Down
33 changes: 21 additions & 12 deletions src/app/pages/dashboard/services/widget-resources.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { Store } from '@ngrx/store';
import { subHours, subMinutes } from 'date-fns';
import {
Observable, Subject, catchError, combineLatestWith, debounceTime,
filter,
forkJoin, map, of, repeat, shareReplay, switchMap, take, timer,
} from 'rxjs';
import { SystemUpdateStatus } from 'app/enums/system-update.enum';
import { toLoadingState } from 'app/helpers/operators/to-loading-state.helper';
import { App } from 'app/interfaces/app.interface';
import { LoadingState, toLoadingState } from 'app/helpers/operators/to-loading-state.helper';
import { ApiEvent } from 'app/interfaces/api-message.interface';
import { App, AppStartQueryParams, AppStats } from 'app/interfaces/app.interface';
import { Dataset } from 'app/interfaces/dataset.interface';
import { Disk } from 'app/interfaces/disk.interface';
import { Job } from 'app/interfaces/job.interface';
Expand Down Expand Up @@ -141,28 +143,35 @@ export class WidgetResourcesService {
);
}

getApp(appName: string): Observable<App> {
return this.ws.call('app.query', [[['name', '=', appName]]]).pipe(
getApp(appName: string): Observable<LoadingState<App>> {
return this.ws.callAndSubscribe('app.query', [[['name', '=', appName]]]).pipe(
map((apps) => {
if (apps.length === 0) {
throw new Error(`App «${appName}» not found. Configure widget to choose another app.`);
}
return apps[0];
}),
toLoadingState(),
shareReplay({ bufferSize: 1, refCount: true }),
);
}

// TODO: Fix when stats API is ready
getAppStats(appName: string): Observable<unknown> {
console.error(`getAppStats(${appName}) not implemented yet`);
return of();
getAppStats(appName: string): Observable<LoadingState<AppStats>> {
return this.ws.subscribe('app.stats').pipe(
filter(() => Boolean(appName)),
map((event) => event.fields.find((stats) => stats.app_name === appName)),
toLoadingState(),
shareReplay({ bufferSize: 1, refCount: true }),
);
}

// TODO: Fix when stats API is ready
getAppStatusUpdates(appName: string): Observable<Job> {
console.error(`getAppStatusUpdates(${appName}) not implemented yet`);
return of();
getAppStatusUpdates(appName: string): Observable<Job<void, AppStartQueryParams>> {
return this.ws.subscribe('core.get_jobs').pipe(
filter((event) => ['app.start', 'app.stop'].includes(event.fields.method)),
filter((event: ApiEvent<Job<void, AppStartQueryParams>>) => event.fields.arguments[0] === appName),
map((event) => event.fields),
shareReplay({ bufferSize: 1, refCount: true }),
);
}

constructor(
Expand Down
5 changes: 5 additions & 0 deletions src/app/pages/dashboard/types/widget.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
* Provide migration if possible.
*/
export enum WidgetType {
App = 'app',
AppCpu = 'app-cpu',
AppNetwork = 'app-network',
AppMemory = 'app-memory',
AppInfo = 'app-info',
Ipv4Address = 'ipv4-address',
Ipv6Address = 'ipv6-address',
Help = 'help',
Expand Down
20 changes: 20 additions & 0 deletions src/app/pages/dashboard/widgets/all-widgets.constant.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { WidgetType } from 'app/pages/dashboard/types/widget.interface';
import { appWidget } from 'app/pages/dashboard/widgets/apps/widget-app/widget-app.definition';
import { appCpuWidget } from 'app/pages/dashboard/widgets/apps/widget-app-cpu/widget-app-cpu.definition';
import { appInfoWidget } from 'app/pages/dashboard/widgets/apps/widget-app-info/widget-app-info.definition';
import { appMemoryWidget } from 'app/pages/dashboard/widgets/apps/widget-app-memory/widget-app-memory.definition';
import { appNetworkWidget } from 'app/pages/dashboard/widgets/apps/widget-app-network/widget-app-network.definition';
import { backupTasksWidget } from 'app/pages/dashboard/widgets/backup/widget-backup/widget-backup.definition';
import { cpuWidget } from 'app/pages/dashboard/widgets/cpu/widget-cpu/widget-cpu.definition';
import { cpuModelWidget } from 'app/pages/dashboard/widgets/cpu/widget-cpu-model/widget-cpu-model.definition';
Expand Down Expand Up @@ -34,6 +39,16 @@ import { systemImageWidget } from 'app/pages/dashboard/widgets/system/widget-sys
import { systemUptimeWidget } from 'app/pages/dashboard/widgets/system/widget-system-uptime/widget-system-uptime.definition';

export const widgetComponents = [
appWidget.component,
appWidget.settingsComponent,
appCpuWidget.component,
appCpuWidget.settingsComponent,
appInfoWidget.component,
appInfoWidget.settingsComponent,
appMemoryWidget.component,
appMemoryWidget.settingsComponent,
appNetworkWidget.component,
appNetworkWidget.settingsComponent,
ipv4AddressWidget.component,
ipv4AddressWidget.settingsComponent,
helpWidget.component,
Expand Down Expand Up @@ -67,6 +82,11 @@ export const widgetComponents = [
];

export const widgetRegistry = {
[WidgetType.App]: appWidget,
[WidgetType.AppCpu]: appCpuWidget,
[WidgetType.AppMemory]: appMemoryWidget,
[WidgetType.AppNetwork]: appNetworkWidget,
[WidgetType.AppInfo]: appInfoWidget,
[WidgetType.Pool]: poolWidget,
[WidgetType.PoolUsageGauge]: poolUsageGaugeWidget,
[WidgetType.PoolStatus]: poolStatusWidget,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<div class="app-header">
<h3 *ixWithLoadingState="app() as app" class="name">{{ app.name }}</h3>
<div *ixWithLoadingState="app() as app" class="version">v{{ app.metadata?.app_version }}</div>
<div *ixWithLoadingState="app() as app" class="version">
{{ app.metadata?.app_version | appVersion }}
</div>
</div>
<div class="app-status">
<ix-app-state-cell
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ import { App, AppStartQueryParams } from 'app/interfaces/app.interface';
import { Job } from 'app/interfaces/job.interface';
import { AppStateCellComponent } from 'app/pages/apps/components/installed-apps/app-state-cell/app-state-cell.component';
import { AppUpdateCellComponent } from 'app/pages/apps/components/installed-apps/app-update-cell/app-update-cell.component';
import { AppVersionPipe } from 'app/pages/dashboard/widgets/apps/common/utils/app-version.pipe';
import { AppCardInfoComponent } from './app-card-info.component';

describe('AppCardInfoComponent', () => {
let spectator: Spectator<AppCardInfoComponent>;
const createComponent = createComponentFactory({
component: AppCardInfoComponent,
declarations: [MockComponents(AppStateCellComponent, AppUpdateCellComponent)],
declarations: [
MockComponents(AppStateCellComponent, AppUpdateCellComponent),
],
imports: [AppVersionPipe],
});

beforeEach(() => {
Expand All @@ -24,7 +28,7 @@ describe('AppCardInfoComponent', () => {
value: {
name: 'TestApp',
metadata: {
app_version: '1.0.0',
app_version: 'v1.0.0',
},
},
} as LoadingState<App>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export class AppControlsComponent {
onRestartApp(app: App): void {
this.isRestarting.set(true);
this.snackbar.success(this.translate.instant('App is restarting'));

this.appService.restartApplication(app)
.pipe(untilDestroyed(this))
.subscribe({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ <h4>{{ 'Network I/O' | translate }}</h4>
<div class="in-out-row">
<span>{{ 'In' | translate }}:</span>
<span *ixWithLoadingState="stats() as stats">
{{ stats.networks[0].rx_bytes | ixNetworkSpeed }}
{{ incomingTraffic() | ixNetworkSpeed }}
</span>
</div>
<div class="in-out-row">
<span>{{ 'Out' | translate }}:</span>
<span *ixWithLoadingState="stats() as stats">
{{ stats.networks[0].tx_bytes | ixNetworkSpeed }}
{{ outgoingTraffic() | ixNetworkSpeed }}
</span>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ export class AppNetworkInfoComponent {
return [...this.initialNetworkStats, ...cachedStats].slice(-60);
});

readonly incomingTraffic = computed(() => {
return this.stats()?.value?.networks?.reduce((sum, stats) => sum + stats.rx_bytes, 0);
});

readonly outgoingTraffic = computed(() => {
return this.stats()?.value?.networks?.reduce((sum, stats) => sum + stats.tx_bytes, 0);
});

protected networkChartData = computed<ChartData<'line'>>(() => {
const currentTheme = this.theme.currentTheme();
const data = this.networkStats();
Expand Down Expand Up @@ -63,11 +71,12 @@ export class AppNetworkInfoComponent {
private translate: TranslateService,
) {
effect(() => {
// TODO: Fix this
const networkStats = this.stats()?.value?.networks[0];
if (networkStats) {
const networkStats = this.stats()?.value?.networks;
const incomingTraffic = networkStats?.reduce((sum, stats) => sum + stats.rx_bytes, 0);
const outgoingTraffic = networkStats?.reduce((sum, stats) => sum + stats.tx_bytes, 0);
if (networkStats && incomingTraffic && outgoingTraffic) {
this.cachedNetworkStats.update((cachedStats) => {
return [...cachedStats, [networkStats.rx_bytes, networkStats.tx_bytes]].slice(-60);
return [...cachedStats, [incomingTraffic, outgoingTraffic]].slice(-60);
});
}
}, { allowSignalWrites: true });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { AppVersionPipe } from './app-version.pipe';

describe('AppVersionPipe', () => {
let pipe: AppVersionPipe;

beforeEach(() => {
pipe = new AppVersionPipe();
});

it('should return the value if it starts with "v"', () => {
expect(pipe.transform('v1.0.0')).toBe('v1.0.0');
});

it('should prepend "v" to the value if it does not start with "v"', () => {
expect(pipe.transform('1.0.0')).toBe('v1.0.0');
});

it('should handle an empty string', () => {
expect(pipe.transform('')).toBe('');
});
});
Loading

0 comments on commit 4caf239

Please sign in to comment.