This commit is contained in:
2026-01-23 22:15:51 +00:00
commit 74d24cf8b9
44 changed files with 15483 additions and 0 deletions

View File

@@ -0,0 +1,428 @@
import * as plugins from '../plugins.js';
import { apiService, type IMongoIndex } from '../services/index.js';
const { html, css, cssManager, customElement, property, state, DeesElement } = plugins;
@customElement('tsview-mongo-indexes')
export class TsviewMongoIndexes extends DeesElement {
@property({ type: String })
public accessor databaseName: string = '';
@property({ type: String })
public accessor collectionName: string = '';
@state()
private accessor indexes: IMongoIndex[] = [];
@state()
private accessor loading: boolean = false;
@state()
private accessor showCreateDialog: boolean = false;
@state()
private accessor newIndexKeys: string = '';
@state()
private accessor newIndexUnique: boolean = false;
@state()
private accessor newIndexSparse: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
height: 100%;
overflow: hidden;
}
.indexes-container {
display: flex;
flex-direction: column;
height: 100%;
}
.toolbar {
display: flex;
justify-content: flex-end;
padding: 12px;
border-bottom: 1px solid #333;
}
.create-btn {
padding: 8px 16px;
background: rgba(34, 197, 94, 0.2);
border: 1px solid #22c55e;
color: #4ade80;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
}
.create-btn:hover {
background: rgba(34, 197, 94, 0.3);
}
.indexes-list {
flex: 1;
overflow-y: auto;
padding: 12px;
}
.index-card {
background: rgba(0, 0, 0, 0.2);
border: 1px solid #333;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
}
.index-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.index-name {
font-size: 14px;
font-weight: 500;
color: #fff;
}
.index-badges {
display: flex;
gap: 8px;
}
.badge {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.1);
color: #888;
}
.badge.unique {
background: rgba(99, 102, 241, 0.2);
color: #818cf8;
}
.badge.sparse {
background: rgba(251, 191, 36, 0.2);
color: #fbbf24;
}
.index-keys {
font-family: monospace;
font-size: 12px;
color: #888;
background: rgba(0, 0, 0, 0.2);
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 12px;
}
.index-actions {
display: flex;
justify-content: flex-end;
}
.drop-btn {
padding: 6px 12px;
background: transparent;
border: 1px solid #ef4444;
color: #f87171;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.drop-btn:hover {
background: rgba(239, 68, 68, 0.2);
}
.drop-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #666;
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: #888;
}
/* Dialog styles */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.dialog {
background: #1e1e38;
border: 1px solid #333;
border-radius: 12px;
padding: 24px;
width: 400px;
max-width: 90%;
}
.dialog-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
}
.dialog-field {
margin-bottom: 16px;
}
.dialog-label {
display: block;
font-size: 13px;
color: #888;
margin-bottom: 6px;
}
.dialog-input {
width: 100%;
padding: 8px 12px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid #444;
border-radius: 6px;
color: #fff;
font-size: 13px;
font-family: monospace;
}
.dialog-input:focus {
outline: none;
border-color: #6366f1;
}
.dialog-checkbox {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 24px;
}
.dialog-btn {
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
}
.dialog-btn.secondary {
background: transparent;
border: 1px solid #444;
color: #888;
}
.dialog-btn.secondary:hover {
border-color: #666;
color: #aaa;
}
.dialog-btn.primary {
background: rgba(99, 102, 241, 0.2);
border: 1px solid #6366f1;
color: #818cf8;
}
.dialog-btn.primary:hover {
background: rgba(99, 102, 241, 0.3);
}
`,
];
async connectedCallback() {
super.connectedCallback();
await this.loadIndexes();
}
updated(changedProperties: Map<string, unknown>) {
if (changedProperties.has('databaseName') || changedProperties.has('collectionName')) {
this.loadIndexes();
}
}
private async loadIndexes() {
if (!this.databaseName || !this.collectionName) return;
this.loading = true;
try {
this.indexes = await apiService.listIndexes(this.databaseName, this.collectionName);
} catch (err) {
console.error('Error loading indexes:', err);
this.indexes = [];
}
this.loading = false;
}
private openCreateDialog() {
this.newIndexKeys = '';
this.newIndexUnique = false;
this.newIndexSparse = false;
this.showCreateDialog = true;
}
private closeCreateDialog() {
this.showCreateDialog = false;
}
private async createIndex() {
try {
const keys = JSON.parse(this.newIndexKeys);
await apiService.createIndex(this.databaseName, this.collectionName, keys, {
unique: this.newIndexUnique,
sparse: this.newIndexSparse,
});
this.closeCreateDialog();
await this.loadIndexes();
} catch (err) {
console.error('Error creating index:', err);
alert('Invalid JSON or index creation failed');
}
}
private async dropIndex(indexName: string) {
if (indexName === '_id_') {
alert('Cannot drop the _id index');
return;
}
if (!confirm(`Drop index "${indexName}"?`)) return;
try {
await apiService.dropIndex(this.databaseName, this.collectionName, indexName);
await this.loadIndexes();
} catch (err) {
console.error('Error dropping index:', err);
}
}
private formatKeys(keys: Record<string, number>): string {
return JSON.stringify(keys);
}
render() {
return html`
<div class="indexes-container">
<div class="toolbar">
<button class="create-btn" @click=${this.openCreateDialog}>+ Create Index</button>
</div>
<div class="indexes-list">
${this.loading
? html`<div class="loading-state">Loading...</div>`
: this.indexes.length === 0
? html`<div class="empty-state">No indexes found</div>`
: this.indexes.map(
(idx) => html`
<div class="index-card">
<div class="index-header">
<span class="index-name">${idx.name}</span>
<div class="index-badges">
${idx.unique ? html`<span class="badge unique">unique</span>` : ''}
${idx.sparse ? html`<span class="badge sparse">sparse</span>` : ''}
</div>
</div>
<div class="index-keys">${this.formatKeys(idx.keys)}</div>
<div class="index-actions">
<button
class="drop-btn"
?disabled=${idx.name === '_id_'}
@click=${() => this.dropIndex(idx.name)}
>
Drop
</button>
</div>
</div>
`
)}
</div>
</div>
${this.showCreateDialog
? html`
<div class="dialog-overlay" @click=${this.closeCreateDialog}>
<div class="dialog" @click=${(e: Event) => e.stopPropagation()}>
<div class="dialog-title">Create Index</div>
<div class="dialog-field">
<label class="dialog-label">Index Keys (JSON)</label>
<input
type="text"
class="dialog-input"
placeholder='{"field": 1}'
.value=${this.newIndexKeys}
@input=${(e: Event) => (this.newIndexKeys = (e.target as HTMLInputElement).value)}
/>
</div>
<div class="dialog-field">
<label class="dialog-checkbox">
<input
type="checkbox"
.checked=${this.newIndexUnique}
@change=${(e: Event) => (this.newIndexUnique = (e.target as HTMLInputElement).checked)}
/>
Unique
</label>
</div>
<div class="dialog-field">
<label class="dialog-checkbox">
<input
type="checkbox"
.checked=${this.newIndexSparse}
@change=${(e: Event) => (this.newIndexSparse = (e.target as HTMLInputElement).checked)}
/>
Sparse
</label>
</div>
<div class="dialog-actions">
<button class="dialog-btn secondary" @click=${this.closeCreateDialog}>Cancel</button>
<button class="dialog-btn primary" @click=${this.createIndex}>Create</button>
</div>
</div>
</div>
`
: ''}
`;
}
}