472 lines
12 KiB
TypeScript
472 lines
12 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
import { apiService } from '../services/index.js';
|
|
import { themeStyles } from '../styles/index.js';
|
|
|
|
const { html, css, cssManager, customElement, property, state, DeesElement } = plugins;
|
|
const { DeesContextmenu } = plugins.deesCatalog;
|
|
|
|
@customElement('tsview-mongo-documents')
|
|
export class TsviewMongoDocuments extends DeesElement {
|
|
@property({ type: String })
|
|
public accessor databaseName: string = '';
|
|
|
|
@property({ type: String })
|
|
public accessor collectionName: string = '';
|
|
|
|
@state()
|
|
private accessor documents: Record<string, unknown>[] = [];
|
|
|
|
@state()
|
|
private accessor total: number = 0;
|
|
|
|
@state()
|
|
private accessor page: number = 1;
|
|
|
|
@state()
|
|
private accessor pageSize: number = 50;
|
|
|
|
@state()
|
|
private accessor loading: boolean = false;
|
|
|
|
@state()
|
|
private accessor filterText: string = '';
|
|
|
|
@state()
|
|
private accessor selectedId: string = '';
|
|
|
|
public static styles = [
|
|
cssManager.defaultStyles,
|
|
themeStyles,
|
|
css`
|
|
:host {
|
|
display: block;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.documents-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
}
|
|
|
|
.filter-bar {
|
|
display: flex;
|
|
gap: 12px;
|
|
padding: 12px;
|
|
border-bottom: 1px solid #333;
|
|
}
|
|
|
|
.filter-input {
|
|
flex: 1;
|
|
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;
|
|
}
|
|
|
|
.filter-input:focus {
|
|
outline: none;
|
|
border-color: #404040;
|
|
}
|
|
|
|
.filter-input::placeholder {
|
|
color: #666;
|
|
}
|
|
|
|
.filter-btn {
|
|
padding: 8px 16px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border: 1px solid #404040;
|
|
color: #e0e0e0;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.filter-btn:hover {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
}
|
|
|
|
.documents-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 8px;
|
|
}
|
|
|
|
.document-row {
|
|
padding: 10px 12px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
margin-bottom: 4px;
|
|
background: rgba(0, 0, 0, 0.2);
|
|
transition: background 0.1s;
|
|
}
|
|
|
|
.document-row:hover {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
.document-row.selected {
|
|
background: rgba(255, 255, 255, 0.08);
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
}
|
|
|
|
.document-id {
|
|
font-size: 12px;
|
|
color: #e0e0e0;
|
|
font-family: monospace;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.document-preview {
|
|
font-size: 12px;
|
|
color: #888;
|
|
font-family: monospace;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.pagination {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 12px;
|
|
border-top: 1px solid #333;
|
|
font-size: 13px;
|
|
color: #888;
|
|
}
|
|
|
|
.pagination-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.pagination-controls {
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
|
|
.page-btn {
|
|
padding: 6px 12px;
|
|
background: transparent;
|
|
border: 1px solid #444;
|
|
color: #888;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.page-btn:hover:not(:disabled) {
|
|
border-color: #666;
|
|
color: #aaa;
|
|
}
|
|
|
|
.page-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;
|
|
}
|
|
|
|
.actions-bar {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
padding: 8px 12px;
|
|
border-bottom: 1px solid #333;
|
|
}
|
|
|
|
.action-btn {
|
|
padding: 6px 12px;
|
|
background: rgba(34, 197, 94, 0.2);
|
|
border: 1px solid #22c55e;
|
|
color: #4ade80;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.action-btn:hover {
|
|
background: rgba(34, 197, 94, 0.3);
|
|
}
|
|
`,
|
|
];
|
|
|
|
async connectedCallback() {
|
|
super.connectedCallback();
|
|
await this.loadDocuments();
|
|
}
|
|
|
|
updated(changedProperties: Map<string, unknown>) {
|
|
if (changedProperties.has('databaseName') || changedProperties.has('collectionName')) {
|
|
this.page = 1;
|
|
this.loadDocuments();
|
|
}
|
|
}
|
|
|
|
private async loadDocuments() {
|
|
if (!this.databaseName || !this.collectionName) return;
|
|
|
|
this.loading = true;
|
|
try {
|
|
let filter = {};
|
|
if (this.filterText.trim()) {
|
|
try {
|
|
filter = JSON.parse(this.filterText);
|
|
} catch {
|
|
// Invalid JSON, ignore filter
|
|
}
|
|
}
|
|
|
|
const result = await apiService.findDocuments(
|
|
this.databaseName,
|
|
this.collectionName,
|
|
{
|
|
filter,
|
|
skip: (this.page - 1) * this.pageSize,
|
|
limit: this.pageSize,
|
|
}
|
|
);
|
|
|
|
this.documents = result.documents;
|
|
this.total = result.total;
|
|
} catch (err) {
|
|
console.error('Error loading documents:', err);
|
|
this.documents = [];
|
|
this.total = 0;
|
|
}
|
|
this.loading = false;
|
|
}
|
|
|
|
private handleFilterInput(e: Event) {
|
|
this.filterText = (e.target as HTMLInputElement).value;
|
|
}
|
|
|
|
private handleFilterSubmit() {
|
|
this.page = 1;
|
|
this.loadDocuments();
|
|
}
|
|
|
|
private handleKeyPress(e: KeyboardEvent) {
|
|
if (e.key === 'Enter') {
|
|
this.handleFilterSubmit();
|
|
}
|
|
}
|
|
|
|
private selectDocument(doc: Record<string, unknown>) {
|
|
const id = (doc._id as string) || '';
|
|
this.selectedId = id;
|
|
|
|
this.dispatchEvent(
|
|
new CustomEvent('document-selected', {
|
|
detail: { documentId: id, document: doc },
|
|
bubbles: true,
|
|
composed: true,
|
|
})
|
|
);
|
|
}
|
|
|
|
private goToPage(pageNum: number) {
|
|
this.page = pageNum;
|
|
this.loadDocuments();
|
|
}
|
|
|
|
private getDocumentPreview(doc: Record<string, unknown>): string {
|
|
const preview: Record<string, unknown> = {};
|
|
const keys = Object.keys(doc).filter((k) => k !== '_id');
|
|
for (const key of keys.slice(0, 3)) {
|
|
preview[key] = doc[key];
|
|
}
|
|
return JSON.stringify(preview);
|
|
}
|
|
|
|
private get totalPages(): number {
|
|
return Math.ceil(this.total / this.pageSize);
|
|
}
|
|
|
|
private async handleInsertNew() {
|
|
const newDoc = {
|
|
// Default empty document
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
|
|
try {
|
|
const insertedId = await apiService.insertDocument(
|
|
this.databaseName,
|
|
this.collectionName,
|
|
newDoc
|
|
);
|
|
await this.loadDocuments();
|
|
this.selectedId = insertedId;
|
|
this.dispatchEvent(
|
|
new CustomEvent('document-selected', {
|
|
detail: { documentId: insertedId },
|
|
bubbles: true,
|
|
composed: true,
|
|
})
|
|
);
|
|
} catch (err) {
|
|
console.error('Error inserting document:', err);
|
|
}
|
|
}
|
|
|
|
private handleDocumentContextMenu(event: MouseEvent, doc: Record<string, unknown>) {
|
|
event.preventDefault();
|
|
const docId = doc._id as string;
|
|
|
|
DeesContextmenu.openContextMenuWithOptions(event, [
|
|
{
|
|
name: 'View/Edit',
|
|
iconName: 'lucide:edit',
|
|
action: async () => {
|
|
this.selectDocument(doc);
|
|
},
|
|
},
|
|
{
|
|
name: 'Copy as JSON',
|
|
iconName: 'lucide:copy',
|
|
action: async () => {
|
|
await navigator.clipboard.writeText(JSON.stringify(doc, null, 2));
|
|
},
|
|
},
|
|
{
|
|
name: 'Duplicate',
|
|
iconName: 'lucide:copyPlus',
|
|
action: async () => {
|
|
const { _id, ...docWithoutId } = doc;
|
|
const newDoc = { ...docWithoutId, createdAt: new Date().toISOString() };
|
|
try {
|
|
const insertedId = await apiService.insertDocument(
|
|
this.databaseName,
|
|
this.collectionName,
|
|
newDoc
|
|
);
|
|
await this.loadDocuments();
|
|
this.selectedId = insertedId;
|
|
this.dispatchEvent(
|
|
new CustomEvent('document-selected', {
|
|
detail: { documentId: insertedId },
|
|
bubbles: true,
|
|
composed: true,
|
|
})
|
|
);
|
|
} catch (err) {
|
|
console.error('Error duplicating document:', err);
|
|
}
|
|
},
|
|
},
|
|
{ divider: true },
|
|
{
|
|
name: 'Delete',
|
|
iconName: 'lucide:trash2',
|
|
action: async () => {
|
|
if (confirm(`Delete document "${docId}"?`)) {
|
|
const result = await apiService.deleteDocument(
|
|
this.databaseName,
|
|
this.collectionName,
|
|
docId
|
|
);
|
|
if (result.success) {
|
|
await this.loadDocuments();
|
|
if (this.selectedId === docId) {
|
|
this.selectedId = '';
|
|
}
|
|
}
|
|
}
|
|
},
|
|
},
|
|
]);
|
|
}
|
|
|
|
render() {
|
|
const startRecord = (this.page - 1) * this.pageSize + 1;
|
|
const endRecord = Math.min(this.page * this.pageSize, this.total);
|
|
|
|
return html`
|
|
<div class="documents-container">
|
|
<div class="filter-bar">
|
|
<input
|
|
type="text"
|
|
class="filter-input"
|
|
placeholder='Filter: {"field": "value"}'
|
|
.value=${this.filterText}
|
|
@input=${this.handleFilterInput}
|
|
@keypress=${this.handleKeyPress}
|
|
/>
|
|
<button class="filter-btn" @click=${this.handleFilterSubmit}>Apply</button>
|
|
</div>
|
|
|
|
<div class="actions-bar">
|
|
<button class="action-btn" @click=${this.handleInsertNew}>+ Insert Document</button>
|
|
</div>
|
|
|
|
<div class="documents-list">
|
|
${this.loading
|
|
? html`<div class="loading-state">Loading...</div>`
|
|
: this.documents.length === 0
|
|
? html`<div class="empty-state">No documents found</div>`
|
|
: this.documents.map(
|
|
(doc) => html`
|
|
<div
|
|
class="document-row ${this.selectedId === doc._id ? 'selected' : ''}"
|
|
@click=${() => this.selectDocument(doc)}
|
|
@contextmenu=${(e: MouseEvent) => this.handleDocumentContextMenu(e, doc)}
|
|
>
|
|
<div class="document-id">_id: ${doc._id}</div>
|
|
<div class="document-preview">${this.getDocumentPreview(doc)}</div>
|
|
</div>
|
|
`
|
|
)}
|
|
</div>
|
|
|
|
${this.total > 0
|
|
? html`
|
|
<div class="pagination">
|
|
<div class="pagination-info">
|
|
Showing ${startRecord}-${endRecord} of ${this.total}
|
|
</div>
|
|
<div class="pagination-controls">
|
|
<button
|
|
class="page-btn"
|
|
?disabled=${this.page <= 1}
|
|
@click=${() => this.goToPage(this.page - 1)}
|
|
>
|
|
Previous
|
|
</button>
|
|
<button
|
|
class="page-btn"
|
|
?disabled=${this.page >= this.totalPages}
|
|
@click=${() => this.goToPage(this.page + 1)}
|
|
>
|
|
Next
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`
|
|
: ''}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|