mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 15:49:35 +00:00
feat: Instance and Organization breadcrumb
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@ngx-translate/core": "^15.0.0",
|
||||
"@tanstack/angular-query-experimental": "^5.75.4",
|
||||
"@zitadel/client": "1.2.0",
|
||||
"@zitadel/proto": "1.2.0",
|
||||
"angular-oauth2-oidc": "^15.0.1",
|
||||
|
@@ -1,17 +1,17 @@
|
||||
<div class="main-container">
|
||||
<ng-container *ngIf="authService.user | async as user">
|
||||
<cnsl-header
|
||||
[org]="org"
|
||||
[org]="activeOrganizationQuery.data()"
|
||||
[user]="user"
|
||||
[isDarkTheme]="componentCssClass === 'dark-theme'"
|
||||
(changedActiveOrg)="changedOrg($event)"
|
||||
></cnsl-header>
|
||||
(changedActiveOrg)="changedOrg()"
|
||||
/>
|
||||
|
||||
<cnsl-nav
|
||||
id="mainnav"
|
||||
class="nav"
|
||||
[ngClass]="{ shadow: yoffset > 60 }"
|
||||
[org]="org"
|
||||
[org]="activeOrganizationQuery.data()"
|
||||
[user]="user"
|
||||
[isDarkTheme]="componentCssClass === 'dark-theme'"
|
||||
></cnsl-nav>
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||
import { DOCUMENT, ViewportScroller } from '@angular/common';
|
||||
import { Component, DestroyRef, HostBinding, HostListener, Inject, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { Component, DestroyRef, effect, HostBinding, HostListener, Inject, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { MatIconRegistry } from '@angular/material/icon';
|
||||
import { MatDrawer } from '@angular/material/sidenav';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
|
||||
import { LangChangeEvent, TranslateService } from '@ngx-translate/core';
|
||||
import { Observable, of, Subject, switchMap } from 'rxjs';
|
||||
import { filter, map, startWith, takeUntil, tap } from 'rxjs/operators';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { filter, map, startWith } from 'rxjs/operators';
|
||||
|
||||
import { accountCard, adminLineAnimation, navAnimations, routeAnimations, toolbarAnimation } from './animations';
|
||||
import { Org } from './proto/generated/zitadel/org_pb';
|
||||
@@ -22,6 +22,7 @@ import { UpdateService } from './services/update.service';
|
||||
import { fallbackLanguage, supportedLanguages, supportedLanguagesRegexp } from './utils/language';
|
||||
import { PosthogService } from './services/posthog.service';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { NewOrganizationService } from './services/new-organization.service';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-root',
|
||||
@@ -42,12 +43,12 @@ export class AppComponent {
|
||||
@HostListener('window:scroll', ['$event']) onScroll(event: Event): void {
|
||||
this.yoffset = this.viewPortScroller.getScrollPosition()[1];
|
||||
}
|
||||
public org!: Org.AsObject;
|
||||
public orgs$: Observable<Org.AsObject[]> = of([]);
|
||||
public showAccount: boolean = false;
|
||||
public isDarkTheme: Observable<boolean> = of(true);
|
||||
|
||||
public showProjectSection: boolean = false;
|
||||
public activeOrganizationQuery = this.newOrganizationService.activeOrganizationQuery();
|
||||
|
||||
public language: string = 'en';
|
||||
public privacyPolicy!: PrivacyPolicy.AsObject;
|
||||
@@ -70,6 +71,7 @@ export class AppComponent {
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
private posthog: PosthogService,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
private readonly newOrganizationService: NewOrganizationService,
|
||||
) {
|
||||
console.log(
|
||||
'%cWait!',
|
||||
@@ -199,9 +201,9 @@ export class AppComponent {
|
||||
|
||||
this.getProjectCount();
|
||||
|
||||
this.authService.activeOrgChanged.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((org) => {
|
||||
if (org) {
|
||||
this.org = org;
|
||||
effect(() => {
|
||||
const orgId = this.newOrganizationService.orgId();
|
||||
if (orgId) {
|
||||
this.getProjectCount();
|
||||
}
|
||||
});
|
||||
@@ -212,22 +214,23 @@ export class AppComponent {
|
||||
filter(Boolean),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe((org) => this.authService.getActiveOrg(org));
|
||||
|
||||
this.authenticationService.authenticationChanged
|
||||
.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() => this.authService.getActiveOrg()),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe({
|
||||
next: (org) => (this.org = org),
|
||||
error: async (err) => {
|
||||
console.error(err);
|
||||
return this.router.navigate(['/users/me']);
|
||||
},
|
||||
});
|
||||
.subscribe((orgId) => this.newOrganizationService.setOrgId(orgId));
|
||||
|
||||
// todo: think about this one
|
||||
// this.authenticationService.authenticationChanged
|
||||
// .pipe(
|
||||
// filter(Boolean),
|
||||
// switchMap(() => this.authService.getActiveOrg()),
|
||||
// takeUntilDestroyed(this.destroyRef),
|
||||
// )
|
||||
// .subscribe({
|
||||
// next: (org) => (this.org = org),
|
||||
// error: async (err) => {
|
||||
// console.error(err);
|
||||
// return this.router.navigate(['/users/me']);
|
||||
// },
|
||||
// });
|
||||
//
|
||||
this.isDarkTheme = this.themeService.isDarkTheme;
|
||||
this.isDarkTheme.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((dark) => {
|
||||
const theme = dark ? 'dark-theme' : 'light-theme';
|
||||
@@ -266,7 +269,7 @@ export class AppComponent {
|
||||
this.componentCssClass = theme;
|
||||
}
|
||||
|
||||
public changedOrg(org: Org.AsObject): void {
|
||||
public changedOrg(): void {
|
||||
// Reference: https://stackoverflow.com/a/58114797
|
||||
const currentUrl = this.router.url;
|
||||
this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => {
|
||||
|
@@ -73,6 +73,9 @@ import { ThemeService } from './services/theme.service';
|
||||
import { ToastService } from './services/toast.service';
|
||||
import { LanguagesService } from './services/languages.service';
|
||||
import { PosthogService } from './services/posthog.service';
|
||||
import { NewHeaderComponent } from './modules/new-header/new-header.component';
|
||||
import { provideTanStackQuery, QueryClient, withDevtools } from '@tanstack/angular-query-experimental';
|
||||
import { CdkOverlayOrigin } from '@angular/cdk/overlay';
|
||||
|
||||
registerLocaleData(localeDe);
|
||||
i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/de.json'));
|
||||
@@ -168,6 +171,8 @@ const authConfig: AuthConfig = {
|
||||
MatDialogModule,
|
||||
KeyboardShortcutsModule,
|
||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: false }),
|
||||
NewHeaderComponent,
|
||||
CdkOverlayOrigin,
|
||||
],
|
||||
providers: [
|
||||
ThemeService,
|
||||
@@ -242,8 +247,13 @@ const authConfig: AuthConfig = {
|
||||
LanguagesService,
|
||||
PosthogService,
|
||||
{ provide: 'windowObject', useValue: window },
|
||||
provideTanStackQuery(
|
||||
new QueryClient(),
|
||||
withDevtools(() => ({ loadDevtools: 'auto' })),
|
||||
),
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
exports: [],
|
||||
})
|
||||
export class AppModule {
|
||||
constructor() {}
|
||||
|
@@ -21,7 +21,6 @@ import { ToastService } from 'src/app/services/toast.service';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { InputModule } from 'src/app/modules/input/input.module';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module';
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { KeyValue } from '@angular/common';
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Component, DestroyRef, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
|
||||
import { BehaviorSubject, from, Observable, of, Subject } from 'rxjs';
|
||||
import { catchError, debounceTime, scan, take, takeUntil, tap } from 'rxjs/operators';
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from 'src/app/proto/generated/zitadel/management_pb';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
export enum ChangeType {
|
||||
MYUSER = 'myuser',
|
||||
@@ -45,17 +46,18 @@ type ListChanges =
|
||||
| ListOrgChangesResponse.AsObject
|
||||
| ListAppChangesResponse.AsObject;
|
||||
|
||||
// todo: update this component to react to input changes
|
||||
@Component({
|
||||
selector: 'cnsl-changes',
|
||||
templateUrl: './changes.component.html',
|
||||
styleUrls: ['./changes.component.scss'],
|
||||
})
|
||||
export class ChangesComponent implements OnInit, OnDestroy {
|
||||
@Input() public changeType: ChangeType = ChangeType.USER;
|
||||
export class ChangesComponent implements OnInit {
|
||||
@Input({ required: true }) public changeType!: ChangeType;
|
||||
@Input() public id: string = '';
|
||||
@Input() public secId: string = '';
|
||||
@Input() public sortDirectionAsc: boolean = true;
|
||||
@Input() public refresh!: Observable<void>;
|
||||
@Input() public refresh?: Observable<void>;
|
||||
public bottom: boolean = false;
|
||||
|
||||
private _done: BehaviorSubject<any> = new BehaviorSubject(false);
|
||||
@@ -65,30 +67,26 @@ export class ChangesComponent implements OnInit, OnDestroy {
|
||||
loading: Observable<boolean> = this._loading.asObservable();
|
||||
public data: Observable<MappedChange[]> = this._data.asObservable().pipe(
|
||||
scan((acc, val) => {
|
||||
return false ? val.concat(acc) : acc.concat(val);
|
||||
return acc.concat(val);
|
||||
}),
|
||||
);
|
||||
public changes!: ListChanges;
|
||||
private destroyed$: Subject<void> = new Subject();
|
||||
constructor(
|
||||
private mgmtUserService: ManagementService,
|
||||
private authUserService: GrpcAuthService,
|
||||
private readonly mgmtUserService: ManagementService,
|
||||
private readonly authUserService: GrpcAuthService,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.init();
|
||||
if (this.refresh) {
|
||||
this.refresh.pipe(takeUntil(this.destroyed$), debounceTime(2000)).subscribe(() => {
|
||||
this.refresh.pipe(takeUntilDestroyed(this.destroyRef), debounceTime(2000)).subscribe(() => {
|
||||
this._data = new BehaviorSubject([]);
|
||||
this.init();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
let first: Promise<ListChanges>;
|
||||
switch (this.changeType) {
|
||||
|
@@ -37,134 +37,8 @@
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngFor="let bread of breadcrumbService.breadcrumbs$ | async as bc; index as i">
|
||||
<ng-container *ngIf="bread.type === BreadcrumbType.INSTANCE">
|
||||
<ng-template cnslHasRole [hasRole]="['iam.read']">
|
||||
<svg
|
||||
class="slash hide-on-small"
|
||||
viewBox="0 0 24 24"
|
||||
width="32"
|
||||
height="32"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
shape-rendering="geometricPrecision"
|
||||
>
|
||||
<path d="M16.88 3.549L7.12 20.451"></path>
|
||||
</svg>
|
||||
|
||||
<div class="breadcrumb-context hide-on-small">
|
||||
<a matRipple [matRippleUnbounded]="false" class="breadcrumb-link" [routerLink]="bread.routerLink">
|
||||
{{ 'MENU.INSTANCE' | translate }}
|
||||
</a>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="bread.type === BreadcrumbType.ORG">
|
||||
<svg
|
||||
class="slash"
|
||||
viewBox="0 0 24 24"
|
||||
width="32"
|
||||
height="32"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
shape-rendering="geometricPrecision"
|
||||
>
|
||||
<path d="M16.88 3.549L7.12 20.451"></path>
|
||||
</svg>
|
||||
|
||||
<div class="org-context">
|
||||
<a *ngIf="org" matRipple [matRippleUnbounded]="false" class="org-link" id="orglink" [routerLink]="['/org']">
|
||||
{{ org.name ? org.name : 'NO NAME' }}</a
|
||||
>
|
||||
|
||||
<div class="org-context-wrapper" *ngIf="org">
|
||||
<button
|
||||
cdkOverlayOrigin
|
||||
#trigger="cdkOverlayOrigin"
|
||||
matRipple
|
||||
[matRippleUnbounded]="false"
|
||||
id="orgswitchbutton"
|
||||
class="org-switch-button"
|
||||
(click)="showOrgContext = !showOrgContext"
|
||||
>
|
||||
<span class="svgspan">
|
||||
<svg xmlns=" http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
<cnsl-action-keys
|
||||
(actionTriggered)="showOrgContext = !showOrgContext"
|
||||
[type]="ActionKeysType.ORGSWITCHER"
|
||||
></cnsl-action-keys>
|
||||
</button>
|
||||
|
||||
<ng-template
|
||||
cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="trigger"
|
||||
[cdkConnectedOverlayOffsetY]="10"
|
||||
[cdkConnectedOverlayHasBackdrop]="true"
|
||||
[cdkConnectedOverlayPositions]="positions"
|
||||
cdkConnectedOverlayBackdropClass="transparent-backdrop"
|
||||
[cdkConnectedOverlayOpen]="showOrgContext"
|
||||
(backdropClick)="showOrgContext = false"
|
||||
(detach)="showOrgContext = false"
|
||||
>
|
||||
<cnsl-org-context
|
||||
class="context_card"
|
||||
*ngIf="org && showOrgContext"
|
||||
(closedCard)="showOrgContext = false"
|
||||
[org]="org"
|
||||
(setOrg)="setActiveOrg($event)"
|
||||
>
|
||||
</cnsl-org-context>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="bread.type !== BreadcrumbType.INSTANCE && bread.type !== BreadcrumbType.ORG">
|
||||
<svg
|
||||
class="slash"
|
||||
viewBox="0 0 24 24"
|
||||
width="32"
|
||||
height="32"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
shape-rendering="geometricPrecision"
|
||||
>
|
||||
<path d="M16.88 3.549L7.12 20.451"></path>
|
||||
</svg>
|
||||
|
||||
<div class="breadcrumb-context">
|
||||
<a
|
||||
matRipple
|
||||
[matRippleUnbounded]="false"
|
||||
class="breadcrumb-link"
|
||||
[ngClass]="{ maxwidth: bc.length > 1 }"
|
||||
[routerLink]="bread.routerLink"
|
||||
>
|
||||
<ng-container *ngIf="i !== bc.length - 1; else defLabel">
|
||||
<span class="desk">{{ bread.name }}</span>
|
||||
<span class="mob">...</span>
|
||||
</ng-container>
|
||||
<ng-template #defLabel>
|
||||
<span>{{ bread.name }}</span>
|
||||
</ng-template>
|
||||
</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<cnsl-new-header></cnsl-new-header>
|
||||
|
||||
<span class="fill-space"></span>
|
||||
|
||||
@@ -178,33 +52,6 @@
|
||||
</a>
|
||||
</ng-container>
|
||||
|
||||
<div class="system-rel" *ngIf="!isOnMe">
|
||||
<a
|
||||
id="systembutton"
|
||||
*ngIf="!isOnInstance && (['iam.read$', 'iam.write$'] | hasRole | async)"
|
||||
[routerLink]="['/instance']"
|
||||
class="iam-settings"
|
||||
mat-stroked-button
|
||||
>
|
||||
<div class="cnsl-action-button">
|
||||
<span class="iam-label">{{ 'MENU.INSTANCE' | translate }}</span>
|
||||
<i class="las la-cog"></i>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
id="orgbutton"
|
||||
*ngIf="isOnInstance && (['org.read'] | hasRole | async)"
|
||||
[routerLink]="['/org']"
|
||||
class="org-settings"
|
||||
mat-stroked-button
|
||||
>
|
||||
<div class="cnsl-action-button">
|
||||
<span class="iam-label">{{ 'MENU.ORGANIZATION' | translate }}</span>
|
||||
<i class="las la-cog"></i>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="user && user.id">
|
||||
<div class="account-card-wrapper">
|
||||
<button
|
||||
|
@@ -1,12 +1,13 @@
|
||||
import { ConnectedPosition, ConnectionPositionPair } from '@angular/cdk/overlay';
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import { User } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ActionKeysType } from '../action-keys/action-keys.component';
|
||||
import { NewOrganizationService } from '../../services/new-organization.service';
|
||||
import { Organization } from '@zitadel/proto/zitadel/org/v2/org_pb';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-header',
|
||||
@@ -18,11 +19,11 @@ export class HeaderComponent {
|
||||
@Input({ required: true }) public user!: User.AsObject;
|
||||
public showOrgContext: boolean = false;
|
||||
|
||||
@Input() public org!: Org.AsObject;
|
||||
@Output() public changedActiveOrg: EventEmitter<Org.AsObject> = new EventEmitter();
|
||||
@Input() public org?: Organization | null;
|
||||
@Output() public changedActiveOrg = new EventEmitter<void>();
|
||||
public showAccount: boolean = false;
|
||||
public BreadcrumbType: any = BreadcrumbType;
|
||||
public ActionKeysType: any = ActionKeysType;
|
||||
protected readonly BreadcrumbType = BreadcrumbType;
|
||||
protected readonly ActionKeysType = ActionKeysType;
|
||||
|
||||
public positions: ConnectedPosition[] = [
|
||||
new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'top' }, 0, 10),
|
||||
@@ -38,12 +39,12 @@ export class HeaderComponent {
|
||||
public mgmtService: ManagementService,
|
||||
public breadcrumbService: BreadcrumbService,
|
||||
public router: Router,
|
||||
private readonly newOrganizationService: NewOrganizationService,
|
||||
) {}
|
||||
|
||||
public setActiveOrg(org: Org.AsObject): void {
|
||||
this.org = org;
|
||||
this.authService.setActiveOrg(org);
|
||||
this.changedActiveOrg.emit(org);
|
||||
public async setActiveOrg(orgId: string): Promise<void> {
|
||||
await this.newOrganizationService.setOrgId(orgId);
|
||||
this.changedActiveOrg.emit();
|
||||
}
|
||||
|
||||
public get isOnMe(): boolean {
|
||||
|
@@ -17,6 +17,7 @@ import { ActionKeysModule } from '../action-keys/action-keys.module';
|
||||
import { AvatarModule } from '../avatar/avatar.module';
|
||||
import { OrgContextModule } from '../org-context/org-context.module';
|
||||
import { HeaderComponent } from './header.component';
|
||||
import { NewHeaderComponent } from '../new-header/new-header.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [HeaderComponent],
|
||||
@@ -38,6 +39,7 @@ import { HeaderComponent } from './header.component';
|
||||
AvatarModule,
|
||||
AccountsCardModule,
|
||||
HasRolePipeModule,
|
||||
NewHeaderComponent,
|
||||
],
|
||||
exports: [HeaderComponent],
|
||||
})
|
||||
|
@@ -56,8 +56,8 @@
|
||||
*ngIf="instance?.state"
|
||||
class="state"
|
||||
[ngClass]="{
|
||||
active: instance.state === State.INSTANCE_STATE_RUNNING,
|
||||
inactive: instance.state === State.INSTANCE_STATE_STOPPED || instance.state === State.INSTANCE_STATE_STOPPING,
|
||||
active: instance.state === State.STATE_RUNNING,
|
||||
inactive: instance.state === State.STATE_STOPPED || instance.state === State.STATE_STOPPING,
|
||||
}"
|
||||
>
|
||||
{{ 'IAM.STATE.' + instance.state | translate }}
|
||||
|
@@ -9,6 +9,7 @@ import { User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { User as UserV1 } from '@zitadel/proto/zitadel/user_pb';
|
||||
import { User as UserV2 } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { LoginPolicy as LoginPolicyV2 } from '@zitadel/proto/zitadel/policy_pb';
|
||||
import { Organization as OrgV2 } from '@zitadel/proto/zitadel/org/v2/org_pb';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-info-row',
|
||||
@@ -17,7 +18,7 @@ import { LoginPolicy as LoginPolicyV2 } from '@zitadel/proto/zitadel/policy_pb';
|
||||
})
|
||||
export class InfoRowComponent {
|
||||
@Input() public user?: User.AsObject | UserV2 | UserV1;
|
||||
@Input() public org!: Org.AsObject;
|
||||
@Input() public org!: Org.AsObject | OrgV2;
|
||||
@Input() public instance!: InstanceDetail.AsObject;
|
||||
@Input() public app!: App.AsObject;
|
||||
@Input() public idp!: IDP.AsObject;
|
||||
@@ -25,13 +26,13 @@ export class InfoRowComponent {
|
||||
@Input() public grantedProject!: GrantedProject.AsObject;
|
||||
@Input() public loginPolicy?: LoginPolicy.AsObject | LoginPolicyV2;
|
||||
|
||||
public UserState: any = UserState;
|
||||
public State: any = State;
|
||||
public OrgState: any = OrgState;
|
||||
public AppState: any = AppState;
|
||||
public IDPState: any = IDPState;
|
||||
public ProjectState: any = ProjectState;
|
||||
public ProjectGrantState: any = ProjectGrantState;
|
||||
public UserState = UserState;
|
||||
public State = State;
|
||||
public OrgState = OrgState;
|
||||
public AppState = AppState;
|
||||
public IDPState = IDPState;
|
||||
public ProjectState = ProjectState;
|
||||
public ProjectGrantState = ProjectGrantState;
|
||||
|
||||
public copied: string = '';
|
||||
|
||||
|
@@ -4,19 +4,19 @@ import { MatTable } from '@angular/material/table';
|
||||
import { Router } from '@angular/router';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import { Membership } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { OverlayWorkflowService } from 'src/app/services/overlay/overlay-workflow.service';
|
||||
import { OrgContextChangedWorkflowOverlays } from 'src/app/services/overlay/workflows';
|
||||
import { StorageLocation, StorageService } from 'src/app/services/storage.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { getMembershipColor } from 'src/app/utils/color';
|
||||
|
||||
import { PageEvent, PaginatorComponent } from '../paginator/paginator.component';
|
||||
import { MembershipsDataSource } from './memberships-datasource';
|
||||
import { NewOrganizationService } from '../../services/new-organization.service';
|
||||
import { Organization } from '@zitadel/proto/zitadel/org/v2/org_pb';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-memberships-table',
|
||||
@@ -49,7 +49,7 @@ export class MembershipsTableComponent implements OnInit, OnDestroy {
|
||||
private toast: ToastService,
|
||||
private router: Router,
|
||||
private workflowService: OverlayWorkflowService,
|
||||
private storageService: StorageService,
|
||||
private readonly newOrganizationService: NewOrganizationService,
|
||||
) {
|
||||
this.selection.changed.pipe(takeUntil(this.destroyed)).subscribe((_) => {
|
||||
this.changedSelection.emit(this.selection.selected);
|
||||
@@ -116,53 +116,44 @@ export class MembershipsTableComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
public goto(membership: Membership.AsObject): void {
|
||||
const org: Org.AsObject | null = this.storageService.getItem('organization', StorageLocation.session);
|
||||
public async goto(membership: Membership.AsObject) {
|
||||
const orgId = this.newOrganizationService.orgId();
|
||||
|
||||
if (membership.orgId && !membership.projectId && !membership.projectGrantId) {
|
||||
// only shown on auth user, or if currentOrg === resourceOwner
|
||||
this.authService
|
||||
.getActiveOrg(membership.orgId)
|
||||
.then((membershipOrg) => {
|
||||
this.router.navigate(['/org/members']).then(() => {
|
||||
this.startOrgContextWorkflow(membershipOrg, org);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.toast.showInfo('USER.MEMBERSHIPS.NOPERMISSIONTOEDIT', true);
|
||||
});
|
||||
try {
|
||||
const membershipOrg = await this.newOrganizationService.setOrgId(membership.orgId);
|
||||
await this.router.navigate(['/org/members']);
|
||||
this.startOrgContextWorkflow(membershipOrg, orgId);
|
||||
} catch (error) {
|
||||
this.toast.showInfo('USER.MEMBERSHIPS.NOPERMISSIONTOEDIT', true);
|
||||
}
|
||||
} else if (membership.projectGrantId && membership.details?.resourceOwner) {
|
||||
// only shown on auth user
|
||||
this.authService
|
||||
.getActiveOrg(membership.details?.resourceOwner)
|
||||
.then((membershipOrg) => {
|
||||
this.router.navigate(['/granted-projects', membership.projectId, 'grants', membership.projectGrantId]).then(() => {
|
||||
this.startOrgContextWorkflow(membershipOrg, org);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.toast.showInfo('USER.MEMBERSHIPS.NOPERMISSIONTOEDIT', true);
|
||||
});
|
||||
try {
|
||||
const membershipOrg = await this.newOrganizationService.setOrgId(membership.details?.resourceOwner);
|
||||
await this.router.navigate(['/granted-projects', membership.projectId, 'grants', membership.projectGrantId]);
|
||||
this.startOrgContextWorkflow(membershipOrg, orgId);
|
||||
} catch (error) {
|
||||
this.toast.showInfo('USER.MEMBERSHIPS.NOPERMISSIONTOEDIT', true);
|
||||
}
|
||||
} else if (membership.projectId && membership.details?.resourceOwner) {
|
||||
// only shown on auth user, or if currentOrg === resourceOwner
|
||||
this.authService
|
||||
.getActiveOrg(membership.details?.resourceOwner)
|
||||
.then((membershipOrg) => {
|
||||
this.router.navigate(['/projects', membership.projectId, 'members']).then(() => {
|
||||
this.startOrgContextWorkflow(membershipOrg, org);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.toast.showInfo('USER.MEMBERSHIPS.NOPERMISSIONTOEDIT', true);
|
||||
});
|
||||
try {
|
||||
const membershipOrg = await this.newOrganizationService.setOrgId(membership.details?.resourceOwner);
|
||||
await this.router.navigate(['/projects', membership.projectId, 'members']);
|
||||
this.startOrgContextWorkflow(membershipOrg, orgId);
|
||||
} catch (error) {
|
||||
this.toast.showInfo('USER.MEMBERSHIPS.NOPERMISSIONTOEDIT', true);
|
||||
}
|
||||
} else if (membership.iam) {
|
||||
// only shown on auth user
|
||||
this.router.navigate(['/instance/members']);
|
||||
await this.router.navigate(['/instance/members']);
|
||||
}
|
||||
}
|
||||
|
||||
private startOrgContextWorkflow(membershipOrg: Org.AsObject, currentOrg?: Org.AsObject | null): void {
|
||||
if (!currentOrg || (membershipOrg.id && currentOrg.id && currentOrg.id !== membershipOrg.id)) {
|
||||
private startOrgContextWorkflow(membershipOrg: Organization, currentOrgId?: string | null): void {
|
||||
if (!currentOrgId || (membershipOrg.id && currentOrgId && currentOrgId !== membershipOrg.id)) {
|
||||
setTimeout(() => {
|
||||
this.workflowService.startWorkflow(OrgContextChangedWorkflowOverlays, null);
|
||||
}, 1000);
|
||||
|
@@ -5,16 +5,15 @@ import { Component, ElementRef, Input, OnDestroy, ViewChild } from '@angular/cor
|
||||
import { UntypedFormControl } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { BehaviorSubject, combineLatest, map, Observable, Subject } from 'rxjs';
|
||||
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import { User } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { AuthenticationService } from 'src/app/services/authentication.service';
|
||||
import { BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||
import { EnvironmentService } from 'src/app/services/environment.service';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { KeyboardShortcutsService } from 'src/app/services/keyboard-shortcuts/keyboard-shortcuts.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { StorageLocation, StorageService } from 'src/app/services/storage.service';
|
||||
import { Organization } from '@zitadel/proto/zitadel/org/v2/org_pb';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-nav',
|
||||
@@ -83,7 +82,7 @@ export class NavComponent implements OnDestroy {
|
||||
}),
|
||||
);
|
||||
|
||||
@Input() public org!: Org.AsObject;
|
||||
@Input() public org?: Organization | null;
|
||||
public filterControl: UntypedFormControl = new UntypedFormControl('');
|
||||
public orgLoading$: BehaviorSubject<any> = new BehaviorSubject(false);
|
||||
public showAccount: boolean = false;
|
||||
|
@@ -0,0 +1,6 @@
|
||||
<button class="header-button" cnslInput>
|
||||
<ng-content></ng-content>
|
||||
<div class="cnsl-action-button">
|
||||
<i class="las la-arrows-alt-v"></i>
|
||||
</div>
|
||||
</button>
|
@@ -0,0 +1,11 @@
|
||||
.header-button {
|
||||
width: unset;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
gap: 0.5rem;
|
||||
padding-right: 0;
|
||||
height: 32px;
|
||||
max-height: 32px;
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-header-button',
|
||||
templateUrl: './header-button.component.html',
|
||||
styleUrls: ['./header-button.component.scss'],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class HeaderButtonComponent {}
|
@@ -0,0 +1,15 @@
|
||||
<!-- the cdk overlay doesn't like it's properties being changed that's why we used the ng if to rerender it -->
|
||||
<ng-template
|
||||
*ngIf="isOpen$ | async as isOpen"
|
||||
cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="trigger"
|
||||
[cdkConnectedOverlayOpen]="isOpen"
|
||||
[cdkConnectedOverlayPositionStrategy]="positionStrategy()"
|
||||
[cdkConnectedOverlayScrollStrategy]="scrollStrategy()"
|
||||
[cdkConnectedOverlayHasBackdrop]="isHandset()"
|
||||
(overlayOutsideClick)="closed.emit()"
|
||||
>
|
||||
<div class="dropdown-content">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</ng-template>
|
@@ -0,0 +1,48 @@
|
||||
@mixin header-dropdown-theme($theme) {
|
||||
$foreground: map-get($theme, foreground);
|
||||
$background: map-get($theme, background);
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
|
||||
$border-radius: 1rem;
|
||||
|
||||
.dropdown-content {
|
||||
max-height: 50vh;
|
||||
min-width: 300px;
|
||||
max-width: 80vw;
|
||||
border-radius: $border-radius;
|
||||
border: 1px solid rgba(#8795a1, 0.2);
|
||||
box-shadow: 0 0 15px 0 rgb(0 0 0 / 10%);
|
||||
background: map-get($background, cards);
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 599px) {
|
||||
.dropdown-content {
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-content > :first-child > :first-child {
|
||||
border-top-left-radius: $border-radius;
|
||||
}
|
||||
|
||||
.dropdown-content > :last-child > :first-child {
|
||||
border-top-right-radius: $border-radius;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 599px) {
|
||||
.dropdown-content > :first-child > :last-child {
|
||||
border-bottom-left-radius: $border-radius;
|
||||
}
|
||||
|
||||
.dropdown-content > :last-child > :last-child {
|
||||
border-bottom-right-radius: $border-radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
EventEmitter,
|
||||
Injector,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
runInInjectionContext,
|
||||
Signal,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
CdkConnectedOverlay,
|
||||
CdkOverlayOrigin,
|
||||
FlexibleConnectedPositionStrategy,
|
||||
Overlay,
|
||||
ScrollStrategy,
|
||||
} from '@angular/cdk/overlay';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { AsyncPipe, NgIf } from '@angular/common';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-header-dropdown',
|
||||
templateUrl: './header-dropdown.component.html',
|
||||
styleUrls: ['./header-dropdown.component.scss'],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CdkConnectedOverlay, NgIf, AsyncPipe],
|
||||
})
|
||||
export class HeaderDropdownComponent implements OnInit {
|
||||
@Input({ required: true })
|
||||
public trigger!: CdkOverlayOrigin;
|
||||
|
||||
@Input({ required: true })
|
||||
public set isOpen(isOpen: boolean) {
|
||||
this.isOpen$.next(isOpen);
|
||||
}
|
||||
|
||||
@Output()
|
||||
public closed = new EventEmitter<void>();
|
||||
|
||||
protected readonly isOpen$ = new ReplaySubject<boolean>(1);
|
||||
protected readonly isHandset: Signal<boolean>;
|
||||
protected readonly positionStrategy: Signal<FlexibleConnectedPositionStrategy>;
|
||||
protected readonly scrollStrategy: Signal<ScrollStrategy>;
|
||||
|
||||
constructor(
|
||||
private readonly overlay: Overlay,
|
||||
private readonly breakpointObserver: BreakpointObserver,
|
||||
private readonly injector: Injector,
|
||||
) {
|
||||
this.isHandset = this.getIsHandset();
|
||||
this.positionStrategy = this.getPositionStrategy(this.isHandset);
|
||||
this.scrollStrategy = this.getScrollStrategy(this.isHandset);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// because closeWhenResized accesses the input properties, we need to run it in ngOnInit
|
||||
// this method is used to close the dropdown when the screen is resized
|
||||
// to make sure the dropdown will be rendered in the correct position
|
||||
runInInjectionContext(this.injector, () => {
|
||||
const isOpen = toSignal(this.isOpen$, { requireSync: true });
|
||||
effect(
|
||||
() => {
|
||||
this.isHandset();
|
||||
if (untracked(() => isOpen())) {
|
||||
this.closed.emit();
|
||||
}
|
||||
},
|
||||
{ allowSignalWrites: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private getIsHandset() {
|
||||
const mediaQuery = '(max-width: 599px)';
|
||||
const isHandset$ = this.breakpointObserver.observe(mediaQuery).pipe(map(({ matches }) => matches));
|
||||
return toSignal(isHandset$, { initialValue: this.breakpointObserver.isMatched(mediaQuery) });
|
||||
}
|
||||
|
||||
private getPositionStrategy(isHandset: Signal<boolean>): Signal<FlexibleConnectedPositionStrategy> {
|
||||
return computed(() =>
|
||||
isHandset()
|
||||
? this.overlay
|
||||
.position()
|
||||
.flexibleConnectedTo(document.body)
|
||||
.withPositions([
|
||||
{
|
||||
originX: 'start',
|
||||
originY: 'bottom',
|
||||
overlayX: 'start',
|
||||
overlayY: 'bottom',
|
||||
},
|
||||
])
|
||||
: undefined!,
|
||||
);
|
||||
}
|
||||
|
||||
private getScrollStrategy(isHandset: Signal<boolean>): Signal<ScrollStrategy> {
|
||||
return computed(() => (isHandset() ? this.overlay.scrollStrategies.block() : undefined!));
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
<div class="upper-content">
|
||||
<span class="dropdown-label">{{ 'MENU.INSTANCEOVERVIEW' | translate }}</span>
|
||||
<a
|
||||
(click)="setInstance(instance)"
|
||||
mat-button
|
||||
class="dropdown-button"
|
||||
>{{ instance.name }}
|
||||
<i class="las la-1x la-angle-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<a [routerLink]="['/instance']" (click)="settingsClicked.emit()" mat-button class="dropdown-button settings-button">
|
||||
<h3>{{ 'MENU.SETTINGS' | translate }}</h3>
|
||||
<i class="las la-1x la-cog"></i>
|
||||
</a>
|
||||
</div>
|
@@ -0,0 +1,46 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@mixin instance-selector-theme($theme) {
|
||||
$background: map-get($theme, background);
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
|
||||
.upper-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
background: map-get($background, footer);
|
||||
}
|
||||
|
||||
.dropdown-label {
|
||||
color: if($is-dark-theme, #ffffff60, #00000060);
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dropdown-button {
|
||||
height: 32px;
|
||||
max-height: 32px;
|
||||
}
|
||||
|
||||
.dropdown-button > span:nth-child(2) {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 10px;
|
||||
padding-top: 5px;
|
||||
background: map-get($background, cards);
|
||||
border-bottom-left-radius: inherit;
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Output, Input } from '@angular/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { InstanceDetail } from '@zitadel/proto/zitadel/instance_pb';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-instance-selector',
|
||||
templateUrl: './instance-selector.component.html',
|
||||
styleUrls: ['./instance-selector.component.scss'],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TranslateModule, MatButtonModule, RouterLink],
|
||||
})
|
||||
export class InstanceSelectorComponent {
|
||||
@Output() public instanceChanged = new EventEmitter<string>();
|
||||
@Output() public settingsClicked = new EventEmitter<void>();
|
||||
|
||||
@Input({ required: true })
|
||||
public instance!: InstanceDetail;
|
||||
|
||||
constructor(private readonly router: Router) {}
|
||||
|
||||
protected async setInstance({ id }: InstanceDetail) {
|
||||
this.instanceChanged.emit(id);
|
||||
await this.router.navigate(['/']);
|
||||
}
|
||||
}
|
58
console/src/app/modules/new-header/new-header.component.html
Normal file
58
console/src/app/modules/new-header/new-header.component.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<div class="new-header-wrapper">
|
||||
<ng-container *ngIf="(['iam.read$', 'iam.write$'] | hasRole | async) && 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
|
||||
>
|
||||
<cnsl-header-dropdown
|
||||
[trigger]="instanceTrigger"
|
||||
[isOpen]="isInstanceDropdownOpen()"
|
||||
(closed)="isInstanceDropdownOpen.set(false); instanceSelectorSecondStep.set(false)"
|
||||
>
|
||||
<cnsl-instance-selector
|
||||
*ngIf="!isHandset() || !instanceSelectorSecondStep()"
|
||||
[instance]="instance"
|
||||
(instanceChanged)="instanceSelectorSecondStep.set(true)"
|
||||
(settingsClicked)="isInstanceDropdownOpen.set(false)"
|
||||
></cnsl-instance-selector>
|
||||
<cnsl-organization-selector
|
||||
*ngIf="instanceSelectorSecondStep()"
|
||||
[backButton]="isHandset() ? instance.name : ''"
|
||||
(backButtonPressed)="instanceSelectorSecondStep.set(false)"
|
||||
(orgChanged)="isInstanceDropdownOpen.set(false); instanceSelectorSecondStep.set(false)"
|
||||
></cnsl-organization-selector>
|
||||
</cnsl-header-dropdown>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="activeOrganizationQuery.data() as org">
|
||||
<ng-container *ngTemplateOutlet="slash"></ng-container>
|
||||
<cnsl-header-button
|
||||
cdkOverlayOrigin
|
||||
#orgTrigger="cdkOverlayOrigin"
|
||||
(click)="isOrgDropdownOpen.set(!isOrgDropdownOpen())"
|
||||
>
|
||||
{{ org.name }}
|
||||
</cnsl-header-button>
|
||||
<cnsl-header-dropdown [trigger]="orgTrigger" [isOpen]="isOrgDropdownOpen()" (closed)="isOrgDropdownOpen.set(false)">
|
||||
<cnsl-organization-selector (orgChanged)="isOrgDropdownOpen.set(false)"></cnsl-organization-selector>
|
||||
</cnsl-header-dropdown>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-template #slash>
|
||||
<svg
|
||||
class="slash"
|
||||
viewBox="0 0 24 24"
|
||||
width="32"
|
||||
height="32"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
shape-rendering="geometricPrecision"
|
||||
>
|
||||
<path d="M16.88 3.549L7.12 20.451"></path>
|
||||
</svg>
|
||||
</ng-template>
|
@@ -0,0 +1,7 @@
|
||||
.new-header-wrapper {
|
||||
padding-right: 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
75
console/src/app/modules/new-header/new-header.component.ts
Normal file
75
console/src/app/modules/new-header/new-header.component.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { ChangeDetectionStrategy, Component, 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 { injectQuery } from '@tanstack/angular-query-experimental';
|
||||
import { OrganizationSelectorComponent } from './organization-selector/organization-selector.component';
|
||||
import { CdkOverlayOrigin } from '@angular/cdk/overlay';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { InputModule } from '../input/input.module';
|
||||
import { HeaderButtonComponent } from './header-button/header-button.component';
|
||||
import { HeaderDropdownComponent } from './header-dropdown/header-dropdown.component';
|
||||
import { InstanceSelectorComponent } from './instance-selector/instance-selector.component';
|
||||
import { HasRolePipeModule } from '../../pipes/has-role-pipe/has-role-pipe.module';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { NewAdminService } from '../../services/new-admin.service';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-new-header',
|
||||
templateUrl: './new-header.component.html',
|
||||
styleUrls: ['./new-header.component.scss'],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
MatToolbarModule,
|
||||
OrganizationSelectorComponent,
|
||||
CdkOverlayOrigin,
|
||||
MatSelectModule,
|
||||
NgIf,
|
||||
InputModule,
|
||||
HeaderButtonComponent,
|
||||
HeaderDropdownComponent,
|
||||
InstanceSelectorComponent,
|
||||
NgTemplateOutlet,
|
||||
AsyncPipe,
|
||||
HasRolePipeModule,
|
||||
],
|
||||
})
|
||||
export class NewHeaderComponent {
|
||||
protected readonly myInstanceQuery = this.adminService.getMyInstanceQuery();
|
||||
protected readonly organizationsQuery = injectQuery(() => this.newOrganizationService.listOrganizationsQueryOptions());
|
||||
protected readonly isInstanceDropdownOpen = signal(false);
|
||||
protected readonly isOrgDropdownOpen = signal(false);
|
||||
protected readonly instanceSelectorSecondStep = signal(false);
|
||||
protected readonly activeOrganizationQuery = this.newOrganizationService.activeOrganizationQuery();
|
||||
protected readonly isHandset: Signal<boolean>;
|
||||
|
||||
constructor(
|
||||
private readonly newOrganizationService: NewOrganizationService,
|
||||
private readonly toastService: ToastService,
|
||||
private readonly breakpointObserver: BreakpointObserver,
|
||||
private readonly adminService: NewAdminService,
|
||||
) {
|
||||
this.isHandset = this.getIsHandset();
|
||||
effect(() => {
|
||||
if (this.organizationsQuery.isError()) {
|
||||
this.toastService.showError(this.organizationsQuery.error());
|
||||
}
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
if (this.myInstanceQuery.isError()) {
|
||||
this.toastService.showError(this.myInstanceQuery.error());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getIsHandset() {
|
||||
const mediaQuery = '(max-width: 599px)';
|
||||
const isHandset$ = this.breakpointObserver.observe(mediaQuery).pipe(map(({ matches }) => matches));
|
||||
return toSignal(isHandset$, { initialValue: this.breakpointObserver.isMatched(mediaQuery) });
|
||||
}
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
<div cdkTrapFocus class="focus-trapper">
|
||||
<!-- <div *ngIf="organizationsQuery.isPending() || setOrgId.isPending()">-->
|
||||
<!-- Loading organizations...-->
|
||||
<!-- </div>-->
|
||||
<div class="org-header">
|
||||
<button *ngIf="backButton" (click)="backButtonPressed.emit()" mat-button class="dropdown-button">
|
||||
<span class="back-button">
|
||||
<i class="las la-arrow-alt-circle-left"></i>
|
||||
<h3>Back to {{ backButton }}</h3>
|
||||
</span>
|
||||
</button>
|
||||
<span class="dropdown-label">{{ 'MENU.ORGANIZATION' | translate }}</span>
|
||||
<form [formGroup]="form" class="form">
|
||||
<i class="las la-1x la-search search-icon"></i>
|
||||
<input
|
||||
class="search-input"
|
||||
autocomplete="off"
|
||||
cnslInput
|
||||
[formControl]="form.controls.name"
|
||||
[placeholder]="'PROJECT.GRANT.CREATE.SEL_ORG' | translate"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="org-list">
|
||||
<!-- Make sure active org is always at the top -->
|
||||
<a *ngIf="activeOrgIfSearchMatches() as org" class="dropdown-button" mat-button (click)="changeOrg(org.id)">
|
||||
{{ org.name }}
|
||||
<i class="las la-1x la-check"></i>
|
||||
</a>
|
||||
<ng-container *ngFor="let page of organizationsQuery.data()?.pages; last as lastPage">
|
||||
<ng-container *ngFor="let org of page.result; trackBy: trackOrg">
|
||||
<a *ngIf="org.id !== activeOrg.data()?.id" class="dropdown-button" mat-button (click)="changeOrg(org.id)">
|
||||
{{ org.name }}
|
||||
</a>
|
||||
</ng-container>
|
||||
<button
|
||||
class="dropdown-button"
|
||||
mat-stroked-button
|
||||
*ngIf="lastPage && page.details?.totalResult as totalResult"
|
||||
(click)="organizationsQuery.fetchNextPage()"
|
||||
[disabled]="!organizationsQuery.hasNextPage() || organizationsQuery.isFetchingNextPage()"
|
||||
>
|
||||
...{{ totalResult - loadedOrgsCount() }} {{ 'PAGINATOR.MORE' | translate }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,82 @@
|
||||
:host {
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@mixin organization-selector-theme($theme) {
|
||||
$foreground: map-get($theme, foreground);
|
||||
$background: map-get($theme, background);
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
|
||||
.dropdown-label {
|
||||
color: if($is-dark-theme, #ffffff60, #00000060);
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.focus-trapper {
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
max-height: calc(100% - 10px);
|
||||
// needed otherwise an unexpected scrollbar appears
|
||||
height: calc(100% - 10px);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.org-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.org-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
|
||||
.dropdown-button {
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.dropdown-button > span:nth-child(2) {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.form {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: scaleX(-1) translate(0, -50%);
|
||||
// default input padding
|
||||
left: 10px;
|
||||
color: if($is-dark-theme, #ffffff60, #00000060);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
margin-bottom: 0;
|
||||
height: 32px;
|
||||
// size of icon plus half of default padding of input
|
||||
padding-left: calc(1rem + 15px);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
@@ -0,0 +1,181 @@
|
||||
import { ChangeDetectionStrategy, Component, computed, effect, EventEmitter, Input, Output, Signal } 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';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { FormBuilder, FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ListOrganizationsRequestSchema } from '@zitadel/proto/zitadel/org/v2/org_service_pb';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { TextQueryMethod } from '@zitadel/proto/zitadel/object/v2/object_pb';
|
||||
import { A11yModule } from '@angular/cdk/a11y';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { Organization } from '@zitadel/proto/zitadel/org/v2/org_pb';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { InputModule } from '../../input/input.module';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
type NameQuery = Extract<
|
||||
NonNullable<MessageInitShape<typeof ListOrganizationsRequestSchema>['queries']>[number]['query'],
|
||||
{ case: 'nameQuery' }
|
||||
>;
|
||||
|
||||
const QUERY_LIMIT = 5;
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-organization-selector',
|
||||
templateUrl: './organization-selector.component.html',
|
||||
styleUrls: ['./organization-selector.component.scss'],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
NgForOf,
|
||||
NgIf,
|
||||
ReactiveFormsModule,
|
||||
A11yModule,
|
||||
MatButtonModule,
|
||||
TranslateModule,
|
||||
MatMenuModule,
|
||||
InputModule,
|
||||
MatOptionModule,
|
||||
],
|
||||
})
|
||||
export class OrganizationSelectorComponent {
|
||||
@Input()
|
||||
public backButton = '';
|
||||
|
||||
@Output()
|
||||
public backButtonPressed = new EventEmitter<void>();
|
||||
|
||||
@Output()
|
||||
public orgChanged = new EventEmitter<Organization>();
|
||||
|
||||
protected setOrgId = injectMutation(() => ({
|
||||
mutationFn: (orgId: string) => this.newOrganizationService.setOrgId(orgId),
|
||||
}));
|
||||
|
||||
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>;
|
||||
|
||||
constructor(
|
||||
private readonly newOrganizationService: NewOrganizationService,
|
||||
private readonly formBuilder: FormBuilder,
|
||||
private readonly router: Router,
|
||||
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(() => {
|
||||
if (this.organizationsQuery.isError()) {
|
||||
toast.showError(this.organizationsQuery.error());
|
||||
}
|
||||
});
|
||||
effect(() => {
|
||||
if (this.setOrgId.isError()) {
|
||||
toast.showError(this.setOrgId.error());
|
||||
}
|
||||
});
|
||||
effect(() => {
|
||||
if (this.activeOrg.isError()) {
|
||||
toast.showError(this.activeOrg.error());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private buildForm() {
|
||||
return this.formBuilder.group({
|
||||
name: new FormControl('', { nonNullable: true }),
|
||||
});
|
||||
}
|
||||
|
||||
private getNameQuery(form: ReturnType<typeof this.buildForm>): Signal<NameQuery | undefined> {
|
||||
const name$ = form.controls.name.valueChanges.pipe(debounceTime(125));
|
||||
const nameSignal = toSignal(name$, { initialValue: form.controls.name.value });
|
||||
|
||||
return computed(() => {
|
||||
const name = nameSignal();
|
||||
if (!name) {
|
||||
return undefined;
|
||||
}
|
||||
const nameQuery: NameQuery = {
|
||||
case: 'nameQuery' as const,
|
||||
value: {
|
||||
name,
|
||||
method: TextQueryMethod.CONTAINS_IGNORE_CASE,
|
||||
},
|
||||
};
|
||||
return nameQuery;
|
||||
});
|
||||
}
|
||||
|
||||
private getOrganizationsQuery(nameQuery: Signal<NameQuery | undefined>) {
|
||||
return injectInfiniteQuery(() => {
|
||||
const query = nameQuery();
|
||||
return {
|
||||
queryKey: ['listOrganizationsInfinite', query],
|
||||
queryFn: ({ pageParam, signal }) => this.newOrganizationService.listOrganizations(pageParam, signal),
|
||||
initialPageParam: {
|
||||
query: {
|
||||
limit: QUERY_LIMIT,
|
||||
offset: BigInt(0),
|
||||
},
|
||||
queries: query ? [{ query }] : undefined,
|
||||
},
|
||||
placeholderData: keepPreviousData,
|
||||
getNextPageParam: (lastPage, _, pageParam) =>
|
||||
// if we received less than the limit last time we are at the end
|
||||
lastPage.result.length < pageParam.query.limit
|
||||
? undefined
|
||||
: {
|
||||
...pageParam,
|
||||
query: {
|
||||
...pageParam.query,
|
||||
offset: pageParam.query.offset + BigInt(lastPage.result.length),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getLoadedOrgsCount(organizationsQuery: ReturnType<typeof this.getOrganizationsQuery>) {
|
||||
return computed(() => {
|
||||
const pages = organizationsQuery.data()?.pages;
|
||||
if (!pages) {
|
||||
return BigInt(0);
|
||||
}
|
||||
return pages.reduce((acc, page) => acc + BigInt(page.result.length), BigInt(0));
|
||||
});
|
||||
}
|
||||
|
||||
private getActiveOrgIfSearchMatches(nameQuery: Signal<NameQuery | undefined>) {
|
||||
return computed(() => {
|
||||
const activeOrg = this.activeOrg.data() ?? undefined;
|
||||
const query = nameQuery();
|
||||
if (!activeOrg || !query?.value?.name) {
|
||||
return activeOrg;
|
||||
}
|
||||
return activeOrg.name.toLowerCase().includes(query.value.name.toLowerCase()) ? activeOrg : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
protected async changeOrg(orgId: string) {
|
||||
const org = await this.setOrgId.mutateAsync(orgId);
|
||||
this.orgChanged.emit(org);
|
||||
await this.router.navigate(['/org']);
|
||||
}
|
||||
|
||||
protected trackOrg(_: number, { id }: Organization): string {
|
||||
return id;
|
||||
}
|
||||
}
|
@@ -1,12 +1,13 @@
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { UntypedFormControl } from '@angular/forms';
|
||||
import { BehaviorSubject, catchError, debounceTime, finalize, from, map, Observable, of, pipe, scan, take, tap } from 'rxjs';
|
||||
import { BehaviorSubject, catchError, debounceTime, from, map, Observable, of, pipe, take, tap } from 'rxjs';
|
||||
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
|
||||
import { Org, OrgFieldName, OrgNameQuery, OrgQuery, OrgState, OrgStateQuery } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import { AuthenticationService } from 'src/app/services/authentication.service';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { Organization } from '@zitadel/proto/zitadel/org/v2/org_pb';
|
||||
|
||||
const ORG_QUERY_LIMIT = 100;
|
||||
|
||||
@@ -44,7 +45,7 @@ export class OrgContextComponent implements OnInit {
|
||||
);
|
||||
|
||||
public filterControl: UntypedFormControl = new UntypedFormControl('');
|
||||
@Input() public org!: Org.AsObject;
|
||||
@Input({ required: true }) public org!: Organization;
|
||||
@ViewChild('input', { static: false }) input!: ElementRef;
|
||||
@Output() public closedCard: EventEmitter<void> = new EventEmitter();
|
||||
@Output() public setOrg: EventEmitter<Org.AsObject> = new EventEmitter();
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<cnsl-refresh-table [hideRefresh]="true" (refreshed)="refresh()" [loading]="loading$ | async">
|
||||
<cnsl-filter-org actions (filterChanged)="applySearchQuery($any($event))" (filterOpen)="filterOpen = $event">
|
||||
<cnsl-refresh-table [hideRefresh]="true" [loading]="listOrganizationsQuery.isPending()">
|
||||
<cnsl-filter-org actions (filterChanged)="applySearchQuery($any($event), paginator)">
|
||||
</cnsl-filter-org>
|
||||
|
||||
<ng-template actions cnslHasRole [hasRole]="['org.create', 'iam.write']">
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="cnsl-action-button">
|
||||
<mat-icon class="icon">add</mat-icon>
|
||||
<span>{{ 'ACTIONS.NEW' | translate }}</span>
|
||||
<cnsl-action-keys (actionTriggered)="gotoRouterLink(['/orgs', 'create'])"> </cnsl-action-keys>
|
||||
<cnsl-action-keys (actionTriggered)="router.navigate(['/orgs', 'create'])"> </cnsl-action-keys>
|
||||
</div>
|
||||
</a>
|
||||
</ng-template>
|
||||
@@ -22,12 +22,12 @@
|
||||
>
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'ORG.PAGES.ID' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let org" (click)="setAndNavigateToOrg(org)">{{ org.id }}</td>
|
||||
<td mat-cell *cnslCellDef="let org; dataSource: dataSource" (click)="setAndNavigateToOrg(org)">{{ org.id }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="primaryDomain">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'ORG.PAGES.PRIMARYDOMAIN' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let org" (click)="setAndNavigateToOrg(org)">
|
||||
<td mat-cell *cnslCellDef="let org; dataSource: dataSource" (click)="setAndNavigateToOrg(org)">
|
||||
<div class="primary-domain-wrapper">
|
||||
<span>{{ org.primaryDomain }}</span>
|
||||
<button
|
||||
@@ -50,7 +50,7 @@
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef>
|
||||
{{ 'ORG.PAGES.NAME' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let org" (click)="setAndNavigateToOrg(org)">
|
||||
<td mat-cell *cnslCellDef="let org; dataSource: dataSource" (click)="setAndNavigateToOrg(org)">
|
||||
<span>{{ org.name }}</span
|
||||
><span *ngIf="defaultOrgId === org.id" class="state orgdefaultlabel">{{
|
||||
'ORG.PAGES.DEFAULTLABEL' | translate
|
||||
@@ -60,12 +60,12 @@
|
||||
|
||||
<ng-container matColumnDef="state">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'ORG.PAGES.STATE' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let org" (click)="setAndNavigateToOrg(org)">
|
||||
<td mat-cell *cnslCellDef="let org; dataSource: dataSource" (click)="setAndNavigateToOrg(org)">
|
||||
<span
|
||||
class="state"
|
||||
[ngClass]="{
|
||||
active: org.state === OrgState.ORG_STATE_ACTIVE,
|
||||
inactive: org.state === OrgState.ORG_STATE_INACTIVE,
|
||||
active: org.state === OrganizationState.ACTIVE,
|
||||
inactive: org.state === OrganizationState.INACTIVE,
|
||||
}"
|
||||
*ngIf="org.state"
|
||||
>{{ 'ORG.STATE.' + org.state | translate }}</span
|
||||
@@ -77,7 +77,7 @@
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
{{ 'ORG.PAGES.CREATIONDATE' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let org" (click)="setAndNavigateToOrg(org)">
|
||||
<td mat-cell *cnslCellDef="let org; dataSource: dataSource" (click)="setAndNavigateToOrg(org)">
|
||||
{{ org.details?.creationDate | timestampToDate | localizedDate: 'fromNow' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -86,14 +86,14 @@
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
{{ 'ORG.PAGES.DATECHANGED' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let org" (click)="setAndNavigateToOrg(org)">
|
||||
<td mat-cell *cnslCellDef="let org; dataSource: dataSource" (click)="setAndNavigateToOrg(org)">
|
||||
{{ org.details?.changeDate | timestampToDate | localizedDate: 'fromNow' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th mat-header-cell *matHeaderCellDef class="user-tr-actions"></th>
|
||||
<td mat-cell *matCellDef="let org" class="user-tr-actions">
|
||||
<td mat-cell *cnslCellDef="let org; dataSource: dataSource" class="user-tr-actions">
|
||||
<cnsl-table-actions [hasActions]="true">
|
||||
<button menuActions mat-menu-item (click)="setDefaultOrg(org)" data-e2e="set-default-button">
|
||||
{{ 'ORG.PAGES.SETASDEFAULT' | translate }}
|
||||
@@ -108,10 +108,10 @@
|
||||
<cnsl-paginator
|
||||
#paginator
|
||||
class="paginator"
|
||||
[timestamp]="timestamp"
|
||||
[length]="totalResult || 0"
|
||||
[pageSize]="initialLimit"
|
||||
[timestamp]="listOrganizationsQuery.data()?.details?.timestamp"
|
||||
[length]="Number(listOrganizationsQuery.data()?.details?.totalResult ?? 0)"
|
||||
[pageSize]="listQuery().limit"
|
||||
[pageSizeOptions]="[10, 20, 50, 100]"
|
||||
(page)="changePage()"
|
||||
(page)="pageChanged($event)"
|
||||
></cnsl-paginator>
|
||||
</cnsl-refresh-table>
|
||||
|
@@ -1,24 +1,26 @@
|
||||
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
||||
import { Component, Input, ViewChild } from '@angular/core';
|
||||
import { MatSort, Sort } from '@angular/material/sort';
|
||||
import { Component, computed, effect, signal } from '@angular/core';
|
||||
import { Sort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
|
||||
import { BehaviorSubject, catchError, finalize, from, map, Observable, of, Subject, switchMap, takeUntil } from 'rxjs';
|
||||
import { Org, OrgFieldName, OrgQuery, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import { OrgQuery } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { NewOrganizationService } from '../../services/new-organization.service';
|
||||
import { injectQuery, keepPreviousData } from '@tanstack/angular-query-experimental';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { ListOrganizationsRequestSchema } from '@zitadel/proto/zitadel/org/v2/org_service_pb';
|
||||
import { PageEvent } from '@angular/material/paginator';
|
||||
import { OrganizationFieldName } from '@zitadel/proto/zitadel/org/v2/query_pb';
|
||||
import { Organization, OrganizationState } from '@zitadel/proto/zitadel/org/v2/org_pb';
|
||||
import { PaginatorComponent } from '../paginator/paginator.component';
|
||||
|
||||
enum OrgListSearchKey {
|
||||
NAME = 'NAME',
|
||||
}
|
||||
|
||||
type Request = { limit: number; offset: number; queries: OrgQuery[] };
|
||||
type ListQuery = NonNullable<MessageInitShape<typeof ListOrganizationsRequestSchema>['query']>;
|
||||
type SearchQuery = NonNullable<MessageInitShape<typeof ListOrganizationsRequestSchema>['queries']>[number];
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-org-table',
|
||||
@@ -26,100 +28,80 @@ type Request = { limit: number; offset: number; queries: OrgQuery[] };
|
||||
styleUrls: ['./org-table.component.scss'],
|
||||
})
|
||||
export class OrgTableComponent {
|
||||
public orgSearchKey: OrgListSearchKey | undefined = undefined;
|
||||
|
||||
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
|
||||
@ViewChild('input') public filter!: Input;
|
||||
|
||||
public dataSource: MatTableDataSource<Org.AsObject> = new MatTableDataSource<Org.AsObject>([]);
|
||||
public displayedColumns: string[] = ['name', 'state', 'primaryDomain', 'creationDate', 'changeDate', 'actions'];
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
public activeOrg!: Org.AsObject;
|
||||
public initialLimit: number = 20;
|
||||
public timestamp: Timestamp.AsObject | undefined = undefined;
|
||||
public totalResult: number = 0;
|
||||
public filterOpen: boolean = false;
|
||||
public OrgState: any = OrgState;
|
||||
public copied: string = '';
|
||||
@ViewChild(MatSort) public sort!: MatSort;
|
||||
|
||||
private searchQueries: OrgQuery[] = [];
|
||||
private destroy$: Subject<void> = new Subject();
|
||||
private requestOrgs$: BehaviorSubject<Request> = new BehaviorSubject<Request>({
|
||||
limit: this.initialLimit,
|
||||
offset: 0,
|
||||
queries: [],
|
||||
});
|
||||
public defaultOrgId: string = '';
|
||||
private requestOrgsObservable$ = this.requestOrgs$.pipe(takeUntil(this.destroy$));
|
||||
|
||||
protected readonly listQuery = signal<ListQuery & { limit: number }>({ limit: 20, offset: BigInt(0) });
|
||||
private readonly searchQueries = signal<SearchQuery[]>([]);
|
||||
private readonly sortingColumn = signal<OrganizationFieldName | undefined>(undefined);
|
||||
|
||||
private readonly req = computed<MessageInitShape<typeof ListOrganizationsRequestSchema>>(() => ({
|
||||
query: this.listQuery(),
|
||||
queries: this.searchQueries().length ? this.searchQueries() : undefined,
|
||||
sortingColumn: this.sortingColumn(),
|
||||
}));
|
||||
|
||||
protected listOrganizationsQuery = injectQuery(() => ({
|
||||
...this.newOrganizationService.listOrganizationsQueryOptions(this.req()),
|
||||
placeholderData: keepPreviousData,
|
||||
}));
|
||||
|
||||
protected readonly dataSource = this.getDataSource();
|
||||
|
||||
constructor(
|
||||
private authService: GrpcAuthService,
|
||||
private mgmtService: ManagementService,
|
||||
private adminService: AdminService,
|
||||
private router: Router,
|
||||
private toast: ToastService,
|
||||
private _liveAnnouncer: LiveAnnouncer,
|
||||
private translate: TranslateService,
|
||||
private readonly authService: GrpcAuthService,
|
||||
private readonly mgmtService: ManagementService,
|
||||
private readonly adminService: AdminService,
|
||||
protected readonly router: Router,
|
||||
private readonly toast: ToastService,
|
||||
private readonly liveAnnouncer: LiveAnnouncer,
|
||||
private readonly translate: TranslateService,
|
||||
private readonly newOrganizationService: NewOrganizationService,
|
||||
) {
|
||||
this.requestOrgs$.next({ limit: this.initialLimit, offset: 0, queries: this.searchQueries });
|
||||
this.authService.getActiveOrg().then((org) => (this.activeOrg = org));
|
||||
|
||||
this.requestOrgsObservable$.pipe(switchMap((req) => this.loadOrgs(req))).subscribe((orgs) => {
|
||||
this.dataSource = new MatTableDataSource<Org.AsObject>(orgs);
|
||||
});
|
||||
|
||||
this.mgmtService.getIAM().then((iam) => {
|
||||
this.defaultOrgId = iam.defaultOrgId;
|
||||
});
|
||||
}
|
||||
|
||||
public loadOrgs(request: Request): Observable<Org.AsObject[]> {
|
||||
this.loadingSubject.next(true);
|
||||
|
||||
let sortingField: OrgFieldName | undefined = undefined;
|
||||
if (this.sort?.active && this.sort?.direction)
|
||||
switch (this.sort.active) {
|
||||
case 'name':
|
||||
sortingField = OrgFieldName.ORG_FIELD_NAME_NAME;
|
||||
break;
|
||||
effect(() => {
|
||||
if (this.listOrganizationsQuery.isError()) {
|
||||
this.toast.showError(this.listOrganizationsQuery.error());
|
||||
}
|
||||
|
||||
return from(
|
||||
this.adminService.listOrgs(request.limit, request.offset, request.queries, sortingField, this.sort?.direction),
|
||||
).pipe(
|
||||
map((resp) => {
|
||||
this.timestamp = resp.details?.viewTimestamp;
|
||||
this.totalResult = resp.details?.totalResult ?? 0;
|
||||
return resp.resultList;
|
||||
}),
|
||||
catchError((error) => {
|
||||
this.toast.showError(error);
|
||||
return of([]);
|
||||
}),
|
||||
finalize(() => this.loadingSubject.next(false)),
|
||||
);
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
this.requestOrgs$.next({
|
||||
limit: this.paginator.pageSize,
|
||||
offset: this.paginator.pageSize * this.paginator.pageIndex,
|
||||
queries: this.searchQueries,
|
||||
});
|
||||
}
|
||||
|
||||
public sortChange(sortState: Sort) {
|
||||
if (sortState.direction && sortState.active) {
|
||||
this._liveAnnouncer.announce(`Sorted ${sortState.direction}ending`);
|
||||
this.refresh();
|
||||
private getDataSource() {
|
||||
const dataSource = new MatTableDataSource<Organization>();
|
||||
effect(() => {
|
||||
const organizations = this.listOrganizationsQuery.data()?.result ?? [];
|
||||
if (dataSource.data != organizations) {
|
||||
dataSource.data = organizations;
|
||||
}
|
||||
});
|
||||
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
public async sortChange(sortState: Sort) {
|
||||
this.sortingColumn.set(sortState.active === 'name' ? OrganizationFieldName.NAME : undefined);
|
||||
|
||||
const listQuery = { ...this.listQuery() };
|
||||
if (sortState.direction === 'asc') {
|
||||
this.listQuery.set({ ...listQuery, asc: true });
|
||||
} else {
|
||||
this._liveAnnouncer.announce('Sorting cleared');
|
||||
delete listQuery.asc;
|
||||
this.listQuery.set(listQuery);
|
||||
}
|
||||
|
||||
if (sortState.direction && sortState.active) {
|
||||
await this.liveAnnouncer.announce(`Sorted ${sortState.direction}ending`);
|
||||
} else {
|
||||
await this.liveAnnouncer.announce('Sorting cleared');
|
||||
}
|
||||
}
|
||||
|
||||
public setDefaultOrg(org: Org.AsObject) {
|
||||
public setDefaultOrg(org: Organization) {
|
||||
this.adminService
|
||||
.setDefaultOrg(org.id)
|
||||
.then(() => {
|
||||
@@ -131,34 +113,56 @@ export class OrgTableComponent {
|
||||
});
|
||||
}
|
||||
|
||||
public applySearchQuery(searchQueries: OrgQuery[]): void {
|
||||
this.searchQueries = searchQueries;
|
||||
this.requestOrgs$.next({
|
||||
limit: this.paginator ? this.paginator.pageSize : this.initialLimit,
|
||||
offset: this.paginator ? this.paginator.pageSize * this.paginator.pageIndex : 0,
|
||||
queries: this.searchQueries,
|
||||
});
|
||||
}
|
||||
|
||||
public setFilter(key: OrgListSearchKey): void {
|
||||
setTimeout(() => {
|
||||
if (this.filter) {
|
||||
(this.filter as any).nativeElement.focus();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
if (this.orgSearchKey !== key) {
|
||||
this.orgSearchKey = key;
|
||||
} else {
|
||||
this.orgSearchKey = undefined;
|
||||
this.refresh();
|
||||
public applySearchQuery(searchQueries: OrgQuery[], paginator: PaginatorComponent): void {
|
||||
if (this.searchQueries().length === 0 && searchQueries.length === 0) {
|
||||
return;
|
||||
}
|
||||
paginator.pageIndex = 0;
|
||||
this.searchQueries.set(searchQueries.map((q) => ({ query: this.oldQueryToNewQuery(q.toObject()) })));
|
||||
}
|
||||
|
||||
public setAndNavigateToOrg(org: Org.AsObject): void {
|
||||
if (org.state !== OrgState.ORG_STATE_REMOVED) {
|
||||
this.authService.setActiveOrg(org);
|
||||
this.router.navigate(['/org']);
|
||||
private oldQueryToNewQuery(query: OrgQuery.AsObject): SearchQuery['query'] {
|
||||
if (query.idQuery) {
|
||||
return {
|
||||
case: 'idQuery' as const,
|
||||
value: {
|
||||
id: query.idQuery.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (query.stateQuery) {
|
||||
return {
|
||||
case: 'stateQuery' as const,
|
||||
value: {
|
||||
state: query.stateQuery.state as unknown as any,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (query.domainQuery) {
|
||||
return {
|
||||
case: 'domainQuery' as const,
|
||||
value: {
|
||||
domain: query.domainQuery.domain,
|
||||
method: query.domainQuery.method as unknown as any,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (query.nameQuery) {
|
||||
return {
|
||||
case: 'nameQuery' as const,
|
||||
value: {
|
||||
name: query.nameQuery.name,
|
||||
method: query.nameQuery.method as unknown as any,
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new Error('Invalid query');
|
||||
}
|
||||
|
||||
public async setAndNavigateToOrg(org: Organization): Promise<void> {
|
||||
if (org.state !== OrganizationState.REMOVED) {
|
||||
await this.newOrganizationService.setOrgId(org.id);
|
||||
await this.router.navigate(['/org']);
|
||||
} else {
|
||||
this.translate.get('ORG.TOAST.ORG_WAS_DELETED').subscribe((data) => {
|
||||
this.toast.showInfo(data);
|
||||
@@ -166,11 +170,13 @@ export class OrgTableComponent {
|
||||
}
|
||||
}
|
||||
|
||||
public changePage(): void {
|
||||
this.refresh();
|
||||
protected pageChanged(event: PageEvent) {
|
||||
this.listQuery.set({
|
||||
limit: event.pageSize,
|
||||
offset: BigInt(event.pageSize) * BigInt(event.pageIndex),
|
||||
});
|
||||
}
|
||||
|
||||
public gotoRouterLink(rL: any) {
|
||||
this.router.navigate(rL);
|
||||
}
|
||||
protected readonly Number = Number;
|
||||
protected readonly OrganizationState = OrganizationState;
|
||||
}
|
||||
|
@@ -21,6 +21,7 @@ import { PaginatorModule } from '../paginator/paginator.module';
|
||||
import { RefreshTableModule } from '../refresh-table/refresh-table.module';
|
||||
import { TableActionsModule } from '../table-actions/table-actions.module';
|
||||
import { OrgTableComponent } from './org-table.component';
|
||||
import { TypeSafeCellDefModule } from '../../directives/type-safe-cell-def/type-safe-cell-def.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [OrgTableComponent],
|
||||
@@ -45,6 +46,7 @@ import { OrgTableComponent } from './org-table.component';
|
||||
MatRadioModule,
|
||||
InputModule,
|
||||
FormsModule,
|
||||
TypeSafeCellDefModule,
|
||||
],
|
||||
exports: [OrgTableComponent],
|
||||
})
|
||||
|
@@ -7,15 +7,15 @@ import {
|
||||
UpdateDomainPolicyRequest,
|
||||
} from 'src/app/proto/generated/zitadel/admin_pb';
|
||||
import { GetOrgIAMPolicyResponse } from 'src/app/proto/generated/zitadel/management_pb';
|
||||
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import { DomainPolicy, OrgIAMPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { StorageLocation, StorageService } from 'src/app/services/storage.service';
|
||||
import { StorageService } from 'src/app/services/storage.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component';
|
||||
import { PolicyComponentServiceType } from '../policy-component-types.enum';
|
||||
import { NewOrganizationService } from '../../../services/new-organization.service';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-domain-policy',
|
||||
@@ -30,7 +30,7 @@ export class DomainPolicyComponent implements OnInit, OnDestroy {
|
||||
|
||||
public loading: boolean = false;
|
||||
private sub: Subscription = new Subscription();
|
||||
private org!: Org.AsObject;
|
||||
private orgId = this.newOrganizationService.getOrgId();
|
||||
|
||||
public PolicyComponentServiceType: any = PolicyComponentServiceType;
|
||||
|
||||
@@ -40,6 +40,7 @@ export class DomainPolicyComponent implements OnInit, OnDestroy {
|
||||
private injector: Injector,
|
||||
private adminService: AdminService,
|
||||
private storageService: StorageService,
|
||||
private readonly newOrganizationService: NewOrganizationService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -69,12 +70,6 @@ export class DomainPolicyComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private async getData(): Promise<GetCustomOrgIAMPolicyResponse.AsObject | GetOrgIAMPolicyResponse.AsObject | any> {
|
||||
const org: Org.AsObject | null = this.storageService.getItem('organization', StorageLocation.session);
|
||||
|
||||
if (org?.id) {
|
||||
this.org = org;
|
||||
}
|
||||
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
return this.managementService.getDomainPolicy();
|
||||
@@ -90,7 +85,7 @@ export class DomainPolicyComponent implements OnInit, OnDestroy {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
if ((this.domainData as OrgIAMPolicy.AsObject).isDefault) {
|
||||
const req = new AddCustomDomainPolicyRequest();
|
||||
req.setOrgId(this.org.id);
|
||||
req.setOrgId(this.orgId());
|
||||
req.setUserLoginMustBeDomain(this.domainData.userLoginMustBeDomain);
|
||||
req.setValidateOrgDomains(this.domainData.validateOrgDomains);
|
||||
req.setSmtpSenderAddressMatchesInstanceDomain(this.domainData.smtpSenderAddressMatchesInstanceDomain);
|
||||
@@ -106,7 +101,7 @@ export class DomainPolicyComponent implements OnInit, OnDestroy {
|
||||
break;
|
||||
} else {
|
||||
const req = new AddCustomDomainPolicyRequest();
|
||||
req.setOrgId(this.org.id);
|
||||
req.setOrgId(this.orgId());
|
||||
req.setUserLoginMustBeDomain(this.domainData.userLoginMustBeDomain);
|
||||
req.setValidateOrgDomains(this.domainData.validateOrgDomains);
|
||||
req.setSmtpSenderAddressMatchesInstanceDomain(this.domainData.smtpSenderAddressMatchesInstanceDomain);
|
||||
@@ -154,7 +149,7 @@ export class DomainPolicyComponent implements OnInit, OnDestroy {
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
this.adminService
|
||||
.resetCustomDomainPolicyToDefault(this.org.id)
|
||||
.resetCustomDomainPolicyToDefault(this.orgId())
|
||||
.then(() => {
|
||||
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
|
||||
setTimeout(() => {
|
||||
|
@@ -85,7 +85,7 @@ export class PrivateLabelingPolicyComponent implements OnInit, OnDestroy {
|
||||
public fontName = '';
|
||||
|
||||
public refreshPreview: EventEmitter<void> = new EventEmitter();
|
||||
public org!: Org.AsObject;
|
||||
public org!: string;
|
||||
public InfoSectionType: any = InfoSectionType;
|
||||
private iconChanged: boolean = false;
|
||||
|
||||
@@ -152,7 +152,7 @@ export class PrivateLabelingPolicyComponent implements OnInit, OnDestroy {
|
||||
if (theme === Theme.DARK) {
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
return this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTDARKLOGO, formData, this.org.id));
|
||||
return this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTDARKLOGO, formData, this.org));
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
return this.handleUploadPromise(this.assetService.upload(AssetEndpoint.IAMDARKLOGO, formData));
|
||||
}
|
||||
@@ -160,7 +160,7 @@ export class PrivateLabelingPolicyComponent implements OnInit, OnDestroy {
|
||||
if (theme === Theme.LIGHT) {
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
return this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTLOGO, formData, this.org.id));
|
||||
return this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTLOGO, formData, this.org));
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
return this.handleUploadPromise(this.assetService.upload(AssetEndpoint.IAMLOGO, formData));
|
||||
}
|
||||
@@ -182,7 +182,7 @@ export class PrivateLabelingPolicyComponent implements OnInit, OnDestroy {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
this.service = this.injector.get(ManagementService as Type<ManagementService>);
|
||||
|
||||
const org: Org.AsObject | null = this.storageService.getItem(StorageKey.organization, StorageLocation.session);
|
||||
const org = this.storageService.getItem(StorageKey.organizationId, StorageLocation.session);
|
||||
|
||||
if (org) {
|
||||
this.org = org;
|
||||
@@ -209,7 +209,7 @@ export class PrivateLabelingPolicyComponent implements OnInit, OnDestroy {
|
||||
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
return this.handleFontUploadPromise(this.assetService.upload(AssetEndpoint.MGMTFONT, formData, this.org.id));
|
||||
return this.handleFontUploadPromise(this.assetService.upload(AssetEndpoint.MGMTFONT, formData, this.org));
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
return this.handleFontUploadPromise(this.assetService.upload(AssetEndpoint.IAMFONT, formData));
|
||||
}
|
||||
@@ -334,7 +334,7 @@ export class PrivateLabelingPolicyComponent implements OnInit, OnDestroy {
|
||||
if (theme === Theme.DARK) {
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTDARKICON, formData, this.org.id));
|
||||
this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTDARKICON, formData, this.org));
|
||||
break;
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
this.handleUploadPromise(this.assetService.upload(AssetEndpoint.IAMDARKICON, formData));
|
||||
@@ -344,7 +344,7 @@ export class PrivateLabelingPolicyComponent implements OnInit, OnDestroy {
|
||||
if (theme === Theme.LIGHT) {
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTICON, formData, this.org.id));
|
||||
this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTICON, formData, this.org));
|
||||
break;
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
this.handleUploadPromise(this.assetService.upload(AssetEndpoint.IAMICON, formData));
|
||||
|
@@ -211,7 +211,8 @@ export class ProviderSamlSpComponent {
|
||||
// @ts-ignore
|
||||
req.setNameIdFormat(SAMLNameIDFormat[this.nameIDFormat?.value]);
|
||||
req.setTransientMappingAttributeName(this.transientMapping?.value);
|
||||
req.setFederatedLogoutEnabled(this.federatedLogoutEnabled?.value);
|
||||
// todo: figure out what happened here
|
||||
// req.setFederatedLogoutEnabled(this.federatedLogoutEnabled?.value);
|
||||
req.setProviderOptions(this.options);
|
||||
|
||||
this.loading = true;
|
||||
@@ -252,7 +253,8 @@ export class ProviderSamlSpComponent {
|
||||
req.setNameIdFormat(SAMLNameIDFormat[this.nameIDFormat.value]);
|
||||
}
|
||||
req.setTransientMappingAttributeName(this.transientMapping?.value);
|
||||
req.setFederatedLogoutEnabled(this.federatedLogoutEnabled?.value);
|
||||
// todo: figure out what happened here
|
||||
//req.setFederatedLogoutEnabled(this.federatedLogoutEnabled?.value);
|
||||
this.loading = true;
|
||||
this.service
|
||||
.addSAMLProvider(req)
|
||||
|
@@ -1,13 +1,14 @@
|
||||
import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { Component, effect, OnDestroy } from '@angular/core';
|
||||
import { merge, Subject, takeUntil } from 'rxjs';
|
||||
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import { ProjectState } from 'src/app/proto/generated/zitadel/project_pb';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { StorageLocation, StorageService } from 'src/app/services/storage.service';
|
||||
import { StorageKey, StorageLocation, StorageService } from 'src/app/services/storage.service';
|
||||
|
||||
import { SETTINGLINKS } from '../settings-grid/settinglinks';
|
||||
import { NewOrganizationService } from '../../services/new-organization.service';
|
||||
|
||||
export interface ShortcutItem {
|
||||
id: string;
|
||||
@@ -80,7 +81,7 @@ const CREATE_USER: ShortcutItem = {
|
||||
styleUrls: ['./shortcuts.component.scss'],
|
||||
})
|
||||
export class ShortcutsComponent implements OnDestroy {
|
||||
public org!: Org.AsObject;
|
||||
public orgId!: string;
|
||||
|
||||
public main: ShortcutItem[] = [];
|
||||
public secondary: ShortcutItem[] = [];
|
||||
@@ -96,22 +97,15 @@ export class ShortcutsComponent implements OnDestroy {
|
||||
private storageService: StorageService,
|
||||
private auth: GrpcAuthService,
|
||||
private mgmtService: ManagementService,
|
||||
private newOrganizationService: NewOrganizationService,
|
||||
) {
|
||||
const org: Org.AsObject | null = this.storageService.getItem('organization', StorageLocation.session);
|
||||
if (org && org.id) {
|
||||
this.org = org;
|
||||
this.loadProjectShortcuts();
|
||||
}
|
||||
|
||||
merge(this.auth.activeOrgChanged, this.mgmtService.ownedProjects)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
const org: Org.AsObject | null = this.storageService.getItem('organization', StorageLocation.session);
|
||||
if (org && org.id) {
|
||||
this.org = org;
|
||||
this.loadProjectShortcuts();
|
||||
}
|
||||
});
|
||||
effect(() => {
|
||||
const orgId = this.newOrganizationService.orgId();
|
||||
if (orgId) {
|
||||
this.orgId = orgId;
|
||||
this.loadProjectShortcuts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public loadProjectShortcuts(): void {
|
||||
@@ -151,14 +145,14 @@ export class ShortcutsComponent implements OnDestroy {
|
||||
});
|
||||
|
||||
this.ALL_SHORTCUTS = [...routesShortcuts, ...settingsShortcuts, ...mapped];
|
||||
this.loadShortcuts(this.org);
|
||||
this.loadShortcuts(this.orgId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public loadShortcuts(org: Org.AsObject): void {
|
||||
public loadShortcuts(orgId: string): void {
|
||||
['main', 'secondary', 'third'].map((listName) => {
|
||||
const joinedShortcuts = this.storageService.getItem(`shortcuts:${listName}:${org.id}`, StorageLocation.local);
|
||||
const joinedShortcuts = this.storageService.getItem(`shortcuts:${listName}:${orgId}`, StorageLocation.local);
|
||||
if (joinedShortcuts) {
|
||||
const parsedIds: string[] = joinedShortcuts.split(',');
|
||||
if (parsedIds && parsedIds.length) {
|
||||
@@ -244,26 +238,26 @@ export class ShortcutsComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
public saveStateToStorage(): void {
|
||||
const org: Org.AsObject | null = this.storageService.getItem('organization', StorageLocation.session);
|
||||
if (org && org.id) {
|
||||
this.storageService.setItem(`shortcuts:main:${org.id}`, this.main.map((p) => p.id).join(','), StorageLocation.local);
|
||||
const orgId = this.newOrganizationService.orgId();
|
||||
if (orgId) {
|
||||
this.storageService.setItem(`shortcuts:main:${orgId}`, this.main.map((p) => p.id).join(','), StorageLocation.local);
|
||||
this.storageService.setItem(
|
||||
`shortcuts:secondary:${org.id}`,
|
||||
`shortcuts:secondary:${orgId}`,
|
||||
this.secondary.map((p) => p.id).join(','),
|
||||
StorageLocation.local,
|
||||
);
|
||||
this.storageService.setItem(`shortcuts:third:${org.id}`, this.third.map((p) => p.id).join(','), StorageLocation.local);
|
||||
this.storageService.setItem(`shortcuts:third:${orgId}`, this.third.map((p) => p.id).join(','), StorageLocation.local);
|
||||
}
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
const org: Org.AsObject | null = this.storageService.getItem('organization', StorageLocation.session);
|
||||
if (org && org.id) {
|
||||
const orgId = this.newOrganizationService.orgId();
|
||||
if (orgId) {
|
||||
['main', 'secondary', 'third'].map((listName) => {
|
||||
this.storageService.removeItem(`shortcuts:${listName}:${org.id}`, StorageLocation.local);
|
||||
this.storageService.removeItem(`shortcuts:${listName}:${orgId}`, StorageLocation.local);
|
||||
});
|
||||
|
||||
this.loadShortcuts(org);
|
||||
this.loadShortcuts(orgId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -39,7 +39,7 @@
|
||||
<div class="cnsl-action-button">
|
||||
<mat-icon class="icon">add</mat-icon>
|
||||
<span>{{ 'GRANTS.ADD_BTN' | translate }}</span>
|
||||
<cnsl-action-keys (actionTriggered)="gotoCreateLink(routerLink)" [type]="ActionKeysType.ADD"></cnsl-action-keys>
|
||||
<cnsl-action-keys (actionTriggered)="router.navigate(routerLink)" [type]="ActionKeysType.ADD"></cnsl-action-keys>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
|
@@ -7,7 +7,6 @@ import { Router } from '@angular/router';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { enterAnimations } from 'src/app/animations';
|
||||
import { UserGrant as AuthUserGrant } from 'src/app/proto/generated/zitadel/auth_pb';
|
||||
import { Role } from 'src/app/proto/generated/zitadel/project_pb';
|
||||
import {
|
||||
Type,
|
||||
UserGrant as MgmtUserGrant,
|
||||
@@ -24,7 +23,9 @@ import { PageEvent, PaginatorComponent } from '../paginator/paginator.component'
|
||||
import { UserGrantRoleDialogComponent } from '../user-grant-role-dialog/user-grant-role-dialog.component';
|
||||
import { WarnDialogComponent } from '../warn-dialog/warn-dialog.component';
|
||||
import { UserGrantContext, UserGrantsDataSource } from './user-grants-datasource';
|
||||
import { Org, OrgIDQuery, OrgQuery, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import { QueryClient } from '@tanstack/angular-query-experimental';
|
||||
import { NewOrganizationService } from '../../services/new-organization.service';
|
||||
|
||||
export enum UserGrantListSearchKey {
|
||||
DISPLAY_NAME,
|
||||
@@ -43,7 +44,6 @@ type UserGrantAsObject = AuthUserGrant.AsObject | MgmtUserGrant.AsObject;
|
||||
})
|
||||
export class UserGrantsComponent implements OnInit, AfterViewInit {
|
||||
public userGrantListSearchKey: UserGrantListSearchKey | undefined = undefined;
|
||||
public UserGrantListSearchKey: any = UserGrantListSearchKey;
|
||||
|
||||
public INITIAL_PAGE_SIZE: number = 50;
|
||||
@Input() context: UserGrantContext = UserGrantContext.NONE;
|
||||
@@ -62,27 +62,24 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
|
||||
@Input() grantId: string = '';
|
||||
@ViewChild('input') public filter!: MatInput;
|
||||
|
||||
public projectRoleOptions: Role.AsObject[] = [];
|
||||
public routerLink: any = undefined;
|
||||
|
||||
public loadedId: string = '';
|
||||
public loadedProjectId: string = '';
|
||||
public grantToEdit: string = '';
|
||||
|
||||
public UserGrantContext: any = UserGrantContext;
|
||||
public Type: any = Type;
|
||||
public ActionKeysType: any = ActionKeysType;
|
||||
public UserGrantState: any = UserGrantState;
|
||||
public UserGrantContext = UserGrantContext;
|
||||
public Type = Type;
|
||||
public ActionKeysType = ActionKeysType;
|
||||
public UserGrantState = UserGrantState;
|
||||
@Input() public type: Type | undefined = undefined;
|
||||
|
||||
public filterOpen: boolean = false;
|
||||
public myOrgs: Array<Org.AsObject> = [];
|
||||
constructor(
|
||||
private authService: GrpcAuthService,
|
||||
private userService: ManagementService,
|
||||
private toast: ToastService,
|
||||
private dialog: MatDialog,
|
||||
private router: Router,
|
||||
private readonly authService: GrpcAuthService,
|
||||
private readonly userService: ManagementService,
|
||||
private readonly toast: ToastService,
|
||||
private readonly dialog: MatDialog,
|
||||
private readonly queryClient: QueryClient,
|
||||
protected readonly router: Router,
|
||||
private readonly newOrganizationService: NewOrganizationService,
|
||||
) {}
|
||||
|
||||
@Input() public displayedColumns: string[] = [
|
||||
@@ -149,10 +146,6 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
public gotoCreateLink(rL: any): void {
|
||||
this.router.navigate(rL);
|
||||
}
|
||||
|
||||
private loadGrantsPage(type: Type | undefined, searchQueries?: UserGrantQuery[]): void {
|
||||
let queries: UserGrantQuery[] = [];
|
||||
|
||||
@@ -315,15 +308,12 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
public async showUser(grant: UserGrant.AsObject) {
|
||||
const orgQuery = new OrgQuery();
|
||||
const orgIdQuery = new OrgIDQuery();
|
||||
orgIdQuery.setId(grant.grantedOrgId);
|
||||
orgQuery.setIdQuery(orgIdQuery);
|
||||
|
||||
const orgs = (await this.authService.listMyProjectOrgs(1, 0, [orgQuery])).resultList;
|
||||
if (orgs.length === 1) {
|
||||
this.authService.setActiveOrg(orgs[0]);
|
||||
this.router.navigate(['/users', grant.userId]);
|
||||
const org = await this.queryClient.fetchQuery(
|
||||
this.newOrganizationService.organizationByIdQueryOptions(grant.grantedOrgId),
|
||||
);
|
||||
if (org) {
|
||||
this.newOrganizationService.setOrgId(grant.grantedOrgId);
|
||||
await this.router.navigate(['/users', grant.userId]);
|
||||
} else {
|
||||
this.toast.showInfo('GRANTS.TOAST.CANTSHOWINFO', true);
|
||||
}
|
||||
|
@@ -5,10 +5,10 @@
|
||||
<cnsl-quickstart></cnsl-quickstart>
|
||||
|
||||
<ng-container *ngIf="['iam.read$'] | hasRole | async; else defaultHome">
|
||||
<cnsl-onboarding></cnsl-onboarding>
|
||||
<cnsl-onboarding />
|
||||
</ng-container>
|
||||
<ng-template #defaultHome>
|
||||
<cnsl-shortcuts></cnsl-shortcuts>
|
||||
<cnsl-shortcuts />
|
||||
</ng-template>
|
||||
|
||||
<p class="disclaimer cnsl-secondary-text">{{ 'HOME.DISCLAIMER' | translate }}</p>
|
||||
|
@@ -5,17 +5,17 @@ import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/
|
||||
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
||||
import { Router } from '@angular/router';
|
||||
import { passwordConfirmValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators';
|
||||
import { SetUpOrgRequest } from 'src/app/proto/generated/zitadel/admin_pb';
|
||||
import { Gender } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { LanguagesService } from 'src/app/services/languages.service';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { PasswordComplexityPolicy } from '@zitadel/proto/zitadel/policy_pb';
|
||||
import { NewMgmtService } from 'src/app/services/new-mgmt.service';
|
||||
import { PasswordComplexityValidatorFactoryService } from 'src/app/services/password-complexity-validator-factory.service';
|
||||
import { injectMutation } from '@tanstack/angular-query-experimental';
|
||||
import { NewOrganizationService } from '../../services/new-organization.service';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { SetUpOrgRequestSchema } from '@zitadel/proto/zitadel/admin_pb';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-org-create',
|
||||
@@ -32,32 +32,33 @@ import { PasswordComplexityValidatorFactoryService } from 'src/app/services/pass
|
||||
],
|
||||
})
|
||||
export class OrgCreateComponent {
|
||||
public orgForm: UntypedFormGroup = this.fb.group({
|
||||
protected orgForm = this.fb.group({
|
||||
name: ['', [requiredValidator]],
|
||||
domain: [''],
|
||||
});
|
||||
|
||||
public userForm?: UntypedFormGroup;
|
||||
public pwdForm?: UntypedFormGroup;
|
||||
protected userForm?: UntypedFormGroup;
|
||||
protected pwdForm?: UntypedFormGroup;
|
||||
|
||||
public genders: Gender[] = [Gender.GENDER_FEMALE, Gender.GENDER_MALE, Gender.GENDER_UNSPECIFIED];
|
||||
protected readonly genders: Gender[] = [Gender.GENDER_FEMALE, Gender.GENDER_MALE, Gender.GENDER_UNSPECIFIED];
|
||||
|
||||
public policy?: PasswordComplexityPolicy;
|
||||
public usePassword: boolean = false;
|
||||
protected policy?: PasswordComplexityPolicy;
|
||||
protected usePassword: boolean = false;
|
||||
|
||||
public forSelf: boolean = true;
|
||||
protected forSelf: boolean = true;
|
||||
|
||||
protected readonly setupOrgMutation = injectMutation(this.newOrganizationService.setupOrgMutationOptions);
|
||||
protected readonly addOrgMutation = injectMutation(this.newOrganizationService.addOrgMutationOptions);
|
||||
|
||||
constructor(
|
||||
private readonly router: Router,
|
||||
private readonly toast: ToastService,
|
||||
private readonly adminService: AdminService,
|
||||
private readonly _location: Location,
|
||||
private readonly location: Location,
|
||||
private readonly fb: UntypedFormBuilder,
|
||||
private readonly mgmtService: ManagementService,
|
||||
private readonly newMgmtService: NewMgmtService,
|
||||
private readonly authService: GrpcAuthService,
|
||||
private readonly passwordComplexityValidatorFactory: PasswordComplexityValidatorFactoryService,
|
||||
public readonly langSvc: LanguagesService,
|
||||
private readonly newOrganizationService: NewOrganizationService,
|
||||
breadcrumbService: BreadcrumbService,
|
||||
) {
|
||||
const instanceBread = new Breadcrumb({
|
||||
@@ -73,38 +74,38 @@ export class OrgCreateComponent {
|
||||
public createSteps: number = 2;
|
||||
public currentCreateStep: number = 1;
|
||||
|
||||
public finish(): void {
|
||||
const createOrgRequest: SetUpOrgRequest.Org = new SetUpOrgRequest.Org();
|
||||
createOrgRequest.setName(this.name?.value);
|
||||
createOrgRequest.setDomain(this.domain?.value);
|
||||
public async finish(): Promise<void> {
|
||||
const req: MessageInitShape<typeof SetUpOrgRequestSchema> = {
|
||||
org: {
|
||||
name: this.name?.value,
|
||||
domain: this.domain?.value,
|
||||
},
|
||||
user: {
|
||||
case: 'human',
|
||||
value: {
|
||||
email: {
|
||||
email: this.email?.value,
|
||||
isEmailVerified: this.isVerified?.value,
|
||||
},
|
||||
userName: this.userName?.value,
|
||||
profile: {
|
||||
firstName: this.firstName?.value,
|
||||
lastName: this.lastName?.value,
|
||||
nickName: this.nickName?.value,
|
||||
gender: this.gender?.value,
|
||||
preferredLanguage: this.preferredLanguage?.value,
|
||||
},
|
||||
password: this.usePassword && this.password ? this.password.value : undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const humanRequest: SetUpOrgRequest.Human = new SetUpOrgRequest.Human();
|
||||
humanRequest.setEmail(
|
||||
new SetUpOrgRequest.Human.Email().setEmail(this.email?.value).setIsEmailVerified(this.isVerified?.value),
|
||||
);
|
||||
humanRequest.setUserName(this.userName?.value);
|
||||
|
||||
const profile: SetUpOrgRequest.Human.Profile = new SetUpOrgRequest.Human.Profile();
|
||||
profile.setFirstName(this.firstName?.value);
|
||||
profile.setLastName(this.lastName?.value);
|
||||
profile.setNickName(this.nickName?.value);
|
||||
profile.setGender(this.gender?.value);
|
||||
profile.setPreferredLanguage(this.preferredLanguage?.value);
|
||||
|
||||
humanRequest.setProfile(profile);
|
||||
if (this.usePassword && this.password) {
|
||||
humanRequest.setPassword(this.password?.value);
|
||||
try {
|
||||
await this.setupOrgMutation.mutateAsync(req);
|
||||
await this.router.navigate(['/orgs']);
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
|
||||
this.adminService
|
||||
.SetUpOrg(createOrgRequest, humanRequest)
|
||||
.then(() => {
|
||||
this.authService.revalidateOrgs().then();
|
||||
this.router.navigate(['/orgs']).then();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public next(): void {
|
||||
@@ -161,17 +162,15 @@ export class OrgCreateComponent {
|
||||
}
|
||||
}
|
||||
|
||||
public createOrgForSelf(): void {
|
||||
if (this.name && this.name.value) {
|
||||
this.mgmtService
|
||||
.addOrg(this.name.value)
|
||||
.then(() => {
|
||||
this.authService.revalidateOrgs().then();
|
||||
this.router.navigate(['/orgs']).then();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
public async createOrgForSelf() {
|
||||
if (!this.name?.value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.addOrgMutation.mutateAsync(this.name.value);
|
||||
await this.router.navigate(['/orgs']);
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,6 +223,6 @@ export class OrgCreateComponent {
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this._location.back();
|
||||
this.location.back();
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,6 @@
|
||||
<h1>{{ 'ORG.PAGES.LIST' | translate }}</h1>
|
||||
<p class="org-desc cnsl-secondary-text">{{ 'ORG.PAGES.LISTDESCRIPTION' | translate }}</p>
|
||||
|
||||
<cnsl-org-table></cnsl-org-table>
|
||||
<cnsl-org-table />
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,78 +1,72 @@
|
||||
<cnsl-top-view
|
||||
*ngIf="['org.write:' + org?.id, 'org.write$'] | hasRole as hasWrite$"
|
||||
[hasBackButton]="false"
|
||||
title="{{ org?.name }}"
|
||||
[isActive]="org?.state === OrgState.ORG_STATE_ACTIVE"
|
||||
[isInactive]="org?.state === OrgState.ORG_STATE_INACTIVE"
|
||||
[hasContributors]="true"
|
||||
stateTooltip="{{ 'ORG.STATE.' + org?.state | translate }}"
|
||||
[hasActions]="hasWrite$ | async"
|
||||
>
|
||||
<ng-container topActions *ngIf="hasWrite$ | async">
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="org?.state === OrgState.ORG_STATE_ACTIVE"
|
||||
(click)="changeState(OrgState.ORG_STATE_INACTIVE)"
|
||||
>
|
||||
{{ 'ORG.PAGES.DEACTIVATE' | translate }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="org?.state === OrgState.ORG_STATE_INACTIVE"
|
||||
(click)="changeState(OrgState.ORG_STATE_ACTIVE)"
|
||||
>
|
||||
{{ 'ORG.PAGES.REACTIVATE' | translate }}
|
||||
</button>
|
||||
|
||||
<button data-e2e="rename" mat-menu-item (click)="renameOrg()">
|
||||
{{ 'ORG.PAGES.RENAME.ACTION' | translate }}
|
||||
</button>
|
||||
|
||||
<button data-e2e="delete" mat-menu-item (click)="deleteOrg()">
|
||||
{{ 'ORG.PAGES.DELETE' | translate }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<cnsl-contributors
|
||||
topContributors
|
||||
[totalResult]="totalMemberResult"
|
||||
[loading]="loading$ | async"
|
||||
[membersSubject]="membersSubject"
|
||||
title="{{ 'PROJECT.MEMBER.TITLE' | translate }}"
|
||||
description="{{ 'PROJECT.MEMBER.TITLEDESC' | translate }}"
|
||||
(addClicked)="openAddMember()"
|
||||
(showDetailClicked)="showDetail()"
|
||||
(refreshClicked)="loadMembers()"
|
||||
[disabled]="(['org.member.write'] | hasRole | async) === false"
|
||||
<ng-container *ngIf="orgQuery.data() as org">
|
||||
<cnsl-top-view
|
||||
*ngIf="['org.write:' + org.id, 'org.write$'] | hasRole as hasWrite$"
|
||||
[hasBackButton]="false"
|
||||
title="{{ org.name }}"
|
||||
[isActive]="org.state === OrganizationState.ACTIVE"
|
||||
[isInactive]="org.state === OrganizationState.INACTIVE"
|
||||
[hasContributors]="true"
|
||||
stateTooltip="{{ 'ORG.STATE.' + org.state | translate }}"
|
||||
[hasActions]="hasWrite$ | async"
|
||||
>
|
||||
</cnsl-contributors>
|
||||
<ng-container topActions *ngIf="hasWrite$ | async">
|
||||
<button mat-menu-item *ngIf="org.state === OrganizationState.ACTIVE" (click)="changeState(OrganizationState.INACTIVE)">
|
||||
{{ 'ORG.PAGES.DEACTIVATE' | translate }}
|
||||
</button>
|
||||
|
||||
<cnsl-info-row topContent *ngIf="org" [org]="org"></cnsl-info-row>
|
||||
</cnsl-top-view>
|
||||
<div class="max-width-container">
|
||||
<cnsl-meta-layout>
|
||||
<ng-container *ngIf="['policy.read'] | hasRole | async; else nopolicyreadpermission">
|
||||
<cnsl-settings-grid [type]="PolicyComponentServiceType.MGMT"></cnsl-settings-grid>
|
||||
<button mat-menu-item *ngIf="org.state === OrganizationState.INACTIVE" (click)="changeState(OrganizationState.ACTIVE)">
|
||||
{{ 'ORG.PAGES.REACTIVATE' | translate }}
|
||||
</button>
|
||||
|
||||
<button data-e2e="rename" mat-menu-item (click)="renameOrg(org)">
|
||||
{{ 'ORG.PAGES.RENAME.ACTION' | translate }}
|
||||
</button>
|
||||
|
||||
<button data-e2e="delete" mat-menu-item (click)="deleteOrg(org)">
|
||||
{{ 'ORG.PAGES.DELETE' | translate }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<cnsl-contributors
|
||||
topContributors
|
||||
[totalResult]="totalMemberResult"
|
||||
[loading]="loading$ | async"
|
||||
[membersSubject]="membersSubject"
|
||||
title="{{ 'PROJECT.MEMBER.TITLE' | translate }}"
|
||||
description="{{ 'PROJECT.MEMBER.TITLEDESC' | translate }}"
|
||||
(addClicked)="openAddMember()"
|
||||
(showDetailClicked)="showDetail()"
|
||||
(refreshClicked)="loadMembers()"
|
||||
[disabled]="(['org.member.write'] | hasRole | async) === false"
|
||||
>
|
||||
</cnsl-contributors>
|
||||
|
||||
<cnsl-metadata
|
||||
[description]="'DESCRIPTIONS.ORG.METADATA' | translate"
|
||||
[metadata]="metadata"
|
||||
[disabled]="(['org.write'] | hasRole | async) === false"
|
||||
(editClicked)="editMetadata()"
|
||||
(refresh)="loadMetadata()"
|
||||
></cnsl-metadata>
|
||||
<cnsl-info-row topContent [org]="org"></cnsl-info-row>
|
||||
</cnsl-top-view>
|
||||
<div class="max-width-container">
|
||||
<cnsl-meta-layout>
|
||||
<ng-container *ngIf="['policy.read'] | hasRole | async; else nopolicyreadpermission">
|
||||
<cnsl-settings-grid [type]="PolicyComponentServiceType.MGMT"></cnsl-settings-grid>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #nopolicyreadpermission>
|
||||
<div class="no-permission-warn-wrapper">
|
||||
<cnsl-info-section class="info-section-warn" [fitWidth]="true" [type]="InfoSectionType.ALERT">{{
|
||||
'ORG.PAGES.NOPERMISSION' | translate
|
||||
}}</cnsl-info-section>
|
||||
<cnsl-metadata
|
||||
[description]="'DESCRIPTIONS.ORG.METADATA' | translate"
|
||||
[metadata]="metadata"
|
||||
[disabled]="(['org.write'] | hasRole | async) === false"
|
||||
(editClicked)="editMetadata()"
|
||||
(refresh)="loadMetadata()"
|
||||
></cnsl-metadata>
|
||||
|
||||
<ng-template #nopolicyreadpermission>
|
||||
<div class="no-permission-warn-wrapper">
|
||||
<cnsl-info-section class="info-section-warn" [fitWidth]="true" [type]="InfoSectionType.ALERT">{{
|
||||
'ORG.PAGES.NOPERMISSION' | translate
|
||||
}}</cnsl-info-section>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<div metainfo>
|
||||
<cnsl-changes *ngIf="org && reloadChanges()" [changeType]="ChangeType.ORG" [id]="org.id"></cnsl-changes>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<div metainfo>
|
||||
<cnsl-changes *ngIf="org" [changeType]="ChangeType.ORG" [id]="org.id"></cnsl-changes>
|
||||
</div>
|
||||
</cnsl-meta-layout>
|
||||
</div>
|
||||
</cnsl-meta-layout>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, effect, OnInit, signal } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Router } from '@angular/router';
|
||||
import { BehaviorSubject, from, Observable, of, Subject, takeUntil } from 'rxjs';
|
||||
import { catchError, finalize, map } from 'rxjs/operators';
|
||||
import { BehaviorSubject, from, lastValueFrom, Observable, of } from 'rxjs';
|
||||
import { catchError, distinctUntilChanged, finalize, map } from 'rxjs/operators';
|
||||
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component';
|
||||
import { ChangeType } from 'src/app/modules/changes/changes.component';
|
||||
import { InfoSectionType } from 'src/app/modules/info-section/info-section.component';
|
||||
@@ -12,24 +12,24 @@ import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-comp
|
||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
import { Member } from 'src/app/proto/generated/zitadel/member_pb';
|
||||
import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb';
|
||||
import { Org, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import { User } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { NewOrganizationService } from '../../../services/new-organization.service';
|
||||
import { injectMutation } from '@tanstack/angular-query-experimental';
|
||||
import { Organization, OrganizationState } from '@zitadel/proto/zitadel/org/v2/org_pb';
|
||||
import { toObservable } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-org-detail',
|
||||
templateUrl: './org-detail.component.html',
|
||||
styleUrls: ['./org-detail.component.scss'],
|
||||
})
|
||||
export class OrgDetailComponent implements OnInit, OnDestroy {
|
||||
public org?: Org.AsObject;
|
||||
export class OrgDetailComponent implements OnInit {
|
||||
public PolicyComponentServiceType: any = PolicyComponentServiceType;
|
||||
|
||||
public OrgState: any = OrgState;
|
||||
public OrganizationState = OrganizationState;
|
||||
public ChangeType: any = ChangeType;
|
||||
|
||||
public metadata: Metadata.AsObject[] = [];
|
||||
@@ -40,18 +40,25 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
public totalMemberResult: number = 0;
|
||||
public membersSubject: BehaviorSubject<Member.AsObject[]> = new BehaviorSubject<Member.AsObject[]>([]);
|
||||
private destroy$: Subject<void> = new Subject();
|
||||
|
||||
public InfoSectionType: any = InfoSectionType;
|
||||
|
||||
protected readonly orgQuery = this.newOrganizationService.activeOrganizationQuery();
|
||||
private readonly reactivateOrgMutation = injectMutation(this.newOrganizationService.reactivateOrgMutationOptions);
|
||||
private readonly deactivateOrgMutation = injectMutation(this.newOrganizationService.deactivateOrgMutationOptions);
|
||||
private readonly deleteOrgMutation = injectMutation(this.newOrganizationService.deleteOrgMutationOptions);
|
||||
private readonly renameOrgMutation = injectMutation(this.newOrganizationService.renameOrgMutationOptions);
|
||||
|
||||
protected reloadChanges = signal(true);
|
||||
|
||||
constructor(
|
||||
private auth: GrpcAuthService,
|
||||
private dialog: MatDialog,
|
||||
public mgmtService: ManagementService,
|
||||
private adminService: AdminService,
|
||||
private toast: ToastService,
|
||||
private router: Router,
|
||||
private readonly dialog: MatDialog,
|
||||
private readonly mgmtService: ManagementService,
|
||||
private readonly toast: ToastService,
|
||||
private readonly router: Router,
|
||||
private readonly newOrganizationService: NewOrganizationService,
|
||||
breadcrumbService: BreadcrumbService,
|
||||
cdr: ChangeDetectorRef,
|
||||
) {
|
||||
const bread: Breadcrumb = {
|
||||
type: BreadcrumbType.ORG,
|
||||
@@ -59,26 +66,30 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
breadcrumbService.setBreadcrumb([bread]);
|
||||
|
||||
auth.activeOrgChanged.pipe(takeUntil(this.destroy$)).subscribe((org) => {
|
||||
if (this.org && org) {
|
||||
this.getData();
|
||||
this.loadMetadata();
|
||||
effect(() => {
|
||||
const orgId = this.newOrganizationService.orgId();
|
||||
if (!orgId) {
|
||||
return;
|
||||
}
|
||||
this.loadMembers();
|
||||
this.loadMetadata();
|
||||
});
|
||||
|
||||
// force rerender changes because it is not reactive to orgId changes
|
||||
toObservable(this.newOrganizationService.orgId).subscribe(() => {
|
||||
this.reloadChanges.set(false);
|
||||
cdr.detectChanges();
|
||||
this.reloadChanges.set(true);
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.getData();
|
||||
this.loadMembers();
|
||||
this.loadMetadata();
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
public changeState(newState: OrgState): void {
|
||||
if (newState === OrgState.ORG_STATE_ACTIVE) {
|
||||
public async changeState(newState: OrganizationState) {
|
||||
if (newState === OrganizationState.ACTIVE) {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.REACTIVATE',
|
||||
@@ -88,20 +99,20 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
this.mgmtService
|
||||
.reactivateOrg()
|
||||
.then(() => {
|
||||
this.toast.showInfo('ORG.TOAST.REACTIVATED', true);
|
||||
this.org!.state = OrgState.ORG_STATE_ACTIVE;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (newState === OrgState.ORG_STATE_INACTIVE) {
|
||||
const resp = await lastValueFrom(dialogRef.afterClosed());
|
||||
if (!resp) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.reactivateOrgMutation.mutateAsync();
|
||||
this.toast.showInfo('ORG.TOAST.REACTIVATED', true);
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (newState === OrganizationState.INACTIVE) {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.DEACTIVATE',
|
||||
@@ -111,23 +122,21 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
this.mgmtService
|
||||
.deactivateOrg()
|
||||
.then(() => {
|
||||
this.toast.showInfo('ORG.TOAST.DEACTIVATED', true);
|
||||
this.org!.state = OrgState.ORG_STATE_INACTIVE;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const resp = await lastValueFrom(dialogRef.afterClosed());
|
||||
if (!resp) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.deactivateOrgMutation.mutateAsync();
|
||||
this.toast.showInfo('ORG.TOAST.DEACTIVATED', true);
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public deleteOrg(): void {
|
||||
public async deleteOrg(org: Organization) {
|
||||
const mgmtUserData = {
|
||||
confirmKey: 'ACTIONS.DELETE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
@@ -136,66 +145,24 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
|
||||
hintKey: 'ORG.DIALOG.DELETE.TYPENAME',
|
||||
hintParam: 'ORG.DIALOG.DELETE.DESCRIPTION',
|
||||
confirmationKey: 'ORG.DIALOG.DELETE.ORGNAME',
|
||||
confirmation: this.org?.name,
|
||||
confirmation: org.name,
|
||||
};
|
||||
|
||||
if (this.org) {
|
||||
let dialogRef;
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: mgmtUserData,
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: mgmtUserData,
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
// Before we remove the org we get the current default org
|
||||
// we have to query before the current org is removed
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
this.adminService
|
||||
.getDefaultOrg()
|
||||
.then((response) => {
|
||||
const org = response?.org;
|
||||
if (org) {
|
||||
// We now remove the org
|
||||
this.mgmtService
|
||||
.removeOrg()
|
||||
.then(() => {
|
||||
setTimeout(() => {
|
||||
// We change active org to default org as
|
||||
// current org was deleted to avoid Organization doesn't exist
|
||||
this.auth.setActiveOrg(org);
|
||||
// Now we visit orgs
|
||||
this.router.navigate(['/orgs']);
|
||||
}, 1000);
|
||||
this.toast.showInfo('ORG.TOAST.DELETED', true);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
} else {
|
||||
this.toast.showError('ORG.TOAST.DEFAULTORGNOTFOUND', false, true);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!(await lastValueFrom(dialogRef.afterClosed()))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async getData(): Promise<void> {
|
||||
this.mgmtService
|
||||
.getMyOrg()
|
||||
.then((resp) => {
|
||||
if (resp.org) {
|
||||
this.org = resp.org;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
this.loadMembers();
|
||||
try {
|
||||
await this.deleteOrgMutation.mutateAsync();
|
||||
await this.router.navigate(['/orgs']);
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public openAddMember(): void {
|
||||
@@ -234,8 +201,8 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
public showDetail(): void {
|
||||
this.router.navigate(['org/members']);
|
||||
public showDetail() {
|
||||
return this.router.navigate(['org/members']);
|
||||
}
|
||||
|
||||
public loadMembers(): void {
|
||||
@@ -296,10 +263,10 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
public renameOrg(): void {
|
||||
public async renameOrg(org: Organization): Promise<void> {
|
||||
const dialogRef = this.dialog.open(NameDialogComponent, {
|
||||
data: {
|
||||
name: this.org?.name,
|
||||
name: org.name,
|
||||
titleKey: 'ORG.PAGES.RENAME.TITLE',
|
||||
descKey: 'ORG.PAGES.RENAME.DESCRIPTION',
|
||||
labelKey: 'ORG.PAGES.NAME',
|
||||
@@ -307,37 +274,20 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((name) => {
|
||||
if (name) {
|
||||
this.updateOrg(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
const name = await lastValueFrom(dialogRef.afterClosed());
|
||||
if (org.name === name) {
|
||||
return;
|
||||
}
|
||||
|
||||
public updateOrg(name: string): void {
|
||||
if (this.org) {
|
||||
this.mgmtService
|
||||
.updateOrg(name)
|
||||
.then(() => {
|
||||
this.toast.showInfo('ORG.TOAST.UPDATED', true);
|
||||
if (this.org) {
|
||||
this.org.name = name;
|
||||
}
|
||||
this.mgmtService
|
||||
.getMyOrg()
|
||||
.then((resp) => {
|
||||
if (resp.org) {
|
||||
this.org = resp.org;
|
||||
this.auth.setActiveOrg(resp.org);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
try {
|
||||
await this.renameOrgMutation.mutateAsync(name);
|
||||
this.toast.showInfo('ORG.TOAST.UPDATED', true);
|
||||
const resp = await this.mgmtService.getMyOrg();
|
||||
if (resp.org) {
|
||||
await this.newOrganizationService.setOrgId(resp.org.id);
|
||||
}
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -220,13 +220,13 @@ export class ProjectGridComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private async getPrefixedItem(key: string): Promise<string | null> {
|
||||
const org = this.storage.getItem<Org.AsObject>(StorageKey.organization, StorageLocation.session) as Org.AsObject;
|
||||
return localStorage.getItem(`${org?.id}:${key}`);
|
||||
const org = this.storage.getItem(StorageKey.organizationId, StorageLocation.session);
|
||||
return localStorage.getItem(`${org}:${key}`);
|
||||
}
|
||||
|
||||
private async setPrefixedItem(key: string, value: any): Promise<void> {
|
||||
const org = this.storage.getItem<Org.AsObject>(StorageKey.organization, StorageLocation.session) as Org.AsObject;
|
||||
return localStorage.setItem(`${org.id}:${key}`, value);
|
||||
const org = this.storage.getItem(StorageKey.organizationId, StorageLocation.session);
|
||||
return localStorage.setItem(`${org}:${key}`, value);
|
||||
}
|
||||
|
||||
public navigateToProject(type: ProjectType, item: Project.AsObject | GrantedProject.AsObject, event: any): void {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<cnsl-create-layout
|
||||
*ngIf="activateOrganizationQuery.data() as org"
|
||||
title="{{ 'GRANTS.CREATE.TITLE' | translate }}"
|
||||
[createSteps]="createSteps"
|
||||
[currentCreateStep]="currentCreateStep"
|
||||
|
@@ -1,17 +1,16 @@
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { Component, effect, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { ProjectType } from 'src/app/modules/project-members/project-members-datasource';
|
||||
import { UserTarget } from 'src/app/modules/search-user-autocomplete/search-user-autocomplete.component';
|
||||
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
|
||||
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import { GrantedProject, Project } from 'src/app/proto/generated/zitadel/project_pb';
|
||||
import { User } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { StorageKey, StorageLocation, StorageService } from 'src/app/services/storage.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { NewOrganizationService } from '../../services/new-organization.service';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-user-grant-create',
|
||||
@@ -21,7 +20,7 @@ import { ToastService } from 'src/app/services/toast.service';
|
||||
export class UserGrantCreateComponent implements OnDestroy {
|
||||
public context!: UserGrantContext;
|
||||
|
||||
public org?: Org.AsObject;
|
||||
public activateOrganizationQuery = this.newOrganizationService.activeOrganizationQuery();
|
||||
public userIds: string[] = [];
|
||||
|
||||
public project?: Project.AsObject;
|
||||
@@ -37,16 +36,15 @@ export class UserGrantCreateComponent implements OnDestroy {
|
||||
public user?: User.AsObject;
|
||||
public UserTarget: any = UserTarget;
|
||||
|
||||
public editState: boolean = false;
|
||||
private destroy$: Subject<void> = new Subject();
|
||||
|
||||
constructor(
|
||||
private userService: ManagementService,
|
||||
private toast: ToastService,
|
||||
private _location: Location,
|
||||
private route: ActivatedRoute,
|
||||
private mgmtService: ManagementService,
|
||||
private storage: StorageService,
|
||||
private readonly userService: ManagementService,
|
||||
private readonly toast: ToastService,
|
||||
private readonly _location: Location,
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly mgmtService: ManagementService,
|
||||
private readonly newOrganizationService: NewOrganizationService,
|
||||
breadcrumbService: BreadcrumbService,
|
||||
) {
|
||||
breadcrumbService.setBreadcrumb([
|
||||
@@ -101,10 +99,11 @@ export class UserGrantCreateComponent implements OnDestroy {
|
||||
}
|
||||
});
|
||||
|
||||
const temporg = this.storage.getItem<Org.AsObject>(StorageKey.organization, StorageLocation.session);
|
||||
if (temporg) {
|
||||
this.org = temporg;
|
||||
}
|
||||
effect(() => {
|
||||
if (this.activateOrganizationQuery.isError()) {
|
||||
this.toast.showError(this.activateOrganizationQuery.error());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
|
@@ -33,6 +33,7 @@ import { PasswordComplexityValidatorFactoryService } from 'src/app/services/pass
|
||||
import { NewFeatureService } from 'src/app/services/new-feature.service';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { NewOrganizationService } from '../../../../services/new-organization.service';
|
||||
|
||||
type PwdForm = ReturnType<UserCreateV2Component['buildPwdForm']>;
|
||||
type AuthenticationFactor =
|
||||
@@ -54,6 +55,7 @@ export class UserCreateV2Component implements OnInit {
|
||||
private readonly passwordComplexityPolicy$: Observable<PasswordComplexityPolicy>;
|
||||
protected readonly authenticationFactor$: Observable<AuthenticationFactor>;
|
||||
private readonly useLoginV2$: Observable<LoginV2FeatureFlag | undefined>;
|
||||
private orgId = this.organizationService.getOrgId();
|
||||
|
||||
constructor(
|
||||
private readonly router: Router,
|
||||
@@ -67,6 +69,7 @@ export class UserCreateV2Component implements OnInit {
|
||||
private readonly route: ActivatedRoute,
|
||||
protected readonly location: Location,
|
||||
private readonly authService: GrpcAuthService,
|
||||
private readonly organizationService: NewOrganizationService,
|
||||
) {
|
||||
this.userForm = this.buildUserForm();
|
||||
|
||||
@@ -182,12 +185,11 @@ export class UserCreateV2Component implements OnInit {
|
||||
private async createUserV2Try(authenticationFactor: AuthenticationFactor) {
|
||||
this.loading.set(true);
|
||||
|
||||
const org = await this.authService.getActiveOrg();
|
||||
|
||||
this.organizationService.getOrgId();
|
||||
const userValues = this.userForm.getRawValue();
|
||||
|
||||
const humanReq: MessageInitShape<typeof AddHumanUserRequestSchema> = {
|
||||
organization: { org: { case: 'orgId', value: org.id } },
|
||||
organization: { org: { case: 'orgId', value: this.orgId() } },
|
||||
username: userValues.username,
|
||||
profile: {
|
||||
givenName: userValues.givenName,
|
||||
|
@@ -1,34 +1,34 @@
|
||||
<ng-container *ngIf="user$ | async as userQuery">
|
||||
<ng-container>
|
||||
<cnsl-top-view
|
||||
title="{{ userName$ | async }}"
|
||||
sub="{{ user(userQuery)?.preferredLoginName }}"
|
||||
[isActive]="user(userQuery)?.state === UserState.ACTIVE"
|
||||
[isInactive]="user(userQuery)?.state === UserState.INACTIVE"
|
||||
stateTooltip="{{ 'USER.STATE.' + user(userQuery)?.state | translate }}"
|
||||
[title]="userName()"
|
||||
sub="{{ user.data()?.preferredLoginName }}"
|
||||
[isActive]="user.data()?.state === UserState.ACTIVE"
|
||||
[isInactive]="user.data()?.state === UserState.INACTIVE"
|
||||
stateTooltip="{{ 'USER.STATE.' + user.data()?.state | translate }}"
|
||||
[hasBackButton]="['org.read'] | hasRole | async"
|
||||
>
|
||||
<cnsl-info-row
|
||||
topContent
|
||||
*ngIf="user(userQuery) as user"
|
||||
*ngIf="user.data() as user"
|
||||
[user]="user"
|
||||
[loginPolicy]="(loginPolicy$ | async) ?? undefined"
|
||||
></cnsl-info-row>
|
||||
/>
|
||||
</cnsl-top-view>
|
||||
|
||||
<div *ngIf="(user$ | async)?.state === 'loading'" class="max-width-container">
|
||||
<div *ngIf="user.isLoading()" class="max-width-container">
|
||||
<div class="user-spinner-wrapper">
|
||||
<mat-progress-spinner diameter="25" color="primary" mode="indeterminate"></mat-progress-spinner>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-width-container">
|
||||
<cnsl-meta-layout *ngIf="user(userQuery) as user">
|
||||
<cnsl-meta-layout *ngIf="user.data() as user">
|
||||
<cnsl-sidenav
|
||||
[setting]="currentSetting$()"
|
||||
(settingChange)="currentSetting$.set($event)"
|
||||
[settingsList]="settingsList"
|
||||
>
|
||||
<ng-container *ngIf="currentSetting$().id === 'general' && humanUser(userQuery) as humanUser">
|
||||
<ng-container *ngIf="currentSetting$().id === 'general' && humanUser(user) as humanUser">
|
||||
<cnsl-card
|
||||
*ngIf="humanUser.type.value.profile as profile"
|
||||
class="app-card"
|
||||
@@ -45,7 +45,7 @@
|
||||
(changedLanguage)="changedLanguage($event)"
|
||||
(changeUsernameClicked)="changeUsername(user)"
|
||||
(submitData)="saveProfile(user, $event)"
|
||||
(avatarChanged)="refreshChanges$.emit()"
|
||||
(avatarChanged)="invalidateUser()"
|
||||
>
|
||||
</cnsl-detail-form>
|
||||
</cnsl-card>
|
||||
@@ -58,7 +58,7 @@
|
||||
class="icon-button"
|
||||
card-actions
|
||||
mat-icon-button
|
||||
(click)="refreshChanges$.emit()"
|
||||
(click)="invalidateUser()"
|
||||
matTooltip="{{ 'ACTIONS.REFRESH' | translate }}"
|
||||
>
|
||||
<mat-icon class="icon">refresh</mat-icon>
|
||||
@@ -94,7 +94,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting$().id === 'security'">
|
||||
<cnsl-card *ngIf="humanUser(userQuery) as humanUser" title="{{ 'USER.PASSWORD.TITLE' | translate }}">
|
||||
<cnsl-card *ngIf="humanUser(user) as humanUser" title="{{ 'USER.PASSWORD.TITLE' | translate }}">
|
||||
<div class="contact-method-col">
|
||||
<div class="contact-method-row">
|
||||
<div class="left">
|
||||
@@ -121,7 +121,7 @@
|
||||
<cnsl-auth-passwordless #mfaComponent></cnsl-auth-passwordless>
|
||||
|
||||
<cnsl-auth-user-mfa
|
||||
[phoneVerified]="humanUser(userQuery)?.type?.value?.phone?.isVerified ?? false"
|
||||
[phoneVerified]="humanUser(user)?.type?.value?.phone?.isVerified ?? false"
|
||||
></cnsl-auth-user-mfa>
|
||||
</ng-container>
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
</cnsl-sidenav>
|
||||
|
||||
<div metainfo>
|
||||
<cnsl-changes class="changes" [refresh]="refreshChanges$" [changeType]="ChangeType.MYUSER"> </cnsl-changes>
|
||||
<cnsl-changes class="changes" [refresh]="refreshChanges$" [changeType]="ChangeType.MYUSER" />
|
||||
</div>
|
||||
</cnsl-meta-layout>
|
||||
</div>
|
||||
|
@@ -1,11 +1,10 @@
|
||||
import { MediaMatcher } from '@angular/cdk/layout';
|
||||
import { Component, DestroyRef, EventEmitter, OnInit, signal } from '@angular/core';
|
||||
import { Component, computed, DestroyRef, effect, OnInit, signal } from '@angular/core';
|
||||
import { Validators } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Buffer } from 'buffer';
|
||||
import { defer, EMPTY, mergeWith, Observable, of, shareReplay, Subject, switchMap, take } from 'rxjs';
|
||||
import { defer, EMPTY, Observable, of, shareReplay, Subject, switchMap, take } from 'rxjs';
|
||||
import { ChangeType } from 'src/app/modules/changes/changes.component';
|
||||
import { phoneValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators';
|
||||
import { InfoDialogComponent } from 'src/app/modules/info-dialog/info-dialog.component';
|
||||
@@ -34,8 +33,7 @@ import { Metadata } from '@zitadel/proto/zitadel/metadata_pb';
|
||||
import { UserService } from 'src/app/services/user.service';
|
||||
import { LoginPolicy } from '@zitadel/proto/zitadel/policy_pb';
|
||||
import { query } from '@angular/animations';
|
||||
|
||||
type UserQuery = { state: 'success'; value: User } | { state: 'error'; error: any } | { state: 'loading'; value?: User };
|
||||
import { QueryClient } from '@tanstack/angular-query-experimental';
|
||||
|
||||
type MetadataQuery =
|
||||
| { state: 'success'; value: Metadata[] }
|
||||
@@ -57,7 +55,6 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
protected readonly UserState = UserState;
|
||||
|
||||
protected USERGRANTCONTEXT: UserGrantContext = UserGrantContext.AUTHUSER;
|
||||
protected readonly refreshChanges$: EventEmitter<void> = new EventEmitter();
|
||||
protected readonly refreshMetadata$ = new Subject<true>();
|
||||
|
||||
protected readonly settingsList: SidenavSetting[] = [
|
||||
@@ -72,12 +69,33 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
requiredRoles: { [PolicyComponentServiceType.MGMT]: ['user.read'] },
|
||||
},
|
||||
];
|
||||
protected readonly user$: Observable<UserQuery>;
|
||||
protected readonly metadata$: Observable<MetadataQuery>;
|
||||
private readonly savedLanguage$: Observable<string>;
|
||||
protected readonly currentSetting$ = signal<SidenavSetting>(this.settingsList[0]);
|
||||
protected readonly loginPolicy$: Observable<LoginPolicy>;
|
||||
protected readonly userName$: Observable<string>;
|
||||
protected readonly user = this.userService.userQuery();
|
||||
protected readonly refreshChanges$ = new Subject<void>();
|
||||
|
||||
protected readonly userName = computed(() => {
|
||||
const user = this.user.data();
|
||||
if (!user) {
|
||||
return '';
|
||||
}
|
||||
if (user.type.case === 'human') {
|
||||
return user.type.value.profile?.displayName ?? '';
|
||||
}
|
||||
if (user.type.case === 'machine') {
|
||||
return user.type.value.name;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
protected savedLanguage = computed(() => {
|
||||
const user = this.user.data();
|
||||
if (!user || user.type.case !== 'human' || !user.type.value.profile?.preferredLanguage) {
|
||||
return this.translate.defaultLang;
|
||||
}
|
||||
return user.type.value.profile?.preferredLanguage;
|
||||
});
|
||||
|
||||
constructor(
|
||||
private translate: TranslateService,
|
||||
@@ -92,11 +110,8 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
private readonly newMgmtService: NewMgmtService,
|
||||
private readonly userService: UserService,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
private readonly router: Router,
|
||||
private readonly queryClient: QueryClient,
|
||||
) {
|
||||
this.user$ = this.getUser$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.userName$ = this.getUserName(this.user$);
|
||||
this.savedLanguage$ = this.getSavedLanguage$(this.user$);
|
||||
this.metadata$ = this.getMetadata$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
|
||||
this.loginPolicy$ = defer(() => this.newMgmtService.getLoginPolicy()).pipe(
|
||||
@@ -104,61 +119,41 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
map(({ policy }) => policy),
|
||||
filter(Boolean),
|
||||
);
|
||||
}
|
||||
|
||||
getUserName(user$: Observable<UserQuery>) {
|
||||
return user$.pipe(
|
||||
map((query) => {
|
||||
const user = this.user(query);
|
||||
if (!user) {
|
||||
return '';
|
||||
}
|
||||
if (user.type.case === 'human') {
|
||||
return user.type.value.profile?.displayName ?? '';
|
||||
}
|
||||
if (user.type.case === 'machine') {
|
||||
return user.type.value.name;
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
);
|
||||
}
|
||||
effect(() => {
|
||||
const user = this.user.data();
|
||||
if (!user || user.type.case !== 'human') {
|
||||
return;
|
||||
}
|
||||
|
||||
getSavedLanguage$(user$: Observable<UserQuery>) {
|
||||
return user$.pipe(
|
||||
switchMap((query) => {
|
||||
if (query.state !== 'success' || query.value.type.case !== 'human') {
|
||||
return EMPTY;
|
||||
}
|
||||
return query.value.type.value.profile?.preferredLanguage ?? EMPTY;
|
||||
}),
|
||||
startWith(this.translate.defaultLang),
|
||||
);
|
||||
}
|
||||
this.breadcrumbService.setBreadcrumb([
|
||||
new Breadcrumb({
|
||||
type: BreadcrumbType.AUTHUSER,
|
||||
name: user.type.value.profile?.displayName,
|
||||
routerLink: ['/users', 'me'],
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
ngOnInit(): void {
|
||||
this.user$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((query) => {
|
||||
if ((query.state === 'loading' || query.state === 'success') && query.value?.type.case === 'human') {
|
||||
this.breadcrumbService.setBreadcrumb([
|
||||
new Breadcrumb({
|
||||
type: BreadcrumbType.AUTHUSER,
|
||||
name: query.value.type.value.profile?.displayName,
|
||||
routerLink: ['/users', 'me'],
|
||||
}),
|
||||
]);
|
||||
effect(() => {
|
||||
const error = this.user.error();
|
||||
if (error) {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
});
|
||||
|
||||
this.user$.pipe(mergeWith(this.metadata$), takeUntilDestroyed(this.destroyRef)).subscribe((query) => {
|
||||
effect(() => {
|
||||
this.translate.use(this.savedLanguage());
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.metadata$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((query) => {
|
||||
if (query.state == 'error') {
|
||||
this.toast.showError(query.error);
|
||||
}
|
||||
});
|
||||
|
||||
this.savedLanguage$
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((savedLanguage) => this.translate.use(savedLanguage));
|
||||
|
||||
const param = this.route.snapshot.queryParamMap.get('id');
|
||||
if (!param) {
|
||||
return;
|
||||
@@ -170,28 +165,6 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
this.currentSetting$.set(setting);
|
||||
}
|
||||
|
||||
private getUser$(): Observable<UserQuery> {
|
||||
return this.refreshChanges$.pipe(
|
||||
startWith(true),
|
||||
switchMap(() => this.getMyUser()),
|
||||
pairwiseStartWith(undefined),
|
||||
map(([prev, curr]) => {
|
||||
if (prev?.state === 'success' && curr.state === 'loading') {
|
||||
return { state: 'loading', value: prev.value } as const;
|
||||
}
|
||||
return curr;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private getMyUser(): Observable<UserQuery> {
|
||||
return this.userService.user$.pipe(
|
||||
map((user) => ({ state: 'success' as const, value: user })),
|
||||
catchError((error) => of({ state: 'error', error } as const)),
|
||||
startWith({ state: 'loading' } as const),
|
||||
);
|
||||
}
|
||||
|
||||
getMetadata$(): Observable<MetadataQuery> {
|
||||
return this.refreshMetadata$.pipe(
|
||||
startWith(true),
|
||||
@@ -214,7 +187,14 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
public changeUsername(user: User): void {
|
||||
protected invalidateUser() {
|
||||
this.refreshChanges$.next();
|
||||
return this.queryClient.invalidateQueries({
|
||||
queryKey: this.userService.userQueryOptions().queryKey,
|
||||
});
|
||||
}
|
||||
|
||||
protected changeUsername(user: User): void {
|
||||
const data = {
|
||||
confirmKey: 'ACTIONS.CHANGE' as const,
|
||||
cancelKey: 'ACTIONS.CANCEL' as const,
|
||||
@@ -239,7 +219,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toast.showInfo('USER.TOAST.USERNAMECHANGED', true);
|
||||
this.refreshChanges$.emit();
|
||||
this.invalidateUser().then();
|
||||
},
|
||||
error: (error) => {
|
||||
this.toast.showError(error);
|
||||
@@ -262,7 +242,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
})
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.SAVED', true);
|
||||
this.refreshChanges$.emit();
|
||||
this.invalidateUser().then();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
@@ -274,7 +254,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
.verifyMyPhone(code)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
|
||||
this.refreshChanges$.emit();
|
||||
this.invalidateUser().then();
|
||||
this.promptSetupforSMSOTP();
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -315,7 +295,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
.resendHumanEmailVerification(user.userId)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
|
||||
this.refreshChanges$.emit();
|
||||
this.invalidateUser().then();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
@@ -327,7 +307,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
.resendHumanPhoneVerification(user.userId)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
|
||||
this.refreshChanges$.emit();
|
||||
this.invalidateUser().then();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
@@ -339,7 +319,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
.removePhone(user.userId)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
|
||||
this.refreshChanges$.emit();
|
||||
this.invalidateUser().then();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
@@ -388,7 +368,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
|
||||
this.refreshChanges$.emit();
|
||||
this.invalidateUser().then();
|
||||
},
|
||||
error: (error) => this.toast.showError(error),
|
||||
});
|
||||
@@ -420,7 +400,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
|
||||
this.refreshChanges$.emit();
|
||||
this.invalidateUser().then();
|
||||
},
|
||||
error: (error) => {
|
||||
this.toast.showError(error);
|
||||
@@ -482,24 +462,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
|
||||
protected readonly query = query;
|
||||
|
||||
protected user(user: UserQuery): User | undefined {
|
||||
if (user.state === 'success' || user.state === 'loading') {
|
||||
return user.value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
public async goToSetting(setting: string) {
|
||||
await this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { id: setting },
|
||||
queryParamsHandling: 'merge',
|
||||
skipLocationChange: true,
|
||||
});
|
||||
}
|
||||
|
||||
public humanUser(userQuery: UserQuery): UserWithHumanType | undefined {
|
||||
const user = this.user(userQuery);
|
||||
public humanUser(user: User | undefined): UserWithHumanType | undefined {
|
||||
if (user?.type.case === 'human') {
|
||||
return { ...user, type: user.type };
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, DestroyRef, OnInit } from '@angular/core';
|
||||
import { Component, computed, DestroyRef, OnInit } from '@angular/core';
|
||||
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import {
|
||||
@@ -17,7 +17,7 @@ import { passwordConfirmValidator, requiredValidator } from 'src/app/modules/for
|
||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { catchError, filter } from 'rxjs/operators';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
|
||||
import { UserService } from 'src/app/services/user.service';
|
||||
import { User } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { NewAuthService } from 'src/app/services/new-auth.service';
|
||||
@@ -62,12 +62,16 @@ export class PasswordComponent implements OnInit {
|
||||
}
|
||||
|
||||
private getUser() {
|
||||
return this.userService.user$.pipe(
|
||||
catchError((err) => {
|
||||
this.toast.showError(err);
|
||||
return EMPTY;
|
||||
}),
|
||||
);
|
||||
const userQuery = this.userService.userQuery();
|
||||
const userSignal = computed(() => {
|
||||
if (userQuery.isError()) {
|
||||
this.toast.showError(userQuery.error());
|
||||
}
|
||||
|
||||
return userQuery.data();
|
||||
});
|
||||
|
||||
return toObservable(userSignal).pipe(filter(Boolean));
|
||||
}
|
||||
|
||||
private getUsername(user$: Observable<User>) {
|
||||
|
@@ -1,176 +1,177 @@
|
||||
<cnsl-refresh-table
|
||||
*ngIf="type$ | async as type"
|
||||
[loading]="loading()"
|
||||
(refreshed)="this.refresh$.next(true)"
|
||||
[hideRefresh]="true"
|
||||
[timestamp]="(users$ | async)?.details?.timestamp"
|
||||
[selection]="selection"
|
||||
[showBorder]="true"
|
||||
>
|
||||
<div leftActions class="user-toggle-group">
|
||||
<cnsl-nav-toggle
|
||||
label="{{ 'DESCRIPTIONS.USERS.HUMANS.TITLE' | translate }}"
|
||||
(clicked)="setType(Type.HUMAN)"
|
||||
[active]="type === Type.HUMAN"
|
||||
data-e2e="list-humans"
|
||||
></cnsl-nav-toggle>
|
||||
<cnsl-nav-toggle
|
||||
label="{{ 'DESCRIPTIONS.USERS.MACHINES.TITLE' | translate }}"
|
||||
(clicked)="setType(Type.MACHINE)"
|
||||
[active]="type === Type.MACHINE"
|
||||
data-e2e="list-machines"
|
||||
></cnsl-nav-toggle>
|
||||
</div>
|
||||
<p class="user-sub cnsl-secondary-text">
|
||||
{{
|
||||
(type === Type.HUMAN ? 'DESCRIPTIONS.USERS.HUMANS.DESCRIPTION' : 'DESCRIPTIONS.USERS.MACHINES.DESCRIPTION') | translate
|
||||
}}
|
||||
</p>
|
||||
<ng-template cnslHasRole [hasRole]="['user.write']" actions>
|
||||
<button
|
||||
(click)="deactivateSelectedUsers()"
|
||||
class="bg-state inactive"
|
||||
mat-raised-button
|
||||
*ngIf="selection.hasValue() && multipleDeactivatePossible"
|
||||
[disabled]="(canWrite$ | async) === false"
|
||||
color="primary"
|
||||
>
|
||||
<div class="cnsl-action-button">
|
||||
<span class="">{{ 'USER.TABLE.DEACTIVATE' | translate }}</span>
|
||||
<cnsl-action-keys (actionTriggered)="deactivateSelectedUsers()" [type]="ActionKeysType.DEACTIVATE">
|
||||
</cnsl-action-keys>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
(click)="reactivateSelectedUsers()"
|
||||
class="bg-state active margin-left"
|
||||
mat-raised-button
|
||||
*ngIf="selection.hasValue() && multipleActivatePossible"
|
||||
[disabled]="(canWrite$ | async) === false"
|
||||
color="primary"
|
||||
>
|
||||
<div class="cnsl-action-button">
|
||||
<span class="">{{ 'USER.TABLE.ACTIVATE' | translate }}</span>
|
||||
<cnsl-action-keys (actionTriggered)="reactivateSelectedUsers()" [type]="ActionKeysType.REACTIVATE">
|
||||
</cnsl-action-keys>
|
||||
</div>
|
||||
</button>
|
||||
</ng-template>
|
||||
<cnsl-filter-user
|
||||
actions
|
||||
*ngIf="!selection.hasValue()"
|
||||
(filterChanged)="this.searchQueries$.next($any($event))"
|
||||
(filterOpen)="filterOpen = $event"
|
||||
></cnsl-filter-user>
|
||||
<ng-template cnslHasRole [hasRole]="['user.write']" actions>
|
||||
<button
|
||||
(click)="router.navigate(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
|
||||
color="primary"
|
||||
mat-raised-button
|
||||
[disabled]="(canWrite$ | async) === false"
|
||||
<ng-container *ngIf="userQuery.data() as authUser">
|
||||
<cnsl-refresh-table
|
||||
*ngIf="type$ | async as type"
|
||||
[loading]="loading()"
|
||||
(refreshed)="this.refresh$.next(true)"
|
||||
[hideRefresh]="true"
|
||||
[timestamp]="(users$ | async)?.details?.timestamp"
|
||||
[selection]="selection"
|
||||
[showBorder]="true"
|
||||
>
|
||||
<div leftActions class="user-toggle-group">
|
||||
<cnsl-nav-toggle
|
||||
label="{{ 'DESCRIPTIONS.USERS.HUMANS.TITLE' | translate }}"
|
||||
(clicked)="setType(Type.HUMAN)"
|
||||
[active]="type === Type.HUMAN"
|
||||
data-e2e="list-humans"
|
||||
></cnsl-nav-toggle>
|
||||
<cnsl-nav-toggle
|
||||
label="{{ 'DESCRIPTIONS.USERS.MACHINES.TITLE' | translate }}"
|
||||
(clicked)="setType(Type.MACHINE)"
|
||||
[active]="type === Type.MACHINE"
|
||||
data-e2e="list-machines"
|
||||
></cnsl-nav-toggle>
|
||||
</div>
|
||||
<p class="user-sub cnsl-secondary-text">
|
||||
{{
|
||||
(type === Type.HUMAN ? 'DESCRIPTIONS.USERS.HUMANS.DESCRIPTION' : 'DESCRIPTIONS.USERS.MACHINES.DESCRIPTION') | translate
|
||||
}}
|
||||
</p>
|
||||
<ng-template cnslHasRole [hasRole]="['user.write']" actions>
|
||||
<button
|
||||
(click)="deactivateSelectedUsers()"
|
||||
class="bg-state inactive"
|
||||
mat-raised-button
|
||||
*ngIf="selection.hasValue() && multipleDeactivatePossible"
|
||||
[disabled]="(canWrite$ | async) === false"
|
||||
color="primary"
|
||||
>
|
||||
<div class="cnsl-action-button">
|
||||
<span class="">{{ 'USER.TABLE.DEACTIVATE' | translate }}</span>
|
||||
<cnsl-action-keys (actionTriggered)="deactivateSelectedUsers()" [type]="ActionKeysType.DEACTIVATE">
|
||||
</cnsl-action-keys>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
(click)="reactivateSelectedUsers()"
|
||||
class="bg-state active margin-left"
|
||||
mat-raised-button
|
||||
*ngIf="selection.hasValue() && multipleActivatePossible"
|
||||
[disabled]="(canWrite$ | async) === false"
|
||||
color="primary"
|
||||
>
|
||||
<div class="cnsl-action-button">
|
||||
<span class="">{{ 'USER.TABLE.ACTIVATE' | translate }}</span>
|
||||
<cnsl-action-keys (actionTriggered)="reactivateSelectedUsers()" [type]="ActionKeysType.REACTIVATE">
|
||||
</cnsl-action-keys>
|
||||
</div>
|
||||
</button>
|
||||
</ng-template>
|
||||
<cnsl-filter-user
|
||||
actions
|
||||
*ngIf="!selection.hasValue()"
|
||||
data-e2e="create-user-button"
|
||||
>
|
||||
<div class="cnsl-action-button">
|
||||
<mat-icon class="icon">add</mat-icon>
|
||||
<span>{{ 'ACTIONS.NEW' | translate }}</span>
|
||||
<cnsl-action-keys
|
||||
*ngIf="!filterOpen"
|
||||
(actionTriggered)="router.navigate(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
|
||||
>
|
||||
</cnsl-action-keys>
|
||||
</div>
|
||||
</button>
|
||||
</ng-template>
|
||||
(filterChanged)="this.searchQueries$.next($any($event))"
|
||||
(filterOpen)="filterOpen = $event"
|
||||
></cnsl-filter-user>
|
||||
<ng-template cnslHasRole [hasRole]="['user.write']" actions>
|
||||
<button
|
||||
(click)="router.navigate(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
|
||||
color="primary"
|
||||
mat-raised-button
|
||||
[disabled]="(canWrite$ | async) === false"
|
||||
*ngIf="!selection.hasValue()"
|
||||
data-e2e="create-user-button"
|
||||
>
|
||||
<div class="cnsl-action-button">
|
||||
<mat-icon class="icon">add</mat-icon>
|
||||
<span>{{ 'ACTIONS.NEW' | translate }}</span>
|
||||
<cnsl-action-keys
|
||||
*ngIf="!filterOpen"
|
||||
(actionTriggered)="router.navigate(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
|
||||
>
|
||||
</cnsl-action-keys>
|
||||
</div>
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table class="table" mat-table [dataSource]="dataSource" matSort>
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<div class="selection">
|
||||
<mat-checkbox
|
||||
class="checkbox"
|
||||
[disabled]="(canWrite$ | async) === false"
|
||||
color="primary"
|
||||
(change)="$event ? masterToggle() : null"
|
||||
[checked]="selection.hasValue() && isAllSelected()"
|
||||
[indeterminate]="selection.hasValue() && !isAllSelected()"
|
||||
>
|
||||
<cnsl-avatar class="hidden" [isMachine]="true">
|
||||
<i class="las la-robot"></i>
|
||||
</cnsl-avatar>
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user">
|
||||
<div class="selection">
|
||||
<mat-checkbox
|
||||
class="checkbox"
|
||||
[disabled]="(canWrite$ | async) === false"
|
||||
color="primary"
|
||||
(click)="$event.stopPropagation()"
|
||||
(change)="$event ? selection.toggle(user) : null"
|
||||
[checked]="selection.isSelected(user)"
|
||||
>
|
||||
<cnsl-avatar
|
||||
*ngIf="user.type.case === 'human' && user.type.value.profile; else cog"
|
||||
class="avatar"
|
||||
[name]="user.type.value.profile.displayName"
|
||||
[avatarUrl]="user.type.value.profile.avatarUrl || ''"
|
||||
[forColor]="user.preferredLoginName"
|
||||
[size]="32"
|
||||
<div class="table-wrapper">
|
||||
<table class="table" mat-table [dataSource]="dataSource" matSort>
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<div class="selection">
|
||||
<mat-checkbox
|
||||
class="checkbox"
|
||||
[disabled]="(canWrite$ | async) === false"
|
||||
color="primary"
|
||||
(change)="$event ? masterToggle() : null"
|
||||
[checked]="selection.hasValue() && isAllSelected()"
|
||||
[indeterminate]="selection.hasValue() && !isAllSelected()"
|
||||
>
|
||||
</cnsl-avatar>
|
||||
<ng-template #cog>
|
||||
<cnsl-avatar [forColor]="user?.preferredLoginName" [isMachine]="true">
|
||||
<cnsl-avatar class="hidden" [isMachine]="true">
|
||||
<i class="las la-robot"></i>
|
||||
</cnsl-avatar>
|
||||
</ng-template>
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user">
|
||||
<div class="selection">
|
||||
<mat-checkbox
|
||||
class="checkbox"
|
||||
[disabled]="(canWrite$ | async) === false"
|
||||
color="primary"
|
||||
(click)="$event.stopPropagation()"
|
||||
(change)="$event ? selection.toggle(user) : null"
|
||||
[checked]="selection.isSelected(user)"
|
||||
>
|
||||
<cnsl-avatar
|
||||
*ngIf="user.type.case === 'human' && user.type.value.profile; else cog"
|
||||
class="avatar"
|
||||
[name]="user.type.value.profile.displayName"
|
||||
[avatarUrl]="user.type.value.profile.avatarUrl || ''"
|
||||
[forColor]="user.preferredLoginName"
|
||||
[size]="32"
|
||||
>
|
||||
</cnsl-avatar>
|
||||
<ng-template #cog>
|
||||
<cnsl-avatar [forColor]="user?.preferredLoginName" [isMachine]="true">
|
||||
<i class="las la-robot"></i>
|
||||
</cnsl-avatar>
|
||||
</ng-template>
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="displayName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||
{{ 'USER.PROFILE.DISPLAYNAME' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
<span *ngIf="user.type.case === 'human'">{{ user.type.value?.profile?.displayName }}</span>
|
||||
<span *ngIf="user.type.case === 'machine'">{{ user.type.value.name }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="displayName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||
{{ 'USER.PROFILE.DISPLAYNAME' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
<span *ngIf="user.type.case === 'human'">{{ user.type.value?.profile?.displayName }}</span>
|
||||
<span *ngIf="user.type.case === 'machine'">{{ user.type.value.name }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="preferredLoginName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||
{{ 'USER.PROFILE.PREFERREDLOGINNAME' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
<span *ngIf="user.type.case === 'human'">{{ user.preferredLoginName }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="preferredLoginName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||
{{ 'USER.PROFILE.PREFERREDLOGINNAME' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
<span *ngIf="user.type.case === 'human'">{{ user.preferredLoginName }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="username">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||
{{ 'USER.PROFILE.USERNAME' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
{{ user.username }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="username">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||
{{ 'USER.PROFILE.USERNAME' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
{{ user.username }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="email">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||
{{ 'USER.EMAIL' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
<span *ngIf="user.type?.value?.email?.email">{{ user.type.value.email.email }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="email">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||
{{ 'USER.EMAIL' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
<span *ngIf="user.type?.value?.email?.email">{{ user.type.value.email.email }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="state">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'USER.DATA.STATE' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
<ng-container matColumnDef="state">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'USER.DATA.STATE' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
<span
|
||||
class="state"
|
||||
[ngClass]="{
|
||||
@@ -180,65 +181,66 @@
|
||||
>
|
||||
{{ 'USER.STATEV2.' + user.state | translate }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="creationDate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'USER.TABLE.CREATIONDATE' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
<span class="no-break">{{ user.details.creationDate | timestampToDate | localizedDate: 'regular' }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="creationDate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'USER.TABLE.CREATIONDATE' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
<span class="no-break">{{ user.details.creationDate | timestampToDate | localizedDate: 'regular' }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="changeDate">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'USER.TABLE.CHANGEDATE' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
<span class="no-break">{{ user.details.changeDate | timestampToDate | localizedDate: 'regular' }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="changeDate">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'USER.TABLE.CHANGEDATE' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
<span class="no-break">{{ user.details.changeDate | timestampToDate | localizedDate: 'regular' }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th mat-header-cell *matHeaderCellDef class="user-tr-actions"></th>
|
||||
<td mat-cell *matCellDef="let user" class="user-tr-actions">
|
||||
<cnsl-table-actions>
|
||||
<button
|
||||
actions
|
||||
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
|
||||
color="warn"
|
||||
(click)="deleteUser(user)"
|
||||
[disabled]="(canWrite$ | async) === false || (canDelete$ | async) === false"
|
||||
[attr.data-e2e]="
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th mat-header-cell *matHeaderCellDef class="user-tr-actions"></th>
|
||||
<td mat-cell *matCellDef="let user" class="user-tr-actions">
|
||||
<cnsl-table-actions>
|
||||
<button
|
||||
actions
|
||||
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
|
||||
color="warn"
|
||||
(click)="deleteUser(user, authUser)"
|
||||
[disabled]="(canWrite$ | async) === false || (canDelete$ | async) === false"
|
||||
[attr.data-e2e]="
|
||||
(canWrite$ | async) === false || (canDelete$ | async) === false
|
||||
? 'disabled-delete-button'
|
||||
: 'enabled-delete-button'
|
||||
"
|
||||
mat-icon-button
|
||||
>
|
||||
<i class="las la-trash"></i>
|
||||
</button>
|
||||
</cnsl-table-actions>
|
||||
</td>
|
||||
</ng-container>
|
||||
mat-icon-button
|
||||
>
|
||||
<i class="las la-trash"></i>
|
||||
</button>
|
||||
</cnsl-table-actions>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="type === Type.HUMAN ? displayedColumnsHuman : displayedColumnsMachine"></tr>
|
||||
<tr
|
||||
class="highlight pointer"
|
||||
mat-row
|
||||
*matRowDef="let user; columns: type === Type.HUMAN ? displayedColumnsHuman : displayedColumnsMachine"
|
||||
></tr>
|
||||
</table>
|
||||
</div>
|
||||
<tr mat-header-row *matHeaderRowDef="type === Type.HUMAN ? displayedColumnsHuman : displayedColumnsMachine"></tr>
|
||||
<tr
|
||||
class="highlight pointer"
|
||||
mat-row
|
||||
*matRowDef="let user; columns: type === Type.HUMAN ? displayedColumnsHuman : displayedColumnsMachine"
|
||||
></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!loading() && !dataSource?.data?.length" class="no-content-row">
|
||||
<i class="las la-exclamation"></i>
|
||||
<span>{{ 'USER.TABLE.EMPTY' | translate }}</span>
|
||||
</div>
|
||||
<cnsl-paginator
|
||||
class="paginator"
|
||||
[length]="dataSize()"
|
||||
[pageSize]="INITIAL_PAGE_SIZE"
|
||||
[timestamp]="(users$ | async)?.details?.timestamp"
|
||||
[pageSizeOptions]="[10, 20, 50, 100]"
|
||||
></cnsl-paginator>
|
||||
<!-- (page)="changePage($event)"-->
|
||||
</cnsl-refresh-table>
|
||||
<div *ngIf="!loading() && !dataSource?.data?.length" class="no-content-row">
|
||||
<i class="las la-exclamation"></i>
|
||||
<span>{{ 'USER.TABLE.EMPTY' | translate }}</span>
|
||||
</div>
|
||||
<cnsl-paginator
|
||||
class="paginator"
|
||||
[length]="dataSize()"
|
||||
[pageSize]="INITIAL_PAGE_SIZE"
|
||||
[timestamp]="(users$ | async)?.details?.timestamp"
|
||||
[pageSizeOptions]="[10, 20, 50, 100]"
|
||||
></cnsl-paginator>
|
||||
<!-- (page)="changePage($event)"-->
|
||||
</cnsl-refresh-table>
|
||||
</ng-container>
|
||||
|
@@ -1,5 +1,16 @@
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, signal, Signal, ViewChild } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
DestroyRef,
|
||||
effect,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
signal,
|
||||
Signal,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSort, SortDirection } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
@@ -11,13 +22,11 @@ import {
|
||||
delay,
|
||||
distinctUntilChanged,
|
||||
EMPTY,
|
||||
from,
|
||||
Observable,
|
||||
of,
|
||||
ReplaySubject,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
toArray,
|
||||
} from 'rxjs';
|
||||
import { catchError, filter, finalize, map, startWith, take } from 'rxjs/operators';
|
||||
import { enterAnimations } from 'src/app/animations';
|
||||
@@ -26,7 +35,7 @@ import { PaginatorComponent } from 'src/app/modules/paginator/paginator.componen
|
||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { UserService } from 'src/app/services/user.service';
|
||||
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
||||
import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop';
|
||||
import { SearchQuery as UserSearchQuery } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { Type, UserFieldName } from '@zitadel/proto/zitadel/user/v2/query_pb';
|
||||
import { UserState, User } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
@@ -35,6 +44,7 @@ import { ListUsersRequestSchema, ListUsersResponse } from '@zitadel/proto/zitade
|
||||
import { AuthenticationService } from 'src/app/services/authentication.service';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { UserState as UserStateV1 } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { NewOrganizationService } from '../../../../services/new-organization.service';
|
||||
|
||||
type Query = Exclude<
|
||||
Exclude<MessageInitShape<typeof ListUsersRequestSchema>['queries'], undefined>[number]['query'],
|
||||
@@ -50,20 +60,25 @@ type Query = Exclude<
|
||||
export class UserTableComponent implements OnInit {
|
||||
protected readonly Type = Type;
|
||||
protected readonly refresh$ = new ReplaySubject<true>(1);
|
||||
protected readonly userQuery = this.userService.userQuery();
|
||||
|
||||
@Input() public canWrite$: Observable<boolean> = of(false);
|
||||
@Input() public canDelete$: Observable<boolean> = of(false);
|
||||
|
||||
protected readonly dataSize: Signal<number>;
|
||||
protected readonly loading = signal(false);
|
||||
protected readonly loading = signal(true);
|
||||
|
||||
private readonly paginator$ = new ReplaySubject<PaginatorComponent>(1);
|
||||
@ViewChild(PaginatorComponent) public set paginator(paginator: PaginatorComponent) {
|
||||
this.paginator$.next(paginator);
|
||||
@ViewChild(PaginatorComponent) public set paginator(paginator: PaginatorComponent | null) {
|
||||
if (paginator) {
|
||||
this.paginator$.next(paginator);
|
||||
}
|
||||
}
|
||||
private readonly sort$ = new ReplaySubject<MatSort>(1);
|
||||
@ViewChild(MatSort) public set sort(sort: MatSort) {
|
||||
this.sort$.next(sort);
|
||||
@ViewChild(MatSort) public set sort(sort: MatSort | null) {
|
||||
if (sort) {
|
||||
this.sort$.next(sort);
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly INITIAL_PAGE_SIZE = 20;
|
||||
@@ -73,7 +88,6 @@ export class UserTableComponent implements OnInit {
|
||||
protected readonly users$: Observable<ListUsersResponse>;
|
||||
protected readonly type$: Observable<Type>;
|
||||
protected readonly searchQueries$ = new ReplaySubject<UserSearchQuery[]>(1);
|
||||
protected readonly myUser: Signal<User | undefined>;
|
||||
|
||||
@Input() public displayedColumnsHuman: string[] = [
|
||||
'select',
|
||||
@@ -112,10 +126,10 @@ export class UserTableComponent implements OnInit {
|
||||
private readonly destroyRef: DestroyRef,
|
||||
private readonly authenticationService: AuthenticationService,
|
||||
private readonly authService: GrpcAuthService,
|
||||
private readonly newOrganizationService: NewOrganizationService,
|
||||
) {
|
||||
this.type$ = this.getType$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.users$ = this.getUsers(this.type$).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.myUser = toSignal(this.getMyUser());
|
||||
|
||||
this.dataSize = toSignal(
|
||||
this.users$.pipe(
|
||||
@@ -124,6 +138,12 @@ export class UserTableComponent implements OnInit {
|
||||
),
|
||||
{ initialValue: 0 },
|
||||
);
|
||||
|
||||
effect(() => {
|
||||
if (this.userQuery.isError()) {
|
||||
this.toast.showError(this.userQuery.error());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -158,15 +178,6 @@ export class UserTableComponent implements OnInit {
|
||||
.then();
|
||||
}
|
||||
|
||||
private getMyUser() {
|
||||
return this.userService.user$.pipe(
|
||||
catchError((error) => {
|
||||
this.toast.showError(error);
|
||||
return EMPTY;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private getType$(): Observable<Type> {
|
||||
return this.route.queryParamMap.pipe(
|
||||
map((params) => params.get('type')),
|
||||
@@ -221,20 +232,18 @@ export class UserTableComponent implements OnInit {
|
||||
}
|
||||
|
||||
private getQueries(type$: Observable<Type>): Observable<Query[]> {
|
||||
const activeOrgId$ = this.getActiveOrgId();
|
||||
|
||||
return this.searchQueries$.pipe(
|
||||
startWith([]),
|
||||
combineLatestWith(type$, activeOrgId$),
|
||||
switchMap(([queries, type, organizationId]) =>
|
||||
from(queries).pipe(
|
||||
map((query) => this.searchQueryToV2(query.toObject())),
|
||||
startWith({ case: 'typeQuery' as const, value: { type } }),
|
||||
startWith(organizationId ? { case: 'organizationIdQuery' as const, value: { organizationId } } : undefined),
|
||||
filter(Boolean),
|
||||
toArray(),
|
||||
),
|
||||
),
|
||||
combineLatestWith(type$, toObservable(this.newOrganizationService.getOrgId())),
|
||||
map(([queries, type, organizationId]) => {
|
||||
const mappedQueries = queries.map((q) => this.searchQueryToV2(q.toObject()));
|
||||
|
||||
return [
|
||||
{ case: 'typeQuery' as const, value: { type } },
|
||||
organizationId ? { case: 'organizationIdQuery' as const, value: { organizationId } } : undefined,
|
||||
...mappedQueries,
|
||||
].filter((q): q is NonNullable<typeof q> => !!q);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -399,7 +408,7 @@ export class UserTableComponent implements OnInit {
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
public deleteUser(user: User): void {
|
||||
public deleteUser(user: User, authUser: User): void {
|
||||
const authUserData = {
|
||||
confirmKey: 'ACTIONS.DELETE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
@@ -423,9 +432,7 @@ export class UserTableComponent implements OnInit {
|
||||
};
|
||||
|
||||
if (user?.userId) {
|
||||
const authUser = this.myUser();
|
||||
console.log('my user', authUser);
|
||||
const isMe = authUser?.userId === user.userId;
|
||||
const isMe = authUser.userId === user.userId;
|
||||
|
||||
let dialogRef;
|
||||
|
||||
@@ -469,20 +476,4 @@ export class UserTableComponent implements OnInit {
|
||||
const selected = this.selection.selected;
|
||||
return selected ? selected.findIndex((user) => user.state !== UserState.INACTIVE) > -1 : false;
|
||||
}
|
||||
|
||||
private getActiveOrgId() {
|
||||
return this.authenticationService.authenticationChanged.pipe(
|
||||
startWith(true),
|
||||
filter(Boolean),
|
||||
switchMap(() =>
|
||||
from(this.authService.getActiveOrg()).pipe(
|
||||
catchError((err) => {
|
||||
this.toast.showError(err);
|
||||
return of(undefined);
|
||||
}),
|
||||
),
|
||||
),
|
||||
map((org) => org?.id),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -171,8 +171,6 @@ import {
|
||||
ListLoginPolicySecondFactorsResponse,
|
||||
ListMilestonesRequest,
|
||||
ListMilestonesResponse,
|
||||
ListOrgsRequest,
|
||||
ListOrgsResponse,
|
||||
ListProvidersRequest,
|
||||
ListProvidersResponse,
|
||||
ListSecretGeneratorsRequest,
|
||||
@@ -307,7 +305,6 @@ import {
|
||||
UpdateSMTPConfigRequest,
|
||||
UpdateSMTPConfigResponse,
|
||||
} from '../proto/generated/zitadel/admin_pb';
|
||||
import { Event } from '../proto/generated/zitadel/event_pb';
|
||||
import {
|
||||
ResetCustomDomainClaimedMessageTextToDefaultRequest,
|
||||
ResetCustomDomainClaimedMessageTextToDefaultResponse,
|
||||
@@ -340,9 +337,6 @@ import {
|
||||
MilestoneQuery,
|
||||
MilestoneType,
|
||||
} from '../proto/generated/zitadel/milestone/v1/milestone_pb';
|
||||
import { OrgFieldName, OrgQuery } from '../proto/generated/zitadel/org_pb';
|
||||
import { SortDirection } from '@angular/material/sort';
|
||||
import { SMTPConfig } from '../proto/generated/zitadel/settings_pb';
|
||||
|
||||
export interface OnboardingActions {
|
||||
order: number;
|
||||
@@ -1404,33 +1398,4 @@ export class AdminService {
|
||||
public listMilestones(req: ListMilestonesRequest): Promise<ListMilestonesResponse.AsObject> {
|
||||
return this.grpcService.admin.listMilestones(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public listOrgs(
|
||||
limit: number,
|
||||
offset: number,
|
||||
queriesList?: OrgQuery[],
|
||||
sortingColumn?: OrgFieldName,
|
||||
sortingDirection?: SortDirection,
|
||||
): Promise<ListOrgsResponse.AsObject> {
|
||||
const req = new ListOrgsRequest();
|
||||
const query = new ListQuery();
|
||||
if (limit) {
|
||||
query.setLimit(limit);
|
||||
}
|
||||
if (offset) {
|
||||
query.setOffset(offset);
|
||||
}
|
||||
if (sortingDirection) {
|
||||
query.setAsc(sortingDirection === 'asc');
|
||||
}
|
||||
req.setQuery(query);
|
||||
if (sortingColumn) {
|
||||
req.setSortingColumn(sortingColumn);
|
||||
}
|
||||
|
||||
if (queriesList) {
|
||||
req.setQueriesList(queriesList);
|
||||
}
|
||||
return this.grpcService.admin.listOrgs(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
}
|
||||
|
@@ -96,11 +96,12 @@ import {
|
||||
import { ChangeQuery } from '../proto/generated/zitadel/change_pb';
|
||||
import { MetadataQuery } from '../proto/generated/zitadel/metadata_pb';
|
||||
import { ListQuery } from '../proto/generated/zitadel/object_pb';
|
||||
import { Org, OrgFieldName, OrgIDQuery, OrgQuery } from '../proto/generated/zitadel/org_pb';
|
||||
import { Org, OrgFieldName, OrgQuery } from '../proto/generated/zitadel/org_pb';
|
||||
import { LabelPolicy, PrivacyPolicy } from '../proto/generated/zitadel/policy_pb';
|
||||
import { Gender, MembershipQuery, User, WebAuthNVerification } from '../proto/generated/zitadel/user_pb';
|
||||
import { GrpcService } from './grpc.service';
|
||||
import { StorageKey, StorageLocation, StorageService } from './storage.service';
|
||||
import { NewOrganizationService } from './new-organization.service';
|
||||
import { toObservable } from '@angular/core/rxjs-interop';
|
||||
|
||||
const ORG_LIMIT = 10;
|
||||
|
||||
@@ -108,7 +109,6 @@ const ORG_LIMIT = 10;
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class GrpcAuthService {
|
||||
private _activeOrgChanged: Subject<Org.AsObject | undefined> = new Subject();
|
||||
public user: Observable<User.AsObject | undefined>;
|
||||
private triggerPermissionsRefresh: Subject<void> = new Subject();
|
||||
public zitadelPermissions: Observable<string[]>;
|
||||
@@ -128,19 +128,21 @@ export class GrpcAuthService {
|
||||
constructor(
|
||||
private readonly grpcService: GrpcService,
|
||||
private oauthService: OAuthService,
|
||||
private storage: StorageService,
|
||||
newOrganizationService: NewOrganizationService,
|
||||
) {
|
||||
this.labelpolicy$ = this.activeOrgChanged.pipe(
|
||||
const activeOrg = toObservable(newOrganizationService.orgId);
|
||||
|
||||
this.labelpolicy$ = activeOrg.pipe(
|
||||
tap(() => this.labelPolicyLoading$.next(true)),
|
||||
switchMap((org) => this.getMyLabelPolicy(org ? org.id : '')),
|
||||
switchMap((org) => this.getMyLabelPolicy(org ?? '')),
|
||||
tap(() => this.labelPolicyLoading$.next(false)),
|
||||
finalize(() => this.labelPolicyLoading$.next(false)),
|
||||
filter((policy) => !!policy),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
this.privacypolicy$ = this.activeOrgChanged.pipe(
|
||||
switchMap((org) => this.getMyPrivacyPolicy(org ? org.id : '')),
|
||||
this.privacypolicy$ = activeOrg.pipe(
|
||||
switchMap((org) => this.getMyPrivacyPolicy(org ?? '')),
|
||||
filter((policy) => !!policy),
|
||||
catchError((err) => {
|
||||
console.error(err);
|
||||
@@ -161,7 +163,7 @@ export class GrpcAuthService {
|
||||
);
|
||||
|
||||
this.zitadelPermissions = this.user.pipe(
|
||||
combineLatestWith(this.activeOrgChanged),
|
||||
combineLatestWith(activeOrg),
|
||||
// ignore errors from observables
|
||||
catchError(() => of(true)),
|
||||
// make sure observable never completes
|
||||
@@ -197,81 +199,6 @@ export class GrpcAuthService {
|
||||
return this.grpcService.auth.listMyMetadata(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public async getActiveOrg(id?: string): Promise<Org.AsObject> {
|
||||
if (id) {
|
||||
const find = this.cachedOrgs.getValue().find((tmp) => tmp.id === id);
|
||||
if (find) {
|
||||
this.setActiveOrg(find);
|
||||
return Promise.resolve(find);
|
||||
} else {
|
||||
const orgQuery = new OrgQuery();
|
||||
const orgIdQuery = new OrgIDQuery();
|
||||
orgIdQuery.setId(id);
|
||||
orgQuery.setIdQuery(orgIdQuery);
|
||||
|
||||
const orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0, [orgQuery])).resultList;
|
||||
if (orgs.length === 1) {
|
||||
this.setActiveOrg(orgs[0]);
|
||||
return Promise.resolve(orgs[0]);
|
||||
} else {
|
||||
// throw error if the org was specifically requested but not found
|
||||
return Promise.reject(new Error('requested organization not found'));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let orgs = this.cachedOrgs.getValue();
|
||||
const org = this.storage.getItem<Org.AsObject>(StorageKey.organization, StorageLocation.local);
|
||||
|
||||
if (org) {
|
||||
orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0)).resultList;
|
||||
this.cachedOrgs.next(orgs);
|
||||
|
||||
const find = this.cachedOrgs.getValue().find((tmp) => tmp.id === id);
|
||||
if (find) {
|
||||
this.setActiveOrg(find);
|
||||
return Promise.resolve(find);
|
||||
} else {
|
||||
const orgQuery = new OrgQuery();
|
||||
const orgIdQuery = new OrgIDQuery();
|
||||
orgIdQuery.setId(org.id);
|
||||
orgQuery.setIdQuery(orgIdQuery);
|
||||
|
||||
const specificOrg = (await this.listMyProjectOrgs(ORG_LIMIT, 0, [orgQuery])).resultList;
|
||||
if (specificOrg.length === 1) {
|
||||
this.setActiveOrg(specificOrg[0]);
|
||||
return Promise.resolve(specificOrg[0]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0)).resultList;
|
||||
this.cachedOrgs.next(orgs);
|
||||
}
|
||||
|
||||
if (orgs.length === 0) {
|
||||
this._activeOrgChanged.next(undefined);
|
||||
return Promise.reject(new Error('No organizations found!'));
|
||||
}
|
||||
|
||||
const orgToSet = orgs.find((element) => element.id !== '0' && element.name !== '');
|
||||
if (orgToSet) {
|
||||
this.setActiveOrg(orgToSet);
|
||||
return Promise.resolve(orgToSet);
|
||||
}
|
||||
return Promise.resolve(orgs[0]);
|
||||
}
|
||||
}
|
||||
|
||||
public get activeOrgChanged(): Observable<Org.AsObject | undefined> {
|
||||
return this._activeOrgChanged.asObservable();
|
||||
}
|
||||
|
||||
public setActiveOrg(org: Org.AsObject): void {
|
||||
// Set organization in localstorage to get the last used organization in a new tab
|
||||
this.storage.setItem(StorageKey.organization, org, StorageLocation.local);
|
||||
this.storage.setItem(StorageKey.organization, org, StorageLocation.session);
|
||||
this._activeOrgChanged.next(org);
|
||||
}
|
||||
|
||||
private loadPermissions(): void {
|
||||
this.triggerPermissionsRefresh.next();
|
||||
}
|
||||
|
@@ -16,10 +16,15 @@ import { ExhaustedGrpcInterceptor } from './interceptors/exhausted.grpc.intercep
|
||||
import { I18nInterceptor } from './interceptors/i18n.interceptor';
|
||||
import { NewConnectWebOrgInterceptor, OrgInterceptor, OrgInterceptorProvider } from './interceptors/org.interceptor';
|
||||
import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb';
|
||||
import {
|
||||
createFeatureServiceClient,
|
||||
createUserServiceClient,
|
||||
createSessionServiceClient,
|
||||
createOrganizationServiceClient,
|
||||
// @ts-ignore
|
||||
} from '@zitadel/client/v2';
|
||||
//@ts-ignore
|
||||
import { createFeatureServiceClient, createUserServiceClient, createSessionServiceClient } from '@zitadel/client/v2';
|
||||
//@ts-ignore
|
||||
import { createAuthServiceClient, createManagementServiceClient } from '@zitadel/client/v1';
|
||||
import { createAdminServiceClient, createAuthServiceClient, createManagementServiceClient } from '@zitadel/client/v1';
|
||||
import { createGrpcWebTransport } from '@connectrpc/connect-web';
|
||||
// @ts-ignore
|
||||
import { createClientFor } from '@zitadel/client';
|
||||
@@ -45,6 +50,10 @@ export class GrpcService {
|
||||
public featureNew!: ReturnType<typeof createFeatureServiceClient>;
|
||||
public actionNew!: ReturnType<typeof createActionServiceClient>;
|
||||
public webKey!: ReturnType<typeof createWebKeyServiceClient>;
|
||||
public organizationNew!: ReturnType<typeof createOrganizationServiceClient>;
|
||||
public adminNew!: ReturnType<typeof createAdminServiceClient>;
|
||||
|
||||
public assets!: void;
|
||||
|
||||
constructor(
|
||||
private readonly envService: EnvironmentService,
|
||||
@@ -120,6 +129,8 @@ export class GrpcService {
|
||||
this.featureNew = createFeatureServiceClient(transport);
|
||||
this.actionNew = createActionServiceClient(transport);
|
||||
this.webKey = createWebKeyServiceClient(transport);
|
||||
this.organizationNew = createOrganizationServiceClient(transport);
|
||||
this.adminNew = createAdminServiceClient(transportOldAPIs);
|
||||
|
||||
const authConfig: AuthConfig = {
|
||||
scope: 'openid profile email',
|
||||
|
@@ -4,9 +4,6 @@ import { Org } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
|
||||
import { StorageKey, StorageLocation, StorageService } from '../storage.service';
|
||||
import { ConnectError, Interceptor } from '@connectrpc/connect';
|
||||
import { firstValueFrom, identity, Observable, Subject } from 'rxjs';
|
||||
import { debounceTime, filter, map } from 'rxjs/operators';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
const ORG_HEADER_KEY = 'x-zitadel-orgid';
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -43,8 +40,7 @@ export class OrgInterceptorProvider {
|
||||
constructor(private storageService: StorageService) {}
|
||||
|
||||
getOrgId() {
|
||||
const org: Org.AsObject | null = this.storageService.getItem(StorageKey.organization, StorageLocation.session);
|
||||
return org?.id;
|
||||
return this.storageService.getItem(StorageKey.organizationId, StorageLocation.session);
|
||||
}
|
||||
|
||||
handleError = (error: any): never => {
|
||||
@@ -57,7 +53,7 @@ export class OrgInterceptorProvider {
|
||||
error.code === StatusCode.PERMISSION_DENIED &&
|
||||
error.message.startsWith("Organisation doesn't exist")
|
||||
) {
|
||||
this.storageService.removeItem(StorageKey.organization, StorageLocation.session);
|
||||
this.storageService.removeItem(StorageKey.organizationId, StorageLocation.session);
|
||||
}
|
||||
|
||||
throw error;
|
||||
|
36
console/src/app/services/new-admin.service.ts
Normal file
36
console/src/app/services/new-admin.service.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { GrpcService } from './grpc.service';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import {
|
||||
GetDefaultOrgResponse,
|
||||
GetMyInstanceResponse,
|
||||
SetUpOrgRequestSchema,
|
||||
SetUpOrgResponse,
|
||||
} from '@zitadel/proto/zitadel/admin_pb';
|
||||
import { injectQuery } from '@tanstack/angular-query-experimental';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class NewAdminService {
|
||||
constructor(private readonly grpcService: GrpcService) {}
|
||||
|
||||
public setupOrg(req: MessageInitShape<typeof SetUpOrgRequestSchema>): Promise<SetUpOrgResponse> {
|
||||
return this.grpcService.adminNew.setupOrg(req);
|
||||
}
|
||||
|
||||
public getDefaultOrg(): Promise<GetDefaultOrgResponse> {
|
||||
return this.grpcService.adminNew.getDefaultOrg({});
|
||||
}
|
||||
|
||||
private getMyInstance(signal?: AbortSignal): Promise<GetMyInstanceResponse> {
|
||||
return this.grpcService.adminNew.getMyInstance({}, { signal });
|
||||
}
|
||||
|
||||
public getMyInstanceQuery() {
|
||||
return injectQuery(() => ({
|
||||
queryKey: ['admin', 'getMyInstance'],
|
||||
queryFn: ({ signal }) => this.getMyInstance(signal),
|
||||
}));
|
||||
}
|
||||
}
|
@@ -6,7 +6,6 @@ import {
|
||||
GetMyLoginPolicyResponse,
|
||||
GetMyLoginPolicyRequestSchema,
|
||||
GetMyPasswordComplexityPolicyResponse,
|
||||
GetMyUserResponse,
|
||||
ListMyAuthFactorsRequestSchema,
|
||||
ListMyAuthFactorsResponse,
|
||||
RemoveMyAuthFactorOTPEmailRequestSchema,
|
||||
@@ -27,10 +26,6 @@ import {
|
||||
export class NewAuthService {
|
||||
constructor(private readonly grpcService: GrpcService) {}
|
||||
|
||||
public getMyUser(): Promise<GetMyUserResponse> {
|
||||
return this.grpcService.authNew.getMyUser({});
|
||||
}
|
||||
|
||||
public verifyMyPhone(code: string): Promise<VerifyMyPhoneResponse> {
|
||||
return this.grpcService.authNew.verifyMyPhone({ code });
|
||||
}
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { GrpcService } from './grpc.service';
|
||||
import {
|
||||
AddOrgResponse,
|
||||
DeactivateOrgResponse,
|
||||
GenerateMachineSecretRequestSchema,
|
||||
GenerateMachineSecretResponse,
|
||||
GetDefaultPasswordComplexityPolicyResponse,
|
||||
@@ -9,8 +11,10 @@ import {
|
||||
GetPasswordComplexityPolicyResponse,
|
||||
ListUserMetadataRequestSchema,
|
||||
ListUserMetadataResponse,
|
||||
ReactivateOrgResponse,
|
||||
RemoveMachineSecretRequestSchema,
|
||||
RemoveMachineSecretResponse,
|
||||
RemoveOrgResponse,
|
||||
RemoveUserMetadataRequestSchema,
|
||||
RemoveUserMetadataResponse,
|
||||
ResendHumanEmailVerificationRequestSchema,
|
||||
@@ -26,6 +30,8 @@ import {
|
||||
SetUserMetadataResponse,
|
||||
UpdateMachineRequestSchema,
|
||||
UpdateMachineResponse,
|
||||
UpdateOrgRequestSchema,
|
||||
UpdateOrgResponse,
|
||||
} from '@zitadel/proto/zitadel/management_pb';
|
||||
import { MessageInitShape, create } from '@bufbuild/protobuf';
|
||||
|
||||
@@ -99,4 +105,24 @@ export class NewMgmtService {
|
||||
public getDefaultPasswordComplexityPolicy(): Promise<GetDefaultPasswordComplexityPolicyResponse> {
|
||||
return this.grpcService.mgmtNew.getDefaultPasswordComplexityPolicy({});
|
||||
}
|
||||
|
||||
public updateOrg(req: MessageInitShape<typeof UpdateOrgRequestSchema>): Promise<UpdateOrgResponse> {
|
||||
return this.grpcService.mgmtNew.updateOrg(req);
|
||||
}
|
||||
|
||||
public removeOrg(): Promise<RemoveOrgResponse> {
|
||||
return this.grpcService.mgmtNew.removeOrg({});
|
||||
}
|
||||
|
||||
public reactivateOrg(): Promise<ReactivateOrgResponse> {
|
||||
return this.grpcService.mgmtNew.reactivateOrg({});
|
||||
}
|
||||
|
||||
public deactivateOrg(): Promise<DeactivateOrgResponse> {
|
||||
return this.grpcService.mgmtNew.deactivateOrg({});
|
||||
}
|
||||
|
||||
public addOrg(name: string): Promise<AddOrgResponse> {
|
||||
return this.grpcService.mgmtNew.addOrg({ name });
|
||||
}
|
||||
}
|
||||
|
210
console/src/app/services/new-organization.service.ts
Normal file
210
console/src/app/services/new-organization.service.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { computed, Injectable, signal } from '@angular/core';
|
||||
import { GrpcService } from './grpc.service';
|
||||
import { injectQuery, mutationOptions, QueryClient, queryOptions, skipToken } from '@tanstack/angular-query-experimental';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { ListOrganizationsRequestSchema, ListOrganizationsResponse } from '@zitadel/proto/zitadel/org/v2/org_service_pb';
|
||||
import { NewMgmtService } from './new-mgmt.service';
|
||||
import { OrgInterceptorProvider } from './interceptors/org.interceptor';
|
||||
import { NewAdminService } from './new-admin.service';
|
||||
import { SetUpOrgRequestSchema } from '@zitadel/proto/zitadel/admin_pb';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { StorageKey, StorageLocation, StorageService } from './storage.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class NewOrganizationService {
|
||||
constructor(
|
||||
private readonly grpcService: GrpcService,
|
||||
private readonly newMgtmService: NewMgmtService,
|
||||
private readonly newAdminService: NewAdminService,
|
||||
private readonly orgInterceptorProvider: OrgInterceptorProvider,
|
||||
private readonly queryClient: QueryClient,
|
||||
private readonly translate: TranslateService,
|
||||
private readonly storage: StorageService,
|
||||
) {}
|
||||
|
||||
private readonly orgIdSignal = signal<string | undefined>(
|
||||
this.storage.getItem(StorageKey.organizationId, StorageLocation.session) ??
|
||||
this.storage.getItem(StorageKey.organizationId, StorageLocation.local) ??
|
||||
undefined,
|
||||
);
|
||||
public readonly orgId = this.orgIdSignal.asReadonly();
|
||||
|
||||
public getOrgId() {
|
||||
return computed(() => {
|
||||
const orgId = this.orgIdSignal();
|
||||
if (orgId === undefined) {
|
||||
throw new Error('No organization ID set');
|
||||
}
|
||||
return orgId;
|
||||
});
|
||||
}
|
||||
|
||||
public async setOrgId(orgId?: string) {
|
||||
const organization = await this.queryClient.fetchQuery(this.organizationByIdQueryOptions(orgId ?? this.getOrgId()()));
|
||||
if (organization) {
|
||||
this.storage.setItem(StorageKey.organizationId, orgId, StorageLocation.session);
|
||||
this.storage.setItem(StorageKey.organizationId, orgId, StorageLocation.local);
|
||||
this.orgIdSignal.set(orgId);
|
||||
} else {
|
||||
throw new Error('request organization not found');
|
||||
}
|
||||
return organization;
|
||||
}
|
||||
|
||||
public organizationByIdQueryOptions(organizationId?: string) {
|
||||
const req = {
|
||||
query: {
|
||||
limit: 1,
|
||||
},
|
||||
queries: [
|
||||
{
|
||||
query: {
|
||||
case: 'idQuery' as const,
|
||||
value: {
|
||||
id: organizationId?.toString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return queryOptions({
|
||||
queryKey: ['listOrganizations', req],
|
||||
queryFn: organizationId
|
||||
? () => this.listOrganizations(req).then((resp) => resp.result.find(Boolean) ?? null)
|
||||
: skipToken,
|
||||
});
|
||||
}
|
||||
|
||||
public activeOrganizationQuery() {
|
||||
return injectQuery(() => this.organizationByIdQueryOptions(this.orgId()));
|
||||
}
|
||||
|
||||
public listOrganizationsQueryOptions(req?: MessageInitShape<typeof ListOrganizationsRequestSchema>) {
|
||||
return queryOptions({
|
||||
queryKey: this.listOrganizationsQueryKey(req),
|
||||
queryFn: () => this.listOrganizations(req ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
public listOrganizationsQueryKey(req?: MessageInitShape<typeof ListOrganizationsRequestSchema>) {
|
||||
if (!req) {
|
||||
return ['listOrganizations'];
|
||||
}
|
||||
|
||||
// needed because angular query isn't able to serialize a bigint key
|
||||
const query = req.query ? { ...req.query, offset: req.query.offset ? Number(req.query.offset) : undefined } : undefined;
|
||||
const queryKey = {
|
||||
...req,
|
||||
...(query ? { query } : {}),
|
||||
};
|
||||
|
||||
return ['listOrganizations', queryKey];
|
||||
}
|
||||
|
||||
public listOrganizations(
|
||||
req: MessageInitShape<typeof ListOrganizationsRequestSchema>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ListOrganizationsResponse> {
|
||||
return this.grpcService.organizationNew.listOrganizations(req, { signal });
|
||||
}
|
||||
|
||||
private async getDefaultOrganization() {
|
||||
let resp = await this.listOrganizations({
|
||||
query: {
|
||||
limit: 1,
|
||||
},
|
||||
queries: [
|
||||
{
|
||||
query: {
|
||||
case: 'defaultQuery',
|
||||
value: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
return resp.result.find(Boolean) ?? null;
|
||||
}
|
||||
|
||||
private invalidateAllOrganizationQueries() {
|
||||
return this.queryClient.invalidateQueries({
|
||||
queryKey: this.listOrganizationsQueryOptions().queryKey,
|
||||
});
|
||||
}
|
||||
|
||||
public renameOrgMutationOptions = () =>
|
||||
mutationOptions({
|
||||
mutationKey: ['renameOrg'],
|
||||
mutationFn: (name: string) => this.newMgtmService.updateOrg({ name }),
|
||||
onSettled: () => this.invalidateAllOrganizationQueries(),
|
||||
});
|
||||
|
||||
public deleteOrgMutationOptions = () =>
|
||||
mutationOptions({
|
||||
mutationKey: ['deleteOrg'],
|
||||
mutationFn: async () => {
|
||||
// Before we remove the org we get the current default org
|
||||
// we have to query before the current org is removed
|
||||
const defaultOrg = await this.getDefaultOrganization();
|
||||
if (!defaultOrg) {
|
||||
const error$ = this.translate.get('ORG.TOAST.DEFAULTORGOTFOUND').pipe(first());
|
||||
throw { message: await lastValueFrom(error$) };
|
||||
}
|
||||
|
||||
const resp = await this.newMgtmService.removeOrg();
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
// We change active org to default org as
|
||||
// current org was deleted to avoid Organization doesn't exist
|
||||
await this.setOrgId(defaultOrg.id);
|
||||
|
||||
return resp;
|
||||
},
|
||||
onSettled: async () => {
|
||||
const orgId = this.orgInterceptorProvider.getOrgId();
|
||||
if (orgId) {
|
||||
this.queryClient.removeQueries({
|
||||
queryKey: this.organizationByIdQueryOptions(orgId).queryKey,
|
||||
});
|
||||
}
|
||||
|
||||
await this.invalidateAllOrganizationQueries();
|
||||
},
|
||||
});
|
||||
|
||||
public reactivateOrgMutationOptions = () =>
|
||||
mutationOptions({
|
||||
mutationKey: ['reactivateOrg'],
|
||||
mutationFn: () => this.newMgtmService.reactivateOrg(),
|
||||
onSettled: () => this.invalidateAllOrganizationQueries(),
|
||||
});
|
||||
|
||||
public deactivateOrgMutationOptions = () =>
|
||||
mutationOptions({
|
||||
mutationKey: ['deactivateOrg'],
|
||||
mutationFn: () => this.newMgtmService.deactivateOrg(),
|
||||
onSettled: () => this.invalidateAllOrganizationQueries(),
|
||||
});
|
||||
|
||||
public setupOrgMutationOptions = () =>
|
||||
mutationOptions({
|
||||
mutationKey: ['setupOrg'],
|
||||
mutationFn: (req: MessageInitShape<typeof SetUpOrgRequestSchema>) => this.newAdminService.setupOrg(req),
|
||||
onSettled: async () => {
|
||||
await this.invalidateAllOrganizationQueries();
|
||||
},
|
||||
});
|
||||
|
||||
public addOrgMutationOptions = () =>
|
||||
mutationOptions({
|
||||
mutationKey: ['addOrg'],
|
||||
mutationFn: (name: string) => this.newMgtmService.addOrg(name),
|
||||
onSettled: async () => {
|
||||
await this.invalidateAllOrganizationQueries();
|
||||
},
|
||||
});
|
||||
}
|
@@ -43,7 +43,7 @@ export class StorageConfig {
|
||||
}
|
||||
|
||||
export enum StorageKey {
|
||||
organization = 'organization',
|
||||
organizationId = 'organizationId',
|
||||
}
|
||||
|
||||
export enum StorageLocation {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { GrpcService } from './grpc.service';
|
||||
import {
|
||||
AddHumanUserRequestSchema,
|
||||
@@ -18,9 +18,6 @@ import {
|
||||
ListPasskeysResponse,
|
||||
ListUsersRequestSchema,
|
||||
ListUsersResponse,
|
||||
LockUserRequestSchema,
|
||||
LockUserResponse,
|
||||
PasswordResetRequestSchema,
|
||||
ReactivateUserRequestSchema,
|
||||
ReactivateUserResponse,
|
||||
RemoveOTPEmailRequestSchema,
|
||||
@@ -49,46 +46,30 @@ import {
|
||||
UpdateHumanUserResponse,
|
||||
} from '@zitadel/proto/zitadel/user/v2/user_service_pb';
|
||||
import type { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import {
|
||||
AccessTokenType,
|
||||
Gender,
|
||||
HumanProfile,
|
||||
HumanProfileSchema,
|
||||
HumanUser,
|
||||
HumanUserSchema,
|
||||
MachineUser,
|
||||
MachineUserSchema,
|
||||
User as UserV2,
|
||||
UserSchema,
|
||||
UserState,
|
||||
} from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { create } from '@bufbuild/protobuf';
|
||||
import { Timestamp as TimestampV2, TimestampSchema } from '@bufbuild/protobuf/wkt';
|
||||
import { Details, DetailsSchema } from '@zitadel/proto/zitadel/object/v2/object_pb';
|
||||
import { Human, Machine, Phone, Profile, User } from '../proto/generated/zitadel/user_pb';
|
||||
import { ObjectDetails } from '../proto/generated/zitadel/object_pb';
|
||||
import { Timestamp } from '../proto/generated/google/protobuf/timestamp_pb';
|
||||
import { HumanPhone, HumanPhoneSchema } from '@zitadel/proto/zitadel/user/v2/phone_pb';
|
||||
import { OAuthService } from 'angular-oauth2-oidc';
|
||||
import { debounceTime, EMPTY, Observable, of, ReplaySubject, shareReplay, switchAll, switchMap } from 'rxjs';
|
||||
import { catchError, filter, map, startWith } from 'rxjs/operators';
|
||||
import { EMPTY, of, switchMap } from 'rxjs';
|
||||
import { filter, map, startWith } from 'rxjs/operators';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { injectQuery, queryOptions, skipToken } from '@tanstack/angular-query-experimental';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UserService {
|
||||
private user$$ = new ReplaySubject<Observable<UserV2>>(1);
|
||||
public user$ = this.user$$.pipe(
|
||||
startWith(this.getUser()),
|
||||
// makes sure if many subscribers reset the observable only one wins
|
||||
debounceTime(10),
|
||||
switchAll(),
|
||||
catchError((err) => {
|
||||
// reset user observable on error
|
||||
this.user$$.next(this.getUser());
|
||||
throw err;
|
||||
}),
|
||||
);
|
||||
private userId = this.getUserId();
|
||||
|
||||
public userQuery() {
|
||||
return injectQuery(() => this.userQueryOptions());
|
||||
}
|
||||
|
||||
public userQueryOptions() {
|
||||
const userId = this.userId();
|
||||
return queryOptions({
|
||||
queryKey: ['user', userId],
|
||||
queryFn: userId ? () => this.getUserById(userId).then((resp) => resp.user) : skipToken,
|
||||
});
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly grpcService: GrpcService,
|
||||
@@ -96,10 +77,12 @@ export class UserService {
|
||||
) {}
|
||||
|
||||
private getUserId() {
|
||||
return this.oauthService.events.pipe(
|
||||
const userId$ = this.oauthService.events.pipe(
|
||||
filter((event) => event.type === 'token_received'),
|
||||
map(() => this.oauthService.getIdToken()),
|
||||
startWith(this.oauthService.getIdToken()),
|
||||
// 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
|
||||
@@ -118,15 +101,8 @@ export class UserService {
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private getUser() {
|
||||
return this.getUserId().pipe(
|
||||
switchMap((id) => this.getUserById(id)),
|
||||
map((resp) => resp.user),
|
||||
filter(Boolean),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
return toSignal(userId$, { initialValue: undefined });
|
||||
}
|
||||
|
||||
public addHumanUser(req: MessageInitShape<typeof AddHumanUserRequestSchema>): Promise<AddHumanUserResponse> {
|
||||
@@ -157,10 +133,6 @@ export class UserService {
|
||||
return this.grpcService.userNew.updateHumanUser(create(UpdateHumanUserRequestSchema, req));
|
||||
}
|
||||
|
||||
public lockUser(userId: string): Promise<LockUserResponse> {
|
||||
return this.grpcService.userNew.lockUser(create(LockUserRequestSchema, { userId }));
|
||||
}
|
||||
|
||||
public unlockUser(userId: string): Promise<UnlockUserResponse> {
|
||||
return this.grpcService.userNew.unlockUser(create(UnlockUserRequestSchema, { userId }));
|
||||
}
|
||||
@@ -221,102 +193,7 @@ export class UserService {
|
||||
return this.grpcService.userNew.createInviteCode(create(CreateInviteCodeRequestSchema, req));
|
||||
}
|
||||
|
||||
public passwordReset(req: MessageInitShape<typeof PasswordResetRequestSchema>) {
|
||||
return this.grpcService.userNew.passwordReset(create(PasswordResetRequestSchema, req));
|
||||
}
|
||||
|
||||
public setPassword(req: MessageInitShape<typeof SetPasswordRequestSchema>): Promise<SetPasswordResponse> {
|
||||
return this.grpcService.userNew.setPassword(create(SetPasswordRequestSchema, req));
|
||||
}
|
||||
}
|
||||
|
||||
function userToV2(user: User): UserV2 {
|
||||
const details = user.getDetails();
|
||||
return create(UserSchema, {
|
||||
userId: user.getId(),
|
||||
details: details && detailsToV2(details),
|
||||
state: user.getState() as number as UserState,
|
||||
username: user.getUserName(),
|
||||
loginNames: user.getLoginNamesList(),
|
||||
preferredLoginName: user.getPreferredLoginName(),
|
||||
type: typeToV2(user),
|
||||
});
|
||||
}
|
||||
|
||||
function detailsToV2(details: ObjectDetails): Details {
|
||||
const changeDate = details.getChangeDate();
|
||||
return create(DetailsSchema, {
|
||||
sequence: BigInt(details.getSequence()),
|
||||
changeDate: changeDate && timestampToV2(changeDate),
|
||||
resourceOwner: details.getResourceOwner(),
|
||||
});
|
||||
}
|
||||
|
||||
function timestampToV2(timestamp: Timestamp): TimestampV2 {
|
||||
return create(TimestampSchema, {
|
||||
seconds: BigInt(timestamp.getSeconds()),
|
||||
nanos: timestamp.getNanos(),
|
||||
});
|
||||
}
|
||||
|
||||
function typeToV2(user: User): UserV2['type'] {
|
||||
const human = user.getHuman();
|
||||
if (human) {
|
||||
return { case: 'human', value: humanToV2(user, human) };
|
||||
}
|
||||
|
||||
const machine = user.getMachine();
|
||||
if (machine) {
|
||||
return { case: 'machine', value: machineToV2(machine) };
|
||||
}
|
||||
|
||||
return { case: undefined };
|
||||
}
|
||||
|
||||
function humanToV2(user: User, human: Human): HumanUser {
|
||||
const profile = human.getProfile();
|
||||
const email = human.getEmail()?.getEmail();
|
||||
const phone = human.getPhone();
|
||||
const passwordChanged = human.getPasswordChanged();
|
||||
|
||||
return create(HumanUserSchema, {
|
||||
userId: user.getId(),
|
||||
state: user.getState() as number as UserState,
|
||||
username: user.getUserName(),
|
||||
loginNames: user.getLoginNamesList(),
|
||||
preferredLoginName: user.getPreferredLoginName(),
|
||||
profile: profile && humanProfileToV2(profile),
|
||||
email: { email },
|
||||
phone: phone && humanPhoneToV2(phone),
|
||||
passwordChangeRequired: false,
|
||||
passwordChanged: passwordChanged && timestampToV2(passwordChanged),
|
||||
});
|
||||
}
|
||||
|
||||
function humanProfileToV2(profile: Profile): HumanProfile {
|
||||
return create(HumanProfileSchema, {
|
||||
givenName: profile.getFirstName(),
|
||||
familyName: profile.getLastName(),
|
||||
nickName: profile.getNickName(),
|
||||
displayName: profile.getDisplayName(),
|
||||
preferredLanguage: profile.getPreferredLanguage(),
|
||||
gender: profile.getGender() as number as Gender,
|
||||
avatarUrl: profile.getAvatarUrl(),
|
||||
});
|
||||
}
|
||||
|
||||
function humanPhoneToV2(phone: Phone): HumanPhone {
|
||||
return create(HumanPhoneSchema, {
|
||||
phone: phone.getPhone(),
|
||||
isVerified: phone.getIsPhoneVerified(),
|
||||
});
|
||||
}
|
||||
|
||||
function machineToV2(machine: Machine): MachineUser {
|
||||
return create(MachineUserSchema, {
|
||||
name: machine.getName(),
|
||||
description: machine.getDescription(),
|
||||
hasSecret: machine.getHasSecret(),
|
||||
accessTokenType: machine.getAccessTokenType() as number as AccessTokenType,
|
||||
});
|
||||
}
|
||||
|
@@ -78,6 +78,9 @@
|
||||
@import './styles/codemirror.scss';
|
||||
@import 'src/app/components/copy-row/copy-row.component.scss';
|
||||
@import 'src/app/modules/providers/provider-next/provider-next.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/header-dropdown/header-dropdown.component.scss';
|
||||
|
||||
@mixin component-themes($theme) {
|
||||
@include cnsl-color-theme($theme);
|
||||
@@ -159,4 +162,7 @@
|
||||
@include copy-row-theme($theme);
|
||||
@include provider-next-theme($theme);
|
||||
@include smtp-settings-theme($theme);
|
||||
@include organization-selector-theme($theme);
|
||||
@include instance-selector-theme($theme);
|
||||
@include header-dropdown-theme($theme);
|
||||
}
|
||||
|
@@ -2872,6 +2872,24 @@
|
||||
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2"
|
||||
integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==
|
||||
|
||||
"@tanstack/angular-query-experimental@^5.75.4":
|
||||
version "5.75.4"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/angular-query-experimental/-/angular-query-experimental-5.75.4.tgz#e83db518827e69b71ead484f99b0148b86890003"
|
||||
integrity sha512-diJ2CaSpwloBS7WnOD0TTeHjPi37t7LtflmJXmQHfKDVpnpRHV6mO/JcO79sMozXXSWX9ZWU5eflXvixqstr5A==
|
||||
dependencies:
|
||||
"@tanstack/query-core" "5.75.4"
|
||||
"@tanstack/query-devtools" "5.74.7"
|
||||
|
||||
"@tanstack/query-core@5.75.4":
|
||||
version "5.75.4"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.75.4.tgz#e05f2fe4145fb5354271ad19e63eec61f6ce3012"
|
||||
integrity sha512-pcqOUgWG9oGlzkfRQQMMsEFmtQu0wq81A414CtELZGq+ztVwSTAaoB3AZRAXQJs88LmNMk2YpUKuQbrvzNDyRg==
|
||||
|
||||
"@tanstack/query-devtools@5.74.7":
|
||||
version "5.74.7"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.74.7.tgz#c9b022b386ac86e6395228b5d6912e6444b3b971"
|
||||
integrity sha512-nSNlfuGdnHf4yB0S+BoNYOE1o3oAH093weAYZolIHfS2stulyA/gWfSk/9H4ZFk5mAAHb5vNqAeJOmbdcGPEQw==
|
||||
|
||||
"@tootallnate/once@1":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
||||
|
Reference in New Issue
Block a user