diff --git a/console/package.json b/console/package.json index 0095360017..f4b5ce7845 100644 --- a/console/package.json +++ b/console/package.json @@ -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", diff --git a/console/src/app/app.component.html b/console/src/app/app.component.html index 9907e233e1..0d8831c355 100644 --- a/console/src/app/app.component.html +++ b/console/src/app/app.component.html @@ -1,17 +1,17 @@
+ (changedActiveOrg)="changedOrg()" + /> diff --git a/console/src/app/app.component.ts b/console/src/app/app.component.ts index bd46e30cee..2441f6409d 100644 --- a/console/src/app/app.component.ts +++ b/console/src/app/app.component.ts @@ -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 = of([]); public showAccount: boolean = false; public isDarkTheme: Observable = 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(() => { diff --git a/console/src/app/app.module.ts b/console/src/app/app.module.ts index d6e7e60bea..08b52ea185 100644 --- a/console/src/app/app.module.ts +++ b/console/src/app/app.module.ts @@ -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() {} diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts index 60b4025650..ad045840a9 100644 --- a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts @@ -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'; diff --git a/console/src/app/modules/changes/changes.component.ts b/console/src/app/modules/changes/changes.component.ts index d4ae9b030c..addfad40d4 100644 --- a/console/src/app/modules/changes/changes.component.ts +++ b/console/src/app/modules/changes/changes.component.ts @@ -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; + @Input() public refresh?: Observable; public bottom: boolean = false; private _done: BehaviorSubject = new BehaviorSubject(false); @@ -65,30 +67,26 @@ export class ChangesComponent implements OnInit, OnDestroy { loading: Observable = this._loading.asObservable(); public data: Observable = 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 = 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; switch (this.changeType) { diff --git a/console/src/app/modules/header/header.component.html b/console/src/app/modules/header/header.component.html index f1a3244792..923dfcdbb7 100644 --- a/console/src/app/modules/header/header.component.html +++ b/console/src/app/modules/header/header.component.html @@ -37,134 +37,8 @@ - - - - - - - - - - - - - - - -
- - {{ org.name ? org.name : 'NO NAME' }} - -
- - - - - - -
-
-
- - - - - - - - -
+ @@ -178,33 +52,6 @@
-
- -
- {{ 'MENU.INSTANCE' | translate }} - -
-
- -
- {{ 'MENU.ORGANIZATION' | translate }} - -
-
-
- diff --git a/console/src/app/pages/orgs/org-detail/org-detail.component.html b/console/src/app/pages/orgs/org-detail/org-detail.component.html index c5a6f9533c..c0a818ebfe 100644 --- a/console/src/app/pages/orgs/org-detail/org-detail.component.html +++ b/console/src/app/pages/orgs/org-detail/org-detail.component.html @@ -1,78 +1,72 @@ - - - - - - - - - - - + - + + - - -
- - - + + + + + + + - + + +
+ + + + - -
- {{ - 'ORG.PAGES.NOPERMISSION' | translate - }} + + + +
+ {{ + 'ORG.PAGES.NOPERMISSION' | translate + }} +
+
+ +
+
- - -
- -
- -
+
+
+ diff --git a/console/src/app/pages/orgs/org-detail/org-detail.component.ts b/console/src/app/pages/orgs/org-detail/org-detail.component.ts index 39514d33d3..2c81089ed2 100644 --- a/console/src/app/pages/orgs/org-detail/org-detail.component.ts +++ b/console/src/app/pages/orgs/org-detail/org-detail.component.ts @@ -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 = this.loadingSubject.asObservable(); public totalMemberResult: number = 0; public membersSubject: BehaviorSubject = new BehaviorSubject([]); - private destroy$: Subject = 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 { - 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 { 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); } } } diff --git a/console/src/app/pages/projects/project-grid/project-grid.component.ts b/console/src/app/pages/projects/project-grid/project-grid.component.ts index fba5dc358f..407315853f 100644 --- a/console/src/app/pages/projects/project-grid/project-grid.component.ts +++ b/console/src/app/pages/projects/project-grid/project-grid.component.ts @@ -220,13 +220,13 @@ export class ProjectGridComponent implements OnInit, OnDestroy { } private async getPrefixedItem(key: string): Promise { - const org = this.storage.getItem(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 { - const org = this.storage.getItem(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 { diff --git a/console/src/app/pages/user-grant-create/user-grant-create.component.html b/console/src/app/pages/user-grant-create/user-grant-create.component.html index e2ebbf9583..5d414d4350 100644 --- a/console/src/app/pages/user-grant-create/user-grant-create.component.html +++ b/console/src/app/pages/user-grant-create/user-grant-create.component.html @@ -1,4 +1,5 @@ = 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(StorageKey.organization, StorageLocation.session); - if (temporg) { - this.org = temporg; - } + effect(() => { + if (this.activateOrganizationQuery.isError()) { + this.toast.showError(this.activateOrganizationQuery.error()); + } + }); } public close(): void { diff --git a/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts index b92c112357..061576a628 100644 --- a/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts +++ b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts @@ -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; type AuthenticationFactor = @@ -54,6 +55,7 @@ export class UserCreateV2Component implements OnInit { private readonly passwordComplexityPolicy$: Observable; protected readonly authenticationFactor$: Observable; private readonly useLoginV2$: Observable; + 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 = { - organization: { org: { case: 'orgId', value: org.id } }, + organization: { org: { case: 'orgId', value: this.orgId() } }, username: userValues.username, profile: { givenName: userValues.givenName, diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.html b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.html index c39caba8c4..888150380e 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.html +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.html @@ -1,34 +1,34 @@ - + + /> -
+
- + - + @@ -58,7 +58,7 @@ class="icon-button" card-actions mat-icon-button - (click)="refreshChanges$.emit()" + (click)="invalidateUser()" matTooltip="{{ 'ACTIONS.REFRESH' | translate }}" > refresh @@ -94,7 +94,7 @@ - +
@@ -121,7 +121,7 @@ @@ -170,7 +170,7 @@
- +
diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts index b29d21dc9e..aa75b72db3 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts @@ -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 = new EventEmitter(); protected readonly refreshMetadata$ = new Subject(); protected readonly settingsList: SidenavSetting[] = [ @@ -72,12 +69,33 @@ export class AuthUserDetailComponent implements OnInit { requiredRoles: { [PolicyComponentServiceType.MGMT]: ['user.read'] }, }, ]; - protected readonly user$: Observable; protected readonly metadata$: Observable; - private readonly savedLanguage$: Observable; protected readonly currentSetting$ = signal(this.settingsList[0]); protected readonly loginPolicy$: Observable; - protected readonly userName$: Observable; + protected readonly user = this.userService.userQuery(); + protected readonly refreshChanges$ = new Subject(); + + 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) { - 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) { - 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 { - 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 { - 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 { 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 }; } diff --git a/console/src/app/pages/users/user-detail/password/password.component.ts b/console/src/app/pages/users/user-detail/password/password.component.ts index ef18559e09..e18116c2c3 100644 --- a/console/src/app/pages/users/user-detail/password/password.component.ts +++ b/console/src/app/pages/users/user-detail/password/password.component.ts @@ -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) { diff --git a/console/src/app/pages/users/user-list/user-table/user-table.component.html b/console/src/app/pages/users/user-list/user-table/user-table.component.html index af293c01a4..180b0e082d 100644 --- a/console/src/app/pages/users/user-list/user-table/user-table.component.html +++ b/console/src/app/pages/users/user-list/user-table/user-table.component.html @@ -1,176 +1,177 @@ - -
- - -
-

- {{ - (type === Type.HUMAN ? 'DESCRIPTIONS.USERS.HUMANS.DESCRIPTION' : 'DESCRIPTIONS.USERS.MACHINES.DESCRIPTION') | translate - }} -

- - - - - - - + + + -
- add - {{ 'ACTIONS.NEW' | translate }} - - -
- - + (filterChanged)="this.searchQueries$.next($any($event))" + (filterOpen)="filterOpen = $event" + >
+ + + -
- - - - + +
-
- - - -
-
-
- - + + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - + - - + + + + - - - - + + + + - - - + - + mat-icon-button + > + + + + + - - -
+
+ - - - + - - -
- - + + +
+
+ + + + + + + + + +
+
- {{ 'USER.PROFILE.DISPLAYNAME' | translate }} - - {{ user.type.value?.profile?.displayName }} - {{ user.type.value.name }} - + {{ 'USER.PROFILE.DISPLAYNAME' | translate }} + + {{ user.type.value?.profile?.displayName }} + {{ user.type.value.name }} + - {{ 'USER.PROFILE.PREFERREDLOGINNAME' | translate }} - - {{ user.preferredLoginName }} - + {{ 'USER.PROFILE.PREFERREDLOGINNAME' | translate }} + + {{ user.preferredLoginName }} + - {{ 'USER.PROFILE.USERNAME' | translate }} - - {{ user.username }} - + {{ 'USER.PROFILE.USERNAME' | translate }} + + {{ user.username }} + - {{ 'USER.EMAIL' | translate }} - - {{ user.type.value.email.email }} - + {{ 'USER.EMAIL' | translate }} + + {{ user.type.value.email.email }} + {{ 'USER.DATA.STATE' | translate }} + + {{ 'USER.DATA.STATE' | translate }} - {{ 'USER.TABLE.CREATIONDATE' | translate }} - {{ user.details.creationDate | timestampToDate | localizedDate: 'regular' }} - {{ 'USER.TABLE.CREATIONDATE' | translate }} + {{ user.details.creationDate | timestampToDate | localizedDate: 'regular' }} + {{ 'USER.TABLE.CHANGEDATE' | translate }} - {{ user.details.changeDate | timestampToDate | localizedDate: 'regular' }} - {{ 'USER.TABLE.CHANGEDATE' | translate }} + {{ user.details.changeDate | timestampToDate | localizedDate: 'regular' }} + - - + + - -
-
+
+
-
- - {{ 'USER.TABLE.EMPTY' | translate }} -
- - -
+
+ + {{ 'USER.TABLE.EMPTY' | translate }} +
+ + + + diff --git a/console/src/app/pages/users/user-list/user-table/user-table.component.ts b/console/src/app/pages/users/user-list/user-table/user-table.component.ts index 6ff0d2cd67..67a10ace42 100644 --- a/console/src/app/pages/users/user-list/user-table/user-table.component.ts +++ b/console/src/app/pages/users/user-list/user-table/user-table.component.ts @@ -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['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(1); + protected readonly userQuery = this.userService.userQuery(); @Input() public canWrite$: Observable = of(false); @Input() public canDelete$: Observable = of(false); protected readonly dataSize: Signal; - protected readonly loading = signal(false); + protected readonly loading = signal(true); private readonly paginator$ = new ReplaySubject(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(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; protected readonly type$: Observable; protected readonly searchQueries$ = new ReplaySubject(1); - protected readonly myUser: Signal; @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 { return this.route.queryParamMap.pipe( map((params) => params.get('type')), @@ -221,20 +232,18 @@ export class UserTableComponent implements OnInit { } private getQueries(type$: Observable): Observable { - 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 => !!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), - ); - } } diff --git a/console/src/app/services/admin.service.ts b/console/src/app/services/admin.service.ts index 59c125380f..506056d915 100644 --- a/console/src/app/services/admin.service.ts +++ b/console/src/app/services/admin.service.ts @@ -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 { return this.grpcService.admin.listMilestones(req, null).then((resp) => resp.toObject()); } - - public listOrgs( - limit: number, - offset: number, - queriesList?: OrgQuery[], - sortingColumn?: OrgFieldName, - sortingDirection?: SortDirection, - ): Promise { - 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()); - } } diff --git a/console/src/app/services/grpc-auth.service.ts b/console/src/app/services/grpc-auth.service.ts index 105cb62487..d9c1e12a66 100644 --- a/console/src/app/services/grpc-auth.service.ts +++ b/console/src/app/services/grpc-auth.service.ts @@ -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 = new Subject(); public user: Observable; private triggerPermissionsRefresh: Subject = new Subject(); public zitadelPermissions: Observable; @@ -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 { - 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(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 { - 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(); } diff --git a/console/src/app/services/grpc.service.ts b/console/src/app/services/grpc.service.ts index d2add12f41..3e83de7d38 100644 --- a/console/src/app/services/grpc.service.ts +++ b/console/src/app/services/grpc.service.ts @@ -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; public actionNew!: ReturnType; public webKey!: ReturnType; + public organizationNew!: ReturnType; + public adminNew!: ReturnType; + + 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', diff --git a/console/src/app/services/interceptors/org.interceptor.ts b/console/src/app/services/interceptors/org.interceptor.ts index e9e9745b12..626b032ee6 100644 --- a/console/src/app/services/interceptors/org.interceptor.ts +++ b/console/src/app/services/interceptors/org.interceptor.ts @@ -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; diff --git a/console/src/app/services/new-admin.service.ts b/console/src/app/services/new-admin.service.ts new file mode 100644 index 0000000000..bbf8609487 --- /dev/null +++ b/console/src/app/services/new-admin.service.ts @@ -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): Promise { + return this.grpcService.adminNew.setupOrg(req); + } + + public getDefaultOrg(): Promise { + return this.grpcService.adminNew.getDefaultOrg({}); + } + + private getMyInstance(signal?: AbortSignal): Promise { + return this.grpcService.adminNew.getMyInstance({}, { signal }); + } + + public getMyInstanceQuery() { + return injectQuery(() => ({ + queryKey: ['admin', 'getMyInstance'], + queryFn: ({ signal }) => this.getMyInstance(signal), + })); + } +} diff --git a/console/src/app/services/new-auth.service.ts b/console/src/app/services/new-auth.service.ts index 4864d8d95f..b7a4463168 100644 --- a/console/src/app/services/new-auth.service.ts +++ b/console/src/app/services/new-auth.service.ts @@ -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 { - return this.grpcService.authNew.getMyUser({}); - } - public verifyMyPhone(code: string): Promise { return this.grpcService.authNew.verifyMyPhone({ code }); } diff --git a/console/src/app/services/new-mgmt.service.ts b/console/src/app/services/new-mgmt.service.ts index 6798d25f41..250838e746 100644 --- a/console/src/app/services/new-mgmt.service.ts +++ b/console/src/app/services/new-mgmt.service.ts @@ -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 { return this.grpcService.mgmtNew.getDefaultPasswordComplexityPolicy({}); } + + public updateOrg(req: MessageInitShape): Promise { + return this.grpcService.mgmtNew.updateOrg(req); + } + + public removeOrg(): Promise { + return this.grpcService.mgmtNew.removeOrg({}); + } + + public reactivateOrg(): Promise { + return this.grpcService.mgmtNew.reactivateOrg({}); + } + + public deactivateOrg(): Promise { + return this.grpcService.mgmtNew.deactivateOrg({}); + } + + public addOrg(name: string): Promise { + return this.grpcService.mgmtNew.addOrg({ name }); + } } diff --git a/console/src/app/services/new-organization.service.ts b/console/src/app/services/new-organization.service.ts new file mode 100644 index 0000000000..beaf8c00cc --- /dev/null +++ b/console/src/app/services/new-organization.service.ts @@ -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( + 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) { + return queryOptions({ + queryKey: this.listOrganizationsQueryKey(req), + queryFn: () => this.listOrganizations(req ?? {}), + }); + } + + public listOrganizationsQueryKey(req?: MessageInitShape) { + 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, + signal?: AbortSignal, + ): Promise { + 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) => 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(); + }, + }); +} diff --git a/console/src/app/services/storage.service.ts b/console/src/app/services/storage.service.ts index 5b3ab3f985..3ea841bed9 100644 --- a/console/src/app/services/storage.service.ts +++ b/console/src/app/services/storage.service.ts @@ -43,7 +43,7 @@ export class StorageConfig { } export enum StorageKey { - organization = 'organization', + organizationId = 'organizationId', } export enum StorageLocation { diff --git a/console/src/app/services/user.service.ts b/console/src/app/services/user.service.ts index a5bbd0aaff..56db2b10a2 100644 --- a/console/src/app/services/user.service.ts +++ b/console/src/app/services/user.service.ts @@ -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>(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): Promise { @@ -157,10 +133,6 @@ export class UserService { return this.grpcService.userNew.updateHumanUser(create(UpdateHumanUserRequestSchema, req)); } - public lockUser(userId: string): Promise { - return this.grpcService.userNew.lockUser(create(LockUserRequestSchema, { userId })); - } - public unlockUser(userId: string): Promise { 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) { - return this.grpcService.userNew.passwordReset(create(PasswordResetRequestSchema, req)); - } - public setPassword(req: MessageInitShape): Promise { 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, - }); -} diff --git a/console/src/component-themes.scss b/console/src/component-themes.scss index d695affe9d..f539f963a9 100644 --- a/console/src/component-themes.scss +++ b/console/src/component-themes.scss @@ -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); } diff --git a/console/yarn.lock b/console/yarn.lock index 3763923846..87b80f3aec 100644 --- a/console/yarn.lock +++ b/console/yarn.lock @@ -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"