feat: rehauled console (#3525)

* new console

* move npm ci to angular build

* rel path for assets

* local grpc copy

* login policy, rm clear views, features rel path

* lock

Co-authored-by: Livio Amstutz <[email protected]>
This commit is contained in:
Max Peintner
2022-04-28 10:35:02 +00:00
committed by GitHub
co-authored by Livio Amstutz
parent 00f7dbe875
commit 08ae39ae19
668 changed files with 47746 additions and 19117 deletions
@@ -9,12 +9,12 @@
button {
margin-bottom: 14px;
margin-right: -.5rem;
margin-right: -0.5rem;
}
}
.uri-list {
margin: 0 .5rem;
margin: 0 0.5rem;
width: 100%;
.uri-line {
@@ -24,6 +24,7 @@
.uri {
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
font-size: 14px;
overflow-x: auto;
}
.fill-space {
@@ -58,5 +59,5 @@
.error {
font-size: 13px;
color: var(--warn);
margin: 0 .5rem 1.5rem .5rem;
margin: 0 0.5rem 1.5rem 0.5rem;
}
@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
@@ -12,14 +12,14 @@ export class AdditionalOriginsComponent implements OnInit, OnDestroy {
@Input() canWrite: boolean = false;
@Input() public urisList: string[] = [];
@Input() public redirectControl: FormControl = new FormControl({ value: '', disabled: true });
@Input() public changedUris: EventEmitter<string[]> = new EventEmitter();
@Output() public changedUris: EventEmitter<string[]> = new EventEmitter<string[]>();
@Input() public getValues: Observable<void> = new Observable();
public placeholder: string = '<scheme> "://" <hostname> [ ":" <port> ]';
@ViewChild('originInput') input!: any;
private sub: Subscription = new Subscription();
constructor() { }
constructor() {}
ngOnInit(): void {
if (this.canWrite) {
@@ -1,270 +1,275 @@
<div class="container">
<div class="abort-container">
<button (click)="close()" mat-icon-button>
<mat-icon>close</mat-icon>
</button>
<span class="abort">{{ 'APP.PAGES.CREATE_OIDC' | translate }}</span>
</div>
<div class="app-create-container">
<div class="abort-container">
<button (click)="close()" mat-icon-button>
<mat-icon>close</mat-icon>
</button>
<span class="abort">{{ 'APP.PAGES.CREATE_OIDC' | translate }}</span>
</div>
<h1>{{'APP.PAGES.CREATE_OIDC_DESC_TITLE' | translate}}</h1>
<p class="desc">{{'APP.PAGES.CREATE_OIDC_DESC_SUB' | translate}}</p>
<h1>{{'APP.PAGES.CREATE_OIDC_DESC_TITLE' | translate}}</h1>
<p class="desc cnsl-secondary-text">{{'APP.PAGES.CREATE_OIDC_DESC_SUB' | translate}}</p>
<mat-progress-bar class="progress-bar" color="primary" *ngIf="loading" mode="indeterminate"></mat-progress-bar>
<mat-progress-bar class="progress-bar" color="primary" *ngIf="loading" mode="indeterminate"></mat-progress-bar>
<mat-checkbox class="proswitch" color="primary" [(ngModel)]="devmode">
{{'APP.OIDC.PROSWITCH' | translate}}
</mat-checkbox>
<mat-checkbox class="proswitch" color="primary" [(ngModel)]="devmode">
{{'APP.OIDC.PROSWITCH' | translate}}
</mat-checkbox>
<mat-horizontal-stepper class="stepper" *ngIf="!devmode" linear #stepper labelPosition="bottom"
(selectionChange)="changeStep($event)">
<mat-step [stepControl]="firstFormGroup" [editable]="true">
<form [formGroup]="firstFormGroup">
<ng-template matStepLabel>{{'APP.OIDC.NAMEANDTYPESECTION' | translate}}</ng-template>
<mat-horizontal-stepper class="stepper" *ngIf="!devmode" linear #stepper labelPosition="bottom"
(selectionChange)="changeStep($event)">
<mat-step [stepControl]="firstFormGroup" [editable]="true">
<form [formGroup]="firstFormGroup">
<ng-template matStepLabel>{{'APP.OIDC.NAMEANDTYPESECTION' | translate}}</ng-template>
<p class="step-title">{{'APP.OIDC.TITLEFIRST' | translate}}</p>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label>
<input cnslInput cdkFocusInitial formControlName="name" />
<span cnslError *ngIf="name?.errors?.required">{{'PROJECT.APP.NAMEREQUIRED' | translate}}</span>
</cnsl-form-field>
<p class="step-title">{{'APP.OIDC.TITLEFIRST' | translate}}</p>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label>
<input cnslInput cdkFocusInitial formControlName="name" />
<span cnslError *ngIf="name?.errors?.required">{{'PROJECT.APP.NAMEREQUIRED' | translate}}</span>
</cnsl-form-field>
<p class="step-title">{{'APP.OIDC.TYPETITLE' | translate}}</p>
<p class="step-title">{{'APP.OIDC.TYPETITLE' | translate}}</p>
<cnsl-type-radio [types]="appTypes" (selectedType)="appType?.setValue($event)"
[selected]="appType?.value">
</cnsl-type-radio>
<div class="actions">
<button mat-raised-button [disabled]="firstFormGroup.invalid" color="primary"
matStepperNext>{{'ACTIONS.CONTINUE' | translate}}</button>
</div>
</form>
</mat-step>
<cnsl-type-radio [types]="appTypes" (selectedType)="appType?.setValue($event)" [selected]="appType?.value">
</cnsl-type-radio>
<div class="app-create-actions">
<span class="fill-space"></span>
<button mat-raised-button [disabled]="firstFormGroup.invalid" color="primary" matStepperNext
[attr.data-e2e]="'continue-button-nameandtype'">{{'ACTIONS.CONTINUE' | translate}}</button>
</div>
</form>
</mat-step>
<!-- skip for native applications -->
<mat-step *ngIf="oidcAppRequest.appType !== OIDCAppType.OIDC_APP_TYPE_NATIVE" [stepControl]="secondFormGroup"
[editable]="true">
<form [formGroup]="secondFormGroup">
<ng-template matStepLabel>{{'APP.AUTHMETHODSECTION' | translate}}</ng-template>
<!-- skip for native applications -->
<mat-step *ngIf="oidcAppRequest.appType !== OIDCAppType.OIDC_APP_TYPE_NATIVE" [stepControl]="secondFormGroup"
[editable]="true">
<form [formGroup]="secondFormGroup">
<ng-template matStepLabel>{{'APP.AUTHMETHODSECTION' | translate}}</ng-template>
<cnsl-auth-method-radio [authMethods]="authMethods" [selected]="authMethod?.value"
[isOIDC]="appType?.value?.createType === AppCreateType.OIDC"
(selectedMethod)="authMethod?.setValue($event)">
</cnsl-auth-method-radio>
<cnsl-auth-method-radio [authMethods]="authMethods" [selected]="authMethod?.value"
[isOIDC]="appType?.value?.createType === AppCreateType.OIDC" (selectedMethod)="authMethod?.setValue($event)">
</cnsl-auth-method-radio>
<div class="actions">
<button mat-stroked-button color="primary" matStepperPrevious>{{'ACTIONS.BACK' |
translate}}</button>
<button mat-raised-button color="primary" [disabled]="secondFormGroup.invalid"
matStepperNext>{{'ACTIONS.CONTINUE' | translate}}</button>
</div>
</form>
</mat-step>
<div class="app-create-actions">
<button mat-stroked-button color="primary" matStepperPrevious>{{'ACTIONS.BACK' |
translate}}</button>
<span class="fill-space"></span>
<button mat-raised-button color="primary" [disabled]="secondFormGroup.invalid" matStepperNext
[attr.data-e2e]="'continue-button-authmethod'">{{'ACTIONS.CONTINUE' | translate}}</button>
</div>
</form>
</mat-step>
<!-- show redirect step only for OIDC apps -->
<mat-step *ngIf="appType?.value?.createType === AppCreateType.OIDC" [editable]="true">
<ng-template matStepLabel>{{'APP.OIDC.REDIRECTSECTION' | translate}}</ng-template>
<!-- show redirect step only for OIDC apps -->
<mat-step *ngIf="appType?.value?.createType === AppCreateType.OIDC" [editable]="true">
<ng-template matStepLabel>{{'APP.OIDC.REDIRECTSECTION' | translate}}</ng-template>
<p class="step-title">{{'APP.OIDC.REDIRECTTITLE' | translate}}</p>
<p class="step-description" *ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
{{'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate}}</p>
<p class="step-description" *ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_WEB">
{{'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate}}</p>
<p class="step-title">{{'APP.OIDC.REDIRECTTITLE' | translate}}</p>
<p class="step-description cnsl-secondary-text"
*ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
{{'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate}}</p>
<p class="step-description cnsl-secondary-text" *ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_WEB">
{{'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate}}</p>
<cnsl-redirect-uris class="redirect-section" [canWrite]="true"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
(changedUris)="oidcAppRequest.redirectUrisList = $any($event)" [urisList]="oidcAppRequest.redirectUrisList"
[getValues]="requestRedirectValuesSubject$" title="{{ 'APP.OIDC.REDIRECT' | translate }}">
</cnsl-redirect-uris>
<cnsl-redirect-uris class="redirect-section" [canWrite]="true"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
(changedUris)="oidcAppRequest.redirectUrisList = $any($event)" [urisList]="oidcAppRequest.redirectUrisList"
[getValues]="requestRedirectValuesSubject$" title="{{ 'APP.OIDC.REDIRECT' | translate }}">
</cnsl-redirect-uris>
<p class="step-title">{{'APP.OIDC.POSTREDIRECTTITLE' | translate}}</p>
<p class="step-description" *ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
{{'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate}}</p>
<p class="step-description"
*ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_WEB || oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT">
{{'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate}}</p>
<p class="step-title">{{'APP.OIDC.POSTREDIRECTTITLE' | translate}}</p>
<p class="step-description cnsl-secondary-text"
*ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
{{'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate}}</p>
<p class="step-description cnsl-secondary-text"
*ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_WEB || oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT">
{{'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate}}</p>
<cnsl-redirect-uris class="redirect-section" [canWrite]="true"
(changedUris)="oidcAppRequest.postLogoutRedirectUrisList = $any($event)"
[urisList]="oidcAppRequest.postLogoutRedirectUrisList"
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}" [getValues]="requestRedirectValuesSubject$"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
</cnsl-redirect-uris>
<cnsl-redirect-uris class="redirect-section" [canWrite]="true"
(changedUris)="oidcAppRequest.postLogoutRedirectUrisList = $any($event)"
[urisList]="oidcAppRequest.postLogoutRedirectUrisList" title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
[getValues]="requestRedirectValuesSubject$"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
</cnsl-redirect-uris>
<div class="actions">
<button mat-stroked-button color="primary" matStepperPrevious>{{'ACTIONS.BACK' | translate}}</button>
<button mat-raised-button color="primary" matStepperNext>{{'ACTIONS.CONTINUE' | translate}}</button>
</div>
</mat-step>
<div class="app-create-actions">
<button mat-stroked-button color="primary" matStepperPrevious>{{'ACTIONS.BACK' | translate}}</button>
<span class="fill-space"></span>
<button mat-raised-button color="primary" matStepperNext
[attr.data-e2e]="'continue-button-redirecturis'">{{'ACTIONS.CONTINUE' | translate}}</button>
</div>
</mat-step>
<mat-step>
<ng-template matStepLabel>{{'APP.OIDC.OVERVIEWSECTION'| translate}}</ng-template>
<p class="step-title">{{'APP.OIDC.OVERVIEWTITLE' | translate}}</p>
<div class="row">
<span class="left">
{{ 'APP.NAME' | translate }}
</span>
<span class="right">
{{oidcAppRequest.name}}
</span>
</div>
<mat-step>
<ng-template matStepLabel>{{'APP.OIDC.OVERVIEWSECTION'| translate}}</ng-template>
<p class="step-title">{{'APP.OIDC.OVERVIEWTITLE' | translate}}</p>
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.NAME' | translate }}
</span>
<span class="right">
{{oidcAppRequest.name}}
</span>
</div>
<ng-container *ngIf="appType?.value?.createType === AppCreateType.OIDC">
<div class="row">
<span class="left">
{{ 'APP.TYPE' | translate }}
</span>
<span class="right">
{{'APP.OIDC.APPTYPE.'+oidcAppRequest.appType | translate}}
</span>
</div>
<div class="row">
<span class="left">
{{ 'APP.GRANT' | translate }}
</span>
<span class="right" *ngIf="oidcAppRequest.grantTypesList && oidcAppRequest.grantTypesList.length > 0">
[<span *ngFor="let element of oidcAppRequest.grantTypesList; index as i">
{{'APP.OIDC.GRANT.'+element | translate}}
{{i < oidcAppRequest.grantTypesList.length - 1 ? ', ' : '' }} </span>]
</span>
</div>
<div class="row">
<span class="left">
{{ 'APP.OIDC.RESPONSETYPE' | translate }}
</span>
<span class="right" *ngIf="oidcAppRequest.responseTypesList && oidcAppRequest.responseTypesList.length > 0">
[<span *ngFor="let element of oidcAppRequest.responseTypesList; index as i">
{{('APP.OIDC.RESPONSE.'+element | translate)}}
{{i < oidcAppRequest.responseTypesList.length - 1 ? ', ' : '' }} </span>]
</span>
</div>
<ng-container *ngIf="appType?.value?.createType === AppCreateType.OIDC">
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.TYPE' | translate }}
</span>
<span class="right">
{{'APP.OIDC.APPTYPE.'+oidcAppRequest.appType | translate}}
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.GRANT' | translate }}
</span>
<span class="right" *ngIf="oidcAppRequest.grantTypesList && oidcAppRequest.grantTypesList.length > 0">
[<span *ngFor="let element of oidcAppRequest.grantTypesList; index as i">
{{'APP.OIDC.GRANT.'+element | translate}}
{{i < oidcAppRequest.grantTypesList.length - 1 ? ', ' : '' }} </span>]
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.OIDC.RESPONSETYPE' | translate }}
</span>
<span class="right" *ngIf="oidcAppRequest.responseTypesList && oidcAppRequest.responseTypesList.length > 0">
[<span *ngFor="let element of oidcAppRequest.responseTypesList; index as i">
{{('APP.OIDC.RESPONSE.'+element | translate)}}
{{i < oidcAppRequest.responseTypesList.length - 1 ? ', ' : '' }} </span>]
</span>
</div>
<div class="row">
<span class="left">
{{ 'APP.AUTHMETHOD' | translate }}
</span>
<span class="right">
<span>
{{'APP.OIDC.AUTHMETHOD.'+oidcAppRequest?.authMethodType | translate}}
</span>
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.AUTHMETHOD' | translate }}
</span>
<span class="right">
<span>
{{'APP.OIDC.AUTHMETHOD.'+oidcAppRequest?.authMethodType | translate}}
</span>
</span>
</div>
<div class="row">
<span class="left">
{{ 'APP.OIDC.REDIRECT' | translate }}
</span>
<span class="right" *ngIf="oidcAppRequest.redirectUrisList && oidcAppRequest.redirectUrisList.length > 0">
[<span *ngFor="let redirect of oidcAppRequest.redirectUrisList; index as i">
{{redirect}}
{{i < oidcAppRequest.redirectUrisList.length - 1 ? ', ' : '' }} </span>]
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.OIDC.REDIRECT' | translate }}
</span>
<span class="right" *ngIf="oidcAppRequest.redirectUrisList && oidcAppRequest.redirectUrisList.length > 0">
[<span *ngFor="let redirect of oidcAppRequest.redirectUrisList; index as i">
{{redirect}}
{{i < oidcAppRequest.redirectUrisList.length - 1 ? ', ' : '' }} </span>]
</span>
</div>
<div class="row">
<span class="left">
{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}
</span>
<span class="right" *ngIf="oidcAppRequest.postLogoutRedirectUrisList && oidcAppRequest.postLogoutRedirectUrisList.length > 0">
[<span *ngFor="let redirect of oidcAppRequest.postLogoutRedirectUrisList; index as i">
{{redirect}}
{{i < oidcAppRequest.postLogoutRedirectUrisList.length - 1 ? ', ' : '' }} </span>]
</span>
</div>
</ng-container>
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}
</span>
<span class="right"
*ngIf="oidcAppRequest.postLogoutRedirectUrisList && oidcAppRequest.postLogoutRedirectUrisList.length > 0">
[<span *ngFor="let redirect of oidcAppRequest.postLogoutRedirectUrisList; index as i">
{{redirect}}
{{i < oidcAppRequest.postLogoutRedirectUrisList.length - 1 ? ', ' : '' }} </span>]
</span>
</div>
</ng-container>
<ng-container *ngIf="appType?.value?.createType === AppCreateType.API">
<div class="row">
<span class="left">
{{ 'APP.AUTHMETHOD' | translate }}
</span>
<span class="right">
<span>
{{'APP.API.AUTHMETHOD.'+apiAppRequest?.authMethodType | translate}}
</span>
</span>
</div>
</ng-container>
<ng-container *ngIf="appType?.value?.createType === AppCreateType.API">
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.AUTHMETHOD' | translate }}
</span>
<span class="right">
<span>
{{'APP.API.AUTHMETHOD.'+apiAppRequest?.authMethodType | translate}}
</span>
</span>
</div>
</ng-container>
<div class="actions">
<button mat-stroked-button color="primary" matStepperPrevious>{{'ACTIONS.BACK' | translate}}</button>
<button mat-raised-button color="primary" (click)="createApp()">{{'ACTIONS.CREATE' |
translate}}</button>
</div>
</mat-step>
</mat-horizontal-stepper>
<div class="app-create-actions">
<button mat-stroked-button color="primary" matStepperPrevious>{{'ACTIONS.BACK' | translate}}</button>
<span class="fill-space"></span>
<button mat-raised-button color="primary" (click)="createApp()"
[attr.data-e2e]="'create-button'">{{'ACTIONS.CREATE' |
translate}}</button>
</div>
</mat-step>
</mat-horizontal-stepper>
<div *ngIf="devmode" class="dev">
<form [formGroup]="form" (ngSubmit)="createApp()">
<div class="content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" />
</cnsl-form-field>
<div *ngIf="devmode" class="dev">
<form [formGroup]="form" (ngSubmit)="createApp()" [attr.data-e2e]="'create-app-wizzard-3'">
<div class="content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.TYPE' | translate }}</cnsl-label>
<mat-select formControlName="appType">
<mat-option *ngFor="let appType of appTypes" [value]="appType">
{{ appType.titleI18nKey | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.TYPE' | translate }}</cnsl-label>
<mat-select formControlName="appType">
<mat-option *ngFor="let appType of appTypes" [value]="appType">
{{ appType.titleI18nKey | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<ng-container *ngIf="formappType?.value?.createType === AppCreateType.OIDC">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.OIDC.GRANTTYPE' | translate }}</cnsl-label>
<mat-select formControlName="grantTypesList" multiple>
<mat-option *ngFor="let grant of oidcGrantTypes" [value]="grant.type">
{{ ('APP.OIDC.GRANT.' + grant.type) | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<ng-container *ngIf="formappType?.value?.createType === AppCreateType.OIDC">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.OIDC.GRANTTYPE' | translate }}</cnsl-label>
<mat-select formControlName="grantTypesList" multiple>
<mat-option *ngFor="let grant of oidcGrantTypes" [value]="grant.type">
{{ ('APP.OIDC.GRANT.' + grant.type) | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.OIDC.RESPONSETYPE' | translate }}</cnsl-label>
<mat-select formControlName="responseTypesList" multiple>
<mat-option *ngFor="let type of oidcResponseTypes" [value]="type.type">
{{ 'APP.OIDC.RESPONSE.'+type.type | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
</ng-container>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.OIDC.RESPONSETYPE' | translate }}</cnsl-label>
<mat-select formControlName="responseTypesList" multiple>
<mat-option *ngFor="let type of oidcResponseTypes" [value]="type.type">
{{ 'APP.OIDC.RESPONSE.'+type.type | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
</ng-container>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.AUTHMETHOD' | translate }}</cnsl-label>
<mat-select formControlName="authMethodType">
<mat-option *ngFor="let type of authMethodTypes" [value]="type.type">
<span *ngIf="type.oidc">{{ 'APP.OIDC.AUTHMETHOD.'+type.type | translate }}</span>
<span *ngIf="type.api">{{ 'APP.API.AUTHMETHOD.'+type.type | translate }}</span>
</mat-option>
</mat-select>
</cnsl-form-field>
</div>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.AUTHMETHOD' | translate }}</cnsl-label>
<mat-select formControlName="authMethodType">
<mat-option *ngFor="let type of authMethodTypes" [value]="type.type">
<span *ngIf="type.oidc">{{ 'APP.OIDC.AUTHMETHOD.'+type.type | translate }}</span>
<span *ngIf="type.api">{{ 'APP.API.AUTHMETHOD.'+type.type | translate }}</span>
</mat-option>
</mat-select>
</cnsl-form-field>
</div>
<div class="content" *ngIf="formappType?.value?.createType === AppCreateType.OIDC">
<div class="formfield full-width">
<cnsl-redirect-uris class="redirect-section" [canWrite]="true"
(changedUris)="oidcAppRequest.redirectUrisList = $any($event)"
[urisList]="oidcAppRequest.redirectUrisList" title="{{ 'APP.OIDC.REDIRECT' | translate }}"
[getValues]="requestRedirectValuesSubject$"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
</cnsl-redirect-uris>
<div class="content" *ngIf="formappType?.value?.createType === AppCreateType.OIDC">
<div class="formfield full-width">
<cnsl-redirect-uris class="redirect-section" [canWrite]="true"
(changedUris)="oidcAppRequest.redirectUrisList = $any($event)" [urisList]="oidcAppRequest.redirectUrisList"
title="{{ 'APP.OIDC.REDIRECT' | translate }}" [getValues]="requestRedirectValuesSubject$"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
</cnsl-redirect-uris>
<cnsl-redirect-uris class="redirect-section" [canWrite]="true"
(changedUris)="oidcAppRequest.postLogoutRedirectUrisList = $any($event)"
[urisList]="oidcAppRequest.postLogoutRedirectUrisList"
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
[getValues]="requestRedirectValuesSubject$"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
</cnsl-redirect-uris>
</div>
</div>
<cnsl-redirect-uris class="redirect-section" [canWrite]="true"
(changedUris)="oidcAppRequest.postLogoutRedirectUrisList = $any($event)"
[urisList]="oidcAppRequest.postLogoutRedirectUrisList"
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}" [getValues]="requestRedirectValuesSubject$"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
</cnsl-redirect-uris>
</div>
</div>
<button color="primary" mat-raised-button class="continue-button" [disabled]="form.invalid" cdkFocusInitial
type="submit">
{{ 'ACTIONS.SAVE' | translate }}
</button>
</form>
</div>
<button color="primary" mat-raised-button class="continue-button" [disabled]="form.invalid" cdkFocusInitial
type="submit">
{{ 'ACTIONS.SAVE' | translate }}
</button>
</form>
</div>
</div>
@@ -5,7 +5,6 @@ h1 {
p.desc {
font-size: 14px;
color: var(--grey);
}
.proswitch {
@@ -13,8 +12,8 @@ p.desc {
display: block;
}
.container {
padding: 4rem 4rem 2rem 4rem;
.app-create-container {
padding-top: 2rem;
.progress-bar {
margin-bottom: 1rem;
@@ -39,7 +38,7 @@ p.desc {
}
.margin-right {
margin-right: .5rem;
margin-right: 0.5rem;
}
.full-width {
@@ -57,12 +56,11 @@ p.desc {
.step-title {
font-size: 1rem;
text-transform: uppercase;
letter-spacing: .05em;
letter-spacing: 0.05em;
}
.step-description {
font-size: .9rem;
color: var(--grey);
font-size: 0.9rem;
}
}
@@ -71,7 +69,7 @@ p.desc {
flex-direction: column;
.checkbox {
margin: .5rem 0;
margin: 0.5rem 0;
}
}
@@ -81,38 +79,31 @@ p.desc {
.left,
.right {
margin-bottom: .5rem;
margin-bottom: 0.5rem;
font-size: 14px;
color: var(--grey);
}
}
.actions {
button[mat-stroked-button] {
float: left;
margin-top: 1rem;
border-radius: .5rem;
}
.app-create-actions {
margin-top: 1rem;
display: flex;
button[mat-raised-button] {
float: right;
margin-top: 1rem;
border-radius: .5rem;
min-width: 100px;
.fill-space {
flex: 1;
}
}
.dev {
.content {
display: flex;
margin: 0 -.5rem;
margin: 0 -0.5rem;
flex-wrap: wrap;
flex-direction: row;
.formfield {
flex: 1;
box-sizing: border-box;
margin: 0 .5rem;
margin: 0 0.5rem;
&.full-width {
flex-basis: 80%;
@@ -123,7 +114,7 @@ p.desc {
.continue-button {
margin-top: 3rem;
display: block;
padding: .5rem 4rem;
padding: 0.5rem 4rem;
float: right;
}
}
@@ -21,6 +21,7 @@ import {
AddOIDCAppRequest,
AddOIDCAppResponse,
} from 'src/app/proto/generated/zitadel/management_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -36,7 +37,6 @@ import {
} from '../authmethods';
import { API_TYPE, AppCreateType, NATIVE_TYPE, RadioItemAppType, USER_AGENT_TYPE, WEB_TYPE } from '../authtypes';
@Component({
selector: 'cnsl-app-create',
templateUrl: './app-create.component.html',
@@ -52,7 +52,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
public oidcAppRequest: AddOIDCAppRequest.AsObject = new AddOIDCAppRequest().toObject();
public apiAppRequest: AddAPIAppRequest.AsObject = new AddAPIAppRequest().toObject();
public oidcResponseTypes: { type: OIDCResponseType, checked: boolean; disabled: boolean; }[] = [
public oidcResponseTypes: { type: OIDCResponseType; checked: boolean; disabled: boolean }[] = [
{ type: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE, checked: false, disabled: false },
{ type: OIDCResponseType.OIDC_RESPONSE_TYPE_ID_TOKEN, checked: false, disabled: false },
{ type: OIDCResponseType.OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN, checked: false, disabled: false },
@@ -63,32 +63,22 @@ export class AppCreateComponent implements OnInit, OnDestroy {
OIDCAppType.OIDC_APP_TYPE_NATIVE,
OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
];
public appTypes: any = [
WEB_TYPE,
NATIVE_TYPE,
USER_AGENT_TYPE,
API_TYPE,
];
public appTypes: any = [WEB_TYPE, NATIVE_TYPE, USER_AGENT_TYPE, API_TYPE];
public authMethods: RadioItemAuthType[] = [
PKCE_METHOD,
CODE_METHOD,
PK_JWT_METHOD,
POST_METHOD,
];
public authMethods: RadioItemAuthType[] = [PKCE_METHOD, CODE_METHOD, PK_JWT_METHOD, POST_METHOD];
// set to oidc first
public authMethodTypes: {
type: OIDCAuthMethodType | APIAuthMethodType,
checked: boolean,
type: OIDCAuthMethodType | APIAuthMethodType;
checked: boolean;
disabled: boolean;
api?: boolean;
oidc?: boolean;
}[] = [
{ type: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_BASIC, checked: false, disabled: false, oidc: true },
{ type: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE, checked: false, disabled: false, oidc: true },
{ type: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST, checked: false, disabled: false, oidc: true },
];
{ type: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_BASIC, checked: false, disabled: false, oidc: true },
{ type: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE, checked: false, disabled: false, oidc: true },
{ type: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST, checked: false, disabled: false, oidc: true },
];
// stepper
firstFormGroup!: FormGroup;
@@ -103,15 +93,15 @@ export class AppCreateComponent implements OnInit, OnDestroy {
public OIDCAuthMethodType: any = OIDCAuthMethodType;
public oidcGrantTypes: {
type: OIDCGrantType,
checked: boolean,
disabled: boolean,
type: OIDCGrantType;
checked: boolean;
disabled: boolean;
}[] = [
{ type: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, checked: true, disabled: false },
{ type: OIDCGrantType.OIDC_GRANT_TYPE_IMPLICIT, checked: false, disabled: true },
// { type: OIDCGrantType.OIDCGRANTTYPE_REFRESH_TOKEN, checked: false, disabled: true },
// TODO show when implemented
];
{ type: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, checked: true, disabled: false },
{ type: OIDCGrantType.OIDC_GRANT_TYPE_IMPLICIT, checked: false, disabled: true },
// { type: OIDCGrantType.OIDCGRANTTYPE_REFRESH_TOKEN, checked: false, disabled: true },
// TODO show when implemented
];
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
public requestRedirectValuesSubject$: Subject<void> = new Subject();
@@ -124,6 +114,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
private mgmtService: ManagementService,
private fb: FormBuilder,
private _location: Location,
private breadcrumbService: BreadcrumbService,
) {
this.form = this.fb.group({
name: ['', [Validators.required]],
@@ -140,7 +131,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
appType: [WEB_TYPE, [Validators.required]],
});
this.firstFormGroup.valueChanges.subscribe(value => {
this.firstFormGroup.valueChanges.subscribe((value) => {
if (this.firstFormGroup.valid) {
this.oidcAppRequest.name = this.name?.value;
this.apiAppRequest.name = this.name?.value;
@@ -153,9 +144,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
switch (this.oidcAppRequest.appType) {
case OIDCAppType.OIDC_APP_TYPE_NATIVE:
this.authMethods = [
PKCE_METHOD,
];
this.authMethods = [PKCE_METHOD];
// automatically set to PKCE and skip step
this.oidcAppRequest.responseTypesList = [OIDCResponseType.OIDC_RESPONSE_TYPE_CODE];
@@ -165,30 +154,19 @@ export class AppCreateComponent implements OnInit, OnDestroy {
break;
case OIDCAppType.OIDC_APP_TYPE_WEB:
// PK_JWT_METHOD.recommended = false;
this.authMethods = [
PKCE_METHOD,
CODE_METHOD,
PK_JWT_METHOD,
POST_METHOD,
];
this.authMethods = [PKCE_METHOD, CODE_METHOD, PK_JWT_METHOD, POST_METHOD];
this.authMethod?.setValue(PKCE_METHOD.key);
break;
case OIDCAppType.OIDC_APP_TYPE_USER_AGENT:
this.authMethods = [
PKCE_METHOD,
IMPLICIT_METHOD,
];
this.authMethods = [PKCE_METHOD, IMPLICIT_METHOD];
this.authMethod?.setValue(PKCE_METHOD.key);
break;
}
} else if (this.isStepperAPI) {
// PK_JWT_METHOD.recommended = true;
this.authMethods = [
PK_JWT_METHOD,
BASIC_AUTH_METHOD,
];
this.authMethods = [PK_JWT_METHOD, BASIC_AUTH_METHOD];
this.authMethod?.setValue(PK_JWT_METHOD.key);
}
@@ -198,28 +176,48 @@ export class AppCreateComponent implements OnInit, OnDestroy {
this.secondFormGroup = this.fb.group({
authMethod: [this.authMethods[0].key, [Validators.required]],
});
this.secondFormGroup.valueChanges.subscribe(form => {
this.secondFormGroup.valueChanges.subscribe((form) => {
const partialConfig = getPartialConfigFromAuthMethod(form.authMethod);
if (this.isStepperOIDC && partialConfig && partialConfig.oidc) {
this.oidcAppRequest.responseTypesList = partialConfig.oidc?.responseTypesList
?? [];
this.oidcAppRequest.responseTypesList = partialConfig.oidc?.responseTypesList ?? [];
this.oidcAppRequest.grantTypesList = partialConfig.oidc?.grantTypesList
?? [];
this.oidcAppRequest.authMethodType = partialConfig.oidc?.authMethodType
?? OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE;
this.oidcAppRequest.grantTypesList = partialConfig.oidc?.grantTypesList ?? [];
this.oidcAppRequest.authMethodType =
partialConfig.oidc?.authMethodType ?? OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE;
} else if (this.isStepperAPI && partialConfig && partialConfig.api) {
this.apiAppRequest.authMethodType = partialConfig.api?.authMethodType
?? APIAuthMethodType.API_AUTH_METHOD_TYPE_BASIC;
this.apiAppRequest.authMethodType =
partialConfig.api?.authMethodType ?? APIAuthMethodType.API_AUTH_METHOD_TYPE_BASIC;
}
});
}
public ngOnInit(): void {
this.subscription = this.route.params.subscribe(params => this.getData(params));
this.subscription = this.route.params.subscribe((params) => this.getData(params));
const projectId = this.route.snapshot.paramMap.get('projectid');
if (projectId) {
const breadcrumbs = [
new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'IAM',
routerLink: ['/system'],
}),
new Breadcrumb({
type: BreadcrumbType.ORG,
routerLink: ['/org'],
}),
new Breadcrumb({
type: BreadcrumbType.PROJECT,
name: '',
param: { key: 'projectid', value: projectId },
routerLink: ['/projects', projectId],
isZitadel: false,
}),
];
this.breadcrumbService.setBreadcrumb(breadcrumbs);
}
}
public ngOnDestroy(): void {
@@ -228,23 +226,21 @@ export class AppCreateComponent implements OnInit, OnDestroy {
}
public initForm(): void {
this.form.valueChanges.pipe(
takeUntil(this.destroyed$),
debounceTime(150)).subscribe(() => {
this.oidcAppRequest.name = this.formname?.value;
this.apiAppRequest.name = this.formname?.value;
this.form.valueChanges.pipe(takeUntil(this.destroyed$), debounceTime(150)).subscribe(() => {
this.oidcAppRequest.name = this.formname?.value;
this.apiAppRequest.name = this.formname?.value;
this.oidcAppRequest.responseTypesList = this.formresponseTypesList?.value;
this.oidcAppRequest.grantTypesList = this.formgrantTypesList?.value;
this.oidcAppRequest.responseTypesList = this.formresponseTypesList?.value;
this.oidcAppRequest.grantTypesList = this.formgrantTypesList?.value;
this.oidcAppRequest.authMethodType = this.formauthMethodType?.value;
this.apiAppRequest.authMethodType = this.formauthMethodType?.value;
this.oidcAppRequest.authMethodType = this.formauthMethodType?.value;
this.apiAppRequest.authMethodType = this.formauthMethodType?.value;
const oidcAppType = (this.formappType?.value as RadioItemAppType).oidcAppType;
if (oidcAppType !== undefined) {
this.oidcAppRequest.appType = oidcAppType;
}
});
const oidcAppType = (this.formappType?.value as RadioItemAppType).oidcAppType;
if (oidcAppType !== undefined) {
this.oidcAppRequest.appType = oidcAppType;
}
});
this.formappType?.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(() => {
this.setDevFormValidators();
@@ -306,18 +302,20 @@ export class AppCreateComponent implements OnInit, OnDestroy {
.addOIDCApp(this.oidcAppRequest)
.then((resp) => {
this.loading = false;
this.toast.showInfo('APP.TOAST.CREATED', true);
if (resp.clientId || resp.clientSecret) {
this.showSavedDialog(resp);
} else {
this.router.navigate(['projects', this.projectId, 'apps', resp.appId]);
}
})
.catch(error => {
.catch((error) => {
this.loading = false;
this.toast.showError(error);
});
} else if (appAPICheck) {
this.loading = true;
this.toast.showInfo('APP.TOAST.CREATED', true);
this.mgmtService
.addAPIApp(this.apiAppRequest)
.then((resp) => {
@@ -329,7 +327,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
this.router.navigate(['projects', this.projectId, 'apps', resp.appId]);
}
})
.catch(error => {
.catch((error) => {
this.loading = false;
this.toast.showError(error);
});
@@ -364,7 +362,12 @@ export class AppCreateComponent implements OnInit, OnDestroy {
return this.firstFormGroup.get('appType');
}
public grantTypeChecked(type: OIDCGrantType): boolean {
return this.oidcGrantTypes.filter(gt => gt.checked).map(gt => gt.type).findIndex(t => t === type) > -1;
return (
this.oidcGrantTypes
.filter((gt) => gt.checked)
.map((gt) => gt.type)
.findIndex((t) => t === type) > -1
);
}
get responseTypesList(): AbstractControl | null {
return this.secondFormGroup.get('responseTypesList');
@@ -410,4 +413,3 @@ export class AppCreateComponent implements OnInit, OnDestroy {
return (this.appType?.value as RadioItemAppType).createType === AppCreateType.API;
}
}
@@ -1,282 +1,271 @@
<cnsl-meta-layout>
<div class="enlarged-container">
<div class="head">
<a [routerLink]="['/projects', projectId]" mat-icon-button>
<mat-icon class="icon">arrow_back</mat-icon>
</a>
<div class="title-col">
<h1>{{app?.name}}</h1>
<span *ngIf="app?.oidcConfig">{{'APP.OIDC.APPTYPE.'+app?.oidcConfig?.appType |
translate}}</span>
<span *ngIf="app?.apiConfig">API</span>
</div>
<ng-container *ngIf="isZitadel === false">
<span class="fill-space"></span>
<ng-template cnslHasRole [hasRole]="['project.app.write:'+projectId, 'project.app.write']">
<button class="actions-trigger" mat-raised-button color="primary" [matMenuTriggerFor]="actions">
<span>{{'ACTIONS.ACTIONS' | translate}}</span>
<mat-icon class="icon">keyboard_arrow_down</mat-icon>
</button>
<mat-menu #actions="matMenu" xPosition="before">
<button mat-menu-item (click)="openNameDialog()"
aria-label="Edit project name" *ngIf="isZitadel === false">
{{'ACTIONS.RENAME' | translate}}
</button>
<button mat-menu-item
*ngIf="app?.state !== AppState.APP_STATE_INACTIVE"
(click)="changeState(AppState.APP_STATE_INACTIVE)">
{{'ACTIONS.DEACTIVATE' | translate}}
</button>
<button mat-menu-item *ngIf="app?.state === AppState.APP_STATE_INACTIVE"
(click)="changeState(AppState.APP_STATE_ACTIVE)">
{{'ACTIONS.REACTIVATE' | translate}}
</button>
<ng-template cnslHasRole [hasRole]="['project.app.delete:'+projectId, 'project.app.delete']">
<button mat-menu-item matTooltip="{{'APP.PAGES.DELETE' | translate}}"
(click)="deleteApp()">
<span [style.color]="'var(--warn)'">{{'APP.PAGES.DELETE' | translate}}</span>
</button>
</ng-template>
</mat-menu>
</ng-template>
</ng-container>
<cnsl-top-view title="{{app?.name}}"
[hasActions]="isZitadel === false && (['project.app.write:'+projectId, 'project.app.write'] | hasRole | async)"
docLink="https://docs.zitadel.ch/docs/guides/basics/projects"
[sub]="app?.oidcConfig ? ('APP.OIDC.APPTYPE.'+app?.oidcConfig?.appType | translate): 'API'"
[isActive]="app?.state === AppState.APP_STATE_ACTIVE" [isInactive]="app?.state === AppState.APP_STATE_INACTIVE"
stateTooltip="{{('APP.PAGES.DETAIL.STATE.'+app?.state) | translate}}">
<ng-template topActions cnslHasRole [hasRole]="['project.app.write:'+projectId, 'project.app.write']">
<div class="regen-secret"
*ngIf="isZitadel === false && this.app?.oidcConfig && (currentAuthMethod === 'CODE' || currentAuthMethod === 'POST')">
<button type="button" [disabled]="!canWrite" mat-menu-item
(click)="regenerateOIDCClientSecret()">{{'APP.OIDC.REGENERATESECRET' | translate}}</button>
</div>
<p class="desc">{{ 'APP.PAGES.DESCRIPTION' | translate }}</p>
<p *ngIf="isZitadel" class="zitadel-warning">{{'PROJECT.PAGES.ZITADELPROJECT' | translate}}</p>
</div>
<div class="regen-secret" *ngIf="isZitadel === false && this.app?.apiConfig && currentAuthMethod === 'BASIC'">
<button [disabled]="!canWrite" mat-menu-item (click)="regenerateAPIClientSecret()">{{'APP.API.REGENERATESECRET' |
translate}}</button>
</div>
<span *ngIf="errorMessage" class="err-container">{{errorMessage}}</span>
<button *ngIf="isZitadel === false" mat-menu-item (click)="openNameDialog()" aria-label="Edit project name">
{{'ACTIONS.RENAME' | translate}}
</button>
<button mat-menu-item *ngIf="isZitadel === false && app?.state !== AppState.APP_STATE_INACTIVE"
(click)="changeState(AppState.APP_STATE_INACTIVE)">
{{'ACTIONS.DEACTIVATE' | translate}}
</button>
<button mat-menu-item *ngIf="isZitadel === false && app?.state === AppState.APP_STATE_INACTIVE"
(click)="changeState(AppState.APP_STATE_ACTIVE)">
{{'ACTIONS.REACTIVATE' | translate}}
</button>
<ng-template cnslHasRole [hasRole]="['project.app.delete:'+projectId, 'project.app.delete']">
<button *ngIf="isZitadel === false" mat-menu-item matTooltip="{{'APP.PAGES.DELETE' | translate}}"
(click)="deleteApp()">
<span [style.color]="'var(--warn)'">{{'APP.PAGES.DELETE' | translate}}</span>
</button>
</ng-template>
</ng-template>
<span *ngIf="errorMessage" class="app-err-container">{{errorMessage}}</span>
<div class="compliance"
*ngIf="app?.oidcConfig?.complianceProblemsList && app.oidcConfig?.complianceProblemsList?.length">
<cnsl-info-section class="problem" [type]="InfoSectionType.WARN">
<ul style="margin: 0;">
<li style="margin: 0 0 .5rem 0;"
*ngFor="let problem of app.oidcConfig?.complianceProblemsList || []">
{{problem.localizedMessage}}</li>
</ul>
</cnsl-info-section>
</div>
<cnsl-info-row topContent *ngIf="app" [app]="app"></cnsl-info-row>
</cnsl-top-view>
<cnsl-info-row *ngIf="app" [app]="app"></cnsl-info-row>
<div class="max-width-container">
<cnsl-auth-method-radio *ngIf="authMethods && initialAuthMethod && (app?.oidcConfig || app?.apiConfig)"
[authMethods]="authMethods" [selected]="initialAuthMethod" [current]="currentAuthMethod"
[isOIDC]="app?.oidcConfig !== undefined" (selectedMethod)="setPartialConfigFromAuthMethod($event)">
</cnsl-auth-method-radio>
<div *ngIf="app?.oidcConfig" class="expandables">
<div class="expandable">
<p class="title">{{'APP.OIDC.REDIRECTSECTIONTITLE' | translate}}
<button mat-icon-button (click)="showRedirects = !showRedirects"
matTooltip="{{(showRedirects ? 'ACTIONS.HIDE' : 'ACTIONS.SHOW') | translate}}">
<mat-icon *ngIf="!showRedirects">expand_more</mat-icon>
<mat-icon *ngIf="showRedirects">expand_less</mat-icon>
</button>
</p>
<ng-container *ngIf="showRedirects">
<cnsl-info-section *ngIf="appType?.value === OIDCAppType.OIDC_APP_TYPE_NATIVE">
<div class="dev-col">
<span>{{'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate}}</span>
<mat-slide-toggle *ngIf="devMode" color="primary" class="devmode" [formControl]="devMode" name="devMode"
matTooltip="{{'APP.OIDC.DEVMODEDESC' | translate}}">
{{ 'APP.OIDC.DEVMODE' | translate }}
</mat-slide-toggle>
</div>
</cnsl-info-section>
<cnsl-info-section
*ngIf="appType?.value === OIDCAppType.OIDC_APP_TYPE_WEB || appType?.value === OIDCAppType.OIDC_APP_TYPE_USER_AGENT">
<div class="dev-col">
<span>{{'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate}}</span>
<mat-slide-toggle *ngIf="devMode" color="primary" class="devmode" [formControl]="devMode" name="devMode"
matTooltip="{{'APP.OIDC.DEVMODEDESC' | translate}}">
{{ 'APP.OIDC.DEVMODE' | translate }}
</mat-slide-toggle>
</div>
</cnsl-info-section>
<div class="content">
<cnsl-redirect-uris *ngIf="appType?.value !== undefined" class="redirect-section" [canWrite]="canWrite"
[devMode]="devMode?.value" [getValues]="requestRedirectValuesSubject$"
(changedUris)="redirectUrisList = $any($event)" [urisList]="redirectUrisList"
title="{{ 'APP.OIDC.REDIRECT' | translate }}"
[isNative]="appType?.value === OIDCAppType.OIDC_APP_TYPE_NATIVE">
</cnsl-redirect-uris>
<cnsl-redirect-uris *ngIf="appType?.value !== undefined" class="redirect-section" [canWrite]="canWrite"
[devMode]="devMode?.value" (changedUris)="postLogoutRedirectUrisList = $any($event)"
[urisList]="postLogoutRedirectUrisList" [getValues]="requestRedirectValuesSubject$"
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
[isNative]="appType?.value === OIDCAppType.OIDC_APP_TYPE_NATIVE">
</cnsl-redirect-uris>
</div>
</ng-container>
</div>
<div class="app-main-content">
<cnsl-meta-layout>
<cnsl-sidenav [(ngModel)]="currentSetting" [settingsList]="settingsList">
<ng-container *ngIf="currentSetting === 'redirect-uris'">
<cnsl-card title=" {{ 'APP.OIDC.REDIRECTSECTIONTITLE' | translate }}">
<cnsl-info-section *ngIf="appType?.value === OIDCAppType.OIDC_APP_TYPE_NATIVE">
<div class="dev-col">
<span>{{'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate}}</span>
<mat-slide-toggle *ngIf="devMode" color="primary" class="devmode" [formControl]="devMode" name="devMode"
matTooltip="{{'APP.OIDC.DEVMODEDESC' | translate}}">
{{ 'APP.OIDC.DEVMODE' | translate }}
</mat-slide-toggle>
</div>
</cnsl-info-section>
<cnsl-info-section
*ngIf="appType?.value === OIDCAppType.OIDC_APP_TYPE_WEB || appType?.value === OIDCAppType.OIDC_APP_TYPE_USER_AGENT">
<div class="dev-col">
<span>{{'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate}}</span>
<mat-slide-toggle *ngIf="devMode" color="primary" class="devmode" [formControl]="devMode" name="devMode"
matTooltip="{{'APP.OIDC.DEVMODEDESC' | translate}}">
{{ 'APP.OIDC.DEVMODE' | translate }}
</mat-slide-toggle>
</div>
</cnsl-info-section>
<div class="expandable">
<p class="title">{{'APP.ADDITIONALORIGINS' | translate}}
<button mat-icon-button (click)="showAdditionalOrigins = !showAdditionalOrigins"
matTooltip="{{(showAdditionalOrigins ? 'ACTIONS.HIDE' : 'ACTIONS.SHOW') | translate}}">
<mat-icon *ngIf="!showAdditionalOrigins">expand_more</mat-icon>
<mat-icon *ngIf="showAdditionalOrigins">expand_less</mat-icon>
</button>
</p>
<ng-container *ngIf="showAdditionalOrigins">
<p class="desc">{{'APP.ADDITIONALORIGINSDESC' | translate}}</p>
<cnsl-additional-origins *ngIf="appType?.value !== undefined" class="input" [canWrite]="canWrite"
[getValues]="requestRedirectValuesSubject$" (changedUris)="additionalOriginsList = $any($event)"
[urisList]="additionalOriginsList" title="{{ 'APP.ORIGINS' | translate }}">
</cnsl-additional-origins>
</ng-container>
</div>
<cnsl-redirect-uris *ngIf="appType?.value !== undefined" class="redirect-section" [canWrite]="canWrite"
[devMode]="devMode?.value" [getValues]="requestRedirectValuesSubject$"
(changedUris)="redirectUrisList = $any($event)" [urisList]="redirectUrisList"
title="{{ 'APP.OIDC.REDIRECT' | translate }}"
[isNative]="appType?.value === OIDCAppType.OIDC_APP_TYPE_NATIVE">
</cnsl-redirect-uris>
<div style="margin: .5rem" class="divider"></div>
</div>
<cnsl-auth-method-radio *ngIf="authMethods && initialAuthMethod && (app?.oidcConfig || app?.apiConfig)"
[authMethods]="authMethods" [selected]="initialAuthMethod" [current]="currentAuthMethod"
[isOIDC]="app?.oidcConfig !== undefined" (selectedMethod)="setPartialConfigFromAuthMethod($event)">
</cnsl-auth-method-radio>
<cnsl-card *ngIf="currentAuthMethod === 'PK_JWT' && projectId && app?.id" [expanded]="false"
title="{{ 'USER.MACHINE.KEYSTITLE' | translate }}" description="{{ 'USER.MACHINE.KEYSDESC' | translate }}">
<cnsl-client-keys [projectId]="projectId" [appId]="app.id"></cnsl-client-keys>
</cnsl-card>
<div *ngIf="apiForm && app?.apiConfig" class="btn-container">
<button class="submit-button" (click)="saveAPIApp()" color="primary"
[disabled]="apiForm.invalid || !canWrite" mat-raised-button>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
<div *ngIf="currentAuthMethod === 'BASIC'">
<button [disabled]="!canWrite" mat-stroked-button
(click)="regenerateAPIClientSecret()">{{'APP.API.REGENERATESECRET' | translate}}</button>
</div>
<form *ngIf="oidcForm && app?.oidcConfig" [formGroup]="oidcForm" (ngSubmit)="saveOIDCApp()">
<cnsl-card title=" {{ 'APP.OIDC.TITLE' | translate }}" *ngIf="app && app.oidcConfig" [expanded]="false">
<div card-actions *ngIf="currentAuthMethod === 'CODE' || currentAuthMethod === 'POST'">
<button type="button" [disabled]="!canWrite" mat-stroked-button
(click)="regenerateOIDCClientSecret()">{{'APP.OIDC.REGENERATESECRET' | translate}}</button>
</div>
<div class="content">
<div class="grid">
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'APP.OIDC.CLIENTID' | translate }}</cnsl-label>
<input cnslInput formControlName="clientId" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.TYPE' | translate }}</cnsl-label>
<mat-select formControlName="appType">
<mat-option *ngFor="let type of oidcAppTypes" [value]="type">
{{ 'APP.OIDC.APPTYPE.'+type | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'APP.OIDC.RESPONSETYPE' | translate }}</cnsl-label>
<mat-select formControlName="responseTypesList" multiple>
<mat-option *ngFor="let type of oidcResponseTypes" [value]="type">
{{ 'APP.OIDC.RESPONSE.'+type | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.AUTHMETHOD' | translate }}</cnsl-label>
<mat-select formControlName="authMethodType">
<mat-option *ngFor="let type of oidcAuthMethodType" [value]="type">
{{ 'APP.OIDC.AUTHMETHOD.'+type | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'APP.OIDC.GRANTTYPE' | translate }}</cnsl-label>
<mat-select formControlName="grantTypesList" multiple>
<mat-option *ngFor="let grant of oidcGrantTypes" [value]="grant">
{{ 'APP.OIDC.GRANT.'+grant | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<mat-checkbox color="primary" class="rt"
(change)="toggleRefreshToken($event)" [disabled]="!this.grantTypesList?.value.includes(OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE)" [checked]="this.grantTypesList?.value.includes(OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN)">
{{ 'APP.OIDC.REFRESHTOKEN' | translate }}
</mat-checkbox>
</div>
<div class="divider"></div>
<p class="full-width section-title">{{'APP.OIDC.TOKENSECTIONTITLE' | translate}}</p>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.OIDC.TOKENTYPE' | translate }}</cnsl-label>
<mat-select formControlName="accessTokenType">
<mat-option *ngFor="let type of oidcTokenTypes" [value]="type">
{{ 'APP.OIDC.TOKENTYPE'+type | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<mat-checkbox *ngIf="accessTokenType?.value === OIDCTokenType.OIDC_TOKEN_TYPE_JWT" class="full-width"
formControlName="accessTokenRoleAssertion" color="primary">
{{'APP.OIDC.ACCESSTOKENROLEASSERTION' | translate}}</mat-checkbox>
<cnsl-info-section class="full-width desc">
<span>{{'APP.OIDC.ACCESSTOKENROLEASSERTION_DESCRIPTION' | translate}}</span>
</cnsl-info-section>
<mat-checkbox class="full-width" style="margin-bottom: .5rem" formControlName="idTokenRoleAssertion"
color="primary">
{{'APP.OIDC.IDTOKENROLEASSERTION' | translate}}</mat-checkbox>
<cnsl-info-section class="full-width desc">
<span>{{'APP.OIDC.IDTOKENROLEASSERTION_DESCRIPTION' | translate}}</span>
</cnsl-info-section>
<mat-checkbox class="full-width" style="margin-bottom: .5rem"
formControlName="idTokenUserinfoAssertion" color="primary">
{{'APP.OIDC.IDTOKENUSERINFOASSERTION' | translate}}</mat-checkbox>
<cnsl-info-section class="full-width desc">
<span>{{'APP.OIDC.IDTOKENUSERINFOASSERTION_DESCRIPTION' | translate}}</span>
</cnsl-info-section>
<p class="clockskew-title">ClockSkew</p>
<mat-slider color="primary" formControlName="clockSkewSeconds" class="clockskew-slider" thumbLabel
[displayWith]="formatClockSkewLabel" tickInterval=".1" min="0" [step]="1" max="5">
</mat-slider>
<cnsl-info-section class="full-width desc">
<span>{{'APP.OIDC.CLOCKSKEW' | translate}}</span>
</cnsl-info-section>
</div>
</cnsl-card>
<cnsl-redirect-uris *ngIf="appType?.value !== undefined" class="redirect-section" [canWrite]="canWrite"
[devMode]="devMode?.value" (changedUris)="postLogoutRedirectUrisList = $any($event)"
[urisList]="postLogoutRedirectUrisList" [getValues]="requestRedirectValuesSubject$"
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
[isNative]="appType?.value === OIDCAppType.OIDC_APP_TYPE_NATIVE">
</cnsl-redirect-uris>
<div class="btn-container">
<button class="submit-button" color="primary" (click)="saveOIDCApp()" [disabled]="!canWrite"
mat-raised-button>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</cnsl-card>
</ng-container>
<ng-container *ngIf="currentSetting === 'additional-origins'">
<cnsl-card title=" {{ 'APP.ADDITIONALORIGINS' | translate }}">
<p class="app-desc cnsl-secondary-text">{{'APP.ADDITIONALORIGINSDESC' | translate}}</p>
<cnsl-additional-origins *ngIf="additionalOriginsList && appType && appType?.value !== undefined"
class="app-input" [canWrite]="canWrite" [getValues]="requestRedirectValuesSubject$"
(changedUris)="additionalOriginsListChanged($event)" [urisList]="additionalOriginsList"
title="{{ 'APP.ORIGINS' | translate }}">
</cnsl-additional-origins>
<div class="btn-container">
<button class="submit-button" (click)="saveOIDCApp()" color="primary" [disabled]="!canWrite"
mat-raised-button>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</cnsl-card>
</ng-container>
<ng-container *ngIf="currentSetting === 'urls'">
<cnsl-card title=" {{ 'APP.URLS' | translate }}">
<div class="app-info-row">
<div class="app-info-wrapper" *ngFor="let environmentV of (environmentMap | keyvalue)">
<p class="app-info-row-title cnsl-secondary-text">{{environmentV.key}}</p>
<div class="app-copy-row">
<div *ngIf="environmentV.value" class="environment">
<button [disabled]="copied === environmentV.value"
[matTooltip]="(copied !== environmentV.value ? 'ACTIONS.COPY' : 'ACTIONS.COPIED' ) | translate"
cnslCopyToClipboard [valueToCopy]="environmentV.value" (copiedValue)="copied = environmentV.key">
{{environmentV.value}}
</button>
</div>
</div>
</div>
</div>
</cnsl-card>
</ng-container>
<ng-container *ngIf="currentSetting === 'configuration'">
<cnsl-card *ngIf="oidcForm && app?.oidcConfig" title=" {{ 'APP.OIDC.TITLE' | translate }}">
<form [formGroup]="oidcForm" (ngSubmit)="saveOIDCApp()">
<div class="compliance"
*ngIf="app?.oidcConfig?.complianceProblemsList && app.oidcConfig?.complianceProblemsList?.length">
<h2 class="compliance-title">{{'APP.COMPLIANCE' | translate}}</h2>
<cnsl-info-section class="problem" [type]="InfoSectionType.WARN">
<ul style="margin: 0;">
<li style="margin: 0 0 .5rem 0;"
*ngFor="let problem of app.oidcConfig?.complianceProblemsList || []">
{{problem.localizedMessage}}</li>
</ul>
</cnsl-info-section>
</div>
<div class="app-oidc-content">
<div class="app-oidc-grid">
<cnsl-form-field class="app-formfield" appearance="outline">
<cnsl-label>{{ 'APP.OIDC.CLIENTID' | translate }}</cnsl-label>
<input cnslInput formControlName="clientId" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="app-formfield">
<cnsl-label>{{ 'APP.TYPE' | translate }}</cnsl-label>
<mat-select formControlName="appType">
<mat-option *ngFor="let type of oidcAppTypes" [value]="type">
{{ 'APP.OIDC.APPTYPE.'+type | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="app-formfield" appearance="outline">
<cnsl-label>{{ 'APP.OIDC.RESPONSETYPE' | translate }}</cnsl-label>
<mat-select formControlName="responseTypesList" multiple>
<mat-option *ngFor="let type of oidcResponseTypes" [value]="type">
{{ 'APP.OIDC.RESPONSE.'+type | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="app-formfield">
<cnsl-label>{{ 'APP.AUTHMETHOD' | translate }}</cnsl-label>
<mat-select formControlName="authMethodType">
<mat-option *ngFor="let type of oidcAuthMethodType" [value]="type">
{{ 'APP.OIDC.AUTHMETHOD.'+type | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="app-formfield" appearance="outline">
<cnsl-label>{{ 'APP.OIDC.GRANTTYPE' | translate }}</cnsl-label>
<mat-select formControlName="grantTypesList" multiple>
<mat-option *ngFor="let grant of oidcGrantTypes" [value]="grant">
{{ 'APP.OIDC.GRANT.'+grant | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<mat-checkbox color="primary" class="rt" (change)="toggleRefreshToken($event)"
[disabled]="!this.grantTypesList?.value.includes(OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE)"
[checked]="this.grantTypesList?.value.includes(OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN)">
{{ 'APP.OIDC.REFRESHTOKEN' | translate }}
</mat-checkbox>
</div>
<p class="app-full-width app-section-title">{{'APP.OIDC.TOKENSECTIONTITLE' | translate}}</p>
<cnsl-form-field appearance="outline" class="app-formfield">
<cnsl-label>{{ 'APP.OIDC.TOKENTYPE' | translate }}</cnsl-label>
<mat-select formControlName="accessTokenType">
<mat-option *ngFor="let type of oidcTokenTypes" [value]="type">
{{ 'APP.OIDC.TOKENTYPE'+type | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<mat-checkbox *ngIf="accessTokenType?.value === OIDCTokenType.OIDC_TOKEN_TYPE_JWT" class="full-width"
formControlName="accessTokenRoleAssertion" color="primary">
{{'APP.OIDC.ACCESSTOKENROLEASSERTION' | translate}}</mat-checkbox>
<cnsl-info-section class="app-full-width app-desc">
<span>{{'APP.OIDC.ACCESSTOKENROLEASSERTION_DESCRIPTION' | translate}}</span>
</cnsl-info-section>
<mat-checkbox class="app-full-width" style="margin-top: 1.5rem" formControlName="idTokenRoleAssertion"
color="primary">
{{'APP.OIDC.IDTOKENROLEASSERTION' | translate}}</mat-checkbox>
<cnsl-info-section class="full-width app-desc">
<span>{{'APP.OIDC.IDTOKENROLEASSERTION_DESCRIPTION' | translate}}</span>
</cnsl-info-section>
<mat-checkbox class="app-full-width" style="margin-top: 1.5rem"
formControlName="idTokenUserinfoAssertion" color="primary">
{{'APP.OIDC.IDTOKENUSERINFOASSERTION' | translate}}</mat-checkbox>
<cnsl-info-section class="app-full-width app-desc">
<span>{{'APP.OIDC.IDTOKENUSERINFOASSERTION_DESCRIPTION' | translate}}</span>
</cnsl-info-section>
<p class="clockskew-title cnsl-secondary-text">ClockSkew</p>
<mat-slider color="primary" formControlName="clockSkewSeconds" class="clockskew-slider" thumbLabel
[displayWith]="formatClockSkewLabel" tickInterval=".1" min="0" [step]="1" max="5">
</mat-slider>
<cnsl-info-section class="app-full-width app-desc">
<span>{{'APP.OIDC.CLOCKSKEW' | translate}}</span>
</cnsl-info-section>
</div>
<div class="btn-container">
<button class="submit-button" type="submit" color="primary" [disabled]="oidcForm.invalid || !canWrite"
mat-raised-button>
{{ 'ACTIONS.SAVE' | translate }}
mat-raised-button>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</form>
</cnsl-card>
</ng-container>
<ng-container *ngIf="currentSetting === 'configuration'">
<cnsl-card *ngIf="currentAuthMethod === 'PK_JWT' && projectId && app?.id"
title="{{ 'USER.MACHINE.KEYSTITLE' | translate }}" description="{{ 'USER.MACHINE.KEYSDESC' | translate }}">
<cnsl-client-keys [projectId]="projectId" [appId]="app.id"></cnsl-client-keys>
<div *ngIf="apiForm && app?.apiConfig" class="btn-container">
<button class="submit-button" (click)="saveAPIApp()" color="primary"
[disabled]="apiForm.invalid || !canWrite" mat-raised-button>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</form>
</cnsl-card>
</ng-container>
</cnsl-sidenav>
<cnsl-links [links]="nextLinks"></cnsl-links>
</div>
<div class="side" metainfo>
<div class="meta-details">
<div class="meta-row">
<span class="first">{{'RESOURCEID' | translate}}:</span>
<span *ngIf="app?.id" class="second">{{ app.id }}</span>
</div>
<div class="meta-row">
<span class="first">{{'PROJECT.STATE.TITLE' | translate}}:</span>
<span *ngIf="app && app.state !== undefined"
[ngClass]="{'active': app.state === AppState.APP_STATE_ACTIVE, 'inactive': app.state === AppState.APP_STATE_INACTIVE}"
class="state">{{'APP.PAGES.DETAIL.STATE.'+app.state | translate}}</span>
</div>
</div>
<!-- <cnsl-links [links]="nextLinks"></cnsl-links> -->
<div metainfo>
<cnsl-changes *ngIf="app" [changeType]="ChangeType.APP" [id]="app.id" [secId]="projectId"></cnsl-changes>
</div>
</cnsl-meta-layout>
</div>
</cnsl-meta-layout>
</div>
</div>
@@ -1,68 +1,131 @@
.head {
display: flex;
align-items: center;
border-bottom: 1px solid #ffffff20;
flex-wrap: wrap;
@use '@angular/material' as mat;
a {
display: block;
@mixin app-detail-theme($theme) {
$foreground: map-get($theme, foreground);
$background: map-get($theme, background);
$is-dark-theme: map-get($theme, is-dark);
$warn: map-get($theme, warn);
$warn-color: map-get($warn, 500);
$button-text-color: map-get($foreground, text);
$button-disabled-text-color: map-get($foreground, disabled-button);
$divider-color: map-get($foreground, dividers);
.app-zitadel-warning {
font-size: 14px;
color: $warn-color;
}
.title-col {
margin-left: 2rem;
margin-right: 1rem;
.app-err-container {
color: $warn-color;
font-size: 14px;
}
h1 {
font-size: 2rem;
margin: 0;
font-weight: normal;
}
.dev-col {
display: flex;
flex-direction: column;
align-items: flex-start;
span {
font-size: 14px;
margin: .5rem 0;
color: var(--grey);
.devmode {
margin: 1rem 0 0.5rem 0;
}
}
.fill-space {
flex: 1;
.app-desc {
font-size: 14px;
margin: 0;
}
.desc {
.app-input {
width: 100%;
display: block;
font-size: .9rem;
color: var(--grey);
}
.app-info-row {
display: flex;
flex-direction: column;
margin: 0 -0.5rem;
@media only screen and (min-width: 500px) {
flex-direction: row;
flex-wrap: wrap;
}
.app-info-wrapper {
display: flex;
flex-direction: column;
margin: 0 0.5rem;
flex: 1;
align-items: flex-start;
box-sizing: border-box;
&:not(.width) {
min-width: 100px;
}
.app-info-row-title {
font-size: 12px;
margin: 0;
}
.app-info-row-desc {
margin: 0;
font-size: 14px;
padding: 2px 0;
}
.app-copy-row {
display: flex;
flex-direction: column;
width: 100%;
align-items: stretch;
button {
transition: opacity 0.15s ease-in-out;
background-color: #8795a110;
border: 1px solid #8795a160;
border-radius: 4px;
padding: 0.25rem 1rem;
margin: 0.25rem 0;
color: $button-text-color;
text-overflow: ellipsis;
overflow: hidden;
cursor: copy;
&[disabled] {
color: $button-disabled-text-color;
}
}
.environment {
display: flex;
flex-direction: column;
width: 100%;
margin: 0.25rem 0;
.key {
font-size: 14px;
}
}
}
}
}
.app-main-content {
border-top: 1px solid $divider-color;
margin-top: 1.5rem;
}
}
.zitadel-warning {
.compliance {
.compliance-title {
margin: 1rem 0 1rem 0;
font-size: 16px;
}
.problem {
font-size: 14px;
color: rgb(201, 51, 71);
margin: 0 0 0.5rem 0;
display: block;
}
.actions-trigger {
margin-top: .25rem;
display: flex;
align-items: center;
.icon {
margin-left: .5rem;
margin-right: -.5rem;
}
}
}
.err-container {
color: rgb(201, 51, 71);
font-size: 14px;
}
.compliance .problem {
font-size: 14px;
margin-bottom: .5rem;
display: block;
}
.name-content {
@@ -75,7 +138,7 @@
outline: none;
margin-top: 1rem;
margin-right: 1rem;
border-radius: .5rem;
border-radius: 0.5rem;
* {
outline: none;
@@ -93,11 +156,11 @@
.docs-line {
flex-basis: 100%;
margin: .5rem 0;
margin: 0.5rem 0;
p {
margin: 0;
margin-bottom: .5rem;
margin-bottom: 0.5rem;
}
p:last-child {
@@ -105,99 +168,51 @@
}
}
.grid {
.app-oidc-grid {
width: 100%;
display: grid;
grid-gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
.rt {
margin-top: 2.3rem;
margin-left: .5rem;
margin-left: 0.5rem;
}
}
.expandables {
padding-top: 1rem;
.expandable {
display: block;
width: 100%;
margin: 0 .5rem;
.title {
margin: 0;
text-transform: uppercase;
letter-spacing: .05em;
font-size: 14px;
}
.dev-col {
display: flex;
flex-direction: column;
align-items: flex-start;
.devmode {
margin: 1rem 0 .5rem 0;
}
}
.content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -.5rem;
padding-bottom: 2rem;
.redirect-section {
flex: 1;
margin: 0 .5rem;
}
}
.desc {
color: var(--grey);
font-size: 14px;
margin: 0;
}
.input {
width: 100%;
}
}
.main-app-content {
display: flex;
flex-direction: column;
}
.formfield {
.app-formfield {
flex: 1 1 30%;
margin: 0 .5rem;
&.full-width {
flex-basis: 100%;
}
}
.section-title {
.app-section-title {
margin: 1.5rem 0 0 0;
}
.full-width {
.app-full-width {
flex-basis: 100%;
margin-left: .5rem;
margin-right: .5rem;
margin-right: 0.5rem;
}
.clockskew-title {
font-size: 14px;
color: var(--grey);
margin: 1rem .5rem 0 .5rem;
margin: 1rem 0.5rem 0 0.5rem;
}
.clockskew-slider {
width: 100%;
margin: 0 .5rem;
margin: 0 0.5rem;
}
.desc {
color: var(--grey);
font-size: 14px;
margin-bottom: 1.5rem;
}
@@ -205,7 +220,7 @@
.error {
font-size: 13px;
color: var(--warn);
margin: 0 .5rem 1.5rem .5rem;
margin: 0 0.5rem 1.5rem 0.5rem;
}
.btn-container {
@@ -213,10 +228,3 @@
justify-content: flex-end;
margin-top: 1rem;
}
.divider {
flex-basis: 100%;
margin: 1.5rem .5rem;
height: 1px;
background-color: rgba(#8795a1, .2);
}
@@ -1,11 +1,12 @@
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
import { Location } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
import { Subject, Subscription } from 'rxjs';
@@ -15,6 +16,7 @@ import { ChangeType } from 'src/app/modules/changes/changes.component';
import { InfoSectionType } from 'src/app/modules/info-section/info-section.component';
import { CnslLinks } from 'src/app/modules/links/links.component';
import { NameDialogComponent } from 'src/app/modules/name-dialog/name-dialog.component';
import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import {
APIAuthMethodType,
@@ -33,6 +35,7 @@ import {
UpdateAPIAppConfigRequest,
UpdateOIDCAppConfigRequest,
} from 'src/app/proto/generated/zitadel/management_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -64,15 +67,15 @@ export class AppDetailComponent implements OnInit, OnDestroy {
public removable: boolean = true;
public addOnBlur: boolean = true;
public showAdditionalOrigins: boolean = false;
public showRedirects: boolean = false;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
public authMethods: RadioItemAuthType[] = [];
private subscription?: Subscription;
public projectId: string = '';
public app!: App.AsObject;
public environmentMap: { [key: string]: string } = {};
public oidcResponseTypes: OIDCResponseType[] = [
OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
OIDCResponseType.OIDC_RESPONSE_TYPE_ID_TOKEN,
@@ -96,10 +99,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
];
public oidcTokenTypes: OIDCTokenType[] = [
OIDCTokenType.OIDC_TOKEN_TYPE_BEARER,
OIDCTokenType.OIDC_TOKEN_TYPE_JWT,
];
public oidcTokenTypes: OIDCTokenType[] = [OIDCTokenType.OIDC_TOKEN_TYPE_BEARER, OIDCTokenType.OIDC_TOKEN_TYPE_JWT];
public AppState: any = AppState;
public oidcForm!: FormGroup;
@@ -124,6 +124,10 @@ export class AppDetailComponent implements OnInit, OnDestroy {
public copiedKey: any = '';
public nextLinks: Array<CnslLinks> = [];
public InfoSectionType: any = InfoSectionType;
public copied: string = '';
public settingsList: SidenavSetting[] = [{ id: 'configuration', i18nKey: 'APP.CONFIGURATION', featureRequired: false }];
public currentSetting: string | undefined = this.settingsList[0].id;
constructor(
public translate: TranslateService,
@@ -136,6 +140,8 @@ export class AppDetailComponent implements OnInit, OnDestroy {
private authService: GrpcAuthService,
private router: Router,
private snackbar: MatSnackBar,
private breadcrumbService: BreadcrumbService,
private http: HttpClient,
) {
this.oidcForm = this.fb.group({
devMode: [{ value: false, disabled: true }, []],
@@ -154,12 +160,25 @@ export class AppDetailComponent implements OnInit, OnDestroy {
this.apiForm = this.fb.group({
authMethodType: [{ value: '', disabled: true }],
});
this.http.get('./assets/environment.json').subscribe((env: any) => {
this.environmentMap = {
issuer: env.issuer,
adminServiceUrl: env.adminServiceUrl,
mgmtServiceUrl: env.mgmtServiceUrl,
authServiceUrl: env.adminServiceUrl,
};
});
}
public formatClockSkewLabel(seconds: number): string {
return seconds + 's';
}
public additionalOriginsListChanged(origins: string[]): void {
this.additionalOriginsList = origins;
}
public openNameDialog(): void {
const dialogRef = this.dialog.open(NameDialogComponent, {
data: {
@@ -171,7 +190,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
width: '400px',
});
dialogRef.afterClosed().subscribe(name => {
dialogRef.afterClosed().subscribe((name) => {
if (name) {
this.app.name = name;
this.saveApp();
@@ -180,7 +199,13 @@ export class AppDetailComponent implements OnInit, OnDestroy {
}
public ngOnInit(): void {
this.subscription = this.route.params.subscribe(params => this.getData(params));
const projectId = this.route.snapshot.paramMap.get('projectid');
const appId = this.route.snapshot.paramMap.get('appid');
if (projectId && appId) {
this.projectId = projectId;
this.getData(projectId, appId);
}
}
public ngOnDestroy(): void {
@@ -192,7 +217,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
{
i18nTitle: 'APP.PAGES.NEXTSTEPS.0.TITLE',
i18nDesc: 'APP.PAGES.NEXTSTEPS.0.DESC',
routerLink: ['/projects', this.projectId],
routerLink: ['/projects', this.projectId, 'roles'],
iconClasses: 'las la-user-tag',
},
{
@@ -200,7 +225,8 @@ export class AppDetailComponent implements OnInit, OnDestroy {
i18nDesc: 'APP.PAGES.NEXTSTEPS.1.DESC',
routerLink: ['/users', 'create'],
iconClasses: 'las la-user-plus',
}, {
},
{
i18nTitle: 'APP.PAGES.NEXTSTEPS.2.TITLE',
i18nDesc: 'APP.PAGES.NEXTSTEPS.2.DESC',
href: 'https://docs.zitadel.ch',
@@ -209,107 +235,151 @@ export class AppDetailComponent implements OnInit, OnDestroy {
];
}
private async getData({ projectid, id }: Params): Promise<void> {
this.projectId = projectid;
private async getData(projectId: string, appId: string): Promise<void> {
this.initLinks();
this.mgmtService.getIAM().then(iam => {
this.mgmtService.getIAM().then((iam) => {
this.isZitadel = iam.iamProjectId === this.projectId;
});
this.authService.isAllowed(['project.app.write$', 'project.app.write:' + projectid])
this.authService
.isAllowed(['project.app.write$', 'project.app.write:' + projectId])
.pipe(take(1))
.subscribe((allowed) => {
this.canWrite = allowed;
this.mgmtService.getAppByID(projectid, id).then(app => {
if (app.app) {
this.app = app.app;
if (this.app.oidcConfig) {
this.getAuthMethodOptions('OIDC');
this.mgmtService
.getAppByID(projectId, appId)
.then((app) => {
if (app.app) {
this.app = app.app;
this.initialAuthMethod = this.authMethodFromPartialConfig({ oidc: this.app.oidcConfig });
this.currentAuthMethod = this.initialAuthMethod;
if (this.initialAuthMethod === CUSTOM_METHOD.key) {
if (!this.authMethods.includes(CUSTOM_METHOD)) {
this.authMethods.push(CUSTOM_METHOD);
const breadcrumbs = [
new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'IAM',
routerLink: ['/system'],
}),
new Breadcrumb({
type: BreadcrumbType.ORG,
routerLink: ['/org'],
}),
new Breadcrumb({
type: BreadcrumbType.PROJECT,
name: '',
param: { key: 'projectid', value: projectId },
routerLink: ['/projects', projectId],
}),
new Breadcrumb({
type: BreadcrumbType.APP,
name: app.app.name,
param: { key: 'appid', value: appId },
routerLink: ['/projects', projectId, 'apps', appId],
}),
];
this.breadcrumbService.setBreadcrumb(breadcrumbs);
if (this.app.oidcConfig) {
this.getAuthMethodOptions('OIDC');
this.settingsList = [
{ id: 'configuration', i18nKey: 'APP.CONFIGURATION', featureRequired: false },
{ id: 'redirect-uris', i18nKey: 'APP.OIDC.REDIRECTSECTIONTITLE', featureRequired: false },
{ id: 'additional-origins', i18nKey: 'APP.ADDITIONALORIGINS', featureRequired: false },
{ id: 'urls', i18nKey: 'APP.URLS', featureRequired: false },
];
this.initialAuthMethod = this.authMethodFromPartialConfig({ oidc: this.app.oidcConfig });
this.currentAuthMethod = this.initialAuthMethod;
if (this.initialAuthMethod === CUSTOM_METHOD.key) {
if (!this.authMethods.includes(CUSTOM_METHOD)) {
this.authMethods.push(CUSTOM_METHOD);
}
} else {
this.authMethods = this.authMethods.filter((element) => element !== CUSTOM_METHOD);
}
} else {
this.authMethods = this.authMethods.filter(element => element !== CUSTOM_METHOD);
}
} else if (this.app.apiConfig) {
this.getAuthMethodOptions('API');
} else if (this.app.apiConfig) {
this.getAuthMethodOptions('API');
this.initialAuthMethod = this.authMethodFromPartialConfig({ api: this.app.apiConfig });
this.currentAuthMethod = this.initialAuthMethod;
if (this.initialAuthMethod === CUSTOM_METHOD.key) {
if (!this.authMethods.includes(CUSTOM_METHOD)) {
this.authMethods.push(CUSTOM_METHOD);
this.initialAuthMethod = this.authMethodFromPartialConfig({ api: this.app.apiConfig });
if (this.initialAuthMethod === 'BASIC') {
this.settingsList = [{ id: 'urls', i18nKey: 'APP.URLS', featureRequired: false }];
this.currentSetting = 'urls';
} else {
this.settingsList = [
{ id: 'configuration', i18nKey: 'APP.CONFIGURATION', featureRequired: false },
{ id: 'urls', i18nKey: 'APP.URLS', featureRequired: false },
];
}
} else {
this.authMethods = this.authMethods.filter(element => element !== CUSTOM_METHOD);
}
}
if (allowed) {
this.oidcForm.enable();
this.apiForm.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?.additionalOriginsList) {
this.additionalOriginsList = this.app.oidcConfig.additionalOriginsList;
}
if (this.app.oidcConfig?.clockSkew) {
const inSecs = this.app.oidcConfig?.clockSkew.seconds +
this.app.oidcConfig?.clockSkew.nanos / 100000;
this.oidcForm.controls['clockSkewSeconds'].setValue(inSecs);
}
if (this.app.oidcConfig) {
this.oidcForm.patchValue(this.app.oidcConfig);
}
if (this.app.apiConfig) {
this.apiForm.patchValue(this.app.apiConfig);
}
this.oidcForm.valueChanges.subscribe((oidcConfig) => {
this.initialAuthMethod = this.authMethodFromPartialConfig({ oidc: oidcConfig });
if (this.initialAuthMethod === CUSTOM_METHOD.key) {
if (!this.authMethods.includes(CUSTOM_METHOD)) {
this.authMethods.push(CUSTOM_METHOD);
this.currentAuthMethod = this.initialAuthMethod;
if (this.initialAuthMethod === CUSTOM_METHOD.key) {
if (!this.authMethods.includes(CUSTOM_METHOD)) {
this.authMethods.push(CUSTOM_METHOD);
}
} else {
this.authMethods = this.authMethods.filter((element) => element !== CUSTOM_METHOD);
}
} else {
this.authMethods = this.authMethods.filter(element => element !== CUSTOM_METHOD);
}
this.showSaveSnack();
});
this.apiForm.valueChanges.subscribe((apiConfig) => {
this.initialAuthMethod = this.authMethodFromPartialConfig({ api: apiConfig });
if (this.initialAuthMethod === CUSTOM_METHOD.key) {
if (!this.authMethods.includes(CUSTOM_METHOD)) {
this.authMethods.push(CUSTOM_METHOD);
}
} else {
this.authMethods = this.authMethods.filter(element => element !== CUSTOM_METHOD);
if (allowed) {
this.oidcForm.enable();
this.apiForm.enable();
}
this.showSaveSnack();
});
}
}).catch(error => {
console.error(error);
this.toast.showError(error);
this.errorMessage = error.message;
});
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?.additionalOriginsList) {
this.additionalOriginsList = this.app.oidcConfig.additionalOriginsList;
}
if (this.app.oidcConfig?.clockSkew) {
const inSecs = this.app.oidcConfig?.clockSkew.seconds + this.app.oidcConfig?.clockSkew.nanos / 100000;
this.oidcForm.controls['clockSkewSeconds'].setValue(inSecs);
}
if (this.app.oidcConfig) {
this.oidcForm.patchValue(this.app.oidcConfig);
}
if (this.app.apiConfig) {
this.apiForm.patchValue(this.app.apiConfig);
}
this.oidcForm.valueChanges.subscribe((oidcConfig) => {
this.initialAuthMethod = this.authMethodFromPartialConfig({ oidc: oidcConfig });
if (this.initialAuthMethod === CUSTOM_METHOD.key) {
if (!this.authMethods.includes(CUSTOM_METHOD)) {
this.authMethods.push(CUSTOM_METHOD);
}
} else {
this.authMethods = this.authMethods.filter((element) => element !== CUSTOM_METHOD);
}
this.showSaveSnack();
});
this.apiForm.valueChanges.subscribe((apiConfig) => {
this.initialAuthMethod = this.authMethodFromPartialConfig({ api: apiConfig });
if (this.initialAuthMethod === CUSTOM_METHOD.key) {
if (!this.authMethods.includes(CUSTOM_METHOD)) {
this.authMethods.push(CUSTOM_METHOD);
}
} else {
this.authMethods = this.authMethods.filter((element) => element !== CUSTOM_METHOD);
}
this.showSaveSnack();
});
}
})
.catch((error) => {
console.error(error);
this.toast.showError(error);
this.errorMessage = error.message;
});
});
this.docs = (await this.mgmtService.getOIDCInformation());
this.docs = await this.mgmtService.getOIDCInformation();
}
private async showSaveSnack(): Promise<void> {
@@ -330,36 +400,22 @@ export class AppDetailComponent implements OnInit, OnDestroy {
if (type === 'OIDC') {
switch (this.app.oidcConfig?.appType) {
case OIDCAppType.OIDC_APP_TYPE_NATIVE:
this.authMethods = [
PKCE_METHOD,
CUSTOM_METHOD,
];
this.authMethods = [PKCE_METHOD, CUSTOM_METHOD];
break;
case OIDCAppType.OIDC_APP_TYPE_WEB:
this.authMethods = [
PKCE_METHOD,
CODE_METHOD,
PK_JWT_METHOD,
POST_METHOD,
];
this.authMethods = [PKCE_METHOD, CODE_METHOD, PK_JWT_METHOD, POST_METHOD];
break;
case OIDCAppType.OIDC_APP_TYPE_USER_AGENT:
this.authMethods = [
PKCE_METHOD,
IMPLICIT_METHOD,
];
this.authMethods = [PKCE_METHOD, IMPLICIT_METHOD];
break;
}
}
if (type === 'API') {
this.authMethods = [
PK_JWT_METHOD,
BASIC_AUTH_METHOD,
];
this.authMethods = [PK_JWT_METHOD, BASIC_AUTH_METHOD];
}
}
public authMethodFromPartialConfig(config: { oidc?: OIDCConfig.AsObject, api?: APIConfig.AsObject; }): string {
public authMethodFromPartialConfig(config: { oidc?: OIDCConfig.AsObject; api?: APIConfig.AsObject }): string {
const key = getAuthMethodFromPartialConfig(config);
return key;
}
@@ -367,23 +423,17 @@ export class AppDetailComponent implements OnInit, OnDestroy {
public setPartialConfigFromAuthMethod(authMethod: string): void {
const partialConfig = getPartialConfigFromAuthMethod(authMethod);
if (partialConfig && partialConfig.oidc && this.app.oidcConfig) {
this.app.oidcConfig.responseTypesList =
(partialConfig.oidc as Partial<OIDCConfig.AsObject>).responseTypesList
?? [];
this.app.oidcConfig.responseTypesList = (partialConfig.oidc as Partial<OIDCConfig.AsObject>).responseTypesList ?? [];
this.app.oidcConfig.grantTypesList =
(partialConfig.oidc as Partial<OIDCConfig.AsObject>).grantTypesList
?? [];
this.app.oidcConfig.grantTypesList = (partialConfig.oidc as Partial<OIDCConfig.AsObject>).grantTypesList ?? [];
this.app.oidcConfig.authMethodType =
(partialConfig.oidc as Partial<OIDCConfig.AsObject>).authMethodType
?? OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE;
(partialConfig.oidc as Partial<OIDCConfig.AsObject>).authMethodType ?? OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE;
this.oidcForm.patchValue(this.app.oidcConfig);
} else if (partialConfig && partialConfig.api && this.app.apiConfig) {
this.app.apiConfig.authMethodType =
(partialConfig.api as Partial<APIConfig.AsObject>).authMethodType
?? APIAuthMethodType.API_AUTH_METHOD_TYPE_BASIC;
(partialConfig.api as Partial<APIConfig.AsObject>).authMethodType ?? APIAuthMethodType.API_AUTH_METHOD_TYPE_BASIC;
this.apiForm.patchValue(this.app.apiConfig);
}
@@ -399,34 +449,43 @@ export class AppDetailComponent implements OnInit, OnDestroy {
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
dialogRef.afterClosed().subscribe((resp) => {
if (resp && this.projectId && this.app.id) {
this.mgmtService.removeApp(this.projectId, this.app.id).then(() => {
this.toast.showInfo('APP.TOAST.DELETED', true);
this.mgmtService
.removeApp(this.projectId, this.app.id)
.then(() => {
this.toast.showInfo('APP.TOAST.DELETED', true);
this.router.navigate(['/projects', this.projectId]);
}).catch(error => {
this.toast.showError(error);
});
this.router.navigate(['/projects', this.projectId]);
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
public changeState(state: AppState): void {
if (state === AppState.APP_STATE_ACTIVE) {
this.mgmtService.reactivateApp(this.projectId, this.app.id).then(() => {
this.app.state = state;
this.toast.showInfo('APP.TOAST.REACTIVATED', true);
}).catch((error: any) => {
this.toast.showError(error);
});
this.mgmtService
.reactivateApp(this.projectId, this.app.id)
.then(() => {
this.app.state = state;
this.toast.showInfo('APP.TOAST.REACTIVATED', true);
})
.catch((error: any) => {
this.toast.showError(error);
});
} else if (state === AppState.APP_STATE_INACTIVE) {
this.mgmtService.deactivateApp(this.projectId, this.app.id).then(() => {
this.app.state = state;
this.toast.showInfo('APP.TOAST.DEACTIVATED', true);
}).catch((error: any) => {
this.toast.showError(error);
});
this.mgmtService
.deactivateApp(this.projectId, this.app.id)
.then(() => {
this.app.state = state;
this.toast.showInfo('APP.TOAST.DEACTIVATED', true);
})
.catch((error: any) => {
this.toast.showError(error);
});
}
}
@@ -437,7 +496,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
this.toast.showInfo('APP.TOAST.UPDATED', true);
this.editState = false;
})
.catch(error => {
.catch((error) => {
this.toast.showError(error);
});
}
@@ -450,8 +509,9 @@ export class AppDetailComponent implements OnInit, OnDestroy {
this.grantTypesList?.setValue([OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN, ...c]);
}
} else {
const index = (this.grantTypesList?.value as OIDCGrantType[])
.findIndex(gt => gt === OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN);
const index = (this.grantTypesList?.value as OIDCGrantType[]).findIndex(
(gt) => gt === OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN,
);
if (index > -1) {
const copy = Object.assign([], this.grantTypesList?.value);
copy.splice(index, 1);
@@ -460,7 +520,6 @@ export class AppDetailComponent implements OnInit, OnDestroy {
}
}
public saveOIDCApp(): void {
this.requestRedirectValuesSubject$.next();
if (this.oidcForm.valid) {
@@ -496,7 +555,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
if (this.clockSkewSeconds?.value) {
const dur = new Duration();
dur.setSeconds(Math.floor(this.clockSkewSeconds?.value));
dur.setNanos((Math.floor(this.clockSkewSeconds?.value % 1) * 10000));
dur.setNanos(Math.floor(this.clockSkewSeconds?.value % 1) * 10000);
req.setClockSkew(dur);
}
this.mgmtService
@@ -508,7 +567,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
}
this.toast.showInfo('APP.TOAST.OIDCUPDATED', true);
})
.catch(error => {
.catch((error) => {
this.toast.showError(error);
});
}
@@ -530,44 +589,59 @@ export class AppDetailComponent implements OnInit, OnDestroy {
if (this.app.apiConfig) {
const config = { api: this.app.apiConfig };
this.currentAuthMethod = this.authMethodFromPartialConfig(config);
if (this.currentAuthMethod === 'BASIC') {
this.settingsList = [{ id: 'urls', i18nKey: 'APP.URLS', featureRequired: false }];
this.currentSetting = 'urls';
} else {
this.settingsList = [
{ id: 'configuration', i18nKey: 'APP.CONFIGURATION', featureRequired: false },
{ id: 'urls', i18nKey: 'APP.URLS', featureRequired: false },
];
this.currentSetting = 'configuration';
}
}
this.toast.showInfo('APP.TOAST.APIUPDATED', true);
})
.catch(error => {
.catch((error) => {
this.toast.showError(error);
});
}
}
public regenerateOIDCClientSecret(): void {
this.mgmtService.regenerateOIDCClientSecret(this.app.id, this.projectId).then(resp => {
this.toast.showInfo('APP.TOAST.CLIENTSECRETREGENERATED', true);
this.dialog.open(AppSecretDialogComponent, {
data: {
// clientId: data.toObject() as ClientSecret.AsObject.clientId,
clientSecret: resp.clientSecret,
},
width: '400px',
this.mgmtService
.regenerateOIDCClientSecret(this.app.id, this.projectId)
.then((resp) => {
this.toast.showInfo('APP.TOAST.CLIENTSECRETREGENERATED', true);
this.dialog.open(AppSecretDialogComponent, {
data: {
// clientId: data.toObject() as ClientSecret.AsObject.clientId,
clientSecret: resp.clientSecret,
},
width: '400px',
});
})
.catch((error) => {
this.toast.showError(error);
});
}).catch(error => {
this.toast.showError(error);
});
}
public regenerateAPIClientSecret(): void {
this.mgmtService.regenerateAPIClientSecret(this.app.id, this.projectId).then(resp => {
this.toast.showInfo('APP.TOAST.CLIENTSECRETREGENERATED', true);
this.dialog.open(AppSecretDialogComponent, {
data: {
clientSecret: resp.clientSecret,
},
width: '400px',
this.mgmtService
.regenerateAPIClientSecret(this.app.id, this.projectId)
.then((resp) => {
this.toast.showInfo('APP.TOAST.CLIENTSECRETREGENERATED', true);
this.dialog.open(AppSecretDialogComponent, {
data: {
clientSecret: resp.clientSecret,
},
width: '400px',
});
})
.catch((error) => {
this.toast.showError(error);
});
}).catch(error => {
this.toast.showError(error);
});
}
public navigateBack(): void {
@@ -1,33 +1,33 @@
<h1 mat-dialog-title>
<span class="title">{{'APP.OIDC.CLIENTSECRET' | translate}}</span>
<span class="title">{{'APP.OIDC.CLIENTSECRET' | translate}}</span>
</h1>
<p class="desc">{{'APP.OIDC.CLIENTSECRET_DESCRIPTION' | translate}}</p>
<p class="desc cnsl-secondary-text">{{'APP.OIDC.CLIENTSECRET_DESCRIPTION' | translate}}</p>
<div mat-dialog-content>
<div class="flex" *ngIf="data.clientId">
<span class="overflow-auto"><span class="desc">ClientId:</span> {{data.clientId}}</span>
<button color="primary" [disabled]="copied === data.clientId" matTooltip="copy to clipboard" cnslCopyToClipboard
[valueToCopy]="data.clientId" (copiedValue)="this.copied = $event" mat-icon-button>
<i *ngIf="copied !== data.clientId" class="las la-clipboard"></i>
<i *ngIf="copied === data.clientId" class="las la-clipboard-check"></i>
</button>
</div>
<div class="flex" *ngIf="data.clientId">
<span class="overflow-auto"><span class="desc">ClientId:</span> {{data.clientId}}</span>
<button color="primary" [disabled]="copied === data.clientId" matTooltip="copy to clipboard" cnslCopyToClipboard
[valueToCopy]="data.clientId" (copiedValue)="this.copied = $event" mat-icon-button>
<i *ngIf="copied !== data.clientId" class="las la-clipboard"></i>
<i *ngIf="copied === data.clientId" class="las la-clipboard-check"></i>
</button>
</div>
<div *ngIf="data.clientSecret; else showNoSecretInfo" class="flex">
<span class="overflow-auto"><span class="desc">ClientSecret:</span> {{data.clientSecret}}</span>
<button color="primary" [disabled]="copied === data.clientSecret" matTooltip="copy to clipboard"
cnslCopyToClipboard [valueToCopy]="data.clientSecret" (copiedValue)="this.copied = $event" mat-icon-button>
<i *ngIf="copied !== data.clientSecret" class="las la-clipboard"></i>
<i *ngIf="copied === data.clientSecret" class="las la-clipboard-check"></i>
</button>
</div>
<div *ngIf="data.clientSecret; else showNoSecretInfo" class="flex">
<span class="overflow-auto"><span class="desc cnsl-secondary-text">ClientSecret:</span> {{data.clientSecret}}</span>
<button color="primary" [disabled]="copied === data.clientSecret" matTooltip="copy to clipboard" cnslCopyToClipboard
[valueToCopy]="data.clientSecret" (copiedValue)="this.copied = $event" mat-icon-button>
<i *ngIf="copied !== data.clientSecret" class="las la-clipboard"></i>
<i *ngIf="copied === data.clientSecret" class="las la-clipboard-check"></i>
</button>
</div>
<ng-template #showNoSecretInfo>
<cnsl-info-section>{{'APP.OIDC.CLIENTSECRET_NOSECRET' | translate}}</cnsl-info-section>
</ng-template>
<ng-template #showNoSecretInfo>
<cnsl-info-section>{{'APP.OIDC.CLIENTSECRET_NOSECRET' | translate}}</cnsl-info-section>
</ng-template>
</div>
<div mat-dialog-actions class="action">
<button cdkFocusInitial color="primary" mat-raised-button class="ok-button" (click)="closeDialog()">
{{'ACTIONS.CLOSE' | translate}}
</button>
<button cdkFocusInitial color="primary" mat-raised-button class="ok-button" (click)="closeDialog()"
[attr.data-e2e]="'close-dialog'">
{{'ACTIONS.CLOSE' | translate}}
</button>
</div>
@@ -3,8 +3,7 @@
}
.desc {
color: var(--grey);
font-size: .9rem;
font-size: 0.9rem;
}
.full-width {
@@ -16,7 +15,7 @@
justify-content: flex-end;
.ok-button {
margin-left: .5rem;
margin-left: 0.5rem;
}
}
@@ -24,7 +23,8 @@
display: flex;
align-items: center;
border: 1px solid #ffffff20;
border-radius: .5rem;
border-radius: 0.5rem;
padding-left: 0.5rem;
justify-content: space-between;
.overflow-auto {
@@ -32,7 +32,6 @@
.desc {
font-size: 14px;
color: var(--grey);
}
}
}
@@ -5,20 +5,20 @@ import { AppCreateComponent } from '../apps/app-create/app-create.component';
import { AppDetailComponent } from '../apps/app-detail/app-detail.component';
const routes: Routes = [
{
path: 'create',
component: AppCreateComponent,
data: { animation: 'AddPage' },
},
{
path: ':id',
component: AppDetailComponent,
data: { animation: 'HomePage' },
},
{
path: 'create',
component: AppCreateComponent,
data: { animation: 'AddPage' },
},
{
path: ':appid',
component: AppDetailComponent,
data: { animation: 'HomePage' },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class AppsRoutingModule { }
export class AppsRoutingModule {}
@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips';
import { MatDialogModule } from '@angular/material/dialog';
@@ -30,6 +29,9 @@ import { InputModule } from 'src/app/modules/input/input.module';
import { LinksModule } from 'src/app/modules/links/links.module';
import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module';
import { NameDialogModule } from 'src/app/modules/name-dialog/name-dialog.module';
import { SidenavModule } from 'src/app/modules/sidenav/sidenav.module';
import { TopViewModule } from 'src/app/modules/top-view/top-view.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { OriginPipeModule } from 'src/app/pipes/origin-pipe/origin-pipe.module';
import { RedirectPipeModule } from 'src/app/pipes/redirect-pipe/redirect-pipe.module';
@@ -60,21 +62,22 @@ import { RedirectUrisComponent } from './redirect-uris/redirect-uris.component';
InfoRowModule,
TranslateModule,
OriginPipeModule,
MatMenuModule,
ReactiveFormsModule,
HasRoleModule,
MatMenuModule,
SidenavModule,
MatChipsModule,
ClientKeysModule,
HasRolePipeModule,
MatIconModule,
MatSelectModule,
MatButtonToggleModule,
MatButtonModule,
MatProgressSpinnerModule,
MatProgressBarModule,
MatDialogModule,
MatCheckboxModule,
CardModule,
TopViewModule,
MatMenuModule,
MatTooltipModule,
TranslateModule,
MatStepperModule,
@@ -89,4 +92,4 @@ import { RedirectUrisComponent } from './redirect-uris/redirect-uris.component';
],
exports: [TranslateModule],
})
export class AppsModule { }
export class AppsModule {}
@@ -14,7 +14,7 @@ export const CODE_METHOD: RadioItemAuthType = {
descI18nKey: 'APP.AUTHMETHODS.CODE.DESCRIPTION',
disabled: false,
prefix: 'CODE',
background: 'rgb(89 115 128)',
background: 'linear-gradient(40deg, rgb(25 105 143) 30%, rgb(23 95 129))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
grantType: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_BASIC,
@@ -26,7 +26,7 @@ export const PKCE_METHOD: RadioItemAuthType = {
descI18nKey: 'APP.AUTHMETHODS.PKCE.DESCRIPTION',
disabled: false,
prefix: 'PKCE',
background: 'rgb(80 110 92)',
background: 'linear-gradient(40deg, #059669 30%, #047857)',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
grantType: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
@@ -38,7 +38,7 @@ export const POST_METHOD: RadioItemAuthType = {
descI18nKey: 'APP.AUTHMETHODS.POST.DESCRIPTION',
disabled: false,
prefix: 'POST',
background: 'rgb(144 75 75)',
background: 'linear-gradient(40deg, #c53b3b 30%, rgb(169 51 51))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
grantType: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST,
@@ -50,7 +50,7 @@ export const PK_JWT_METHOD: RadioItemAuthType = {
descI18nKey: 'APP.AUTHMETHODS.PK_JWT.DESCRIPTION',
disabled: false,
prefix: 'JWT',
background: 'rgb(89, 93, 128)',
background: 'linear-gradient(40deg, rgb(70 77 145) 30%, rgb(58 65 124))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
grantType: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
@@ -63,7 +63,7 @@ export const BASIC_AUTH_METHOD: RadioItemAuthType = {
descI18nKey: 'APP.AUTHMETHODS.BASIC.DESCRIPTION',
disabled: false,
prefix: 'BASIC',
background: 'rgb(144 75 75)',
background: 'linear-gradient(40deg, #c53b3b 30%, rgb(169 51 51))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
grantType: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST,
@@ -76,7 +76,7 @@ export const IMPLICIT_METHOD: RadioItemAuthType = {
descI18nKey: 'APP.AUTHMETHODS.IMPLICIT.DESCRIPTION',
disabled: false,
prefix: 'IMP',
background: 'rgb(144 75 75)',
background: 'linear-gradient(40deg, #c53b3b 30%, rgb(169 51 51))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_ID_TOKEN,
grantType: OIDCGrantType.OIDC_GRANT_TYPE_IMPLICIT,
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
@@ -89,16 +89,18 @@ export const CUSTOM_METHOD: RadioItemAuthType = {
descI18nKey: 'APP.AUTHMETHODS.CUSTOM.DESCRIPTION',
disabled: false,
prefix: 'CUSTOM',
background: '#333',
background: 'linear-gradient(40deg, #1f2937, #111827)',
};
export function getPartialConfigFromAuthMethod(authMethod: string): {
oidc?: Partial<OIDCConfig.AsObject>;
api?: Partial<APIConfig.AsObject>;
} | undefined {
export function getPartialConfigFromAuthMethod(authMethod: string):
| {
oidc?: Partial<OIDCConfig.AsObject>;
api?: Partial<APIConfig.AsObject>;
}
| undefined {
let config: {
oidc?: Partial<OIDCConfig.AsObject>,
api?: Partial<APIConfig.AsObject>,
oidc?: Partial<OIDCConfig.AsObject>;
api?: Partial<APIConfig.AsObject>;
};
switch (authMethod) {
case CODE_METHOD.key:
@@ -167,107 +169,98 @@ export function getPartialConfigFromAuthMethod(authMethod: string): {
}
}
export function getAuthMethodFromPartialConfig(
config: {
oidc?: Partial<OIDCConfig.AsObject>,
api?: Partial<APIConfig.AsObject>,
},
): string {
export function getAuthMethodFromPartialConfig(config: {
oidc?: Partial<OIDCConfig.AsObject>;
api?: Partial<APIConfig.AsObject>;
}): string {
if (config?.oidc) {
const toCheck = [config.oidc.responseTypesList, config.oidc.grantTypesList?.sort(), config.oidc.authMethodType];
const code = JSON.stringify(
[
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_BASIC,
],
);
const code = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_BASIC,
]);
const codeWithRefresh = JSON.stringify(
[
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN].sort(),
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_BASIC,
],
);
const codeWithRefresh = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN].sort(),
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_BASIC,
]);
const pkce = JSON.stringify(
[
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
],
);
const pkce = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
]);
const pkceWithRefresh = JSON.stringify(
[
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN].sort(),
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
],
);
const pkceWithRefresh = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN].sort(),
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
]);
const post = JSON.stringify(
[
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST,
],
);
const post = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST,
]);
const postWithRefresh = JSON.stringify(
[
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN].sort(),
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST,
],
);
const postWithRefresh = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN].sort(),
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST,
]);
const pkjwt = JSON.stringify(
[
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
],
);
const pkjwt = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
]);
const pkjwtWithRefresh = JSON.stringify(
[
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN].sort(),
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
],
);
const pkjwtWithRefresh = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN].sort(),
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
]);
const implicit = JSON.stringify(
[
[OIDCResponseType.OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN],
[OIDCGrantType.OIDC_GRANT_TYPE_IMPLICIT],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
],
);
const implicit = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN],
[OIDCGrantType.OIDC_GRANT_TYPE_IMPLICIT],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
]);
switch (JSON.stringify(toCheck)) {
case code: return CODE_METHOD.key;
case codeWithRefresh: return CODE_METHOD.key;
case code:
return CODE_METHOD.key;
case codeWithRefresh:
return CODE_METHOD.key;
case pkce: return PKCE_METHOD.key;
case pkceWithRefresh: return PKCE_METHOD.key;
case pkce:
return PKCE_METHOD.key;
case pkceWithRefresh:
return PKCE_METHOD.key;
case post: return POST_METHOD.key;
case postWithRefresh: return POST_METHOD.key;
case post:
return POST_METHOD.key;
case postWithRefresh:
return POST_METHOD.key;
case pkjwt: return PK_JWT_METHOD.key;
case pkjwtWithRefresh: return PK_JWT_METHOD.key;
case pkjwt:
return PK_JWT_METHOD.key;
case pkjwtWithRefresh:
return PK_JWT_METHOD.key;
case implicit: return IMPLICIT_METHOD.key;
case implicit:
return IMPLICIT_METHOD.key;
default:
return CUSTOM_METHOD.key;
}
} else if (config.api && config.api.authMethodType !== undefined) {
switch (config.api.authMethodType.toString()) {
case APIAuthMethodType.API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT.toString(): return PK_JWT_METHOD.key;
case APIAuthMethodType.API_AUTH_METHOD_TYPE_BASIC.toString(): return BASIC_AUTH_METHOD.key;
case APIAuthMethodType.API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT.toString():
return PK_JWT_METHOD.key;
case APIAuthMethodType.API_AUTH_METHOD_TYPE_BASIC.toString():
return BASIC_AUTH_METHOD.key;
default:
return CUSTOM_METHOD.key;
}
@@ -8,55 +8,55 @@ import { OIDCAppType } from 'src/app/proto/generated/zitadel/app_pb';
// }
export enum AppCreateType {
API = 'API',
OIDC = 'OIDC',
API = 'API',
OIDC = 'OIDC',
}
export interface RadioItemAppType {
// key: string;
createType: AppCreateType;
oidcAppType?: OIDCAppType;
titleI18nKey: string;
descI18nKey: string;
prefix: string;
background: string;
// key: string;
createType: AppCreateType;
oidcAppType?: OIDCAppType;
titleI18nKey: string;
descI18nKey: string;
prefix: string;
background: string;
}
export const WEB_TYPE = {
// key: AppType.WEB,
titleI18nKey: 'APP.OIDC.SELECTION.APPTYPE.WEB.TITLE',
descI18nKey: 'APP.OIDC.SELECTION.APPTYPE.WEB.DESCRIPTION',
createType: AppCreateType.OIDC,
oidcAppType: OIDCAppType.OIDC_APP_TYPE_WEB,
prefix: 'WEB',
background: 'rgb(80, 110, 110)',
export const WEB_TYPE: RadioItemAppType = {
// key: AppType.WEB,
titleI18nKey: 'APP.OIDC.SELECTION.APPTYPE.WEB.TITLE',
descI18nKey: 'APP.OIDC.SELECTION.APPTYPE.WEB.DESCRIPTION',
createType: AppCreateType.OIDC,
oidcAppType: OIDCAppType.OIDC_APP_TYPE_WEB,
prefix: 'WEB',
background: 'linear-gradient(40deg, #059669 30%, #047857)',
};
export const USER_AGENT_TYPE = {
// key: AppType.USER_AGENT,
titleI18nKey: 'APP.OIDC.SELECTION.APPTYPE.USERAGENT.TITLE',
descI18nKey: 'APP.OIDC.SELECTION.APPTYPE.USERAGENT.DESCRIPTION',
createType: AppCreateType.OIDC,
oidcAppType: OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
prefix: 'UA',
background: '#6a506e',
export const USER_AGENT_TYPE: RadioItemAppType = {
// key: AppType.USER_AGENT,
titleI18nKey: 'APP.OIDC.SELECTION.APPTYPE.USERAGENT.TITLE',
descI18nKey: 'APP.OIDC.SELECTION.APPTYPE.USERAGENT.DESCRIPTION',
createType: AppCreateType.OIDC,
oidcAppType: OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
prefix: 'UA',
background: 'linear-gradient(40deg, #dc2626 30%, #db2777)',
};
export const NATIVE_TYPE = {
// key: AppType.NATIVE,
titleI18nKey: 'APP.OIDC.SELECTION.APPTYPE.NATIVE.TITLE',
descI18nKey: 'APP.OIDC.SELECTION.APPTYPE.NATIVE.DESCRIPTION',
createType: AppCreateType.OIDC,
oidcAppType: OIDCAppType.OIDC_APP_TYPE_NATIVE,
prefix: 'N',
background: '#595d80',
export const NATIVE_TYPE: RadioItemAppType = {
// key: AppType.NATIVE,
titleI18nKey: 'APP.OIDC.SELECTION.APPTYPE.NATIVE.TITLE',
descI18nKey: 'APP.OIDC.SELECTION.APPTYPE.NATIVE.DESCRIPTION',
createType: AppCreateType.OIDC,
oidcAppType: OIDCAppType.OIDC_APP_TYPE_NATIVE,
prefix: 'N',
background: 'linear-gradient(40deg, #306ccc 30%, #4f46e5)',
};
export const API_TYPE = {
// key: AppType.API,
titleI18nKey: 'APP.API.SELECTION.TITLE',
descI18nKey: 'APP.API.SELECTION.DESCRIPTION',
createType: AppCreateType.API,
prefix: 'API',
background: 'rgb(73,73,73)',
export const API_TYPE: RadioItemAppType = {
// key: AppType.API,
titleI18nKey: 'APP.API.SELECTION.TITLE',
descI18nKey: 'APP.API.SELECTION.DESCRIPTION',
createType: AppCreateType.API,
prefix: 'API',
background: 'linear-gradient(40deg, #1f2937, #111827)',
};
@@ -1,4 +1,4 @@
<form class="form" (ngSubmit)="add(redInput)">
<form class="form" (ngSubmit)="add(redInput)" [attr.data-e2e]="'redirect-uris'">
<cnsl-form-field class="formfield">
<cnsl-label>{{ title }}</cnsl-label>
@@ -9,12 +9,12 @@
button {
margin-bottom: 14px;
margin-right: -.5rem;
margin-right: -0.5rem;
}
}
.uri-list {
margin: 0 .5rem;
margin: 0 0.5rem;
width: 100%;
.uri-line {
@@ -24,6 +24,7 @@
.uri {
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
font-size: 14px;
overflow-x: auto;
}
.fill-space {
@@ -58,5 +59,5 @@
.error {
font-size: 13px;
color: var(--warn);
margin: 0 .5rem 1.5rem .5rem;
margin: 0 0.5rem 1.5rem 0.5rem;
}
@@ -1,56 +1,56 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
@Component({
selector: 'cnsl-redirect-uris',
templateUrl: './redirect-uris.component.html',
styleUrls: ['./redirect-uris.component.scss'],
selector: 'cnsl-redirect-uris',
templateUrl: './redirect-uris.component.html',
styleUrls: ['./redirect-uris.component.scss'],
})
export class RedirectUrisComponent implements OnInit, OnDestroy {
@Input() title: string = '';
@Input() devMode: boolean = false;
@Input() canWrite: boolean = false;
@Input() isNative!: boolean;
@Input() public urisList: string[] = [];
@Input() public redirectControl: FormControl = new FormControl({ value: '', disabled: true });
@Input() public changedUris: EventEmitter<string[]> = new EventEmitter();
@Input() public getValues: Observable<void> = new Observable();
@Input() title: string = '';
@Input() devMode: boolean = false;
@Input() canWrite: boolean = false;
@Input() isNative!: boolean;
@Input() public urisList: string[] = [];
@Input() public redirectControl: FormControl = new FormControl({ value: '', disabled: true });
@Output() public changedUris: EventEmitter<string[]> = new EventEmitter();
@Input() public getValues: Observable<void> = new Observable();
@ViewChild('redInput') input!: any;
private sub: Subscription = new Subscription();
constructor() { }
@ViewChild('redInput') input!: any;
private sub: Subscription = new Subscription();
constructor() {}
ngOnInit(): void {
if (this.canWrite) {
this.redirectControl.enable();
}
this.sub = this.getValues.subscribe(() => {
this.add(this.input.nativeElement);
});
ngOnInit(): void {
if (this.canWrite) {
this.redirectControl.enable();
}
ngOnDestroy(): void {
this.sub.unsubscribe();
}
this.sub = this.getValues.subscribe(() => {
this.add(this.input.nativeElement);
});
}
public add(input: any): void {
if (this.redirectControl.valid) {
if (input.value !== '' && input.value !== ' ' && input.value !== '/') {
this.urisList.push(input.value);
}
if (input) {
input.value = '';
}
}
}
ngOnDestroy(): void {
this.sub.unsubscribe();
}
public remove(redirect: any): void {
const index = this.urisList.indexOf(redirect);
if (index >= 0) {
this.urisList.splice(index, 1);
}
public add(input: any): void {
if (this.redirectControl.valid) {
if (input.value !== '' && input.value !== ' ' && input.value !== '/') {
this.urisList.push(input.value);
}
if (input) {
input.value = '';
}
}
}
public remove(redirect: any): void {
const index = this.urisList.indexOf(redirect);
if (index >= 0) {
this.urisList.splice(index, 1);
}
}
}
@@ -1,55 +1,39 @@
<cnsl-meta-layout>
<div class="max-width-container">
<div class="head" *ngIf="project?.projectId">
<a [routerLink]="[ '/granted-projects' ]" mat-icon-button>
<mat-icon class="icon">arrow_back</mat-icon>
</a>
<h1 class="h1">{{ 'PROJECT.PAGES.TITLE' | translate }} {{project?.projectName}}</h1>
<cnsl-top-view title="{{project?.projectName}}" [hasActions]="false"
docLink="https://docs.zitadel.ch/docs/guides/basics/projects#what-is-a-granted-project"
sub="{{'PROJECT.PAGES.TYPE.GRANTED_SINGULAR' | translate}} {{'ACTIONS.OF' | translate}} <strong>{{project?.projectOwnerName}}</strong>"
[isActive]="project?.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE"
[isInactive]="project?.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE"
stateTooltip="{{'ORG.STATE.'+project.state | translate}}" [hasContributors]="true">
<p topContent *ngIf="isZitadel" class="granted-project-sub zitadel-warning">
{{'PROJECT.PAGES.ZITADELPROJECT' | translate}}
</p>
<cnsl-contributors topContributors class="project-contributors" *ngIf="project" [loading]="loading$ | async"
[totalResult]="totalMemberResult" [membersSubject]="membersSubject" title="{{ 'PROJECT.MEMBER.TITLE' | translate }}"
description="{{ 'PROJECT.MEMBER.TITLEDESC' | translate }}" (addClicked)="openAddMember()"
(showDetailClicked)="showDetail()" (refreshClicked)="loadMembers()"
[disabled]="(['project.member.write$', 'project.member.write:'+ project.projectId]| hasRole | async) === false">
</cnsl-contributors>
<cnsl-info-row topContent *ngIf="project" [grantedProject]="project"></cnsl-info-row>
</cnsl-top-view>
<div class="full-width">
<p class="desc">{{ 'PROJECT.PAGES.DESCRIPTION' | translate }}</p>
<p *ngIf="isZitadel" class="zitadel-warning">{{'PROJECT.PAGES.ZITADELPROJECT' | translate}}</p>
</div>
</div>
<div class="max-width-container">
<cnsl-meta-layout>
<ng-template cnslHasRole [hasRole]="['user.grant.read', 'user.grant.read:'+grantId]">
<cnsl-card *ngIf="project?.projectId" title="{{ 'GRANTS.PROJECT.TITLE' | translate }}"
description="{{'GRANTS.PROJECT.DESCRIPTION' | translate }}">
<cnsl-user-grants *ngIf="projectId && grantId" [context]="UserGrantContext.GRANTED_PROJECT"
[projectId]="projectId" [grantId]="grantId"
[displayedColumns]="['select','user', 'projectId', 'creationDate','changeDate', 'roleNamesList','actions']"
[disableWrite]="(['user.grant.write$','user.grant.write:'+grantId] | hasRole | async) === false"
[disableDelete]="(['user.grant.delete$','user.grant.delete:'+grantId] | hasRole | async) === false"
[refreshOnPreviousRoutes]="['/grant-create/project/{{projectId}}/grant/{{grantId}}']">
</cnsl-user-grants>
</cnsl-card>
</ng-template>
<ng-template cnslHasRole [hasRole]="['user.grant.read', 'user.grant.read:'+grantId]">
<cnsl-card *ngIf="project?.projectId" title="{{ 'GRANTS.PROJECT.TITLE' | translate }}"
description="{{'GRANTS.PROJECT.DESCRIPTION' | translate }}">
<cnsl-user-grants *ngIf="projectId && grantId" [context]="UserGrantContext.GRANTED_PROJECT"
[projectId]="projectId" [grantId]="grantId"
[displayedColumns]="['select','user', 'projectId', 'dates', 'roleNamesList']"
[disableWrite]="(['user.grant.write$','user.grant.write:'+grantId] | hasRole | async) === false"
[disableDelete]="(['user.grant.delete$','user.grant.delete:'+grantId] | hasRole | async) === false"
[refreshOnPreviousRoutes]="['/grant-create/project/{{projectId}}/grant/{{grantId}}']">
</cnsl-user-grants>
</cnsl-card>
</ng-template>
<div metainfo>
<cnsl-changes *ngIf="project" [changeType]="ChangeType.PROJECT" [id]="project.projectId"></cnsl-changes>
</div>
<div class="side" metainfo>
<div class="meta-details">
<div class="meta-row">
<span class="first">{{'RESOURCEID' | translate}}:</span>
<span *ngIf="projectId" class="second">{{ project?.grantId }}</span>
</div>
<div class="meta-row">
<span class="first">{{'PROJECT.STATE.TITLE' | translate}}:</span>
<span *ngIf="project && project.state !== undefined" class="state"
[ngClass]="{'active': project.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE, 'inactive': project.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE}">{{'PROJECT.STATE.'+project.state | translate}}</span>
</div>
</div>
<mat-tab-group mat-stretch-tabs class="tab-group" [disablePagination]="true">
<mat-tab label="Details">
<cnsl-contributors *ngIf="project" [loading]="loading$ | async" [totalResult]="totalMemberResult"
[membersSubject]="membersSubject" title="{{ 'PROJECT.MEMBER.TITLE' | translate }}"
description="{{ 'PROJECT.MEMBER.TITLEDESC' | translate }}" (addClicked)="openAddMember()"
(showDetailClicked)="showDetail()" (refreshClicked)="loadMembers()"
[disabled]="(['project.member.write$', 'project.member.write:'+ project.projectId]| hasRole | async) === false">
</cnsl-contributors>
</mat-tab>
<mat-tab label="{{ 'CHANGES.PROJECT.TITLE' | translate }}" class="meta-flex-col">
<cnsl-changes *ngIf="project" [changeType]="ChangeType.PROJECT" [id]="project.projectId"></cnsl-changes>
</mat-tab>
</mat-tab-group>
</div>
</cnsl-meta-layout>
</cnsl-meta-layout>
</div>
@@ -1,41 +1,14 @@
.head {
display: flex;
align-items: center;
border-bottom: 1px solid #ffffff20;
flex-wrap: wrap;
margin-bottom: 1rem;
@use '@angular/material' as mat;
a {
display: block;
}
@mixin granted-project-detail-theme($theme) {
$foreground: map-get($theme, foreground);
$background: map-get($theme, background);
$is-dark-theme: map-get($theme, is-dark);
$warn: map-get($theme, warn);
$warn-color: map-get($warn, 500);
.h1 {
font-size: 2rem;
margin: 0 1rem;
margin-left: 2rem;
font-weight: normal;
}
.full-width {
padding-top: 1rem;
width: 100%;
display: block;
.desc {
font-size: .9rem;
color: var(--grey);
}
.zitadel-warning {
font-size: 14px;
color: rgb(201, 51, 71);
}
.zitadel-warning {
font-size: 14px;
color: $warn-color;
}
}
.desc {
width: 100%;
display: block;
font-size: .9rem;
color: var(--grey);
}
@@ -11,6 +11,7 @@ import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-dataso
import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { GrantedProject, ProjectGrantState } from 'src/app/proto/generated/zitadel/project_pb';
import { User } from 'src/app/proto/generated/zitadel/user_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -35,8 +36,7 @@ export class GrantedProjectDetailComponent implements OnInit, OnDestroy {
// members
public totalMemberResult: number = 0;
public membersSubject: BehaviorSubject<Member.AsObject[]>
= new BehaviorSubject<Member.AsObject[]>([]);
public membersSubject: BehaviorSubject<Member.AsObject[]> = new BehaviorSubject<Member.AsObject[]>([]);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
@@ -48,11 +48,11 @@ export class GrantedProjectDetailComponent implements OnInit, OnDestroy {
private _location: Location,
private router: Router,
private dialog: MatDialog,
) {
}
private breadcrumbService: BreadcrumbService,
) {}
public ngOnInit(): void {
this.subscription = this.route.params.subscribe(params => this.getData(params));
this.subscription = this.route.params.subscribe((params) => this.getData(params));
}
public ngOnDestroy(): void {
@@ -63,18 +63,41 @@ export class GrantedProjectDetailComponent implements OnInit, OnDestroy {
this.projectId = id;
this.grantId = grantId;
this.mgmtService.getIAM().then(iam => {
const breadcrumbs = [
new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'IAM',
routerLink: ['/system'],
}),
new Breadcrumb({
type: BreadcrumbType.ORG,
routerLink: ['/org'],
}),
new Breadcrumb({
type: BreadcrumbType.GRANTEDPROJECT,
name: '',
param: { key: 'id', value: id },
routerLink: ['/granted-projects', id],
isZitadel: this.isZitadel,
}),
];
this.breadcrumbService.setBreadcrumb(breadcrumbs);
this.mgmtService.getIAM().then((iam) => {
this.isZitadel = iam.iamProjectId === this.projectId;
});
if (this.projectId && this.grantId) {
this.mgmtService.getGrantedProjectByID(this.projectId, this.grantId).then(proj => {
if (proj.grantedProject) {
this.project = proj.grantedProject;
}
}).catch(error => {
this.toast.showError(error);
});
this.mgmtService
.getGrantedProjectByID(this.projectId, this.grantId)
.then((proj) => {
if (proj.grantedProject) {
this.project = proj.grantedProject;
}
})
.catch((error) => {
this.toast.showError(error);
});
this.loadMembers();
}
@@ -82,9 +105,9 @@ export class GrantedProjectDetailComponent implements OnInit, OnDestroy {
public loadMembers(): void {
this.loadingSubject.next(true);
from(this.mgmtService.listProjectGrantMembers(this.projectId,
this.grantId, 100, 0)).pipe(
map(resp => {
from(this.mgmtService.listProjectGrantMembers(this.projectId, this.grantId, 100, 0))
.pipe(
map((resp) => {
if (resp.details?.totalResult) {
this.totalMemberResult = resp.details.totalResult;
} else {
@@ -94,7 +117,8 @@ export class GrantedProjectDetailComponent implements OnInit, OnDestroy {
}),
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)),
).subscribe(members => {
)
.subscribe((members) => {
this.membersSubject.next(members);
});
}
@@ -111,26 +135,24 @@ export class GrantedProjectDetailComponent implements OnInit, OnDestroy {
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
const users: User.AsObject[] = resp.users;
const roles: string[] = resp.roles;
if (users && users.length && roles && roles.length) {
users.forEach(user => {
return this.mgmtService.addProjectGrantMember(
this.projectId,
this.grantId,
user.id,
roles,
).then(() => {
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
setTimeout(() => {
this.loadMembers();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
users.forEach((user) => {
return this.mgmtService
.addProjectGrantMember(this.projectId, this.grantId, user.id, roles)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
setTimeout(() => {
this.loadMembers();
}, 1000);
})
.catch((error) => {
this.toast.showError(error);
});
});
}
}
@@ -1,70 +0,0 @@
<div class="view-toggle">
<button (click)="changedView.emit(true)" matTooltip="{{'PROJECT.TABLE.SHOWTABLE' | translate}}" mat-icon-button>
<i class="show list view las la-th-list"></i>
</button>
</div>
<div class="granted-project-grid-container">
<mat-progress-bar *ngIf="loading" class="spinner" color="primary" mode="indeterminate"></mat-progress-bar>
<p class="n-items" *ngIf="!loading && selection.selected.length > 0">{{'PROJECT.PAGES.PINNED' | translate}}</p>
<div class="item card" *ngFor="let item of selection.selected; index as i"
(click)="navigateToProject(item.projectId,item.grantId, $event)">
<div class="text-part">
<span *ngIf="item.details && item.details.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
{{
item.details.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span>
<div class="name-row">
<span class="name" *ngIf="item.projectName">{{ item.projectName }}</span>
<div class="state-dot" [ngClass]="{'active': item.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE, 'inactive': item.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE}"></div>
</div>
<span class="description" *ngIf="item.projectOwnerName">{{item.projectOwnerName}}</span>
<span *ngIf="item.details && item.details.creationDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
{{ item.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span>
<span class="fill-space"></span>
</div>
<template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{key: item}"></template>
</div>
</div>
<div class="granted-project-grid-container">
<p class="n-items" *ngIf="!loading && notPinned.length > 0">{{'PROJECT.PAGES.ALL' | translate}}</p>
<div class="item card" *ngFor="let item of notPinned; index as i"
(click)="navigateToProject(item.projectId,item.grantId, $event)">
<div class="text-part">
<span *ngIf="item.details && item.details.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
{{
item.details.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span>
<div class="name-row">
<span class="name" *ngIf="item.projectName">{{ item.projectName }}</span>
<div class="state-dot" [ngClass]="{'active': item.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE, 'inactive': item.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE}"></div>
</div>
<span class="description" *ngIf="item.projectOwnerName">{{item.projectOwnerName}}</span>
<span *ngIf="item.details && item.details.creationDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
{{ item.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
<span class="fill-space"></span>
</div>
<button [ngClass]="{ selected: selection.isSelected(item)}"
(click)="selection.toggle(item); $event.stopPropagation()" class="edit-button" mat-icon-button>
<mat-icon *ngIf="selection.isSelected(item)" svgIcon="mdi_pin"></mat-icon>
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(item)"></mat-icon>
</button>
</div>
<p class="n-items" *ngIf="!loading && items.length === 0 && selection.selected.length === 0">
{{'PROJECT.PAGES.NOITEMS' | translate}}</p>
</div>
<ng-template #toggleButton let-key="key">
<button matTooltip="{{'ACTIONS.PIN' | translate}}" [ngClass]="{ selected: selection.isSelected(key)}"
(click)="selection.toggle(key); $event.stopPropagation()" class="edit-button" mat-icon-button>
<mat-icon *ngIf="selection.isSelected(key)" svgIcon="mdi_pin"></mat-icon>
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(key)"></mat-icon>
</button>
</ng-template>
@@ -1,181 +0,0 @@
@mixin granted-project-grid-theme($theme) {
$foreground: map-get($theme, foreground);
.view-toggle {
border-bottom: 1px solid map-get($foreground, divider);
}
}
.view-toggle {
width: 100%;
display: flex;
justify-content: flex-end;
padding-bottom: .5rem;
.anim-list {
display: flex;
justify-content: flex-end;
align-items: center;
}
button {
&.left-button {
margin-right: 1rem;
}
}
}
.spinner {
margin: 1rem;
}
.granted-project-grid-container {
display: flex;
flex-wrap: wrap;
margin: 0 -1rem;
box-sizing: border-box;
h2 {
padding: 0 1rem;
flex-basis: 100%;
}
.item {
position: relative;
z-index: 100;
margin: 1rem;
flex-basis: 270px;
display: flex;
text-decoration: none;
cursor: pointer;
padding-top: 0;
padding-right: 0;
padding-bottom: 0;
padding-left: 1rem;
border-radius: .5rem;
box-sizing: border-box;
min-height: 166px;
transition: box-shadow .1s ease-in;
&.inactive {
color: var(--grey);
}
img {
height: 50px;
width: 50px;
margin: 1rem;
}
.text-part {
flex: 1;
display: flex;
flex-direction: column;
min-height: 70px;
padding: .5rem 1rem 0 0;
.top {
font-size: .8rem;
margin-bottom: 0;
margin-top: .5rem;
color: var(--grey);
}
.name-row {
display: flex;
align-items: center;
margin: 1rem 0 .5rem 0;
.name {
font-size: 1.2rem;
margin-right: .5rem;
}
.state-dot {
height: 8px;
width: 8px;
border-radius: 50%;
&.active {
background-color: var(--success);
}
&.inactive {
background-color: var(--warn);
}
}
}
.description {
font-size: .8rem;
}
.created {
font-size: .8rem;
color: var(--grey);
}
.organization {
display: flex;
align-items: center;
margin-top: 1rem;
.org_avatar {
height: 25px;
width: 25px;
border-radius: 50%;
margin: 0;
margin-right: 1rem;
}
}
.fill-space {
flex: 1;
}
span {
margin: 2px 0;
}
}
.edit-button {
opacity: 0;
user-select: none;
position: absolute;
bottom: 0;
right: 0;
margin: 0;
margin-bottom: .25rem;
color: var(--grey);
&.selected {
opacity: 1;
}
}
&:hover {
box-shadow: 0 5px 10px rgba(0, 0, 0, .12);
.edit-button {
opacity: 1;
}
}
&.selected {
.edit-button {
opacity: 1;
}
}
@media only screen and (max-width: 450px) {
flex-basis: 100%;
}
}
}
.n-items {
flex-basis: 100%;
padding: 0 1rem;
font-size: .8rem;
color: var(--grey);
}
@@ -1,25 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { GrantedProjectGridComponent } from './granted-project-grid.component';
describe('GridComponent', () => {
let component: GrantedProjectGridComponent;
let fixture: ComponentFixture<GrantedProjectGridComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [GrantedProjectGridComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GrantedProjectGridComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,100 +0,0 @@
import { animate, animateChild, query, stagger, style, transition, trigger } from '@angular/animations';
import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { Router } from '@angular/router';
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
import { GrantedProject, ProjectGrantState } from 'src/app/proto/generated/zitadel/project_pb';
import { StorageKey, StorageLocation, StorageService } from 'src/app/services/storage.service';
@Component({
selector: 'cnsl-granted-project-grid',
templateUrl: './granted-project-grid.component.html',
styleUrls: ['./granted-project-grid.component.scss'],
animations: [
trigger('list', [
transition(':enter', [
query('@animate',
stagger(100, animateChild()),
),
]),
]),
trigger('animate', [
transition(':enter', [
style({ opacity: 0, transform: 'translateY(-100%)' }),
animate('100ms', style({ opacity: 1, transform: 'translateY(0)' })),
]),
transition(':leave', [
style({ opacity: 1, transform: 'translateY(0)' }),
animate('100ms', style({ opacity: 0, transform: 'translateY(100%)' })),
]),
]),
],
})
export class GrantedProjectGridComponent implements OnChanges {
@Input() items: Array<GrantedProject.AsObject> = [];
public notPinned: Array<GrantedProject.AsObject> = [];
@Output() newClicked: EventEmitter<boolean> = new EventEmitter();
@Output() changedView: EventEmitter<boolean> = new EventEmitter();
@Input() loading: boolean | null = false;
public selection: SelectionModel<GrantedProject.AsObject> = new SelectionModel<GrantedProject.AsObject>(true, []);
public showNewProject: boolean = false;
public ProjectGrantState: any = ProjectGrantState;
constructor(private storage: StorageService, private router: Router) {
this.selection.changed.subscribe(selection => {
this.setPrefixedItem('pinned-granted-projects', JSON.stringify(
this.selection.selected.map(item => item.projectId),
)).then(() => {
selection.added.forEach(item => {
const index = this.notPinned.findIndex(i => i.projectId === item.projectId);
this.notPinned.splice(index, 1);
});
this.notPinned.push(...selection.removed);
});
});
}
public addItem(): void {
this.newClicked.emit(true);
}
public ngOnChanges(changes: SimpleChanges): void {
if (changes.items?.currentValue && changes.items.currentValue.length > 0) {
this.notPinned = Object.assign([], this.items);
this.reorganizeItems();
}
}
public reorganizeItems(): void {
this.getPrefixedItem('pinned-granted-projects').then(storageEntry => {
if (storageEntry) {
const array: string[] = JSON.parse(storageEntry);
const toSelect: GrantedProject.AsObject[] = this.items.filter((item) => {
if (array.includes(item.projectId)) {
return true;
} else {
return false;
}
});
this.selection.select(...toSelect);
}
});
}
private async getPrefixedItem(key: string): Promise<string | null> {
const org = this.storage.getItem<Org.AsObject>(StorageKey.organization, StorageLocation.session) as Org.AsObject;
return localStorage.getItem(`${org?.id}:${key}`);
}
private async setPrefixedItem(key: string, value: any): Promise<void> {
const org = this.storage.getItem<Org.AsObject>(StorageKey.organization, StorageLocation.session) as Org.AsObject;
return localStorage.setItem(`${org.id}:${key}`, value);
}
public navigateToProject(projectId: string, id: string, event: any): void {
if (event && event.srcElement && event.srcElement.localName !== 'button') {
this.router.navigate(['/granted-projects', projectId, 'grant', id]);
}
}
}
@@ -1,81 +0,0 @@
<cnsl-granted-project-grid *ngIf="grid" [loading]="loading$ | async" (changedView)="grid = false"
[items]="grantedProjectList">
</cnsl-granted-project-grid>
<div *ngIf="!grid" class="view-toggle">
<button class="icon-button" (click)="grid = true" mat-icon-button
matTooltip="{{'PROJECT.TABLE.SHOWGRID' | translate}}">
<i matTooltip="show grid view" class="las la-th-large"></i>
</button>
</div>
<div *ngIf="!grid && grantedProjectList">
<cnsl-refresh-table (refreshed)="refreshPage()" [dataSize]="totalResult" [timestamp]="viewTimestamp"
[selection]="selection" [loading]="loading$ | async">
<div class="table-wrapper">
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.NAME' | translate }} </th>
<td mat-cell *matCellDef="let project"> {{project.projectName}} </td>
</ng-container>
<ng-container matColumnDef="resourceOwnerName">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.RESOURCEOWNER' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let project">
{{project.projectOwnerName}} </td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.STATE' | translate }} </th>
<td mat-cell *matCellDef="let project"><span *ngIf="project.state">{{'PROJECT.STATE.'+project.state
| translate}}</span></td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CREATIONDATE' | translate }} </th>
<td mat-cell *matCellDef="let project">
<span *ngIf="project.details.creationDate">{{project.details.creationDate | timestampToDate |
localizedDate:
'EEE dd. MMM, HH:mm'}}</span>
</td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CHANGEDATE' | translate }} </th>
<td mat-cell *matCellDef="let project">
<span *ngIf="project.details.changeDate">
{{project.details.changeDate
| timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}}</span>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"
[routerLink]="['/granted-projects', row.projectId, 'grant', row.grantId]"></tr>
</table>
<div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{'PROJECT.TABLE.EMPTY' | translate}}</span>
</div>
<cnsl-paginator class="paginator" #paginator [timestamp]="viewTimestamp" [length]="totalResult" [pageSize]="10"
[pageSizeOptions]="[5, 10, 20]" (page)="changePage($event)"></cnsl-paginator>
</div>
</cnsl-refresh-table>
</div>
@@ -1,39 +0,0 @@
.view-toggle {
width: 100%;
display: flex;
justify-content: flex-end;
.icon-button {
display: block;
}
}
.table-wrapper {
overflow: auto;
.table,
.paginator {
width: 100%;
background-color: #2d2e30;
td,
th {
padding-left: .5rem;
padding-right: .5rem;
&:first-child {
padding-left: 0;
padding-right: .5rem;
}
&:last-child {
padding-right: 0;
}
}
}
}
tr {
outline: none;
}
@@ -1,25 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { GrantedProjectListComponent } from './granted-project-list.component';
describe('ProjectListComponent', () => {
let component: GrantedProjectListComponent;
let fixture: ComponentFixture<GrantedProjectListComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [GrantedProjectListComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GrantedProjectListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,119 +0,0 @@
import { animate, animateChild, query, stagger, style, transition, trigger } from '@angular/animations';
import { SelectionModel } from '@angular/cdk/collections';
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { PageEvent, PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
import { GrantedProject } from 'src/app/proto/generated/zitadel/project_pb';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'cnsl-granted-project-list',
templateUrl: './granted-project-list.component.html',
styleUrls: ['./granted-project-list.component.scss'],
animations: [
trigger('list', [
transition(':enter', [
query('@animate',
stagger(80, animateChild()),
),
]),
]),
trigger('animate', [
transition(':enter', [
style({ opacity: 0, transform: 'translateY(-100%)' }),
animate('100ms', style({ opacity: 1, transform: 'translateY(0)' })),
]),
transition(':leave', [
style({ opacity: 1, transform: 'translateY(0)' }),
animate('100ms', style({ opacity: 0, transform: 'translateY(100%)' })),
]),
]),
],
})
export class GrantedProjectListComponent implements OnInit, OnDestroy {
public totalResult: number = 0;
public viewTimestamp!: Timestamp.AsObject;
public dataSource: MatTableDataSource<GrantedProject.AsObject> =
new MatTableDataSource<GrantedProject.AsObject>();
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
public grantedProjectList: GrantedProject.AsObject[] = [];
public displayedColumns: string[] = ['select', 'name', 'resourceOwnerName', 'state', 'creationDate', 'changeDate'];
public selection: SelectionModel<GrantedProject.AsObject> = new SelectionModel<GrantedProject.AsObject>(true, []);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public grid: boolean = true;
private subscription?: Subscription;
constructor(private router: Router,
public translate: TranslateService,
private mgmtService: ManagementService,
private toast: ToastService,
) { }
public ngOnInit(): void {
this.getData(10, 0);
}
public ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
public isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.data.length;
return numSelected === numRows;
}
public masterToggle(): void {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.data.forEach(row => this.selection.select(row));
}
public changePage(event: PageEvent): void {
this.getData(event.pageSize, event.pageIndex);
}
public addProject(): void {
this.router.navigate(['/projects', 'create']);
}
private async getData(limit: number, offset: number): Promise<void> {
this.loadingSubject.next(true);
this.mgmtService.listGrantedProjects(limit, offset).then(resp => {
this.grantedProjectList = resp.resultList;
if (resp.details?.totalResult) {
this.totalResult = resp.details.totalResult;
} else {
this.totalResult = 0;
}
if (resp.details?.viewTimestamp) {
this.viewTimestamp = resp.details?.viewTimestamp;
}
if (this.totalResult > 5) {
this.grid = false;
}
this.dataSource.data = this.grantedProjectList;
this.loadingSubject.next(false);
}).catch(error => {
console.error(error);
this.toast.showError(error);
this.loadingSubject.next(false);
});
}
public refreshPage(): void {
this.selection.clear();
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize);
}
}
@@ -1,57 +1,27 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from 'src/app/guards/auth.guard';
import { RoleGuard } from 'src/app/guards/role.guard';
import { ProjectType } from 'src/app/modules/project-members/project-members-datasource';
import { GrantedProjectDetailComponent } from './granted-project-detail/granted-project-detail.component';
import { GrantedProjectsComponent } from './granted-projects.component';
const routes: Routes = [
{
path: '',
component: GrantedProjectsComponent,
data: { animation: 'HomePage' },
},
{
path: 'create',
loadChildren: () => import('../project-create/project-create.module').then(m => m.ProjectCreateModule),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['project.write'],
},
},
{
path: ':projectid/grant/:grantid/members',
data: {
type: ProjectType.PROJECTTYPE_GRANTED,
roles: ['project.grant.member.read'],
},
loadChildren: () => import('src/app/modules/project-members/project-members.module')
.then(m => m.ProjectMembersModule),
},
{
path: ':id/grant/:grantId',
component: GrantedProjectDetailComponent,
data: { animation: 'HomePage' },
},
{
path: ':projectid/roles/create',
loadChildren: () => import('../project-role-create/project-role-create.module').then(m => m.ProjectRoleCreateModule),
canActivate: [RoleGuard],
data: {
roles: ['project.write'],
},
},
{
path: ':projectid/grants/create',
loadChildren: () => import('../project-grant-create/project-grant-create.module')
.then(m => m.ProjectGrantCreateModule),
{
path: ':projectid/grant/:grantid/members',
data: {
type: ProjectType.PROJECTTYPE_GRANTED,
roles: ['project.grant.member.read'],
},
loadChildren: () => import('src/app/modules/project-members/project-members.module').then((m) => m.ProjectMembersModule),
},
{
path: ':id/grant/:grantId',
component: GrantedProjectDetailComponent,
data: { animation: 'HomePage' },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class GrantedProjectsRoutingModule { }
export class GrantedProjectsRoutingModule {}
@@ -1,8 +0,0 @@
<div class="enlarged-container">
<h1>{{ 'PROJECT.PAGES.LIST' | translate }}</h1>
<p class="sub">{{ 'PROJECT.PAGES.LISTDESCRIPTION' | translate }}</p>
<h2>{{'PROJECT.PAGES.TYPE.GRANTED' | translate}}</h2>
<cnsl-granted-project-list>
</cnsl-granted-project-list>
</div>
@@ -1,13 +0,0 @@
h1 {
margin: 0;
}
.sub {
color: var(--grey);
margin-bottom: 2rem;
font-size: 14px;
}
.max-width-container {
padding-bottom: 3rem;
}
@@ -1,25 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { GrantedProjectsComponent } from './granted-projects.component';
describe('GrantedProjectsComponent', () => {
let component: GrantedProjectsComponent;
let fixture: ComponentFixture<GrantedProjectsComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [GrantedProjectsComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GrantedProjectsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,10 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'cnsl-granted-projects',
templateUrl: './granted-projects.component.html',
styleUrls: ['./granted-projects.component.scss'],
})
export class GrantedProjectsComponent {
constructor() { }
}
@@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatRippleModule } from '@angular/material/core';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@@ -17,62 +18,58 @@ import { MemberCreateDialogModule } from 'src/app/modules/add-member-dialog/memb
import { CardModule } from 'src/app/modules/card/card.module';
import { ChangesModule } from 'src/app/modules/changes/changes.module';
import { ContributorsModule } from 'src/app/modules/contributors/contributors.module';
import { InfoRowModule } from 'src/app/modules/info-row/info-row.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module';
import { PaginatorModule } from 'src/app/modules/paginator/paginator.module';
import { ProjectRolesModule } from 'src/app/modules/project-roles/project-roles.module';
import { ProjectRolesTableModule } from 'src/app/modules/project-roles-table/project-roles-table.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { TopViewModule } from 'src/app/modules/top-view/top-view.module';
import { UserGrantsModule } from 'src/app/modules/user-grants/user-grants.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { GrantedProjectDetailComponent } from './granted-project-detail/granted-project-detail.component';
import { GrantedProjectGridComponent } from './granted-project-list/granted-project-grid/granted-project-grid.component';
import { GrantedProjectListComponent } from './granted-project-list/granted-project-list.component';
import { GrantedProjectsRoutingModule } from './granted-projects-routing.module';
import { GrantedProjectsComponent } from './granted-projects.component';
@NgModule({
declarations: [
GrantedProjectsComponent,
GrantedProjectListComponent,
GrantedProjectGridComponent,
GrantedProjectDetailComponent,
],
imports: [
CommonModule,
UserGrantsModule,
GrantedProjectsRoutingModule,
ContributorsModule,
FormsModule,
TranslateModule,
ReactiveFormsModule,
HasRoleModule,
MatTableModule,
PaginatorModule,
InputModule,
ChangesModule,
MatIconModule,
MatSelectModule,
MatButtonModule,
MatTabsModule,
MatProgressSpinnerModule,
MetaLayoutModule,
MatProgressBarModule,
ProjectRolesModule,
MatCheckboxModule,
CardModule,
MatTooltipModule,
MatSortModule,
HasRolePipeModule,
TranslateModule,
TimestampToDatePipeModule,
SharedModule,
LocalizedDatePipeModule,
MemberCreateDialogModule,
RefreshTableModule,
],
declarations: [GrantedProjectDetailComponent],
imports: [
CommonModule,
UserGrantsModule,
GrantedProjectsRoutingModule,
ContributorsModule,
FormsModule,
TranslateModule,
ReactiveFormsModule,
HasRoleModule,
MatTableModule,
PaginatorModule,
InputModule,
ChangesModule,
MatIconModule,
MatSelectModule,
MatButtonModule,
MatTabsModule,
MatProgressSpinnerModule,
MetaLayoutModule,
MatProgressBarModule,
ProjectRolesTableModule,
MatCheckboxModule,
CardModule,
MatTooltipModule,
MatSortModule,
HasRolePipeModule,
TimestampToDatePipeModule,
TopViewModule,
SharedModule,
InfoRowModule,
LocalizedDatePipeModule,
MemberCreateDialogModule,
MatRippleModule,
RefreshTableModule,
],
})
export class GrantedProjectsModule { }
export class GrantedProjectsModule {}
@@ -1,40 +1,40 @@
<div class="app-grid-header">
<h3>{{'APP.LIST' | translate}}</h3>
<span class="fill-space"></span>
<button mat-icon-button (click)="closeView()">
<i class="show list view las la-th-list"></i>
</button>
<h3>{{'APP.LIST' | translate}}</h3>
<span class="fill-space"></span>
<button mat-icon-button (click)="closeView()">
<i class="show list view las la-th-list"></i>
</button>
</div>
<div class="app-container">
<div class="sp-container" *ngIf="(loading$ | async)">
<mat-spinner color="primary" diameter="50"></mat-spinner>
<div class="sp-container" *ngIf="(loading$ | async)">
<mat-spinner color="primary" diameter="50"></mat-spinner>
</div>
<div [routerLink]="['/projects', projectId, 'apps', app.id ]" [attr.data-e2e]="'app-card'" class="app-wrap"
*ngFor="let app of appsSubject | async" matTooltip="{{'ACTIONS.EDIT' | translate}}">
<cnsl-app-card class="grid-card" matRipple [type]="app.oidcConfig?.appType"
[isApiApp]="app.apiConfig !== undefined">
{{ app.name.charAt(0)}}
<ng-container *ngIf="app.oidcConfig?.appType !== undefined">
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE" class="las la-mobile"></i>
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_WEB" class="las la-code"></i>
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT" class="las la-code"></i>
<i *ngIf="app.apiConfig" class="las la-robot"></i>
</ng-container>
</cnsl-app-card>
<span class="name">{{app.name}}</span>
<span *ngIf="app.oidcConfig?.appType !== undefined && app.oidcConfig?.appType !== null" class="type">
{{'APP.OIDC.APPTYPE.'+app.oidcConfig?.appType | translate}}</span>
<span *ngIf="app.apiConfig !== undefined" class="type"> API</span>
</div>
<ng-template cnslHasRole [hasRole]="['project.app.write']">
<div class="app-wrap" *ngIf="!disabled" [attr.data-e2e]="'app-card-add'"
[routerLink]="[ '/projects', projectId, 'apps', 'create']">
<cnsl-app-card class="grid-card" matRipple [type]="OIDCAppType.ADD">
<mat-icon>add</mat-icon>
</cnsl-app-card>
<span class="name">{{ 'ACTIONS.NEW' | translate }}</span>
</div>
<div [routerLink]="['/projects', projectId, 'apps', app.id ]" class="app-wrap"
*ngFor="let app of appsSubject | async" matTooltip="{{'ACTIONS.EDIT' | translate}}">
<cnsl-app-card class="grid-card" matRipple [type]="app.oidcConfig?.appType"
[isApiApp]="app.apiConfig !== undefined">
{{ app.name.charAt(0)}}
<ng-container *ngIf="app.oidcConfig?.appType !== undefined">
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE" class="las la-mobile"></i>
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_WEB" class="las la-code"></i>
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT" class="las la-code"></i>
<i *ngIf="app.apiConfig" class="las la-code"></i>
</ng-container>
</cnsl-app-card>
<span class="name">{{app.name}}</span>
<span *ngIf="app.oidcConfig?.appType !== undefined && app.oidcConfig?.appType !== null" class="type">
{{'APP.OIDC.APPTYPE.'+app.oidcConfig?.appType | translate}}</span>
<span *ngIf="app.apiConfig !== undefined" class="type"> API</span>
</div>
<ng-template cnslHasRole [hasRole]="['project.app.write']">
<div class="app-wrap" *ngIf="!disabled" [routerLink]="[ '/projects', projectId, 'apps', 'create']">
<cnsl-app-card class="grid-card add" matRipple>
<mat-icon>add</mat-icon>
</cnsl-app-card>
<span class="name">{{ 'ACTIONS.NEW' | translate }}</span>
</div>
</ng-template>
</ng-template>
</div>
@@ -2,12 +2,13 @@ h3 {
font-weight: 400;
font-size: 16px;
text-transform: uppercase;
letter-spacing: .05em;
letter-spacing: 0.05em;
}
.app-grid-header {
display: flex;
align-items: center;
padding-top: 1rem;
.fill-space {
flex: 1;
@@ -46,13 +47,9 @@ h3 {
width: 80px;
margin: 1rem;
text-transform: uppercase;
border-radius: .5rem;
border-radius: 0.5rem;
font-weight: 800;
box-sizing: border-box;
&.add {
border: 2px solid var(--grey);
}
}
.name {
@@ -1,52 +1,76 @@
<cnsl-refresh-table [loading]="dataSource.loading$ | async" [selection]="selection" (refreshed)="refreshPage()"
[dataSize]="dataSource.totalResult" [timestamp]="dataSource?.viewTimestamp">
<ng-template cnslHasRole [hasRole]="['project.app.write']" actions>
<a [disabled]="disabled" [routerLink]="[ '/projects', projectId, 'apps', 'create']" color="primary"
mat-raised-button>
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a>
</ng-template>
[dataSize]="dataSource.totalResult" [timestamp]="dataSource?.viewTimestamp">
<ng-template cnslHasRole [hasRole]="['project.app.write']" actions>
<a [disabled]="disabled" [routerLink]="[ '/projects', projectId, 'apps', 'create']" color="primary"
mat-raised-button>
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a>
</ng-template>
<div class="table-wrapper">
<table [dataSource]="dataSource" mat-table class="table" aria-label="Elements">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
</mat-checkbox>
</td>
</ng-container>
<div class="table-wrapper">
<table [dataSource]="dataSource" mat-table class="table" aria-label="Elements">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'APP.NAME' | translate }} </th>
<td class="pointer" [routerLink]="['/projects', projectId, 'apps', app.id ]" mat-cell
*matCellDef="let app">
{{app.name}} </td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'APP.NAME' | translate }} </th>
<td class="pointer" [routerLink]="['/projects', projectId, 'apps', app.id ]" mat-cell *matCellDef="let app">
{{app.name}} </td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef> {{ 'APP.TYPE' | translate }} </th>
<td class="pointer" [routerLink]="['/projects', projectId, 'apps', app.id ]" mat-cell
*matCellDef="let app">
<span *ngIf="app?.oidcConfig?.appType !== undefined && app?.oidcConfig?.appType !== null">
{{'APP.OIDC.APPTYPE.'+app?.oidcConfig?.appType | translate}}
</span>
<span *ngIf="app.apiConfig">API</span>
</td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef> {{ 'APP.TYPE' | translate }} </th>
<td class="pointer" [routerLink]="['/projects', projectId, 'apps', app.id ]" mat-cell *matCellDef="let app">
<span *ngIf="app?.oidcConfig?.appType !== undefined && app?.oidcConfig?.appType !== null">
{{'APP.OIDC.APPTYPE.'+app?.oidcConfig?.appType | translate}}
</span>
<span *ngIf="app.apiConfig">API</span>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.DATA.STATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let app" [routerLink]="['/projects', projectId, 'apps', app.id ]">
<span class="state"
[ngClass]="{'active': app.state === AppState.APP_STATE_ACTIVE, 'inactive': app.state === AppState.APP_STATE_INACTIVE}">
{{('APP.PAGES.DETAIL.STATE.'+app?.state) | translate}}
</span>
</td>
</ng-container>
<cnsl-paginator class="paginator" #paginator [timestamp]="dataSource?.viewTimestamp" [length]="dataSource.totalResult" [pageSize]="25"
[pageSizeOptions]="[25, 50, 100, 250]">
</cnsl-paginator>
</div>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.CREATIONDATE' | translate }} </th>
<td class="pointer" [routerLink]="['/projects', projectId, 'apps', app.id ]" mat-cell *matCellDef="let app">
<span *ngIf="app?.details?.creationDate">{{app.details.creationDate | timestampToDate |
localizedDate: 'dd. MMM, HH:mm' }}</span>
</td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.CHANGEDATE' | translate }} </th>
<td class="pointer" [routerLink]="['/projects', projectId, 'apps', app.id ]" mat-cell *matCellDef="let app">
<span *ngIf="app?.details?.changeDate">{{app.details.changeDate | timestampToDate |
localizedDate: 'dd. MMM, HH:mm' }}</span>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
<cnsl-paginator class="paginator" #paginator [timestamp]="dataSource?.viewTimestamp" [length]="dataSource.totalResult"
[pageSize]="25" [pageSizeOptions]="[25, 50, 100, 250]">
</cnsl-paginator>
</cnsl-refresh-table>
@@ -1,28 +1,5 @@
.table-wrapper {
overflow: auto;
.table,
.paginator {
width: 100%;
background-color: #2d2e30;
td,
th {
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
.margin-neg {
margin-left: -1rem;
}
}
.margin-neg {
margin-left: -1rem;
}
.pointer {
@@ -1,5 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@@ -7,28 +6,25 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ApplicationsComponent } from './applications.component';
describe('ProjectApplicationsComponent', () => {
let component: ApplicationsComponent;
let fixture: ComponentFixture<ApplicationsComponent>;
let component: ApplicationsComponent;
let fixture: ComponentFixture<ApplicationsComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ApplicationsComponent],
imports: [
NoopAnimationsModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
],
}).compileComponents();
}));
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ApplicationsComponent],
imports: [NoopAnimationsModule, MatSortModule, MatTableModule],
}).compileComponents();
}),
);
beforeEach(() => {
fixture = TestBed.createComponent(ApplicationsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(ApplicationsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
});
@@ -4,7 +4,7 @@ import { MatTable } from '@angular/material/table';
import { merge } from 'rxjs';
import { tap } from 'rxjs/operators';
import { PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
import { App } from 'src/app/proto/generated/zitadel/app_pb';
import { App, AppState } from 'src/app/proto/generated/zitadel/app_pb';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ProjectApplicationsDataSource } from './applications-datasource';
@@ -22,9 +22,9 @@ export class ApplicationsComponent implements AfterViewInit, OnInit {
public dataSource!: ProjectApplicationsDataSource;
public selection: SelectionModel<App.AsObject> = new SelectionModel<App.AsObject>(true, []);
public displayedColumns: string[] = ['select', 'name', 'type'];
constructor(private mgmtService: ManagementService) { }
public displayedColumns: string[] = ['name', 'type', 'state', 'creationDate', 'changeDate'];
public AppState: any = AppState;
constructor(private mgmtService: ManagementService) {}
public ngOnInit(): void {
this.dataSource = new ProjectApplicationsDataSource(this.mgmtService);
@@ -33,18 +33,12 @@ export class ApplicationsComponent implements AfterViewInit, OnInit {
public ngAfterViewInit(): void {
merge(this.paginator.page)
.pipe(
tap(() => this.loadRolesPage()),
)
.pipe(tap(() => this.loadRolesPage()))
.subscribe();
}
private loadRolesPage(): void {
this.dataSource.loadApps(
this.projectId,
this.paginator.pageIndex,
this.paginator.pageSize,
);
this.dataSource.loadApps(this.projectId, this.paginator.pageIndex, this.paginator.pageSize);
}
public isAllSelected(): boolean {
@@ -54,9 +48,9 @@ export class ApplicationsComponent implements AfterViewInit, OnInit {
}
public masterToggle(): void {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.appsSubject.value.forEach((row: App.AsObject) => this.selection.select(row));
this.isAllSelected()
? this.selection.clear()
: this.dataSource.appsSubject.value.forEach((row: App.AsObject) => this.selection.select(row));
}
public refreshPage(): void {
@@ -1,61 +1,50 @@
<cnsl-meta-layout>
<div class="max-width-container">
<div class="head" *ngIf="project?.id">
<a [routerLink]="[ '/projects' ]" mat-icon-button>
<mat-icon class="icon">arrow_back</mat-icon>
</a>
<h1>{{ 'PROJECT.PAGES.TITLE' | translate }} {{project?.name}}</h1>
<cnsl-top-view title="{{project?.name}}" docLink="https://docs.zitadel.ch/docs/guides/basics/projects"
sub="{{'PROJECT.PAGES.TYPE.OWNED_SINGULAR' | translate}}"
[isActive]="project?.state === ProjectState.PROJECT_STATE_ACTIVE"
[isInactive]="project?.state === ProjectState.PROJECT_STATE_INACTIVE" [hasContributors]="true"
stateTooltip="{{'PROJECT.STATE.'+project?.state | translate}}" [hasActions]="true">
<ng-template topActions cnslHasRole [hasRole]="['project.write:'+projectId, 'project.write']">
<button mat-menu-item (click)="openNameDialog()" aria-label="Edit project name" *ngIf="isZitadel === false">
{{'ACTIONS.RENAME' | translate}}
</button>
<span class="fill-space"></span>
<button mat-menu-item
[disabled]="isZitadel || (['project.write$', 'project.write:'+ project.id]| hasRole | async) === false"
*ngIf="project?.state === ProjectState.PROJECT_STATE_ACTIVE"
(click)="changeState(ProjectState.PROJECT_STATE_INACTIVE)">
{{'PROJECT.TABLE.DEACTIVATE' | translate}}
</button>
<ng-template cnslHasRole [hasRole]="['project.write:'+projectId, 'project.write']">
<button class="actions-trigger" mat-raised-button color="primary" [matMenuTriggerFor]="actions">
<span>{{'ACTIONS.ACTIONS' | translate}}</span>
<mat-icon class="icon">keyboard_arrow_down</mat-icon>
</button>
<mat-menu #actions="matMenu" xPosition="before">
<button mat-menu-item (click)="openNameDialog()" aria-label="Edit project name" *ngIf="isZitadel === false">
{{'ACTIONS.RENAME' | translate}}
</button>
<button mat-menu-item
[disabled]="isZitadel || (['project.write$', 'project.write:'+ project.id]| hasRole | async) === false"
*ngIf="project?.state === ProjectState.PROJECT_STATE_INACTIVE"
(click)="changeState(ProjectState.PROJECT_STATE_ACTIVE)">
{{'PROJECT.TABLE.ACTIVATE' | translate}}
</button>
<button mat-menu-item
[disabled]="isZitadel || (['project.write$', 'project.write:'+ project.id]| hasRole | async) === false"
*ngIf="project?.state === ProjectState.PROJECT_STATE_ACTIVE"
(click)="changeState(ProjectState.PROJECT_STATE_INACTIVE)">
{{'PROJECT.TABLE.DEACTIVATE' | translate}}
</button>
<ng-template cnslHasRole [hasRole]="['project.delete$', 'project.delete:'+projectId]">
<button mat-menu-item matTooltip="{{'ACTIONS.DELETE' | translate}}" (click)="deleteProject()"
aria-label="Edit project name" *ngIf="isZitadel === false">
<span [style.color]="'var(--warn)'">{{'PROJECT.PAGES.DELETE' | translate}}</span>
</button>
</ng-template>
</ng-template>
<cnsl-contributors topContributors class="project-contributors" *ngIf="project" [loading]="loading$ | async"
[totalResult]="totalMemberResult" [membersSubject]="membersSubject" title="{{ 'PROJECT.MEMBER.TITLE' | translate }}"
description="{{ 'PROJECT.MEMBER.TITLEDESC' | translate }}" (addClicked)="openAddMember()"
(showDetailClicked)="showDetail()" (refreshClicked)="loadMembers()"
[disabled]="(['project.member.write$', 'project.member.write:'+ project.id]| hasRole | async) === false">
</cnsl-contributors>
<button mat-menu-item
[disabled]="isZitadel || (['project.write$', 'project.write:'+ project.id]| hasRole | async) === false"
*ngIf="project?.state === ProjectState.PROJECT_STATE_INACTIVE"
(click)="changeState(ProjectState.PROJECT_STATE_ACTIVE)">
{{'PROJECT.TABLE.ACTIVATE' | translate}}
</button>
<p topContent *ngIf="isZitadel" class="zitadel-warning">{{'PROJECT.PAGES.ZITADELPROJECT' | translate}}</p>
<ng-template cnslHasRole [hasRole]="['project.delete$', 'project.delete:'+projectId]">
<button mat-menu-item matTooltip="{{'ACTIONS.DELETE' | translate}}" (click)="deleteProject()"
aria-label="Edit project name" *ngIf="isZitadel === false">
<span [style.color]="'var(--warn)'">{{'PROJECT.PAGES.DELETE' | translate}}</span>
</button>
</ng-template>
</mat-menu>
</ng-template>
<cnsl-info-row topContent *ngIf="project" [project]="project"></cnsl-info-row>
</cnsl-top-view>
<div class="full-width">
<p class="desc">{{ 'PROJECT.PAGES.DESCRIPTION' | translate }}</p>
<p *ngIf="isZitadel" class="zitadel-warning">{{'PROJECT.PAGES.ZITADELPROJECT' | translate}}</p>
</div>
</div>
<div class="max-width-container">
<cnsl-meta-layout>
<ng-container *ngIf="project">
<div class="privatelabel-info">
<h2 class="setting-title">{{'PROJECT.PAGES.PRIVATELABEL.TITLE' | translate}}</h2>
<p class="setting-desc">
<span>{{'PROJECT.PAGES.PRIVATELABEL.'+project.privateLabelingSetting+'.TITLE' | translate}}</span>
<button [disabled]="((['project.write$', 'project.write:'+ project.id]| hasRole | async)) === false"
(click)="openPrivateLabelingDialog()" mat-icon-button><i class="las la-edit"></i></button>
</p>
</div>
<ng-template cnslHasRole [hasRole]="['project.app.read:' + project.id, 'project.app.read']">
<cnsl-application-grid *ngIf="grid" [disabled]="isZitadel" (changeView)="grid = false" [projectId]="projectId">
@@ -71,83 +60,46 @@
</ng-template>
<ng-container *ngIf="isZitadel === false">
<ng-template cnslHasRole [hasRole]="['project.grant.read:' + project.id, 'project.grant.read']">
<cnsl-card title="{{ 'PROJECT.GRANT.TITLE' | translate }}"
description="{{ 'PROJECT.GRANT.DESCRIPTION' | translate }}">
<cnsl-project-grants
[refreshOnPreviousRoutes]="['/projects/'+projectId+'/grants/create','/projects/'+projectId+'/roles/create']"
[disabled]="((['project.grant.write$', 'project.grant.write:'+ project.id]| hasRole | async)) === false"
[projectId]="projectId">
</cnsl-project-grants>
</cnsl-card>
</ng-template>
<ng-template cnslHasRole [hasRole]="['project.role.read:' + project.id, 'project.role.read']">
<cnsl-card id="roles" title="{{ 'PROJECT.ROLE.TITLE' | translate }}"
description="{{ 'PROJECT.ROLE.DESCRIPTION' | translate }}">
<p>{{'PROJECT.ROLE.OPTIONS' | translate}}</p>
<cnsl-card id="roles" title="{{ 'PROJECT.SETTINGS.TITLE' | translate }}"
description="{{ 'PROJECT.SETTINGS.DESCRIPTION' | translate }}">
<div class="project-detail-privatelabel-info">
<h2 class="setting-title">{{'PROJECT.PAGES.PRIVATELABEL.TITLE' | translate}}</h2>
<p class="setting-desc">
<span
class="cnsl-secondary-text">{{'PROJECT.PAGES.PRIVATELABEL.'+project.privateLabelingSetting+'.TITLE' |
translate}}</span>
<button matTooltip="{{'ACTIONS.EDIT' | translate}}" [disabled]=" ((['project.write$', 'project.write:'
+ project.id]| hasRole | async))===false" (click)="openPrivateLabelingDialog()" mat-icon-button><i
class="las la-pen"></i></button>
</p>
</div>
<mat-checkbox [(ngModel)]="project.projectRoleAssertion"
[disabled]="((['project.write$', 'project.write:'+ project.id]| hasRole | async)) === false"
(change)="saveProject()" color="primary">
color="primary">
{{'PROJECT.ROLE.ASSERTION' | translate}}</mat-checkbox>
<p class="desc">{{'PROJECT.ROLE.ASSERTION_DESCRIPTION' | translate}}</p>
<p class="desc cnsl-secondary-text">{{'PROJECT.ROLE.ASSERTION_DESCRIPTION' | translate}}</p>
<mat-checkbox [(ngModel)]="project.projectRoleCheck"
[disabled]="((['project.write$', 'project.write:'+ project.id]| hasRole | async)) === false"
(change)="saveProject()" color="primary">
color="primary">
{{'PROJECT.ROLE.CHECK' | translate}}</mat-checkbox>
<p class="desc">{{'PROJECT.ROLE.CHECK_DESCRIPTION' | translate}}</p>
<p class="desc cnsl-secondary-text">{{'PROJECT.ROLE.CHECK_DESCRIPTION' | translate}}</p>
<mat-checkbox [(ngModel)]="project.hasProjectCheck"
[disabled]="((['project.write$', 'project.write:'+ project.id]| hasRole | async)) === false"
(change)="saveProject()" color="primary">
color="primary">
{{'PROJECT.HAS_PROJECT' | translate}}</mat-checkbox>
<p class="desc">{{'PROJECT.HAS_PROJECT_DESCRIPTION' | translate}}</p>
<div class="divider"></div>
<cnsl-project-roles
[disabled]="(['project.role.write$', 'project.role.write:'+ project.id]| hasRole | async) === false"
[actionsVisible]="true" [projectId]="projectId">
</cnsl-project-roles>
</cnsl-card>
</ng-template>
<p class="desc cnsl-secondary-text">{{'PROJECT.HAS_PROJECT_DESCRIPTION' | translate}}</p>
<ng-template cnslHasRole [hasRole]="['user.grant.read']">
<cnsl-card *ngIf="project?.id" title="{{ 'GRANTS.PROJECT.TITLE' | translate }}"
description="{{'GRANTS.PROJECT.DESCRIPTION' | translate }}">
<cnsl-user-grants [context]="UserGrantContext.OWNED_PROJECT" [projectId]="projectId"
[refreshOnPreviousRoutes]="['/grant-create/project/'+projectId]"
[disableWrite]="((['user.grant.write$', 'user.grant.write:'+projectId] | hasRole) | async) === false"
[disableDelete]="((['user.grant.delete$','user.grant.delete:'+projectId] | hasRole) | async) === false">
</cnsl-user-grants>
<div class="project-detail-btn-container">
<button mat-raised-button color="primary" (click)="saveProject()">{{'ACTIONS.SAVE' | translate}}</button>
</div>
</cnsl-card>
</ng-template>
</ng-container>
</ng-container>
</div>
<div class="side" metainfo>
<div class="meta-details">
<div class="meta-row">
<span class="first">{{'RESOURCEID' | translate}}:</span>
<span *ngIf="projectId" class="second">{{ projectId }}</span>
</div>
<div class="meta-row">
<span class="first">{{'PROJECT.STATE.TITLE' | translate}}:</span>
<span *ngIf="project && project.state !== undefined" class="state"
[ngClass]="{'active': project.state === ProjectState.PROJECT_STATE_ACTIVE, 'inactive': project.state === ProjectState.PROJECT_STATE_INACTIVE}">{{'PROJECT.STATE.'+project.state
| translate}}</span>
</div>
</div>
<mat-tab-group mat-stretch-tabs class="tab-group" [disablePagination]="true">
<mat-tab label="Details">
<cnsl-contributors *ngIf="project" [loading]="loading$ | async" [totalResult]="totalMemberResult"
[membersSubject]="membersSubject" title="{{ 'PROJECT.MEMBER.TITLE' | translate }}"
description="{{ 'PROJECT.MEMBER.TITLEDESC' | translate }}" (addClicked)="openAddMember()"
(showDetailClicked)="showDetail()" (refreshClicked)="loadMembers()"
[disabled]="(['project.member.write$', 'project.member.write:'+ project.id]| hasRole | async) === false">
</cnsl-contributors>
</mat-tab>
<mat-tab label="{{ 'CHANGES.PROJECT.TITLE' | translate }}" class="meta-flex-col">
<cnsl-changes *ngIf="project" [changeType]="ChangeType.PROJECT" [id]="project.id"></cnsl-changes>
</mat-tab>
</mat-tab-group>
</div>
</cnsl-meta-layout>
<div metainfo>
<cnsl-changes *ngIf="project" [changeType]="ChangeType.PROJECT" [id]="projectId"></cnsl-changes>
</div>
</cnsl-meta-layout>
</div>
@@ -1,101 +1,51 @@
.head {
display: flex;
align-items: center;
border-bottom: 1px solid #81868a30;
flex-wrap: wrap;
margin-bottom: 1rem;
@use '@angular/material' as mat;
.fill-space {
flex: 1;
}
@mixin owned-project-detail-theme($theme) {
$foreground: map-get($theme, foreground);
$background: map-get($theme, background);
$is-dark-theme: map-get($theme, is-dark);
$warn: map-get($theme, warn);
$warn-color: map-get($warn, 500);
a {
display: block;
}
h1 {
margin: 0 1rem;
margin-left: 2rem;
font-weight: normal;
}
.actions-trigger {
margin-top: .25rem;
display: flex;
align-items: center;
.icon {
margin-left: .5rem;
margin-right: -.5rem;
}
}
.full-width {
padding-top: 1rem;
width: 100%;
display: block;
.icon-button {
margin-right: 1rem;
}
.desc {
font-size: .9rem;
color: var(--grey);
}
.zitadel-warning {
font-size: 14px;
color: rgb(201, 51, 71);
}
}
}
.privatelabel-info {
display: flex;
flex-direction: column;
border-bottom: 1px solid #81868a30;
padding: .5rem 0 1rem 0;
.setting-title {
.zitadel-warning {
font-size: 14px;
color: var(--grey);
text-transform: uppercase;
margin: 0;
color: $warn-color;
}
.setting-desc {
margin: 0;
.project-detail-privatelabel-info {
display: flex;
align-items: center;
flex-direction: column;
padding: 0 0 1rem 0;
margin-bottom: 1rem;
border-bottom: 1px solid map-get($foreground, divider);
.icon-button {
margin-left: .5rem;
.setting-title {
margin: 0;
font-size: 16px;
}
.setting-desc {
margin: 0;
display: flex;
align-items: center;
span {
font-size: 14px;
}
.icon-button {
margin-left: 0.5rem;
}
}
}
}
.line {
.formfield {
flex: 1;
.project-detail-btn-container {
display: flex;
justify-content: flex-end;
}
button {
margin-top: 1rem;
}
display: flex;
align-items: center;
}
.desc {
color: var(--grey);
font-size: 14px;
margin-bottom: 1.5rem;
}
.divider {
height: 1px;
background-color: var(--grey);
margin-bottom: 1.5rem;
margin-bottom: 2rem;
}
@@ -1,10 +1,10 @@
import { Location } from '@angular/common';
import { Component, EventEmitter, OnDestroy, OnInit } from '@angular/core';
import { Component, EventEmitter, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, from, Observable, of, Subscription } from 'rxjs';
import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators';
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component';
import { ChangeType } from 'src/app/modules/changes/changes.component';
@@ -18,18 +18,20 @@ import { ListAppsResponse, UpdateProjectRequest } from 'src/app/proto/generated/
import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { PrivateLabelingSetting, Project, ProjectState } from 'src/app/proto/generated/zitadel/project_pb';
import { User } from 'src/app/proto/generated/zitadel/user_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { NameDialogComponent } from '../../../../modules/name-dialog/name-dialog.component';
const ROUTEPARAM = 'projectid';
@Component({
selector: 'cnsl-owned-project-detail',
templateUrl: './owned-project-detail.component.html',
styleUrls: ['./owned-project-detail.component.scss'],
})
export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
export class OwnedProjectDetailComponent implements OnInit {
public projectId: string = '';
public project!: Project.AsObject;
@@ -42,7 +44,6 @@ export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
public ChangeType: any = ChangeType;
public grid: boolean = true;
private subscription?: Subscription;
public isZitadel: boolean = false;
@@ -50,8 +51,7 @@ export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
// members
public totalMemberResult: number = 0;
public membersSubject: BehaviorSubject<Member.AsObject[]>
= new BehaviorSubject<Member.AsObject[]>([]);
public membersSubject: BehaviorSubject<Member.AsObject[]> = new BehaviorSubject<Member.AsObject[]>([]);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public refreshChanges$: EventEmitter<void> = new EventEmitter();
@@ -64,14 +64,15 @@ export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
private _location: Location,
private dialog: MatDialog,
private router: Router,
) { }
private breadcrumbService: BreadcrumbService,
) {}
public ngOnInit(): void {
this.subscription = this.route.params.subscribe(params => this.getData(params));
}
public ngOnDestroy(): void {
this.subscription?.unsubscribe();
const projectId = this.route.snapshot.paramMap.get(ROUTEPARAM);
if (projectId) {
this.projectId = projectId;
this.getData(projectId);
}
}
public openNameDialog(): void {
@@ -85,7 +86,7 @@ export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
width: '400px',
});
dialogRef.afterClosed().subscribe(name => {
dialogRef.afterClosed().subscribe((name) => {
if (name) {
this.project.name = name;
this.updateName();
@@ -104,46 +105,68 @@ export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
dialogRef.afterClosed().subscribe((resp: PrivateLabelingSetting) => {
if (resp !== undefined) {
this.project.privateLabelingSetting = resp;
this.saveProject();
}
});
}
private async getData({ id }: Params): Promise<void> {
this.projectId = id;
private async getData(projectId: string): Promise<void> {
this.mgmtService
.getProjectByID(projectId)
.then((resp) => {
if (resp.project) {
this.project = resp.project;
this.mgmtService.getIAM().then(iam => {
this.isZitadel = iam.iamProjectId === this.projectId;
});
this.mgmtService.getIAM().then((iam) => {
this.isZitadel = iam.iamProjectId === this.projectId;
this.mgmtService.getProjectByID(id).then(resp => {
if (resp.project) {
this.project = resp.project;
}
}).catch(error => {
console.error(error);
this.toast.showError(error);
});
const breadcrumbs = [
new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'IAM',
routerLink: ['/system'],
}),
new Breadcrumb({
type: BreadcrumbType.ORG,
routerLink: ['/org'],
}),
new Breadcrumb({
type: BreadcrumbType.PROJECT,
name: this.project.name,
param: { key: ROUTEPARAM, value: projectId },
routerLink: ['/projects', projectId],
isZitadel: this.isZitadel,
}),
];
this.breadcrumbService.setBreadcrumb(breadcrumbs);
});
}
})
.catch((error) => {
console.error(error);
this.toast.showError(error);
});
this.loadMembers();
}
public loadMembers(): void {
this.loadingSubject.next(true);
from(this.mgmtService.listProjectMembers(this.projectId, 100, 0)).pipe(
map(resp => {
if (resp.details?.totalResult) {
this.totalMemberResult = resp.details?.totalResult;
} else {
this.totalMemberResult = 0;
}
return resp.resultList;
}),
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)),
).subscribe(members => {
this.membersSubject.next(members);
});
from(this.mgmtService.listProjectMembers(this.projectId, 100, 0))
.pipe(
map((resp) => {
if (resp.details?.totalResult) {
this.totalMemberResult = resp.details?.totalResult;
} else {
this.totalMemberResult = 0;
}
return resp.resultList;
}),
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)),
)
.subscribe((members) => {
this.membersSubject.next(members);
});
}
public changeState(newState: ProjectState): void {
@@ -157,18 +180,20 @@ export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.mgmtService.reactivateProject(this.projectId).then(() => {
this.toast.showInfo('PROJECT.TOAST.REACTIVATED', true);
this.project.state = ProjectState.PROJECT_STATE_ACTIVE;
this.refreshChanges$.emit();
}).catch(error => {
this.toast.showError(error);
});
this.mgmtService
.reactivateProject(this.projectId)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.REACTIVATED', true);
this.project.state = ProjectState.PROJECT_STATE_ACTIVE;
this.refreshChanges$.emit();
})
.catch((error) => {
this.toast.showError(error);
});
}
});
} else if (newState === ProjectState.PROJECT_STATE_INACTIVE) {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
@@ -179,15 +204,18 @@ export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.mgmtService.deactivateProject(this.projectId).then(() => {
this.toast.showInfo('PROJECT.TOAST.DEACTIVATED', true);
this.project.state = ProjectState.PROJECT_STATE_INACTIVE;
this.refreshChanges$.emit();
}).catch(error => {
this.toast.showError(error);
});
this.mgmtService
.deactivateProject(this.projectId)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.DEACTIVATED', true);
this.project.state = ProjectState.PROJECT_STATE_INACTIVE;
this.refreshChanges$.emit();
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
@@ -203,17 +231,20 @@ export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.mgmtService.removeProject(this.projectId).then(() => {
this.toast.showInfo('PROJECT.TOAST.DELETED', true);
const params: Params = {
'deferredReload': true,
};
this.router.navigate(['/projects'], { queryParams: params });
}).catch(error => {
this.toast.showError(error);
});
this.mgmtService
.removeProject(this.projectId)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.DELETED', true);
const params: Params = {
deferredReload: true,
};
this.router.navigate(['/projects'], { queryParams: params });
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
@@ -227,12 +258,15 @@ export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
req.setHasProjectCheck(this.project.hasProjectCheck);
req.setPrivateLabelingSetting(this.project.privateLabelingSetting);
this.mgmtService.updateProject(req).then(() => {
this.toast.showInfo('PROJECT.TOAST.UPDATED', true);
this.refreshChanges$.emit();
}).catch(error => {
this.toast.showError(error);
});
this.mgmtService
.updateProject(req)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.UPDATED', true);
this.refreshChanges$.emit();
})
.catch((error) => {
this.toast.showError(error);
});
}
public navigateBack(): void {
@@ -252,20 +286,22 @@ export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
const users: User.AsObject[] = resp.users;
const roles: string[] = resp.roles;
if (users && users.length && roles && roles.length) {
users.forEach(user => {
return this.mgmtService.addProjectMember(this.projectId, user.id, roles)
users.forEach((user) => {
return this.mgmtService
.addProjectMember(this.projectId, user.id, roles)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
setTimeout(() => {
this.loadMembers();
}, 1000);
}).catch(error => {
})
.catch((error) => {
this.toast.showError(error);
});
});
@@ -18,11 +18,13 @@ import { AppCardModule } from 'src/app/modules/app-card/app-card.module';
import { CardModule } from 'src/app/modules/card/card.module';
import { ChangesModule } from 'src/app/modules/changes/changes.module';
import { ContributorsModule } from 'src/app/modules/contributors/contributors.module';
import { InfoRowModule } from 'src/app/modules/info-row/info-row.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module';
import { PaginatorModule } from 'src/app/modules/paginator/paginator.module';
import { ProjectRolesModule } from 'src/app/modules/project-roles/project-roles.module';
import { ProjectRolesTableModule } from 'src/app/modules/project-roles-table/project-roles-table.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { TopViewModule } from 'src/app/modules/top-view/top-view.module';
import { UserGrantsModule } from 'src/app/modules/user-grants/user-grants.module';
import { WarnDialogModule } from 'src/app/modules/warn-dialog/warn-dialog.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
@@ -33,15 +35,9 @@ import { ApplicationGridComponent } from './application-grid/application-grid.co
import { ApplicationsComponent } from './applications/applications.component';
import { OwnedProjectDetailRoutingModule } from './owned-project-detail-routing.module';
import { OwnedProjectDetailComponent } from './owned-project-detail.component';
import { ProjectGrantsComponent } from './project-grants/project-grants.component';
@NgModule({
declarations: [
OwnedProjectDetailComponent,
ApplicationGridComponent,
ApplicationsComponent,
ProjectGrantsComponent,
],
declarations: [OwnedProjectDetailComponent, ApplicationGridComponent, ApplicationsComponent],
imports: [
CommonModule,
FormsModule,
@@ -52,11 +48,12 @@ import { ProjectGrantsComponent } from './project-grants/project-grants.componen
HasRoleModule,
MatButtonModule,
MatIconModule,
InfoRowModule,
ContributorsModule,
MatTabsModule,
WarnDialogModule,
MatTooltipModule,
ProjectRolesModule,
ProjectRolesTableModule,
HasRolePipeModule,
UserGrantsModule,
TimestampToDatePipeModule,
@@ -65,6 +62,7 @@ import { ProjectGrantsComponent } from './project-grants/project-grants.componen
CardModule,
PaginatorModule,
MatRippleModule,
TopViewModule,
MatCheckboxModule,
MatSelectModule,
MatMenuModule,
@@ -76,4 +74,4 @@ import { ProjectGrantsComponent } from './project-grants/project-grants.componen
LocalizedDatePipeModule,
],
})
export class OwnedProjectDetailModule { }
export class OwnedProjectDetailModule {}
@@ -1,97 +0,0 @@
<cnsl-refresh-table [loading]="dataSource?.loading$ | async" *ngIf="projectId" (refreshed)="loadGrantsPage()"
[dataSize]="dataSource.totalResult" [selection]="selection" [timestamp]="dataSource?.viewTimestamp"
[emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes" (refreshed)="getRoleOptions(projectId)">
<ng-template cnslHasRole [hasRole]="['project.grant.member.delete:'+projectId, 'project.grant.member.delete']"
actions>
<button (click)="deleteSelectedGrants()" [disabled]="disabled" mat-icon-button *ngIf="selection.hasValue()"
color="warn">
<i class="las la-trash"></i>
</button>
</ng-template>
<ng-template cnslHasRole [hasRole]="['project.grant.member.write:'+projectId,'project.grant.member.write']" actions>
<a [disabled]="disabled" color="primary" color="primary" mat-raised-button
[routerLink]="[ '/projects', projectId, 'grants', 'create']">
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a>
</ng-template>
<div class="table-wrapper">
<table mat-table multiTemplateDataRows class="table" aria-label="Elements" [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox [disabled]="disabled" color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox [disabled]="disabled" color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="grantedOrgName">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.GRANTEDORG' | translate }} </th>
<td [routerLink]="['/projects',grant.projectId,'grant', grant.grantId]" class="pointer" mat-cell
*matCellDef="let grant">
{{grant.grantedOrgName}} </td>
</ng-container>
<ng-container matColumnDef="dates">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.DATES' | translate }} </th>
<td [routerLink]="['/projects',grant.projectId,'grant', grant.grantId]" class="pointer" mat-cell
*matCellDef="let grant">
<div class="date-block">
<span class="date-sub">{{ 'PROJECT.GRANT.CREATIONDATE' | translate }}:</span>
<span>{{grant.details.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
</div>
<div class="date-block">
<span class="date-sub">{{ 'PROJECT.GRANT.CHANGEDATE' | translate }}</span>
<span>{{grant.details.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
</div>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.STATE' | translate }} </th>
<td mat-cell *matCellDef="let grant" [routerLink]="grant.id ? ['/users', grant.id ]: null">
<span class="state"
[ngClass]="{'active': grant.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE, 'inactive': grant.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE}">
{{ 'PROJECT.GRANT.STATES.'+grant.state | translate }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="roleNamesList">
<th mat-header-cell *matHeaderCellDef class="role-row"> {{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}
</th>
<td class="pointer role-row" mat-cell *matCellDef="let grant">
<cnsl-form-field class="form-field" appearance="outline" *ngIf="grant.roleKeysList">
<cnsl-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</cnsl-label>
<mat-select [(ngModel)]="grant.roleKeysList" multiple [disabled]="disabled"
(selectionChange)="updateRoles(grant, $event)">
<mat-option *ngFor="let role of memberRoleOptions" [value]="role.key">
{{role.key}}
</mat-option>
</mat-select>
</cnsl-form-field>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let grant; columns: displayedColumns;">
</tr>
</table>
<div *ngIf="(dataSource.loading$ | async) === false && !dataSource?.totalResult" class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{'PROJECT.GRANT.EMPTY' | translate}}</span>
</div>
<cnsl-paginator class="paginator" #paginator [pageSize]="50" [timestamp]="dataSource?.viewTimestamp"
[pageSizeOptions]="[25, 50, 100, 250]" [length]="dataSource.totalResult"
(page)="loadGrantsPage($event.pageIndex, $event.pageSize)">
</cnsl-paginator>
</div>
</cnsl-refresh-table>
@@ -1,64 +0,0 @@
.table-wrapper {
overflow: auto;
.table,
.paginator {
width: 100%;
background-color: #2d2e30;
td,
th {
padding: 0 1rem;
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
.role {
display: inline-block;
border-radius: .5rem;
border: 1px solid #ffffff30;
padding: .5rem 1rem;
margin-top: .25rem;
margin-bottom: .25rem;
margin-left: 0;
margin-right: .5rem;
cursor: pointer;
}
}
tr {
outline: none;
}
tr.detail-row {
height: 0;
}
}
}
.pointer {
outline: none;
cursor: pointer;
}
.role-row {
min-width: 100px;
}
.date-block {
margin: .5rem 0;
display: block;
min-width: 120px;
.date-sub {
font-size: 13px;
color: var(--grey);
display: block;
}
}
@@ -1,112 +0,0 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { SelectionModel } from '@angular/cdk/collections';
import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core';
import { MatSelectChange } from '@angular/material/select';
import { MatTable } from '@angular/material/table';
import { tap } from 'rxjs/operators';
import { PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
import { GrantedProject, ProjectGrantState, Role } from 'src/app/proto/generated/zitadel/project_pb';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { ProjectGrantsDataSource } from './project-grants-datasource';
@Component({
selector: 'cnsl-project-grants',
templateUrl: './project-grants.component.html',
styleUrls: ['./project-grants.component.scss'],
animations: [
trigger('detailExpand', [
state('collapsed', style({ height: '0px', minHeight: '0' })),
state('expanded', style({ height: '*' })),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
]),
],
})
export class ProjectGrantsComponent implements OnInit, AfterViewInit {
@Input() refreshOnPreviousRoutes: string[] = [];
@Input() public projectId: string = '';
@Input() public disabled: boolean = false;
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
@ViewChild(MatTable) public table!: MatTable<GrantedProject.AsObject>;
public dataSource!: ProjectGrantsDataSource;
public selection: SelectionModel<GrantedProject.AsObject> = new SelectionModel<GrantedProject.AsObject>(true, []);
public memberRoleOptions: Role.AsObject[] = [];
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
public displayedColumns: string[] = ['select', 'grantedOrgName','state', 'dates'];
ProjectGrantState: any = ProjectGrantState;
constructor(private mgmtService: ManagementService, private toast: ToastService) { }
public ngOnInit(): void {
this.dataSource = new ProjectGrantsDataSource(this.mgmtService);
this.dataSource.loadGrants(this.projectId, 0, 25, 'asc');
this.getRoleOptions(this.projectId);
}
public ngAfterViewInit(): void {
this.paginator.page
.pipe(
tap(() => this.loadGrantsPage()),
)
.subscribe();
}
public loadGrantsPage(pageIndex?: number, pageSize?: number): void {
this.dataSource.loadGrants(
this.projectId,
pageIndex ?? this.paginator.pageIndex,
pageSize ?? this.paginator.pageSize,
);
}
public isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.grantsSubject.value.length;
return numSelected === numRows;
}
public masterToggle(): void {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.grantsSubject.value.forEach(row => this.selection.select(row));
}
public getRoleOptions(projectId: string): void {
this.mgmtService.listProjectRoles(projectId, 100, 0).then(resp => {
this.memberRoleOptions = resp.resultList;
});
}
updateRoles(grant: GrantedProject.AsObject, selectionChange: MatSelectChange): void {
this.mgmtService.updateProjectGrant(grant.grantId, grant.projectId, selectionChange.value)
.then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTCHANGED', true);
}).catch(error => {
this.toast.showError(error);
});
}
deleteSelectedGrants(): void {
const promises = this.selection.selected.map(grant => {
return this.mgmtService.removeProjectGrant(grant.grantId, grant.projectId);
});
Promise.all(promises).then(() => {
this.toast.showInfo('GRANTS.TOAST.BULKREMOVED', true);
const data = this.dataSource.grantsSubject.getValue();
this.selection.selected.forEach((item) => {
const index = data.findIndex(i => i.grantId === item.grantId);
if (index > -1) {
data.splice(index, 1);
this.dataSource.grantsSubject.next(data);
}
});
this.selection.clear();
}).catch(error => {
this.toast.showError(error);
});
}
}
@@ -1,80 +0,0 @@
<div class="view-toggle">
<button (click)="closeGridView()" mat-icon-button matTooltip="{{'PROJECT.TABLE.SHOWTABLE' | translate}}">
<i class="show list view las la-th-list"></i>
</button>
</div>
<div class="owned-project-grid-container">
<mat-progress-bar *ngIf="loading" class="spinner" color="primary" mode="indeterminate"></mat-progress-bar>
<p class="n-items" *ngIf="!loading && selection.selected.length > 0">{{'PROJECT.PAGES.PINNED' | translate}}</p>
<div class="item card" *ngFor="let item of selection.selected; index as i"
(click)="navigateToProject(item.id, $event)"
[ngClass]="{ inactive: item.state !== ProjectState.PROJECT_STATE_ACTIVE}">
<div class="text-part">
<span *ngIf="item.details && item.details.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
{{ item.details.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
<div class="name-row">
<span class="name" *ngIf="item.name">{{ item.name }}</span>
<div class="state-dot" [ngClass]="{'active': item.state === ProjectState.PROJECT_STATE_ACTIVE, 'inactive': item.state === ProjectState.PROJECT_STATE_INACTIVE}"></div>
</div>
<span *ngIf="item.details && item.details.creationDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
{{ item.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
<span class="fill-space"></span>
</div>
<template [ngTemplateOutlet]="deleteButton" [ngTemplateOutletContext]="{key: item}"></template>
<template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{key: item}"></template>
</div>
</div>
<div class="owned-project-grid-container">
<p class="n-items" *ngIf="!loading && notPinned.length > 0">{{'PROJECT.PAGES.ALL' | translate}}</p>
<div class="item card" *ngFor="let item of notPinned; index as i" (click)="navigateToProject(item.id, $event)"
[ngClass]="{ inactive: item.state !== ProjectState.PROJECT_STATE_ACTIVE}">
<div class="text-part">
<span *ngIf="item.details && item.details.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
{{ item.details.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
<div class="name-row">
<span class="name" *ngIf="item.name">{{ item.name }}</span>
<div class="state-dot" [ngClass]="{'active': item.state === ProjectState.PROJECT_STATE_ACTIVE, 'inactive': item.state === ProjectState.PROJECT_STATE_INACTIVE}"></div>
</div>
<span *ngIf="item.details && item.details.creationDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
{{
item.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span>
<span class="fill-space"></span>
</div>
<template [ngTemplateOutlet]="deleteButton" [ngTemplateOutletContext]="{key: item}"></template>
<template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{key: item}"></template>
</div>
<p class="n-items" *ngIf="!loading && items.length === 0">{{'PROJECT.PAGES.NOITEMS' | translate}}</p>
<ng-template cnslHasRole [hasRole]="['project.create']">
<div class="add-project-button card" (click)="addItem()">
<mat-icon class="icon">add</mat-icon>
<span>{{'PROJECT.PAGES.ADDNEW' | translate}}</span>
</div>
</ng-template>
</div>
<ng-template #deleteButton let-key="key">
<button *ngIf="key.id !== zitadelProjectId" matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn"
(click)="deleteProject($event, key)" class="delete-button" mat-icon-button>
<i class="las la-trash"></i>
</button>
</ng-template>
<ng-template #toggleButton let-key="key">
<button matTooltip="{{'ACTIONS.PIN' | translate}}" [ngClass]="{ selected: selection.isSelected(key)}"
(click)="toggle(key,$event)" class="edit-button" mat-icon-button>
<mat-icon *ngIf="selection.isSelected(key)" svgIcon="mdi_pin"></mat-icon>
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(key)"></mat-icon>
</button>
</ng-template>
@@ -1,237 +0,0 @@
@mixin owned-project-grid-theme($theme) {
$foreground: map-get($theme, foreground);
.view-toggle {
border-bottom: 1px solid map-get($foreground, divider);
}
}
.view-toggle {
width: 100%;
display: flex;
justify-content: flex-end;
padding-bottom: .5rem;
.anim-list {
display: flex;
justify-content: flex-end;
align-items: center;
}
button {
&.left-button {
margin-right: 1rem;
}
}
}
.spinner {
margin: 1rem;
}
.owned-project-grid-container {
display: flex;
flex-wrap: wrap;
margin: 0 -1rem;
box-sizing: border-box;
h2 {
padding: 0 1rem;
flex-basis: 100%;
}
.item {
position: relative;
z-index: 100;
margin: 1rem;
flex-basis: 270px;
display: flex;
text-decoration: none;
cursor: pointer;
padding-top: 0;
padding-right: 0;
padding-bottom: 0;
padding-left: 1rem;
border-radius: .5rem;
box-sizing: border-box;
min-height: 166px;
transition: box-shadow .1s ease-in;
img {
height: 50px;
width: 50px;
margin: 1rem;
}
.text-part {
flex: 1;
display: flex;
flex-direction: column;
min-height: 70px;
padding: .5rem 1rem 0 0;
.top {
font-size: .8rem;
margin-bottom: 0;
margin-top: .5rem;
color: var(--grey);
}
.name-row {
display: flex;
align-items: center;
margin: 1rem 0 .5rem 0;
.name {
font-size: 1.2rem;
margin-right: .5rem;
}
.state-dot {
height: 8px;
width: 8px;
border-radius: 50%;
&.active {
background-color: var(--success);
}
&.inactive {
background-color: var(--warn);
}
}
}
.description {
font-size: .8rem;
}
.created {
font-size: .8rem;
color: var(--grey);
}
.organization {
display: flex;
align-items: center;
margin-top: 1rem;
.org_avatar {
height: 25px;
width: 25px;
border-radius: 50%;
margin: 0;
margin-right: 1rem;
}
}
.fill-space {
flex: 1;
}
span {
margin: 2px 0;
}
}
.delete-button {
opacity: 0;
user-select: none;
position: absolute;
bottom: 0;
right: 30px;
margin: 0;
margin-bottom: .25rem;
&:not(:hover) {
color: #8795a1;
}
}
.edit-button {
opacity: 0;
user-select: none;
position: absolute;
bottom: 0;
right: 0;
margin: 0;
margin-bottom: .25rem;
color: var(--grey);
&.selected {
opacity: 1;
}
}
&:hover {
box-shadow: 0 5px 10px rgba(0, 0, 0, .12);
.delete-button,
.edit-button {
opacity: 1;
}
}
&.selected {
.delete-button,
.edit-button {
opacity: 1;
}
.icon {
opacity: 1;
}
}
@media only screen and (max-width: 450px) {
flex-basis: 100%;
}
}
.add-project-button {
z-index: 100;
flex-basis: 260px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
min-height: 166px;
border-radius: .5rem;
margin: 1rem;
box-sizing: border-box;
transition: box-shadow .1s ease-in;
.icon {
display: flex;
justify-content: center;
align-self: center;
margin-bottom: 1rem;
height: 2.5rem;
line-height: 2.5rem;
font-size: 2.5rem;
}
&:hover {
box-shadow: 0 5px 10px rgba(0, 0, 0, .12);
.icon,
span {
&.disabled {
color: gray;
}
}
}
@media only screen and (max-width: 450px) {
flex-basis: 100%;
}
}
}
.n-items {
padding: 0 1rem;
font-size: .8rem;
color: var(--grey);
flex-basis: 100%;
}
@@ -1,170 +0,0 @@
import { animate, animateChild, keyframes, query, stagger, style, transition, trigger } from '@angular/animations';
import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
import { Project, ProjectState } from 'src/app/proto/generated/zitadel/project_pb';
import { ManagementService } from 'src/app/services/mgmt.service';
import { StorageKey, StorageLocation, StorageService } from 'src/app/services/storage.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'cnsl-owned-project-grid',
templateUrl: './owned-project-grid.component.html',
styleUrls: ['./owned-project-grid.component.scss'],
animations: [
trigger('cardAnimation', [
transition('* => *', [
query('@animate', stagger('100ms', animateChild()), { optional: true }),
]),
]),
trigger('animate', [
transition(':enter', [
animate('.2s ease-in', keyframes([
style({ opacity: 0, transform: 'translateY(-50%)', offset: 0 }),
style({ opacity: .5, transform: 'translateY(-10px) scale(1.1)', offset: 0.3 }),
style({ opacity: 1, transform: 'translateY(0)', offset: 1 }),
])),
]),
transition(':leave', [
animate('.2s ease-out', keyframes([
style({ opacity: 1, transform: 'scale(1.1)', offset: 0 }),
style({ opacity: .5, transform: 'scale(.5)', offset: 0.3 }),
style({ opacity: 0, transform: 'scale(0)', offset: 1 }),
])),
]),
]),
],
})
export class OwnedProjectGridComponent implements OnChanges {
@Input() items: Array<Project.AsObject> = [];
public notPinned: Array<Project.AsObject> = [];
@Output() newClicked: EventEmitter<boolean> = new EventEmitter();
@Output() changedView: EventEmitter<boolean> = new EventEmitter();
@Input() loading: boolean | null = false;
public selection: SelectionModel<Project.AsObject> = new SelectionModel<Project.AsObject>(true, []);
public showNewProject: boolean = false;
public ProjectState: any = ProjectState;
@Input() public zitadelProjectId: string = '';
constructor(
private router: Router,
private dialog: MatDialog,
private storage: StorageService,
private mgmtService: ManagementService,
private toast: ToastService,
) {
this.selection.changed.subscribe(selection => {
this.setPrefixedItem('pinned-projects', JSON.stringify(
this.selection.selected.map(item => item.id),
)).then(() => {
selection.added.forEach(item => {
const index = this.notPinned.findIndex(i => i.id === item.id);
this.notPinned.splice(index, 1);
});
this.notPinned.push(...selection.removed);
});
});
}
public selectItem(item: Project.AsObject, event?: any): void {
if (event && !event.target.classList.contains('mat-icon')) {
this.router.navigate(['/projects', item.id]);
} else if (!event) {
this.router.navigate(['/projects', item.id]);
}
}
public addItem(): void {
this.newClicked.emit(true);
}
public ngOnChanges(changes: SimpleChanges): void {
if (changes.items.currentValue && changes.items.currentValue.length > 0) {
this.notPinned = Object.assign([], this.items);
this.reorganizeItems();
}
}
public reorganizeItems(): void {
this.getPrefixedItem('pinned-projects').then(storageEntry => {
if (storageEntry) {
const array: string[] = JSON.parse(storageEntry);
const toSelect: Project.AsObject[] = this.items.filter((item) => {
if (array.includes(item.id)) {
return true;
} else {
return false;
}
});
this.selection.select(...toSelect);
}
});
}
private async getPrefixedItem(key: string): Promise<string | null> {
const org = this.storage.getItem<Org.AsObject>(StorageKey.organization, StorageLocation.session) as Org.AsObject;
return localStorage.getItem(`${org?.id}:${key}`);
}
private async setPrefixedItem(key: string, value: any): Promise<void> {
const org = this.storage.getItem<Org.AsObject>(StorageKey.organization, StorageLocation.session) as Org.AsObject;
return localStorage.setItem(`${org.id}:${key}`, value);
}
public navigateToProject(id: string, event: any): void {
if (event && event.srcElement && event.srcElement.localName !== 'button') {
this.router.navigate(['/projects', id]);
}
}
public closeGridView(): void {
this.changedView.emit(true);
}
public toggle(item: Project.AsObject, event: any): void {
event.stopPropagation();
this.selection.toggle(item);
}
public deleteProject(event: any, item: Project.AsObject): void {
event.stopPropagation();
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'PROJECT.PAGES.DIALOG.DELETE.TITLE',
descriptionKey: 'PROJECT.PAGES.DIALOG.DELETE.DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp && item.id !== this.zitadelProjectId) {
this.mgmtService.removeProject(item.id).then(() => {
this.toast.showInfo('PROJECT.TOAST.DELETED', true);
const index = this.items.findIndex(iter => iter.id === item.id);
if (index > -1) {
this.items.splice(index, 1);
}
const indexSelection = this.selection.selected.findIndex(iter => iter.id === item.id);
if (indexSelection > -1) {
this.selection.selected.splice(indexSelection, 1);
}
const indexPinned = this.notPinned.findIndex(iter => iter.id === item.id);
if (indexPinned > -1) {
this.notPinned.splice(indexPinned, 1);
}
}).catch(error => {
this.toast.showError(error);
});
}
});
}
}
@@ -1,90 +0,0 @@
<cnsl-owned-project-grid [zitadelProjectId]="zitadelProjectId" *ngIf="grid && zitadelProjectId"
[loading]="loading$ | async" (changedView)="grid = false" [items]="ownedProjectList || []"
(newClicked)="addProject()">
</cnsl-owned-project-grid>
<div *ngIf="!grid" class="view-toggle">
<button (click)="grid = true" mat-icon-button class="icon-button"
matTooltip="{{'PROJECT.TABLE.SHOWGRID' | translate}}">
<i matTooltip="show grid view" class="las la-th-large"></i>
</button>
</div>
<div *ngIf="!grid && ownedProjectList">
<cnsl-refresh-table (refreshed)="refreshPage()" [dataSize]="totalResult" [timestamp]="viewTimestamp"
[selection]="selection" [loading]="loading$ | async">
<ng-template actions cnslHasRole [hasRole]="['project.create']">
<a [routerLink]="[ '/projects', 'create']" color="primary" mat-raised-button>
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a>
</ng-template>
<div class="table-wrapper">
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.NAME' | translate }} </th>
<td [routerLink]="['/projects', project.id]" mat-cell *matCellDef="let project"> {{project.name}} </td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.STATE' | translate }} </th>
<td [routerLink]="['/projects', project.id]" mat-cell *matCellDef="let project">
<span class="state" [ngClass]="{'active': project.state === ProjectState.PROJECT_STATE_ACTIVE, 'inactive': project.state === ProjectState.PROJECT_STATE_INACTIVE}" *ngIf="project.state">{{'PROJECT.STATE.'+project.state | translate}}</span>
</td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CREATIONDATE' | translate }} </th>
<td [routerLink]="['/projects', project.id]" mat-cell *matCellDef="let project">
<span *ngIf="project.details.creationDate">{{project.details.creationDate | timestampToDate |
localizedDate: 'EEE dd. MMM, HH:mm'}}</span>
</td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CHANGEDATE' | translate }} </th>
<td [routerLink]="['/projects', project.id]" mat-cell *matCellDef="let project">
<span *ngIf="project.details.changeDate">{{project.details.changeDate | timestampToDate |
localizedDate: 'EEE dd. MMM, HH:mm'}}</span>
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let project">
<button class="dlt-button" *ngIf="project.id !== zitadelProjectId" color="warn"
mat-icon-button matTooltip="{{'ACTIONS.DELETE' | translate}}"
(click)="deleteProject(project.id)">
<i class="las la-trash"></i>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{'PROJECT.TABLE.EMPTY' | translate}}</span>
</div>
<cnsl-paginator class="paginator" [timestamp]="viewTimestamp" [length]="totalResult" [pageSize]="10" [pageSizeOptions]="[5, 10, 20]"
(page)="changePage($event)"></cnsl-paginator>
</div>
</cnsl-refresh-table>
</div>
@@ -1,58 +0,0 @@
h1 {
margin-top: 0;
}
.sub {
color: var(--grey);
margin-bottom: 2rem;
}
.view-toggle {
width: 100%;
display: flex;
justify-content: flex-end;
.icon-button {
display: block;
border-radius: .5rem;
}
}
.table-wrapper {
overflow: auto;
.table,
.paginator {
width: 100%;
background-color: #2d2e30;
td,
th {
padding-left: .5rem;
padding-right: .5rem;
&:first-child {
padding-left: 0;
padding-right: .5rem;
}
&:last-child {
padding-right: 0;
}
}
}
}
tr {
outline: none;
.dlt-button {
visibility: hidden;
}
&:hover {
.dlt-button {
visibility: visible;
}
}
}
@@ -1,25 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { OwnedProjectListComponent } from './owned-project-list.component';
describe('OwnedProjectListComponent', () => {
let component: OwnedProjectListComponent;
let fixture: ComponentFixture<OwnedProjectListComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [OwnedProjectListComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OwnedProjectListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,190 +0,0 @@
import { animate, animateChild, query, stagger, style, transition, trigger } from '@angular/animations';
import { SelectionModel } from '@angular/cdk/collections';
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { PageEvent, PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { Project, ProjectState } from 'src/app/proto/generated/zitadel/project_pb';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'cnsl-owned-project-list',
templateUrl: './owned-project-list.component.html',
styleUrls: ['./owned-project-list.component.scss'],
animations: [
trigger('list', [
transition(':enter', [
query('@animate',
stagger(80, animateChild()),
),
]),
]),
trigger('animate', [
transition(':enter', [
style({ opacity: 0, transform: 'translateY(-100%)' }),
animate('100ms', style({ opacity: 1, transform: 'translateY(0)' })),
]),
transition(':leave', [
style({ opacity: 1, transform: 'translateY(0)' }),
animate('100ms', style({ opacity: 0, transform: 'translateY(100%)' })),
]),
]),
],
})
export class OwnedProjectListComponent implements OnInit, OnDestroy {
public totalResult: number = 0;
public viewTimestamp!: Timestamp.AsObject;
public dataSource: MatTableDataSource<Project.AsObject> =
new MatTableDataSource<Project.AsObject>();
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
public ownedProjectList: Project.AsObject[] = [];
public displayedColumns: string[] = ['select', 'name', 'state', 'creationDate', 'changeDate', 'actions'];
public selection: SelectionModel<Project.AsObject> = new SelectionModel<Project.AsObject>(true, []);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public grid: boolean = true;
private subscription?: Subscription;
public zitadelProjectId: string = '';
public ProjectState: any = ProjectState;
constructor(private router: Router,
private route: ActivatedRoute,
public translate: TranslateService,
private mgmtService: ManagementService,
private toast: ToastService,
private dialog: MatDialog,
) {
this.mgmtService.getIAM().then(iam => {
this.zitadelProjectId = iam.iamProjectId;
});
}
public ngOnInit(): void {
this.route.queryParams.pipe(take(1)).subscribe(params => {
this.getData();
if (params.deferredReload) {
setTimeout(() => {
this.getData();
}, 2000);
}
});
}
public ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
public isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.data.length;
return numSelected === numRows;
}
public masterToggle(): void {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.data.forEach(row => this.selection.select(row));
}
public changePage(event: PageEvent): void {
this.getData(event.pageSize, event.pageSize * event.pageIndex);
}
public addProject(): void {
this.router.navigate(['/projects', 'create']);
}
private async getData(limit?: number, offset?: number): Promise<void> {
this.loadingSubject.next(true);
this.mgmtService.listProjects(limit, offset).then(resp => {
this.ownedProjectList = resp.resultList;
if (resp.details?.totalResult) {
this.totalResult = resp.details.totalResult;
} else {
this.totalResult = 0;
}
if (this.totalResult > 10) {
this.grid = false;
}
if (resp.details?.viewTimestamp) {
this.viewTimestamp = resp.details?.viewTimestamp;
}
this.dataSource.data = this.ownedProjectList;
this.loadingSubject.next(false);
}).catch(error => {
console.error(error);
this.toast.showError(error);
this.loadingSubject.next(false);
});
this.ownedProjectList = [];
}
public reactivateSelectedProjects(): void {
const promises = this.selection.selected.map(project => {
this.mgmtService.reactivateProject(project.id);
});
Promise.all(promises).then(() => {
this.toast.showInfo('PROJECT.TOAST.REACTIVATED', true);
}).catch(error => {
this.toast.showError(error);
});
}
public deactivateSelectedProjects(): void {
const promises = this.selection.selected.map(project => {
this.mgmtService.deactivateProject(project.id);
});
Promise.all(promises).then(() => {
this.toast.showInfo('PROJECT.TOAST.DEACTIVATED', true);
}).catch(error => {
this.toast.showError(error);
});
}
public refreshPage(): void {
this.selection.clear();
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize);
}
public deleteProject(id: string): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'PROJECT.PAGES.DIALOG.DELETE.TITLE',
descriptionKey: 'PROJECT.PAGES.DIALOG.DELETE.DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (this.zitadelProjectId && resp && id !== this.zitadelProjectId) {
this.mgmtService.removeProject(id).then(() => {
this.toast.showInfo('PROJECT.TOAST.DELETED', true);
setTimeout(() => {
this.refreshPage();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
});
}
}
@@ -2,77 +2,78 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { RoleGuard } from 'src/app/guards/role.guard';
import { ProjectType } from 'src/app/modules/project-members/project-members-datasource';
import { OwnedProjectsComponent } from './owned-projects.component';
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
const routes: Routes = [
{
path: '',
component: OwnedProjectsComponent,
data: { animation: 'HomePage' },
{
path: '',
data: {
animation: 'HomePage',
roles: ['project.read'],
},
{
path: 'create',
loadChildren: () => import('../project-create/project-create.module').then(m => m.ProjectCreateModule),
canActivate: [RoleGuard],
data: {
roles: ['project.create'],
},
canActivate: [RoleGuard],
loadChildren: () => import('./owned-project-detail/owned-project-detail.module').then((m) => m.OwnedProjectDetailModule),
},
{
path: 'members',
data: {
type: ProjectType.PROJECTTYPE_OWNED,
roles: ['project.member.read'],
},
{
path: ':id',
data: {
animation: 'HomePage',
roles: ['project.read'],
},
canActivate: [RoleGuard],
loadChildren: () => import('./owned-project-detail/owned-project-detail.module')
.then(m => m.OwnedProjectDetailModule),
canActivate: [RoleGuard],
loadChildren: () => import('src/app/modules/project-members/project-members.module').then((m) => m.ProjectMembersModule),
},
{
path: 'apps',
data: {
animation: 'AddPage',
roles: ['project.app.read'],
},
{
path: ':projectid',
children: [
{
path: 'members',
data: {
type: ProjectType.PROJECTTYPE_OWNED,
roles: ['project.member.read'],
},
canActivate: [RoleGuard],
loadChildren: () => import('src/app/modules/project-members/project-members.module')
.then(m => m.ProjectMembersModule),
},
{
path: 'apps',
data: {
animation: 'AddPage',
roles: ['project.app.read'],
},
canActivate: [RoleGuard],
loadChildren: () => import('src/app/pages/projects/apps/apps.module')
.then(m => m.AppsModule),
},
{
path: 'roles/create',
loadChildren: () => import('../project-role-create/project-role-create.module')
.then(m => m.ProjectRoleCreateModule),
},
{
path: 'grants/create',
loadChildren: () => import('../project-grant-create/project-grant-create.module')
.then(m => m.ProjectGrantCreateModule),
},
{
path: 'grant/:grantid',
loadChildren: () => import('./project-grant-detail/project-grant-detail.module')
.then(m => m.ProjectGrantDetailModule),
},
],
canActivate: [RoleGuard],
loadChildren: () => import('src/app/pages/projects/apps/apps.module').then((m) => m.AppsModule),
},
{
path: 'projectgrants',
data: {
// animation: 'AddPage',
// roles: ['project.grant.read:' + ':projectid', 'project.grant.read'],
},
// canActivate: [RoleGuard],
loadChildren: () =>
import('src/app/pages/projects/owned-projects/project-grants/project-grants.module').then(
(m) => m.ProjectGrantsModule,
),
},
{
path: 'grants',
loadChildren: () => import('src/app/pages/grants/grants.module').then((m) => m.GrantsModule),
canActivate: [RoleGuard],
data: {
roles: ['user.grant.read'],
context: UserGrantContext.OWNED_PROJECT,
},
},
{
path: 'roles',
loadChildren: () =>
import('src/app/pages/projects/owned-projects/project-roles/project-roles.module').then((m) => m.ProjectRolesModule),
},
{
path: 'roles/create',
loadChildren: () => import('./project-role-create/project-role-create.module').then((m) => m.ProjectRoleCreateModule),
},
{
path: 'projectgrants/create',
loadChildren: () => import('./project-grant-create/project-grant-create.module').then((m) => m.ProjectGrantCreateModule),
},
{
path: 'projectgrants/:grantid',
loadChildren: () => import('./project-grant-detail/project-grant-detail.module').then((m) => m.ProjectGrantDetailModule),
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class OwnedProjectsRoutingModule { }
export class OwnedProjectsRoutingModule {}
@@ -1,7 +0,0 @@
<div class="enlarged-container">
<h1>{{ 'PROJECT.PAGES.LIST' | translate }}</h1>
<p class="sub">{{ 'PROJECT.PAGES.LISTDESCRIPTION' | translate }}</p>
<h2>{{'PROJECT.PAGES.TYPE.OWNED' | translate}}</h2>
<cnsl-owned-project-list></cnsl-owned-project-list>
</div>
@@ -1,13 +0,0 @@
h1 {
margin: 0;
}
.sub {
color: var(--grey);
margin-bottom: 2rem;
font-size: 14px;
}
.max-width-container {
padding-bottom: 3rem;
}
@@ -1,25 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { OwnedProjectsComponent } from './owned-projects.component';
describe('OwnedProjectComponent', () => {
let component: OwnedProjectsComponent;
let fixture: ComponentFixture<OwnedProjectsComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [OwnedProjectsComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OwnedProjectsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,8 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'cnsl-owned-projects',
templateUrl: './owned-projects.component.html',
styleUrls: ['./owned-projects.component.scss'],
})
export class OwnedProjectsComponent { }
@@ -4,6 +4,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips';
import { MatRippleModule } from '@angular/material/core';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@@ -28,24 +29,16 @@ import { NameDialogModule } from '../../../modules/name-dialog/name-dialog.modul
import {
ProjectPrivateLabelingDialogModule,
} from '../../../modules/project-private-labeling-dialog/project-private-labeling-dialog.module';
import { OwnedProjectGridComponent } from './owned-project-list/owned-project-grid/owned-project-grid.component';
import { OwnedProjectListComponent } from './owned-project-list/owned-project-list.component';
import { OwnedProjectsRoutingModule } from './owned-projects-routing.module';
import { OwnedProjectsComponent } from './owned-projects.component';
@NgModule({
declarations: [
OwnedProjectsComponent,
OwnedProjectListComponent,
OwnedProjectGridComponent,
],
declarations: [],
imports: [
CommonModule,
OwnedProjectsRoutingModule,
UserGrantsModule,
FormsModule,
NameDialogModule,
ReactiveFormsModule,
TranslateModule,
AvatarModule,
ReactiveFormsModule,
@@ -69,6 +62,7 @@ import { OwnedProjectsComponent } from './owned-projects.component';
LocalizedDatePipeModule,
SharedModule,
RefreshTableModule,
MatRippleModule,
],
})
export class OwnedProjectsModule { }
export class OwnedProjectsModule {}
@@ -0,0 +1,62 @@
<div class="max-width-container container">
<div class="abort-container">
<button (click)="close()" mat-icon-button>
<mat-icon>close</mat-icon>
</button>
<span class="abort">{{ 'PROJECT.GRANT.CREATE.TITLE' | translate }}</span><span class="abort-2">Step
{{ currentCreateStep }} of
{{ STEPS }}</span>
</div>
<div class="create-content">
<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>
</ng-container>
<ng-container *ngIf="currentCreateStep === 2">
<h1>{{'PROJECT.GRANT.CREATE.SEL_ROLES' | translate}}</h1>
<cnsl-card *ngIf="projectId">
<cnsl-project-roles-table
[displayedColumns]="['select', 'key', 'displayname', 'group', 'creationDate', 'changeDate']"
(changedSelection)="selectRoles($event)" [projectId]="projectId">
</cnsl-project-roles-table>
</cnsl-card>
</ng-container>
<div class="btn-container">
<ng-container *ngIf="currentCreateStep === 1">
<button [disabled]="!org" (click)="next()" color="primary" mat-raised-button class="big-button" cdkFocusInitial>
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</ng-container>
<ng-container *ngIf="currentCreateStep === STEPS">
<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>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</ng-container>
</div>
</div>
</div>
@@ -1,9 +1,5 @@
.container {
padding: 4rem 4rem 2rem 4rem;
@media only screen and (max-width: 450px) {
padding: 4rem 1rem 2rem 1rem;
}
padding: 4rem 0 2rem 0;
.abort-container {
display: flex;
@@ -22,11 +18,18 @@
white-space: nowrap;
}
}
}
.org-domain {
width: 400px;
margin-right: 1rem;
.create-content {
padding-left: 4.5rem;
form {
max-width: 35rem;
.org-domain {
margin-right: 1rem;
}
}
}
}
.domain-button {
@@ -46,6 +49,6 @@
.big-button {
display: block;
padding: .5rem 4rem;
padding: 0.5rem 4rem;
}
}
@@ -11,31 +11,31 @@ import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { CardModule } from 'src/app/modules/card/card.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { ProjectRolesModule } from 'src/app/modules/project-roles/project-roles.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 { ProjectGrantCreateRoutingModule } from './project-grant-create-routing.module';
import { ProjectGrantCreateComponent } from './project-grant-create.component';
@NgModule({
declarations: [ProjectGrantCreateComponent],
imports: [
ProjectGrantCreateRoutingModule,
CommonModule,
MatAutocompleteModule,
MatChipsModule,
MatButtonModule,
InputModule,
CardModule,
MatCheckboxModule,
ProjectRolesModule,
MatIconModule,
MatTooltipModule,
HasRolePipeModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
FormsModule,
TranslateModule,
],
declarations: [ProjectGrantCreateComponent],
imports: [
ProjectGrantCreateRoutingModule,
CommonModule,
MatAutocompleteModule,
MatChipsModule,
MatButtonModule,
InputModule,
CardModule,
MatCheckboxModule,
ProjectRolesTableModule,
MatIconModule,
MatTooltipModule,
HasRolePipeModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
FormsModule,
TranslateModule,
],
})
export class ProjectGrantCreateModule { }
export class ProjectGrantCreateModule {}
@@ -1,64 +1,65 @@
<cnsl-detail-layout [backRouterLink]="[ '/projects', projectid]" title="{{ 'PROJECT.GRANT.DETAIL.TITLE' | translate }}"
description="{{ 'PROJECT.GRANT.DETAIL.DESC' | translate }}">
<cnsl-detail-layout [hasBackButton]="true" title="{{ 'PROJECT.GRANT.DETAIL.TITLE' | translate }}">
<p class="subinfo" sub>
<span class="cnsl-secondary-text">{{ 'PROJECT.GRANT.DETAIL.DESC' | translate }}</span>
<a mat-icon-button href="https://docs.zitadel.ch/docs/concepts/structure/projects#granted-organizations"
target="_blank">
<i class="las la-info-circle"></i>
</a>
</p>
<div actions>
<button class="actions-trigger-desk" mat-raised-button color="primary" [matMenuTriggerFor]="actions">
<span>{{'ACTIONS.ACTIONS' | translate}}</span>
<mat-icon class="icon">keyboard_arrow_down</mat-icon>
</button>
<button class="actions-trigger-mob" matTooltip="{{'ACTIONS.ACTIONS' | translate}}" mat-icon-button
[matMenuTriggerFor]="actions">
<i class="las la-ellipsis-v"></i>
</button>
</div>
<mat-menu #actions="matMenu" xPosition="before">
<button mat-menu-item *ngIf="grant?.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE"
(click)="changeState(ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE)">{{'USER.PAGES.DEACTIVATE' |
translate}}</button>
<button mat-menu-item *ngIf="grant?.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE"
(click)="changeState(ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE)">{{'USER.PAGES.REACTIVATE' |
translate}}</button>
</mat-menu>
<div class="master-row">
<div class="left-col">
<div class="row">
<span class="first">{{'PROJECT.GRANT.DETAIL.PROJECTNAME' | translate}}</span>
<span class="fill-space"></span>
<span>{{grant?.projectName}}</span>
</div>
<div class="row">
<span class="first">{{'PROJECT.GRANT.DETAIL.RESOURCEOWNER' | translate}}</span>
<span class="fill-space"></span>
<span>{{grant?.projectOwnerName}}</span>
</div>
<div class="row">
<span class="first">{{'PROJECT.GRANT.DETAIL.GRANTEDORG' | translate}}</span>
<span class="fill-space"></span>
<span>{{grant?.grantedOrgName}}</span>
</div>
</div>
<span class="fill-space"></span>
<div>
<button mat-stroked-button color="warn" *ngIf="grant?.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE"
(click)="changeState(ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE)">{{'USER.PAGES.DEACTIVATE' |
translate}}</button>
<button mat-stroked-button color="warn" *ngIf="grant?.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE"
(click)="changeState(ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE)">{{'USER.PAGES.REACTIVATE' |
translate}}</button>
</div>
</div>
<cnsl-form-field class="formfield" appearance="outline" *ngIf="grant && grant.grantedRoleKeysList">
<cnsl-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</cnsl-label>
<mat-select [(ngModel)]="grant.grantedRoleKeysList" multiple (selectionChange)="updateRoles($event)">
<mat-option *ngFor="let role of projectRoleOptions" [value]="role.key">
{{role.key}}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-project-grant-illustration *ngIf="grant" [grantedProject]="grant" [projectRoleOptions]="projectRoleOptions"
(roleRemoved)="removeRole($event)" (editRoleClicked)="editRoles()">
</cnsl-project-grant-illustration>
<div class="divider"></div>
<h2 class="project-grant-h2">{{ 'PROJECT.GRANT.DETAIL.MEMBERTITLE' | translate }}</h2>
<p class="desc max-width-description">{{ 'PROJECT.GRANT.DETAIL.MEMBERDESC' | translate }}</p>
<h1 class="h1">{{ 'PROJECT.GRANT.DETAIL.MEMBERTITLE' | translate }}</h1>
<p class="desc">{{ 'PROJECT.GRANT.DETAIL.MEMBERDESC' | translate }}</p>
<cnsl-members-table *ngIf="grant" style="width: 100%;" [dataSource]="dataSource"
<cnsl-members-table *ngIf="grant" [dataSource]="dataSource"
[canWrite]="['project.grant.member.write','project.grant.member.write:' + grant.grantId] | hasRole | async"
[canDelete]="['project.grant.member.delete','project.grant.member.delete:' + grant.grantId] | hasRole | async"
[memberRoleOptions]="memberRoleOptions" (updateRoles)="updateMemberRoles($event.member, $event.change)"
[factoryLoadFunc]="changePageFactory" (changedSelection)="selection = $event" [refreshTrigger]="changePage">
<button selectactions (click)="removeProjectMemberSelection()"
(deleteMember)="removeProjectMember($event)" [factoryLoadFunc]="changePageFactory"
(changedSelection)="selection = $event" [refreshTrigger]="changePage">
<button class="cnsl-action-button" selectactions (click)="removeProjectMemberSelection()"
[disabled]="(['project.grant.member.delete','project.grant.member.delete:' + grant.grantId] | hasRole | async) === false"
matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}" class="del-button" color="warn" mat-raised-button>
matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}" color="warn" mat-raised-button>
<i class="las la-trash"></i>
{{'ACTIONS.SELECTIONDELETE' | translate}}
<span>{{'ACTIONS.SELECTIONDELETE' | translate}}</span>
<cnsl-action-keys [type]="ActionKeysType.DELETE" (actionTriggered)="removeProjectMemberSelection()">
</cnsl-action-keys>
</button>
<a writeactions color="primary"
<button class="cnsl-action-button" writeactions color="primary"
[disabled]="(['project.grant.member.write','project.grant.member.write:' + grant.grantId] | hasRole | async) === false"
(click)="openAddMember()" color="primary" mat-raised-button>
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a>
<cnsl-action-keys (actionTriggered)="openAddMember()">
</cnsl-action-keys>
</button>
</cnsl-members-table>
</cnsl-detail-layout>
@@ -1,16 +1,44 @@
.desc {
.subinfo {
display: flex;
align-items: center;
font-size: 14px;
color: var(--grey);
margin: -1.5rem 0 0 0;
i {
font-size: 1.2rem;
height: 1.2rem;
line-height: 1.2rem;
}
}
.del-button {
margin-right: .5rem;
.actions-trigger-desk {
margin-top: 0.25rem;
display: none;
align-items: center;
@media only screen and (min-width: 700px) {
display: flex;
}
.icon {
margin-left: 0.25rem;
margin-right: -0.5rem;
}
}
.actions-trigger-mob {
display: inline;
@media only screen and (min-width: 700px) {
display: none;
}
}
.master-row {
display: flex;
flex-wrap: wrap;
width: 100%;
margin-top: 1rem;
.left-col {
flex: 1;
@@ -22,14 +50,10 @@
max-width: 400px;
display: flex;
align-items: center;
padding: .5rem 0;
padding: 0.5rem 0;
span {
font-size: .9rem;
}
.first {
color: var(--grey);
font-size: 0.9rem;
}
}
}
@@ -39,11 +63,12 @@
}
}
.formfield {
.grant-detail-roles {
display: block;
width: 100%;
max-width: 400px;
}
.divider {
height: 1px;
background-color: #8795a140;
.project-grant-h2 {
margin-top: 1rem;
}
@@ -1,10 +1,12 @@
import { Component, EventEmitter } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { PageEvent } from '@angular/material/paginator';
import { MatSelectChange } from '@angular/material/select';
import { ActivatedRoute } from '@angular/router';
import { ActionKeysType } from 'src/app/modules/action-keys/action-keys.component';
import { UserGrantRoleDialogComponent } from 'src/app/modules/user-grant-role-dialog/user-grant-role-dialog.component';
import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { GrantedProject, ProjectGrantState, Role } from 'src/app/proto/generated/zitadel/project_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -38,13 +40,17 @@ export class ProjectGrantDetailComponent {
public changePage: EventEmitter<void> = new EventEmitter();
public selection: Array<Member.AsObject> = [];
public dataSource!: ProjectGrantMembersDataSource;
public ActionKeysType: any = ActionKeysType;
constructor(
private mgmtService: ManagementService,
private route: ActivatedRoute,
private toast: ToastService,
private dialog: MatDialog,
private breadcrumbService: BreadcrumbService,
) {
this.route.params.subscribe(params => {
this.route.params.subscribe((params) => {
this.projectid = params.projectid;
this.grantid = params.grantid;
@@ -66,6 +72,25 @@ export class ProjectGrantDetailComponent {
this.mgmtService.getProjectGrantByID(this.grantid, this.projectid).then((resp) => {
if (resp.projectGrant) {
this.grant = resp.projectGrant;
const breadcrumbs = [
new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'IAM',
routerLink: ['/system'],
}),
new Breadcrumb({
type: BreadcrumbType.ORG,
routerLink: ['/org'],
}),
new Breadcrumb({
type: BreadcrumbType.PROJECT,
name: '',
param: { key: 'projectid', value: resp.projectGrant.projectId },
routerLink: ['/projects', resp.projectGrant.projectId],
}),
];
this.breadcrumbService.setBreadcrumb(breadcrumbs);
}
});
});
@@ -73,61 +98,90 @@ export class ProjectGrantDetailComponent {
public changeState(newState: ProjectGrantState): void {
if (newState === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE) {
this.mgmtService.reactivateProjectGrant(this.grantid, this.projectid).then(() => {
this.toast.showInfo('PROJECT.TOAST.REACTIVATED', true);
this.grant.state = newState;
}).catch(error => {
this.toast.showError(error);
});
this.mgmtService
.reactivateProjectGrant(this.grantid, this.projectid)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.REACTIVATED', true);
this.grant.state = newState;
})
.catch((error) => {
this.toast.showError(error);
});
} else if (newState === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE) {
this.mgmtService.deactivateProjectGrant(this.grantid, this.projectid).then(() => {
this.toast.showInfo('PROJECT.TOAST.DEACTIVATED', true);
this.grant.state = newState;
}).catch(error => {
this.toast.showError(error);
});
this.mgmtService
.deactivateProjectGrant(this.grantid, this.projectid)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.DEACTIVATED', true);
this.grant.state = newState;
})
.catch((error) => {
this.toast.showError(error);
});
}
}
public getRoleOptions(projectId: string): void {
this.mgmtService.listProjectRoles(projectId, 100, 0).then(resp => {
this.mgmtService.listProjectRoles(projectId, 100, 0).then((resp) => {
this.projectRoleOptions = resp.resultList;
});
}
public getMemberRoleOptions(): void {
this.mgmtService.listProjectGrantMemberRoles().then(resp => {
this.memberRoleOptions = resp.resultList;
}).catch(error => {
this.toast.showError(error);
});
this.mgmtService
.listProjectGrantMemberRoles()
.then((resp) => {
this.memberRoleOptions = resp.resultList;
})
.catch((error) => {
this.toast.showError(error);
});
}
updateRoles(selectionChange: MatSelectChange): void {
this.mgmtService.updateProjectGrant(this.grant.grantId, this.grant.projectId, selectionChange.value)
updateRoles(selectionChange: string[]): void {
this.mgmtService
.updateProjectGrant(this.grant.grantId, this.grant.projectId, selectionChange)
.then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTUPDATED', true);
}).catch(error => {
})
.catch((error) => {
this.toast.showError(error);
});
}
public removeProjectMemberSelection(): void {
Promise.all(this.selection.map(member => {
return this.mgmtService.removeProjectGrantMember(this.grant.projectId, this.grant.grantId, member.userId)
.then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTMEMBERREMOVED', true);
setTimeout(() => {
this.changePage.emit();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}));
Promise.all(
this.selection.map((member) => {
return this.mgmtService
.removeProjectGrantMember(this.grant.projectId, this.grant.grantId, member.userId)
.then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTMEMBERREMOVED', true);
setTimeout(() => {
this.changePage.emit();
}, 1000);
})
.catch((error) => {
this.toast.showError(error);
});
}),
);
}
public removeProjectMember(member: Member.AsObject): void {
this.mgmtService
.removeProjectGrantMember(this.grant.projectId, this.grant.grantId, member.userId)
.then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTMEMBERREMOVED', true);
setTimeout(() => {
this.changePage.emit();
}, 1000);
})
.catch((error) => {
this.toast.showError(error);
});
}
public async openAddMember(): Promise<any> {
const keysList = (await this.mgmtService.listProjectGrantMemberRoles());
const keysList = await this.mgmtService.listProjectGrantMemberRoles();
const dialogRef = this.dialog.open(ProjectGrantMembersCreateDialogComponent, {
data: {
@@ -138,35 +192,78 @@ export class ProjectGrantDetailComponent {
dialogRef.afterClosed().subscribe((dataToAdd: ProjectGrantMembersCreateDialogExportType) => {
if (dataToAdd) {
Promise.all(dataToAdd.userIds.map((userid: string) => {
return this.mgmtService.addProjectGrantMember(
this.grant.projectId,
this.grant.grantId,
userid,
dataToAdd.rolesKeyList,
);
})).then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTMEMBERADDED', true);
setTimeout(() => {
this.changePage.emit();
}, 3000);
}).catch(error => {
this.toast.showError(error);
});
Promise.all(
dataToAdd.userIds.map((userid: string) => {
return this.mgmtService.addProjectGrantMember(
this.grant.projectId,
this.grant.grantId,
userid,
dataToAdd.rolesKeyList,
);
}),
)
.then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTMEMBERADDED', true);
setTimeout(() => {
this.changePage.emit();
}, 3000);
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
updateMemberRoles(member: Member.AsObject, selectionChange: MatSelectChange): void {
this.mgmtService.updateProjectGrantMember(
this.grant.projectId,
this.grant.grantId,
member.userId,
selectionChange.value,
).then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTMEMBERCHANGED', true);
}).catch(error => {
this.toast.showError(error);
updateMemberRoles(member: Member.AsObject, selectionChange: string[]): void {
this.mgmtService
.updateProjectGrantMember(this.grant.projectId, this.grant.grantId, member.userId, selectionChange)
.then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTMEMBERCHANGED', true);
})
.catch((error) => {
this.toast.showError(error);
});
}
removeRole(role: string): void {
const index = this.grant.grantedRoleKeysList.findIndex((r) => r === role);
if (index > -1) {
this.grant.grantedRoleKeysList.splice(index, 1);
this.mgmtService
.updateProjectGrant(this.grant.grantId, this.grant.projectId, this.grant.grantedRoleKeysList)
.then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTUPDATED', true);
})
.catch((error) => {
this.toast.showError(error);
});
}
}
public editRoles(): void {
const dialogRef = this.dialog.open(UserGrantRoleDialogComponent, {
data: {
projectId: this.grant.projectId,
selectedRoleKeysList: this.grant.grantedRoleKeysList,
i18nTitle: 'PROJECT.GRANT.EDITTITLE',
},
width: '600px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp && resp.roles) {
this.mgmtService
.updateProjectGrant(this.grant.grantId, this.grant.projectId, resp.roles)
.then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTUPDATED', true);
this.grant.grantedRoleKeysList = resp.roles;
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
}
@@ -7,49 +7,56 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { ActionKeysModule } from 'src/app/modules/action-keys/action-keys.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { MembersTableModule } from 'src/app/modules/members-table/members-table.module';
import { ProjectRoleChipModule } from 'src/app/modules/project-role-chip/project-role-chip.module';
import { UserGrantRoleDialogModule } from 'src/app/modules/user-grant-role-dialog/user-grant-role-dialog.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { ProjectGrantDetailRoutingModule } from './project-grant-detail-routing.module';
import { ProjectGrantDetailComponent } from './project-grant-detail.component';
import { ProjectGrantIllustrationComponent } from './project-grant-illustration/project-grant-illustration.component';
import {
ProjectGrantMembersCreateDialogModule,
ProjectGrantMembersCreateDialogModule,
} from './project-grant-members-create-dialog/project-grant-members-create-dialog.module';
@NgModule({
declarations: [ProjectGrantDetailComponent],
imports: [
CommonModule,
ProjectGrantDetailRoutingModule,
ProjectGrantMembersCreateDialogModule,
MatAutocompleteModule,
HasRoleModule,
MatChipsModule,
MatButtonModule,
MatCheckboxModule,
MatIconModule,
MatTableModule,
MatPaginatorModule,
InputModule,
MatTooltipModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
FormsModule,
TranslateModule,
MatSelectModule,
DetailLayoutModule,
HasRolePipeModule,
MembersTableModule,
MatDialogModule,
],
declarations: [ProjectGrantDetailComponent, ProjectGrantIllustrationComponent],
imports: [
CommonModule,
ProjectGrantDetailRoutingModule,
ProjectGrantMembersCreateDialogModule,
MatAutocompleteModule,
HasRoleModule,
MatChipsModule,
MatButtonModule,
MatCheckboxModule,
MatMenuModule,
UserGrantRoleDialogModule,
MatIconModule,
MatTableModule,
InputModule,
MatTooltipModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
ActionKeysModule,
ProjectRoleChipModule,
FormsModule,
TranslateModule,
MatSelectModule,
DetailLayoutModule,
HasRolePipeModule,
MembersTableModule,
MatDialogModule,
],
})
export class ProjectGrantDetailModule { }
export class ProjectGrantDetailModule {}
@@ -0,0 +1,47 @@
<div class="pgi" *ngIf="grantedProject">
<div class="pgi-org-wrapper first-org">
<span class="pgi-label">{{grantedProject.projectOwnerName}}</span>
<div class="pgi-project-wrapper fill-space">
<span class="pgi-label">{{grantedProject.projectName}}</span>
<div class="pgi-role-wrapper fill-space">
<div class="pgi-role-row">
<cnsl-project-role-chip [roleName]="role.key" *ngFor="let role of projectRoleOptions">{{role.key}}
</cnsl-project-role-chip>
</div>
<span class="fill-space"></span>
<span class="pgi-label-sec cnsl-secondary-text">roles</span>
</div>
<span class="pgi-label-sec cnsl-secondary-text">project</span>
</div>
<span class="pgi-label-sec cnsl-secondary-text">org</span>
</div>
<i class="pgi-arrow las la-long-arrow-alt-right"></i>
<div class="pgi-org-wrapper">
<span class="pgi-label italic">{{grantedProject.grantedOrgName}}</span>
<div class="pgi-project-wrapper fill-space">
<div class="pgi-label-wrapper">
<span class="pgi-label">{{grantedProject.projectName}}</span>
<div class="pgi-state-dot" *ngIf="grantedProject && grantedProject.state !== undefined"
matTooltip="{{'PROJECT.STATE.'+grantedProject.state | translate}}"
[ngClass]="{'active': grantedProject.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE, 'inactive': grantedProject.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE }">
</div>
</div>
<div class="pgi-role-wrapper fill-space">
<div class="pgi-role-row">
<cnsl-project-role-chip [showRemove]="true" (removed)="removeRole(role)" [roleName]="role"
*ngFor="let role of grantedProject.grantedRoleKeysList">{{role}}
</cnsl-project-role-chip>
</div>
<button matTooltip="{{'ACTIONS.EDIT' | translate}}" mat-icon-button (click)="editRoleClicked.emit()">
<i class="las la-pen"></i>
</button>
<span class="fill-space"></span>
<span class="pgi-label-sec cnsl-secondary-text">granted roles</span>
</div>
<span class="pgi-label-sec cnsl-secondary-text">granted project</span>
</div>
<span class="pgi-label-sec cnsl-secondary-text">granted org</span>
</div>
</div>
@@ -0,0 +1,120 @@
@mixin project-grant-illustration-theme($theme) {
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$overlayColor: if($is-dark-theme, #ffffff10, #00000008);
$highlightedOverlayColor: if($is-dark-theme, #68c4cf14, #68c4cf14);
$borderColor: if($is-dark-theme, #ffffff20, #00000020);
.pgi {
display: flex;
flex-direction: row;
justify-content: center;
padding: 2rem 0;
.pgi-arrow {
display: none;
align-self: center;
font-size: 3rem;
margin: 1rem;
}
.fill-space {
flex: 1;
}
.pgi-label {
font-size: 14px;
margin-bottom: 1rem;
display: block;
align-self: flex-start;
&.italic {
font-style: italic;
}
}
.pgi-label-wrapper {
display: flex;
align-items: center;
margin-bottom: 1rem;
align-self: flex-start;
.pgi-label {
margin: 0;
}
.pgi-state-dot {
height: 8px;
width: 8px;
border-radius: 50%;
margin-left: 0.5rem;
flex-shrink: 0;
&.active {
background-color: var(--success);
}
&.inactive {
background-color: var(--warn);
}
}
}
.pgi-label-sec {
font-size: 11px;
text-transform: uppercase;
margin: 1rem 0 0 0;
text-align: center;
display: block;
}
.pgi-org-wrapper {
border-radius: 1rem;
padding: 1rem;
background: $overlayColor;
border: 1px solid $borderColor;
display: flex;
flex-direction: column;
align-items: center;
.pgi-project-wrapper {
border-radius: 1rem;
background: $highlightedOverlayColor;
border: 1px solid $borderColor;
padding: 1rem;
display: flex;
flex-direction: column;
align-items: center;
.pgi-role-wrapper {
border-radius: 1rem;
padding: 1rem;
background: $overlayColor;
border: 1px solid $borderColor;
display: flex;
flex-direction: column;
align-items: center;
.pgi-role-row {
display: flex;
align-items: center;
flex-direction: row;
flex-wrap: wrap;
max-width: 250px;
}
}
}
}
.first-org {
display: none;
}
@media only screen and (min-width: 600px) {
.pgi-arrow,
.first-org {
display: block;
}
}
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProjectGrantIllustrationComponent } from './project-grant-illustration.component';
describe('ProjectGrantIllustrationComponent', () => {
let component: ProjectGrantIllustrationComponent;
let fixture: ComponentFixture<ProjectGrantIllustrationComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ProjectGrantIllustrationComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ProjectGrantIllustrationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,21 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { GrantedProject, ProjectGrantState, Role } from 'src/app/proto/generated/zitadel/project_pb';
@Component({
selector: 'cnsl-project-grant-illustration',
templateUrl: './project-grant-illustration.component.html',
styleUrls: ['./project-grant-illustration.component.scss'],
})
export class ProjectGrantIllustrationComponent {
@Input() public grantedProject!: GrantedProject.AsObject;
@Input() public projectRoleOptions: Role.AsObject[] = [];
@Output() public roleRemoved: EventEmitter<string> = new EventEmitter();
@Output() public editRoleClicked: EventEmitter<void> = new EventEmitter();
ProjectGrantState: any = ProjectGrantState;
constructor() {}
public removeRole(roleKey: string): void {
this.roleRemoved.emit(roleKey);
}
}
@@ -1,27 +1,27 @@
<h1 mat-dialog-title>
<span class="title">{{'MEMBER.ADD' | translate}}</span>
<span class="title">{{'MEMBER.ADD' | translate}}</span>
</h1>
<p class="desc"> {{'ORG_DETAIL.MEMBER.ADDDESCRIPTION' | translate}}</p>
<div mat-dialog-content>
<cnsl-search-user-autocomplete (selectionChanged)="selectUsers($event)">
</cnsl-search-user-autocomplete>
<cnsl-search-user-autocomplete (selectionChanged)="selectUsers($event)">
</cnsl-search-user-autocomplete>
<cnsl-form-field class="full-width" appearance="outline">
<cnsl-label>{{ 'PROJECT.MEMBER.ROLES' | translate }}</cnsl-label>
<mat-select [(ngModel)]="roleKeyList" multiple>
<mat-option *ngFor="let key of data.roleKeysList" [value]="key">
{{ key }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="full-width" appearance="outline">
<cnsl-label>{{ 'PROJECT.MEMBER.ROLES' | translate }}</cnsl-label>
<mat-select [(ngModel)]="roleKeyList" multiple>
<mat-option *ngFor="let key of data.roleKeysList" [value]="key">
{{ key }}
</mat-option>
</mat-select>
</cnsl-form-field>
</div>
<div mat-dialog-actions class="action">
<button mat-button (click)="closeDialog()">
cancel
</button>
<button mat-stroked-button (click)="closeDialog()">
{{'ACTIONS.CANCEL' | translate}}
</button>
<button [disabled]="userIds.length === 0 || roleKeyList.length === 0" color="primary" mat-raised-button
class="ok-button" (click)="closeDialogWithSuccess()">
Add
</button>
<button [disabled]="userIds.length === 0 || roleKeyList.length === 0" color="primary" mat-raised-button
class="ok-button" (click)="closeDialogWithSuccess()">
{{'ACTIONS.ADD' | translate}}
</button>
</div>
@@ -1,9 +1,9 @@
import { DataSource } from '@angular/cdk/collections';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators';
import { BehaviorSubject, Observable } from 'rxjs';
import { GrantedProject } from 'src/app/proto/generated/zitadel/project_pb';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
/**
* Data source for the ProjectMembers view. This class should
@@ -17,7 +17,7 @@ export class ProjectGrantsDataSource extends DataSource<GrantedProject.AsObject>
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
constructor(private mgmtService: ManagementService) {
constructor(private mgmtService: ManagementService, private toast: ToastService) {
super();
}
@@ -25,25 +25,27 @@ export class ProjectGrantsDataSource extends DataSource<GrantedProject.AsObject>
const offset = pageIndex * pageSize;
this.loadingSubject.next(true);
from(this.mgmtService.listProjectGrants(projectId, pageSize, offset)).pipe(
map(resp => {
this.mgmtService
.listProjectGrants(projectId, pageSize, offset)
.then((resp) => {
if (resp.details?.totalResult) {
this.totalResult = resp.details.totalResult;
} else {
this.totalResult = 0;
}
if (resp.details?.viewTimestamp) {
this.viewTimestamp = resp.details?.viewTimestamp;
}
return resp.resultList;
}),
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)),
).subscribe(grants => {
this.grantsSubject.next(grants);
});
}
this.grantsSubject.next(resp.resultList);
this.loadingSubject.next(false);
})
.catch((error) => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
}
/**
* Connect this data source to the table. The table will only update when
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProjectGrantsComponent } from './project-grants.component';
const routes: Routes = [
{
path: '',
component: ProjectGrantsComponent,
data: { animation: 'HomePage' },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ProjectGrantsRoutingModule {}
@@ -0,0 +1,120 @@
<div class="max-width-container">
<div class="project-grants-wrapper">
<h1>{{'PROJECT.GRANT.TITLE' | translate}}</h1>
<p class="desc cnsl-secondary-text">{{'PROJECT.GRANT.DESCRIPTION' | translate}}</p>
<div class="project-grants">
<cnsl-refresh-table [loading]="dataSource?.loading$ | async" *ngIf="projectId" (refreshed)="loadGrantsPage()"
[dataSize]="dataSource.totalResult" [selection]="selection" [timestamp]="dataSource?.viewTimestamp"
(refreshed)="getRoleOptions(projectId)">
<div actions>
<a [disabled]="((['project.grant.write$', 'project.grant.write:'+ projectId]| hasRole | async)) === false"
color="primary" class="cnsl-action-button" mat-raised-button
[routerLink]="[ '/projects', projectId, 'projectgrants', 'create']">
<mat-icon class="icon">add</mat-icon>
<span>{{ 'ACTIONS.NEW' | translate }}</span>
<cnsl-action-keys (actionTriggered)="gotoRouterLink([ '/projects', projectId, 'projectgrants', 'create'])">
</cnsl-action-keys>
</a>
</div>
<div class="table-wrapper">
<table mat-table multiTemplateDataRows class="table" aria-label="Elements" [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox
[disabled]="((['project.grant.write$', 'project.grant.write:'+ projectId]| hasRole | async)) === false"
color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox
[disabled]="((['project.grant.write$', 'project.grant.write:'+ projectId]| hasRole | async)) === false"
color="primary" (click)="$event.stopPropagation()" (change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="grantedOrgName">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.GRANTEDORG' | translate }} </th>
<td [routerLink]="['/projects',grant.projectId,'projectgrants', grant.grantId]" class="pointer" mat-cell
*matCellDef="let grant">
{{grant.grantedOrgName}} </td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{'PROJECT.GRANT.CREATIONDATE' | translate}} </th>
<td class="pointer" mat-cell *matCellDef="let grant"
[routerLink]="['/projects',grant.projectId,'projectgrants', grant.grantId]">
<span>{{grant.details.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
</td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CHANGEDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant"
[routerLink]="['/projects',grant.projectId,'projectgrants', grant.grantId]">
<span>{{grant.details.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.STATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant"
[routerLink]="['/projects',grant.projectId,'projectgrants', grant.grantId]">
<span class="state"
[ngClass]="{'active': grant.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE, 'inactive': grant.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE}">
{{ 'PROJECT.GRANT.STATES.'+grant.state | translate }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="roleNamesList">
<th mat-header-cell *matHeaderCellDef class="role-row"> {{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}
</th>
<td class="pointer role-row" mat-cell *matCellDef="let grant">
<div class="flex-row">
<cnsl-project-role-chip [roleName]="role" *ngFor="let role of grant.grantedRoleKeysList">{{ role }}
</cnsl-project-role-chip>
</div>
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef class="role-table-actions"></th>
<td mat-cell *matCellDef="let grant" class="role-table-actions">
<cnsl-table-actions>
<button actions
[disabled]="(['project.role.delete', 'project.role.delete:' + projectId] | hasRole | async) === false"
mat-icon-button color="warn" matTooltip="{{'ACTIONS.DELETE' | translate}}"
(click)="deleteGrant(grant)">
<i class="las la-trash"></i>
</button>
</cnsl-table-actions>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let grant; columns: displayedColumns;">
</tr>
</table>
</div>
<div *ngIf="(dataSource.loading$ | async) === false && !dataSource?.totalResult" class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{'PROJECT.GRANT.EMPTY' | translate}}</span>
</div>
<cnsl-paginator class="paginator" #paginator [pageSize]="50" [timestamp]="dataSource?.viewTimestamp"
[pageSizeOptions]="[25, 50, 100, 250]" [length]="dataSource.totalResult"
(page)="loadGrantsPage($event.pageIndex, $event.pageSize)">
</cnsl-paginator>
</cnsl-refresh-table>
</div>
</div>
</div>
@@ -0,0 +1,45 @@
.project-grants-wrapper {
padding-top: 2rem;
h1 {
margin: 0;
}
.desc {
margin: 1rem 0 0 0;
font-size: 14px;
}
.project-grants {
padding-top: 2rem;
.role {
display: inline-block;
border-radius: 0.5rem;
border: 1px solid #ffffff30;
padding: 0.5rem 1rem;
margin-top: 0.25rem;
margin-bottom: 0.25rem;
margin-left: 0;
margin-right: 0.5rem;
cursor: pointer;
}
tr {
outline: none;
}
tr.detail-row {
height: 0;
}
.pointer {
outline: none;
cursor: pointer;
}
.role-row {
min-width: 100px;
}
}
}
@@ -0,0 +1,156 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { SelectionModel } from '@angular/cdk/collections';
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSelectChange } from '@angular/material/select';
import { MatTable } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import { tap } from 'rxjs/operators';
import { PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { GrantedProject, ProjectGrantState, Role } from 'src/app/proto/generated/zitadel/project_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { ProjectGrantsDataSource } from './project-grants-datasource';
const ROUTEPARAM = 'projectid';
@Component({
selector: 'cnsl-project-grants',
templateUrl: './project-grants.component.html',
styleUrls: ['./project-grants.component.scss'],
animations: [
trigger('detailExpand', [
state('collapsed', style({ height: '0px', minHeight: '0' })),
state('expanded', style({ height: '*' })),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
]),
],
})
export class ProjectGrantsComponent implements OnInit, AfterViewInit {
public projectId: string = '';
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
@ViewChild(MatTable) public table!: MatTable<GrantedProject.AsObject>;
public dataSource!: ProjectGrantsDataSource;
public selection: SelectionModel<GrantedProject.AsObject> = new SelectionModel<GrantedProject.AsObject>(true, []);
public memberRoleOptions: Role.AsObject[] = [];
public displayedColumns: string[] = ['grantedOrgName', 'state', 'creationDate', 'changeDate', 'roleNamesList', 'actions'];
ProjectGrantState: any = ProjectGrantState;
constructor(
private mgmtService: ManagementService,
private toast: ToastService,
private route: ActivatedRoute,
private dialog: MatDialog,
private breadcrumbService: BreadcrumbService,
private router: Router,
) {
const projectId = this.route.snapshot.paramMap.get(ROUTEPARAM);
if (projectId) {
this.projectId = projectId;
const breadcrumbs = [
new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'IAM',
routerLink: ['/system'],
}),
new Breadcrumb({
type: BreadcrumbType.ORG,
routerLink: ['/org'],
}),
new Breadcrumb({
type: BreadcrumbType.PROJECT,
name: '',
param: { key: ROUTEPARAM, value: projectId },
routerLink: ['/projects', projectId],
}),
];
this.breadcrumbService.setBreadcrumb(breadcrumbs);
}
}
public gotoRouterLink(rL: any) {
this.router.navigate(rL);
}
public ngOnInit(): void {
this.dataSource = new ProjectGrantsDataSource(this.mgmtService, this.toast);
this.dataSource.loadGrants(this.projectId, 0, 25, 'asc');
this.getRoleOptions(this.projectId);
}
public ngAfterViewInit(): void {
this.paginator.page.pipe(tap(() => this.loadGrantsPage())).subscribe();
}
public loadGrantsPage(pageIndex?: number, pageSize?: number): void {
this.dataSource.loadGrants(this.projectId, pageIndex ?? this.paginator.pageIndex, pageSize ?? this.paginator.pageSize);
}
public isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.grantsSubject.value.length;
return numSelected === numRows;
}
public masterToggle(): void {
this.isAllSelected()
? this.selection.clear()
: this.dataSource.grantsSubject.value.forEach((row) => this.selection.select(row));
}
public getRoleOptions(projectId: string): void {
this.mgmtService.listProjectRoles(projectId, 100, 0).then((resp) => {
this.memberRoleOptions = resp.resultList;
});
}
public updateRoles(grant: GrantedProject.AsObject, selectionChange: MatSelectChange): void {
this.mgmtService
.updateProjectGrant(grant.grantId, grant.projectId, selectionChange.value)
.then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTCHANGED', true);
})
.catch((error) => {
this.toast.showError(error);
});
}
public deleteGrant(grant: GrantedProject.AsObject): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'PROJECT.GRANT.DIALOG.DELETE_TITLE',
descriptionKey: 'PROJECT.GRANT.DIALOG.DELETE_DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.mgmtService
.removeProjectGrant(grant.grantId, grant.projectId)
.then(() => {
this.toast.showInfo('GRANTS.TOAST.REMOVED', true);
const data = this.dataSource.grantsSubject.getValue();
this.selection.selected.forEach((item) => {
const index = data.findIndex((i) => i.grantId === item.grantId);
if (index > -1) {
data.splice(index, 1);
this.dataSource.grantsSubject.next(data);
}
});
this.selection.clear();
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
}
@@ -0,0 +1,48 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatSelectModule } from '@angular/material/select';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { ActionKeysModule } from 'src/app/modules/action-keys/action-keys.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { PaginatorModule } from 'src/app/modules/paginator/paginator.module';
import { ProjectRoleChipModule } from 'src/app/modules/project-role-chip/project-role-chip.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { TableActionsModule } from 'src/app/modules/table-actions/table-actions.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { ProjectGrantsRoutingModule } from './project-grants-routing.module';
import { ProjectGrantsComponent } from './project-grants.component';
@NgModule({
declarations: [ProjectGrantsComponent],
imports: [
CommonModule,
FormsModule,
ProjectGrantsRoutingModule,
TimestampToDatePipeModule,
TableActionsModule,
MatTooltipModule,
MatCheckboxModule,
RefreshTableModule,
LocalizedDatePipeModule,
ProjectRoleChipModule,
MatButtonModule,
HasRolePipeModule,
MatIconModule,
InputModule,
MatTableModule,
TranslateModule,
ActionKeysModule,
MatSelectModule,
PaginatorModule,
],
})
export class ProjectGrantsModule {}
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProjectRoleCreateComponent } from './project-role-create.component';
const routes: Routes = [
{
path: '',
component: ProjectRoleCreateComponent,
data: { animation: 'AddPage' },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ProjectRoleCreateRoutingModule {}
@@ -38,7 +38,8 @@
{{'PROJECT.ROLE.ADDNEWLINE' | translate}}
</button>
<button color="primary" mat-raised-button class="continue-button" [disabled]="formArray.invalid" type="submit">
<button color="primary" mat-raised-button class="continue-button" [disabled]="formArray.invalid" type="submit"
[attr.data-e2e]="'save-button'">
{{ 'ACTIONS.SAVE' | translate }}
</button>
</form>
@@ -13,13 +13,7 @@ import { ToastService } from 'src/app/services/toast.service';
templateUrl: './project-role-create.component.html',
styleUrls: ['./project-role-create.component.scss'],
animations: [
trigger('list', [
transition(':enter', [
query('@animate',
stagger(80, animateChild()),
),
]),
]),
trigger('list', [transition(':enter', [query('@animate', stagger(80, animateChild()))])]),
trigger('animate', [
transition(':enter', [
style({ opacity: 0, transform: 'translateY(-100%)' }),
@@ -71,7 +65,7 @@ export class ProjectRoleCreateComponent implements OnInit, OnDestroy {
}
public ngOnInit(): void {
this.subscription = this.route.params.subscribe(params => this.getData(params));
this.subscription = this.route.params.subscribe((params) => this.getData(params));
}
public ngOnDestroy(): void {
@@ -84,21 +78,24 @@ export class ProjectRoleCreateComponent implements OnInit, OnDestroy {
public addRole(): void {
const rolesToAdd: BulkAddProjectRolesRequest.Role[] = this.formArray.value.map((element: any) => {
const role = new BulkAddProjectRolesRequest.Role;
const role = new BulkAddProjectRolesRequest.Role();
role.setKey(element.key);
role.setDisplayName(element.displayName);
role.setGroup(element.group);
return role;
});
this.mgmtService.bulkAddProjectRoles(this.projectId, rolesToAdd).then(() => {
this.router.navigate(['projects', this.projectId]);
}).catch(error => {
this.toast.showError(error);
});
this.mgmtService
.bulkAddProjectRoles(this.projectId, rolesToAdd)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.ROLESCREATED', true);
this.router.navigate(['projects', this.projectId, 'roles']);
})
.catch((error) => {
this.toast.showError(error);
});
}
public close(): void {
this._location.back();
}
@@ -0,0 +1,30 @@
<h1 mat-dialog-title class="title">
<span>{{'PROJECT.ROLE.EDITTITLE' | translate}}</span>
</h1>
<p class="desc cnsl-secondary-text"> {{'PROJECT.ROLE.EDITDESCRIPTION' | translate}}</p>
<div mat-dialog-content>
<form *ngIf="formGroup" class="role-detail-full-width" [formGroup]="formGroup" (ngSubmit)="submitForm()">
<cnsl-form-field>
<cnsl-label>{{ 'PROJECT.ROLE.KEY' | translate }}</cnsl-label>
<input cnslInput formControlName="key" />
</cnsl-form-field>
<cnsl-form-field>
<cnsl-label>{{ 'PROJECT.ROLE.DISPLAY_NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="displayName" />
</cnsl-form-field>
<cnsl-form-field>
<cnsl-label>{{ 'PROJECT.ROLE.GROUP' | translate }}</cnsl-label>
<input cnslInput formControlName="group" />
</cnsl-form-field>
</form>
</div>
<div mat-dialog-actions class="action">
<button mat-stroked-button (click)="closeDialog()">
{{'ACTIONS.CANCEL' | translate}}
</button>
<button [disabled]="formGroup.invalid" color="primary" mat-raised-button class="ok-button" (click)="submitForm()">
{{'ACTIONS.SAVE' | translate}}
</button>
</div>
@@ -0,0 +1,16 @@
.title {
font-size: 1.2rem;
}
.desc {
font-size: 0.9rem;
}
.action {
display: flex;
justify-content: flex-end;
.ok-button {
margin-left: 0.5rem;
}
}
@@ -1,20 +1,20 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { OwnedProjectGridComponent } from './owned-project-grid.component';
import { ProjectRoleDetailComponent } from './project-role-detail.component';
describe('GridComponent', () => {
let component: OwnedProjectGridComponent;
let fixture: ComponentFixture<OwnedProjectGridComponent>;
describe('ProjectRoleDetailComponent', () => {
let component: ProjectRoleDetailComponent;
let fixture: ComponentFixture<ProjectRoleDetailComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [OwnedProjectGridComponent],
declarations: [ProjectRoleDetailComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OwnedProjectGridComponent);
fixture = TestBed.createComponent(ProjectRoleDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -0,0 +1,55 @@
import { Component, Inject } from '@angular/core';
import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'cnsl-project-role-detail',
templateUrl: './project-role-detail.component.html',
styleUrls: ['./project-role-detail.component.scss'],
})
export class ProjectRoleDetailComponent {
public projectId: string = '';
public formGroup!: FormGroup;
constructor(private mgmtService: ManagementService, private toast: ToastService,
public dialogRef: MatDialogRef<ProjectRoleDetailComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
this.projectId = data.projectId;
this.formGroup = new FormGroup({
key: new FormControl({ value: '', disabled: true }, [Validators.required]),
displayName: new FormControl(''),
group: new FormControl(''),
});
this.formGroup.patchValue(data.role);
}
submitForm(): void {
if (this.formGroup.valid && this.key?.value && this.group?.value && this.displayName?.value) {
this.mgmtService.updateProjectRole(this.projectId, this.key.value, this.displayName.value, this.group.value)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.ROLECHANGED', true);
this.dialogRef.close(true);
}).catch(error => {
this.toast.showError(error);
});
}
}
public closeDialog(): void {
this.dialogRef.close(false);
}
public get key(): AbstractControl | null {
return this.formGroup.get('key');
}
public get displayName(): AbstractControl | null {
return this.formGroup.get('displayName');
}
public get group(): AbstractControl | null {
return this.formGroup.get('group');
}
}
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProjectRolesComponent } from './project-roles.component';
const routes: Routes = [
{
path: '',
component: ProjectRolesComponent,
data: { animation: 'HomePage' },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ProjectRolesRoutingModule {}
@@ -0,0 +1,11 @@
<div class="max-width-container">
<div class="project-roles-wrapper">
<h1>{{'PROJECT.ROLE.TITLE' | translate}}</h1>
<p class="desc cnsl-secondary-text">{{'PROJECT.ROLE.DESCRIPTION' | translate}}</p>
<cnsl-project-roles-table class="project-roles"
[disabled]="(['project.role.write$', 'project.role.write:'+ projectId]| hasRole | async) === false"
[actionsVisible]="true" [projectId]="projectId">
</cnsl-project-roles-table>
</div>
</div>
@@ -0,0 +1,16 @@
.project-roles-wrapper {
padding-top: 2rem;
h1 {
margin: 0;
}
.desc {
margin: 1rem 0 0 0;
font-size: 14px;
}
.project-roles {
padding-top: 2rem;
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProjectRolesComponent } from './project-roles.component';
describe('ProjectRolesComponent', () => {
let component: ProjectRolesComponent;
let fixture: ComponentFixture<ProjectRolesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ProjectRolesComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ProjectRolesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,40 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
const ROUTEPARAM = 'projectid';
@Component({
selector: 'cnsl-project-roles',
templateUrl: './project-roles.component.html',
styleUrls: ['./project-roles.component.scss'],
})
export class ProjectRolesComponent {
public projectId: string = '';
constructor(private route: ActivatedRoute, private breadcrumbService: BreadcrumbService) {
const projectId = this.route.snapshot.paramMap.get(ROUTEPARAM);
if (projectId) {
this.projectId = projectId;
const breadcrumbs = [
new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'IAM',
routerLink: ['/system'],
}),
new Breadcrumb({
type: BreadcrumbType.ORG,
routerLink: ['/org'],
}),
new Breadcrumb({
type: BreadcrumbType.PROJECT,
name: '',
param: { key: ROUTEPARAM, value: projectId },
routerLink: ['/projects', projectId],
}),
];
this.breadcrumbService.setBreadcrumb(breadcrumbs);
}
}
}
@@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
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 { ProjectRoleDetailComponent } from './project-role-detail/project-role-detail.component';
import { ProjectRolesRoutingModule } from './project-roles-routing.module';
import { ProjectRolesComponent } from './project-roles.component';
@NgModule({
declarations: [ProjectRolesComponent, ProjectRoleDetailComponent],
imports: [
CommonModule,
ProjectRolesRoutingModule,
HasRoleModule,
ProjectRolesTableModule,
ReactiveFormsModule,
HasRolePipeModule,
InputModule,
TranslateModule,
MatButtonModule,
FormsModule,
],
exports: [ProjectRolesComponent],
})
export class ProjectRolesModule {}
@@ -1,26 +1,30 @@
<div class="container">
<div class="max-width-container">
<div class="enlarged-container">
<div class="abort-container">
<button (click)="close()" mat-icon-button>
<mat-icon>close</mat-icon>
</button>
<span class="abort">{{ 'PROJECT.PAGES.CREATE' | translate }}</span><span class="abort-2">Step
{{ currentCreateStep }} of
{{ createSteps }}</span>
<button (click)="close()" mat-icon-button>
<mat-icon>close</mat-icon>
</button>
<span class="abort">{{ 'PROJECT.PAGES.CREATE' | translate }}</span><span class="abort-2">Step
{{ currentCreateStep }} of
{{ createSteps }}</span>
</div>
<h1>{{'PROJECT.PAGES.CREATE_DESC' | translate}}</h1>
<form cdkFocusRegionStart (ngSubmit)="saveProject()">
<div class="project-create-content">
<h1>{{'PROJECT.PAGES.CREATE_DESC' | translate}}</h1>
<form cdkFocusRegionStart (ngSubmit)="saveProject()">
<div class="column">
<cnsl-form-field class="formfield" hintLabel="The name is required!">
<cnsl-label>{{'PROJECT.NAME' | translate}}</cnsl-label>
<input cnslInput cdkFocusInitial autofocus [(ngModel)]="project.name"
[ngModelOptions]="{ standalone: true }" />
</cnsl-form-field>
<cnsl-form-field class="formfield" hintLabel="The name is required!">
<cnsl-label>{{'PROJECT.NAME' | translate}}</cnsl-label>
<input cnslInput cdkFocusInitial autofocus [(ngModel)]="project.name"
[ngModelOptions]="{ standalone: true }" />
</cnsl-form-field>
</div>
<button color="primary" mat-raised-button class="continue-button" [disabled]="!project.name" cdkFocusInitial
type="submit">
{{'ACTIONS.CONTINUE' | translate}}
type="submit" [attr.data-e2e]="'continue-button'">
{{'ACTIONS.CONTINUE' | translate}}
</button>
</form>
</form>
</div>
</div>
</div>
@@ -2,33 +2,29 @@ h1 {
font-weight: 500;
}
.container {
padding: 4rem 4rem 2rem 4rem;
.abort-container {
display: flex;
align-items: center;
margin-bottom: 2rem;
@media only screen and (max-width: 450px) {
padding: 4rem 1rem 2rem 1rem;
.abort {
font-size: 1.2rem;
margin-left: 2rem;
}
.abort-container {
display: flex;
align-items: center;
margin-bottom: 2rem;
.abort {
font-size: 1.2rem;
margin-left: 2rem;
}
.abort-2 {
font-size: 1.2rem;
margin-left: 2rem;
white-space: nowrap;
}
.abort-2 {
font-size: 1.2rem;
margin-left: 2rem;
white-space: nowrap;
}
}
.project-create-content {
padding-left: 4.5rem;
}
.margin-right {
margin-right: .5rem;
margin-right: 0.5rem;
}
.column {
@@ -36,7 +32,8 @@ h1 {
flex-direction: column;
.formfield {
width: 400px;
max-width: 400px;
display: block;
&.autocomplete {
margin-top: 1rem;
@@ -46,7 +43,7 @@ h1 {
.slider {
margin-top: 2rem;
margin-bottom: 1rem;
font-size: .8rem;
font-size: 0.8rem;
}
.access-token {
@@ -57,6 +54,6 @@ h1 {
.continue-button {
margin-top: 3rem;
display: block;
padding: .5rem 4rem;
border-radius: .5rem;
padding: 0.5rem 4rem;
border-radius: 0.5rem;
}

Some files were not shown because too many files have changed in this diff Show More