mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-12 00:55:12 +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
@@ -0,0 +1,170 @@
|
||||
<ng-container *ngIf="((['iam.read$','iam.write$'] | hasRole)) as iamuser$">
|
||||
<div class="nav-col" [ngClass]="{'is-admin': (iamuser$ | async)}">
|
||||
|
||||
<ng-container
|
||||
*ngIf="breadcrumbService.breadcrumbsExtended$ && (breadcrumbService.breadcrumbsExtended$ | async) as breadc">
|
||||
|
||||
<ng-container
|
||||
*ngIf="breadc[breadc.length - 1] && !breadc[breadc.length - 1].hideNav && breadc[breadc.length - 1].type !== BreadcrumbType.GRANTEDPROJECT && breadc[breadc.length - 1].type !== BreadcrumbType.APP && breadc[breadc.length - 1].type !== BreadcrumbType.AUTHUSER"
|
||||
[ngSwitch]="breadc[breadc.length - 1].type">
|
||||
<div class="nav-row" @navrow>
|
||||
<ng-container *ngSwitchCase="BreadcrumbType.IAM">
|
||||
<div class="nav-row-abs" @navroworg>
|
||||
<ng-template cnslHasRole [hasRole]="['iam.read']">
|
||||
<a class="nav-item" [routerLinkActiveOptions]="{ exact: false }" [routerLinkActive]="['active']"
|
||||
[routerLink]="[ '/system' ]">
|
||||
|
||||
<div class="c_label">
|
||||
<span> {{'MENU.IAM' | translate}} </span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="nav-item" [routerLinkActiveOptions]="{ exact: false }" [routerLinkActive]="['active']"
|
||||
[routerLink]="[ '/views']">
|
||||
|
||||
<div class="c_label">
|
||||
<span> {{'MENU.VIEWS' | translate}} </span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="nav-item" [routerLinkActiveOptions]="{ exact: false }" [routerLinkActive]="['active']"
|
||||
[routerLink]="[ '/failed-events']">
|
||||
|
||||
<div class="c_label">
|
||||
<span> {{'MENU.FAILEDEVENTS' | translate}} </span>
|
||||
</div>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<template [ngTemplateOutlet]="shortcutKeyRef"></template>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="BreadcrumbType.ORG">
|
||||
<div class="nav-row-abs" @navroworg>
|
||||
<a class="nav-item" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }"
|
||||
[routerLink]="[ '/']">
|
||||
<span class="label">{{'MENU.DASHBOARD' | translate}}</span>
|
||||
</a>
|
||||
|
||||
<ng-container class="org-list" *ngIf="org" [@navAnimation]="org">
|
||||
<ng-template cnslHasRole [hasRole]="['org.read']">
|
||||
<a class="nav-item" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: false }"
|
||||
[routerLink]="[ '/org']">
|
||||
<span class="label">{{'MENU.ORGANIZATION' | translate}}</span>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['project.read(:[0-9]*)?']">
|
||||
<a class="nav-item" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: false }"
|
||||
[routerLink]="[ '/projects']">
|
||||
|
||||
<div class="c_label">
|
||||
<span> {{'MENU.PROJECT' | translate}} </span>
|
||||
<small
|
||||
*ngIf="(mgmtService?.ownedProjectsCount | async) && (mgmtService?.grantedProjectsCount | async)"
|
||||
class="count">({{((mgmtService?.ownedProjectsCount | async) ?? 0) +
|
||||
((mgmtService?.grantedProjectsCount | async) ?? 0)}})</small>
|
||||
</div>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['user.read(:[0-9]*)?']">
|
||||
<a class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/users']"
|
||||
[routerLinkActiveOptions]="{ exact: false }">
|
||||
<span class="label">{{ 'MENU.HUMANUSERS' | translate }}</span>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['user.grant.read(:[0-9]*)?']">
|
||||
<a class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/grants']"
|
||||
[routerLinkActiveOptions]="{ exact: true }">
|
||||
<span class="label">{{ 'MENU.GRANTS' | translate }}</span>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-template cnslHasFeature [hasFeature]="['actions']">
|
||||
<ng-template cnslHasRole [hasRole]="['org.action.read']">
|
||||
<a class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/actions']"
|
||||
[routerLinkActiveOptions]="{ exact: true }">
|
||||
<span class="label">{{ 'MENU.ACTIONS' | translate }}</span>
|
||||
</a>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['org.read']">
|
||||
<a class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/domains']"
|
||||
[routerLinkActiveOptions]="{ exact: true }">
|
||||
<span class="label">{{'MENU.DOMAINS' | translate}}</span>
|
||||
</a>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<template [ngTemplateOutlet]="shortcutKeyRef"></template>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="BreadcrumbType.PROJECT">
|
||||
<div *ngIf="breadc[breadc.length - 1]?.param?.value" class="nav-row-abs" @navrowproject>
|
||||
<ng-template cnslHasRole [hasRole]="['project.read(:[0-9]*)?']">
|
||||
<a class="nav-item" [routerLinkActiveOptions]="{ exact: true }" [routerLinkActive]="['active']"
|
||||
[routerLink]="[ '/projects', breadc[breadc.length - 1]?.param?.value ]">
|
||||
|
||||
<div class="c_label">
|
||||
<span> {{'MENU.PROJECTOVERVIEW' | translate}} </span>
|
||||
</div>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['project.read(:[0-9]*)?']">
|
||||
<a *ngIf="!breadc[breadc.length - 1]?.isZitadel" class="nav-item"
|
||||
[routerLinkActiveOptions]="{ exact: false }" [routerLinkActive]="['active']"
|
||||
[routerLink]="[ '/projects', breadc[breadc.length - 1]?.param?.value, 'roles']">
|
||||
|
||||
<div class="c_label">
|
||||
<span> {{'MENU.ROLES' | translate}} </span>
|
||||
</div>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['project.read(:[0-9]*)?']">
|
||||
<a *ngIf="!breadc[breadc.length - 1]?.isZitadel" class="nav-item"
|
||||
[routerLinkActiveOptions]="{ exact: false }" [routerLinkActive]="['active']"
|
||||
[routerLink]="[ '/projects', breadc[breadc.length - 1]?.param?.value, 'projectgrants' ]">
|
||||
|
||||
<div class="c_label">
|
||||
<span> {{'MENU.PROJECTGRANTS' | translate}} </span>
|
||||
</div>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['project.read(:[0-9]*)?']">
|
||||
<a *ngIf="!breadc[breadc.length - 1]?.isZitadel" class="nav-item"
|
||||
[routerLinkActiveOptions]="{ exact: true }" [routerLinkActive]="['active']"
|
||||
[routerLink]="[ '/projects', breadc[breadc.length - 1]?.param?.value, 'grants' ]">
|
||||
|
||||
<div class="c_label">
|
||||
<span> {{'MENU.GRANTS' | translate}} </span>
|
||||
</div>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<template [ngTemplateOutlet]="shortcutKeyRef"></template>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-template #shortcutKeyRef>
|
||||
<ng-container *ngIf="(isHandset$ | async) === false">
|
||||
<span class="fill-space"></span>
|
||||
<div class="nav-shortcut-action-key" matTooltip="{{'MENU.OPENSHORTCUTSTOOLTIP' | translate}}">
|
||||
<div class="nav-key-overlay"></div>
|
||||
<span>?</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,190 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
@mixin nav-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);
|
||||
|
||||
$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);
|
||||
|
||||
.nav-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
position: relative;
|
||||
align-items: flex-start;
|
||||
transform: transale3d(0, 0, 0);
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) !important;
|
||||
|
||||
.nav-row {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
|
||||
.nav-row-abs {
|
||||
padding: 0 2rem;
|
||||
max-width: 80rem;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
align-self: stretch;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
padding: 0 1rem 0 1rem;
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
padding: 0.4rem 12px;
|
||||
color: mat.get-color-from-palette($foreground, text) !important;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
border-radius: 50vw;
|
||||
font-weight: 500;
|
||||
margin: 0.25rem 2px;
|
||||
white-space: nowrap;
|
||||
|
||||
.c_label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
.count {
|
||||
display: none;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: if($is-dark-theme, #ffffff40, #00000010);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $primary-color;
|
||||
color: mat.get-color-from-palette($foreground, toolbar-items) !important;
|
||||
|
||||
.c_label {
|
||||
.count {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-menu-content,
|
||||
.mat-menu-panel {
|
||||
background-color: $back;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
@include mat.elevation(5);
|
||||
|
||||
.active {
|
||||
color: $primary-color !important;
|
||||
background-color: rgba($color: $primary-color, $alpha: 0.1) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.root-header {
|
||||
background-color: $back !important;
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
|
||||
.slash {
|
||||
color: if($is-dark-theme, #525454, #d1d5d9);
|
||||
}
|
||||
|
||||
.org-button {
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
.span {
|
||||
border-color: map-get($foreground, divider);
|
||||
}
|
||||
|
||||
.line {
|
||||
background-color: map-get($foreground, divider);
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.3);
|
||||
background-color: map-get($background, background);
|
||||
border-radius: 1px;
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
background-color: map-get($background, background);
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: $primary-color;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-shortcut-action-key {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
position: relative;
|
||||
margin: 0 4px;
|
||||
cursor: default;
|
||||
|
||||
.nav-key-overlay {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: if($is-dark-theme, #fff, #000);
|
||||
opacity: 0.15;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.span {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50% -50%);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NavComponent } from './nav.component';
|
||||
|
||||
describe('NavComponent', () => {
|
||||
let component: NavComponent;
|
||||
let fixture: ComponentFixture<NavComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [NavComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(NavComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import { animate, keyframes, style, transition, trigger } from '@angular/animations';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { Component, ElementRef, Input, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { BehaviorSubject, map, Observable, Subject } from 'rxjs';
|
||||
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import { LabelPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
||||
import { User } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { AuthenticationService } from 'src/app/services/authentication.service';
|
||||
import { BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-nav',
|
||||
templateUrl: './nav.component.html',
|
||||
styleUrls: ['./nav.component.scss'],
|
||||
animations: [
|
||||
trigger('navrow', [
|
||||
transition(':enter', [
|
||||
animate('.2s ease-in', keyframes([style({ opacity: 0, height: '0' }), style({ opacity: 1, height: '*' })])),
|
||||
]),
|
||||
transition(':leave', [
|
||||
animate('.2s ease-out', keyframes([style({ opacity: 1, height: '*' }), style({ opacity: 0, height: '0' })])),
|
||||
]),
|
||||
]),
|
||||
trigger('navroworg', [
|
||||
transition(':enter', [
|
||||
animate(
|
||||
'.2s ease-in',
|
||||
keyframes([
|
||||
style({ opacity: 0, transform: 'translateY(-50%)' }),
|
||||
style({ opacity: 1, transform: 'translateY(0%)' }),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
transition(':leave', [
|
||||
animate(
|
||||
'.2s ease-out',
|
||||
keyframes([
|
||||
style({ opacity: 1, transform: 'translateY(0%)' }),
|
||||
style({ opacity: 0, transform: 'translateY(-50%)' }),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
trigger('navrowproject', [
|
||||
transition(':enter', [
|
||||
animate(
|
||||
'.2s ease-in',
|
||||
keyframes([
|
||||
style({ opacity: 0, transform: 'translateY(+50%)' }),
|
||||
style({ opacity: 1, transform: 'translateY(0%)' }),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
transition(':leave', [
|
||||
animate(
|
||||
'.2s ease-out',
|
||||
keyframes([
|
||||
style({ opacity: 1, transform: 'translateY(0%)' }),
|
||||
style({ opacity: 0, transform: 'translateY(+50%)' }),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class NavComponent implements OnDestroy {
|
||||
@ViewChild('input', { static: false }) input!: ElementRef;
|
||||
|
||||
@Input() public isDarkTheme: boolean = true;
|
||||
@Input() public user!: User.AsObject;
|
||||
@Input() public labelpolicy!: LabelPolicy.AsObject;
|
||||
public isHandset$: Observable<boolean> = this.breakpointObserver.observe('(max-width: 599px)').pipe(
|
||||
map((result) => {
|
||||
return result.matches;
|
||||
}),
|
||||
);
|
||||
|
||||
@Input() public org!: Org.AsObject;
|
||||
public filterControl: FormControl = new FormControl('');
|
||||
public orgLoading$: BehaviorSubject<any> = new BehaviorSubject(false);
|
||||
public showAccount: boolean = false;
|
||||
public hideAdminWarn: boolean = true;
|
||||
private destroy$: Subject<void> = new Subject();
|
||||
|
||||
public BreadcrumbType: any = BreadcrumbType;
|
||||
|
||||
constructor(
|
||||
public authenticationService: AuthenticationService,
|
||||
public breadcrumbService: BreadcrumbService,
|
||||
public mgmtService: ManagementService,
|
||||
private router: Router,
|
||||
private breakpointObserver: BreakpointObserver,
|
||||
) {
|
||||
this.hideAdminWarn = localStorage.getItem('hideAdministratorWarning') === 'true' ? true : false;
|
||||
}
|
||||
|
||||
public toggleAdminHide(): void {
|
||||
this.hideAdminWarn = !this.hideAdminWarn;
|
||||
localStorage.setItem('hideAdministratorWarning', this.hideAdminWarn.toString());
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
public get isUserLinkActive(): boolean {
|
||||
const url = this.router.url;
|
||||
return url.substring(0, 6) === '/users';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { OverlayModule } from '@angular/cdk/overlay';
|
||||
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 { MatMenuModule } from '@angular/material/menu';
|
||||
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 { HasFeatureModule } from 'src/app/directives/has-feature/has-feature.module';
|
||||
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
|
||||
import { HasFeaturePipeModule } from 'src/app/pipes/has-feature-pipe/has-feature-pipe.module';
|
||||
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
|
||||
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { NavComponent } from './nav.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [NavComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
TranslateModule,
|
||||
MatIconModule,
|
||||
RouterModule,
|
||||
MatTooltipModule,
|
||||
HasRolePipeModule,
|
||||
HasFeaturePipeModule,
|
||||
HasFeatureModule,
|
||||
HasRoleModule,
|
||||
MatMenuModule,
|
||||
MatButtonModule,
|
||||
MatProgressSpinnerModule,
|
||||
SharedModule,
|
||||
OverlayModule,
|
||||
],
|
||||
exports: [NavComponent],
|
||||
})
|
||||
export class NavModule {}
|
||||
Reference in New Issue
Block a user