mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-15 10:25:11 +00:00
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:
co-authored by
Livio Amstutz
parent
00f7dbe875
commit
08ae39ae19
+4
-3
@@ -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;
|
||||
}
|
||||
|
||||
+3
-3
@@ -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 {
|
||||
|
||||
+25
-25
@@ -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>
|
||||
+4
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+35
-51
@@ -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>
|
||||
+10
-37
@@ -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);
|
||||
}
|
||||
|
||||
+54
-32
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
-70
@@ -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>
|
||||
-181
@@ -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);
|
||||
}
|
||||
-25
@@ -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();
|
||||
});
|
||||
});
|
||||
-100
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
-81
@@ -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>
|
||||
-39
@@ -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;
|
||||
}
|
||||
-25
@@ -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();
|
||||
});
|
||||
});
|
||||
-119
@@ -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);
|
||||
}
|
||||
}
|
||||
+15
-45
@@ -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 {}
|
||||
|
||||
+35
-35
@@ -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>
|
||||
+3
-6
@@ -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 {
|
||||
|
||||
+69
-45
@@ -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>
|
||||
+2
-25
@@ -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 {
|
||||
|
||||
+18
-22
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+9
-15
@@ -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 {
|
||||
|
||||
+66
-114
@@ -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>
|
||||
+35
-85
@@ -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;
|
||||
}
|
||||
|
||||
+115
-79
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+8
-10
@@ -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 {}
|
||||
|
||||
-97
@@ -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>
|
||||
-64
@@ -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;
|
||||
}
|
||||
}
|
||||
-112
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
-80
@@ -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>
|
||||
-237
@@ -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%;
|
||||
}
|
||||
-170
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
-90
@@ -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>
|
||||
-58
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
-25
@@ -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();
|
||||
});
|
||||
});
|
||||
-190
@@ -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 {}
|
||||
|
||||
+62
@@ -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>
|
||||
+13
-10
@@ -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;
|
||||
}
|
||||
}
|
||||
+21
-21
@@ -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 {}
|
||||
+48
-47
@@ -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>
|
||||
+39
-14
@@ -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;
|
||||
}
|
||||
|
||||
+157
-60
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+35
-28
@@ -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 {}
|
||||
|
||||
+47
@@ -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>
|
||||
+120
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -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();
|
||||
});
|
||||
});
|
||||
+21
@@ -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);
|
||||
}
|
||||
}
|
||||
+18
-18
@@ -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>
|
||||
+15
-13
@@ -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
|
||||
+18
@@ -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 {}
|
||||
+120
@@ -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>
|
||||
+45
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+156
@@ -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 {}
|
||||
+18
@@ -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 {}
|
||||
+2
-1
@@ -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>
|
||||
+12
-15
@@ -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();
|
||||
}
|
||||
+30
@@ -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>
|
||||
+16
@@ -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;
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -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();
|
||||
});
|
||||
+55
@@ -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');
|
||||
}
|
||||
}
|
||||
+18
@@ -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 {}
|
||||
+11
@@ -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>
|
||||
+16
@@ -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;
|
||||
}
|
||||
}
|
||||
+25
@@ -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();
|
||||
});
|
||||
});
|
||||
+40
@@ -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
Reference in New Issue
Block a user