Implemented Check for Updates Button

This commit is contained in:
DerDavidBohl
2025-12-20 15:20:13 +01:00
parent c46e98ea34
commit b255db560b
8 changed files with 101 additions and 65 deletions

View File

@@ -27,4 +27,9 @@ public class DeploymentUpdateController {
deploymentUpdateService.updateDeployment(deploymentName);
}
@PostMapping("check")
public void podtDeploymentUpdateCheck() {
deploymentUpdateService.checkAllDeploymentForUpdates();
}
}

View File

@@ -56,6 +56,20 @@ public class DeploymentUpdateService {
@Value("${dirigent.compose.command}")
private String composeCommand;
@Transactional
public void updateDeployment(String deploymentName) {
File deploymentDir = new File("deployments/" + deploymentName);
String command = this.composeCommand + " pull";
processRunner.executeCommand(Arrays.asList(command.split(" ")), deploymentDir);
this.applicationEventPublisher.publishEvent(new NamedDeploymentStartRequestedEvent(this, deploymentName, true));
this.deploymentUpdateRepository.deleteAllByDeploymentName(deploymentName);
}
@Scheduled(fixedRateString = "${dirigent.update.rate:3}", timeUnit = TimeUnit.HOURS)
public void checkAllDeploymentForUpdates() {
@@ -71,20 +85,6 @@ public class DeploymentUpdateService {
checkIfImageUpdatesExistForDeployment(deployment);
}
}
@Transactional
public void updateDeployment(String deploymentName) {
File deploymentDir = new File("deployments/" + deploymentName);
String command = this.composeCommand + " pull";
processRunner.executeCommand(Arrays.asList(command.split(" ")), deploymentDir);
this.applicationEventPublisher.publishEvent(new NamedDeploymentStartRequestedEvent(this, deploymentName, true));
this.deploymentUpdateRepository.deleteAllByDeploymentName(deploymentName);
}
@Async
public void checkIfImageUpdatesExistForDeployment(Deployment deployment) {
@@ -129,7 +129,6 @@ public class DeploymentUpdateService {
List<DeploymentUpdateEntity> deploymentUpdates = deploymentUpdateRepository
.findAllByDeploymentNameAndServiceAndImage(deployment.name(), service, container.getImage());
;
if (deploymentUpdates.size() > 0)
return;

View File

@@ -1,6 +1,6 @@
import {Injectable} from '@angular/core';
import {Observable, ReplaySubject, tap} from 'rxjs';
import {Deployment} from './deployment';
import {DeploymentState} from './deployment-state';
import {HttpClient} from '@angular/common/http';
import {Secret} from './secret';
import {SystemInformation} from './system-information';
@@ -11,13 +11,14 @@ import { DeploymentUpdate } from './deployment-update';
})
export class ApiService {
private _deploymentStates: ReplaySubject<Array<Deployment>> = new ReplaySubject<Array<Deployment>>(1);
private _deploymentStates: ReplaySubject<Array<DeploymentState>> = new ReplaySubject<Array<DeploymentState>>(1);
private _secrets: ReplaySubject<Array<Secret>> = new ReplaySubject<Array<Secret>>(1);
private _deploymentUpdates: ReplaySubject<Array<DeploymentUpdate>> = new ReplaySubject<Array<DeploymentUpdate>>;
constructor(private http: HttpClient) {
}
get deploymentStates$(): Observable<Array<Deployment>> {
get deploymentStates$(): Observable<Array<DeploymentState>> {
return this._deploymentStates.asObservable();
}
@@ -25,10 +26,22 @@ export class ApiService {
return this._secrets.asObservable();
}
updateDeployment(deployment: Deployment): Observable<void> {
get deploymentUpdates$(): Observable<Array<DeploymentUpdate>> {
return this._deploymentUpdates.asObservable();
}
reloadDeployementUpdates(): void {
this.getDeploymentUpdates().subscribe(r => this._deploymentUpdates.next(r))
}
updateDeployment(deployment: DeploymentState): Observable<void> {
return this.http.post<void>(`api/v1/deployment-updates/${deployment.name}/run`, {});
}
checkForUpdates() {
return this.http.post<void>(`api/v1/deployment-updates/check`, {});
}
getDeploymentUpdates(): Observable<Array<DeploymentUpdate>> {
return this.http.get<Array<DeploymentUpdate>>('api/v1/deployment-updates');
}
@@ -37,19 +50,19 @@ export class ApiService {
this.getAllDeploymentStates().subscribe(r => this._deploymentStates.next(r));
}
getAllDeploymentStates(): Observable<Array<Deployment>> {
return this.http.get<Array<Deployment>>('api/v1/deployments');
getAllDeploymentStates(): Observable<Array<DeploymentState>> {
return this.http.get<Array<DeploymentState>>('api/v1/deployments');
}
getSystemInformation(): Observable<SystemInformation> {
return this.http.get<SystemInformation>('api/v1/system-information');
}
stopDeployment(deploymentState: Deployment): Observable<void> {
stopDeployment(deploymentState: DeploymentState): Observable<void> {
return this.http.post<void>(`api/v1/deployments/${deploymentState.name}/stop`, {});
}
startDeployment(deploymentState: Deployment, force: boolean): Observable<void> {
startDeployment(deploymentState: DeploymentState, force: boolean): Observable<void> {
return this.http.post<void>(`api/v1/deployments/${deploymentState.name}/start?forceRecreate=${force}`, {});
}

View File

@@ -0,0 +1,9 @@
import { ImageUpdate } from "./image-update";
export interface DeploymentState {
name: string;
state: string;
message: string;
source: string;
}

View File

@@ -1,12 +0,0 @@
export interface Deployment {
name: string;
state: string;
message: string;
source: string;
imageUpdates: Array<ImageUpdate>
}
export interface ImageUpdate {
service: string;
image: string;
}

View File

@@ -0,0 +1,5 @@
export interface ImageUpdate {
service: string;
image: string;
}

View File

@@ -1,9 +1,6 @@
<div class="space-y-4">
<h2 class="text-2xl">Deployments&#64;{{ (systemInformation$ | async)?.instanceName }} (<strong>{{ (deployments$ | async)?.length}}</strong>)</h2>
<div>
<a [href]="(systemInformation$ | async)?.gitUrl" target="_blank">View Source</a>
</div>
<h2 class="text-2xl">Deployments&#64;{{ (systemInformation$ | async)?.instanceName }} (<strong>{{ (deploymentStates$ | async)?.length}}</strong>)</h2>
<mat-form-field class="md:col-span-2 w-3xl">
<mat-label>Search...</mat-label>
<input matInput (keyup)="search($event)" >
@@ -16,6 +13,14 @@
</mat-chip-option>
}
</mat-chip-listbox>
<button [disabled]="isCheckingForUpdates" (click)="checkForUpdates()" mat-button>
@if(isCheckingForUpdates) {
checking for updates...
} @else {
Check for updates
}
</button>
<a mat-button [href]="(systemInformation$ | async)?.gitUrl" target="_blank">View Source</a>
<table [dataSource]="tableDataSource$" class="mat-elevation-z8" mat-table matSort (matSortChange)="announceSortChange($event)" >
<!--- Note that these columns can be defined in any order.
@@ -25,14 +30,14 @@
<ng-container matColumnDef="actions">
<th *matHeaderCellDef mat-header-cell> Actions</th>
<td *matCellDef="let element" mat-cell>
<div matBadge="Update" matBadgeSize="small" [matBadgeHidden]="!(isUpdateAvailable(element.name) | async)" [matBadgePosition]="'above before'" class="h-full">
<div matBadge="Update" matBadgeSize="small" [matBadgeHidden]="!(element.updates.length && element.updates.length > 0)" [matBadgePosition]="'above before'" class="h-full">
<button [matMenuTriggerFor]="menu" mat-icon-button [matBadge]="element.e" matBadgeColor="accent">
<mat-icon>more_vert</mat-icon>
</button>
</div>
<mat-menu #menu="matMenu">
@if (isUpdateAvailable(element.name) | async) {
@if (element.updates.length && element.updates.length > 0) {
<button (click)="updateDeployment(element)" mat-menu-item>
Update
</button>

View File

@@ -2,7 +2,7 @@ import { AsyncPipe } from '@angular/common';
import { Component, inject, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatBadge } from '@angular/material/badge';
import { MatIconButton } from '@angular/material/button';
import { MatIconButton, MatButton, MatAnchor } from '@angular/material/button';
import { MatChip, MatChipListbox, MatChipListboxChange, MatChipOption } from '@angular/material/chips';
import { MatDialog } from '@angular/material/dialog';
import { MatIcon } from '@angular/material/icon';
@@ -21,10 +21,10 @@ import {
MatRowDef,
MatTable
} from '@angular/material/table';
import { interval, Observable, ReplaySubject } from 'rxjs';
import { distinctUntilChanged, map, shareReplay, switchMap } from 'rxjs/operators';
import { combineLatest, interval, Observable, ReplaySubject } from 'rxjs';
import { distinctUntilChanged, map, shareReplay, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../api/api.service';
import { Deployment } from '../api/deployment';
import { DeploymentState } from '../api/deployment-state';
import { DeploymentUpdate } from '../api/deployment-update';
import { SystemInformation } from '../api/system-information';
import { StartDialogComponent } from './start-dialog/start-dialog.component';
@@ -57,8 +57,10 @@ import { StartDialogComponent } from './start-dialog/start-dialog.component';
MatSort,
MatLabel,
MatInput,
MatFormField
],
MatFormField,
MatButton,
MatAnchor
],
templateUrl: './deployments.component.html',
styleUrl: './deployments.component.css',
})
@@ -68,31 +70,40 @@ export class DeploymentsComponent implements OnInit {
sort$ = new ReplaySubject<Sort>(1);
search$ = new ReplaySubject<string>(1);
deployments$: Observable<Array<Deployment>>;
tableDataSource$: Observable<Array<Deployment>>;
deploymentStates$: Observable<Array<DeploymentState>>;
tableDataSource$: Observable<Array<DeploymentState>>;
filterValues$: Observable<string[]>;
systemInformation$: Observable<SystemInformation>;
deploymentUpdates$: Observable<Array<DeploymentUpdate>>;
statesAndUpdates$: Observable<Array<DeploymentState & {updates: Array<DeploymentUpdate>}>>;
displayedColumns = ['actions', 'name', 'state', 'message'];
isCheckingForUpdates = false;
readonly dialog = inject(MatDialog);
constructor(private apiService: ApiService) {
this.deploymentUpdates$ = apiService.getDeploymentUpdates().pipe(shareReplay(1));
this.deploymentUpdates$ = this.apiService.deploymentUpdates$;
this.deploymentStates$ = this.apiService.deploymentStates$;
this.systemInformation$ = apiService.getSystemInformation();
this.systemInformation$.subscribe(i => document.title = `Dirigent@${i.instanceName}`)
this.deployments$ = this.apiService.deploymentStates$.pipe(
distinctUntilChanged((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr))
);
this.filterValues$ = this.deployments$.pipe(map(ds => [...new Set(ds.map(ds => ds.state))]));
this.statesAndUpdates$ = combineLatest([this.deploymentStates$, this.deploymentUpdates$])
.pipe(
distinctUntilChanged((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr)),
tap(a => console.log('triggered', a)),
map(([states, updates]) =>
states.map(s => ({...s, updates: updates.filter(u => u.deploymentName === s.name)})
)));
this.filterValues$ = this.statesAndUpdates$.pipe(map(ds => [...new Set(ds.map(ds => ds.state))]));
this.tableDataSource$ = this.selectedFilterValues$.pipe(
switchMap(selectedFilterValues => this.deployments$.pipe(
switchMap(selectedFilterValues => this.statesAndUpdates$.pipe(
map(ds => ds.filter(ds => selectedFilterValues.includes(ds.state))),
)),
switchMap(ds => this.search$.pipe(
@@ -121,7 +132,10 @@ export class DeploymentsComponent implements OnInit {
);
this.apiService.reloadDeploymentStates();
interval(2000).subscribe(() => this.apiService.reloadDeploymentStates());
interval(2000).subscribe(() => {
this.apiService.reloadDeploymentStates();
this.apiService.reloadDeployementUpdates();
});
}
@@ -131,19 +145,17 @@ export class DeploymentsComponent implements OnInit {
this.search$.next('');
}
isUpdateAvailable(deploymentName: string): Observable<boolean> {
return this.deploymentUpdates$.pipe(
map(dus => dus.find(du => du.deploymentName === deploymentName)),
map(Boolean)
);
checkForUpdates() {
this.isCheckingForUpdates = true;
this.apiService.checkForUpdates().subscribe(() => this.isCheckingForUpdates = false)
}
updateDeployment(deployment: Deployment) {
updateDeployment(deployment: DeploymentState) {
this.apiService.updateDeployment(deployment)
.subscribe(() => this.apiService.reloadDeploymentStates());
}
startDeployment(deploymentState: Deployment) {
startDeployment(deploymentState: DeploymentState) {
const dialogRef = this.dialog.open(StartDialogComponent);
dialogRef.afterClosed().subscribe((result) => {
@@ -154,7 +166,7 @@ export class DeploymentsComponent implements OnInit {
});
}
stopDeployment(element: Deployment) {
stopDeployment(element: DeploymentState) {
this.apiService.stopDeployment(element).subscribe(() => this.apiService.reloadDeploymentStates());
@@ -187,7 +199,7 @@ export class DeploymentsComponent implements OnInit {
}
countDeploymentsByState(state: string): Observable<number> {
return this.deployments$.pipe(
return this.deploymentStates$.pipe(
map(deployments => deployments.filter(deployment => deployment.state === state).length)
);
}