Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add selecting a license for a deployment #463

Merged
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
13 changes: 7 additions & 6 deletions api/deployment_target.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ type DeploymentTargetAccessTokenResponse struct {
}

type DeploymentRequest struct {
ID uuid.UUID `json:"deploymentId"`
DeploymentTargetID uuid.UUID `json:"deploymentTargetId"`
ApplicationVersionID uuid.UUID `json:"applicationVersionId"`
ReleaseName *string `json:"releaseName"`
ValuesYaml []byte `json:"valuesYaml"`
EnvFileData []byte `json:"envFileData"`
DeploymentID *uuid.UUID `json:"deploymentId"`
DeploymentTargetID uuid.UUID `json:"deploymentTargetId"`
ApplicationVersionID uuid.UUID `json:"applicationVersionId"`
ApplicationLicenseID *uuid.UUID `json:"applicationLicenseId"`
ReleaseName *string `json:"releaseName"`
ValuesYaml []byte `json:"valuesYaml"`
EnvFileData []byte `json:"envFileData"`
}

func (d DeploymentRequest) ParsedValuesFile() (result map[string]any, err error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,86 +200,7 @@ <h3 class="font-normal text-gray-900 dark:text-white">
<div class="space-y-6 mt-4">
<div class="space-y-4">
<h3 class="font-normal text-gray-900 dark:text-white">Deploy</h3>
<div class="grid gap-4 mb-4 grid-cols-2">
<div class="col-span-2 sm:col-span-1">
<label for="applicationId" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>Application</label
>
<select
id="applicationId"
[formControl]="deployForm.controls.applicationId"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
@for (app of applications$ | async; track app.id) {
<option [value]="app.id">{{ app.name }}</option>
}
</select>
@if (deployForm.controls.applicationId.invalid && deployForm.controls.applicationId.touched) {
<p class="mt-1 text-sm text-red-600 dark:text-red-500">Field is required.</p>
}
</div>
<div class="col-span-2 sm:col-span-1">
<label for="applicationVersion" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>Version</label
>
<select
id="applicationVersion"
[formControl]="deployForm.controls.applicationVersionId"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
@if (selectedApplication?.versions) {
@for (version of selectedApplication?.versions; track version.id) {
<option [value]="version.id">{{ version.name }}</option>
}
}
</select>
@if (
deployForm.controls.applicationVersionId.invalid &&
deployForm.controls.applicationVersionId.touched
) {
<p class="mt-1 text-sm text-red-600 dark:text-red-500">Field is required.</p>
}
</div>
@if (deployForm.controls.releaseName.enabled) {
<div class="col-span-2">
<label for="release-name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Helm relase name
</label>
<input
type="text"
id="release-name"
autotrim
[formControl]="deployForm.controls.releaseName"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" />
</div>
@if (deployForm.controls.releaseName.invalid && deployForm.controls.releaseName.touched) {
<p class="mt-1 text-sm text-red-600 dark:text-red-500">Field is required.</p>
}
}
@if (deployForm.controls.valuesYaml.enabled) {
<div class="col-span-2">
<label for="helm-values" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Helm values
</label>

<app-yaml-editor
class="block p-2.5 w-full font-mono text-sm text-gray-900 caret-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:caret-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
[formControl]="deployForm.controls.valuesYaml">
</app-yaml-editor>
</div>
}
@if (deployForm.controls.envFileData.enabled) {
<div class="col-span-2">
<label for="env-file" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Environment values
</label>

<app-yaml-editor
id="env-file"
class="block p-2.5 w-full font-mono text-sm text-gray-900 caret-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:caret-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
[formControl]="deployForm.controls.envFileData">
</app-yaml-editor>
</div>
}
</div>
<app-deployment-form [formControl]="deployForm"></app-deployment-form>
<div class="space-y-4 sm:flex sm:space-x-4 sm:space-y-0">
<div class="w-full"></div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import {CdkStep, CdkStepper} from '@angular/cdk/stepper';
import {AsyncPipe} from '@angular/common';
import {Component, EventEmitter, inject, OnDestroy, OnInit, Output, signal, ViewChild} from '@angular/core';
import {toObservable} from '@angular/core/rxjs-interop';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {faShip, faXmark} from '@fortawesome/free-solid-svg-icons';
import {combineLatest, firstValueFrom, map, of, Subject, switchMap, takeUntil, tap, withLatestFrom} from 'rxjs';
import {getFormDisplayedError} from '../../../util/errors';
import {modalFlyInOut} from '../../animations/modal';
import {ConnectInstructionsComponent} from '../connect-instructions/connect-instructions.component';
import {ApplicationsService} from '../../services/applications.service';
import {DeploymentTargetsService} from '../../services/deployment-targets.service';
import {DeploymentStatusService} from '../../services/deployment-status.service';
import {ToastService} from '../../services/toast.service';
import {YamlEditorComponent} from '../yaml-editor.component';
import {InstallationWizardStepperComponent} from './installation-wizard-stepper.component';
import {AutotrimDirective} from '../../directives/autotrim.directive';
import {
Application,
ApplicationVersion,
DeploymentRequest,
DeploymentTarget,
DeploymentTargetScope,
DeploymentType,
} from '@glasskube/distr-sdk';
import {combineLatest, firstValueFrom, map, Subject, takeUntil} from 'rxjs';
import {getFormDisplayedError} from '../../../util/errors';
import {modalFlyInOut} from '../../animations/modal';
import {DeploymentFormComponent, DeploymentFormValue} from '../../deployment-form/deployment-form.component';
import {AutotrimDirective} from '../../directives/autotrim.directive';
import {ApplicationsService} from '../../services/applications.service';
import {DeploymentTargetsService} from '../../services/deployment-targets.service';
import {FeatureFlagService} from '../../services/feature-flag.service';
import {ToastService} from '../../services/toast.service';
import {ConnectInstructionsComponent} from '../connect-instructions/connect-instructions.component';
import {InstallationWizardStepperComponent} from './installation-wizard-stepper.component';

@Component({
selector: 'app-installation-wizard',
Expand All @@ -32,10 +32,9 @@ import {
FaIconComponent,
InstallationWizardStepperComponent,
CdkStep,
AsyncPipe,
ConnectInstructionsComponent,
YamlEditorComponent,
AutotrimDirective,
DeploymentFormComponent,
],
animations: [modalFlyInOut],
})
Expand All @@ -46,6 +45,7 @@ export class InstallationWizardComponent implements OnInit, OnDestroy {
private readonly toast = inject(ToastService);
private readonly applications = inject(ApplicationsService);
private readonly deploymentTargets = inject(DeploymentTargetsService);
protected readonly featureFlags = inject(FeatureFlagService);

@ViewChild('stepper') private stepper?: CdkStepper;

Expand All @@ -67,71 +67,20 @@ export class InstallationWizardComponent implements OnInit, OnDestroy {

readonly agentForm = new FormGroup({});

readonly deployForm = new FormGroup({
deploymentTargetId: new FormControl<string | undefined>(undefined, Validators.required),
applicationId: new FormControl<string | undefined>(undefined, Validators.required),
applicationVersionId: new FormControl<string | undefined>({value: undefined, disabled: true}, Validators.required),
valuesYaml: new FormControl<string>({value: '', disabled: true}),
releaseName: new FormControl<string>({value: '', disabled: true}, Validators.required),
envFileData: new FormControl<string>({value: '', disabled: true}),
});
readonly deployForm = new FormControl<DeploymentFormValue | undefined>(undefined, Validators.required);

protected selectedApplication?: Application;
protected availableApplicationVersions = signal<ApplicationVersion[]>([]);
protected selectedDeploymentTarget = signal<DeploymentTarget | null>(null);

readonly applications$ = combineLatest([this.applications.list(), toObservable(this.selectedDeploymentTarget)]).pipe(
map(([apps, dt]) => apps.filter((app) => app.type === dt?.type))
);

private loading = false;
private readonly destroyed$ = new Subject<void>();

ngOnInit() {
this.deployForm.controls.applicationId.valueChanges
.pipe(
takeUntil(this.destroyed$),
withLatestFrom(this.applications$),
tap(([selected, apps]) => this.updatedSelectedApplication(apps, selected))
)
.subscribe(() => {
if (this.selectedApplication && (this.selectedApplication.versions ?? []).length > 0) {
const versions = this.selectedApplication.versions!;
this.deployForm.controls.applicationVersionId.patchValue(versions[versions.length - 1].id);
} else {
this.deployForm.controls.applicationVersionId.reset();
}
});
this.deployForm.controls.applicationVersionId.valueChanges
.pipe(
takeUntil(this.destroyed$),
switchMap((id) =>
this.applications
.getTemplateFile(this.selectedApplication?.id!, id!)
.pipe(map((data) => [this.selectedApplication?.type, data]))
)
)
.subscribe(([type, templateFile]) => {
if (type === 'kubernetes') {
this.deployForm.controls.releaseName.enable();
this.deployForm.controls.valuesYaml.enable();
this.deployForm.controls.envFileData.disable();
this.deployForm.patchValue({valuesYaml: templateFile});
if (!this.deployForm.value.releaseName) {
const releaseName = this.selectedDeploymentTarget()?.name.trim().toLowerCase().replaceAll(/\W+/g, '-');
this.deployForm.patchValue({releaseName});
}
} else {
this.deployForm.controls.envFileData.enable();
this.deployForm.controls.envFileData.patchValue(templateFile ?? '');
this.deployForm.controls.releaseName.disable();
this.deployForm.controls.valuesYaml.disable();
}
});
this.deployForm.controls.applicationId.statusChanges.pipe(takeUntil(this.destroyed$)).subscribe((s) => {
if (s === 'VALID') {
this.deployForm.controls.applicationVersionId.enable();
} else {
this.deployForm.controls.applicationVersionId.disable();
}
});
this.deploymentTargetForm.controls.type.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe((s) => {
if (s === 'kubernetes') {
this.deploymentTargetForm.controls.namespace.enable();
Expand All @@ -143,6 +92,7 @@ export class InstallationWizardComponent implements OnInit, OnDestroy {
this.deploymentTargetForm.controls.scope.disable();
}
});

this.deploymentTargetForm.controls.clusterScope.valueChanges
.pipe(takeUntil(this.destroyed$))
.subscribe((value) => this.deploymentTargetForm.controls.scope.setValue(value ? 'cluster' : 'namespace'));
Expand Down Expand Up @@ -189,6 +139,9 @@ export class InstallationWizardComponent implements OnInit, OnDestroy {
})
);
this.selectedDeploymentTarget.set(created as DeploymentTarget);
this.deployForm.setValue({
deploymentTargetId: created.id,
});
this.nextStep();
} catch (e) {
const msg = getFormDisplayedError(e);
Expand All @@ -201,18 +154,14 @@ export class InstallationWizardComponent implements OnInit, OnDestroy {
}

private async continueFromDeployStep() {
this.deployForm.patchValue({
deploymentTargetId: this.selectedDeploymentTarget()!.id,
});

this.deployForm.markAllAsTouched();
if (!this.deployForm.valid || this.loading) {
return;
}

try {
this.loading = true;
const deployment = this.deployForm.value;
const deployment = this.deployForm.value!;
if (deployment.valuesYaml) {
deployment.valuesYaml = btoa(deployment.valuesYaml);
} else {
Expand Down Expand Up @@ -244,8 +193,4 @@ export class InstallationWizardComponent implements OnInit, OnDestroy {
this.loading = false;
this.stepper?.next();
}

updatedSelectedApplication(applications: Application[], applicationId?: string | null) {
this.selectedApplication = applications.find((a) => a.id === applicationId);
}
}
Loading
Loading