feat(app): wire dashboard administration flows
This commit is contained in:
+793
-133
@@ -1,27 +1,19 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import * as states from '../../states/accountstate.js';
|
||||
import { IdpState } from '../../states/idp.state.js';
|
||||
import { BulkInviteModal } from './bulk-invite-modal.js';
|
||||
import { CreateOrgModal } from './create-org-modal.js';
|
||||
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
cssManager,
|
||||
unsafeCSS,
|
||||
css,
|
||||
state,
|
||||
type TemplateResult
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { LeleAccountNavigation } from './navigation.js';
|
||||
import { OrgSelectModal, type IOrgSelectResult } from './org-select-modal.js';
|
||||
import { CreateOrgModal } from './create-org-modal.js';
|
||||
import { accountDesignTokens } from './sharedstyles.js';
|
||||
|
||||
import * as views from './views/index.js';
|
||||
import * as accountstate from '../../states/accountstate.js';
|
||||
|
||||
import { commitinfo } from '../../../ts/00_commitinfo_data.js';
|
||||
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'idp-accountcontent': IdpAccountContent;
|
||||
@@ -32,6 +24,61 @@ declare global {
|
||||
export class IdpAccountContent extends DeesElement {
|
||||
|
||||
public subrouter: plugins.deesDomtools.plugins.smartrouter.SmartRouter;
|
||||
private dataLoadRun = 0;
|
||||
|
||||
@state()
|
||||
private accessor adminPage: plugins.idpCatalog.IdpAdminShell['page'] = 'overview';
|
||||
|
||||
@state()
|
||||
private accessor adminUser: plugins.idpCatalog.IIdpAdminUser = {
|
||||
name: 'Loading account',
|
||||
email: '',
|
||||
};
|
||||
|
||||
@state()
|
||||
private accessor adminOrgs: plugins.idpCatalog.IIdpAdminOrg[] = [];
|
||||
|
||||
@state()
|
||||
private accessor selectedOrgId = '';
|
||||
|
||||
@state()
|
||||
private accessor globalAdmin = false;
|
||||
|
||||
@state()
|
||||
private accessor dataLoading = false;
|
||||
|
||||
@state()
|
||||
private accessor dataError = '';
|
||||
|
||||
@state()
|
||||
private accessor sessions: plugins.idpCatalog.IIdpAdminSession[] = [];
|
||||
|
||||
@state()
|
||||
private accessor activities: plugins.idpCatalog.IIdpAdminActivity[] = [];
|
||||
|
||||
@state()
|
||||
private accessor orgMembers: plugins.idpCatalog.IIdpAdminMember[] = [];
|
||||
|
||||
@state()
|
||||
private accessor orgInvitations: plugins.idpCatalog.IIdpAdminInvitation[] = [];
|
||||
|
||||
@state()
|
||||
private accessor orgRoleDefinitions: plugins.idpCatalog.IIdpAdminOrgRoleDefinition[] = [];
|
||||
|
||||
@state()
|
||||
private accessor orgApps: plugins.idpCatalog.IIdpAdminApp[] = [];
|
||||
|
||||
@state()
|
||||
private accessor adminApps: plugins.idpCatalog.IIdpAdminApp[] = [];
|
||||
|
||||
@state()
|
||||
private accessor passportDevices: plugins.idpCatalog.IIdpAdminPassportDevice[] = [];
|
||||
|
||||
@state()
|
||||
private accessor passportEnrollment: plugins.idpCatalog.IIdpAdminPassportEnrollment | null = null;
|
||||
|
||||
@state()
|
||||
private accessor credentialMessage = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -39,169 +86,782 @@ export class IdpAccountContent extends DeesElement {
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
accountDesignTokens,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
background: var(--background);
|
||||
overflow: hidden;
|
||||
background: var(--idp-bg, hsl(240 10% 3.9%));
|
||||
}
|
||||
:host([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
.main {
|
||||
position: absolute;
|
||||
idp-admin-shell {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
lele-accountnavigation {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
height: 100vh;
|
||||
width: 200px;
|
||||
}
|
||||
.viewcontainer {
|
||||
will-change: transform;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
width: calc(100vw - 200px);
|
||||
height: 100vh;
|
||||
overflow-y: scroll;
|
||||
overscroll-behavior: contain;
|
||||
transition: all 0.3s ease;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.viewcontainer.changing {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style></style>
|
||||
<div class="main">
|
||||
<lele-accountnavigation></lele-accountnavigation>
|
||||
<div class="viewcontainer">
|
||||
<!--<lele-accountview-subscription></lele-accountview-subscription>-->
|
||||
</div>
|
||||
</div>
|
||||
<idp-admin-shell
|
||||
.page=${this.adminPage}
|
||||
.user=${this.adminUser}
|
||||
.orgs=${this.adminOrgs}
|
||||
.selectedOrgId=${this.selectedOrgId}
|
||||
.globalAdmin=${this.globalAdmin}
|
||||
.dataLoading=${this.dataLoading}
|
||||
.dataError=${this.dataError}
|
||||
.sessions=${this.sessions}
|
||||
.activities=${this.activities}
|
||||
.orgMembers=${this.orgMembers}
|
||||
.orgInvitations=${this.orgInvitations}
|
||||
.orgRoleDefinitions=${this.orgRoleDefinitions}
|
||||
.orgApps=${this.orgApps}
|
||||
.adminApps=${this.adminApps}
|
||||
.passportDevices=${this.passportDevices}
|
||||
.passportEnrollment=${this.passportEnrollment}
|
||||
.credentialMessage=${this.credentialMessage}
|
||||
@idp-admin-navigate=${this.handleAdminNavigate}
|
||||
@idp-admin-org-select=${this.handleOrgSelect}
|
||||
@idp-admin-org-create=${this.handleOrgCreate}
|
||||
@idp-admin-org-update=${this.handleOrgUpdate}
|
||||
@idp-admin-org-transfer=${this.handleOrgTransfer}
|
||||
@idp-admin-org-delete=${this.handleOrgDelete}
|
||||
@idp-admin-session-revoke=${this.handleSessionRevoke}
|
||||
@idp-admin-app-toggle=${this.handleAppToggle}
|
||||
@idp-admin-password-change=${this.handlePasswordChange}
|
||||
@idp-admin-passport-enroll=${this.handlePassportEnroll}
|
||||
@idp-admin-passport-revoke=${this.handlePassportRevoke}
|
||||
@idp-admin-member-invite=${this.handleMemberInvite}
|
||||
@idp-admin-member-remove=${this.handleMemberRemove}
|
||||
@idp-admin-member-roles-update=${this.handleMemberRolesUpdate}
|
||||
@idp-admin-invitation-resend=${this.handleInvitationResend}
|
||||
@idp-admin-invitation-cancel=${this.handleInvitationCancel}
|
||||
@idp-admin-org-role-upsert=${this.handleOrgRoleUpsert}
|
||||
@idp-admin-org-role-delete=${this.handleOrgRoleDelete}
|
||||
@idp-admin-app-role-mappings-update=${this.handleAppRoleMappingsUpdate}
|
||||
></idp-admin-shell>
|
||||
`;
|
||||
}
|
||||
|
||||
private setAdminPage(pageArg: plugins.idpCatalog.IdpAdminShell['page']) {
|
||||
this.adminPage = pageArg;
|
||||
if (this.subrouter) {
|
||||
void this.loadAdminShellData();
|
||||
}
|
||||
}
|
||||
|
||||
private getSelectedOrgSlug(): string {
|
||||
const currentState = states.accountState.getState();
|
||||
const selectedOrg = currentState.selectedOrg
|
||||
|| currentState.organizations.find((orgArg) => orgArg.id === this.selectedOrgId)
|
||||
|| currentState.organizations[0];
|
||||
return selectedOrg?.data?.slug || this.adminOrgs.find((orgArg) => orgArg.id === this.selectedOrgId)?.slug || this.adminOrgs[0]?.slug || '';
|
||||
}
|
||||
|
||||
private getPathForPage(pageArg: plugins.idpCatalog.IdpAdminShell['page']): string | null {
|
||||
const orgSlug = this.getSelectedOrgSlug();
|
||||
const orgPath = (suffixArg = '') => orgSlug ? `/org/${orgSlug}${suffixArg}` : null;
|
||||
|
||||
const pageMap: Record<plugins.idpCatalog.IdpAdminShell['page'], string | null> = {
|
||||
overview: '/overview',
|
||||
profile: '/account/profile',
|
||||
security: '/account/security',
|
||||
sessions: '/account/sessions',
|
||||
apps: '/account/apps',
|
||||
'org-general': orgPath(),
|
||||
'org-settings': orgPath('/settings'),
|
||||
'org-members': orgPath('/users'),
|
||||
'org-apps': orgPath('/apps'),
|
||||
support: '/support',
|
||||
'ga-users': '/admin/users',
|
||||
'ga-orgs': '/admin/orgs',
|
||||
'ga-apps': '/admin/apps',
|
||||
};
|
||||
|
||||
return pageMap[pageArg];
|
||||
}
|
||||
|
||||
private pushDashPath(pathArg: string) {
|
||||
const normalizedPath = pathArg || '';
|
||||
const absolutePath = `/dash${normalizedPath}`.replace(/\/$/, '') || '/dash';
|
||||
if (window.location.pathname.replace(/\/$/, '') === absolutePath) {
|
||||
return;
|
||||
}
|
||||
this.subrouter.pushUrl(normalizedPath);
|
||||
}
|
||||
|
||||
private async handleAdminNavigate(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminNavigateEventDetail>) {
|
||||
const page = eventArg.detail.page;
|
||||
this.setAdminPage(page);
|
||||
const path = this.getPathForPage(page);
|
||||
if (path !== null) {
|
||||
this.pushDashPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleOrgSelect(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminOrgSelectEventDetail>) {
|
||||
const currentState = states.accountState.getState();
|
||||
const selectedOrg = currentState.organizations.find((orgArg) => orgArg.id === eventArg.detail.orgId)
|
||||
|| currentState.organizations.find((orgArg) => orgArg.data.slug === eventArg.detail.org?.slug);
|
||||
|
||||
this.selectedOrgId = eventArg.detail.orgId;
|
||||
this.setAdminPage('org-general');
|
||||
|
||||
if (selectedOrg) {
|
||||
await states.accountState.dispatchAction(states.setSelectedOrg, selectedOrg);
|
||||
this.pushDashPath(`/org/${selectedOrg.data.slug}`);
|
||||
} else if (eventArg.detail.org?.slug) {
|
||||
this.pushDashPath(`/org/${eventArg.detail.org.slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleOrgCreate() {
|
||||
const org = await CreateOrgModal.show();
|
||||
if (!org) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.applyAccountState();
|
||||
this.selectedOrgId = org.id;
|
||||
this.setAdminPage('org-general');
|
||||
this.pushDashPath(`/org/${org.data.slug}`);
|
||||
}
|
||||
|
||||
private async handleOrgUpdate(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminOrgUpdateEventDetail>) {
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_UpdateOrganization>('updateOrganization');
|
||||
const response = await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
organizationId: eventArg.detail.organizationId,
|
||||
name: eventArg.detail.name,
|
||||
slug: eventArg.detail.slug,
|
||||
confirmationText: eventArg.detail.confirmationText,
|
||||
});
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Organization update failed.');
|
||||
}
|
||||
|
||||
await states.accountState.dispatchAction(states.getOrganizationsAction, null);
|
||||
const refreshedOrg = states.accountState.getState().organizations.find((orgArg) => orgArg.id === response.organization.id) || response.organization;
|
||||
await states.accountState.dispatchAction(states.setSelectedOrg, refreshedOrg);
|
||||
this.applyAccountState();
|
||||
this.selectedOrgId = refreshedOrg.id;
|
||||
this.setAdminPage('org-settings');
|
||||
this.pushDashPath(`/org/${refreshedOrg.data.slug}/settings`);
|
||||
});
|
||||
}
|
||||
|
||||
private async handleOrgTransfer(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminOrgTransferEventDetail>) {
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_TransferOwnership>('transferOwnership');
|
||||
const response = await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
organizationId: eventArg.detail.organizationId,
|
||||
newOwnerId: eventArg.detail.newOwnerId,
|
||||
confirmationText: eventArg.detail.confirmationText,
|
||||
});
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Ownership transfer failed.');
|
||||
}
|
||||
|
||||
await states.accountState.dispatchAction(states.getOrganizationsAction, null);
|
||||
const refreshedOrg = states.accountState.getState().organizations.find((orgArg) => orgArg.id === eventArg.detail.organizationId);
|
||||
if (refreshedOrg) {
|
||||
await states.accountState.dispatchAction(states.setSelectedOrg, refreshedOrg);
|
||||
this.selectedOrgId = refreshedOrg.id;
|
||||
}
|
||||
this.applyAccountState();
|
||||
this.setAdminPage('org-settings');
|
||||
});
|
||||
}
|
||||
|
||||
private async handleOrgDelete(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminOrgDeleteEventDetail>) {
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_DeleteOrganization>('deleteOrganization');
|
||||
const response = await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
organizationId: eventArg.detail.organizationId,
|
||||
confirmationText: eventArg.detail.confirmationText,
|
||||
});
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Organization deletion failed.');
|
||||
}
|
||||
|
||||
await states.accountState.dispatchAction(states.getOrganizationsAction, null);
|
||||
const nextOrg = states.accountState.getState().organizations[0] || null;
|
||||
if (nextOrg) {
|
||||
await states.accountState.dispatchAction(states.setSelectedOrg, nextOrg);
|
||||
} else {
|
||||
await states.accountState.dispatchAction(states.setSelectedOrg, null as any);
|
||||
}
|
||||
this.selectedOrgId = nextOrg?.id || '';
|
||||
this.applyAccountState();
|
||||
this.setAdminPage('overview');
|
||||
this.pushDashPath('/overview');
|
||||
});
|
||||
}
|
||||
|
||||
private async syncSelectedOrgFromPath() {
|
||||
const orgSlug = window.location.pathname.match(/^\/dash\/org\/([^/]+)/)?.[1];
|
||||
if (!orgSlug) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentState = states.accountState.getState();
|
||||
const selectedOrg = currentState.organizations.find((orgArg) => orgArg.data.slug === orgSlug);
|
||||
if (!selectedOrg) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedOrgId = selectedOrg.id;
|
||||
if (currentState.selectedOrg?.id !== selectedOrg.id) {
|
||||
await states.accountState.dispatchAction(states.setSelectedOrg, selectedOrg);
|
||||
}
|
||||
}
|
||||
|
||||
private applyAccountState() {
|
||||
const currentState = states.accountState.getState();
|
||||
const user = currentState.user;
|
||||
|
||||
if (user) {
|
||||
this.adminUser = {
|
||||
name: user.data.name || user.data.username || user.data.email,
|
||||
email: user.data.email,
|
||||
username: user.data.username,
|
||||
mobileNumber: user.data.mobileNumber,
|
||||
status: user.data.status,
|
||||
};
|
||||
this.globalAdmin = Boolean(user.data.isGlobalAdmin);
|
||||
}
|
||||
|
||||
this.adminOrgs = currentState.organizations.map((orgArg) => {
|
||||
const role = currentState.roles.find((roleArg) => roleArg.data.organizationId === orgArg.id);
|
||||
return {
|
||||
id: orgArg.id,
|
||||
name: orgArg.data.name,
|
||||
slug: orgArg.data.slug,
|
||||
myRole: role?.data.roles?.[0] || 'member',
|
||||
};
|
||||
});
|
||||
|
||||
this.selectedOrgId = currentState.selectedOrg?.id || this.selectedOrgId || currentState.organizations[0]?.id || '';
|
||||
const selectedOrg = currentState.organizations.find((orgArg) => orgArg.id === this.selectedOrgId) || currentState.selectedOrg || currentState.organizations[0];
|
||||
this.orgRoleDefinitions = selectedOrg?.data.roleDefinitions || [];
|
||||
}
|
||||
|
||||
private async setOrgPage(pageArg: plugins.idpCatalog.IdpAdminShell['page']) {
|
||||
await this.syncSelectedOrgFromPath();
|
||||
this.setAdminPage(pageArg);
|
||||
}
|
||||
|
||||
private getSelectedOrganization(): plugins.idpInterfaces.data.IOrganization | null {
|
||||
const currentState = states.accountState.getState();
|
||||
return currentState.selectedOrg
|
||||
|| currentState.organizations.find((orgArg) => orgArg.id === this.selectedOrgId)
|
||||
|| currentState.organizations[0]
|
||||
|| null;
|
||||
}
|
||||
|
||||
private async loadSessions(idpStateArg: IdpState, jwtArg: string): Promise<plugins.idpCatalog.IIdpAdminSession[]> {
|
||||
const request = idpStateArg.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetUserSessions>('getUserSessions');
|
||||
const response = await request.fire({ jwt: jwtArg });
|
||||
return (response.sessions || []).map((sessionArg) => ({
|
||||
id: sessionArg.id,
|
||||
deviceName: sessionArg.deviceName,
|
||||
browser: sessionArg.browser,
|
||||
os: sessionArg.os,
|
||||
ip: sessionArg.ip,
|
||||
lastActive: sessionArg.lastActive,
|
||||
createdAt: sessionArg.createdAt,
|
||||
isCurrent: sessionArg.isCurrent,
|
||||
}));
|
||||
}
|
||||
|
||||
private async loadActivities(idpStateArg: IdpState, jwtArg: string): Promise<plugins.idpCatalog.IIdpAdminActivity[]> {
|
||||
const request = idpStateArg.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetUserActivity>('getUserActivity');
|
||||
const response = await request.fire({ jwt: jwtArg, limit: 20 });
|
||||
return (response.activities || []).map((activityArg) => ({
|
||||
id: activityArg.id,
|
||||
action: activityArg.data.action,
|
||||
description: activityArg.data.metadata.description,
|
||||
timestamp: activityArg.data.timestamp,
|
||||
ip: activityArg.data.metadata.ip,
|
||||
targetType: activityArg.data.metadata.targetType,
|
||||
}));
|
||||
}
|
||||
|
||||
private async loadOrgMembers(idpStateArg: IdpState, jwtArg: string, organizationIdArg: string): Promise<plugins.idpCatalog.IIdpAdminMember[]> {
|
||||
const currentState = states.accountState.getState();
|
||||
const request = idpStateArg.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetOrgMembers>('getOrgMembers');
|
||||
const response = await request.fire({ jwt: jwtArg, organizationId: organizationIdArg });
|
||||
return (response.members || []).map((memberArg) => ({
|
||||
userId: memberArg.user.id,
|
||||
name: memberArg.user.data.name || memberArg.user.data.username || memberArg.user.data.email,
|
||||
email: memberArg.user.data.email,
|
||||
roles: memberArg.role.data.roles || [],
|
||||
isCurrentUser: currentState.user?.id === memberArg.user.id,
|
||||
}));
|
||||
}
|
||||
|
||||
private async loadOrgInvitations(idpStateArg: IdpState, jwtArg: string, organizationIdArg: string): Promise<plugins.idpCatalog.IIdpAdminInvitation[]> {
|
||||
const request = idpStateArg.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetOrgInvitations>('getOrgInvitations');
|
||||
const response = await request.fire({ jwt: jwtArg, organizationId: organizationIdArg });
|
||||
return (response.invitations || []).map((invitationArg) => {
|
||||
const orgRef = invitationArg.data.organizationRefs.find((refArg) => refArg.organizationId === organizationIdArg)
|
||||
|| invitationArg.data.organizationRefs[0];
|
||||
return {
|
||||
id: invitationArg.id,
|
||||
email: invitationArg.data.email,
|
||||
roles: orgRef?.roles || [],
|
||||
invitedAt: orgRef?.invitedAt || invitationArg.data.createdAt,
|
||||
expiresAt: invitationArg.data.expiresAt,
|
||||
status: invitationArg.data.status,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async loadOrgApps(idpStateArg: IdpState, jwtArg: string, organizationIdArg: string): Promise<plugins.idpCatalog.IIdpAdminApp[]> {
|
||||
const appsRequest = idpStateArg.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetGlobalApps>('getGlobalApps');
|
||||
const connectionsRequest = idpStateArg.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetAppConnections>('getAppConnections');
|
||||
const [appsResponse, connectionsResponse] = await Promise.all([
|
||||
appsRequest.fire({ jwt: jwtArg }),
|
||||
connectionsRequest.fire({ jwt: jwtArg, organizationId: organizationIdArg }),
|
||||
]);
|
||||
const activeConnectionMap = new Map((connectionsResponse.connections || [])
|
||||
.filter((connectionArg) => connectionArg.data.status === 'active')
|
||||
.map((connectionArg) => [connectionArg.data.appId, connectionArg]));
|
||||
return (appsResponse.apps || []).map((appArg) => ({
|
||||
id: appArg.id,
|
||||
name: appArg.data.name,
|
||||
description: appArg.data.description,
|
||||
logoUrl: appArg.data.logoUrl,
|
||||
appUrl: appArg.data.appUrl,
|
||||
category: appArg.data.category,
|
||||
type: appArg.type,
|
||||
status: appArg.data.isActive ? 'active' : 'inactive',
|
||||
isConnected: activeConnectionMap.has(appArg.id),
|
||||
roleMappings: activeConnectionMap.get(appArg.id)?.data.roleMappings || [],
|
||||
clientId: appArg.data.oauthCredentials.clientId,
|
||||
scopes: activeConnectionMap.get(appArg.id)?.data.grantedScopes || appArg.data.oauthCredentials.allowedScopes || [],
|
||||
grants: appArg.data.oauthCredentials.grantTypes || [],
|
||||
}));
|
||||
}
|
||||
|
||||
private async loadAdminApps(idpStateArg: IdpState, jwtArg: string): Promise<plugins.idpCatalog.IIdpAdminApp[]> {
|
||||
if (!this.globalAdmin) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const request = idpStateArg.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetGlobalAppStats>('getGlobalAppStats');
|
||||
const response = await request.fire({ jwt: jwtArg });
|
||||
return (response.apps || []).map((entryArg) => ({
|
||||
id: entryArg.app.id,
|
||||
name: entryArg.app.data.name,
|
||||
description: entryArg.app.data.description,
|
||||
logoUrl: entryArg.app.data.logoUrl,
|
||||
appUrl: entryArg.app.data.appUrl,
|
||||
category: entryArg.app.data.category,
|
||||
type: entryArg.app.type,
|
||||
status: entryArg.app.data.isActive ? 'active' : 'inactive',
|
||||
connectionCount: entryArg.connectionCount,
|
||||
clientId: entryArg.app.data.oauthCredentials.clientId,
|
||||
scopes: entryArg.app.data.oauthCredentials.allowedScopes || [],
|
||||
grants: entryArg.app.data.oauthCredentials.grantTypes || [],
|
||||
}));
|
||||
}
|
||||
|
||||
private async loadPassportDevices(idpStateArg: IdpState, jwtArg: string): Promise<plugins.idpCatalog.IIdpAdminPassportDevice[]> {
|
||||
const request = idpStateArg.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetPassportDevices>('getPassportDevices');
|
||||
const response = await request.fire({ jwt: jwtArg });
|
||||
return (response.devices || []).map((deviceArg) => ({
|
||||
id: deviceArg.id,
|
||||
label: deviceArg.data.label,
|
||||
platform: deviceArg.data.platform,
|
||||
status: deviceArg.data.status,
|
||||
capabilities: deviceArg.data.capabilities,
|
||||
appVersion: deviceArg.data.appVersion,
|
||||
createdAt: deviceArg.data.createdAt,
|
||||
lastSeenAt: deviceArg.data.lastSeenAt,
|
||||
lastChallengeAt: deviceArg.data.lastChallengeAt,
|
||||
pushRegistered: Boolean(deviceArg.data.pushRegistration),
|
||||
}));
|
||||
}
|
||||
|
||||
private async loadAdminShellData() {
|
||||
const currentRun = ++this.dataLoadRun;
|
||||
this.dataLoading = true;
|
||||
this.dataError = '';
|
||||
|
||||
try {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const jwt = await idpState.idpClient.getJwt();
|
||||
const selectedOrg = this.getSelectedOrganization();
|
||||
const orgId = selectedOrg?.id || '';
|
||||
|
||||
const [sessions, activities, members, invitations, orgApps, adminApps, passportDevices] = await Promise.all([
|
||||
this.loadSessions(idpState, jwt).catch((error) => {
|
||||
console.error('Error loading sessions:', error);
|
||||
return this.sessions;
|
||||
}),
|
||||
this.loadActivities(idpState, jwt).catch((error) => {
|
||||
console.error('Error loading activity:', error);
|
||||
return this.activities;
|
||||
}),
|
||||
orgId ? this.loadOrgMembers(idpState, jwt, orgId).catch((error) => {
|
||||
console.error('Error loading org members:', error);
|
||||
return this.orgMembers;
|
||||
}) : Promise.resolve([]),
|
||||
orgId ? this.loadOrgInvitations(idpState, jwt, orgId).catch((error) => {
|
||||
console.error('Error loading org invitations:', error);
|
||||
return this.orgInvitations;
|
||||
}) : Promise.resolve([]),
|
||||
orgId ? this.loadOrgApps(idpState, jwt, orgId).catch((error) => {
|
||||
console.error('Error loading org apps:', error);
|
||||
return this.orgApps;
|
||||
}) : Promise.resolve([]),
|
||||
this.loadAdminApps(idpState, jwt).catch((error) => {
|
||||
console.error('Error loading admin apps:', error);
|
||||
return this.adminApps;
|
||||
}),
|
||||
this.loadPassportDevices(idpState, jwt).catch((error) => {
|
||||
console.error('Error loading passport devices:', error);
|
||||
return this.passportDevices;
|
||||
}),
|
||||
]);
|
||||
|
||||
if (currentRun !== this.dataLoadRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sessions = sessions;
|
||||
this.activities = activities;
|
||||
this.orgMembers = members;
|
||||
this.orgInvitations = invitations;
|
||||
this.orgApps = orgApps;
|
||||
this.adminApps = adminApps;
|
||||
this.passportDevices = passportDevices;
|
||||
} catch (error) {
|
||||
console.error('Error loading admin shell data:', error);
|
||||
if (currentRun === this.dataLoadRun) {
|
||||
this.dataError = error instanceof Error ? error.message : 'Failed to load admin console data.';
|
||||
}
|
||||
} finally {
|
||||
if (currentRun === this.dataLoadRun) {
|
||||
this.dataLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async runAdminAction(actionArg: () => Promise<void>) {
|
||||
this.dataError = '';
|
||||
try {
|
||||
await actionArg();
|
||||
await this.loadAdminShellData();
|
||||
} catch (error) {
|
||||
console.error('Admin console action failed:', error);
|
||||
this.dataError = error instanceof Error ? error.message : 'Action failed. Please try again.';
|
||||
}
|
||||
}
|
||||
|
||||
private async handleSessionRevoke(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminSessionEventDetail>) {
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RevokeSession>('revokeSession');
|
||||
await request.fire({ jwt: await idpState.idpClient.getJwt(), sessionId: eventArg.detail.sessionId });
|
||||
});
|
||||
}
|
||||
|
||||
private async handleAppToggle(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminAppToggleEventDetail>) {
|
||||
const selectedOrg = this.getSelectedOrganization();
|
||||
if (!selectedOrg) {
|
||||
this.dataError = 'Select an organisation before changing app connections.';
|
||||
return;
|
||||
}
|
||||
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_ToggleAppConnection>('toggleAppConnection');
|
||||
await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
organizationId: selectedOrg.id,
|
||||
appId: eventArg.detail.appId,
|
||||
action: eventArg.detail.connected ? 'connect' : 'disconnect',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async handlePasswordChange(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminPasswordChangeEventDetail>) {
|
||||
const email = states.accountState.getState().user?.data.email;
|
||||
if (!email) {
|
||||
this.credentialMessage = '';
|
||||
this.dataError = 'Cannot change password before account data is loaded.';
|
||||
return;
|
||||
}
|
||||
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_SetNewPassword>('setNewPassword');
|
||||
const response = await request.fire({
|
||||
email,
|
||||
oldPassword: eventArg.detail.currentPassword,
|
||||
newPassword: eventArg.detail.newPassword,
|
||||
});
|
||||
if (response.status !== 'ok') {
|
||||
throw new Error('Password change failed.');
|
||||
}
|
||||
this.credentialMessage = 'Password changed successfully.';
|
||||
});
|
||||
}
|
||||
|
||||
private async handlePassportEnroll(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminPassportEnrollmentEventDetail>) {
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_CreatePassportEnrollmentChallenge>('createPassportEnrollmentChallenge');
|
||||
const response = await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
deviceLabel: eventArg.detail.deviceLabel,
|
||||
platform: 'web',
|
||||
capabilities: {
|
||||
gps: false,
|
||||
nfc: false,
|
||||
push: false,
|
||||
},
|
||||
});
|
||||
this.passportEnrollment = response;
|
||||
this.credentialMessage = 'Passport enrollment challenge created.';
|
||||
});
|
||||
}
|
||||
|
||||
private async handlePassportRevoke(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminPassportDeviceEventDetail>) {
|
||||
const device = this.passportDevices.find((deviceArg) => deviceArg.id === eventArg.detail.deviceId);
|
||||
if (!device || !confirm(`Revoke passport device ${device.label}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RevokePassportDevice>('revokePassportDevice');
|
||||
await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
deviceId: eventArg.detail.deviceId,
|
||||
});
|
||||
this.credentialMessage = 'Passport device revoked.';
|
||||
});
|
||||
}
|
||||
|
||||
private async handleMemberInvite() {
|
||||
const selectedOrg = this.getSelectedOrganization();
|
||||
if (!selectedOrg) {
|
||||
this.dataError = 'Select an organisation before inviting members.';
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await BulkInviteModal.show({
|
||||
organizationId: selectedOrg.id,
|
||||
organizationName: selectedOrg.data.name,
|
||||
});
|
||||
if (result?.invitedCount) {
|
||||
await this.loadAdminShellData();
|
||||
}
|
||||
}
|
||||
|
||||
private async handleMemberRemove(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminMemberEventDetail>) {
|
||||
const selectedOrg = this.getSelectedOrganization();
|
||||
const member = this.orgMembers.find((memberArg) => memberArg.userId === eventArg.detail.userId);
|
||||
if (!selectedOrg || !member || !confirm(`Remove ${member.name} from ${selectedOrg.data.name}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RemoveMember>('removeMember');
|
||||
await request.fire({ jwt: await idpState.idpClient.getJwt(), organizationId: selectedOrg.id, userId: member.userId });
|
||||
});
|
||||
}
|
||||
|
||||
private async handleMemberRolesUpdate(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminMemberRolesEventDetail>) {
|
||||
const selectedOrg = this.getSelectedOrganization();
|
||||
if (!selectedOrg) {
|
||||
this.dataError = 'Select an organisation before editing member roles.';
|
||||
return;
|
||||
}
|
||||
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_UpdateMemberRoles>('updateMemberRoles');
|
||||
const response = await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
organizationId: selectedOrg.id,
|
||||
userId: eventArg.detail.userId,
|
||||
roles: eventArg.detail.roles,
|
||||
});
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Member role update failed.');
|
||||
}
|
||||
await states.accountState.dispatchAction(states.getOrganizationsAction, null);
|
||||
this.applyAccountState();
|
||||
});
|
||||
}
|
||||
|
||||
private async handleOrgRoleUpsert(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminOrgRoleUpsertEventDetail>) {
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_UpsertOrgRoleDefinition>('upsertOrgRoleDefinition');
|
||||
const response = await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
organizationId: eventArg.detail.organizationId,
|
||||
roleDefinition: eventArg.detail.roleDefinition,
|
||||
});
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Organization role update failed.');
|
||||
}
|
||||
await states.accountState.dispatchAction(states.getOrganizationsAction, null);
|
||||
this.applyAccountState();
|
||||
});
|
||||
}
|
||||
|
||||
private async handleOrgRoleDelete(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminOrgRoleDeleteEventDetail>) {
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_DeleteOrgRoleDefinition>('deleteOrgRoleDefinition');
|
||||
const response = await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
organizationId: eventArg.detail.organizationId,
|
||||
roleKey: eventArg.detail.roleKey,
|
||||
confirmationText: eventArg.detail.confirmationText,
|
||||
});
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Organization role delete failed.');
|
||||
}
|
||||
await states.accountState.dispatchAction(states.getOrganizationsAction, null);
|
||||
this.applyAccountState();
|
||||
});
|
||||
}
|
||||
|
||||
private async handleAppRoleMappingsUpdate(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminAppRoleMappingsEventDetail>) {
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_UpdateAppRoleMappings>('updateAppRoleMappings');
|
||||
const response = await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
organizationId: eventArg.detail.organizationId,
|
||||
appId: eventArg.detail.appId,
|
||||
roleMappings: eventArg.detail.roleMappings,
|
||||
});
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'App role mapping update failed.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async handleInvitationResend(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminInvitationEventDetail>) {
|
||||
const selectedOrg = this.getSelectedOrganization();
|
||||
if (!selectedOrg) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_ResendInvitation>('resendInvitation');
|
||||
await request.fire({ jwt: await idpState.idpClient.getJwt(), organizationId: selectedOrg.id, invitationId: eventArg.detail.invitationId });
|
||||
});
|
||||
}
|
||||
|
||||
private async handleInvitationCancel(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminInvitationEventDetail>) {
|
||||
const selectedOrg = this.getSelectedOrganization();
|
||||
if (!selectedOrg || !confirm('Cancel this invitation?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_CancelInvitation>('cancelInvitation');
|
||||
await request.fire({ jwt: await idpState.idpClient.getJwt(), organizationId: selectedOrg.id, invitationId: eventArg.detail.invitationId });
|
||||
});
|
||||
}
|
||||
|
||||
public async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>): Promise<void> {
|
||||
super.firstUpdated(_changedProperties);
|
||||
await this.domtoolsPromise;
|
||||
this.subrouter = this.domtools.router.createSubRouter('/account');
|
||||
const viewcontainer: HTMLDivElement = this.shadowRoot.querySelector('.viewcontainer');
|
||||
|
||||
// Setup event listeners for modals
|
||||
this.addEventListener('open-org-select-modal', (async (e: CustomEvent) => {
|
||||
const result = await OrgSelectModal.show({
|
||||
targetPath: e.detail.targetPath,
|
||||
title: e.detail.title,
|
||||
description: e.detail.description,
|
||||
});
|
||||
if (result) {
|
||||
this.subrouter.pushUrl(result.path);
|
||||
}
|
||||
}) as EventListener);
|
||||
|
||||
this.addEventListener('open-create-org-modal', async () => {
|
||||
const org = await CreateOrgModal.show();
|
||||
if (org) {
|
||||
this.subrouter.pushUrl(`/org/${org.data.slug}/billing`);
|
||||
}
|
||||
});
|
||||
|
||||
const cleanupViews = async () => {
|
||||
for (const child of Array.from(viewcontainer.children)) {
|
||||
viewcontainer.removeChild(child);
|
||||
}
|
||||
};
|
||||
|
||||
viewcontainer.append(new views.BaseView());
|
||||
console.log(`loaded base view`);
|
||||
this.subrouter = this.domtools.router.createSubRouter('/dash');
|
||||
await states.accountState.dispatchAction(states.getOrganizationsAction, null);
|
||||
this.applyAccountState();
|
||||
|
||||
this.subrouter.on('', async () => {
|
||||
viewcontainer.classList.add('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
console.log('We are viewing the account overview');
|
||||
await cleanupViews();
|
||||
viewcontainer.append(new views.BaseView());
|
||||
viewcontainer.classList.remove('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
this.pushDashPath('/overview');
|
||||
});
|
||||
|
||||
this.subrouter.on('/org/:orgName/billing', async () => {
|
||||
viewcontainer.classList.add('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
console.log('We are viewing the billing page');
|
||||
await cleanupViews();
|
||||
viewcontainer.append(new views.SubscriptionView());
|
||||
viewcontainer.classList.remove('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
this.subrouter.on('/overview', async () => {
|
||||
this.setAdminPage('overview');
|
||||
});
|
||||
|
||||
this.subrouter.on('/org/:orgName/paddlesetup', async () => {
|
||||
viewcontainer.classList.add('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
console.log('We are viewing the paddle setup page');
|
||||
await cleanupViews();
|
||||
viewcontainer.append(new views.PaddleSetupView());
|
||||
viewcontainer.classList.remove('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
this.subrouter.on('/account/profile', async () => {
|
||||
this.setAdminPage('profile');
|
||||
});
|
||||
|
||||
this.subrouter.on('/account/security', async () => {
|
||||
this.setAdminPage('security');
|
||||
});
|
||||
|
||||
this.subrouter.on('/account/sessions', async () => {
|
||||
this.setAdminPage('sessions');
|
||||
});
|
||||
|
||||
this.subrouter.on('/account/apps', async () => {
|
||||
this.setAdminPage('apps');
|
||||
});
|
||||
|
||||
this.subrouter.on('/support', async () => {
|
||||
this.setAdminPage('support');
|
||||
});
|
||||
|
||||
this.subrouter.on('/org/:orgName', async () => {
|
||||
viewcontainer.classList.add('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
console.log('We are viewing the org overview page');
|
||||
await cleanupViews();
|
||||
viewcontainer.append(new views.OrgView());
|
||||
viewcontainer.classList.remove('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
await this.setOrgPage('org-general');
|
||||
});
|
||||
|
||||
this.subrouter.on('/org/:orgName/settings', async () => {
|
||||
await this.setOrgPage('org-settings');
|
||||
});
|
||||
|
||||
this.subrouter.on('/org/:orgName/apps', async () => {
|
||||
viewcontainer.classList.add('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
console.log('We are viewing the apps page');
|
||||
await cleanupViews();
|
||||
viewcontainer.append(new views.AppsView());
|
||||
viewcontainer.classList.remove('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
await this.setOrgPage('org-apps');
|
||||
});
|
||||
|
||||
this.subrouter.on('/org/:orgName/users', async () => {
|
||||
viewcontainer.classList.add('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
console.log('We are viewing the users page');
|
||||
await cleanupViews();
|
||||
viewcontainer.append(new views.UsersView());
|
||||
viewcontainer.classList.remove('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
await this.setOrgPage('org-members');
|
||||
});
|
||||
|
||||
this.subrouter.on('/admin', async () => {
|
||||
viewcontainer.classList.add('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
console.log('We are viewing the admin page');
|
||||
await cleanupViews();
|
||||
viewcontainer.append(new views.AdminView());
|
||||
viewcontainer.classList.remove('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
this.pushDashPath('/admin/apps');
|
||||
});
|
||||
|
||||
this.subrouter.on('/admin/users', async () => {
|
||||
this.setAdminPage('ga-users');
|
||||
});
|
||||
|
||||
this.subrouter.on('/admin/orgs', async () => {
|
||||
this.setAdminPage('ga-orgs');
|
||||
});
|
||||
|
||||
this.subrouter.on('/admin/apps', async () => {
|
||||
this.setAdminPage('ga-apps');
|
||||
});
|
||||
|
||||
this.subrouter._handleRouteState();
|
||||
|
||||
states.accountState.select((stateArg) => stateArg.user).subscribe(() => this.applyAccountState());
|
||||
states.accountState.select((stateArg) => stateArg.organizations).subscribe(() => this.applyAccountState());
|
||||
states.accountState.select((stateArg) => stateArg.roles).subscribe(() => this.applyAccountState());
|
||||
states.accountState.select((stateArg) => stateArg.selectedOrg).subscribe(() => this.applyAccountState());
|
||||
|
||||
this.registerGarbageFunction(async () => {
|
||||
this.subrouter.destroy();
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user