mirror of
https://github.com/DerDavidBohl/dirigent-spring.git
synced 2026-01-10 08:50:04 -06:00
11
.gitignore
vendored
11
.gitignore
vendored
@@ -31,10 +31,13 @@ build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
/deployments/
|
||||
/config/
|
||||
backend/deployments/
|
||||
backend/config/
|
||||
|
||||
### Config Files ###
|
||||
/src/main/resources/application.properties
|
||||
/src/main/resources/application-local.properties
|
||||
backend/src/main/resources/application-local.properties
|
||||
backend/data/
|
||||
/backend/src/main/resources/static/
|
||||
/data/
|
||||
/deployments/
|
||||
/config/
|
||||
|
||||
20
Dockerfile
20
Dockerfile
@@ -1,8 +1,18 @@
|
||||
# Use Maven image to build the application
|
||||
FROM maven:3.9.9 AS build
|
||||
|
||||
FROM node:alpine AS frontend-build
|
||||
WORKDIR /app
|
||||
COPY pom.xml .
|
||||
COPY src ./src
|
||||
COPY frontend .
|
||||
RUN rm -rf package-lock.json
|
||||
RUN npm cache clean --force
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
# Use Maven image to build the application
|
||||
FROM maven:3.9.9 AS backend-build
|
||||
WORKDIR /app
|
||||
COPY backend/pom.xml .
|
||||
COPY backend/src ./src
|
||||
COPY --from=frontend-build /app/dist/browser ./src/main/resources/static
|
||||
RUN mvn clean package -DskipTests
|
||||
|
||||
# Use OpenJDK image to run the application
|
||||
@@ -20,6 +30,6 @@ RUN apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugi
|
||||
|
||||
# Finish
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/target/*.jar app.jar
|
||||
COPY --from=backend-build /app/target/*.jar app.jar
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=production"]
|
||||
@@ -2,7 +2,7 @@ POST http://localhost:8080/api/v1/deployments/test2/stop
|
||||
|
||||
###
|
||||
|
||||
POST http://localhost:8080/api/v1/deployments/all/start
|
||||
POST http://localhost:8080/api/v1/deployments/all/start?force=true
|
||||
|
||||
###
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
//package org.davidbohl.dirigent.ui;
|
||||
//
|
||||
//import jakarta.servlet.http.HttpServletRequest;
|
||||
//import org.springframework.stereotype.Controller;
|
||||
//import org.springframework.web.bind.annotation.RequestMapping;
|
||||
//import org.springframework.web.servlet.HandlerMapping;
|
||||
//import org.springframework.web.util.UrlPathHelper;
|
||||
//
|
||||
//@Controller
|
||||
//public class SpaController {
|
||||
//
|
||||
// @RequestMapping(path = { "/ui/", "/ui/**"})
|
||||
// public String forward(HttpServletRequest request) {
|
||||
// UrlPathHelper pathHelper = new UrlPathHelper();
|
||||
// String path = pathHelper.getPathWithinApplication(request);
|
||||
//
|
||||
// if (path.contains(".")) {
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// return "forward:/ui/index.html";
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,16 @@
|
||||
//package org.davidbohl.dirigent.ui;
|
||||
//
|
||||
//import org.springframework.context.annotation.Configuration;
|
||||
//import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
//import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||
//import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
//
|
||||
//@Configuration
|
||||
//public class WebConfig implements WebMvcConfigurer {
|
||||
// @Override
|
||||
// public void addViewControllers(ViewControllerRegistry registry) {
|
||||
// // Forward only if it's NOT a request for a resource with a file extension (like .js, .css, .png)
|
||||
// registry.addViewController("/ui/{path:[^\\.]*}/**") // Match "/ui/something" or "/ui/something/else", excluding URLs with dots.
|
||||
// .setViewName("forward:/ui/index.html");
|
||||
// }
|
||||
//}
|
||||
@@ -8,8 +8,9 @@ spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||
spring.h2.console.enabled=false
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
|
||||
dirigent.git.authToken=
|
||||
dirigent.compose.command=docker compose
|
||||
dirigent.start.all.on.startup=true
|
||||
dirigent.git.authToken=
|
||||
dirigent.delpoyments.schedule.enabled=true
|
||||
dirigent.delpoyments.schedule.cron=0 */5 * * * *
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/dirigent-frontend",
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
@@ -94,5 +94,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
||||
2652
src/frontend/package-lock.json → frontend/package-lock.json
generated
2652
src/frontend/package-lock.json → frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"start": "ng serve --proxy-config proxy.conf.json",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
9
frontend/proxy.conf.json
Normal file
9
frontend/proxy.conf.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"/api/**": {
|
||||
"target": "http://localhost:8080",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
0
frontend/src/app/app.component.spec.ts
Normal file
0
frontend/src/app/app.component.spec.ts
Normal file
15
frontend/src/app/app.config.ts
Normal file
15
frontend/src/app/app.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core';
|
||||
import {provideRouter} from '@angular/router';
|
||||
|
||||
import {routes} from './app.routes';
|
||||
import {provideHttpClient} from '@angular/common/http';
|
||||
import {MAT_DIALOG_DEFAULT_OPTIONS} from '@angular/material/dialog';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
{provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: {hasBackdrop: true}},
|
||||
provideHttpClient(),
|
||||
provideZoneChangeDetection({eventCoalescing: true}),
|
||||
provideRouter(routes)
|
||||
]
|
||||
};
|
||||
9
frontend/src/app/app.routes.ts
Normal file
9
frontend/src/app/app.routes.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import {Routes} from '@angular/router';
|
||||
import {OverviewComponent} from './overview/overview.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: OverviewComponent
|
||||
}
|
||||
];
|
||||
35
frontend/src/app/overview/api.service.ts
Normal file
35
frontend/src/app/overview/api.service.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Observable, ReplaySubject} from 'rxjs';
|
||||
import {DeploymentState} from './deploymentState';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApiService {
|
||||
|
||||
private _deploymentStates: ReplaySubject<Array<DeploymentState>> = new ReplaySubject<Array<DeploymentState>>(1);
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
}
|
||||
|
||||
get deploymentStates$(): Observable<Array<DeploymentState>> {
|
||||
return this._deploymentStates.asObservable();
|
||||
}
|
||||
|
||||
updateDeploymentStates(): void {
|
||||
this.getAllDeploymentStates().subscribe(r => this._deploymentStates.next(r));
|
||||
}
|
||||
|
||||
getAllDeploymentStates(): Observable<Array<DeploymentState>> {
|
||||
return this.http.get<Array<DeploymentState>>('api/v1/deployment-states');
|
||||
}
|
||||
|
||||
stopDeployment(deploymentState: DeploymentState): Observable<void> {
|
||||
return this.http.post<void>(`api/v1/deployments/${deploymentState.name}/stop`, {});
|
||||
}
|
||||
|
||||
startDeployment(deploymentState: DeploymentState, force: boolean): Observable<void> {
|
||||
return this.http.post<void>(`api/v1/deployments/${deploymentState.name}/start?force=${force}`, {});
|
||||
}
|
||||
}
|
||||
5
frontend/src/app/overview/deploymentState.d.ts
vendored
Normal file
5
frontend/src/app/overview/deploymentState.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface DeploymentState {
|
||||
name: string;
|
||||
state: string;
|
||||
message: string;
|
||||
}
|
||||
0
frontend/src/app/overview/overview.component.css
Normal file
0
frontend/src/app/overview/overview.component.css
Normal file
53
frontend/src/app/overview/overview.component.html
Normal file
53
frontend/src/app/overview/overview.component.html
Normal file
@@ -0,0 +1,53 @@
|
||||
<h1 class="text-3xl">Your Deployments</h1>
|
||||
|
||||
<div>
|
||||
<table [dataSource]="dataSource" class="mat-elevation-z8" mat-table>
|
||||
|
||||
<!--- Note that these columns can be defined in any order.
|
||||
The actual rendered columns are set as a property on the row definition" -->
|
||||
|
||||
<!-- Symbol Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th *matHeaderCellDef mat-header-cell> Actions</th>
|
||||
<td *matCellDef="let element" mat-cell>
|
||||
<button [matMenuTriggerFor]="menu" aria-label="Example icon-button with a menu" matIconButton>
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button (click)="startDeployment(element)" mat-menu-item>
|
||||
Start
|
||||
</button>
|
||||
<button (click)="stopDeployment(element)" mat-menu-item>
|
||||
Stop
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<th *matHeaderCellDef mat-header-cell> Name</th>
|
||||
<td *matCellDef="let element" mat-cell> {{ element.name }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Weight Column -->
|
||||
<ng-container matColumnDef="state">
|
||||
<th *matHeaderCellDef mat-header-cell> State</th>
|
||||
<td *matCellDef="let element" mat-cell>
|
||||
<mat-chip class="bg-red-500">{{ element.state }}</mat-chip>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Symbol Column -->
|
||||
<ng-container matColumnDef="message">
|
||||
<th *matHeaderCellDef mat-header-cell> Message</th>
|
||||
<td *matCellDef="let element" mat-cell> {{ element.message }}</td>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr *matRowDef="let row; columns: displayedColumns;" mat-row></tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
81
frontend/src/app/overview/overview.component.ts
Normal file
81
frontend/src/app/overview/overview.component.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {
|
||||
MatCell,
|
||||
MatCellDef,
|
||||
MatColumnDef,
|
||||
MatHeaderCell,
|
||||
MatHeaderCellDef,
|
||||
MatHeaderRow,
|
||||
MatHeaderRowDef,
|
||||
MatRow,
|
||||
MatRowDef,
|
||||
MatTable
|
||||
} from '@angular/material/table';
|
||||
import {DeploymentState} from './deploymentState';
|
||||
import {ApiService} from './api.service';
|
||||
import {MatMenu, MatMenuItem, MatMenuTrigger} from '@angular/material/menu';
|
||||
import {MatIcon} from '@angular/material/icon';
|
||||
import {MatChip} from '@angular/material/chips';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {StartDialogComponent} from './start-dialog/start-dialog.component';
|
||||
import {interval} from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-overview',
|
||||
imports: [
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
MatHeaderCell,
|
||||
MatCell,
|
||||
MatHeaderCellDef,
|
||||
MatCellDef,
|
||||
MatHeaderRow,
|
||||
MatRow,
|
||||
MatRowDef,
|
||||
MatHeaderRowDef,
|
||||
MatMenuTrigger,
|
||||
MatIcon,
|
||||
MatMenu,
|
||||
MatMenuItem,
|
||||
MatChip,
|
||||
|
||||
],
|
||||
templateUrl: './overview.component.html',
|
||||
styleUrl: './overview.component.css',
|
||||
})
|
||||
export class OverviewComponent {
|
||||
dataSource: Array<DeploymentState> = [];
|
||||
displayedColumns = ['actions', 'name', 'state', 'message'];
|
||||
|
||||
readonly dialog = inject(MatDialog);
|
||||
|
||||
constructor(private apiService: ApiService) {
|
||||
this.apiService.deploymentStates$.subscribe(states => {
|
||||
if (JSON.stringify(states) !== JSON.stringify(this.dataSource)) {
|
||||
this.dataSource = states;
|
||||
}
|
||||
});
|
||||
|
||||
this.apiService.updateDeploymentStates();
|
||||
|
||||
interval(2000).subscribe(() => this.apiService.updateDeploymentStates());
|
||||
|
||||
}
|
||||
|
||||
startDeployment(deploymentState: DeploymentState) {
|
||||
const dialogRef = this.dialog.open(StartDialogComponent);
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result.result) {
|
||||
this.apiService.startDeployment(deploymentState, result.force)
|
||||
.subscribe(() => this.apiService.updateDeploymentStates())
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stopDeployment(element: DeploymentState) {
|
||||
|
||||
this.apiService.stopDeployment(element).subscribe(() => this.apiService.updateDeploymentStates());
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<h2 mat-dialog-title>Start Deployment</h2>
|
||||
<mat-dialog-content>
|
||||
<div>
|
||||
Do you want to start the deployment?
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-content class="h-20">
|
||||
<mat-checkbox [(ngModel)]="force">Force</mat-checkbox>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button [mat-dialog-close]="{force, result: true}" mat-button>Yes</button>
|
||||
<button (click)="dialogRef.close()" mat-button>No</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -0,0 +1,32 @@
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatDialogContent,
|
||||
MatDialogRef,
|
||||
MatDialogTitle
|
||||
} from '@angular/material/dialog';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
import {MatCheckbox} from '@angular/material/checkbox';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-start-dialog',
|
||||
imports: [
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogTitle,
|
||||
MatButton,
|
||||
MatCheckbox,
|
||||
FormsModule,
|
||||
MatDialogClose
|
||||
],
|
||||
templateUrl: './start-dialog.component.html',
|
||||
styleUrl: './start-dialog.component.css'
|
||||
})
|
||||
export class StartDialogComponent {
|
||||
readonly dialogRef = inject(MatDialogRef<StartDialogComponent>);
|
||||
force: boolean = false;
|
||||
|
||||
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<title>DirigentFrontend</title>
|
||||
<base href="/">
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<link href="favicon.ico" rel="icon" type="image/x-icon">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
@@ -1,29 +0,0 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {AppComponent} from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AppComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have the 'dirigent-frontend' title`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('dirigent-frontend');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, dirigent-frontend');
|
||||
});
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core';
|
||||
import {provideRouter} from '@angular/router';
|
||||
|
||||
import {routes} from './app.routes';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideZoneChangeDetection({eventCoalescing: true}), provideRouter(routes)]
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
import {Routes} from '@angular/router';
|
||||
|
||||
export const routes: Routes = [];
|
||||
Reference in New Issue
Block a user