451 lines
11 KiB
TypeScript
451 lines
11 KiB
TypeScript
import * as plugins from '../../../plugins.js';
|
|
import {
|
|
customElement,
|
|
DeesElement,
|
|
property,
|
|
html,
|
|
cssManager,
|
|
css,
|
|
state,
|
|
} from '@design.estate/dees-element';
|
|
|
|
import sharedStyles, { accountDesignTokens, cardStyles, typographyStyles } from '../sharedstyles.js';
|
|
import * as accountState from '../../../states/accountstate.js';
|
|
import { IdpState } from '../../../states/idp.state.js';
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'lele-accountview-apps': AppsView;
|
|
}
|
|
}
|
|
|
|
interface IAppDisplay {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
logoUrl: string;
|
|
appUrl: string;
|
|
category: string;
|
|
isConnected: boolean;
|
|
}
|
|
|
|
@customElement('lele-accountview-apps')
|
|
export class AppsView extends DeesElement {
|
|
@state()
|
|
accessor globalApps: IAppDisplay[] = [];
|
|
|
|
@state()
|
|
accessor loading: boolean = true;
|
|
|
|
@state()
|
|
accessor activeTab: 'global' | 'store' | 'custom' = 'global';
|
|
|
|
@state()
|
|
accessor organizationId: string = '';
|
|
|
|
public static styles = [
|
|
cssManager.defaultStyles,
|
|
accountDesignTokens,
|
|
cardStyles,
|
|
typographyStyles,
|
|
css`
|
|
:host {
|
|
display: block;
|
|
padding: 48px;
|
|
max-width: 1000px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
gap: 4px;
|
|
margin-bottom: 32px;
|
|
border-bottom: 1px solid var(--border);
|
|
padding-bottom: 8px;
|
|
}
|
|
|
|
.tab {
|
|
padding: 10px 20px;
|
|
border-radius: 8px 8px 0 0;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--muted-foreground);
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
border: none;
|
|
background: transparent;
|
|
}
|
|
|
|
.tab:hover {
|
|
color: var(--foreground);
|
|
background: var(--muted);
|
|
}
|
|
|
|
.tab.active {
|
|
color: var(--foreground);
|
|
background: var(--muted);
|
|
}
|
|
|
|
.app-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 24px;
|
|
}
|
|
|
|
.app-card {
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.app-card:hover {
|
|
border-color: var(--muted-foreground);
|
|
}
|
|
|
|
.app-header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 16px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.app-logo {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: 12px;
|
|
background: var(--muted);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.app-logo img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.app-logo dees-icon {
|
|
font-size: 24px;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.app-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.app-name {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--foreground);
|
|
margin: 0 0 4px 0;
|
|
}
|
|
|
|
.app-category {
|
|
font-size: 12px;
|
|
color: var(--muted-foreground);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.app-description {
|
|
font-size: 14px;
|
|
color: var(--muted-foreground);
|
|
line-height: 1.5;
|
|
margin: 0 0 16px 0;
|
|
}
|
|
|
|
.app-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.app-link {
|
|
font-size: 13px;
|
|
color: var(--muted-foreground);
|
|
text-decoration: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
transition: color 0.15s ease;
|
|
}
|
|
|
|
.app-link:hover {
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.app-link dees-icon {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.toggle-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.toggle-label {
|
|
font-size: 13px;
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 48px;
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
.empty-state dees-icon {
|
|
font-size: 48px;
|
|
opacity: 0.5;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.loading {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 48px;
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
.coming-soon {
|
|
text-align: center;
|
|
padding: 48px;
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.coming-soon dees-icon {
|
|
font-size: 48px;
|
|
opacity: 0.5;
|
|
margin-bottom: 16px;
|
|
}
|
|
`,
|
|
];
|
|
|
|
public render() {
|
|
return html`
|
|
<h1>Apps</h1>
|
|
<p>Manage apps connected to your organization. Connect global apps, browse the AppStore, or create custom OAuth clients.</p>
|
|
|
|
<div class="tabs">
|
|
<button
|
|
class="tab ${this.activeTab === 'global' ? 'active' : ''}"
|
|
@click=${() => this.activeTab = 'global'}
|
|
>
|
|
Global Apps
|
|
</button>
|
|
<button
|
|
class="tab ${this.activeTab === 'store' ? 'active' : ''}"
|
|
@click=${() => this.activeTab = 'store'}
|
|
>
|
|
App Store
|
|
</button>
|
|
<button
|
|
class="tab ${this.activeTab === 'custom' ? 'active' : ''}"
|
|
@click=${() => this.activeTab = 'custom'}
|
|
>
|
|
Custom OIDC
|
|
</button>
|
|
</div>
|
|
|
|
${this.renderTabContent()}
|
|
`;
|
|
}
|
|
|
|
private renderTabContent() {
|
|
switch (this.activeTab) {
|
|
case 'global':
|
|
return this.renderGlobalApps();
|
|
case 'store':
|
|
return this.renderAppStore();
|
|
case 'custom':
|
|
return this.renderCustomOidc();
|
|
}
|
|
}
|
|
|
|
private renderGlobalApps() {
|
|
if (this.loading) {
|
|
return html`
|
|
<div class="loading">
|
|
<span>Loading apps...</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (this.globalApps.length === 0) {
|
|
return html`
|
|
<div class="empty-state">
|
|
<dees-icon .icon=${'lucide:box'}></dees-icon>
|
|
<h2>No Global Apps Available</h2>
|
|
<p>There are no global apps configured yet.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return html`
|
|
<div class="app-grid">
|
|
${this.globalApps.map(app => html`
|
|
<div class="app-card">
|
|
<div class="app-header">
|
|
<div class="app-logo">
|
|
${app.logoUrl ? html`<img src="${app.logoUrl}" alt="${app.name}" />` : html`<dees-icon .icon=${'lucide:box'}></dees-icon>`}
|
|
</div>
|
|
<div class="app-info">
|
|
<h3 class="app-name">${app.name}</h3>
|
|
<span class="app-category">${app.category}</span>
|
|
</div>
|
|
</div>
|
|
<p class="app-description">${app.description}</p>
|
|
<div class="app-actions">
|
|
<a class="app-link" href="${app.appUrl}" target="_blank">
|
|
<dees-icon .icon=${'lucide:external-link'}></dees-icon>
|
|
Visit App
|
|
</a>
|
|
<div class="toggle-container">
|
|
<span class="toggle-label">${app.isConnected ? 'Connected' : 'Disconnected'}</span>
|
|
<dees-input-checkbox
|
|
.value=${app.isConnected}
|
|
@change=${(e: CustomEvent) => this.toggleAppConnection(app.id, e.detail)}
|
|
></dees-input-checkbox>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`)}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderAppStore() {
|
|
return html`
|
|
<div class="coming-soon">
|
|
<dees-icon .icon=${'lucide:store'}></dees-icon>
|
|
<h2>App Store</h2>
|
|
<p>Browse and install partner apps from other organizations.</p>
|
|
<p><em>Coming soon in Phase 3</em></p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderCustomOidc() {
|
|
return html`
|
|
<div class="coming-soon">
|
|
<dees-icon .icon=${'lucide:key'}></dees-icon>
|
|
<h2>Custom OIDC Apps</h2>
|
|
<p>Create and manage your own OAuth/OIDC client applications.</p>
|
|
<p><em>Coming soon in Phase 2</em></p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
public async firstUpdated() {
|
|
await this.loadApps();
|
|
}
|
|
|
|
private async loadApps() {
|
|
this.loading = true;
|
|
|
|
try {
|
|
// Get the organization ID from the URL
|
|
const pathParts = window.location.pathname.split('/');
|
|
const orgSlug = pathParts[3];
|
|
|
|
const currentState = accountState.accountState.getState();
|
|
const selectedOrg = currentState.organizations.find(org => org.data.slug === orgSlug);
|
|
|
|
if (!selectedOrg) {
|
|
console.error('Organization not found');
|
|
this.loading = false;
|
|
return;
|
|
}
|
|
|
|
this.organizationId = selectedOrg.id;
|
|
|
|
// Get JWT from IdpState
|
|
const idpState = await IdpState.getSingletonInstance();
|
|
const jwt = await idpState.idpClient.getJwt();
|
|
|
|
// Fetch global apps
|
|
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_GetGlobalApps>(
|
|
'/typedrequest',
|
|
'getGlobalApps'
|
|
);
|
|
|
|
const appsResponse = await typedRequest.fire({
|
|
jwt,
|
|
});
|
|
|
|
// Fetch connections for this organization
|
|
const connectionsRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_GetAppConnections>(
|
|
'/typedrequest',
|
|
'getAppConnections'
|
|
);
|
|
|
|
const connectionsResponse = await connectionsRequest.fire({
|
|
jwt,
|
|
organizationId: this.organizationId,
|
|
});
|
|
|
|
// Map apps with connection status
|
|
const connectionMap = new Map(
|
|
connectionsResponse.connections
|
|
.filter(c => c.data.status === 'active')
|
|
.map(c => [c.data.appId, true])
|
|
);
|
|
|
|
this.globalApps = appsResponse.apps.map(app => ({
|
|
id: app.id,
|
|
name: app.data.name,
|
|
description: app.data.description,
|
|
logoUrl: app.data.logoUrl,
|
|
appUrl: app.data.appUrl,
|
|
category: app.data.category,
|
|
isConnected: connectionMap.has(app.id),
|
|
}));
|
|
|
|
} catch (error) {
|
|
console.error('Error loading apps:', error);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
private async toggleAppConnection(appId: string, isConnected: boolean) {
|
|
try {
|
|
// Get JWT from IdpState
|
|
const idpState = await IdpState.getSingletonInstance();
|
|
const jwt = await idpState.idpClient.getJwt();
|
|
|
|
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_ToggleAppConnection>(
|
|
'/typedrequest',
|
|
'toggleAppConnection'
|
|
);
|
|
|
|
await typedRequest.fire({
|
|
jwt,
|
|
organizationId: this.organizationId,
|
|
appId: appId,
|
|
action: isConnected ? 'connect' : 'disconnect',
|
|
});
|
|
|
|
// Update local state
|
|
this.globalApps = this.globalApps.map(app =>
|
|
app.id === appId ? { ...app, isConnected } : app
|
|
);
|
|
|
|
} catch (error) {
|
|
console.error('Error toggling app connection:', error);
|
|
// Revert the checkbox on error
|
|
await this.loadApps();
|
|
}
|
|
}
|
|
}
|