Skip to content

Commit

Permalink
NAS-132759: Refactor Installed Apps
Browse files Browse the repository at this point in the history
  • Loading branch information
Boris Vasilenko committed Dec 4, 2024
1 parent b995aca commit bfea23d
Show file tree
Hide file tree
Showing 12 changed files with 1,091 additions and 827 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<div class="bulk-actions-container">
<div class="bulk-selected">
<span>{{ checkedApps().length }}</span>
<span>{{ 'Selected' | translate }}</span>
</div>

<div class="bulk-button-wrapper">
<label>{{ 'Bulk Actions' | translate }}</label>
<button
*ixRequiresRoles="requiredRoles"
mat-button
ixTest="bulk-actions-menu"
[matMenuTriggerFor]="menu"
>
{{ 'Select action' | translate }}
<ix-icon name="mdi-menu-down" class="menu-caret"></ix-icon>
</button>
</div>

<mat-menu #menu="matMenu">
<button
*ixRequiresRoles="requiredRoles"
mat-menu-item
ixTest="start-selected"
[disabled]="isBulkStartDisabled"
(click)="bulkStart.emit()"
>
<span>{{ 'Start All Selected' | translate }}</span>
</button>
<button
*ixRequiresRoles="requiredRoles"
mat-menu-item
ixTest="stop-selected"
[disabled]="isBulkStopDisabled"
(click)="bulkStop.emit()"
>
<span>{{ 'Stop All Selected' | translate }}</span>
</button>
<button
*ixRequiresRoles="requiredRoles"
mat-menu-item
ixTest="upgrade-selected"
[disabled]="isBulkUpgradeDisabled"
(click)="bulkUpgrade.emit()"
>
<span>{{ 'Upgrade All Selected' | translate }}</span>
</button>
<button
*ixRequiresRoles="requiredRoles"
mat-menu-item
ixTest="delete-selected"
(click)="bulkDelete.emit()"
>
<span>{{ 'Delete All Selected' | translate }}</span>
</button>
</mat-menu>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.bulk-selected {
align-items: center;
align-self: flex-end;
display: inline-flex;
font-size: 16px;
gap: 4px;
height: 36px;
}

.bulk-actions-container {
align-items: flex-end;
display: flex;
gap: 12px;
}

.bulk-button-wrapper {
display: flex;
flex-direction: column;

label {
color: var(--fg2);
font-size: 10px;
margin-bottom: 2px;
}

button {
background-color: var(--bg1);
border: 1px solid var(--lines);
font-size: 12px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatMenuModule } from '@angular/material/menu';
import { MatMenuHarness } from '@angular/material/menu/testing';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils';
import { AppState } from 'app/enums/app-state.enum';
import { App } from 'app/interfaces/app.interface';
import { InstalledAppsListBulkActionsComponent } from './installed-apps-list-bulk-actions.component';

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

const checkedAppsMock = [
{ id: 'ix-app-1', state: AppState.Running, upgrade_available: true },
{ id: 'ix-app-2', state: AppState.Stopped },
] as App[];

const createComponent = createComponentFactory({
component: InstalledAppsListBulkActionsComponent,
imports: [MatMenuModule],
providers: [
mockAuth(),
],
});

beforeEach(async () => {
spectator = createComponent({
props: {
checkedApps: checkedAppsMock,
},
});
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
menu = await loader.getHarness(MatMenuHarness);
await menu.open();
});

it('displays the correct count of selected instances', () => {
const selectedCount = spectator.query('.bulk-selected span:first-child');
expect(selectedCount).toHaveText(String(checkedAppsMock.length));
});

it('emits bulkStart after actions', async () => {
const startSpy = jest.spyOn(spectator.component.bulkStart, 'emit');

await menu.open();
await menu.clickItem({ text: 'Start All Selected' });

expect(startSpy).toHaveBeenCalled();
});

it('emits bulkStop after actions', async () => {
const stopSpy = jest.spyOn(spectator.component.bulkStop, 'emit');

await menu.open();
await menu.clickItem({ text: 'Stop All Selected' });

expect(stopSpy).toHaveBeenCalled();
});

it('emits bulkUpgrade after actions', async () => {
const upgradeSpy = jest.spyOn(spectator.component.bulkUpgrade, 'emit');

await menu.open();
await menu.clickItem({ text: 'Upgrade All Selected' });

expect(upgradeSpy).toHaveBeenCalled();
});

it('emits bulkDelete after actions', async () => {
const deleteSpy = jest.spyOn(spectator.component.bulkDelete, 'emit');

await menu.open();
await menu.clickItem({ text: 'Delete All Selected' });

expect(deleteSpy).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
Component,
ChangeDetectionStrategy,
input,
output,
} from '@angular/core';
import { MatButton } from '@angular/material/button';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { UntilDestroy } from '@ngneat/until-destroy';
import { TranslateModule } from '@ngx-translate/core';
import { RequiresRolesDirective } from 'app/directives/requires-roles/requires-roles.directive';
import { AppState } from 'app/enums/app-state.enum';
import { Role } from 'app/enums/role.enum';
import { App } from 'app/interfaces/app.interface';
import { IxIconComponent } from 'app/modules/ix-icon/ix-icon.component';

@UntilDestroy()
@Component({
selector: 'ix-installed-apps-list-bulk-actions',
templateUrl: './installed-apps-list-bulk-actions.component.html',
styleUrls: ['./installed-apps-list-bulk-actions.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
MatMenuTrigger,
MatMenu,
MatMenuItem,
RequiresRolesDirective,
IxIconComponent,
MatButton,
TranslateModule,
],
})

export class InstalledAppsListBulkActionsComponent {
readonly checkedApps = input.required<App[]>();
readonly bulkStart = output();
readonly bulkStop = output();
readonly bulkUpgrade = output();
readonly bulkDelete = output();

protected readonly requiredRoles = [Role.AppsWrite];

get isBulkStartDisabled(): boolean {
return this.checkedApps().every(
(app) => [AppState.Running, AppState.Deploying].includes(app.state),
);
}

get isBulkStopDisabled(): boolean {
return this.checkedApps().every(
(app) => [AppState.Stopped, AppState.Crashed].includes(app.state),
);
}

get isBulkUpgradeDisabled(): boolean {
return !this.checkedApps().some((app) => app.upgrade_available);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@

<div class="table-header">
<h2>{{ 'Applications' | translate }}</h2>

@if (hasCheckedApps) {
<ix-installed-apps-list-bulk-actions
[checkedApps]="checkedApps"
(bulkStart)="onBulkStart()"
(bulkStop)="onBulkStop()"
(bulkUpgrade)="onBulkUpgrade()"
(bulkDelete)="onBulkDelete()"
></ix-installed-apps-list-bulk-actions>
}
</div>

<div class="item-search">
<ix-fake-progress-bar
class="loader-bar"
[loading]="isLoading()"
></ix-fake-progress-bar>

<ix-search-input1
[maxLength]="100"
[disabled]="!dataSource.length"
[value]="filterString"
(search)="onSearch($event)"
></ix-search-input1>
</div>

<div
class="sticky-header"
matSort
matSortActive="application"
matSortDirection="asc"
matSortDisableClear
(matSortChange)="sortChanged($event)"
>
<div class="app-header-row">
<div>
<span class="name-header">
@if (dataSource.length) {
<mat-checkbox
color="primary"
ixTest="select-all-app"
[checked]="allAppsChecked"
[indeterminate]="!allAppsChecked && !!selection.selected.length"
(change)="toggleAppsChecked($event.checked)"
></mat-checkbox>
}
</span>
</div>
<div
[matColumnDef]="sortableField.Application"
[mat-sort-header]="sortableField.Application"
>
{{ 'Application' | translate }}
</div>
<div
[matColumnDef]="sortableField.State"
[mat-sort-header]="sortableField.State"
>
{{ 'Status' | translate }}
</div>
<div>{{ 'CPU' | translate }}</div>
<div>{{ 'RAM' | translate }}</div>
<div>{{ 'Block I/O' | translate }}</div>
<div>{{ 'Network' | translate }}</div>
<div
class="app-update-header"
[matColumnDef]="sortableField.Updates"
[mat-sort-header]="hasUpdates ? sortableField.Updates : null"
[disabled]="!hasUpdates"
>
<span>{{ 'Updates' | translate }}</span>
@if (hasUpdates) {
<ix-icon
class="has-updates-icon"
name="mdi-alert-circle"
matTooltipPosition="above"
[matTooltip]="'Updates available' | translate"
></ix-icon>
}
</div>
<div>{{ 'Controls' | translate }}</div>
</div>
</div>

<div
matSort
matSortDisableClear
matSortActive="application"
matSortDirection="asc"
class="app-wrapper"
(matSortChange)="sortChanged($event)"
>
<div class="app-inner">
<div class="apps-rows">
@for (app of filteredApps; track app.name) {
<ix-app-row
tabindex="0"
[app]="app"
[stats]="getAppStats(app.name) | async"
[class.selected]="selectedApp.id === app.id"
[selected]="selection.isSelected(app.id)"
[job]="appJobs.get(app.name)"
(startApp)="start(app.name)"
(stopApp)="stop(app.name)"
(restartApp)="restart(app.name)"
(clickStatus)="openStatusDialog(app.name)"
(selectionChange)="selection.toggle(app.id)"
(click)="viewDetails(app)"
(keydown.enter)="viewDetails(app)"
></ix-app-row>
}

@if ((dataSource.length && !filteredApps.length) || (!dataSource.length && !isLoading())) {
<div class="no-apps">
<ix-empty [conf]="entityEmptyConf"></ix-empty>
</div>
}
</div>
</div>
</div>
Loading

0 comments on commit bfea23d

Please sign in to comment.