Skip to content

Commit

Permalink
feat: Add first implementation of environment status (#284)
Browse files Browse the repository at this point in the history
  • Loading branch information
StefanNemeth authored Feb 2, 2025
1 parent 17fe59e commit 0f549e7
Show file tree
Hide file tree
Showing 30 changed files with 757 additions and 45 deletions.
2 changes: 1 addition & 1 deletion .codacy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
exclude_paths:
- "**.spec.ts"
- "**/test/**"
- "**/test-setup.ts"
- "**/test-setup.ts"
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
<app-deployment-state-tag [state]="deployment.state || 'IN_PROGRESS'" />
}

@if (environment.latestStatus; as status) {
<app-environment-status-tag [status]="status" />
}

<div class="flex-grow"></div>

@if (environment.locked) {
Expand Down Expand Up @@ -61,9 +65,20 @@
</div>
</p-accordion-header>
<p-accordion-content>
@if (environment.latestDeployment; as deployment) {
<app-environment-deployment-info [deployment]="deployment" [repositoryId]="environment.repository?.id || 0" [installedApps]="environment.installedApps || []" />
}
<div class="flex justify-between max-w-6xl">
@if (environment.latestDeployment; as deployment) {
<app-environment-deployment-info
class="flex-grow max-w-2xl"
[deployment]="deployment"
[repositoryId]="environment.repository?.id || 0"
[installedApps]="environment.installedApps || []"
/>
}

@if (environment.latestStatus; as status) {
<app-environment-status-info class="flex-grow max-w-xs mt-2 ml-14" [status]="status" />
}
</div>

<div class="flex gap-4 items-center justify-between">
<div class="flex gap-1 mt-2 items-center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { DeploymentStateTagComponent } from '../deployment-state-tag/deployment-
import { LockTagComponent } from '../lock-tag/lock-tag.component';
import { LockTimeComponent } from '../lock-time/lock-time.component';
import { KeycloakService } from '@app/core/services/keycloak/keycloak.service';
import { EnvironmentStatusInfoComponent } from '../environment-status-info/environment-status-info.component';
import { EnvironmentStatusTagComponent } from '../environment-status-tag/environment-status-tag.component';

@Component({
selector: 'app-environment-list-view',
Expand All @@ -37,7 +39,9 @@ import { KeycloakService } from '@app/core/services/keycloak/keycloak.service';
IconsModule,
ButtonModule,
DeploymentStateTagComponent,
EnvironmentStatusTagComponent,
EnvironmentDeploymentInfoComponent,
EnvironmentStatusInfoComponent,
LockTimeComponent,
ConfirmDialogModule,
CommonModule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<div class="flex flex-col">
<span class="text-xs uppercase tracking-tighter font-bold text-gray-500 mb-2">Latest status check</span>

<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">Last Checked:</span>
<span class="text-sm text-gray-500">
{{ status().checkedAt | timeAgo }}
</span>
</div>
<div class="flex items-center justify-between mt-1">
<span class="text-sm font-medium text-gray-700">Status Code:</span>
<span class="text-sm text-gray-500">
{{ status().httpStatusCode || 'N/A' }}
</span>
</div>

@if (status().checkType === 'ARTEMIS_INFO') {
<span class="text-xs uppercase tracking-tighter text-gray-500 mt-3">Artemis Build</span>
@for (item of artemisBuildInfo(); track item.label) {
<div class="flex items-center justify-between mt-1">
<span class="text-sm font-medium text-gray-700">{{ item.label }}:</span>
<span class="text-sm text-gray-500">{{ item.value || '-/-' }}</span>
</div>
}
}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Component, computed, inject, input } from '@angular/core';
import { EnvironmentStatusDto } from '@app/core/modules/openapi';
import { DateService } from '@app/core/services/date.service';
import { TimeAgoPipe } from '@app/pipes/time-ago.pipe';

@Component({
selector: 'app-environment-status-info',
imports: [TimeAgoPipe],
templateUrl: './environment-status-info.component.html',
})
export class EnvironmentStatusInfoComponent {
status = input.required<EnvironmentStatusDto>();
dateService = inject(DateService);

artemisBuildInfo = computed<{ label: string; value?: string }[]>(() => {
const status = this.status();
const metadata = status.metadata as
| {
name?: string;
group?: string;
version?: string;
buildTime?: number;
}
| undefined;

return [
{
label: 'Name',
value: metadata?.name,
},
{
label: 'Group',
value: metadata?.group,
},
{
label: 'Version',
value: metadata?.version,
},
{
label: 'Build Time',
value: metadata?.buildTime ? this.dateService.formatDate(metadata.buildTime * 1000, 'yyyy-MM-dd HH:mm:ss') || '-/-' : undefined,
},
];
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@if (status(); as status) {
<!-- Info State -->
<!-- Success State -->
@if (status.success) {
<p-tag severity="success" [rounded]="true">
<i-tabler name="check" class="!h-4 !w-4 mr-0.5"></i-tabler>
Status check successful
</p-tag>
} @else {
<!-- Error State -->
<p-tag severity="danger" [rounded]="true">
<i-tabler name="exclamation-circle" class="!h-4 !w-4 mr-0.5"></i-tabler>
Status check failed
</p-tag>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Component, input } from '@angular/core';
import { EnvironmentStatusDto } from '@app/core/modules/openapi';
import { IconsModule } from 'icons.module';
import { TagModule } from 'primeng/tag';

@Component({
selector: 'app-environment-status-tag',
imports: [TagModule, IconsModule],
templateUrl: './environment-status-tag.component.html',
})
export class EnvironmentStatusTagComponent {
status = input.required<EnvironmentStatusDto>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@
<p-autoComplete id="installed-apps" formControlName="installedApps" [multiple]="true" [minLength]="1" fluid [typeahead]="false"> </p-autoComplete>
</div>

<div class="flex flex-col gap-1">
<label for="status-check-type">Status Check</label>
<span class="text-sm text-gray-500"> When enabled, the status check will run periodically to check the health of the environment. </span>
<p-select id="status-check-type" class="mt-2" [options]="statusCheckTypes" formControlName="statusCheckType"></p-select>
</div>

@if (environmentForm.get('statusCheckType')?.value !== null) {
<div class="flex flex-col gap-2">
<label for="status-check-url">Status URL</label>
<span class="text-sm text-gray-500">
This can be the URL of the Artemis management info endpoint (if set as the status check type) or any other URL that returns a 200 status code when the environment is
healthy.
</span>
<input pInputText id="status-check-url" type="text" formControlName="statusUrl" />
</div>
}

<!-- Environment Enable/Disable Checkbox -->
<div class="flex gap-2">
<label for="enabled" class="mr-2">Enabled</label>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, computed, effect, inject, input, OnInit } from '@angular/core';
import { Component, computed, inject, input, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonModule } from 'primeng/button';
import { InputSwitchModule } from 'primeng/inputswitch';
Expand All @@ -14,10 +14,12 @@ import {
} from '@app/core/modules/openapi/@tanstack/angular-query-experimental.gen';
import { MessageService } from 'primeng/api';
import { Checkbox } from 'primeng/checkbox';
import { SelectModule } from 'primeng/select';
import { toObservable } from '@angular/core/rxjs-interop';

@Component({
selector: 'app-environment-edit-form',
imports: [AutoCompleteModule, ReactiveFormsModule, InputTextModule, InputSwitchModule, ButtonModule, Checkbox],
imports: [AutoCompleteModule, ReactiveFormsModule, InputTextModule, InputSwitchModule, ButtonModule, Checkbox, SelectModule],
templateUrl: './environment-edit-form.component.html',
})
export class EnvironmentEditFormComponent implements OnInit {
Expand All @@ -27,15 +29,26 @@ export class EnvironmentEditFormComponent implements OnInit {
private route = inject(ActivatedRoute);

constructor(private messageService: MessageService) {
// This effect is needed, because the form is initialized before the data is fetched.
// This subscription is needed, because the form is initialized before the data is fetched.
// As soon as the data is fetched, the form is updated with the fetched data.
effect(() => {
if (this.environment()) {
this.environmentForm.patchValue(this.environment() || {});
//
// We used to call effect() here, but selecting an option within NgPrime Select
// for some reason triggers the effect to run and reset the form to the initial state.
toObservable(this.environment).subscribe(environment => {
if (!environment) {
return;
}

this.environmentForm.patchValue(environment);
});
}

statusCheckTypes = [
{ label: 'Disabled', value: null },
{ label: 'HTTP Status', value: 'HTTP_STATUS' },
{ label: 'Artemis Info', value: 'ARTEMIS_INFO' },
];

environmentId = input<number>(0); // This is the environment id
environmentForm!: FormGroup;

Expand Down Expand Up @@ -64,12 +77,16 @@ export class EnvironmentEditFormComponent implements OnInit {
environment = computed(() => this.environmentQuery.data());

ngOnInit(): void {
const environment = this.environment();

this.environmentForm = this.formBuilder.group({
name: [this.environment()?.name || '', Validators.required],
installedApps: [this.environment()?.installedApps || []],
description: [this.environment()?.description || ''],
serverUrl: [this.environment()?.serverUrl || ''],
enabled: [this.environment()?.enabled || false],
name: [environment?.name || '', Validators.required],
installedApps: [environment?.installedApps || []],
description: [environment?.description || ''],
serverUrl: [environment?.serverUrl || ''],
enabled: [environment?.enabled || false],
statusCheckType: [environment?.statusCheckType || null],
statusUrl: [environment?.statusUrl || ''],
});
}

Expand All @@ -79,7 +96,10 @@ export class EnvironmentEditFormComponent implements OnInit {

submitForm = () => {
if (this.environmentForm && this.environmentForm.valid) {
this.mutateEnvironment.mutate({ path: { id: this.environmentId() }, body: this.environmentForm.value });
this.mutateEnvironment.mutate({
path: { id: this.environmentId() },
body: this.environmentForm.value,
});
}
};
}
40 changes: 40 additions & 0 deletions client/src/app/core/modules/openapi/schemas.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,19 @@ export const EnvironmentDtoSchema = {
serverUrl: {
type: 'string',
},
statusCheckType: {
type: 'string',
enum: ['HTTP_STATUS', 'ARTEMIS_INFO'],
},
statusUrl: {
type: 'string',
},
latestDeployment: {
$ref: '#/components/schemas/EnvironmentDeployment',
},
latestStatus: {
$ref: '#/components/schemas/EnvironmentStatusDto',
},
lockedBy: {
type: 'string',
},
Expand All @@ -136,6 +146,36 @@ export const EnvironmentDtoSchema = {
required: ['id', 'name'],
} as const;

export const EnvironmentStatusDtoSchema = {
type: 'object',
properties: {
id: {
type: 'integer',
format: 'int64',
},
success: {
type: 'boolean',
},
httpStatusCode: {
type: 'integer',
format: 'int32',
},
checkedAt: {
type: 'string',
format: 'date-time',
},
checkType: {
type: 'string',
enum: ['HTTP_STATUS', 'ARTEMIS_INFO'],
},
metadata: {
type: 'object',
additionalProperties: {},
},
},
required: ['checkType', 'checkedAt', 'httpStatusCode', 'id', 'success'],
} as const;

export const RepositoryInfoDtoSchema = {
type: 'object',
properties: {
Expand Down
12 changes: 12 additions & 0 deletions client/src/app/core/modules/openapi/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,23 @@ export type EnvironmentDto = {
installedApps?: Array<string>;
description?: string;
serverUrl?: string;
statusCheckType?: 'HTTP_STATUS' | 'ARTEMIS_INFO';
statusUrl?: string;
latestDeployment?: EnvironmentDeployment;
latestStatus?: EnvironmentStatusDto;
lockedBy?: string;
lockedAt?: string;
};

export type EnvironmentStatusDto = {
id: number;
success: boolean;
httpStatusCode: number;
checkedAt: string;
checkType: 'HTTP_STATUS' | 'ARTEMIS_INFO';
metadata?: {};
};

export type RepositoryInfoDto = {
id: number;
name: string;
Expand Down
2 changes: 1 addition & 1 deletion client/src/app/core/services/date.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { inject, Injectable } from '@angular/core';
export class DateService {
private datePipe = inject(DatePipe);

formatDate = (date?: string, formatString: string = 'd. MMMM y'): string | null => {
formatDate = (date?: Date | string | number, formatString: string = 'd. MMMM y'): string | null => {
return date ? this.datePipe.transform(date, formatString) : null;
};
}
1 change: 0 additions & 1 deletion client/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ Sentry.init({
enabled: environment.sentry.enabled,
// The DSN (Data Source Name) tells the SDK where to send the events to.
dsn: environment.sentry.dsn,
debug: true,
// The browser tracing integration captures performance data
// like throughput and latency
integrations: [Sentry.browserTracingIntegration()],
Expand Down
Loading

0 comments on commit 0f549e7

Please sign in to comment.