mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 20:17:32 +00:00
local changes
This commit is contained in:
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
<button class="header-button" matRipple [matRippleUnbounded]="false">
|
||||||
<ng-icon size="1.2rem" name="heroChevronUpDown"></ng-icon>
|
<ng-icon size="1.2rem" name="heroChevronUpDown"></ng-icon>
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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 {}
|
||||||
|
@@ -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>
|
||||||
|
@@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -112,7 +112,8 @@ export class AuthUserDetailComponent implements OnInit {
|
|||||||
filter(Boolean),
|
filter(Boolean),
|
||||||
);
|
);
|
||||||
|
|
||||||
effect(() => {
|
effect(
|
||||||
|
() => {
|
||||||
const user = this.user.data();
|
const user = this.user.data();
|
||||||
if (!user || user.type.case !== 'human') {
|
if (!user || user.type.case !== 'human') {
|
||||||
return;
|
return;
|
||||||
@@ -125,7 +126,9 @@ export class AuthUserDetailComponent implements OnInit {
|
|||||||
routerLink: ['/users', 'me'],
|
routerLink: ['/users', 'me'],
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
});
|
},
|
||||||
|
{ allowSignalWrites: true },
|
||||||
|
);
|
||||||
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const error = this.user.error();
|
const error = this.user.error();
|
||||||
|
@@ -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,
|
||||||
|
@@ -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),
|
const idToken = toSignal(idToken$, { initialValue: this.oauthService.getIdToken() as string | null });
|
||||||
switchMap((token) => {
|
|
||||||
// we do this in a try catch so the observable will retry this logic if it fails
|
return computed(() => {
|
||||||
try {
|
try {
|
||||||
// split jwt and get base64 encoded payload
|
// split jwt and get base64 encoded payload
|
||||||
const unparsedPayload = atob(token.split('.')[1]);
|
const unparsedPayload = atob((idToken() ?? '').split('.')[1]);
|
||||||
// parse payload
|
// parse payload
|
||||||
const payload: unknown = JSON.parse(unparsedPayload);
|
return JSON.parse(unparsedPayload) as unknown;
|
||||||
// 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 {
|
} catch {
|
||||||
return EMPTY;
|
return undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return toSignal(userId$, { initialValue: 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> {
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user