feat(reception): Add activity logging, session metadata and org-selection UI (backend and frontend)
This commit is contained in:
@@ -12,6 +12,8 @@ import {
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { LeleAccountNavigation } from './navigation.js';
|
||||
import { OrgSelectModal } from './org-select-modal.js';
|
||||
import { CreateOrgModal } from './create-org-modal.js';
|
||||
import { accountDesignTokens } from './sharedstyles.js';
|
||||
|
||||
import * as views from './views/index.js';
|
||||
@@ -91,6 +93,8 @@ export class IdpAccountContent extends DeesElement {
|
||||
<!--<lele-accountview-subscription></lele-accountview-subscription>-->
|
||||
</div>
|
||||
</div>
|
||||
<idp-org-select-modal></idp-org-select-modal>
|
||||
<idp-create-org-modal></idp-create-org-modal>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -100,6 +104,34 @@ export class IdpAccountContent extends DeesElement {
|
||||
this.subrouter = this.domtools.router.createSubRouter('/account');
|
||||
const viewcontainer: HTMLDivElement = this.shadowRoot.querySelector('.viewcontainer');
|
||||
|
||||
// Get modal references
|
||||
const orgSelectModal = this.shadowRoot.querySelector('idp-org-select-modal') as OrgSelectModal;
|
||||
const createOrgModal = this.shadowRoot.querySelector('idp-create-org-modal') as CreateOrgModal;
|
||||
|
||||
// Setup event listeners for modals
|
||||
this.addEventListener('open-org-select-modal', ((e: CustomEvent) => {
|
||||
orgSelectModal.show({
|
||||
targetPath: e.detail.targetPath,
|
||||
title: e.detail.title,
|
||||
description: e.detail.description,
|
||||
});
|
||||
}) as EventListener);
|
||||
|
||||
this.addEventListener('open-create-org-modal', () => {
|
||||
createOrgModal.show();
|
||||
});
|
||||
|
||||
// Handle org selection from modal
|
||||
orgSelectModal.addEventListener('org-selected', ((e: CustomEvent) => {
|
||||
this.subrouter.pushUrl(e.detail.path);
|
||||
}) as EventListener);
|
||||
|
||||
// Handle org creation - navigate to billing
|
||||
createOrgModal.addEventListener('org-created', ((e: CustomEvent) => {
|
||||
const org = e.detail.org;
|
||||
this.subrouter.pushUrl(`/org/${org.data.slug}/billing`);
|
||||
}) as EventListener);
|
||||
|
||||
const cleanupViews = async () => {
|
||||
for (const child of Array.from(viewcontainer.children)) {
|
||||
viewcontainer.removeChild(child);
|
||||
@@ -139,6 +171,16 @@ export class IdpAccountContent extends DeesElement {
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
this.subrouter.on('/org/:orgName/apps', async () => {
|
||||
viewcontainer.classList.add('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
cssManager,
|
||||
css,
|
||||
state,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { accountDesignTokens } from './sharedstyles.js';
|
||||
import * as accountStateModule from '../../states/accountstate.js';
|
||||
import { IdpState } from '../../states/idp.state.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'idp-create-org-modal': CreateOrgModal;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('idp-create-org-modal')
|
||||
export class CreateOrgModal extends DeesElement {
|
||||
@state()
|
||||
accessor visible: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor orgName: string = '';
|
||||
|
||||
@state()
|
||||
accessor orgSlug: string = '';
|
||||
|
||||
@state()
|
||||
accessor validating: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor validationResult: { available: boolean; message: string } | null = null;
|
||||
|
||||
@state()
|
||||
accessor creating: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor error: string = '';
|
||||
|
||||
private validationDebounceTimer: any = null;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
accountDesignTokens,
|
||||
css`
|
||||
:host {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host([visible]) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.15s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #18181b;
|
||||
border: 1px solid #27272a;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
animation: slideIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #27272a;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 4px 0;
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.modal-description {
|
||||
font-size: 14px;
|
||||
color: #71717a;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #a1a1aa;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #27272a;
|
||||
background: #0a0a0a;
|
||||
color: #fafafa;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.form-input:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.slug-preview {
|
||||
margin-top: 12px;
|
||||
padding: 12px 16px;
|
||||
background: #0a0a0a;
|
||||
border: 1px solid #27272a;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.slug-label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #71717a;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.slug-value {
|
||||
font-family: 'Geist Mono', monospace;
|
||||
font-size: 14px;
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.validation-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.validation-status.validating {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.validation-status.available {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.validation-status.unavailable {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.validation-status dees-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 16px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
border-radius: 8px;
|
||||
color: #ef4444;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #27272a;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (!this.visible) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const canCreate = this.orgName.length > 0 &&
|
||||
this.validationResult?.available &&
|
||||
!this.validating &&
|
||||
!this.creating;
|
||||
|
||||
return html`
|
||||
<div class="overlay" @click=${this.handleOverlayClick}>
|
||||
<div class="modal" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Create Organization</h2>
|
||||
<p class="modal-description">Create a new organization to manage apps, users, and billing.</p>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Organization Name</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="e.g., Acme Inc."
|
||||
.value=${this.orgName}
|
||||
@input=${this.handleNameInput}
|
||||
?disabled=${this.creating}
|
||||
/>
|
||||
</div>
|
||||
|
||||
${this.orgSlug ? html`
|
||||
<div class="slug-preview">
|
||||
<div class="slug-label">Organization URL Slug</div>
|
||||
<div class="slug-value">${this.orgSlug}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.renderValidationStatus()}
|
||||
|
||||
${this.error ? html`
|
||||
<div class="error-message">${this.error}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<dees-button type="secondary" @clicked=${this.handleCancel} ?disabled=${this.creating}>
|
||||
Cancel
|
||||
</dees-button>
|
||||
<dees-button @clicked=${this.handleCreate} ?disabled=${!canCreate} .status=${this.creating ? 'pending' : 'normal'}>
|
||||
${this.creating ? 'Creating...' : 'Create Organization'}
|
||||
</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderValidationStatus(): TemplateResult | null {
|
||||
if (!this.orgSlug) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.validating) {
|
||||
return html`
|
||||
<div class="validation-status validating">
|
||||
<dees-icon .icon=${'lucide:loader-2'}></dees-icon>
|
||||
Checking availability...
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.validationResult) {
|
||||
if (this.validationResult.available) {
|
||||
return html`
|
||||
<div class="validation-status available">
|
||||
<dees-icon .icon=${'lucide:check-circle'}></dees-icon>
|
||||
${this.validationResult.message}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return html`
|
||||
<div class="validation-status unavailable">
|
||||
<dees-icon .icon=${'lucide:x-circle'}></dees-icon>
|
||||
${this.validationResult.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public show() {
|
||||
this.orgName = '';
|
||||
this.orgSlug = '';
|
||||
this.validating = false;
|
||||
this.validationResult = null;
|
||||
this.creating = false;
|
||||
this.error = '';
|
||||
this.visible = true;
|
||||
this.setAttribute('visible', '');
|
||||
}
|
||||
|
||||
public hide() {
|
||||
this.visible = false;
|
||||
this.removeAttribute('visible');
|
||||
if (this.validationDebounceTimer) {
|
||||
clearTimeout(this.validationDebounceTimer);
|
||||
}
|
||||
}
|
||||
|
||||
private handleOverlayClick(e: Event) {
|
||||
if ((e.target as HTMLElement).classList.contains('overlay') && !this.creating) {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private handleCancel() {
|
||||
if (!this.creating) {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private handleNameInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
this.orgName = input.value;
|
||||
this.orgSlug = this.generateSlug(this.orgName);
|
||||
this.error = '';
|
||||
|
||||
// Debounce validation
|
||||
if (this.validationDebounceTimer) {
|
||||
clearTimeout(this.validationDebounceTimer);
|
||||
}
|
||||
|
||||
if (this.orgSlug) {
|
||||
this.validating = true;
|
||||
this.validationResult = null;
|
||||
this.validationDebounceTimer = setTimeout(() => {
|
||||
this.validateSlug();
|
||||
}, 500);
|
||||
} else {
|
||||
this.validating = false;
|
||||
this.validationResult = null;
|
||||
}
|
||||
}
|
||||
|
||||
private generateSlug(name: string): string {
|
||||
return name
|
||||
.replace(/[^a-zA-Z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
private async validateSlug() {
|
||||
if (!this.orgSlug) {
|
||||
this.validating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const result = await idpState.idpClient.createOrganization(
|
||||
this.orgName,
|
||||
this.orgSlug,
|
||||
'checkAvailability'
|
||||
);
|
||||
|
||||
this.validationResult = {
|
||||
available: result.nameAvailable,
|
||||
message: result.nameAvailable
|
||||
? 'This name is available!'
|
||||
: 'This name is already taken. Please choose another.',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Validation error:', error);
|
||||
this.validationResult = {
|
||||
available: false,
|
||||
message: 'Unable to validate. Please try again.',
|
||||
};
|
||||
} finally {
|
||||
this.validating = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleCreate() {
|
||||
if (!this.validationResult?.available || this.creating) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.creating = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const result = await idpState.idpClient.createOrganization(
|
||||
this.orgName,
|
||||
this.orgSlug,
|
||||
'manifest'
|
||||
);
|
||||
|
||||
// Update state with new organization
|
||||
const currentState = accountStateModule.accountState.getState();
|
||||
currentState.organizations.push(result.resultingOrganization);
|
||||
accountStateModule.accountState.dispatchAction(
|
||||
accountStateModule.setSelectedOrg,
|
||||
result.resultingOrganization
|
||||
);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('org-created', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: { org: result.resultingOrganization },
|
||||
}));
|
||||
|
||||
this.hide();
|
||||
} catch (error) {
|
||||
console.error('Error creating organization:', error);
|
||||
this.error = error instanceof Error ? error.message : 'Failed to create organization. Please try again.';
|
||||
} finally {
|
||||
this.creating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from './content.js';
|
||||
export * from './navigation.js';
|
||||
export * from './org-select-modal.js';
|
||||
export * from './create-org-modal.js';
|
||||
|
||||
@@ -183,14 +183,6 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
<dees-icon .icon=${'lucide:shield'}></dees-icon>
|
||||
Manage Roles
|
||||
</div>
|
||||
<div
|
||||
class="navigationOption"
|
||||
@click=${async () => {
|
||||
}}
|
||||
>
|
||||
<dees-icon .icon=${'lucide:plus'}></dees-icon>
|
||||
Create Organization
|
||||
</div>
|
||||
<div
|
||||
class="navigationOption"
|
||||
@click=${async () => {
|
||||
@@ -207,15 +199,58 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
<div class="navigationGroupLabel">Organization</div>
|
||||
<dees-input-dropdown
|
||||
.label=${'Select organization'}
|
||||
@selectedOption=${(eventArg: CustomEvent) => {
|
||||
@selectedOption=${async (eventArg: CustomEvent) => {
|
||||
// Handle "Create new..." option
|
||||
if (eventArg.detail.key === '__create_new__') {
|
||||
this.dispatchEvent(new CustomEvent('open-create-org-modal', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const currentState = states.accountState.getState();
|
||||
states.accountState.dispatchAction(
|
||||
states.setSelectedOrg,
|
||||
currentState.organizations.find((org) => org.data.slug === eventArg.detail.payload)
|
||||
);
|
||||
const newOrg = currentState.organizations.find((org) => org.data.slug === eventArg.detail.payload);
|
||||
states.accountState.dispatchAction(states.setSelectedOrg, newOrg);
|
||||
|
||||
// Auto-navigate to new org's current page type (reactivity)
|
||||
const currentPath = window.location.pathname;
|
||||
const subrouter = await this.getAccountRouter();
|
||||
if (currentPath.includes('/org/') && newOrg) {
|
||||
// Extract the page type (apps, billing, etc.) and navigate to new org
|
||||
const pathParts = currentPath.split('/');
|
||||
const pageType = pathParts[5]; // /account/org/:orgName/:pageType
|
||||
if (pageType) {
|
||||
subrouter.pushUrl(`/org/${newOrg.data.slug}/${pageType}`);
|
||||
} else {
|
||||
subrouter.pushUrl(`/org/${newOrg.data.slug}`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<div
|
||||
class="navigationOption"
|
||||
@click=${async () => {
|
||||
const currentState = states.accountState.getState();
|
||||
if (currentState.selectedOrg) {
|
||||
const subrouter = await this.getAccountRouter();
|
||||
subrouter.pushUrl(`/org/${currentState.selectedOrg.data.slug}`);
|
||||
} else {
|
||||
this.dispatchEvent(new CustomEvent('open-org-select-modal', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: {
|
||||
targetPath: '/org/:orgName',
|
||||
title: 'Select Organization',
|
||||
description: 'Choose an organization to view its overview.',
|
||||
},
|
||||
}));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<dees-icon .icon=${'lucide:home'}></dees-icon>
|
||||
Overview
|
||||
</div>
|
||||
<div
|
||||
class="navigationOption"
|
||||
@click=${async () => {
|
||||
@@ -223,6 +258,16 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
if (currentState.selectedOrg) {
|
||||
const subrouter = await this.getAccountRouter();
|
||||
subrouter.pushUrl(`/org/${currentState.selectedOrg.data.slug}/apps`);
|
||||
} else {
|
||||
this.dispatchEvent(new CustomEvent('open-org-select-modal', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: {
|
||||
targetPath: '/org/:orgName/apps',
|
||||
title: 'Select Organization',
|
||||
description: 'Choose an organization to view its apps.',
|
||||
},
|
||||
}));
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -250,6 +295,16 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
if (currentState.selectedOrg) {
|
||||
const subrouter = await this.getAccountRouter();
|
||||
subrouter.pushUrl(`/org/${currentState.selectedOrg.data.slug}/billing`);
|
||||
} else {
|
||||
this.dispatchEvent(new CustomEvent('open-org-select-modal', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: {
|
||||
targetPath: '/org/:orgName/billing',
|
||||
title: 'Select Organization',
|
||||
description: 'Choose an organization to view its billing.',
|
||||
},
|
||||
}));
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -296,11 +351,21 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
payload: orgArg.data.slug,
|
||||
};
|
||||
};
|
||||
|
||||
// "Create new..." option to add at the end
|
||||
const createNewOption = {
|
||||
option: '+ Create new...',
|
||||
key: '__create_new__',
|
||||
payload: '__create_new__',
|
||||
};
|
||||
|
||||
states.accountState
|
||||
.select((stateArg) => stateArg.organizations)
|
||||
.pipe(
|
||||
plugins.deesDomtools.plugins.smartrx.rxjs.ops.map((orgArrayArg) => {
|
||||
return orgArrayArg.map(orgToMenuEntry);
|
||||
const orgEntries = orgArrayArg.map(orgToMenuEntry);
|
||||
// Add "Create new..." at the end
|
||||
return [...orgEntries, createNewOption];
|
||||
})
|
||||
)
|
||||
.subscribe((menuEntries) => {
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
cssManager,
|
||||
css,
|
||||
state,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { accountDesignTokens } from './sharedstyles.js';
|
||||
import * as accountStateModule from '../../states/accountstate.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'idp-org-select-modal': OrgSelectModal;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('idp-org-select-modal')
|
||||
export class OrgSelectModal extends DeesElement {
|
||||
@state()
|
||||
accessor visible: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor organizations: plugins.idpInterfaces.data.IOrganization[] = [];
|
||||
|
||||
@state()
|
||||
accessor targetPath: string = '';
|
||||
|
||||
@state()
|
||||
accessor title: string = 'Select Organization';
|
||||
|
||||
@state()
|
||||
accessor description: string = 'Choose an organization to continue.';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
accountDesignTokens,
|
||||
css`
|
||||
:host {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host([visible]) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.15s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #18181b;
|
||||
border: 1px solid #27272a;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
animation: slideIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #27272a;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 4px 0;
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.modal-description {
|
||||
font-size: 14px;
|
||||
color: #71717a;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.org-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.org-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 24px;
|
||||
border-bottom: 1px solid #27272a;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.org-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.org-item:hover {
|
||||
background: #27272a;
|
||||
}
|
||||
|
||||
.org-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: #27272a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.org-item:hover .org-icon {
|
||||
background: #3f3f46;
|
||||
}
|
||||
|
||||
.org-icon dees-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.org-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.org-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.org-slug {
|
||||
font-size: 12px;
|
||||
color: #71717a;
|
||||
}
|
||||
|
||||
.org-arrow {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 24px;
|
||||
color: #71717a;
|
||||
}
|
||||
|
||||
.empty-state dees-icon {
|
||||
font-size: 40px;
|
||||
opacity: 0.5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #27272a;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (!this.visible) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="overlay" @click=${this.handleOverlayClick}>
|
||||
<div class="modal" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">${this.title}</h2>
|
||||
<p class="modal-description">${this.description}</p>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
${this.organizations.length === 0
|
||||
? this.renderEmptyState()
|
||||
: this.renderOrgList()}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<dees-button type="secondary" @clicked=${this.handleCancel}>
|
||||
Cancel
|
||||
</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderOrgList(): TemplateResult {
|
||||
return html`
|
||||
<div class="org-list">
|
||||
${this.organizations.map((org) => html`
|
||||
<div class="org-item" @click=${() => this.handleSelectOrg(org)}>
|
||||
<div class="org-icon">
|
||||
<dees-icon .icon=${'lucide:building2'}></dees-icon>
|
||||
</div>
|
||||
<div class="org-info">
|
||||
<div class="org-name">${org.data.name}</div>
|
||||
<div class="org-slug">${org.data.slug}</div>
|
||||
</div>
|
||||
<dees-icon class="org-arrow" .icon=${'lucide:chevron-right'}></dees-icon>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderEmptyState(): TemplateResult {
|
||||
return html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .icon=${'lucide:building2'}></dees-icon>
|
||||
<p>You don't have any organizations yet.</p>
|
||||
<dees-button @clicked=${this.handleCreateOrg}>
|
||||
<dees-icon .icon=${'lucide:plus'} slot="iconLeft"></dees-icon>
|
||||
Create Organization
|
||||
</dees-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public show(options: {
|
||||
targetPath: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}) {
|
||||
this.targetPath = options.targetPath;
|
||||
this.title = options.title || 'Select Organization';
|
||||
this.description = options.description || 'Choose an organization to continue.';
|
||||
|
||||
// Load organizations from state
|
||||
const state = accountStateModule.accountState.getState();
|
||||
this.organizations = state.organizations;
|
||||
|
||||
this.visible = true;
|
||||
this.setAttribute('visible', '');
|
||||
}
|
||||
|
||||
public hide() {
|
||||
this.visible = false;
|
||||
this.removeAttribute('visible');
|
||||
}
|
||||
|
||||
private handleOverlayClick(e: Event) {
|
||||
if ((e.target as HTMLElement).classList.contains('overlay')) {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private handleCancel() {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
private handleSelectOrg(org: plugins.idpInterfaces.data.IOrganization) {
|
||||
accountStateModule.accountState.dispatchAction(accountStateModule.setSelectedOrg, org);
|
||||
|
||||
// Replace :orgName placeholder with actual slug
|
||||
const path = this.targetPath.replace(':orgName', org.data.slug);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('org-selected', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: { org, path },
|
||||
}));
|
||||
|
||||
this.hide();
|
||||
}
|
||||
|
||||
private handleCreateOrg() {
|
||||
this.hide();
|
||||
this.dispatchEvent(new CustomEvent('open-create-org-modal', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,14 @@ import {
|
||||
property,
|
||||
html,
|
||||
cssManager,
|
||||
unsafeCSS,
|
||||
css,
|
||||
render,
|
||||
directives,
|
||||
state,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import sharedStyles, { accountDesignTokens, cardStyles, typographyStyles } from '../sharedstyles.js';
|
||||
import { accountDesignTokens } from '../sharedstyles.js';
|
||||
import * as accountStateModule from '../../../states/accountstate.js';
|
||||
import { IdpState } from '../../../states/idp.state.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -19,219 +20,804 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
import * as state from '../../../states/accountstate.js';
|
||||
interface ISessionDisplay {
|
||||
id: string;
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
browser: string;
|
||||
os: string;
|
||||
ip: string;
|
||||
lastActive: number;
|
||||
createdAt: number;
|
||||
isCurrent: boolean;
|
||||
}
|
||||
|
||||
interface IActivityDisplay {
|
||||
id: string;
|
||||
data: plugins.idpInterfaces.data.IActivityLog['data'];
|
||||
}
|
||||
|
||||
@customElement('lele-accountview-baseview')
|
||||
export class BaseView extends DeesElement {
|
||||
@property({
|
||||
type: Array,
|
||||
})
|
||||
accessor subscriptions: any[] = [
|
||||
{
|
||||
organization: 'org1',
|
||||
'subscription type': 'workspace.global SaaS',
|
||||
price: '4€',
|
||||
userFactor: 4,
|
||||
total: '16.00€',
|
||||
},
|
||||
{
|
||||
organization: 'org1',
|
||||
'subscription type': 'workspace.global IaaS Base Access',
|
||||
price: '0€',
|
||||
userFactor: 4,
|
||||
total: '0€',
|
||||
},
|
||||
{
|
||||
organization: 'org1',
|
||||
'subscription type': 'workspace.global SLA Senior',
|
||||
price: '2000€',
|
||||
userFactor: 'none',
|
||||
total: '2000.00€',
|
||||
},
|
||||
];
|
||||
@state()
|
||||
accessor loading: boolean = true;
|
||||
|
||||
@state()
|
||||
accessor sessions: ISessionDisplay[] = [];
|
||||
|
||||
@state()
|
||||
accessor activities: IActivityDisplay[] = [];
|
||||
|
||||
@state()
|
||||
accessor user: plugins.idpInterfaces.data.IUser | null = null;
|
||||
|
||||
@state()
|
||||
accessor organizations: plugins.idpInterfaces.data.IOrganization[] = [];
|
||||
|
||||
@state()
|
||||
accessor roles: plugins.idpInterfaces.data.IRole[] = [];
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
accountDesignTokens,
|
||||
cardStyles,
|
||||
typographyStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 48px;
|
||||
min-height: 100%;
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.viewHost {
|
||||
max-width: 600px;
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #71717a;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
background: #18181b;
|
||||
border: 1px solid #27272a;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.slug {
|
||||
color: var(--foreground);
|
||||
.card.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #27272a;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
font-family: 'Geist Mono', monospace;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: var(--muted-foreground);
|
||||
margin: 16px 0;
|
||||
padding: 12px 16px;
|
||||
background: var(--muted);
|
||||
border-radius: 8px;
|
||||
.card-title dees-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
dees-form {
|
||||
.card-body {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.card-body.no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Profile Card */
|
||||
.profile-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profile-details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.profile-email {
|
||||
font-size: 14px;
|
||||
color: #71717a;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Organizations */
|
||||
.org-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.orgGrid {
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.org {
|
||||
padding: 20px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
color: var(--foreground);
|
||||
transition: all 0.15s ease;
|
||||
.org-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid #27272a;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.org:hover {
|
||||
background: var(--muted);
|
||||
border-color: var(--muted-foreground);
|
||||
.org-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.org dees-icon {
|
||||
.org-item:hover {
|
||||
background: #27272a;
|
||||
}
|
||||
|
||||
.org-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: #27272a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.org-icon dees-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.org-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.org-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.org-role {
|
||||
font-size: 12px;
|
||||
color: #71717a;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
padding: 4px 10px;
|
||||
border-radius: 9999px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.role-badge.admin {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.role-badge.owner {
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
/* Sessions */
|
||||
.session-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid #27272a;
|
||||
}
|
||||
|
||||
.session-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.session-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: #27272a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.session-icon dees-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.session-icon.current {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.session-icon.current dees-icon {
|
||||
color: #22c55e;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.session-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.session-device {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.current-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 9999px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.session-details {
|
||||
font-size: 12px;
|
||||
color: #71717a;
|
||||
}
|
||||
|
||||
.session-actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.revoke-btn {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border: 1px solid #27272a;
|
||||
background: transparent;
|
||||
color: #fafafa;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.revoke-btn:hover {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: #ef4444;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Activity */
|
||||
.activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid #27272a;
|
||||
}
|
||||
|
||||
.activity-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
background: #27272a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-icon dees-icon {
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.activity-icon.login {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.activity-icon.login dees-icon {
|
||||
color: #22c55e;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.activity-icon.logout {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.activity-icon.logout dees-icon {
|
||||
color: #ef4444;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.activity-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.activity-description {
|
||||
font-size: 14px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
font-size: 12px;
|
||||
color: #71717a;
|
||||
}
|
||||
|
||||
/* Empty states */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 32px 20px;
|
||||
color: #71717a;
|
||||
}
|
||||
|
||||
.empty-state dees-icon {
|
||||
font-size: 32px;
|
||||
opacity: 0.5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
color: #71717a;
|
||||
}
|
||||
|
||||
/* Create org button */
|
||||
.create-org-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border: 1px solid #27272a;
|
||||
background: transparent;
|
||||
color: #fafafa;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.create-org-btn:hover {
|
||||
background: #27272a;
|
||||
}
|
||||
|
||||
.create-org-btn dees-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div class="viewHost">
|
||||
public render(): TemplateResult {
|
||||
if (this.loading) {
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="loading">Loading your account...</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
</div> `;
|
||||
const userInitial = this.user?.data?.username?.charAt(0).toUpperCase() ||
|
||||
this.user?.data?.email?.charAt(0).toUpperCase() || '?';
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Account Overview</h1>
|
||||
<p class="subtitle">Manage your profile, organizations, and security settings</p>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<!-- Profile Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">
|
||||
<dees-icon .icon=${'lucide:user'}></dees-icon>
|
||||
Profile
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="profile-info">
|
||||
<div class="avatar">${userInitial}</div>
|
||||
<div class="profile-details">
|
||||
<div class="profile-name">${this.user?.data?.username || 'Unknown User'}</div>
|
||||
<div class="profile-email">${this.user?.data?.email || 'No email'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Organizations Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">
|
||||
<dees-icon .icon=${'lucide:building2'}></dees-icon>
|
||||
Organizations
|
||||
</span>
|
||||
<button class="create-org-btn" @click=${this.handleCreateOrg}>
|
||||
<dees-icon .icon=${'lucide:plus'}></dees-icon>
|
||||
New
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body no-padding">
|
||||
${this.renderOrganizations()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sessions Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">
|
||||
<dees-icon .icon=${'lucide:monitor-smartphone'}></dees-icon>
|
||||
Active Sessions
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body no-padding">
|
||||
${this.renderSessions()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activity Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">
|
||||
<dees-icon .icon=${'lucide:activity'}></dees-icon>
|
||||
Recent Activity
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body no-padding">
|
||||
${this.renderActivity()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
|
||||
await this.domtoolsPromise;
|
||||
super.firstUpdated(_changedProperties);
|
||||
const viewHost: HTMLDivElement = this.shadowRoot.querySelector('.viewHost');
|
||||
await state.accountState.dispatchAction(state.getOrganizationsAction, null);
|
||||
console.log('got orgs');
|
||||
if (state.accountState.getState().organizations.length === 0) {
|
||||
render(
|
||||
html`
|
||||
<div class="card">
|
||||
<h1>Setup Your Account</h1>
|
||||
<p>
|
||||
There are no organizations for your account. Please create one now. Alternatively you
|
||||
can ask an admin of an existing organization to invite you.
|
||||
</p>
|
||||
<dees-form>
|
||||
<dees-input-text .label=${'Organization Name'} .key=${'orgName'}></dees-input-text>
|
||||
</dees-form>
|
||||
<p>
|
||||
The organization slug will be:<br />
|
||||
<span class="slug"
|
||||
>${directives.subscribe(
|
||||
state.accountState.select((stateArg) => stateArg.newOrg.chosenSlug)
|
||||
)}</span
|
||||
>
|
||||
</p>
|
||||
<span class="hint"></span>
|
||||
<dees-button .disabled=${true}>Create the Organization</dees-button>
|
||||
</div>
|
||||
`,
|
||||
viewHost
|
||||
);
|
||||
const subscriptions: plugins.deesDomtools.plugins.smartrx.rxjs.Subscription[] = [];
|
||||
const form = this.shadowRoot.querySelector('dees-form');
|
||||
const orgInput = this.shadowRoot.querySelector('dees-input-text');
|
||||
const hint = this.shadowRoot.querySelector('.hint');
|
||||
const button = this.shadowRoot.querySelector('dees-button');
|
||||
const newOrgSubscription = state.accountState
|
||||
.select((stateArg) => stateArg.newOrg)
|
||||
.subscribe((data) => {
|
||||
if (data.chosenSlug) {
|
||||
hint.innerHTML = 'Waiting: Validating...';
|
||||
} else {
|
||||
hint.innerHTML = 'Hint: Enter a valid organization name.';
|
||||
}
|
||||
if (data.validated && data.validationOk) {
|
||||
hint.innerHTML =
|
||||
'Success: Name is available. Please click the button to create the organization.';
|
||||
button.disabled = false;
|
||||
} else if (!data.validated || !data.validationOk) {
|
||||
hint.innerHTML = `Info: Name not available. Please choose another one.`;
|
||||
button.disabled = true;
|
||||
}
|
||||
});
|
||||
subscriptions.push(newOrgSubscription);
|
||||
private renderOrganizations(): TemplateResult {
|
||||
if (this.organizations.length === 0) {
|
||||
return html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .icon=${'lucide:building2'}></dees-icon>
|
||||
<p>You're not a member of any organizations yet.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const formSubscription = form.changeSubject.subscribe(async (dataArg: any) => {
|
||||
await state.accountState.dispatchAction(state.setNewOrgName, dataArg.orgName);
|
||||
});
|
||||
subscriptions.push(formSubscription);
|
||||
button.addEventListener('clicked', async () => {
|
||||
orgInput.disabled = true;
|
||||
button.text = 'creating org...';
|
||||
button.status = 'pending';
|
||||
hint.innerHTML = 'Waiting for creation of the organization...';
|
||||
await state.accountState.dispatchAction(state.manifestNewOrgName, null);
|
||||
hint.innerHTML = `The Organization with name ${
|
||||
state.accountState.getState().organizations[0].data.name
|
||||
} has been created!`;
|
||||
button.text = 'created!';
|
||||
button.status = 'success';
|
||||
const parentElement = (this.getRootNode() as any).host;
|
||||
parentElement.subrouter.pushUrl(
|
||||
`/org/${state.accountState.getState().organizations[0].data.slug}/billing`
|
||||
);
|
||||
});
|
||||
} else {
|
||||
render(
|
||||
html`
|
||||
<h1>Select An Organization</h1>
|
||||
<p>Choose an organization to manage its settings and billing.</p>
|
||||
<div class="orgGrid">
|
||||
${state.accountState.getState().organizations.map((orgArg) => {
|
||||
return html`
|
||||
<div
|
||||
class="org"
|
||||
@click=${() => {
|
||||
state.accountState.dispatchAction(state.setSelectedOrg, orgArg);
|
||||
const parentElement = (this.getRootNode() as any).host;
|
||||
parentElement.subrouter.pushUrl(`/org/${orgArg.data.slug}/billing`);
|
||||
}}
|
||||
>
|
||||
<dees-icon .icon=${'lucide:building2'} style="display: inline-block; transform: translateY(3px); padding-right: 8px;"></dees-icon> ${orgArg.data.name}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
return html`
|
||||
<div class="org-list">
|
||||
${this.organizations.map((org) => {
|
||||
const roleObj = this.roles.find(r => r.data.organizationId === org.id);
|
||||
const roleName = roleObj?.data.role || 'member';
|
||||
const roleClass = roleName === 'owner' ? 'owner' :
|
||||
roleName === 'admin' ? 'admin' : '';
|
||||
const roleDisplay = roleName.charAt(0).toUpperCase() + roleName.slice(1);
|
||||
return html`
|
||||
<div class="org-item" @click=${() => this.handleSelectOrg(org)}>
|
||||
<div class="org-icon">
|
||||
<dees-icon .icon=${'lucide:building2'}></dees-icon>
|
||||
</div>
|
||||
<div class="org-info">
|
||||
<div class="org-name">${org.data.name}</div>
|
||||
<div class="org-role">${org.data.slug}</div>
|
||||
</div>
|
||||
<span class="role-badge ${roleClass}">${roleDisplay}</span>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderSessions(): TemplateResult {
|
||||
if (this.sessions.length === 0) {
|
||||
return html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .icon=${'lucide:monitor'}></dees-icon>
|
||||
<p>No active sessions found.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="session-list">
|
||||
${this.sessions.map((session) => html`
|
||||
<div class="session-item">
|
||||
<div class="session-icon ${session.isCurrent ? 'current' : ''}">
|
||||
<dees-icon .icon=${this.getDeviceIcon(session.os)}></dees-icon>
|
||||
</div>
|
||||
<div class="session-info">
|
||||
<div class="session-device">
|
||||
${session.deviceName || 'Unknown Device'}
|
||||
${session.isCurrent ? html`<span class="current-badge">Current</span>` : ''}
|
||||
</div>
|
||||
<div class="session-details">
|
||||
${session.browser} · ${session.os} · Last active ${this.formatTimeAgo(session.lastActive)}
|
||||
</div>
|
||||
</div>
|
||||
${!session.isCurrent ? html`
|
||||
<div class="session-actions">
|
||||
<button class="revoke-btn" @click=${() => this.handleRevokeSession(session.id)}>
|
||||
Revoke
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`,
|
||||
viewHost
|
||||
);
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderActivity(): TemplateResult {
|
||||
if (this.activities.length === 0) {
|
||||
return html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .icon=${'lucide:activity'}></dees-icon>
|
||||
<p>No recent activity.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="activity-list">
|
||||
${this.activities.slice(0, 5).map((activity) => html`
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon ${this.getActivityIconClass(activity.data.action)}">
|
||||
<dees-icon .icon=${this.getActivityIcon(activity.data.action)}></dees-icon>
|
||||
</div>
|
||||
<div class="activity-info">
|
||||
<div class="activity-description">${activity.data.metadata.description}</div>
|
||||
<div class="activity-time">${this.formatTimeAgo(activity.data.timestamp)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getDeviceIcon(os: string): string {
|
||||
const osLower = os?.toLowerCase() || '';
|
||||
if (osLower.includes('mac') || osLower.includes('ios')) {
|
||||
return 'lucide:laptop';
|
||||
} else if (osLower.includes('android')) {
|
||||
return 'lucide:smartphone';
|
||||
} else if (osLower.includes('windows')) {
|
||||
return 'lucide:monitor';
|
||||
} else if (osLower.includes('linux')) {
|
||||
return 'lucide:terminal';
|
||||
}
|
||||
return 'lucide:monitor';
|
||||
}
|
||||
|
||||
private getActivityIcon(action: string): string {
|
||||
switch (action) {
|
||||
case 'login':
|
||||
return 'lucide:log-in';
|
||||
case 'logout':
|
||||
return 'lucide:log-out';
|
||||
case 'session_created':
|
||||
return 'lucide:key';
|
||||
case 'session_revoked':
|
||||
return 'lucide:shield-off';
|
||||
case 'org_created':
|
||||
return 'lucide:building-2';
|
||||
case 'org_joined':
|
||||
return 'lucide:user-plus';
|
||||
case 'org_left':
|
||||
return 'lucide:user-minus';
|
||||
case 'role_changed':
|
||||
return 'lucide:shield';
|
||||
case 'profile_updated':
|
||||
return 'lucide:user-cog';
|
||||
case 'app_connected':
|
||||
return 'lucide:plug';
|
||||
case 'app_disconnected':
|
||||
return 'lucide:unplug';
|
||||
default:
|
||||
return 'lucide:activity';
|
||||
}
|
||||
}
|
||||
|
||||
private getActivityIconClass(action: string): string {
|
||||
if (action === 'login' || action === 'session_created' || action === 'org_joined' || action === 'app_connected') {
|
||||
return 'login';
|
||||
}
|
||||
if (action === 'logout' || action === 'session_revoked' || action === 'org_left' || action === 'app_disconnected') {
|
||||
return 'logout';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private formatTimeAgo(timestamp: number): string {
|
||||
const now = Date.now();
|
||||
const diff = now - timestamp;
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const days = Math.floor(diff / 86400000);
|
||||
|
||||
if (minutes < 1) return 'Just now';
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
if (days < 7) return `${days}d ago`;
|
||||
return new Date(timestamp).toLocaleDateString();
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
await this.loadDashboardData();
|
||||
}
|
||||
|
||||
private async loadDashboardData() {
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
|
||||
// Load organizations and roles from account state
|
||||
await accountStateModule.accountState.dispatchAction(accountStateModule.getOrganizationsAction, null);
|
||||
const state = accountStateModule.accountState.getState();
|
||||
this.organizations = state.organizations;
|
||||
this.roles = state.roles;
|
||||
this.user = state.user;
|
||||
|
||||
// Load sessions
|
||||
await this.loadSessions();
|
||||
|
||||
// Load activity
|
||||
await this.loadActivity();
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard data:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadSessions() {
|
||||
try {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const jwt = await idpState.idpClient.getJwt();
|
||||
|
||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_GetUserSessions>(
|
||||
'/typedrequest',
|
||||
'getUserSessions'
|
||||
);
|
||||
|
||||
const response = await typedRequest.fire({ jwt });
|
||||
this.sessions = response?.sessions ?? [];
|
||||
} catch (error) {
|
||||
console.error('Error loading sessions:', error);
|
||||
this.sessions = [];
|
||||
}
|
||||
}
|
||||
|
||||
private async loadActivity() {
|
||||
try {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const jwt = await idpState.idpClient.getJwt();
|
||||
|
||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_GetUserActivity>(
|
||||
'/typedrequest',
|
||||
'getUserActivity'
|
||||
);
|
||||
|
||||
const response = await typedRequest.fire({ jwt, limit: 10 });
|
||||
this.activities = response?.activities ?? [];
|
||||
} catch (error) {
|
||||
console.error('Error loading activity:', error);
|
||||
this.activities = [];
|
||||
}
|
||||
}
|
||||
|
||||
private async handleRevokeSession(sessionId: string) {
|
||||
if (!confirm('Are you sure you want to revoke this session? The device will be logged out.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const jwt = await idpState.idpClient.getJwt();
|
||||
|
||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_RevokeSession>(
|
||||
'/typedrequest',
|
||||
'revokeSession'
|
||||
);
|
||||
|
||||
await typedRequest.fire({ jwt, sessionId });
|
||||
await this.loadSessions();
|
||||
} catch (error) {
|
||||
console.error('Error revoking session:', error);
|
||||
alert('Failed to revoke session');
|
||||
}
|
||||
}
|
||||
|
||||
private handleSelectOrg(org: plugins.idpInterfaces.data.IOrganization) {
|
||||
accountStateModule.accountState.dispatchAction(accountStateModule.setSelectedOrg, org);
|
||||
const parentElement = (this.getRootNode() as any).host;
|
||||
parentElement.subrouter.pushUrl(`/org/${org.data.slug}/billing`);
|
||||
}
|
||||
|
||||
private handleCreateOrg() {
|
||||
// Dispatch event to open create org modal
|
||||
this.dispatchEvent(new CustomEvent('open-create-org-modal', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,5 +2,6 @@ export * from './adminview.js';
|
||||
export * from './appsview.js';
|
||||
export * from './baseview.js';
|
||||
export * from './orgsetup.js';
|
||||
export * from './orgview.js';
|
||||
export * from './paddlesetup.js';
|
||||
export * from './subscriptions.js';
|
||||
|
||||
@@ -0,0 +1,514 @@
|
||||
import * as plugins from '../../../plugins.js';
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
cssManager,
|
||||
css,
|
||||
state,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { accountDesignTokens } from '../sharedstyles.js';
|
||||
import * as accountStateModule from '../../../states/accountstate.js';
|
||||
import { IdpState } from '../../../states/idp.state.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'lele-accountview-orgview': OrgView;
|
||||
}
|
||||
}
|
||||
|
||||
interface IOrgStats {
|
||||
memberCount: number;
|
||||
appCount: number;
|
||||
}
|
||||
|
||||
@customElement('lele-accountview-orgview')
|
||||
export class OrgView extends DeesElement {
|
||||
@state()
|
||||
accessor loading: boolean = true;
|
||||
|
||||
@state()
|
||||
accessor organization: plugins.idpInterfaces.data.IOrganization | null = null;
|
||||
|
||||
@state()
|
||||
accessor userRole: plugins.idpInterfaces.data.IRole | null = null;
|
||||
|
||||
@state()
|
||||
accessor stats: IOrgStats = { memberCount: 0, appCount: 0 };
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
accountDesignTokens,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
min-height: 100%;
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
letter-spacing: -0.02em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
h1 dees-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #71717a;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #18181b;
|
||||
border: 1px solid #27272a;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #71717a;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #18181b;
|
||||
border: 1px solid #27272a;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #27272a;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-title dees-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.card-body.no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Info rows */
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #27272a;
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 13px;
|
||||
color: #71717a;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-value.slug {
|
||||
font-family: 'Geist Mono', monospace;
|
||||
background: #27272a;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Role badge */
|
||||
.role-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 9999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.role-badge.admin {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.role-badge.owner {
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
/* Quick actions */
|
||||
.action-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid #27272a;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.action-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.action-item:hover {
|
||||
background: #27272a;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: #27272a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-item:hover .action-icon {
|
||||
background: #3f3f46;
|
||||
}
|
||||
|
||||
.action-icon dees-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.action-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.action-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.action-description {
|
||||
font-size: 12px;
|
||||
color: #71717a;
|
||||
}
|
||||
|
||||
.action-arrow {
|
||||
color: #71717a;
|
||||
}
|
||||
|
||||
/* Billing status */
|
||||
.billing-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.billing-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #71717a;
|
||||
}
|
||||
|
||||
.billing-indicator.active {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.billing-indicator.none {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
color: #71717a;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (this.loading) {
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="loading">Loading organization...</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!this.organization) {
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="loading">Organization not found</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const roleName = this.userRole?.data.role || 'member';
|
||||
const roleClass = roleName === 'owner' ? 'owner' : roleName === 'admin' ? 'admin' : '';
|
||||
const roleDisplay = roleName.charAt(0).toUpperCase() + roleName.slice(1);
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>
|
||||
<dees-icon .icon=${'lucide:building2'}></dees-icon>
|
||||
${this.organization.data.name}
|
||||
</h1>
|
||||
<p class="subtitle">Organization dashboard and settings</p>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">${this.stats.memberCount}</div>
|
||||
<div class="stat-label">Members</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">${this.stats.appCount}</div>
|
||||
<div class="stat-label">Connected Apps</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">
|
||||
<span class="role-badge ${roleClass}">${roleDisplay}</span>
|
||||
</div>
|
||||
<div class="stat-label">Your Role</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<!-- Organization Info -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">
|
||||
<dees-icon .icon=${'lucide:info'}></dees-icon>
|
||||
Organization Info
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="info-row">
|
||||
<span class="info-label">Name</span>
|
||||
<span class="info-value">${this.organization.data.name}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Slug</span>
|
||||
<span class="info-value slug">${this.organization.data.slug}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Billing</span>
|
||||
<span class="info-value">
|
||||
<div class="billing-status">
|
||||
<span class="billing-indicator ${this.organization.data.billingPlanId ? 'active' : 'none'}"></span>
|
||||
${this.organization.data.billingPlanId ? 'Active' : 'Not configured'}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">
|
||||
<dees-icon .icon=${'lucide:zap'}></dees-icon>
|
||||
Quick Actions
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body no-padding">
|
||||
<div class="action-list">
|
||||
<div class="action-item" @click=${this.navigateToApps}>
|
||||
<div class="action-icon">
|
||||
<dees-icon .icon=${'lucide:box'}></dees-icon>
|
||||
</div>
|
||||
<div class="action-info">
|
||||
<div class="action-name">Manage Apps</div>
|
||||
<div class="action-description">Connect and configure applications</div>
|
||||
</div>
|
||||
<dees-icon class="action-arrow" .icon=${'lucide:chevron-right'}></dees-icon>
|
||||
</div>
|
||||
<div class="action-item" @click=${this.navigateToBilling}>
|
||||
<div class="action-icon">
|
||||
<dees-icon .icon=${'lucide:wallet'}></dees-icon>
|
||||
</div>
|
||||
<div class="action-info">
|
||||
<div class="action-name">View Billing</div>
|
||||
<div class="action-description">Manage subscription and invoices</div>
|
||||
</div>
|
||||
<dees-icon class="action-arrow" .icon=${'lucide:chevron-right'}></dees-icon>
|
||||
</div>
|
||||
<div class="action-item" @click=${this.handleInviteUser}>
|
||||
<div class="action-icon">
|
||||
<dees-icon .icon=${'lucide:user-plus'}></dees-icon>
|
||||
</div>
|
||||
<div class="action-info">
|
||||
<div class="action-name">Invite Member</div>
|
||||
<div class="action-description">Add team members to your organization</div>
|
||||
</div>
|
||||
<dees-icon class="action-arrow" .icon=${'lucide:chevron-right'}></dees-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
await this.loadOrgData();
|
||||
}
|
||||
|
||||
private async loadOrgData() {
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
// Get the organization slug from the URL
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const orgSlug = pathParts[3];
|
||||
|
||||
const currentState = accountStateModule.accountState.getState();
|
||||
const selectedOrg = currentState.organizations.find(org => org.data.slug === orgSlug);
|
||||
|
||||
if (!selectedOrg) {
|
||||
console.error('Organization not found');
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.organization = selectedOrg;
|
||||
|
||||
// Find user's role in this org
|
||||
this.userRole = currentState.roles.find(r => r.data.organizationId === selectedOrg.id) || null;
|
||||
|
||||
// Calculate stats
|
||||
const memberCount = selectedOrg.data.roleIds?.length || 1;
|
||||
|
||||
// Get app connections count
|
||||
let appCount = 0;
|
||||
try {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const jwt = await idpState.idpClient.getJwt();
|
||||
|
||||
const connectionsRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_GetAppConnections>(
|
||||
'/typedrequest',
|
||||
'getAppConnections'
|
||||
);
|
||||
|
||||
const connectionsResponse = await connectionsRequest.fire({
|
||||
jwt,
|
||||
organizationId: selectedOrg.id,
|
||||
});
|
||||
|
||||
appCount = connectionsResponse.connections?.filter(c => c.data.status === 'active').length || 0;
|
||||
} catch (error) {
|
||||
console.error('Error loading app connections:', error);
|
||||
}
|
||||
|
||||
this.stats = { memberCount, appCount };
|
||||
} catch (error) {
|
||||
console.error('Error loading org data:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async navigateToApps() {
|
||||
if (!this.organization) return;
|
||||
const parentElement = (this.getRootNode() as any).host;
|
||||
parentElement.subrouter.pushUrl(`/org/${this.organization.data.slug}/apps`);
|
||||
}
|
||||
|
||||
private async navigateToBilling() {
|
||||
if (!this.organization) return;
|
||||
const parentElement = (this.getRootNode() as any).host;
|
||||
parentElement.subrouter.pushUrl(`/org/${this.organization.data.slug}/billing`);
|
||||
}
|
||||
|
||||
private handleInviteUser() {
|
||||
// TODO: Implement invite user modal
|
||||
alert('Invite member functionality coming soon');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user