mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-01 20:02:15 +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
@@ -1,4 +1,4 @@
|
||||
<div class="card" cnslOutsideClick (clickOutside)="closeCard($event)">
|
||||
<div class="org-context-card" cdkTrapFocus>
|
||||
<div class="spinner-w">
|
||||
<mat-spinner diameter="20" *ngIf="orgLoading$ | async" color="accent">
|
||||
</mat-spinner>
|
||||
@@ -10,19 +10,40 @@
|
||||
</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 class="org-button-with-pin" mat-button
|
||||
[ngClass]="{'active': pinnedorg.id === org?.id, 'border-bottom':pinned.selected.length && i === pinned.selected.length -1}"
|
||||
[disabled]="!pinnedorg.id" *ngFor="let pinnedorg of pinned.selected; index as i"
|
||||
(click)="setActiveOrg(pinnedorg)">
|
||||
<div class="org-flex-row">
|
||||
<span class="org-span">{{pinnedorg?.name ? pinnedorg.name : 'NO NAME'}}</span>
|
||||
<template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{key: pinnedorg}"></template>
|
||||
</div>
|
||||
</button>
|
||||
<ng-container *ngFor="let temporg of orgs$ | async">
|
||||
<button *ngIf="!pinned.isSelected(temporg)" class="org-button-with-pin" mat-button
|
||||
[ngClass]="{'active': temporg.id === org?.id}" [disabled]="!temporg.id" (click)="setActiveOrg(temporg)">
|
||||
<div class="org-flex-row"><span class="org-span">{{temporg?.name ? temporg.name : 'NO NAME'}}</span>
|
||||
<template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{key: temporg}"></template>
|
||||
</div>
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<button mat-button class="show-all" [routerLink]="[ '/org/overview' ]">{{'MENU.SHOWORGS' |
|
||||
<button mat-button class="show-all" [routerLink]="[ '/org/overview' ]" (click)="closedCard.emit()">{{'MENU.SHOWORGS' |
|
||||
translate}}</button>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['org.create','iam.write']">
|
||||
<button mat-button [routerLink]="[ '/org/create' ]">
|
||||
<button mat-button [routerLink]="[ '/org/create' ]" (click)="closedCard.emit()">
|
||||
<mat-icon class="avatar">add</mat-icon>
|
||||
{{'MENU.NEWORG' | translate}}
|
||||
</button>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #toggleButton let-key="key">
|
||||
<button matTooltip="{{'ACTIONS.PIN' | translate}}" [ngClass]="{ selected: pinned.isSelected(key)}"
|
||||
(click)="toggle(key,$event)" class="edit-button" mat-icon-button>
|
||||
<mat-icon *ngIf="pinned.isSelected(key)" svgIcon="mdi_pin"></mat-icon>
|
||||
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!pinned.isSelected(key)"></mat-icon>
|
||||
</button>
|
||||
</ng-template>
|
||||
@@ -1,48 +1,106 @@
|
||||
.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;
|
||||
max-width: 240px;
|
||||
padding-bottom: 0.5rem;
|
||||
position: relative;
|
||||
@mixin org-context-theme($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
$warn: map-get($theme, warn);
|
||||
$background: map-get($theme, background);
|
||||
$accent: map-get($theme, accent);
|
||||
$primary-color: mat.get-color-from-palette($primary, 500);
|
||||
|
||||
.filter-wrapper {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
$warn-color: mat.get-color-from-palette($warn, 500);
|
||||
$accent-color: mat.get-color-from-palette($accent, 500);
|
||||
$foreground: map-get($theme, foreground);
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
$back: map-get($background, background);
|
||||
|
||||
.spinner-w {
|
||||
top: 1rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
.org-context-card {
|
||||
border-radius: 0.5rem;
|
||||
z-index: 200;
|
||||
border: 1px solid #ffffff30;
|
||||
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%;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
min-width: 220px;
|
||||
max-width: 240px;
|
||||
padding-bottom: 0.5rem;
|
||||
position: relative;
|
||||
color: map-get($foreground, text);
|
||||
background: map-get($background, cards);
|
||||
|
||||
button {
|
||||
min-height: 36px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-align: start;
|
||||
text-overflow: ellipsis;
|
||||
.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%;
|
||||
border-top: 1px solid rgba(#8795a1, 0.2);
|
||||
|
||||
.org-button-with-pin {
|
||||
min-height: 36px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.org-flex-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.org-span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1rem;
|
||||
margin-right: -0.5rem;
|
||||
visibility: hidden;
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 20px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.border-bottom {
|
||||
border-bottom: 1px solid #66666630;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.org-flex-row {
|
||||
button {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
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 { BehaviorSubject, catchError, debounceTime, finalize, from, map, Observable, of, pipe, tap } 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';
|
||||
@@ -12,6 +13,8 @@ import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
styleUrls: ['./org-context.component.scss'],
|
||||
})
|
||||
export class OrgContextComponent implements OnInit {
|
||||
public pinned: SelectionModel<Org.AsObject> = new SelectionModel<Org.AsObject>(true, []);
|
||||
|
||||
public orgLoading$: BehaviorSubject<any> = new BehaviorSubject(false);
|
||||
public orgs$: Observable<Org.AsObject[]> = of([]);
|
||||
public filterControl: FormControl = new FormControl('');
|
||||
@@ -54,11 +57,23 @@ export class OrgContextComponent implements OnInit {
|
||||
}
|
||||
|
||||
this.orgLoading$.next(true);
|
||||
this.orgs$ = from(this.auth.listMyProjectOrgs(10, 0, query ? [query] : undefined)).pipe(
|
||||
this.orgs$ = from(this.auth.listMyProjectOrgs(undefined, 0, query ? [query] : undefined)).pipe(
|
||||
map((resp) => {
|
||||
return resp.resultList.sort((left, right) => left.name.localeCompare(right.name));
|
||||
}),
|
||||
catchError(() => of([])),
|
||||
pipe(
|
||||
tap((orgs: Org.AsObject[]) => {
|
||||
this.pinned.clear();
|
||||
this.getPrefixedItem('pinned-orgs').then((stringifiedOrgs) => {
|
||||
if (stringifiedOrgs) {
|
||||
const orgIds: string[] = JSON.parse(stringifiedOrgs);
|
||||
const pinnedOrgs = orgs.filter((o) => orgIds.includes(o.id));
|
||||
pinnedOrgs.forEach((o) => this.pinned.select(o));
|
||||
}
|
||||
});
|
||||
}),
|
||||
),
|
||||
finalize(() => {
|
||||
this.orgLoading$.next(false);
|
||||
}),
|
||||
@@ -76,4 +91,19 @@ export class OrgContextComponent implements OnInit {
|
||||
this.input.nativeElement.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public toggle(item: Org.AsObject, event: any): void {
|
||||
event.stopPropagation();
|
||||
this.pinned.toggle(item);
|
||||
|
||||
this.setPrefixedItem('pinned-orgs', JSON.stringify(this.pinned.selected.map((o) => o.id)));
|
||||
}
|
||||
|
||||
private async getPrefixedItem(key: string): Promise<string | null> {
|
||||
return localStorage.getItem(`${key}`);
|
||||
}
|
||||
|
||||
private async setPrefixedItem(key: string, value: any): Promise<void> {
|
||||
return localStorage.setItem(`${key}`, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { A11yModule } from '@angular/cdk/a11y';
|
||||
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 { MatTooltipModule } from '@angular/material/tooltip';
|
||||
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';
|
||||
@@ -17,13 +18,14 @@ import { OrgContextComponent } from './org-context.component';
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
A11yModule,
|
||||
ReactiveFormsModule,
|
||||
MatIconModule,
|
||||
RouterModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatButtonModule,
|
||||
InputModule,
|
||||
OutsideClickModule,
|
||||
MatTooltipModule,
|
||||
TranslateModule,
|
||||
MatButtonModule,
|
||||
HasRoleModule,
|
||||
|
||||
Reference in New Issue
Block a user