local changes

This commit is contained in:
conblem
2025-07-08 15:15:49 +02:00
parent 5cb108efba
commit 17ea1b3868
12 changed files with 182 additions and 77 deletions

View File

@@ -1,7 +1,7 @@
<ng-container *ngIf="['iam.read$', 'iam.write$'] | hasRole as iamuser$"> <ng-container *ngIf="['iam.read$', 'iam.write$'] | hasRole as iamuser$">
<div class="nav-col" [ngClass]="{ 'is-admin': (iamuser$ | async) }"> <div class="nav-col" [ngClass]="{ 'is-admin': (iamuser$ | async) }">
<ng-container <ng-container
*ngIf="breadcrumbService.breadcrumbsExtended$ && (breadcrumbService.breadcrumbsExtended$ | async) as breadc" *ngIf="breadcrumbService.breadcrumbsExtended$ | async as breadc"
> >
<ng-container <ng-container
*ngIf=" *ngIf="
@@ -96,12 +96,7 @@
[routerLinkActive]="['active']" [routerLinkActive]="['active']"
[routerLinkActiveOptions]="{ exact: false }" [routerLinkActiveOptions]="{ exact: false }"
[routerLink]="['/org-settings']" [routerLink]="['/org-settings']"
*ngIf=" *ngIf="['policy.read'] | hasRole | async"
(['policy.read'] | hasRole | async) &&
((['iam.read$', 'iam.write$'] | hasRole | async) === false ||
(((authService.cachedOrgs | async)?.length ?? 1) > 1 &&
(['iam.read$', 'iam.write$'] | hasRole | async)))
"
> >
<span class="label">{{ 'MENU.SETTINGS' | translate }}</span> <span class="label">{{ 'MENU.SETTINGS' | translate }}</span>
</a> </a>

View File

@@ -1,6 +1,6 @@
<button class="header-button" cnslInput> <span class="header-text">
<ng-content></ng-content> <ng-content></ng-content>
<div class="cnsl-action-button"> </span>
<ng-icon size="1.2rem" name="heroChevronUpDown"></ng-icon> <button class="header-button" matRipple [matRippleUnbounded]="false">
</div> <ng-icon size="1.2rem" name="heroChevronUpDown"></ng-icon>
</button> </button>

View File

@@ -1,5 +1,4 @@
.header-button { :host {
width: unset;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@@ -8,4 +7,23 @@
padding-right: 0; padding-right: 0;
height: 32px; height: 32px;
max-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);
}
} }

View File

@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { NgIconComponent, provideIcons } from '@ng-icons/core'; import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { heroChevronUpDown } from '@ng-icons/heroicons/outline'; import { heroChevronUpDown } from '@ng-icons/heroicons/outline';
import { MatRippleModule } from '@angular/material/core';
@Component({ @Component({
selector: 'cnsl-header-button', selector: 'cnsl-header-button',
@@ -8,7 +9,7 @@ import { heroChevronUpDown } from '@ng-icons/heroicons/outline';
styleUrls: ['./header-button.component.scss'], styleUrls: ['./header-button.component.scss'],
standalone: true, standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgIconComponent], imports: [NgIconComponent, MatRippleModule],
providers: [provideIcons({ heroChevronUpDown })], providers: [provideIcons({ heroChevronUpDown })],
}) })
export class HeaderButtonComponent {} export class HeaderButtonComponent {}

View File

@@ -1,12 +1,12 @@
<div class="new-header-wrapper"> <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 *ngIf="myInstanceQuery.data()?.instance as instance">
<ng-container *ngTemplateOutlet="slash"></ng-container> <ng-container *ngTemplateOutlet="slash"></ng-container>
<cnsl-header-button <cnsl-header-button
cdkOverlayOrigin cdkOverlayOrigin
#instanceTrigger="cdkOverlayOrigin" #instanceTrigger="cdkOverlayOrigin"
(click)="isInstanceDropdownOpen.set(!isInstanceDropdownOpen())" (click)="isInstanceDropdownOpen.set(!isInstanceDropdownOpen())"
>Instance</cnsl-header-button >{{ instance.name }}</cnsl-header-button
> >
<cnsl-header-dropdown <cnsl-header-dropdown
[trigger]="instanceTrigger" [trigger]="instanceTrigger"
@@ -40,6 +40,14 @@
<cnsl-organization-selector (orgChanged)="isOrgDropdownOpen.set(false)"></cnsl-organization-selector> <cnsl-organization-selector (orgChanged)="isOrgDropdownOpen.set(false)"></cnsl-organization-selector>
</cnsl-header-dropdown> </cnsl-header-dropdown>
</ng-container> </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> </div>
<ng-template #slash> <ng-template #slash>

View File

@@ -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 { MatToolbarModule } from '@angular/material/toolbar';
import { NewOrganizationService } from '../../services/new-organization.service'; import { NewOrganizationService } from '../../services/new-organization.service';
import { ToastService } from '../../services/toast.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 { injectQuery } from '@tanstack/angular-query-experimental';
import { OrganizationSelectorComponent } from './organization-selector/organization-selector.component'; import { OrganizationSelectorComponent } from './organization-selector/organization-selector.component';
import { CdkOverlayOrigin } from '@angular/cdk/overlay'; import { CdkOverlayOrigin } from '@angular/cdk/overlay';
@@ -18,6 +18,8 @@ import { BreakpointObserver } from '@angular/cdk/layout';
import { NewAdminService } from '../../services/new-admin.service'; import { NewAdminService } from '../../services/new-admin.service';
import { NewAuthService } from '../../services/new-auth.service'; import { NewAuthService } from '../../services/new-auth.service';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from '../../services/breadcrumb.service';
import { MatRippleModule } from '@angular/material/core';
@Component({ @Component({
selector: 'cnsl-new-header', selector: 'cnsl-new-header',
@@ -39,6 +41,8 @@ import { RouterLink } from '@angular/router';
AsyncPipe, AsyncPipe,
HasRolePipeModule, HasRolePipeModule,
RouterLink, RouterLink,
NgForOf,
MatRippleModule,
], ],
}) })
export class NewHeaderComponent { export class NewHeaderComponent {
@@ -53,6 +57,7 @@ export class NewHeaderComponent {
protected readonly instanceSelectorSecondStep = signal(false); protected readonly instanceSelectorSecondStep = signal(false);
protected readonly activeOrganizationQuery = this.newOrganizationService.activeOrganizationQuery(); protected readonly activeOrganizationQuery = this.newOrganizationService.activeOrganizationQuery();
protected readonly isHandset: Signal<boolean>; protected readonly isHandset: Signal<boolean>;
protected readonly breadcrumbs: Signal<Breadcrumb[]>;
constructor( constructor(
private readonly newOrganizationService: NewOrganizationService, private readonly newOrganizationService: NewOrganizationService,
@@ -60,8 +65,10 @@ export class NewHeaderComponent {
private readonly breakpointObserver: BreakpointObserver, private readonly breakpointObserver: BreakpointObserver,
private readonly adminService: NewAdminService, private readonly adminService: NewAdminService,
private readonly newAuthService: NewAuthService, private readonly newAuthService: NewAuthService,
private readonly breadcrumbService: BreadcrumbService,
) { ) {
this.isHandset = this.getIsHandset(); this.isHandset = this.getIsHandset();
this.breadcrumbs = this.getBreadcrumbs();
effect(() => { effect(() => {
if (this.listMyZitadelPermissionsQuery.isError()) { if (this.listMyZitadelPermissionsQuery.isError()) {
@@ -87,4 +94,13 @@ export class NewHeaderComponent {
const isHandset$ = this.breakpointObserver.observe(mediaQuery).pipe(map(({ matches }) => matches)); const isHandset$ = this.breakpointObserver.observe(mediaQuery).pipe(map(({ matches }) => matches));
return toSignal(isHandset$, { initialValue: this.breakpointObserver.isMatched(mediaQuery) }); 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,
),
);
}
} }

View File

@@ -24,21 +24,22 @@
{{ org.name }} {{ org.name }}
<ng-icon name="heroCheck"></ng-icon> <ng-icon name="heroCheck"></ng-icon>
</a> </a>
<ng-container *ngFor="let page of organizationsQuery.data()?.pages; last as lastPage"> <ng-container *ngIf="organizationsQuery.data() as data">
<ng-container *ngFor="let org of page.result; trackBy: trackOrg"> <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)"> <a *ngIf="org.id !== activeOrg.data()?.id" class="dropdown-button" mat-button (click)="changeOrg(org.id)">
{{ org.name }} {{ org.name }}
</a> </a>
</ng-container> </ng-container>
<ng-container *ngIf="lastPage && page.details?.totalResult as totalResult"> <ng-container *ngIf="data?.totalResult as totalResult">
<button <button
#moreButton
class="dropdown-button" class="dropdown-button"
mat-stroked-button mat-stroked-button
*ngIf="totalResult > QUERY_LIMIT" *ngIf="totalResult > QUERY_LIMIT"
(click)="organizationsQuery.fetchNextPage()" (click)="organizationsQuery.fetchNextPage()"
[disabled]="!organizationsQuery.hasNextPage() || organizationsQuery.isFetchingNextPage()" [disabled]="!organizationsQuery.hasNextPage() || organizationsQuery.isFetchingNextPage()"
> >
...{{ totalResult - loadedOrgsCount() }} {{ 'PAGINATOR.MORE' | translate }} ...{{ totalResult - data.orgs.length }} {{ 'PAGINATOR.MORE' | translate }}
</button> </button>
</ng-container> </ng-container>
</ng-container> </ng-container>

View File

@@ -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 { injectInfiniteQuery, injectMutation, keepPreviousData } from '@tanstack/angular-query-experimental';
import { NewOrganizationService } from 'src/app/services/new-organization.service'; import { NewOrganizationService } from 'src/app/services/new-organization.service';
import { NgForOf, NgIf } from '@angular/common'; import { NgForOf, NgIf } from '@angular/common';
@@ -20,13 +33,14 @@ import { Router } from '@angular/router';
import { NgIconComponent, provideIcons } from '@ng-icons/core'; import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { heroCheck, heroMagnifyingGlass } from '@ng-icons/heroicons/outline'; import { heroCheck, heroMagnifyingGlass } from '@ng-icons/heroicons/outline';
import { heroArrowLeftCircleSolid } from '@ng-icons/heroicons/solid'; import { heroArrowLeftCircleSolid } from '@ng-icons/heroicons/solid';
import { UserService } from '../../../services/user.service';
type NameQuery = Extract< type NameQuery = Extract<
NonNullable<MessageInitShape<typeof ListOrganizationsRequestSchema>['queries']>[number]['query'], NonNullable<MessageInitShape<typeof ListOrganizationsRequestSchema>['queries']>[number]['query'],
{ case: 'nameQuery' } { case: 'nameQuery' }
>; >;
const QUERY_LIMIT = 5; const QUERY_LIMIT = 20;
@Component({ @Component({
selector: 'cnsl-organization-selector', selector: 'cnsl-organization-selector',
@@ -58,6 +72,13 @@ export class OrganizationSelectorComponent {
@Output() @Output()
public orgChanged = new EventEmitter<Organization>(); 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(() => ({ protected setOrgId = injectMutation(() => ({
mutationFn: (orgId: string) => this.newOrganizationService.setOrgId(orgId), mutationFn: (orgId: string) => this.newOrganizationService.setOrgId(orgId),
})); }));
@@ -65,7 +86,6 @@ export class OrganizationSelectorComponent {
protected readonly form: ReturnType<typeof this.buildForm>; protected readonly form: ReturnType<typeof this.buildForm>;
private readonly nameQuery: Signal<NameQuery | undefined>; private readonly nameQuery: Signal<NameQuery | undefined>;
protected readonly organizationsQuery: ReturnType<typeof this.getOrganizationsQuery>; protected readonly organizationsQuery: ReturnType<typeof this.getOrganizationsQuery>;
protected loadedOrgsCount: Signal<bigint>;
protected activeOrg = this.newOrganizationService.activeOrganizationQuery(); protected activeOrg = this.newOrganizationService.activeOrganizationQuery();
protected activeOrgIfSearchMatches: Signal<Organization | undefined>; protected activeOrgIfSearchMatches: Signal<Organization | undefined>;
@@ -73,12 +93,13 @@ export class OrganizationSelectorComponent {
private readonly newOrganizationService: NewOrganizationService, private readonly newOrganizationService: NewOrganizationService,
private readonly formBuilder: FormBuilder, private readonly formBuilder: FormBuilder,
private readonly router: Router, private readonly router: Router,
private readonly destroyRef: DestroyRef,
private readonly userService: UserService,
toast: ToastService, toast: ToastService,
) { ) {
this.form = this.buildForm(); this.form = this.buildForm();
this.nameQuery = this.getNameQuery(this.form); this.nameQuery = this.getNameQuery(this.form);
this.organizationsQuery = this.getOrganizationsQuery(this.nameQuery); this.organizationsQuery = this.getOrganizationsQuery(this.nameQuery);
this.loadedOrgsCount = this.getLoadedOrgsCount(this.organizationsQuery);
this.activeOrgIfSearchMatches = this.getActiveOrgIfSearchMatches(this.nameQuery); this.activeOrgIfSearchMatches = this.getActiveOrgIfSearchMatches(this.nameQuery);
effect(() => { effect(() => {
@@ -99,12 +120,38 @@ export class OrganizationSelectorComponent {
effect(() => { effect(() => {
const orgId = newOrganizationService.orgId(); const orgId = newOrganizationService.orgId();
const orgs = this.organizationsQuery.data()?.pages[0]?.result; const orgs = this.organizationsQuery.data()?.orgs;
if (orgId || !orgs || orgs.length === 0) { if (orgId || !orgs || orgs.length === 0) {
return; return;
} }
const _ = newOrganizationService.setOrgId(orgs[0].id); 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() { private buildForm() {
@@ -136,9 +183,12 @@ export class OrganizationSelectorComponent {
private getOrganizationsQuery(nameQuery: Signal<NameQuery | undefined>) { private getOrganizationsQuery(nameQuery: Signal<NameQuery | undefined>) {
return injectInfiniteQuery(() => { return injectInfiniteQuery(() => {
const query = nameQuery(); const query = nameQuery();
const exp = this.userService.exp();
const isExpired = exp ? exp <= new Date() : true;
return { return {
queryKey: ['organization', 'listOrganizationsInfinite', query], queryKey: ['organization', 'listOrganizationsInfinite', query],
queryFn: ({ pageParam, signal }) => this.newOrganizationService.listOrganizations(pageParam, signal), queryFn: ({ pageParam, signal }) => this.newOrganizationService.listOrganizations(pageParam, signal),
enabled: !isExpired,
initialPageParam: { initialPageParam: {
query: { query: {
limit: QUERY_LIMIT, limit: QUERY_LIMIT,
@@ -157,14 +207,14 @@ export class OrganizationSelectorComponent {
}, },
} }
: undefined, : 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[]) { private countLoadedOrgs(pages?: ListOrganizationsResponse[]) {
if (!pages) { if (!pages) {
return BigInt(0); return BigInt(0);
@@ -189,7 +239,7 @@ export class OrganizationSelectorComponent {
await this.router.navigate(['/org']); await this.router.navigate(['/org']);
} }
protected trackOrg(_: number, { id }: Organization): string { protected trackOrgResponse(_: number, { id }: Organization): string {
return id; return id;
} }

View File

@@ -112,20 +112,23 @@ export class AuthUserDetailComponent implements OnInit {
filter(Boolean), filter(Boolean),
); );
effect(() => { effect(
const user = this.user.data(); () => {
if (!user || user.type.case !== 'human') { const user = this.user.data();
return; if (!user || user.type.case !== 'human') {
} return;
}
this.breadcrumbService.setBreadcrumb([ this.breadcrumbService.setBreadcrumb([
new Breadcrumb({ new Breadcrumb({
type: BreadcrumbType.AUTHUSER, type: BreadcrumbType.AUTHUSER,
name: user.type.value.profile?.displayName, name: user.type.value.profile?.displayName,
routerLink: ['/users', 'me'], routerLink: ['/users', 'me'],
}), }),
]); ]);
}); },
{ allowSignalWrites: true },
);
effect(() => { effect(() => {
const error = this.user.error(); const error = this.user.error();

View File

@@ -121,7 +121,6 @@ export class GrpcAuthService {
PrivacyPolicy.AsObject | undefined PrivacyPolicy.AsObject | undefined
>(undefined); >(undefined);
public cachedOrgs: BehaviorSubject<Org.AsObject[]> = new BehaviorSubject<Org.AsObject[]>([]);
private cachedLabelPolicies: { [orgId: string]: LabelPolicy.AsObject } = {}; private cachedLabelPolicies: { [orgId: string]: LabelPolicy.AsObject } = {};
private cachedPrivacyPolicies: { [orgId: string]: PrivacyPolicy.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()); 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( public listMyProjectOrgs(
limit?: number, limit?: number,
offset?: number, offset?: number,

View File

@@ -1,4 +1,4 @@
import { Injectable, signal } from '@angular/core'; import { computed, Injectable, Signal, signal } from '@angular/core';
import { GrpcService } from './grpc.service'; import { GrpcService } from './grpc.service';
import { import {
AddHumanUserRequestSchema, AddHumanUserRequestSchema,
@@ -48,8 +48,7 @@ import {
import type { MessageInitShape } from '@bufbuild/protobuf'; import type { MessageInitShape } from '@bufbuild/protobuf';
import { create } from '@bufbuild/protobuf'; import { create } from '@bufbuild/protobuf';
import { OAuthService } from 'angular-oauth2-oidc'; import { OAuthService } from 'angular-oauth2-oidc';
import { EMPTY, of, switchMap } from 'rxjs'; import { filter, map } from 'rxjs/operators';
import { filter, map, startWith } from 'rxjs/operators';
import { toSignal } from '@angular/core/rxjs-interop'; import { toSignal } from '@angular/core/rxjs-interop';
import { injectQuery, queryOptions, skipToken } from '@tanstack/angular-query-experimental'; import { injectQuery, queryOptions, skipToken } from '@tanstack/angular-query-experimental';
@@ -57,7 +56,9 @@ import { injectQuery, queryOptions, skipToken } from '@tanstack/angular-query-ex
providedIn: 'root', providedIn: 'root',
}) })
export class UserService { 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() { public userQuery() {
return injectQuery(() => this.userQueryOptions()); return injectQuery(() => this.userQueryOptions());
@@ -74,35 +75,51 @@ export class UserService {
constructor( constructor(
private readonly grpcService: GrpcService, private readonly grpcService: GrpcService,
private readonly oauthService: OAuthService, private readonly oauthService: OAuthService,
) {} ) {
this.payload = this.getPayload();
this.userId = this.getUserId(this.payload);
this.exp = this.getExp(this.payload);
}
private getUserId() { private getPayload() {
const userId$ = this.oauthService.events.pipe( const idToken$ = this.oauthService.events.pipe(
filter((event) => event.type === 'token_received'), filter((event) => event.type === 'token_received'),
// can actually return null // can actually return null
// https://github.com/manfredsteyer/angular-oauth2-oidc/blob/c724ad73eadbb28338b084e3afa5ed49a0ea058c/projects/lib/src/oauth-service.ts#L2365 // https://github.com/manfredsteyer/angular-oauth2-oidc/blob/c724ad73eadbb28338b084e3afa5ed49a0ea058c/projects/lib/src/oauth-service.ts#L2365
map(() => this.oauthService.getIdToken() as string | null), 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> { public addHumanUser(req: MessageInitShape<typeof AddHumanUserRequestSchema>): Promise<AddHumanUserResponse> {

View File

@@ -81,6 +81,7 @@
@import 'src/app/modules/new-header/organization-selector/organization-selector.component.scss'; @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/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-dropdown/header-dropdown.component.scss';
@import 'src/app/modules/new-header/header-button/header-button.component.scss';
@mixin component-themes($theme) { @mixin component-themes($theme) {
@include cnsl-color-theme($theme); @include cnsl-color-theme($theme);
@@ -165,4 +166,5 @@
@include organization-selector-theme($theme); @include organization-selector-theme($theme);
@include instance-selector-theme($theme); @include instance-selector-theme($theme);
@include header-dropdown-theme($theme); @include header-dropdown-theme($theme);
@include header-button-theme($theme);
} }