feat(console): project privatelabelling, catch query param to set org context (#2277)

* feat: privatelabeling setting, query param for context

* lint

* Update console/src/assets/i18n/de.json

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

* Update console/src/assets/i18n/de.json

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

* Update console/src/assets/i18n/en.json

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

* Update console/src/assets/i18n/en.json

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
This commit is contained in:
Max Peintner 2021-08-31 08:25:24 +02:00 committed by GitHub
parent e8da0e3f4f
commit c884a11f1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 300 additions and 59 deletions

View File

@ -6,10 +6,10 @@ import { FormControl } from '@angular/forms';
import { MatIconRegistry } from '@angular/material/icon';
import { MatDrawer } from '@angular/material/sidenav';
import { DomSanitizer } from '@angular/platform-browser';
import { Router, RouterOutlet } from '@angular/router';
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
import { LangChangeEvent, TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, from, Observable, of, Subscription } from 'rxjs';
import { catchError, debounceTime, finalize, map, take } from 'rxjs/operators';
import { BehaviorSubject, from, Observable, of, Subject } from 'rxjs';
import { catchError, debounceTime, finalize, map, take, takeUntil } from 'rxjs/operators';
import { accountCard, adminLineAnimation, navAnimations, routeAnimations, toolbarAnimation } from './animations';
import { TextQueryMethod } from './proto/generated/zitadel/object_pb';
@ -55,8 +55,7 @@ export class AppComponent implements OnDestroy {
public showProjectSection: boolean = false;
public filterControl: FormControl = new FormControl('');
private authSub: Subscription = new Subscription();
private orgSub: Subscription = new Subscription();
private destroy$: Subject<void> = new Subject();
public labelpolicy!: LabelPolicy.AsObject;
public hideAdminWarn: boolean = true;
@ -76,6 +75,7 @@ export class AppComponent implements OnDestroy {
public domSanitizer: DomSanitizer,
private router: Router,
update: UpdateService,
private activatedRoute: ActivatedRoute,
@Inject(DOCUMENT) private document: Document,
) {
console.log('%cWait!', 'text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; color: #5469D4; font-size: 50px');
@ -174,16 +174,27 @@ export class AppComponent implements OnDestroy {
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/api.svg'),
);
this.activatedRoute.queryParams
.pipe(takeUntil(this.destroy$))
.subscribe(route => {
const { org } = route;
if (org) {
this.authService.getActiveOrg(org).then(queriedOrg => {
this.org = queriedOrg;
});
}
});
this.loadPrivateLabelling();
this.getProjectCount();
this.orgSub = this.authService.activeOrgChanged.subscribe(org => {
this.authService.activeOrgChanged.pipe(takeUntil(this.destroy$)).subscribe(org => {
this.org = org;
this.getProjectCount();
});
this.authSub = this.authenticationService.authenticationChanged.subscribe((authenticated) => {
this.authenticationService.authenticationChanged.pipe(takeUntil(this.destroy$)).subscribe((authenticated) => {
if (authenticated) {
this.authService.getActiveOrg().then(org => {
this.org = org;
@ -217,8 +228,8 @@ export class AppComponent implements OnDestroy {
}
public ngOnDestroy(): void {
this.authSub.unsubscribe();
this.orgSub.unsubscribe();
this.destroy$.next();
this.destroy$.complete();
}
public toggleAdminHide(): void {

View File

@ -40,12 +40,5 @@
<div class="divider"></div>
<ng-container
*ngIf="serviceType == PolicyComponentServiceType.MGMT && (['login_policy.idp'] | hasFeature | async) == false">
<cnsl-info-section type="WARN">{{'FEATURES.NOTAVAILABLE' | translate: ({value:
'login_policy.idp'})}}
</cnsl-info-section>
</ng-container>
<app-policy-grid [currentPolicy]="currentPolicy" [type]="serviceType" tagForFilter="text"></app-policy-grid>
</app-detail-layout>

View File

@ -0,0 +1,22 @@
<h1 class="title">{{'PROJECT.PAGES.PRIVATELABEL.DIALOG.TITLE' | translate}} {{data?.number}}</h1>
<p class="desc">{{'PROJECT.PAGES.PRIVATELABEL.DIALOG.DESCRIPTION' | translate}}</p>
<div mat-dialog-content>
<mat-radio-group
class="example-radio-group"
[(ngModel)]="setting">
<mat-radio-button class="radio-button" *ngFor="let selection of settings" [value]="selection">
<span class="label">{{'PROJECT.PAGES.PRIVATELABEL.'+selection+'.TITLE' | translate}}</span>
</mat-radio-button>
</mat-radio-group>
<cnsl-info-section class="info">{{'PROJECT.PAGES.PRIVATELABEL.'+setting+'.DESC' | translate}}</cnsl-info-section>
</div>
<div mat-dialog-actions class="action">
<button cdkFocusInitial color="primary" mat-button class="ok-button" (click)="closeDialog()">
{{'ACTIONS.CLOSE' | translate}}
</button>
<button [disabled]="setting == undefined" cdkFocusInitial color="primary" mat-raised-button class="ok-button"
(click)="closeDialog(setting)">
{{'ACTIONS.OK' | translate}}
</button>
</div>

View File

@ -0,0 +1,35 @@
.title {
font-size: 1.2rem;
margin: 0;
}
.desc {
color: var(--grey);
font-size: .9rem;
}
.radio-button {
margin: .5rem 0;
.label {
white-space: normal;
}
}
.info {
margin: 1rem 0;
display: block;
}
.action {
display: flex;
justify-content: flex-end;
.ok-button {
margin-left: .5rem;
}
button {
border-radius: .5rem;
}
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ProjectPrivateLabelingDialogComponent } from './project-private-labeling-dialog.component';
describe('ProjectPrivateLabelingDialogComponent', () => {
let component: ProjectPrivateLabelingDialogComponent;
let fixture: ComponentFixture<ProjectPrivateLabelingDialogComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ProjectPrivateLabelingDialogComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectPrivateLabelingDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,26 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { PrivateLabelingSetting } from 'src/app/proto/generated/zitadel/project_pb';
@Component({
selector: 'app-project-private-labeling-dialog',
templateUrl: './project-private-labeling-dialog.component.html',
styleUrls: ['./project-private-labeling-dialog.component.scss'],
})
export class ProjectPrivateLabelingDialogComponent {
public setting: PrivateLabelingSetting = PrivateLabelingSetting.PRIVATE_LABELING_SETTING_UNSPECIFIED;
public settings: PrivateLabelingSetting[] = [
PrivateLabelingSetting.PRIVATE_LABELING_SETTING_UNSPECIFIED,
PrivateLabelingSetting.PRIVATE_LABELING_SETTING_ENFORCE_PROJECT_RESOURCE_OWNER_POLICY,
PrivateLabelingSetting.PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY,
];
constructor(public dialogRef: MatDialogRef<ProjectPrivateLabelingDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
this.setting = data.setting;
}
closeDialog(setting?: PrivateLabelingSetting): void {
this.dialogRef.close(setting);
}
}

View File

@ -0,0 +1,24 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatRadioModule } from '@angular/material/radio';
import { TranslateModule } from '@ngx-translate/core';
import { InfoSectionModule } from '../info-section/info-section.module';
import { ProjectPrivateLabelingDialogComponent } from './project-private-labeling-dialog.component';
@NgModule({
declarations: [ProjectPrivateLabelingDialogComponent],
imports: [
CommonModule,
TranslateModule,
MatButtonModule,
FormsModule,
MatRadioModule,
InfoSectionModule,
],
})
export class ProjectPrivateLabelingDialogModule { }

View File

@ -34,7 +34,7 @@
<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.PROJECTSTATE_ACTIVE, 'inactive': project.state === ProjectState.PROJECTSTATE_INACTIVE}">{{'PROJECT.STATE.'+project.state | translate}}</span>
[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>

View File

@ -9,7 +9,7 @@ import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-m
import { ChangeType } from 'src/app/modules/changes/changes.component';
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { GrantedProject, ProjectState } from 'src/app/proto/generated/zitadel/project_pb';
import { GrantedProject, ProjectGrantState } from 'src/app/proto/generated/zitadel/project_pb';
import { User } from 'src/app/proto/generated/zitadel/user_pb';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@ -24,7 +24,7 @@ export class GrantedProjectDetailComponent implements OnInit, OnDestroy {
public grantId: string = '';
public project!: GrantedProject.AsObject;
public ProjectState: any = ProjectState;
public ProjectGrantState: any = ProjectGrantState;
public ChangeType: any = ChangeType;
private subscription?: Subscription;

View File

@ -53,6 +53,14 @@
</div>
<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 (click)="openPrivateLabelingDialog()" mat-icon-button><i class="las la-edit"></i></button>
</p>
</div>
<ng-template appHasRole [appHasRole]="['project.app.read:' + project.id, 'project.app.read']">
<app-application-grid *ngIf="grid" [disabled]="isZitadel" (changeView)="grid = false"
[projectId]="projectId"></app-application-grid>

View File

@ -1,7 +1,7 @@
.head {
display: flex;
align-items: center;
border-bottom: 1px solid #ffffff20;
border-bottom: 1px solid #81868a30;
flex-wrap: wrap;
margin-bottom: 1rem;
@ -41,6 +41,30 @@
}
}
.privatelabel-info {
display: flex;
flex-direction: column;
border-bottom: 1px solid #81868a30;
padding: .5rem 0 1rem 0;
.setting-title {
font-size: 14px;
color: var(--grey);
text-transform: uppercase;
margin: 0;
}
.setting-desc {
margin: 0;
display: flex;
align-items: center;
.icon-button {
margin-left: .5rem;
}
}
}
.line {
.formfield {
flex: 1;

View File

@ -8,12 +8,15 @@ import { BehaviorSubject, from, Observable, of, Subscription } 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';
import {
ProjectPrivateLabelingDialogComponent,
} from 'src/app/modules/project-private-labeling-dialog/project-private-labeling-dialog.component';
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { App } from 'src/app/proto/generated/zitadel/app_pb';
import { ListAppsResponse, UpdateProjectRequest } from 'src/app/proto/generated/zitadel/management_pb';
import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { Project, ProjectState } from 'src/app/proto/generated/zitadel/project_pb';
import { PrivateLabelingSetting, Project, ProjectState } from 'src/app/proto/generated/zitadel/project_pb';
import { User } from 'src/app/proto/generated/zitadel/user_pb';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@ -70,6 +73,22 @@ export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
this.subscription?.unsubscribe();
}
public openPrivateLabelingDialog(): void {
const dialogRef = this.dialog.open(ProjectPrivateLabelingDialogComponent, {
data: {
setting: this.project.privateLabelingSetting,
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp: PrivateLabelingSetting) => {
if (resp !== undefined) {
this.project.privateLabelingSetting = resp;
this.saveProject();
}
});
}
private async getData({ id }: Params): Promise<void> {
this.projectId = id;
@ -186,6 +205,7 @@ export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
req.setProjectRoleAssertion(this.project.projectRoleAssertion);
req.setProjectRoleCheck(this.project.projectRoleCheck);
req.setHasProjectCheck(this.project.hasProjectCheck);
req.setPrivateLabelingSetting(this.project.privateLabelingSetting);
this.mgmtService.updateProject(req).then(() => {
this.toast.showInfo('PROJECT.TOAST.UPDATED', true);

View File

@ -24,45 +24,49 @@ import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.mod
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 {
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,
],
imports: [
CommonModule,
OwnedProjectsRoutingModule,
UserGrantsModule,
FormsModule,
ReactiveFormsModule,
TranslateModule,
AvatarModule,
ReactiveFormsModule,
HasRoleModule,
MatTableModule,
PaginatorModule,
InputModule,
MatChipsModule,
MatIconModule,
WarnDialogModule,
MatButtonModule,
MatProgressSpinnerModule,
MatProgressBarModule,
MatCheckboxModule,
CardModule,
MatTooltipModule,
MatSortModule,
HasRolePipeModule,
TimestampToDatePipeModule,
LocalizedDatePipeModule,
SharedModule,
RefreshTableModule,
],
declarations: [
OwnedProjectsComponent,
OwnedProjectListComponent,
OwnedProjectGridComponent,
],
imports: [
CommonModule,
OwnedProjectsRoutingModule,
UserGrantsModule,
FormsModule,
ReactiveFormsModule,
TranslateModule,
AvatarModule,
ReactiveFormsModule,
HasRoleModule,
MatTableModule,
PaginatorModule,
ProjectPrivateLabelingDialogModule,
InputModule,
MatChipsModule,
MatIconModule,
WarnDialogModule,
MatButtonModule,
MatProgressSpinnerModule,
MatProgressBarModule,
MatCheckboxModule,
CardModule,
MatTooltipModule,
MatSortModule,
HasRolePipeModule,
TimestampToDatePipeModule,
LocalizedDatePipeModule,
SharedModule,
RefreshTableModule,
],
})
export class OwnedProjectsModule { }

View File

@ -140,11 +140,22 @@ export class GrpcAuthService {
public async getActiveOrg(id?: string): Promise<Org.AsObject> {
if (id) {
const org = this.storage.getItem<Org.AsObject>(StorageKey.organization);
if (org && this.cachedOrgs.find(tmp => tmp.id === org.id)) {
return org;
const find = this.cachedOrgs.find(tmp => tmp.id === id);
if (find) {
this.setActiveOrg(find);
return Promise.resolve(find);
} else {
const orgs = (await this.listMyProjectOrgs(10, 0)).resultList;
this.cachedOrgs = orgs;
const toFind = this.cachedOrgs.find(tmp => tmp.id === id);
if (toFind) {
this.setActiveOrg(toFind);
return Promise.resolve(toFind);
} else {
return Promise.reject(new Error('requested organization not found'));
}
}
return Promise.reject(new Error('no cached org'));
} else {
let orgs = this.cachedOrgs;
if (orgs.length === 0) {

View File

@ -910,6 +910,25 @@
"OWNED": "Eigene Projekte",
"GRANTED": "Berechtigte Projekte"
},
"PRIVATELABEL": {
"TITLE":"Private Labeling Verhalten",
"0": {
"TITLE":"Unspezifiziert",
"DESC":"Sobald der Nutzer identifiziert ist, wird das Privatelabeling der von ihm gewählten Organisation angezeigt, davor wird der Default des Systems angezeigt."
},
"1": {
"TITLE":"Durchsetzung der Richtlinie des Projekteigentümers",
"DESC":"Das Privatelabeling der Organisation, die Eigentümerin des Projekts ist, wird angezeigt"
},
"2": {
"TITLE":"Durchsetzung der Richtlinie des Benutzereigentümers",
"DESC":"Das Privatelabeling der Organisation des Projekts wird angezeigt, sobald der Benutzer identifiziert ist, wird jedoch auf die Einstellung der Organisation des identifizierten Benutzers, gewechselt."
},
"DIALOG":{
"TITLE":"Privatelabeling Verhalten",
"DESCRIPTION":"Bestimmen Sie das Verhalten des Projektes im Bezug auf das Privatelabeling."
}
},
"PINNED": "Angepinnt",
"ALL": "Alle",
"CREATEDON": "Erstellt am",

View File

@ -912,6 +912,25 @@
"OWNED": "Owned Projects",
"GRANTED": "Granted Projects"
},
"PRIVATELABEL": {
"TITLE":"Private Labeling Setting",
"0": {
"TITLE":"Unspecified",
"DESC":"As soon as the user is identified, the private labeling of the organisation of the identified user will be shown, before the system default is shown."
},
"1": {
"TITLE":"Enforce project resource owner Policy",
"DESC":"The private labeling of the organisation which owns the project will be shown"
},
"2": {
"TITLE":"Allow Login User resource owner policy",
"DESC":"The private labeling of the organization of the project will be shown, but as soon as the user is identified, the setting of the organization of the identified user, will be shown."
},
"DIALOG":{
"TITLE":"Privatelabeling Setting",
"DESCRIPTION":"Select the behaviour of the login, when using the project."
}
},
"PINNED": "Pinned",
"ALL": "All",
"CREATEDON": "Created on",