feat: show all available organizations when creating project grants (#6040)

* feat: show available orgs (project) grants

* feat: add e2e for project grant

* feat: add bulgarian missing translations

* feat: update docs

* fix: add @peintnermax suggested changes

---------

Co-authored-by: Max Peintner <max@caos.ch>
This commit is contained in:
Miguel Cabrerizo
2023-07-18 08:45:34 +02:00
committed by GitHub
parent e1b3cda98a
commit 7b44209bfd
22 changed files with 328 additions and 70 deletions

View File

@@ -16,6 +16,7 @@
color="primary"
class="cnsl-action-button"
mat-raised-button
data-e2e="create-project-role-button"
>
<mat-icon data-e2e="add-new-role" class="icon">add</mat-icon>
<span>{{ 'ACTIONS.NEW' | translate }}</span>

View File

@@ -0,0 +1,32 @@
<form>
<cnsl-form-field class="full-width">
<cnsl-label>{{ 'PROJECT.GRANT.CREATE.SEL_ORG_FORMFIELD' | translate }}</cnsl-label>
<input
cnslInput
type="text"
placeholder="Organization XY"
#nameInput
[formControl]="myControl"
[matAutocomplete]="auto"
data-e2e="add-org-input"
/>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" [displayWith]="displayFn">
<mat-option *ngIf="isLoading" class="is-loading">
<mat-spinner diameter="30"></mat-spinner>
</mat-option>
<mat-option *ngFor="let org of filteredOrgs" [value]="org">
<div class="org-option" data-e2e="org-option">
<div class="org-option-column">
<span>{{ org.name }}</span>
<span class="fill-space"></span>
<span class="smaller cnsl-secondary-text">{{ org.primaryDomain }}</span>
</div>
</div>
</mat-option>
</mat-autocomplete>
<span class="org-autocomplete-target-desc">
{{ 'PROJECT.GRANT.CREATE.SEL_ORG_DESC' | translate }}
</span>
</cnsl-form-field>
</form>

View File

@@ -0,0 +1,38 @@
.full-width {
width: 100%;
}
input {
max-width: 500px;
}
.org-option {
display: flex;
align-items: center;
.org-option-column {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
width: 100%;
span {
line-height: normal;
}
.fill-space {
flex: 1;
}
.smaller {
font-size: 13px;
}
}
}
.org-autocomplete-target-desc {
font-size: 14px;
display: block;
margin-top: 0.5rem;
}

View File

@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { SearchOrgAutocompleteComponent } from './search-org-autocomplete.component';
describe('SearchOrgComponent', () => {
let component: SearchOrgAutocompleteComponent;
let fixture: ComponentFixture<SearchOrgAutocompleteComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [SearchOrgAutocompleteComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SearchOrgAutocompleteComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,83 @@
import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { UntypedFormControl } from '@angular/forms';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatLegacyAutocomplete as MatAutocomplete } from '@angular/material/legacy-autocomplete';
import { debounceTime, from, map, Subject, switchMap, takeUntil, tap } from 'rxjs';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import { Org, OrgNameQuery, OrgQuery, OrgState, OrgStateQuery } from 'src/app/proto/generated/zitadel/org_pb';
import { AuthenticationService } from 'src/app/services/authentication.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
@Component({
selector: 'cnsl-search-org-autocomplete',
templateUrl: './search-org-autocomplete.component.html',
styleUrls: ['./search-org-autocomplete.component.scss'],
})
export class SearchOrgAutocompleteComponent implements OnInit, OnDestroy {
public selectable: boolean = true;
public myControl: UntypedFormControl = new UntypedFormControl();
public filteredOrgs: Array<Org.AsObject> = [];
public isLoading: boolean = false;
@ViewChild('auto') public matAutocomplete!: MatAutocomplete;
@Output() public selectionChanged: EventEmitter<Org.AsObject> = new EventEmitter();
private unsubscribed$: Subject<void> = new Subject();
constructor(public authService: AuthenticationService, private auth: GrpcAuthService) {
this.myControl.valueChanges
.pipe(
takeUntil(this.unsubscribed$),
debounceTime(200),
tap(() => (this.isLoading = true)),
switchMap((value) => {
const stateQuery = new OrgQuery();
const orgStateQuery = new OrgStateQuery();
orgStateQuery.setState(OrgState.ORG_STATE_ACTIVE);
stateQuery.setStateQuery(orgStateQuery);
let queries: OrgQuery[] = [stateQuery];
if (value) {
const nameQuery = new OrgQuery();
const orgNameQuery = new OrgNameQuery();
orgNameQuery.setName(value);
orgNameQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
nameQuery.setNameQuery(orgNameQuery);
queries = [stateQuery, nameQuery];
}
return from(this.auth.listMyProjectOrgs(undefined, 0, queries)).pipe(
map((resp) => {
return resp.resultList.sort((left, right) => left.name.localeCompare(right.name));
}),
);
}),
)
.subscribe((returnValue) => {
this.isLoading = false;
this.filteredOrgs = returnValue;
});
}
public ngOnInit(): void {
const query = new OrgQuery();
const orgStateQuery = new OrgStateQuery();
orgStateQuery.setState(OrgState.ORG_STATE_ACTIVE);
query.setStateQuery(orgStateQuery);
this.auth.listMyProjectOrgs(undefined, 0, [query]).then((orgs) => {
this.filteredOrgs = orgs.resultList;
});
}
public ngOnDestroy(): void {
this.unsubscribed$.next();
}
public displayFn(org?: Org.AsObject): string {
return org ? `${org.name}` : '';
}
public selected(event: MatAutocompleteSelectedEvent): void {
this.selectionChanged.emit(event.option.value);
}
}

View File

@@ -0,0 +1,32 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { MatLegacyAutocompleteModule as MatAutocompleteModule } from '@angular/material/legacy-autocomplete';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips';
import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner';
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
import { TranslateModule } from '@ngx-translate/core';
import { InputModule } from 'src/app/modules/input/input.module';
import { SearchOrgAutocompleteComponent } from './search-org-autocomplete.component';
@NgModule({
declarations: [SearchOrgAutocompleteComponent],
imports: [
CommonModule,
MatAutocompleteModule,
MatChipsModule,
MatButtonModule,
InputModule,
MatIconModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
FormsModule,
TranslateModule,
MatSelectModule,
],
exports: [SearchOrgAutocompleteComponent],
})
export class SearchOrgAutocompleteModule {}

View File

@@ -6,20 +6,8 @@
>
<ng-container *ngIf="currentCreateStep === 1">
<h1>{{ 'PROJECT.GRANT.CREATE.SEL_ORG' | translate }}</h1>
<p>{{ 'PROJECT.GRANT.CREATE.SEL_ORG_DESC' | translate }}</p>
<form (ngSubmit)="searchOrg(domain.value)">
<cnsl-form-field class="org-domain">
<cnsl-label>{{ 'PROJECT.GRANT.CREATE.SEL_ORG_FORMFIELD' | translate }}</cnsl-label>
<input cnslInput #domain />
</cnsl-form-field>
<button [disabled]="domain.value.length === 0" color="primary" type="submit" class="domain-button" mat-raised-button>
{{ 'PROJECT.GRANT.CREATE.SEL_ORG_BUTTON' | translate }}
</button>
</form>
<span *ngIf="org"> {{ 'PROJECT.GRANT.CREATE.FOR_ORG' | translate }} {{ org.name }} </span>
<cnsl-search-org-autocomplete class="block" (selectionChanged)="selectOrg($event)"> </cnsl-search-org-autocomplete>
<span *ngIf="org"> {{ 'PROJECT.GRANT.CREATE.FOR_ORG' | translate }} {{ org.name }} - {{ org.primaryDomain }} </span>
</ng-container>
<ng-container *ngIf="currentCreateStep === 2">
@@ -37,7 +25,15 @@
<div class="btn-container">
<ng-container *ngIf="currentCreateStep === 1">
<button [disabled]="!org" (click)="next()" color="primary" mat-raised-button class="big-button" cdkFocusInitial>
<button
[disabled]="!org"
(click)="next()"
color="primary"
mat-raised-button
class="big-button"
cdkFocusInitial
data-e2e="project-grant-continue"
>
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</ng-container>
@@ -46,7 +42,15 @@
<button (click)="previous()" mat-stroked-button class="small-button">
{{ 'ACTIONS.BACK' | translate }}
</button>
<button color="primary" [disabled]="!org" (click)="addGrant()" mat-raised-button class="big-button" cdkFocusInitial>
<button
color="primary"
[disabled]="!org"
(click)="addGrant()"
mat-raised-button
class="big-button"
cdkFocusInitial
data-e2e="save-project-grant-button"
>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</ng-container>

View File

@@ -88,6 +88,10 @@ export class ProjectGrantCreateComponent implements OnInit, OnDestroy {
}
}
public selectOrg(org: Org.AsObject): void {
this.org = org;
}
public selectRoles(roles: string[]): void {
this.rolesKeyList = roles;
}

View File

@@ -15,6 +15,7 @@ import { InputModule } from 'src/app/modules/input/input.module';
import { ProjectRolesTableModule } from 'src/app/modules/project-roles-table/project-roles-table.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { SearchOrgAutocompleteModule } from 'src/app/modules/search-org-autocomplete/search-org-autocomplete.module';
import { ProjectGrantCreateRoutingModule } from './project-grant-create-routing.module';
import { ProjectGrantCreateComponent } from './project-grant-create.component';
@@ -38,6 +39,7 @@ import { ProjectGrantCreateComponent } from './project-grant-create.component';
MatProgressSpinnerModule,
FormsModule,
TranslateModule,
SearchOrgAutocompleteModule,
],
})
export default class ProjectGrantCreateModule {}

View File

@@ -17,6 +17,7 @@
class="cnsl-action-button"
mat-raised-button
[routerLink]="['/projects', projectId, 'projectgrants', 'create']"
data-e2e="create-project-grant-button"
>
<mat-icon class="icon">add</mat-icon>
<span>{{ 'ACTIONS.NEW' | translate }}</span>

View File

@@ -12,15 +12,15 @@
<div class="newrole">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'PROJECT.ROLE.KEY' | translate }}</cnsl-label>
<input cnslInput formControlName="key" />
<input cnslInput formControlName="key" data-e2e="role-key-input" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'PROJECT.ROLE.DISPLAY_NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="displayName" />
<input cnslInput formControlName="displayName" data-e2e="role-display-name-input" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'PROJECT.ROLE.GROUP' | translate }}</cnsl-label>
<input cnslInput formControlName="group" />
<input cnslInput formControlName="group" data-e2e="role-group-input" />
</cnsl-form-field>
</div>
<button
@@ -36,7 +36,14 @@
</ng-container>
</div>
<button class="add-line-btn" color="primary" type="button" mat-stroked-button (click)="addEntry()">
<button
class="add-line-btn"
color="primary"
type="button"
data-e2e="new-project-role-button"
mat-stroked-button
(click)="addEntry()"
>
{{ 'PROJECT.ROLE.ADDNEWLINE' | translate }}
</button>

View File

@@ -1502,12 +1502,11 @@
"SEL_PROJECT": "Търсене на проект",
"SEL_ROLES": "Изберете ролите, които искате да бъдат добавени към субсидията",
"SEL_USER": "Изберете потребители",
"SEL_ORG": "Задайте домейна",
"SEL_ORG_DESC": "Въведете пълния домейн, за да посочите организацията, която да предоставите.",
"ORG_TITLE": "Организация",
"SEL_ORG": "Търсете организация",
"SEL_ORG_DESC": "Потърсете организацията за отпускане.",
"ORG_DESCRIPTION": "На път сте да предоставите потребител за организацията {{name}}.",
"ORG_DESCRIPTION_DESC": "Превключете контекста в заглавката по-горе, за да предоставите потребител за друга организация.",
"SEL_ORG_FORMFIELD": "Пълен домейн",
"SEL_ORG_FORMFIELD": "Организация",
"SEL_ORG_BUTTON": "Организация на търсенето",
"FOR_ORG": "Безвъзмездната помощ е създадена за:"
},

View File

@@ -1508,13 +1508,11 @@
"SEL_ROLES": "Selektiere die gewünschten Rollen für das Erstellen der Berechtigung.",
"SEL_PROJECT": "Suchen nach Projekt",
"SEL_USER": "Benutzer auswählen",
"SEL_ORG": "Suchen nach Domain",
"SEL_ORG_DESC": "Gebe die vollständige Domain ein, um Resultate zu erhalten.",
"ORG_TITLE": "Organisation",
"SEL_ORG": "Durchsuchen Sie eine Organisation",
"SEL_ORG_DESC": "Suchen Sie nach der zu gewährenden Organisation.",
"ORG_DESCRIPTION": "Du bist im Begriff, einen Benutzer für die Organisation {{name}} zu berechtigen.",
"ORG_DESCRIPTION_DESC": "Wechsle den Kontext, um die Organisation zu wechseln.",
"SEL_ORG_FORMFIELD": "Vollständige Domain",
"SEL_ORG_BUTTON": "Suche Organisation",
"SEL_ORG_FORMFIELD": "Organisation",
"FOR_ORG": "Die Berechtigung wird erstellt für:"
},
"DETAIL": {

View File

@@ -1501,7 +1501,7 @@
"GRANT": {
"EMPTY": "No granted organization.",
"TITLE": "Project Grants",
"DESCRIPTION": "Allow an other organization to use your project.",
"DESCRIPTION": "Allow another organization to use your project.",
"EDITTITLE": "Edit roles",
"CREATE": {
"TITLE": "Create Organization Grant",
@@ -1509,13 +1509,11 @@
"SEL_PROJECT": "Search for a project",
"SEL_ROLES": "Select the roles you want to be added to the grant",
"SEL_USER": "Select users",
"SEL_ORG": "Set the domain",
"SEL_ORG_DESC": "Enter the complete domain to specify the organization to grant.",
"ORG_TITLE": "Organization",
"SEL_ORG": "Search an organization",
"SEL_ORG_DESC": "Search the organization to grant.",
"ORG_DESCRIPTION": "You are about to grant a user for the organization {{name}}.",
"ORG_DESCRIPTION_DESC": "Switch the context in the header above to grant a user for another organization.",
"SEL_ORG_FORMFIELD": "Complete Domain",
"SEL_ORG_BUTTON": "Search Organization",
"SEL_ORG_FORMFIELD": "Organization",
"FOR_ORG": "The grant is created for:"
},
"DETAIL": {

View File

@@ -1509,13 +1509,11 @@
"SEL_PROJECT": "Buscar un proyecto",
"SEL_ROLES": "Selecciona los roles que quieres que se añadan a la concesión",
"SEL_USER": "Seleccionar usuarios",
"SEL_ORG": "Establecer el dominio",
"SEL_ORG_DESC": "Introduce el dominio completo para especificar la organización concesionaria.",
"ORG_TITLE": "Organización",
"SEL_ORG": "Buscar una organización",
"SEL_ORG_DESC": "Busca la organización concesionaria.",
"ORG_DESCRIPTION": "Estás a punto de conceder acceso a un usuario para la organización {{name}}.",
"ORG_DESCRIPTION_DESC": "Cambia el contexto en la cabecera superior para conceder acceso a un usuario para otra organización.",
"SEL_ORG_FORMFIELD": "Completar dominio",
"SEL_ORG_BUTTON": "Buscar organización",
"SEL_ORG_FORMFIELD": "Organización",
"FOR_ORG": "La concesión se creó para:"
},
"DETAIL": {

View File

@@ -1508,13 +1508,11 @@
"SEL_PROJECT": "Rechercher un projet",
"SEL_ROLES": "Sélectionnez les rôles que vous souhaitez ajouter à l'autorisation.",
"SEL_USER": "Sélectionnez les utilisateurs",
"SEL_ORG": "Définir le domaine",
"SEL_ORG_DESC": "Entrez le domaine complet pour spécifier l'organisation à accorder.",
"ORG_TITLE": "Organisation",
"SEL_ORG": "Rechercher une organisation",
"SEL_ORG_DESC": "Rechercher l'organisme à accorder",
"ORG_DESCRIPTION": "Vous êtes sur le point d'accorder un utilisateur pour l'organisation{{name}}.",
"ORG_DESCRIPTION_DESC": "Changez le contexte dans l'en-tête ci-dessus pour accorder un utilisateur pour une autre organisation.",
"SEL_ORG_FORMFIELD": "Domaine complet",
"SEL_ORG_BUTTON": "Rechercher une organisation",
"SEL_ORG_FORMFIELD": "Organisation",
"FOR_ORG": "L'autorisation est créée pour"
},
"DETAIL": {

View File

@@ -1508,13 +1508,11 @@
"SEL_PROJECT": "Cerca un progetto",
"SEL_ROLES": "Seleziona i ruoli che vuoi aggiungere",
"SEL_USER": "Seleziona utenti",
"SEL_ORG": "Impostare il dominio",
"SEL_ORG_DESC": "Inserisci il dominio completo per specificare l'organizzazione da concedere.",
"ORG_TITLE": "Organizzazione",
"SEL_ORG": "Cerca un'organizzazione",
"SEL_ORG_DESC": "Cerca l'organizzazione da concedere.",
"ORG_DESCRIPTION": "Stai per concedere un utente per l'organizzazione {{name}}.",
"ORG_DESCRIPTION_DESC": "Cambia il contesto nell'intestazione qui sopra per concedere un utente per un'altra organizzazione.",
"SEL_ORG_FORMFIELD": "Dominio completo",
"SEL_ORG_BUTTON": "Ricerca organizzazione",
"SEL_ORG_FORMFIELD": "Organizzazione",
"FOR_ORG": "Org grant \u00e8 creato per:"
},
"DETAIL": {

View File

@@ -1504,13 +1504,11 @@
"SEL_PROJECT": "プロジェクトを検索する",
"SEL_ROLES": "許可するロールを選択する",
"SEL_USER": "ユーザーを選択する",
"SEL_ORG": "ドメインを設定する",
"SEL_ORG_DESC": "完全なドメインを入力して、アクセスを許可する組織を指定する",
"ORG_TITLE": "組織",
"SEL_ORG": "組織を検索する",
"SEL_ORG_DESC": "付与する組織を検索する",
"ORG_DESCRIPTION": "組織 {{name}} にユーザーをグラントします。",
"ORG_DESCRIPTION_DESC": "上記のヘッダーのコンテキストを切り替えることで、別組織のユーザーにグラントできます。",
"SEL_ORG_FORMFIELD": "完全なドメイン",
"SEL_ORG_BUTTON": "組織を検索する",
"SEL_ORG_FORMFIELD": "組織",
"FOR_ORG": "グラントが以下に対して作成されます:"
},
"DETAIL": {

View File

@@ -1508,13 +1508,11 @@
"SEL_PROJECT": "Wyszukaj projekt",
"SEL_ROLES": "Wybierz role, które mają zostać dodane do udzielenia ",
"SEL_USER": "Wybierz użytkowników",
"SEL_ORG": "Ustaw domenę",
"SEL_ORG_DESC": "Wprowadź pełną domenę, aby określić organizację, której chcesz udzielić dostępu.",
"ORG_TITLE": "Organizacja",
"SEL_ORG": "Wyszukaj organizację",
"SEL_ORG_DESC": "Wyszukaj organizację, której chcesz przyznać.",
"ORG_DESCRIPTION": "Masz zamiar udzielić użytkownikowi dostęp dla organizacji {{name}}.",
"ORG_DESCRIPTION_DESC": "Przełącz kontekst w nagłówku powyżej, aby udzielić użytkownikowi dostępu dla innej organizacji.",
"SEL_ORG_FORMFIELD": "Pełna domena",
"SEL_ORG_BUTTON": "Wyszukaj organizację",
"SEL_ORG_FORMFIELD": "Organizacja",
"FOR_ORG": "Dostęp udzielany:"
},
"DETAIL": {

View File

@@ -1507,13 +1507,11 @@
"SEL_PROJECT": "搜索项目",
"SEL_ROLES": "选择要添加到授权中的角色",
"SEL_USER": "选择一个或多个用户",
"SEL_ORG": "选择一个组织",
"SEL_ORG_DESC": "输入完整的域以指定要授予的组织",
"ORG_TITLE": "组织",
"SEL_ORG": "搜索组织",
"SEL_ORG_DESC": "搜索要授予的组织",
"ORG_DESCRIPTION": "您即将授予组织 {{name}} 的用户。",
"ORG_DESCRIPTION_DESC": "切换上面标题中的上下文以授予另一个组织的用户。",
"SEL_ORG_FORMFIELD": "完整域名",
"SEL_ORG_BUTTON": "搜索组织",
"SEL_ORG_FORMFIELD": "组织",
"FOR_ORG": "授予组织:"
},
"DETAIL": {