mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 13:19:21 +00:00
local changes
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<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"
|
||||
*ngIf="breadcrumbService.breadcrumbsExtended$ | async as breadc"
|
||||
>
|
||||
<ng-container
|
||||
*ngIf="
|
||||
@@ -96,12 +96,7 @@
|
||||
[routerLinkActive]="['active']"
|
||||
[routerLinkActiveOptions]="{ exact: false }"
|
||||
[routerLink]="['/org-settings']"
|
||||
*ngIf="
|
||||
(['policy.read'] | hasRole | async) &&
|
||||
((['iam.read$', 'iam.write$'] | hasRole | async) === false ||
|
||||
(((authService.cachedOrgs | async)?.length ?? 1) > 1 &&
|
||||
(['iam.read$', 'iam.write$'] | hasRole | async)))
|
||||
"
|
||||
*ngIf="['policy.read'] | hasRole | async"
|
||||
>
|
||||
<span class="label">{{ 'MENU.SETTINGS' | translate }}</span>
|
||||
</a>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<button class="header-button" cnslInput>
|
||||
<span class="header-text">
|
||||
<ng-content></ng-content>
|
||||
<div class="cnsl-action-button">
|
||||
<ng-icon size="1.2rem" name="heroChevronUpDown"></ng-icon>
|
||||
</div>
|
||||
</span>
|
||||
<button class="header-button" matRipple [matRippleUnbounded]="false">
|
||||
<ng-icon size="1.2rem" name="heroChevronUpDown"></ng-icon>
|
||||
</button>
|
||||
|
@@ -1,5 +1,4 @@
|
||||
.header-button {
|
||||
width: unset;
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@@ -8,4 +7,23 @@
|
||||
padding-right: 0;
|
||||
height: 32px;
|
||||
max-height: 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
@mixin header-button-theme($theme) {
|
||||
$foreground: map-get($theme, foreground);
|
||||
|
||||
.header-button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
color: map-get($foreground, text);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { heroChevronUpDown } from '@ng-icons/heroicons/outline';
|
||||
import { MatRippleModule } from '@angular/material/core';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-header-button',
|
||||
@@ -8,7 +9,7 @@ import { heroChevronUpDown } from '@ng-icons/heroicons/outline';
|
||||
styleUrls: ['./header-button.component.scss'],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgIconComponent],
|
||||
imports: [NgIconComponent, MatRippleModule],
|
||||
providers: [provideIcons({ heroChevronUpDown })],
|
||||
})
|
||||
export class HeaderButtonComponent {}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
<div class="new-header-wrapper">
|
||||
<span routerLink="/" class="new-header-title">CONSOLE</span>
|
||||
<span *ngIf="!isHandset()" routerLink="/" class="new-header-title">CONSOLE</span>
|
||||
<ng-container *ngIf="myInstanceQuery.data()?.instance as instance">
|
||||
<ng-container *ngTemplateOutlet="slash"></ng-container>
|
||||
<cnsl-header-button
|
||||
cdkOverlayOrigin
|
||||
#instanceTrigger="cdkOverlayOrigin"
|
||||
(click)="isInstanceDropdownOpen.set(!isInstanceDropdownOpen())"
|
||||
>Instance</cnsl-header-button
|
||||
>{{ instance.name }}</cnsl-header-button
|
||||
>
|
||||
<cnsl-header-dropdown
|
||||
[trigger]="instanceTrigger"
|
||||
@@ -40,6 +40,14 @@
|
||||
<cnsl-organization-selector (orgChanged)="isOrgDropdownOpen.set(false)"></cnsl-organization-selector>
|
||||
</cnsl-header-dropdown>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!isHandset()">
|
||||
<ng-container *ngFor="let bread of breadcrumbs(); index as i; let last = last">
|
||||
<ng-container *ngTemplateOutlet="slash"></ng-container>
|
||||
<a matRipple [matRippleUnbounded]="false" [routerLink]="bread.routerLink">
|
||||
{{ bread.name }}
|
||||
</a>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-template #slash>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, effect, Signal, signal } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, computed, effect, Signal, signal } from '@angular/core';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { NewOrganizationService } from '../../services/new-organization.service';
|
||||
import { ToastService } from '../../services/toast.service';
|
||||
import { AsyncPipe, NgIf, NgTemplateOutlet } from '@angular/common';
|
||||
import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
|
||||
import { injectQuery } from '@tanstack/angular-query-experimental';
|
||||
import { OrganizationSelectorComponent } from './organization-selector/organization-selector.component';
|
||||
import { CdkOverlayOrigin } from '@angular/cdk/overlay';
|
||||
@@ -18,6 +18,8 @@ import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { NewAdminService } from '../../services/new-admin.service';
|
||||
import { NewAuthService } from '../../services/new-auth.service';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from '../../services/breadcrumb.service';
|
||||
import { MatRippleModule } from '@angular/material/core';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-new-header',
|
||||
@@ -39,6 +41,8 @@ import { RouterLink } from '@angular/router';
|
||||
AsyncPipe,
|
||||
HasRolePipeModule,
|
||||
RouterLink,
|
||||
NgForOf,
|
||||
MatRippleModule,
|
||||
],
|
||||
})
|
||||
export class NewHeaderComponent {
|
||||
@@ -53,6 +57,7 @@ export class NewHeaderComponent {
|
||||
protected readonly instanceSelectorSecondStep = signal(false);
|
||||
protected readonly activeOrganizationQuery = this.newOrganizationService.activeOrganizationQuery();
|
||||
protected readonly isHandset: Signal<boolean>;
|
||||
protected readonly breadcrumbs: Signal<Breadcrumb[]>;
|
||||
|
||||
constructor(
|
||||
private readonly newOrganizationService: NewOrganizationService,
|
||||
@@ -60,8 +65,10 @@ export class NewHeaderComponent {
|
||||
private readonly breakpointObserver: BreakpointObserver,
|
||||
private readonly adminService: NewAdminService,
|
||||
private readonly newAuthService: NewAuthService,
|
||||
private readonly breadcrumbService: BreadcrumbService,
|
||||
) {
|
||||
this.isHandset = this.getIsHandset();
|
||||
this.breadcrumbs = this.getBreadcrumbs();
|
||||
|
||||
effect(() => {
|
||||
if (this.listMyZitadelPermissionsQuery.isError()) {
|
||||
@@ -87,4 +94,13 @@ export class NewHeaderComponent {
|
||||
const isHandset$ = this.breakpointObserver.observe(mediaQuery).pipe(map(({ matches }) => matches));
|
||||
return toSignal(isHandset$, { initialValue: this.breakpointObserver.isMatched(mediaQuery) });
|
||||
}
|
||||
|
||||
private getBreadcrumbs() {
|
||||
const breadcrumbs = toSignal(this.breadcrumbService.breadcrumbs$, { initialValue: [] });
|
||||
return computed(() =>
|
||||
breadcrumbs().filter(
|
||||
(breadcrumb) => breadcrumb.type === BreadcrumbType.PROJECT || breadcrumb.type === BreadcrumbType.APP,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -24,21 +24,22 @@
|
||||
{{ org.name }}
|
||||
<ng-icon name="heroCheck"></ng-icon>
|
||||
</a>
|
||||
<ng-container *ngFor="let page of organizationsQuery.data()?.pages; last as lastPage">
|
||||
<ng-container *ngFor="let org of page.result; trackBy: trackOrg">
|
||||
<ng-container *ngIf="organizationsQuery.data() as data">
|
||||
<ng-container *ngFor="let org of data.orgs; trackBy: trackOrgResponse">
|
||||
<a *ngIf="org.id !== activeOrg.data()?.id" class="dropdown-button" mat-button (click)="changeOrg(org.id)">
|
||||
{{ org.name }}
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="lastPage && page.details?.totalResult as totalResult">
|
||||
<ng-container *ngIf="data?.totalResult as totalResult">
|
||||
<button
|
||||
#moreButton
|
||||
class="dropdown-button"
|
||||
mat-stroked-button
|
||||
*ngIf="totalResult > QUERY_LIMIT"
|
||||
(click)="organizationsQuery.fetchNextPage()"
|
||||
[disabled]="!organizationsQuery.hasNextPage() || organizationsQuery.isFetchingNextPage()"
|
||||
>
|
||||
...{{ totalResult - loadedOrgsCount() }} {{ 'PAGINATOR.MORE' | translate }}
|
||||
...{{ totalResult - data.orgs.length }} {{ 'PAGINATOR.MORE' | translate }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
@@ -1,4 +1,17 @@
|
||||
import { ChangeDetectionStrategy, Component, computed, effect, EventEmitter, Input, Output, Signal } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
DestroyRef,
|
||||
effect,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
signal,
|
||||
Signal,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { injectInfiniteQuery, injectMutation, keepPreviousData } from '@tanstack/angular-query-experimental';
|
||||
import { NewOrganizationService } from 'src/app/services/new-organization.service';
|
||||
import { NgForOf, NgIf } from '@angular/common';
|
||||
@@ -20,13 +33,14 @@ import { Router } from '@angular/router';
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { heroCheck, heroMagnifyingGlass } from '@ng-icons/heroicons/outline';
|
||||
import { heroArrowLeftCircleSolid } from '@ng-icons/heroicons/solid';
|
||||
import { UserService } from '../../../services/user.service';
|
||||
|
||||
type NameQuery = Extract<
|
||||
NonNullable<MessageInitShape<typeof ListOrganizationsRequestSchema>['queries']>[number]['query'],
|
||||
{ case: 'nameQuery' }
|
||||
>;
|
||||
|
||||
const QUERY_LIMIT = 5;
|
||||
const QUERY_LIMIT = 20;
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-organization-selector',
|
||||
@@ -58,6 +72,13 @@ export class OrganizationSelectorComponent {
|
||||
@Output()
|
||||
public orgChanged = new EventEmitter<Organization>();
|
||||
|
||||
@ViewChild('moreButton', { static: false, read: ElementRef })
|
||||
public set moreButton(button: ElementRef<HTMLButtonElement>) {
|
||||
this.moreButtonSignal.set(button);
|
||||
}
|
||||
|
||||
private moreButtonSignal = signal<ElementRef<HTMLButtonElement> | undefined>(undefined);
|
||||
|
||||
protected setOrgId = injectMutation(() => ({
|
||||
mutationFn: (orgId: string) => this.newOrganizationService.setOrgId(orgId),
|
||||
}));
|
||||
@@ -65,7 +86,6 @@ export class OrganizationSelectorComponent {
|
||||
protected readonly form: ReturnType<typeof this.buildForm>;
|
||||
private readonly nameQuery: Signal<NameQuery | undefined>;
|
||||
protected readonly organizationsQuery: ReturnType<typeof this.getOrganizationsQuery>;
|
||||
protected loadedOrgsCount: Signal<bigint>;
|
||||
protected activeOrg = this.newOrganizationService.activeOrganizationQuery();
|
||||
protected activeOrgIfSearchMatches: Signal<Organization | undefined>;
|
||||
|
||||
@@ -73,12 +93,13 @@ export class OrganizationSelectorComponent {
|
||||
private readonly newOrganizationService: NewOrganizationService,
|
||||
private readonly formBuilder: FormBuilder,
|
||||
private readonly router: Router,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
private readonly userService: UserService,
|
||||
toast: ToastService,
|
||||
) {
|
||||
this.form = this.buildForm();
|
||||
this.nameQuery = this.getNameQuery(this.form);
|
||||
this.organizationsQuery = this.getOrganizationsQuery(this.nameQuery);
|
||||
this.loadedOrgsCount = this.getLoadedOrgsCount(this.organizationsQuery);
|
||||
this.activeOrgIfSearchMatches = this.getActiveOrgIfSearchMatches(this.nameQuery);
|
||||
|
||||
effect(() => {
|
||||
@@ -99,12 +120,38 @@ export class OrganizationSelectorComponent {
|
||||
|
||||
effect(() => {
|
||||
const orgId = newOrganizationService.orgId();
|
||||
const orgs = this.organizationsQuery.data()?.pages[0]?.result;
|
||||
const orgs = this.organizationsQuery.data()?.orgs;
|
||||
if (orgId || !orgs || orgs.length === 0) {
|
||||
return;
|
||||
}
|
||||
const _ = newOrganizationService.setOrgId(orgs[0].id);
|
||||
});
|
||||
|
||||
this.infiniteScrollLoading();
|
||||
}
|
||||
|
||||
private infiniteScrollLoading() {
|
||||
const intersection = new IntersectionObserver(async (entries) => {
|
||||
if (!entries[0]?.isIntersecting) {
|
||||
return;
|
||||
}
|
||||
await this.organizationsQuery.fetchNextPage();
|
||||
});
|
||||
this.destroyRef.onDestroy(() => {
|
||||
intersection.disconnect();
|
||||
});
|
||||
|
||||
effect((onCleanup) => {
|
||||
const moreButton = this.moreButtonSignal()?.nativeElement;
|
||||
if (!moreButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
intersection.observe(moreButton);
|
||||
onCleanup(() => {
|
||||
intersection.unobserve(moreButton);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private buildForm() {
|
||||
@@ -136,9 +183,12 @@ export class OrganizationSelectorComponent {
|
||||
private getOrganizationsQuery(nameQuery: Signal<NameQuery | undefined>) {
|
||||
return injectInfiniteQuery(() => {
|
||||
const query = nameQuery();
|
||||
const exp = this.userService.exp();
|
||||
const isExpired = exp ? exp <= new Date() : true;
|
||||
return {
|
||||
queryKey: ['organization', 'listOrganizationsInfinite', query],
|
||||
queryFn: ({ pageParam, signal }) => this.newOrganizationService.listOrganizations(pageParam, signal),
|
||||
enabled: !isExpired,
|
||||
initialPageParam: {
|
||||
query: {
|
||||
limit: QUERY_LIMIT,
|
||||
@@ -157,14 +207,14 @@ export class OrganizationSelectorComponent {
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
select: (data) => ({
|
||||
orgs: data.pages.flatMap((page) => page.result),
|
||||
totalResult: Number(data.pages[data.pages.length - 1]?.details?.totalResult ?? 0),
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getLoadedOrgsCount(organizationsQuery: ReturnType<typeof this.getOrganizationsQuery>) {
|
||||
return computed(() => this.countLoadedOrgs(organizationsQuery.data()?.pages));
|
||||
}
|
||||
|
||||
private countLoadedOrgs(pages?: ListOrganizationsResponse[]) {
|
||||
if (!pages) {
|
||||
return BigInt(0);
|
||||
@@ -189,7 +239,7 @@ export class OrganizationSelectorComponent {
|
||||
await this.router.navigate(['/org']);
|
||||
}
|
||||
|
||||
protected trackOrg(_: number, { id }: Organization): string {
|
||||
protected trackOrgResponse(_: number, { id }: Organization): string {
|
||||
return id;
|
||||
}
|
||||
|
||||
|
@@ -112,20 +112,23 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
filter(Boolean),
|
||||
);
|
||||
|
||||
effect(() => {
|
||||
const user = this.user.data();
|
||||
if (!user || user.type.case !== 'human') {
|
||||
return;
|
||||
}
|
||||
effect(
|
||||
() => {
|
||||
const user = this.user.data();
|
||||
if (!user || user.type.case !== 'human') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.breadcrumbService.setBreadcrumb([
|
||||
new Breadcrumb({
|
||||
type: BreadcrumbType.AUTHUSER,
|
||||
name: user.type.value.profile?.displayName,
|
||||
routerLink: ['/users', 'me'],
|
||||
}),
|
||||
]);
|
||||
});
|
||||
this.breadcrumbService.setBreadcrumb([
|
||||
new Breadcrumb({
|
||||
type: BreadcrumbType.AUTHUSER,
|
||||
name: user.type.value.profile?.displayName,
|
||||
routerLink: ['/users', 'me'],
|
||||
}),
|
||||
]);
|
||||
},
|
||||
{ allowSignalWrites: true },
|
||||
);
|
||||
|
||||
effect(() => {
|
||||
const error = this.user.error();
|
||||
|
@@ -121,7 +121,6 @@ export class GrpcAuthService {
|
||||
PrivacyPolicy.AsObject | undefined
|
||||
>(undefined);
|
||||
|
||||
public cachedOrgs: BehaviorSubject<Org.AsObject[]> = new BehaviorSubject<Org.AsObject[]>([]);
|
||||
private cachedLabelPolicies: { [orgId: string]: LabelPolicy.AsObject } = {};
|
||||
private cachedPrivacyPolicies: { [orgId: string]: PrivacyPolicy.AsObject } = {};
|
||||
|
||||
@@ -272,11 +271,6 @@ export class GrpcAuthService {
|
||||
return this.grpcService.auth.getMyUser(new GetMyUserRequest(), null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public async revalidateOrgs() {
|
||||
const orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0)).resultList;
|
||||
this.cachedOrgs.next(orgs);
|
||||
}
|
||||
|
||||
public listMyProjectOrgs(
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { computed, Injectable, Signal, signal } from '@angular/core';
|
||||
import { GrpcService } from './grpc.service';
|
||||
import {
|
||||
AddHumanUserRequestSchema,
|
||||
@@ -48,8 +48,7 @@ import {
|
||||
import type { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { create } from '@bufbuild/protobuf';
|
||||
import { OAuthService } from 'angular-oauth2-oidc';
|
||||
import { EMPTY, of, switchMap } from 'rxjs';
|
||||
import { filter, map, startWith } from 'rxjs/operators';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { injectQuery, queryOptions, skipToken } from '@tanstack/angular-query-experimental';
|
||||
|
||||
@@ -57,7 +56,9 @@ import { injectQuery, queryOptions, skipToken } from '@tanstack/angular-query-ex
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UserService {
|
||||
private userId = this.getUserId();
|
||||
private readonly payload: Signal<unknown | undefined>;
|
||||
private readonly userId: Signal<string | undefined>;
|
||||
public readonly exp: Signal<Date | undefined>;
|
||||
|
||||
public userQuery() {
|
||||
return injectQuery(() => this.userQueryOptions());
|
||||
@@ -74,35 +75,51 @@ export class UserService {
|
||||
constructor(
|
||||
private readonly grpcService: GrpcService,
|
||||
private readonly oauthService: OAuthService,
|
||||
) {}
|
||||
) {
|
||||
this.payload = this.getPayload();
|
||||
this.userId = this.getUserId(this.payload);
|
||||
this.exp = this.getExp(this.payload);
|
||||
}
|
||||
|
||||
private getUserId() {
|
||||
const userId$ = this.oauthService.events.pipe(
|
||||
private getPayload() {
|
||||
const idToken$ = this.oauthService.events.pipe(
|
||||
filter((event) => event.type === 'token_received'),
|
||||
// can actually return null
|
||||
// https://github.com/manfredsteyer/angular-oauth2-oidc/blob/c724ad73eadbb28338b084e3afa5ed49a0ea058c/projects/lib/src/oauth-service.ts#L2365
|
||||
map(() => this.oauthService.getIdToken() as string | null),
|
||||
startWith(this.oauthService.getIdToken() as string | null),
|
||||
filter(Boolean),
|
||||
switchMap((token) => {
|
||||
// we do this in a try catch so the observable will retry this logic if it fails
|
||||
try {
|
||||
// split jwt and get base64 encoded payload
|
||||
const unparsedPayload = atob(token.split('.')[1]);
|
||||
// parse payload
|
||||
const payload: unknown = JSON.parse(unparsedPayload);
|
||||
// check if sub is in payload and is a string
|
||||
if (payload && typeof payload === 'object' && 'sub' in payload && typeof payload.sub === 'string') {
|
||||
return of(payload.sub);
|
||||
}
|
||||
return EMPTY;
|
||||
} catch {
|
||||
return EMPTY;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const idToken = toSignal(idToken$, { initialValue: this.oauthService.getIdToken() as string | null });
|
||||
|
||||
return toSignal(userId$, { initialValue: undefined });
|
||||
return computed(() => {
|
||||
try {
|
||||
// split jwt and get base64 encoded payload
|
||||
const unparsedPayload = atob((idToken() ?? '').split('.')[1]);
|
||||
// parse payload
|
||||
return JSON.parse(unparsedPayload) as unknown;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getUserId(payloadSignal: Signal<unknown | undefined>) {
|
||||
return computed(() => {
|
||||
const payload = payloadSignal();
|
||||
if (payload && typeof payload === 'object' && 'sub' in payload && typeof payload.sub === 'string') {
|
||||
return payload.sub;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
private getExp(payloadSignal: Signal<unknown | undefined>) {
|
||||
return computed(() => {
|
||||
const payload = payloadSignal();
|
||||
if (payload && typeof payload === 'object' && 'exp' in payload && typeof payload.exp === 'number') {
|
||||
return new Date(payload.exp * 1000);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
public addHumanUser(req: MessageInitShape<typeof AddHumanUserRequestSchema>): Promise<AddHumanUserResponse> {
|
||||
|
@@ -81,6 +81,7 @@
|
||||
@import 'src/app/modules/new-header/organization-selector/organization-selector.component.scss';
|
||||
@import 'src/app/modules/new-header/instance-selector/instance-selector.component.scss';
|
||||
@import 'src/app/modules/new-header/header-dropdown/header-dropdown.component.scss';
|
||||
@import 'src/app/modules/new-header/header-button/header-button.component.scss';
|
||||
|
||||
@mixin component-themes($theme) {
|
||||
@include cnsl-color-theme($theme);
|
||||
@@ -165,4 +166,5 @@
|
||||
@include organization-selector-theme($theme);
|
||||
@include instance-selector-theme($theme);
|
||||
@include header-dropdown-theme($theme);
|
||||
@include header-button-theme($theme);
|
||||
}
|
||||
|
Reference in New Issue
Block a user