fix: new context component (#2823)

This commit is contained in:
Max Peintner 2021-12-10 16:14:24 +01:00 committed by GitHub
parent 2f7d8ca557
commit d1cb7fdc9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 262 additions and 120 deletions

View File

@ -20,40 +20,19 @@
</svg>
</ng-container>
<button class="org-button" (click)="loadOrgs()" *ngIf="user && org" mat-button [matMenuTriggerFor]="menu"
(menuOpened)="focusFilter()">{{org?.name ? org.name : 'NO NAME'}}
<mat-icon>
arrow_drop_down</mat-icon>
</button>
<div class="org-context-wrapper" (clickOutside)="showOrgContext ? showOrgContext = false: null">
<button class="org-button dontcloseonclick" (click)="showOrgContext = !showOrgContext" *ngIf="user && org"
mat-button>{{org?.name
?
org.name : 'NO NAME'}}
<mat-icon class="dontcloseonclick">
arrow_drop_down</mat-icon>
</button>
<cnsl-org-context class="context_card" *ngIf="showOrgContext" (closedCard)="showOrgContext = false" [org]="org"
(setOrg)="setActiveOrg($event)">
</cnsl-org-context>
</div>
<mat-menu class="menu" #menu="matMenu">
<div class="spinner-w">
<mat-spinner diameter="20" *ngIf="orgLoading$ | async" color="accent">
</mat-spinner>
</div>
<div class="filter-wrapper">
<input cnslInput class="filter-input" [formControl]="filterControl" autocomplete="off"
(click)="$event.stopPropagation()" placeholder="{{'ORG.PAGES.FILTERPLACEHOLDER' | translate}}" #input>
</div>
<div class="org-wrapper">
<button [ngClass]="{'active': temporg.id === org?.id}" [disabled]="!temporg.id"
*ngFor="let temporg of orgs$ | async" mat-menu-item (click)="setActiveOrg(temporg)">
{{temporg?.name ? temporg.name : 'NO NAME'}}
</button>
</div>
<button class="show-all" mat-menu-item [routerLink]="[ '/org/overview' ]">{{'MENU.SHOWORGS' |
translate}}</button>
<ng-template cnslHasRole [hasRole]="['org.create','iam.write']">
<button mat-menu-item [routerLink]="[ '/org/create' ]">
<mat-icon class="avatar">add</mat-icon>
{{'MENU.NEWORG' | translate}}
</button>
</ng-template>
</mat-menu>
<span class="fill-space"></span>
<a class="doc-link" href="https://docs.zitadel.ch" mat-stroked-button target="_blank">{{'MENU.DOCUMENTATION'

View File

@ -13,7 +13,7 @@
.org-button {
font-weight: bold;
padding-right: .5rem;
padding-right: 0.5rem;
}
.logo {
@ -30,7 +30,7 @@
}
.context-menu {
border-radius: .5rem;
border-radius: 0.5rem;
background-color: #2d2e30;
}
@ -46,6 +46,21 @@
}
}
.org-context-wrapper {
display: flex;
justify-content: space-between;
position: relative;
user-select: none;
.context_card {
position: absolute;
top: 60px;
left: 0;
overflow: hidden;
border-radius: 0.5rem;
}
}
.icon-container {
display: flex;
justify-content: space-between;
@ -73,7 +88,7 @@
top: 60px;
right: 0;
overflow: hidden;
border-radius: .5rem;
border-radius: 0.5rem;
}
}
}
@ -117,18 +132,18 @@
text-decoration: none;
cursor: pointer;
padding: 0 1rem;
margin-right: .5rem;
margin-right: 0.5rem;
border-top-right-radius: 1.5rem;
border-bottom-right-radius: 1.5rem;
.icon {
margin: .5rem 1rem;
margin: 0.5rem 1rem;
}
.iam-i {
object-fit: contain;
max-height: 24px;
margin: .5rem 1rem;
margin: 0.5rem 1rem;
}
.label {
@ -184,7 +199,7 @@
}
.slash {
margin: 0 .5rem;
margin: 0 0.5rem;
color: var(--grey);
}
}
@ -207,7 +222,7 @@
.theme-section {
display: block;
padding: 0 .5rem;
padding: 0 0.5rem;
margin-top: 2rem;
align-self: flex-start;
border-radius: 1rem;
@ -217,7 +232,7 @@
border-radius: 50%;
height: 30px;
width: 30px;
margin: .5rem;
margin: 0.5rem;
cursor: pointer;
background: linear-gradient(315deg, #e6e6e6, #fff);
}
@ -227,7 +242,7 @@
border-radius: 50%;
height: 30px;
width: 30px;
margin: .5rem;
margin: 0.5rem;
cursor: pointer;
background: linear-gradient(315deg, #000, #000);
}
@ -252,7 +267,7 @@
display: block;
background-color: #81868a40;
height: 1px;
margin: .5rem 0;
margin: 0.5rem 0;
flex: 1;
min-width: 10px;
}
@ -266,7 +281,7 @@
@mixin textvar($theme) {
.filter-form {
margin: 0 .5rem;
margin: 0 0.5rem;
/* stylelint-disable */
$foreground: map-get($theme, foreground);
color: mat.get-color-from-palette($foreground, text) !important;
@ -276,30 +291,7 @@
$primary: map-get($theme, primary);
color: mat.get-color-from-palette($primary, 300) !important;
border-bottom: 1px solid var(--grey);
margin-bottom: .5rem;
margin-bottom: 0.5rem;
}
/* stylelint-enable */
}
.menu {
position: relative;
.filter-wrapper {
padding: 4px;
}
.spinner-w {
top: 1rem;
left: 0;
right: 0;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
}
}
.org-wrapper {
max-height: 350px;
overflow-y: auto;
}

View File

@ -1,19 +1,17 @@
import { BreakpointObserver } from '@angular/cdk/layout';
import { OverlayContainer } from '@angular/cdk/overlay';
import { DOCUMENT, ViewportScroller } from '@angular/common';
import { Component, ElementRef, HostBinding, Inject, OnDestroy, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Component, HostBinding, Inject, OnDestroy, ViewChild } from '@angular/core';
import { MatIconRegistry } from '@angular/material/icon';
import { MatDrawer } from '@angular/material/sidenav';
import { DomSanitizer } from '@angular/platform-browser';
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
import { LangChangeEvent, TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, from, Observable, of, Subject } from 'rxjs';
import { catchError, debounceTime, finalize, map, take, takeUntil } from 'rxjs/operators';
import { Observable, of, Subject } from 'rxjs';
import { map, take, takeUntil } from 'rxjs/operators';
import { accountCard, adminLineAnimation, navAnimations, routeAnimations, toolbarAnimation } from './animations';
import { TextQueryMethod } from './proto/generated/zitadel/object_pb';
import { Org, OrgNameQuery, OrgQuery } from './proto/generated/zitadel/org_pb';
import { Org } from './proto/generated/zitadel/org_pb';
import { LabelPolicy, PrivacyPolicy } from './proto/generated/zitadel/policy_pb';
import { AuthenticationService } from './services/authentication.service';
import { GrpcAuthService } from './services/grpc-auth.service';
@ -29,7 +27,6 @@ import { UpdateService } from './services/update.service';
})
export class AppComponent implements OnDestroy {
@ViewChild('drawer') public drawer!: MatDrawer;
@ViewChild('input', { static: false }) input!: ElementRef;
public isHandset$: Observable<boolean> = this.breakpointObserver.observe('(max-width: 599px)').pipe(
map((result) => {
return result.matches;
@ -38,16 +35,13 @@ export class AppComponent implements OnDestroy {
@HostBinding('class') public componentCssClass: string = 'dark-theme';
public showAccount: boolean = false;
public showOrgContext: boolean = false;
public org!: Org.AsObject;
public orgs$: Observable<Org.AsObject[]> = of([]);
// public user!: User.AsObject;
public isDarkTheme: Observable<boolean> = of(true);
public orgLoading$: BehaviorSubject<any> = new BehaviorSubject(false);
public showProjectSection: boolean = false;
public filterControl: FormControl = new FormControl('');
private destroy$: Subject<void> = new Subject();
public labelpolicy!: LabelPolicy.AsObject;
@ -215,10 +209,6 @@ export class AppComponent implements OnDestroy {
this.language = language.lang;
});
this.filterControl.valueChanges.pipe(debounceTime(300)).subscribe((value) => {
this.loadOrgs(value.trim().toLowerCase());
});
this.hideAdminWarn = localStorage.getItem('hideAdministratorWarning') === 'true' ? true : false;
this.loadPolicies();
@ -290,29 +280,6 @@ export class AppComponent implements OnDestroy {
});
}
public loadOrgs(filter?: string): void {
let query;
if (filter) {
query = new OrgQuery();
const orgNameQuery = new OrgNameQuery();
orgNameQuery.setName(filter);
orgNameQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
query.setNameQuery(orgNameQuery);
}
this.orgLoading$.next(true);
this.orgs$ = from(this.authService.listMyProjectOrgs(10, 0, query ? [query] : undefined)).pipe(
map((resp) => {
return resp.resultList.sort((left, right) => left.name.localeCompare(right.name));
}),
catchError(() => of([])),
finalize(() => {
this.orgLoading$.next(false);
this.focusFilter();
}),
);
}
public prepareRoute(outlet: RouterOutlet): boolean {
return outlet && outlet.activatedRouteData && outlet.activatedRouteData.animation;
}
@ -350,6 +317,7 @@ export class AppComponent implements OnDestroy {
}
public setActiveOrg(org: Org.AsObject): void {
console.log(this.org);
this.org = org;
this.authService.setActiveOrg(org);
this.loadPrivateLabelling();
@ -366,10 +334,4 @@ export class AppComponent implements OnDestroy {
}
});
}
focusFilter(): void {
setTimeout(() => {
this.input.nativeElement.focus();
}, 0);
}
}

View File

@ -9,7 +9,6 @@ import { MatCardModule } from '@angular/material/card';
import { MatNativeDateModule } from '@angular/material/core';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSidenavModule } from '@angular/material/sidenav';
@ -36,6 +35,7 @@ import { OutsideClickModule } from './directives/outside-click/outside-click.mod
import { AccountsCardModule } from './modules/accounts-card/accounts-card.module';
import { AvatarModule } from './modules/avatar/avatar.module';
import { InputModule } from './modules/input/input.module';
import { OrgContextModule } from './modules/org-context/org-context.module';
import { WarnDialogModule } from './modules/warn-dialog/warn-dialog.module';
import { SignedoutComponent } from './pages/signedout/signedout.component';
import { HasFeaturePipeModule } from './pipes/has-feature-pipe/has-feature-pipe.module';
@ -109,6 +109,7 @@ const authConfig: AuthConfig = {
MatNativeDateModule,
QuicklinkModule,
AccountsCardModule,
OrgContextModule,
HasRoleModule,
BrowserAnimationsModule,
HttpClientModule,
@ -126,7 +127,6 @@ const authConfig: AuthConfig = {
MatProgressSpinnerModule,
MatToolbarModule,
ReactiveFormsModule,
MatMenuModule,
MatSnackBarModule,
AvatarModule,
WarnDialogModule,

View File

@ -0,0 +1,28 @@
<div class="card" cnslOutsideClick (clickOutside)="closeCard($event)">
<div class="spinner-w">
<mat-spinner diameter="20" *ngIf="orgLoading$ | async" color="accent">
</mat-spinner>
</div>
<div class="filter-wrapper">
<input cnslInput class="filter-input" [formControl]="filterControl" autocomplete="off"
(click)="$event.stopPropagation()" placeholder="{{'ORG.PAGES.FILTERPLACEHOLDER' | translate}}" #input>
</div>
<div class="org-wrapper">
<button mat-button [ngClass]="{'active': temporg.id === org?.id}" [disabled]="!temporg.id"
*ngFor="let temporg of orgs$ | async" (click)="setActiveOrg(temporg)">
{{temporg?.name ? temporg.name : 'NO NAME'}}
</button>
</div>
<button mat-button class="show-all" [routerLink]="[ '/org/overview' ]">{{'MENU.SHOWORGS' |
translate}}</button>
<ng-template cnslHasRole [hasRole]="['org.create','iam.write']">
<button mat-button [routerLink]="[ '/org/create' ]">
<mat-icon class="avatar">add</mat-icon>
{{'MENU.NEWORG' | translate}}
</button>
</ng-template>
</div>

View File

@ -0,0 +1,43 @@
.card {
border-radius: 0.5rem;
z-index: 200;
border: 1px solid #ffffff30;
display: flex;
flex-direction: column;
align-items: center;
padding: 0;
min-width: 220px;
padding-bottom: 0.5rem;
position: relative;
.filter-wrapper {
padding: 0.5rem;
}
.spinner-w {
top: 1rem;
left: 0;
right: 0;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
}
.show-all {
width: 100%;
}
.org-wrapper {
max-height: 350px;
display: flex;
align-items: stretch;
flex-direction: column;
overflow-y: auto;
width: 100%;
button {
text-align: start;
}
}
}

View File

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

View File

@ -0,0 +1,79 @@
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
import { BehaviorSubject, catchError, debounceTime, finalize, from, map, Observable, of } from 'rxjs';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import { Org, OrgNameQuery, OrgQuery } from 'src/app/proto/generated/zitadel/org_pb';
import { AuthenticationService } from 'src/app/services/authentication.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
@Component({
selector: 'cnsl-org-context',
templateUrl: './org-context.component.html',
styleUrls: ['./org-context.component.scss'],
})
export class OrgContextComponent implements OnInit {
public orgLoading$: BehaviorSubject<any> = new BehaviorSubject(false);
public orgs$: Observable<Org.AsObject[]> = of([]);
public filterControl: FormControl = new FormControl('');
@Input() public org!: Org.AsObject;
@ViewChild('input', { static: false }) input!: ElementRef;
@Output() public closedCard: EventEmitter<void> = new EventEmitter();
@Output() public setOrg: EventEmitter<Org.AsObject> = new EventEmitter();
constructor(public authService: AuthenticationService, private auth: GrpcAuthService) {
this.filterControl.valueChanges.pipe(debounceTime(500)).subscribe((value) => {
this.loadOrgs(value.trim().toLowerCase());
});
}
public ngOnInit(): void {
this.focusFilter();
this.loadOrgs();
}
public setActiveOrg(org: Org.AsObject) {
this.setOrg.emit(org);
this.closedCard.emit();
}
public loadOrgs(filter?: string): void {
if (!filter) {
const value = this.input?.nativeElement?.value;
if (value) {
filter = value;
}
}
let query;
if (filter) {
query = new OrgQuery();
const orgNameQuery = new OrgNameQuery();
orgNameQuery.setName(filter);
orgNameQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
query.setNameQuery(orgNameQuery);
}
this.orgLoading$.next(true);
this.orgs$ = from(this.auth.listMyProjectOrgs(10, 0, query ? [query] : undefined)).pipe(
map((resp) => {
return resp.resultList.sort((left, right) => left.name.localeCompare(right.name));
}),
catchError(() => of([])),
finalize(() => {
this.orgLoading$.next(false);
}),
);
}
public closeCard(element: HTMLElement): void {
if (!element.classList.contains('dontcloseonclick') && !element.classList.contains('mat-button-wrapper')) {
this.closedCard.emit();
}
}
private focusFilter(): void {
setTimeout(() => {
this.input.nativeElement.focus();
}, 0);
}
}

View File

@ -0,0 +1,33 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { OutsideClickModule } from 'src/app/directives/outside-click/outside-click.module';
import { InputModule } from '../input/input.module';
import { OrgContextComponent } from './org-context.component';
@NgModule({
declarations: [OrgContextComponent],
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
MatIconModule,
RouterModule,
MatProgressSpinnerModule,
MatButtonModule,
InputModule,
OutsideClickModule,
TranslateModule,
MatButtonModule,
HasRoleModule,
],
exports: [OrgContextComponent],
})
export class OrgContextModule {}

View File

@ -108,7 +108,7 @@
}
},
"ACTIONS": {
"ACTIONS": "Aktionen",
"ACTIONS": "Actions",
"RENAME": "Rename",
"SET": "Set",
"COPY": "Copy to Clipboard",
@ -1051,7 +1051,7 @@
}
},
"STATE": {
"TITLE": "State",
"TITLE": "Status",
"0": "Not defined",
"1": "Active",
"2": "Inactive"
@ -1108,7 +1108,7 @@
"GRANTEDORG": "Granted Organisation",
"RESOURCEOWNER": "Resource Owner"
},
"STATE": "State",
"STATE": "Status",
"STATES": {
"1": "Active",
"2": "Inactive"
@ -1247,7 +1247,7 @@
"ID": "ID",
"NAME": "Name",
"CONFIG": "Configuration",
"STATE": "State",
"STATE": "Status",
"ISSUER": "Issuer",
"SCOPESLIST": "Scopes List",
"CLIENTID": "Client ID",
@ -1341,7 +1341,7 @@
"CREATE_OIDC": "OIDC Application",
"CREATE_OIDC_DESC_TITLE": "Enter Your Application Details Step by Step",
"CREATE_OIDC_DESC_SUB": "A recommended configuration will be automatically generated.",
"STATE": "State",
"STATE": "Status",
"DATECREATED": "Created",
"DATECHANGED": "Changed",
"URLS": "Urls",