496 lines
14 KiB
TypeScript
496 lines
14 KiB
TypeScript
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`
|
|
<div style="padding: 24px; max-width: 1000px; background: #09090b;">
|
|
<sg-tokens-view
|
|
.tokens=${[
|
|
{ id: 't1', name: 'CI/CD Pipeline', tokenPrefix: 'sg_abc', protocols: ['npm', 'oci'], scopes: [{ protocol: '*', actions: ['read', 'write'] }], expiresAt: '2027-01-01', lastUsedAt: '2026-03-19', usageCount: 245, createdAt: '2026-01-15' },
|
|
{ id: 't2', name: 'Read-only Mirror', tokenPrefix: 'sg_def', protocols: ['npm'], scopes: [{ protocol: 'npm', actions: ['read'] }], lastUsedAt: '2026-03-18', usageCount: 1230, createdAt: '2025-11-01' },
|
|
{ id: 't3', name: 'Deploy Token', tokenPrefix: 'sg_ghi', protocols: ['oci'], scopes: [{ protocol: 'oci', actions: ['read', 'write'] }], organizationId: 'org1', expiresAt: '2026-06-01', usageCount: 56, createdAt: '2026-02-20' },
|
|
]}
|
|
.organizations=${[
|
|
{ id: 'org1', name: 'myorg', displayName: 'My Organization', isPublic: true, memberCount: 8, createdAt: '2025-06-01' },
|
|
]}
|
|
></sg-tokens-view>
|
|
</div>
|
|
`;
|
|
|
|
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`
|
|
<div class="container">
|
|
<div class="header">
|
|
<div class="page-title">API Tokens</div>
|
|
<button class="create-btn" @click=${() => { this.showCreateForm = true; this.requestUpdate(); }}>
|
|
+ New Token
|
|
</button>
|
|
</div>
|
|
|
|
${this.showCreateForm ? this.renderCreateForm() : ''}
|
|
|
|
${this.tokens.length > 0
|
|
? html`
|
|
<div class="token-list">
|
|
${this.tokens.map((token) => this.renderToken(token))}
|
|
</div>
|
|
`
|
|
: html`<div class="empty-state">No API tokens created yet. Create one to authenticate with the registry.</div>`}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderCreateForm(): TemplateResult {
|
|
return html`
|
|
<div class="create-form">
|
|
<div class="form-title">Create New Token</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Token Name</label>
|
|
<input
|
|
type="text"
|
|
class="form-input"
|
|
placeholder="e.g., CI/CD Pipeline"
|
|
@input=${(e: InputEvent) => { this.createName = (e.target as HTMLInputElement).value; }}
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Protocols</label>
|
|
<div class="protocol-selector">
|
|
${ALL_PROTOCOLS.map(
|
|
(proto) => html`
|
|
<button
|
|
class="protocol-chip ${this.createProtocols.includes(proto) ? 'selected' : ''}"
|
|
@click=${() => this.toggleProtocol(proto)}
|
|
>${proto}</button>
|
|
`
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
${this.organizations.length > 0
|
|
? html`
|
|
<div class="form-group">
|
|
<label class="form-label">Organization (optional)</label>
|
|
<select
|
|
class="form-select"
|
|
@change=${(e: Event) => { this.createOrgId = (e.target as HTMLSelectElement).value; }}
|
|
>
|
|
<option value="">No organization (personal token)</option>
|
|
${this.organizations.map(
|
|
(org) => html`<option value=${org.id}>${org.displayName || org.name}</option>`
|
|
)}
|
|
</select>
|
|
</div>
|
|
`
|
|
: ''}
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Expires In (days, 0 = never)</label>
|
|
<input
|
|
type="number"
|
|
class="form-input"
|
|
placeholder="0"
|
|
min="0"
|
|
@input=${(e: InputEvent) => { this.createExpiryDays = parseInt((e.target as HTMLInputElement).value) || 0; }}
|
|
>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button class="form-submit" @click=${this.handleCreate}>Create Token</button>
|
|
<button class="form-cancel" @click=${() => { this.showCreateForm = false; this.requestUpdate(); }}>Cancel</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderToken(token: ISgToken): TemplateResult {
|
|
const isExpired = token.expiresAt && new Date(token.expiresAt) < new Date();
|
|
|
|
return html`
|
|
<div class="token-row">
|
|
<div class="token-info">
|
|
<div class="token-name-row">
|
|
<span class="token-name">${token.name}</span>
|
|
<span class="token-prefix">${token.tokenPrefix}...</span>
|
|
</div>
|
|
<div class="token-protocols">
|
|
${token.protocols.map(
|
|
(p) => html`<span class="token-protocol">${p}</span>`
|
|
)}
|
|
</div>
|
|
<div class="token-meta">
|
|
<span>Created ${this.formatDate(token.createdAt)}</span>
|
|
${token.lastUsedAt ? html`<span>Last used ${this.formatDate(token.lastUsedAt)}</span>` : ''}
|
|
<span>${token.usageCount} uses</span>
|
|
${token.expiresAt
|
|
? isExpired
|
|
? html`<span class="token-expired">Expired</span>`
|
|
: html`<span>Expires ${this.formatDate(token.expiresAt)}</span>`
|
|
: html`<span>No expiry</span>`}
|
|
${token.organizationId ? html`<span>Org-scoped</span>` : ''}
|
|
</div>
|
|
</div>
|
|
<div class="token-actions">
|
|
<button class="revoke-btn" @click=${() => this.handleRevoke(token.id)}>Revoke</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|