diff --git a/Dockerfile b/Dockerfile index bad78fe..578f285 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,24 @@ # Use Maven image to build the application -FROM maven:3.9.9 AS build +FROM maven:3.9.9 AS backend-build WORKDIR /app COPY backend/pom.xml . COPY backend/src ./src RUN mvn clean package -DskipTests +FROM node:24 AS frontend-build +WORKDIR /app +# Install dependencies +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci + +# Copy the rest of the frontend files +COPY frontend/ ./ +# Build the frontend application +RUN npm run build + +# Copy the built frontend files to the backend resources + + # Use OpenJDK image to run the application FROM openjdk:23-jdk-slim-bullseye @@ -20,6 +34,7 @@ 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 +COPY --from=frontend-build /app/dist ./public EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=production"] \ No newline at end of file diff --git a/Test.http b/Test.http index 9b2e648..cc3e062 100644 --- a/Test.http +++ b/Test.http @@ -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 ### diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 8f168ec..007dc3f 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -10,6 +10,5 @@ spring.jpa.hibernate.ddl-auto=update 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 * * * * +dirigent.delpoyments.schedule.cron=0 */5 * * * * \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 10b9db6..d367ba7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,8 +3,8 @@ "version": "0.0.0", "scripts": { "ng": "ng", - "start": "ng serve", - "build": "ng build", + "start": "ng serve --proxy-config proxy.conf.json", + "build": "ng build --base-href /ui/", "watch": "ng build --watch --configuration development", "test": "ng test" }, diff --git a/frontend/proxy.conf.json b/frontend/proxy.conf.json new file mode 100644 index 0000000..47e7bcd --- /dev/null +++ b/frontend/proxy.conf.json @@ -0,0 +1,9 @@ +{ + "/api/**": { + "target": "http://localhost:8080", + "secure": false, + "changeOrigin": true, + "pathRewrite": { + } + } +} diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index b3cb235..6589147 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1,3 +1,4 @@ -
- +
+ +
diff --git a/frontend/src/app/app.component.spec.ts b/frontend/src/app/app.component.spec.ts deleted file mode 100644 index 14d1e9c..0000000 --- a/frontend/src/app/app.component.spec.ts +++ /dev/null @@ -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'); - }); -}); diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 0460e6a..5fda069 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -2,7 +2,14 @@ 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: [provideZoneChangeDetection({eventCoalescing: true}), provideRouter(routes)] + providers: [ + {provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: {hasBackdrop: true}}, + provideHttpClient(), + provideZoneChangeDetection({eventCoalescing: true}), + provideRouter(routes) + ] }; diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 8b5e5fb..fe4326c 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,3 +1,9 @@ import {Routes} from '@angular/router'; +import {OverviewComponent} from './overview/overview.component'; -export const routes: Routes = []; +export const routes: Routes = [ + { + path: '', + component: OverviewComponent + } +]; diff --git a/frontend/src/app/overview/api.service.ts b/frontend/src/app/overview/api.service.ts new file mode 100644 index 0000000..41230fc --- /dev/null +++ b/frontend/src/app/overview/api.service.ts @@ -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> = new ReplaySubject>(1); + + constructor(private http: HttpClient) { + } + + get deploymentStates$(): Observable> { + return this._deploymentStates.asObservable(); + } + + updateDeploymentStates(): void { + this.getAllDeploymentStates().subscribe(r => this._deploymentStates.next(r)); + } + + getAllDeploymentStates(): Observable> { + return this.http.get>('api/v1/deployment-states'); + } + + stopDeployment(deploymentState: DeploymentState): Observable { + return this.http.post(`api/v1/deployments/${deploymentState.name}/stop`, {}); + } + + startDeployment(deploymentState: DeploymentState, force: boolean): Observable { + return this.http.post(`api/v1/deployments/${deploymentState.name}/start?force=${force}`, {}); + } +} diff --git a/frontend/src/app/overview/deploymentState.d.ts b/frontend/src/app/overview/deploymentState.d.ts new file mode 100644 index 0000000..106071a --- /dev/null +++ b/frontend/src/app/overview/deploymentState.d.ts @@ -0,0 +1,5 @@ +export interface DeploymentState { + name: string; + state: string; + message: string; +} diff --git a/frontend/src/app/overview/overview.component.css b/frontend/src/app/overview/overview.component.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/overview/overview.component.html b/frontend/src/app/overview/overview.component.html new file mode 100644 index 0000000..645811f --- /dev/null +++ b/frontend/src/app/overview/overview.component.html @@ -0,0 +1,53 @@ +

Your Deployments

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Actions + + + + + + + Name {{ element.name }} State + {{ element.state }} + Message {{ element.message }}
+ +
diff --git a/frontend/src/app/overview/overview.component.ts b/frontend/src/app/overview/overview.component.ts new file mode 100644 index 0000000..25f9518 --- /dev/null +++ b/frontend/src/app/overview/overview.component.ts @@ -0,0 +1,75 @@ +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 = []; + 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()) + } + }); + } +} diff --git a/frontend/src/app/overview/start-dialog/start-dialog.component.css b/frontend/src/app/overview/start-dialog/start-dialog.component.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/overview/start-dialog/start-dialog.component.html b/frontend/src/app/overview/start-dialog/start-dialog.component.html new file mode 100644 index 0000000..f841195 --- /dev/null +++ b/frontend/src/app/overview/start-dialog/start-dialog.component.html @@ -0,0 +1,13 @@ +

Start Deployment

+ +
+ Do you want to start the deployment? +
+
+ + Force + + + + + diff --git a/frontend/src/app/overview/start-dialog/start-dialog.component.ts b/frontend/src/app/overview/start-dialog/start-dialog.component.ts new file mode 100644 index 0000000..344f9bf --- /dev/null +++ b/frontend/src/app/overview/start-dialog/start-dialog.component.ts @@ -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); + force: boolean = false; + + +} diff --git a/frontend/src/index.html b/frontend/src/index.html index 2daba6a..ca6f0ff 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -4,7 +4,7 @@ DirigentFrontend - + diff --git a/frontend/src/styles.css b/frontend/src/styles.css index e77784c..5ea2488 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1,11 +1,11 @@ /* You can add global styles to this file, and also import other style files */ @import "tailwindcss"; -html, body { - height: 100%; -} +/*html, body {*/ +/* height: 100%;*/ +/*}*/ -body { - margin: 0; - font-family: Roboto, "Helvetica Neue", sans-serif; -} +/*body {*/ +/* margin: 0;*/ +/* font-family: Roboto, "Helvetica Neue", sans-serif;*/ +/*}*/