feat(console): pinned org list, project grant detail view, state change, i18n, domain dialog, policy refactor, theme fixes, refactor user and projects (#449)

* pinned organisations

* project grant detail, state update, user-list pad

* rm entry components

* members nav, i18n, disable actions on non active

* add org domain dialog

* mv password policy rm to detail view

* prefix pinned orgs for userid, fix collapsed pad

* fix app back navigation

* rem pwd required validator

* fix org item overflow

* routing

* move users modules to users page

* reorganize projects

* remove child init of translate

* hide same preferred loginname
This commit is contained in:
Max Peintner
2020-07-13 13:38:49 +02:00
committed by GitHub
parent d8eef34a37
commit b628baeb1a
193 changed files with 967 additions and 500 deletions
@@ -0,0 +1,122 @@
<div class="max-width-container">
<div class="head">
<a [routerLink]="['/projects', projectId]" mat-icon-button>
<mat-icon class="icon">arrow_back</mat-icon>
</a>
<h1>{{ 'APP.PAGES.TITLE' | translate }} {{app?.name}}</h1>
<p class="desc">{{ 'APP.PAGES.DESCRIPTION' | translate }}</p>
<p *ngIf="isZitadel" class="zitadel-warning">{{'PROJECT.PAGES.ZITADELPROJECT' | translate}}</p>
</div>
<span *ngIf="errorMessage" class="err-container">{{errorMessage}}</span>
<app-card title="{{ 'APP.PAGES.DETAIL.TITLE' | translate }}" *ngIf="app">
<form [formGroup]="appNameForm" (ngSubmit)="saveOIDCApp()">
<div class="content">
<mat-button-toggle-group formControlName="state" class="toggle" (change)="changeState($event)">
<mat-button-toggle [value]="AppState.APPSTATE_INACTIVE" matTooltip="Deactivate Org">
{{'APP.PAGES.DETAIL.STATE.'+AppState.APPSTATE_INACTIVE | translate}}
</mat-button-toggle>
<mat-button-toggle [value]="AppState.APPSTATE_ACTIVE" matTooltip="Activate Org">
{{'APP.PAGES.DETAIL.STATE.'+AppState.APPSTATE_ACTIVE | translate}}
</mat-button-toggle>
</mat-button-toggle-group>
<mat-form-field class="formfield">
<mat-label>{{ 'APP.NAME' | translate }}</mat-label>
<input matInput formControlName="name" />
</mat-form-field>
</div>
<div class="btn-container">
<button type="submit" color="primary" [disabled]="appNameForm.invalid || name?.disabled"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div>
</form>
</app-card>
<app-card title="{{ 'APP.OIDC.TITLE' | translate }}" *ngIf="app && app.oidcConfig">
<card-actions class="card-actions">
<button mat-stroked-button
(click)="regenerateOIDCClientSecret()">{{'APP.OIDC.REGENERATESECRET' | translate}}</button>
</card-actions>
<form *ngIf="appForm" [formGroup]="appForm" (ngSubmit)="saveOIDCApp()">
<div class="content">
<mat-form-field appearance="outline">
<mat-label>{{ 'APP.OIDC.CLIENTID' | translate }}</mat-label>
<input matInput formControlName="clientId" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'APP.OIDC.RESPONSE' | translate }}</mat-label>
<mat-select formControlName="responseTypesList" multiple>
<mat-option *ngFor="let type of oidcResponseTypes" [value]="type">
{{ 'APP.OIDC.RESPONSE'+type | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'APP.OIDC.GRANT' | translate }}</mat-label>
<mat-select formControlName="grantTypesList" multiple>
<mat-option *ngFor="let grant of oidcGrantTypes" [value]="grant">
{{ 'APP.OIDC.GRANT'+grant | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'APP.OIDC.APPTYPE' | translate }}</mat-label>
<mat-select formControlName="applicationType">
<mat-option *ngFor="let type of oidcAppTypes" [value]="type">
{{ 'APP.OIDC.APPTYPE'+type | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'APP.OIDC.AUTHMETHOD' | translate }}</mat-label>
<mat-select formControlName="authMethodType">
<mat-option *ngFor="let type of oidcAuthMethodType" [value]="type">
{{ 'APP.OIDC.AUTHMETHOD'+type | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="full-width" appearance="outline">
<mat-label>{{ 'APP.OIDC.REDIRECT' | translate }}</mat-label>
<mat-chip-list #chipRedirectList>
<mat-chip *ngFor="let redirect of redirectUrisList" [selectable]="selectable"
(removed)="remove(redirect, RedirectType.REDIRECT)">
{{redirect}}
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
</mat-chip>
<input [matChipInputFor]="chipRedirectList" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="addOnBlur"
(matChipInputTokenEnd)="add($event, RedirectType.REDIRECT)">
</mat-chip-list>
</mat-form-field>
<mat-form-field class="full-width" appearance="outline">
<mat-label>{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}</mat-label>
<mat-chip-list #chipPostRedirectList>
<mat-chip *ngFor="let redirect of postLogoutRedirectUrisList" [selectable]="selectable"
(removed)="remove(redirect, RedirectType.POSTREDIRECT)">
{{redirect}}
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
</mat-chip>
<input [matChipInputFor]="chipPostRedirectList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputAddOnBlur]="addOnBlur"
(matChipInputTokenEnd)="add($event, RedirectType.POSTREDIRECT)">
</mat-chip-list>
</mat-form-field>
</div>
<div class="btn-container">
<button type="submit" color="primary" [disabled]="appForm.invalid"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div>
</form>
</app-card>
</div>
@@ -0,0 +1,87 @@
.head {
display: flex;
align-items: center;
border-bottom: 1px solid #ffffff20;
flex-wrap: wrap;
a {
display: block;
}
h1 {
font-size: 1.2rem;
margin: 0 1rem;
margin-left: 2rem;
font-weight: normal;
}
.desc {
width: 100%;
display: block;
font-size: .9rem;
color: #8795a1;
}
.zitadel-warning {
font-size: 14px;
color: rgb(201,51,71);
}
}
.err-container {
color: rgb(201,51,71);
font-size: 14px;
}
.card-actions {
button {
border-radius: .5rem;
}
}
.content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -.5rem;
&.nowrap{
flex-wrap: nowrap;
}
&.center {
align-items: center;
}
mat-form-field {
flex: 1 1 30%;
margin: 0 .5rem;
}
.full-width {
flex-basis: 100%;
}
.toggle {
align-self: flex-start;
margin-bottom: 1rem;
margin-right: 1rem;
border-radius: .5rem;
i {
margin-right: 1rem;
}
}
}
.btn-container {
display: flex;
justify-content: flex-end;
margin: 0 -.5rem;
button {
border-radius: .5rem;
margin: 0 .5rem;
}
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AppDetailComponent } from './app-detail.component';
describe('AppDetailComponent', () => {
let component: AppDetailComponent;
let fixture: ComponentFixture<AppDetailComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AppDetailComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AppDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,277 @@
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
import { Location } from '@angular/common';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatButtonToggleChange } from '@angular/material/button-toggle';
import { MatChipInputEvent } from '@angular/material/chips';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Params } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
import {
Application,
AppState,
OIDCApplicationType,
OIDCAuthMethodType,
OIDCConfig,
OIDCGrantType,
OIDCResponseType,
} from 'src/app/proto/generated/management_pb';
import { GrpcService } from 'src/app/services/grpc.service';
import { OrgService } from 'src/app/services/org.service';
import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog.component';
enum RedirectType {
REDIRECT = 'redirect',
POSTREDIRECT = 'postredirect',
}
@Component({
selector: 'app-app-detail',
templateUrl: './app-detail.component.html',
styleUrls: ['./app-detail.component.scss'],
})
export class AppDetailComponent implements OnInit, OnDestroy {
public errorMessage: string = '';
public selectable: boolean = false;
public removable: boolean = true;
public addOnBlur: boolean = true;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
private subscription?: Subscription;
public projectId: string = '';
public app!: Application.AsObject;
public oidcResponseTypes: OIDCResponseType[] = [
OIDCResponseType.OIDCRESPONSETYPE_CODE,
OIDCResponseType.OIDCRESPONSETYPE_ID_TOKEN,
OIDCResponseType.OIDCRESPONSETYPE_ID_TOKEN_TOKEN,
];
public oidcGrantTypes: OIDCGrantType[] = [
OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE,
OIDCGrantType.OIDCGRANTTYPE_IMPLICIT,
OIDCGrantType.OIDCGRANTTYPE_REFRESH_TOKEN,
];
public oidcAppTypes: OIDCApplicationType[] = [
OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB,
OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT,
OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE,
];
public oidcAuthMethodType: OIDCAuthMethodType[] = [
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_BASIC,
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_POST,
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE,
];
public AppState: any = AppState;
public appNameForm!: FormGroup;
public appForm!: FormGroup;
public redirectUrisList: string[] = [];
public postLogoutRedirectUrisList: string[] = [];
public RedirectType: any = RedirectType;
public isZitadel: boolean = false;
constructor(
public translate: TranslateService,
private route: ActivatedRoute,
private toast: ToastService,
private projectService: ProjectService,
private fb: FormBuilder,
private _location: Location,
private dialog: MatDialog,
private grpcService: GrpcService,
private orgService: OrgService,
) {
this.appNameForm = this.fb.group({
state: ['', []],
name: ['', [Validators.required]],
});
this.appForm = this.fb.group({
clientId: [{ value: '', disabled: true }],
responseTypesList: [],
grantTypesList: [],
applicationType: [],
authMethodType: [],
});
}
public ngOnInit(): void {
this.subscription = this.route.params.subscribe(params => this.getData(params));
}
public ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
private async getData({ projectid, id }: Params): Promise<void> {
this.projectId = projectid;
this.orgService.GetIam().then(iam => {
this.isZitadel = iam.toObject().iamProjectId === this.projectId;
});
this.projectService.GetApplicationById(projectid, id).then(app => {
this.app = app.toObject();
this.appNameForm.patchValue(this.app);
if (this.app.state !== AppState.APPSTATE_ACTIVE) {
this.appNameForm.controls['name'].disable();
this.appForm.disable();
} else {
this.appNameForm.controls['name'].enable();
this.appForm.enable();
}
if (this.app.oidcConfig?.redirectUrisList) {
this.redirectUrisList = this.app.oidcConfig.redirectUrisList;
}
if (this.app.oidcConfig?.postLogoutRedirectUrisList) {
this.postLogoutRedirectUrisList = this.app.oidcConfig.postLogoutRedirectUrisList;
}
if (this.app.oidcConfig) {
this.appForm.patchValue(this.app.oidcConfig);
}
}).catch(error => {
console.error(error);
this.toast.showError(error);
this.errorMessage = error.message;
});
}
public changeState(event: MatButtonToggleChange): void {
if (event.value === AppState.APPSTATE_ACTIVE) {
this.projectService.ReactivateApplication(this.projectId, this.app.id).then(() => {
this.toast.showInfo('APP.TOAST.REACTIVATED', true);
}).catch((error: any) => {
this.toast.showError(error);
});
} else if (event.value === AppState.APPSTATE_INACTIVE) {
this.projectService.DeactivateApplication(this.projectId, this.app.id).then(() => {
this.toast.showInfo('APP.TOAST.REACTIVATED', true);
}).catch((error: any) => {
this.toast.showError(error);
});
}
if (event.value !== AppState.APPSTATE_ACTIVE) {
this.appNameForm.controls['name'].disable();
this.appForm.disable();
} else {
this.appNameForm.controls['name'].enable();
this.appForm.enable();
this.clientId?.disable();
}
}
public add(event: MatChipInputEvent, target: RedirectType): void {
if (target === RedirectType.POSTREDIRECT) {
const input = event.input;
if (event.value !== '' && event.value !== ' ' && event.value !== '/') {
this.postLogoutRedirectUrisList.push(event.value);
}
if (input) {
input.value = '';
}
} else if (target === RedirectType.REDIRECT) {
const input = event.input;
if (event.value !== '' && event.value !== ' ' && event.value !== '/') {
this.redirectUrisList.push(event.value);
}
if (input) {
input.value = '';
}
}
}
public remove(redirect: any, target: RedirectType): void {
if (target === RedirectType.POSTREDIRECT) {
const index = this.postLogoutRedirectUrisList.indexOf(redirect);
if (index >= 0) {
this.postLogoutRedirectUrisList.splice(index, 1);
}
} else if (target === RedirectType.REDIRECT) {
const index = this.redirectUrisList.indexOf(redirect);
if (index >= 0) {
this.redirectUrisList.splice(index, 1);
}
}
}
public saveOIDCApp(): void {
if (this.appNameForm.valid) {
this.app.name = this.name?.value;
}
if (this.appForm.valid) {
if (this.app.oidcConfig) {
this.app.oidcConfig.responseTypesList = this.responseTypesList?.value;
this.app.oidcConfig.grantTypesList = this.grantTypesList?.value;
this.app.oidcConfig.applicationType = this.applicationType?.value;
this.app.oidcConfig.authMethodType = this.authMethodType?.value;
this.app.oidcConfig.redirectUrisList = this.redirectUrisList;
this.app.oidcConfig.postLogoutRedirectUrisList = this.postLogoutRedirectUrisList;
this.projectService
.UpdateOIDCAppConfig(this.projectId, this.app.id, this.app.oidcConfig)
.then((data: OIDCConfig) => {
this.toast.showInfo('APP.TOAST.OIDCUPDATED', true);
})
.catch(data => {
this.toast.showError(data.message);
});
}
}
}
public regenerateOIDCClientSecret(): void {
this.projectService.RegenerateOIDCClientSecret(this.app.id, this.projectId).then((data: OIDCConfig) => {
this.toast.showInfo('APP.TOAST.OIDCCLIENTSECRETREGENERATED', true);
this.dialog.open(AppSecretDialogComponent, {
data: {
clientId: data.toObject().clientId,
clientSecret: data.toObject().clientSecret,
},
width: '400px',
});
}).catch(error => {
this.toast.showError(error);
});
}
public navigateBack(): void {
this._location.back();
}
public get name(): AbstractControl | null {
return this.appNameForm.get('name');
}
public get clientId(): AbstractControl | null {
return this.appForm.get('clientId');
}
public get responseTypesList(): AbstractControl | null {
return this.appForm.get('responseTypesList');
}
public get grantTypesList(): AbstractControl | null {
return this.appForm.get('grantTypesList');
}
public get applicationType(): AbstractControl | null {
return this.appForm.get('applicationType');
}
public get authMethodType(): AbstractControl | null {
return this.appForm.get('authMethodType');
}
}