Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ <h2>{{ 'Applications' | translate }}</h2>
<div
class="sticky-header"
matSort
matSortActive="application"
matSortDirection="asc"
matSortDisableClear
[matSortActive]="sortingInfo().active"
[matSortDirection]="sortingInfo().direction"
(matSortChange)="setDatasourceWithSort($event)"
>
<div class="app-header-row">
Expand Down Expand Up @@ -88,9 +88,9 @@ <h2>{{ 'Applications' | translate }}</h2>
<div
matSort
matSortDisableClear
matSortActive="application"
matSortDirection="asc"
class="app-wrapper"
[matSortActive]="sortingInfo().active"
[matSortDirection]="sortingInfo().direction"
(matSortChange)="setDatasourceWithSort($event)"
>
<div class="app-inner">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatCheckboxHarness } from '@angular/material/checkbox/testing';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { SortDirection } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { Router } from '@angular/router';
import { createRoutingFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { MockDeclaration } from 'ng-mocks';
import { ImgFallbackDirective } from 'ngx-img-fallback';
import { NgxPopperjsContentComponent, NgxPopperjsDirective, NgxPopperjsLooseDirective } from 'ngx-popperjs';
import { of } from 'rxjs';
import { BehaviorSubject, of } from 'rxjs';
import { mockApi, mockJob } from 'app/core/testing/utils/mock-api.utils';
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils';
import { AppState } from 'app/enums/app-state.enum';
Expand Down Expand Up @@ -36,6 +37,8 @@ describe('InstalledAppsListComponent', () => {
let spectator: Spectator<InstalledAppsListComponent>;
let applicationsService: ApplicationsService;
let loader: HarnessLoader;
let searchQuery$: BehaviorSubject<string>;
let sortingInfo$: BehaviorSubject<{ active: string; direction: SortDirection }>;

const apps = [
{
Expand Down Expand Up @@ -81,10 +84,21 @@ describe('InstalledAppsListComponent', () => {
isDockerStarted$: of(true),
selectedPool$: of('pool'),
}),
mockProvider(InstalledAppsStore, {
isLoading$: of(false),
installedApps$: of(apps),
}),
{
provide: InstalledAppsStore,
useFactory: () => {
searchQuery$ = new BehaviorSubject('');
sortingInfo$ = new BehaviorSubject({ active: 'application', direction: 'asc' as SortDirection });
return {
isLoading$: of(false),
installedApps$: of(apps),
searchQuery$: searchQuery$.asObservable(),
sortingInfo$: sortingInfo$.asObservable(),
setSearchQuery: jest.fn((query: string) => searchQuery$.next(query)),
setSortingInfo: jest.fn((info: { active: string; direction: SortDirection }) => sortingInfo$.next(info)),
};
},
},
mockProvider(AppsStore, {
isLoading$: of(false),
availableApps$: of([]),
Expand Down Expand Up @@ -233,7 +247,7 @@ describe('InstalledAppsListComponent', () => {
const originalDataSource = [...apps];
component.dataSource = originalDataSource;

component.setDatasourceWithSort({ active: 'application', direction: 'asc' }, []);
component.setDatasourceWithSort({ active: 'application', direction: 'asc' as SortDirection }, []);

expect(component.dataSource).toHaveLength(2);
expect(component.dataSource[0].name).toBe('test-app-1');
Expand All @@ -250,7 +264,7 @@ describe('InstalledAppsListComponent', () => {
upgrade_available: false,
}] as App[];

component.setDatasourceWithSort({ active: 'application', direction: 'asc' }, newApps);
component.setDatasourceWithSort({ active: 'application', direction: 'asc' as SortDirection }, newApps);

expect(component.dataSource).toHaveLength(1);
expect(component.dataSource[0].name).toBe('new-app');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SelectionModel } from '@angular/cdk/collections';
import { AsyncPipe, Location } from '@angular/common';
import { Component, ChangeDetectionStrategy, output, OnInit, ChangeDetectorRef, inject, signal } from '@angular/core';
import { Component, ChangeDetectionStrategy, output, OnInit, ChangeDetectorRef, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatDialog } from '@angular/material/dialog';
Expand Down Expand Up @@ -110,13 +110,10 @@ export class InstalledAppsListComponent implements OnInit {

dataSource: App[] = [];
selectedApp: App | undefined;
searchQuery = signal('');
searchQuery = toSignal(this.installedAppsStore.searchQuery$, { requireSync: true });
appJobs = new Map<string, Job<void, AppStartQueryParams>>();
selection = new SelectionModel<string>(true, []);
sortingInfo: Sort = {
active: SortableField.Application,
direction: 'asc',
};
sortingInfo = toSignal(this.installedAppsStore.sortingInfo$, { requireSync: true });

readonly sortableField = SortableField;

Expand Down Expand Up @@ -186,7 +183,7 @@ export class InstalledAppsListComponent implements OnInit {
}

protected onListFiltered(query: string): void {
this.searchQuery.set(query);
this.installedAppsStore.setSearchQuery(query);

if (!this.filteredApps.length) {
this.showLoadStatus(EmptyType.NoSearchResults);
Expand Down Expand Up @@ -271,7 +268,7 @@ export class InstalledAppsListComponent implements OnInit {
untilDestroyed(this),
).subscribe({
next: ([,, apps]) => {
this.setDatasourceWithSort(this.sortingInfo, apps);
this.setDatasourceWithSort(this.sortingInfo(), apps);
this.selectAppForDetails(this.appId());
this.cdr.markForCheck();
},
Expand All @@ -295,7 +292,7 @@ export class InstalledAppsListComponent implements OnInit {
// This ensures the UI stays in sync even for minimized jobs.
if (job) {
this.appJobs.set(name, job);
this.setDatasourceWithSort(this.sortingInfo);
this.setDatasourceWithSort(this.sortingInfo());
this.cdr.markForCheck();
}
});
Expand All @@ -318,7 +315,7 @@ export class InstalledAppsListComponent implements OnInit {
// This ensures the UI stays in sync even for minimized jobs.
if (job) {
this.appJobs.set(name, job);
this.setDatasourceWithSort(this.sortingInfo);
this.setDatasourceWithSort(this.sortingInfo());
this.cdr.markForCheck();
}
});
Expand All @@ -341,7 +338,7 @@ export class InstalledAppsListComponent implements OnInit {
// This ensures the UI stays in sync even for minimized jobs.
if (job) {
this.appJobs.set(name, job);
this.setDatasourceWithSort(this.sortingInfo);
this.setDatasourceWithSort(this.sortingInfo());
this.cdr.markForCheck();
}
});
Expand Down Expand Up @@ -408,7 +405,7 @@ export class InstalledAppsListComponent implements OnInit {
}

setDatasourceWithSort(sort: Sort, apps?: App[]): void {
this.sortingInfo = sort;
this.installedAppsStore.setSortingInfo(sort);
const sourceArray = apps && apps.length > 0 ? apps : this.dataSource;
this.dataSource = [...sourceArray].sort((a, b) => {
const isAsc = sort.direction === 'asc';
Expand Down Expand Up @@ -515,7 +512,7 @@ export class InstalledAppsListComponent implements OnInit {
.subscribe((event) => {
const [name] = event.fields.arguments;
this.appJobs.set(name, event.fields);
this.setDatasourceWithSort(this.sortingInfo);
this.setDatasourceWithSort(this.sortingInfo());
this.cdr.markForCheck();
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { provideMockStore } from '@ngrx/store/testing';
import { MockComponent, MockDeclaration } from 'ng-mocks';
import { ImgFallbackDirective } from 'ngx-img-fallback';
import { NgxPopperjsContentComponent, NgxPopperjsDirective, NgxPopperjsLooseDirective } from 'ngx-popperjs';
import { of } from 'rxjs';
import { BehaviorSubject, of } from 'rxjs';
import { mockApi } from 'app/core/testing/utils/mock-api.utils';
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils';
import { AppState } from 'app/enums/app-state.enum';
Expand All @@ -29,6 +29,8 @@ import { selectAdvancedConfig, selectSystemConfigState } from 'app/store/system-
describe('InstalledAppsComponent', () => {
let spectator: Spectator<InstalledAppsComponent>;
let applicationsService: ApplicationsService;
let searchQuery$: BehaviorSubject<string>;
let sortingInfo$: BehaviorSubject<{ active: string; direction: string }>;

const app = {
id: 'ix-test-app',
Expand Down Expand Up @@ -59,10 +61,21 @@ describe('InstalledAppsComponent', () => {
isDockerStarted$: of(true),
selectedPool$: of('pool'),
}),
mockProvider(InstalledAppsStore, {
isLoading$: of(false),
installedApps$: of([app]),
}),
{
provide: InstalledAppsStore,
useFactory: () => {
searchQuery$ = new BehaviorSubject('');
sortingInfo$ = new BehaviorSubject({ active: 'application', direction: 'asc' });
return {
isLoading$: of(false),
installedApps$: of([app]),
searchQuery$: searchQuery$.asObservable(),
sortingInfo$: sortingInfo$.asObservable(),
setSearchQuery: jest.fn((query: string) => searchQuery$.next(query)),
setSortingInfo: jest.fn((info: { active: string; direction: string }) => sortingInfo$.next(info)),
};
},
},
mockProvider(LayoutService, {
navigatePreservingScroll: jest.fn(() => of()),
}),
Expand Down
24 changes: 24 additions & 0 deletions src/app/pages/apps/store/installed-apps-store.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Injectable, OnDestroy, inject } from '@angular/core';
import { Sort, SortDirection } from '@angular/material/sort';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { ComponentStore } from '@ngrx/component-store';
import {
Expand All @@ -16,14 +17,27 @@ import { AppsStore } from 'app/pages/apps/store/apps-store.service';
import { DockerStore } from 'app/pages/apps/store/docker.store';
import { ErrorHandlerService } from 'app/services/errors/error-handler.service';

enum SortableField {
Application = 'application',
State = 'state',
Updates = 'updates',
}

export interface InstalledAppsState {
installedApps: App[];
isLoading: boolean;
searchQuery: string;
sortingInfo: Sort;
}

const initialState: InstalledAppsState = {
installedApps: [],
isLoading: false,
searchQuery: '',
sortingInfo: {
active: SortableField.Application,
direction: 'asc' as SortDirection,
},
};

@UntilDestroy()
Expand All @@ -37,6 +51,8 @@ export class InstalledAppsStore extends ComponentStore<InstalledAppsState> imple

readonly installedApps$ = this.select((state) => state.installedApps);
readonly isLoading$ = this.select((state) => state.isLoading);
readonly searchQuery$ = this.select((state) => state.searchQuery);
readonly sortingInfo$ = this.select((state) => state.sortingInfo);
private installedAppsSubscription: Subscription;

constructor() {
Expand Down Expand Up @@ -188,4 +204,12 @@ export class InstalledAppsStore extends ComponentStore<InstalledAppsState> imple
latestApps: updateApps(state.latestApps),
}));
}

setSearchQuery(searchQuery: string): void {
this.patchState({ searchQuery });
}

setSortingInfo(sortingInfo: Sort): void {
this.patchState({ sortingInfo });
}
}
Loading