-
Notifications
You must be signed in to change notification settings - Fork 311
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Boris Vasilenko
committed
Dec 4, 2024
1 parent
b995aca
commit bfea23d
Showing
12 changed files
with
1,091 additions
and
827 deletions.
There are no files selected for viewing
57 changes: 57 additions & 0 deletions
57
...pps-list/installed-apps-list-bulk-actions/installed-apps-list-bulk-actions.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
31 changes: 31 additions & 0 deletions
31
...pps-list/installed-apps-list-bulk-actions/installed-apps-list-bulk-actions.component.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
80 changes: 80 additions & 0 deletions
80
...-list/installed-apps-list-bulk-actions/installed-apps-list-bulk-actions.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
59 changes: 59 additions & 0 deletions
59
...-apps-list/installed-apps-list-bulk-actions/installed-apps-list-bulk-actions.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
123 changes: 123 additions & 0 deletions
123
...ges/apps/components/installed-apps/installed-apps-list/installed-apps-list.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Oops, something went wrong.