import { DeesElement, customElement, html, css, cssManager, property, type TemplateResult, } from '@design.estate/dees-element'; import type { ISgToken, ISgOrganization, TSgProtocol } from '../interfaces.js'; declare global { interface HTMLElementTagNameMap { 'sg-tokens-view': SgTokensView; } } const ALL_PROTOCOLS: TSgProtocol[] = ['npm', 'oci', 'maven', 'cargo', 'composer', 'pypi', 'rubygems']; @customElement('sg-tokens-view') export class SgTokensView extends DeesElement { public static demo = () => html`
`; public static demoGroups = ['Auth']; @property({ type: Array }) public accessor tokens: ISgToken[] = []; @property({ type: Array }) public accessor organizations: ISgOrganization[] = []; private showCreateForm = false; private createName = ''; private createProtocols: TSgProtocol[] = []; private createOrgId = ''; private createExpiryDays = 0; public static styles = [ cssManager.defaultStyles, css` :host { display: block; color: ${cssManager.bdTheme('#111', '#fff')}; } .container { display: flex; flex-direction: column; gap: 24px; } .header { display: flex; justify-content: space-between; align-items: center; } .page-title { font-size: 24px; font-weight: 700; letter-spacing: -0.02em; } .create-btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; background: ${cssManager.bdTheme('#111', '#fff')}; border: none; font-size: 13px; font-weight: 600; color: ${cssManager.bdTheme('#fff', '#111')}; cursor: pointer; transition: opacity 150ms ease; } .create-btn:hover { opacity: 0.85; } /* Create form */ .create-form { background: ${cssManager.bdTheme('#fff', '#111')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; padding: 24px; display: flex; flex-direction: column; gap: 16px; } .form-title { font-size: 16px; font-weight: 600; margin-bottom: 4px; } .form-group { display: flex; flex-direction: column; gap: 6px; } .form-label { font-size: 13px; font-weight: 600; color: ${cssManager.bdTheme('#111', '#ddd')}; text-transform: uppercase; letter-spacing: 0.04em; } .form-input { padding: 10px 12px; background: ${cssManager.bdTheme('#fff', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#ddd', '#333')}; font-size: 14px; color: ${cssManager.bdTheme('#111', '#fff')}; outline: none; font-family: inherit; } .form-input:focus { border-color: ${cssManager.bdTheme('#111', '#fff')}; } .form-select { padding: 10px 12px; background: ${cssManager.bdTheme('#fff', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#ddd', '#333')}; font-size: 14px; color: ${cssManager.bdTheme('#111', '#fff')}; outline: none; font-family: inherit; } .protocol-selector { display: flex; gap: 6px; flex-wrap: wrap; } .protocol-chip { padding: 6px 12px; background: transparent; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; font-size: 12px; font-weight: 500; color: ${cssManager.bdTheme('#666', '#999')}; cursor: pointer; transition: all 150ms ease; text-transform: uppercase; letter-spacing: 0.04em; } .protocol-chip:hover { border-color: ${cssManager.bdTheme('#999', '#666')}; } .protocol-chip.selected { background: ${cssManager.bdTheme('#111', '#fff')}; color: ${cssManager.bdTheme('#fff', '#111')}; border-color: ${cssManager.bdTheme('#111', '#fff')}; } .form-actions { display: flex; gap: 8px; margin-top: 8px; } .form-submit { padding: 8px 20px; background: ${cssManager.bdTheme('#111', '#fff')}; border: none; font-size: 13px; font-weight: 600; color: ${cssManager.bdTheme('#fff', '#111')}; cursor: pointer; transition: opacity 150ms ease; } .form-submit:hover { opacity: 0.85; } .form-cancel { padding: 8px 20px; background: transparent; border: 1px solid ${cssManager.bdTheme('#ddd', '#333')}; font-size: 13px; color: ${cssManager.bdTheme('#666', '#999')}; cursor: pointer; transition: all 150ms ease; } .form-cancel:hover { border-color: ${cssManager.bdTheme('#999', '#666')}; color: ${cssManager.bdTheme('#111', '#fff')}; } /* Token list */ .token-list { display: flex; flex-direction: column; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; } .token-row { display: flex; align-items: center; justify-content: space-between; padding: 16px; background: ${cssManager.bdTheme('#fff', '#111')}; border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; } .token-row:last-child { border-bottom: none; } .token-info { display: flex; flex-direction: column; gap: 6px; min-width: 0; } .token-name-row { display: flex; align-items: center; gap: 8px; } .token-name { font-size: 14px; font-weight: 600; color: ${cssManager.bdTheme('#111', '#fff')}; } .token-prefix { font-size: 12px; font-family: 'JetBrains Mono', monospace; color: ${cssManager.bdTheme('#888', '#777')}; background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')}; padding: 1px 6px; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; } .token-protocols { display: flex; gap: 4px; flex-wrap: wrap; } .token-protocol { font-size: 11px; font-weight: 600; text-transform: uppercase; padding: 1px 6px; background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')}; color: ${cssManager.bdTheme('#666', '#aaa')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; } .token-meta { display: flex; gap: 12px; font-size: 12px; color: ${cssManager.bdTheme('#888', '#777')}; flex-wrap: wrap; } .token-expired { color: #ef4444; font-weight: 600; } .token-actions { display: flex; gap: 8px; flex-shrink: 0; } .revoke-btn { padding: 6px 14px; background: transparent; border: 1px solid rgba(239, 68, 68, 0.3); font-size: 12px; font-weight: 500; color: #ef4444; cursor: pointer; transition: all 150ms ease; } .revoke-btn:hover { background: rgba(239, 68, 68, 0.15); } .empty-state { text-align: center; padding: 48px 32px; font-size: 14px; color: ${cssManager.bdTheme('#888', '#777')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; background: ${cssManager.bdTheme('#fff', '#111')}; } `, ]; public render(): TemplateResult { return html`
API Tokens
${this.showCreateForm ? this.renderCreateForm() : ''} ${this.tokens.length > 0 ? html`
${this.tokens.map((token) => this.renderToken(token))}
` : html`
No API tokens created yet. Create one to authenticate with the registry.
`}
`; } private renderCreateForm(): TemplateResult { return html`
Create New Token
{ this.createName = (e.target as HTMLInputElement).value; }} >
${ALL_PROTOCOLS.map( (proto) => html` ` )}
${this.organizations.length > 0 ? html`
` : ''}
{ this.createExpiryDays = parseInt((e.target as HTMLInputElement).value) || 0; }} >
`; } private renderToken(token: ISgToken): TemplateResult { const isExpired = token.expiresAt && new Date(token.expiresAt) < new Date(); return html`
${token.name} ${token.tokenPrefix}...
${token.protocols.map( (p) => html`${p}` )}
Created ${this.formatDate(token.createdAt)} ${token.lastUsedAt ? html`Last used ${this.formatDate(token.lastUsedAt)}` : ''} ${token.usageCount} uses ${token.expiresAt ? isExpired ? html`Expired` : html`Expires ${this.formatDate(token.expiresAt)}` : html`No expiry`} ${token.organizationId ? html`Org-scoped` : ''}
`; } private toggleProtocol(proto: TSgProtocol) { if (this.createProtocols.includes(proto)) { this.createProtocols = this.createProtocols.filter((p) => p !== proto); } else { this.createProtocols = [...this.createProtocols, proto]; } this.requestUpdate(); } private handleCreate() { if (!this.createName.trim()) return; this.dispatchEvent( new CustomEvent('create', { detail: { name: this.createName.trim(), protocols: this.createProtocols, scopes: [{ protocol: '*' as const, actions: ['read', 'write'] as const }], organizationId: this.createOrgId || undefined, expiresInDays: this.createExpiryDays || undefined, }, bubbles: true, composed: true, }) ); this.showCreateForm = false; this.createName = ''; this.createProtocols = []; this.createOrgId = ''; this.createExpiryDays = 0; this.requestUpdate(); } private handleRevoke(tokenId: string) { this.dispatchEvent( new CustomEvent('revoke', { detail: { tokenId }, bubbles: true, composed: true, }) ); } private formatDate(dateStr: string): string { if (!dateStr) return ''; try { return new Date(dateStr).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); } catch { return dateStr; } } }