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 environment key #282

Merged
merged 8 commits into from
Mar 17, 2023
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: 13 additions & 0 deletions modules/back-end/src/Api/Controllers/EnvironmentController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,17 @@ public async Task<ApiResponse<bool>> DeleteAsync(Guid id)
var success = await Mediator.Send(request);
return Ok(success);
}

[HttpGet("is-key-used")]
public async Task<ApiResponse<bool>> IsKeyUsedAsync(Guid projectId, string key)
{
var request = new IsKeyUsed
{
ProjectId = projectId,
Key = key
};

var isUsed = await Mediator.Send(request);
return Ok(isUsed);
}
}
4 changes: 2 additions & 2 deletions modules/back-end/src/Application/Bases/ErrorCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public static class ErrorCodes

// common
public const string NameIsRequired = nameof(NameIsRequired);

public const string KeyIsRequired = nameof(KeyIsRequired);

// onboarding
public const string OrganizationNameRequired = nameof(OrganizationNameRequired);
public const string ProjectNameRequired = nameof(ProjectNameRequired);
Expand All @@ -47,7 +48,6 @@ public static class ErrorCodes
public const string InvalidVariationType = nameof(InvalidVariationType);
public const string FeatureFlagIdIsRequired = nameof(FeatureFlagIdIsRequired);
public const string FeatureFlagVariationIdIsRequired = nameof(FeatureFlagVariationIdIsRequired);
public const string FeatureFlagKeyIsRequired = nameof(FeatureFlagKeyIsRequired);
public const string InvalidIntervalType = nameof(InvalidIntervalType);
public const string InvalidFrom = nameof(InvalidFrom);
public const string InvalidTo = nameof(InvalidTo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class GetFeatureFlagEndUserListValidator : AbstractValidator<GetFeatureFl
public GetFeatureFlagEndUserListValidator()
{
RuleFor(x => x.Filter.FeatureFlagKey)
.NotEmpty().WithErrorCode(ErrorCodes.FeatureFlagKeyIsRequired);
.NotEmpty().WithErrorCode(ErrorCodes.KeyIsRequired);

RuleFor(x => x.Filter.From)
.GreaterThan(0).WithErrorCode(ErrorCodes.InvalidFrom);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public class CreateEnvironment : IRequest<EnvironmentVm>

public string Name { get; set; }

public string Key { get; set; }

public string Description { get; set; }
}

Expand All @@ -18,6 +20,9 @@ public CreateEnvironmentValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithErrorCode(ErrorCodes.NameIsRequired);

RuleFor(x => x.Key)
.NotEmpty().WithErrorCode(ErrorCodes.KeyIsRequired);
}
}

Expand All @@ -36,7 +41,7 @@ public CreateEnvironmentHandler(IEnvironmentService service, IMapper mapper, IEn

public async Task<EnvironmentVm> Handle(CreateEnvironment request, CancellationToken cancellationToken)
{
var env = new Environment(request.ProjectId, request.Name, request.Description);
var env = new Environment(request.ProjectId, request.Name, request.Key, request.Description);
await _service.AddOneAsync(env);

// add env built-in end-user properties
Expand Down
26 changes: 26 additions & 0 deletions modules/back-end/src/Application/Environments/IsKeyUsed.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Application.Environments;

public class IsKeyUsed : IRequest<bool>
{
public Guid ProjectId { get; set; }

public string Key { get; set; }
}

public class IsKeyUsedHandler : IRequestHandler<IsKeyUsed, bool>
{
private readonly IEnvironmentService _service;

public IsKeyUsedHandler(IEnvironmentService service)
{
_service = service;
}

public async Task<bool> Handle(IsKeyUsed request, CancellationToken cancellationToken)
{
return await _service.AnyAsync(x =>
x.ProjectId == request.ProjectId &&
string.Equals(x.Key, request.Key, StringComparison.OrdinalIgnoreCase)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public CreateFeatureFlagValidator()
.NotEmpty().WithErrorCode(ErrorCodes.NameIsRequired);

RuleFor(x => x.Key)
.NotEmpty().WithErrorCode(ErrorCodes.FeatureFlagKeyIsRequired)
.NotEmpty().WithErrorCode(ErrorCodes.KeyIsRequired)
.Matches(FeatureFlag.KeyFormat).WithErrorCode(ErrorCodes.InvalidFlagKeyFormat);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class GetInsightsValidator : AbstractValidator<GetInsights>
public GetInsightsValidator()
{
RuleFor(x => x.Filter.FeatureFlagKey)
.NotEmpty().WithErrorCode(ErrorCodes.FeatureFlagKeyIsRequired);
.NotEmpty().WithErrorCode(ErrorCodes.KeyIsRequired);

RuleFor(x => x.Filter.IntervalType)
.Must(IntervalType.IsDefined).WithErrorCode(ErrorCodes.InvalidIntervalType);
Expand Down
5 changes: 4 additions & 1 deletion modules/back-end/src/Domain/Environments/Environment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ public class Environment : AuditedEntity

public string Name { get; set; }

public string Key { get; set; }

public string Description { get; set; }

public ICollection<Secret> Secrets { get; set; }

public ICollection<Setting> Settings { get; set; }

public Environment(Guid projectId, string name, string description = "")
public Environment(Guid projectId, string name, string key, string description = "")
{
Id = Guid.NewGuid();

ProjectId = projectId;
Name = name;
Key = key;
Description = description;
Secrets = new List<Secret>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public async Task<ProjectWithEnvs> AddWithEnvsAsync(Project project, IEnumerable
{
await MongoDb.CollectionOf<Project>().InsertOneAsync(project);

var envs = envNames.Select(envName => new Environment(project.Id, envName)).ToList();
var envs = envNames.Select(envName => new Environment(project.Id, envName, envName.ToLower())).ToList();
await MongoDb.CollectionOf<Environment>().InsertManyAsync(envs);

// add env built-in end-user properties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,23 @@
<nz-form-item>
<nz-form-label nzRequired i18n="@@common.name">Name</nz-form-label>
<nz-form-control nzErrorTip="Environment name is mandatory!" i18n-nzErrorTip="@@org.project.envNameMandatory">
<input type="text" nz-input formControlName="name" placeholder="Name" i18n-placeholder="@@common.name"/>
<input type="text" nz-input (ngModelChange)="nameChange($event)" formControlName="name" placeholder="Name" i18n-placeholder="@@common.name"/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label
nzRequired
i18n-nzTooltipTitle="@@org.project.env-key-description"
nzTooltipTitle="We use the key to give you friendly URLs. Keys should only contain letters, numbers, ., _ or -.">
Key
</nz-form-label>
<nz-form-control nzHasFeedback i18n-nzValidatingTip="@@common.validating" nzValidatingTip="Validating..." [nzErrorTip]="keyErrorTpl">
<input nz-input formControlName="key" i18n-placeholder="@@common.key-generated-from-name" placeholder="Key can be auto-generated from name" />
<ng-template #keyErrorTpl let-control>
<ng-container *ngIf="control.hasError('required')" i18n="@@common.key-cannot-be-empty">Key cannot be empty</ng-container>
<ng-container *ngIf="control.hasError('duplicated')" i18n="@@common.key-has-been-used">This key has been used</ng-container>
<ng-container *ngIf="control.hasError('unknown')" i18n="@@common.key-validation-failed">Key validation failed</ng-container>
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { NzMessageService } from 'ng-zorro-antd/message';
import { IEnvironment } from '@shared/types';
import { EnvService } from '@services/env.service';
import { ProjectService } from "@services/project.service";
import { generalResourceRNPattern, permissionActions } from "@shared/permissions";
import { PermissionsService } from "@services/permissions.service";
import { debounceTime, first, map, switchMap } from "rxjs/operators";
import { slugify } from "@utils/index";

@Component({
selector: 'app-env-drawer',
templateUrl: './env-drawer.component.html',
styleUrls: ['./env-drawer.component.less']
})
export class EnvDrawerComponent implements OnInit {
export class EnvDrawerComponent {

private _env: IEnvironment;

Expand All @@ -27,9 +29,11 @@ export class EnvDrawerComponent implements OnInit {
this.isEditing = env && !!env.id;
if (this.isEditing) {
this.title = $localize `:@@org.project.editEnv:Edit environment`;
this.initForm(true);
this.patchForm(env);
} else {
this.title = $localize `:@@org.project.addEnv:Add environment`;
this.initForm(false);
this.resetForm();
}
this._env = env;
Expand All @@ -50,22 +54,45 @@ export class EnvDrawerComponent implements OnInit {
private message: NzMessageService,
private projectSrv: ProjectService,
private permissionsService: PermissionsService
) { }

ngOnInit(): void {
this.initForm();
) {
}

initForm() {
initForm(isKeyDisabled: boolean) {
this.envForm = this.fb.group({
name: [null, [Validators.required]],
key: [{ disabled: isKeyDisabled, value: null }, Validators.required, this.keyAsyncValidator],
description: [null],
});
}

nameChange(name: string) {
if (this.isEditing) return;

let keyControl = this.envForm.get('key')!;
keyControl.setValue(slugify(name ?? ''));
keyControl.markAsDirty();
}

keyAsyncValidator = (control: FormControl) => control.valueChanges.pipe(
debounceTime(300),
switchMap(value => this.envService.isKeyUsed(this.env.projectId, value as string)),
map(isKeyUsed => {
switch (isKeyUsed) {
case true:
return { error: true, duplicated: true };
case undefined:
return { error: true, unknown: true };
default:
return null;
}
}),
first()
);

patchForm(env: Partial<IEnvironment>) {
this.envForm.patchValue({
name: env.name,
key: env.key,
description: env.description,
});
}
Expand All @@ -89,7 +116,7 @@ export class EnvDrawerComponent implements OnInit {

this.isLoading = true;

const { name, description } = this.envForm.value;
const { name, key, description } = this.envForm.value;
const projectId = this.env.projectId;

if (this.isEditing) {
Expand All @@ -101,20 +128,20 @@ export class EnvDrawerComponent implements OnInit {
.subscribe(
({id, name, description, secrets}) => {
this.isLoading = false;
this.close.emit({isEditing: true, env: { name, description, id, projectId, secrets }});
this.close.emit({isEditing: true, env: { name, description, id, key, projectId, secrets }});
this.message.success($localize `:@@org.project.envUpdateSuccess:Environment successfully updated`);
},
() => {
this.isLoading = false;
}
);
} else {
this.envService.postCreateEnv(this.env.projectId, { name, description, projectId })
this.envService.postCreateEnv(this.env.projectId, { name, key, description, projectId })
.pipe()
.subscribe(
({id, name, description, secrets}) => {
this.isLoading = false;
this.close.emit({isEditing: false, env: { name, description, id, projectId, secrets }});
this.close.emit({isEditing: false, env: { name, description, id, key, projectId, secrets }});
this.message.success($localize `:@@org.project.envCreateSuccess:Environment successfully created`);
},
() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
<nz-list class="overflow-y-list" style="--list-height: 320px" [nzHeader]="projectHeader">
<nz-list-item [ngClass]="{'item-selected': selectedProject?.id === project.id}"
*ngFor="let project of availableProjects" (click)="onSelectProject(project)">
{{ project.name }}
{{ project.name }} <nz-tag nzColor="green" *ngIf="isCurrentProject(project)" i18n="@@common.current">Current</nz-tag>
</nz-list-item>
<ng-template #projectHeader>
<span class="header" i18n="@@common.projects">Projects</span>
Expand All @@ -67,7 +67,7 @@
<nz-list class="overflow-y-list" style="--list-height: 320px" [nzHeader]="envHeader">
<nz-list-item [ngClass]="{'item-selected': selectedEnv?.id === env.id}" *ngFor="let env of availableEnvs"
(click)="onSelectEnv(env)">
{{ env.name }}
{{ env.name }} <nz-tag nzColor="green" *ngIf="isCurrentEnv(env)" i18n="@@common.current">Current</nz-tag>
</nz-list-item>
<ng-template #envHeader>
<span class="header" i18n="@@common.environments">Environments</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@
}
}

.ant-tag {
font-size: 12px;
border-radius: 8px;
white-space: nowrap;
padding: 2px 8px;
}

.subscription-plan-wrapper {
margin-right: 16px;
cursor: pointer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ export class HeaderComponent implements OnInit {
});
}

isCurrentProject(project: IProject): boolean {
return this.currentProjectEnv?.projectId === project.id;
}

isCurrentEnv(env: IEnvironment): boolean {
return this.currentProjectEnv?.envId === env.id;
}

canListProjects = false;
get availableProjects() {
return this.canListProjects ? this.allProjects : [];
Expand Down Expand Up @@ -104,6 +112,7 @@ export class HeaderComponent implements OnInit {
projectId: this.selectedProject.id,
projectName: this.selectedProject.name,
envId: this.selectedEnv.id,
envKey: this.selectedEnv.key,
envName: this.selectedEnv.name,
envSecret: this.selectedEnv.secrets[0].value
};
Expand Down
9 changes: 8 additions & 1 deletion modules/front-end/src/app/core/services/env.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Observable, of } from 'rxjs';
import { environment } from 'src/environments/environment';
import { IEnvironment } from '@shared/types';
import { catchError } from "rxjs/operators";

@Injectable({
providedIn: 'root'
Expand Down Expand Up @@ -32,4 +33,10 @@ export class EnvService {
const url = this.baseUrl.replace(/#projectId/ig, `${projectId}`) + `/${envId}`;
return this.http.delete(url);
}

isKeyUsed(projectId: string, key: string): Observable<boolean> {
const url = this.baseUrl.replace(/#projectId/ig, `${projectId}`) + `/is-key-used?key=${key}`;

return this.http.get<boolean>(url).pipe(catchError(() => of(undefined)));
}
}
Loading