feat(tsview): add database and S3 handlers, tswatch/watch scripts, web utilities, assets and release config
This commit is contained in:
8
ts_web/00_commitinfo_data.ts
Normal file
8
ts_web/00_commitinfo_data.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tsview',
|
||||
version: '1.1.0',
|
||||
description: 'A CLI tool for viewing S3 and MongoDB data with a web UI'
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { apiService } from '../services/index.js';
|
||||
import { themeStyles } from '../styles/index.js';
|
||||
|
||||
const { html, css, cssManager, customElement, state, DeesElement } = plugins;
|
||||
|
||||
@@ -37,8 +38,15 @@ export class TsviewApp extends DeesElement {
|
||||
@state()
|
||||
private accessor newCollectionName: string = '';
|
||||
|
||||
@state()
|
||||
private accessor showCreateDatabaseDialog: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor newDatabaseName: string = '';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
@@ -47,7 +55,7 @@ export class TsviewApp extends DeesElement {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #1a1a2e;
|
||||
background: var(--tsview-bg-primary, #1a1a1a);
|
||||
color: #eee;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
@@ -59,7 +67,7 @@ export class TsviewApp extends DeesElement {
|
||||
}
|
||||
|
||||
.app-header {
|
||||
background: #16162a;
|
||||
background: #141414;
|
||||
border-bottom: 1px solid #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -103,8 +111,8 @@ export class TsviewApp extends DeesElement {
|
||||
}
|
||||
|
||||
.nav-tab.active {
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
color: #818cf8;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
@@ -114,7 +122,7 @@ export class TsviewApp extends DeesElement {
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: #1e1e38;
|
||||
background: #1e1e1e;
|
||||
border-right: 1px solid #333;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -148,8 +156,8 @@ export class TsviewApp extends DeesElement {
|
||||
}
|
||||
|
||||
.sidebar-item.selected {
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
color: #818cf8;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.sidebar-item .count {
|
||||
@@ -219,8 +227,8 @@ export class TsviewApp extends DeesElement {
|
||||
}
|
||||
|
||||
.collection-item.selected {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
color: #818cf8;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
@@ -229,18 +237,18 @@ export class TsviewApp extends DeesElement {
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
margin: 8px;
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
border: 1px dashed rgba(99, 102, 241, 0.4);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px dashed rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
color: #818cf8;
|
||||
color: #e0e0e0;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.create-btn:hover {
|
||||
background: rgba(99, 102, 241, 0.3);
|
||||
border-color: rgba(99, 102, 241, 0.6);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
@@ -257,7 +265,7 @@ export class TsviewApp extends DeesElement {
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background: #1e1e38;
|
||||
background: #1e1e1e;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
min-width: 400px;
|
||||
@@ -274,7 +282,7 @@ export class TsviewApp extends DeesElement {
|
||||
.dialog-input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: #16162a;
|
||||
background: #141414;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
@@ -285,7 +293,7 @@ export class TsviewApp extends DeesElement {
|
||||
|
||||
.dialog-input:focus {
|
||||
outline: none;
|
||||
border-color: #818cf8;
|
||||
border-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
@@ -314,19 +322,67 @@ export class TsviewApp extends DeesElement {
|
||||
}
|
||||
|
||||
.dialog-btn-create {
|
||||
background: #6366f1;
|
||||
background: #404040;
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dialog-btn-create:hover {
|
||||
background: #5558e8;
|
||||
background: #505050;
|
||||
}
|
||||
|
||||
.dialog-btn-create:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.dialog-btn-delete {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
border: 1px solid #ef4444;
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.dialog-btn-delete:hover {
|
||||
background: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.sidebar-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar-item-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
opacity: 0;
|
||||
padding: 4px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.sidebar-item:hover .delete-btn,
|
||||
.db-group-header:hover .delete-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #f87171;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -392,6 +448,53 @@ export class TsviewApp extends DeesElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async createDatabase() {
|
||||
if (!this.newDatabaseName.trim()) return;
|
||||
const success = await apiService.createDatabase(this.newDatabaseName.trim());
|
||||
if (success) {
|
||||
this.databases = [...this.databases, { name: this.newDatabaseName.trim() }];
|
||||
this.newDatabaseName = '';
|
||||
this.showCreateDatabaseDialog = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteBucket(bucket: string, e: Event) {
|
||||
e.stopPropagation();
|
||||
if (!confirm(`Delete bucket "${bucket}"? This will delete all objects in the bucket.`)) return;
|
||||
const success = await apiService.deleteBucket(bucket);
|
||||
if (success) {
|
||||
this.buckets = this.buckets.filter(b => b !== bucket);
|
||||
if (this.selectedBucket === bucket) {
|
||||
this.selectedBucket = this.buckets[0] || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteDatabase(dbName: string, e: Event) {
|
||||
e.stopPropagation();
|
||||
if (!confirm(`Delete database "${dbName}"? This will delete all collections and documents.`)) return;
|
||||
const success = await apiService.dropDatabase(dbName);
|
||||
if (success) {
|
||||
this.databases = this.databases.filter(d => d.name !== dbName);
|
||||
if (this.selectedDatabase === dbName) {
|
||||
this.selectedDatabase = this.databases[0]?.name || '';
|
||||
this.selectedCollection = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteCollection(dbName: string, collectionName: string) {
|
||||
if (!confirm(`Delete collection "${collectionName}"? This will delete all documents.`)) return;
|
||||
const success = await apiService.dropCollection(dbName, collectionName);
|
||||
if (success) {
|
||||
if (this.selectedCollection === collectionName) {
|
||||
this.selectedCollection = '';
|
||||
}
|
||||
// Force refresh of the collections list
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="app-container">
|
||||
@@ -433,6 +536,7 @@ export class TsviewApp extends DeesElement {
|
||||
</div>
|
||||
${this.renderCreateBucketDialog()}
|
||||
${this.renderCreateCollectionDialog()}
|
||||
${this.renderCreateDatabaseDialog()}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -498,6 +602,37 @@ export class TsviewApp extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderCreateDatabaseDialog() {
|
||||
if (!this.showCreateDatabaseDialog) return '';
|
||||
return html`
|
||||
<div class="dialog-overlay" @click=${() => this.showCreateDatabaseDialog = false}>
|
||||
<div class="dialog" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<div class="dialog-title">Create New Database</div>
|
||||
<input
|
||||
type="text"
|
||||
class="dialog-input"
|
||||
placeholder="Database name"
|
||||
.value=${this.newDatabaseName}
|
||||
@input=${(e: InputEvent) => this.newDatabaseName = (e.target as HTMLInputElement).value}
|
||||
@keydown=${(e: KeyboardEvent) => e.key === 'Enter' && this.createDatabase()}
|
||||
/>
|
||||
<div class="dialog-actions">
|
||||
<button class="dialog-btn dialog-btn-cancel" @click=${() => this.showCreateDatabaseDialog = false}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="dialog-btn dialog-btn-create"
|
||||
?disabled=${!this.newDatabaseName.trim()}
|
||||
@click=${() => this.createDatabase()}
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderSidebar() {
|
||||
if (this.viewMode === 's3') {
|
||||
return html`
|
||||
@@ -519,7 +654,13 @@ export class TsviewApp extends DeesElement {
|
||||
class="sidebar-item ${bucket === this.selectedBucket ? 'selected' : ''}"
|
||||
@click=${() => this.selectBucket(bucket)}
|
||||
>
|
||||
${bucket}
|
||||
<span class="sidebar-item-name">${bucket}</span>
|
||||
<button class="delete-btn" @click=${(e: Event) => this.deleteBucket(bucket, e)} title="Delete bucket">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
@@ -532,6 +673,13 @@ export class TsviewApp extends DeesElement {
|
||||
return html`
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">Databases & Collections</div>
|
||||
<button class="create-btn" @click=${() => this.showCreateDatabaseDialog = true}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
New Database
|
||||
</button>
|
||||
${this.selectedDatabase ? html`
|
||||
<button class="create-btn" @click=${() => this.showCreateCollectionDialog = true}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
@@ -573,7 +721,13 @@ export class TsviewApp extends DeesElement {
|
||||
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path>
|
||||
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
|
||||
</svg>
|
||||
${db.name}
|
||||
<span style="flex: 1;">${db.name}</span>
|
||||
<button class="delete-btn" @click=${(e: Event) => this.deleteDatabase(db.name, e)} title="Delete database">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
${this.selectedDatabase === db.name ? this.renderCollectionsList(db.name) : ''}
|
||||
</div>
|
||||
@@ -586,10 +740,18 @@ export class TsviewApp extends DeesElement {
|
||||
.databaseName=${dbName}
|
||||
.selectedCollection=${this.selectedCollection}
|
||||
@collection-selected=${(e: CustomEvent) => this.selectCollection(e.detail)}
|
||||
@collection-deleted=${(e: CustomEvent) => this.handleCollectionDeleted(e)}
|
||||
></tsview-mongo-collections>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleCollectionDeleted(e: CustomEvent) {
|
||||
const { collectionName } = e.detail;
|
||||
if (this.selectedCollection === collectionName) {
|
||||
this.selectedCollection = '';
|
||||
}
|
||||
}
|
||||
|
||||
private renderContent() {
|
||||
if (this.viewMode === 's3') {
|
||||
if (!this.selectedBucket) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { apiService, type ICollectionStats } from '../services/index.js';
|
||||
import { formatSize, formatCount } from '../utilities/index.js';
|
||||
import { themeStyles } from '../styles/index.js';
|
||||
|
||||
const { html, css, cssManager, customElement, property, state, DeesElement } = plugins;
|
||||
|
||||
@@ -24,6 +26,7 @@ export class TsviewMongoBrowser extends DeesElement {
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
@@ -92,8 +95,8 @@ export class TsviewMongoBrowser extends DeesElement {
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
color: #818cf8;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -159,23 +162,6 @@ export class TsviewMongoBrowser extends DeesElement {
|
||||
this.selectedDocumentId = e.detail.documentId;
|
||||
}
|
||||
|
||||
private formatCount(num: number): string {
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
private formatSize(bytes: number): string {
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
return `${size.toFixed(unitIndex > 0 ? 1 : 0)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="browser-container">
|
||||
@@ -185,8 +171,8 @@ export class TsviewMongoBrowser extends DeesElement {
|
||||
${this.stats
|
||||
? html`
|
||||
<div class="collection-stats">
|
||||
<span class="stat-item">${this.formatCount(this.stats.count)} docs</span>
|
||||
<span class="stat-item">${this.formatSize(this.stats.size)}</span>
|
||||
<span class="stat-item">${formatCount(this.stats.count)} docs</span>
|
||||
<span class="stat-item">${formatSize(this.stats.size)}</span>
|
||||
<span class="stat-item">${this.stats.indexCount} indexes</span>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { apiService, type IMongoCollection } from '../services/index.js';
|
||||
import { formatCount } from '../utilities/index.js';
|
||||
import { themeStyles } from '../styles/index.js';
|
||||
|
||||
const { html, css, cssManager, customElement, property, state, DeesElement } = plugins;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementEventMap {
|
||||
'collection-deleted': CustomEvent<{ databaseName: string; collectionName: string }>;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('tsview-mongo-collections')
|
||||
export class TsviewMongoCollections extends DeesElement {
|
||||
@property({ type: String })
|
||||
@@ -19,6 +27,7 @@ export class TsviewMongoCollections extends DeesElement {
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
@@ -44,8 +53,8 @@ export class TsviewMongoCollections extends DeesElement {
|
||||
}
|
||||
|
||||
.collection-item.selected {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
color: #818cf8;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.collection-name {
|
||||
@@ -80,6 +89,29 @@ export class TsviewMongoCollections extends DeesElement {
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
opacity: 0;
|
||||
padding: 4px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.collection-item:hover .delete-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #f87171;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -117,11 +149,25 @@ export class TsviewMongoCollections extends DeesElement {
|
||||
);
|
||||
}
|
||||
|
||||
private formatCount(count?: number): string {
|
||||
if (count === undefined) return '';
|
||||
if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
|
||||
if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
|
||||
return count.toString();
|
||||
private async deleteCollection(name: string, e: Event) {
|
||||
e.stopPropagation();
|
||||
if (!confirm(`Delete collection "${name}"? This will delete all documents.`)) return;
|
||||
|
||||
const success = await apiService.dropCollection(this.databaseName, name);
|
||||
if (success) {
|
||||
this.collections = this.collections.filter(c => c.name !== name);
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('collection-deleted', {
|
||||
detail: { databaseName: this.databaseName, collectionName: name },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async refresh() {
|
||||
await this.loadCollections();
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -147,9 +193,17 @@ export class TsviewMongoCollections extends DeesElement {
|
||||
</svg>
|
||||
${coll.name}
|
||||
</span>
|
||||
${coll.count !== undefined
|
||||
? html`<span class="collection-count">${this.formatCount(coll.count)}</span>`
|
||||
: ''}
|
||||
<span style="display: flex; align-items: center; gap: 4px;">
|
||||
${coll.count !== undefined
|
||||
? html`<span class="collection-count">${formatCount(coll.count)}</span>`
|
||||
: ''}
|
||||
<button class="delete-btn" @click=${(e: Event) => this.deleteCollection(coll.name, e)} title="Delete collection">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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;
|
||||
|
||||
@@ -31,6 +32,7 @@ export class TsviewMongoDocument extends DeesElement {
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
@@ -78,13 +80,13 @@ export class TsviewMongoDocument extends DeesElement {
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
border-color: #6366f1;
|
||||
color: #818cf8;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: #404040;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
background: rgba(99, 102, 241, 0.3);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
@@ -113,7 +115,7 @@ export class TsviewMongoDocument extends DeesElement {
|
||||
}
|
||||
|
||||
.json-key {
|
||||
color: #818cf8;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.json-string {
|
||||
@@ -148,7 +150,7 @@ export class TsviewMongoDocument extends DeesElement {
|
||||
|
||||
.edit-area:focus {
|
||||
outline: none;
|
||||
border-color: #6366f1;
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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;
|
||||
|
||||
@@ -34,6 +35,7 @@ export class TsviewMongoDocuments extends DeesElement {
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
@@ -67,7 +69,7 @@ export class TsviewMongoDocuments extends DeesElement {
|
||||
|
||||
.filter-input:focus {
|
||||
outline: none;
|
||||
border-color: #6366f1;
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
.filter-input::placeholder {
|
||||
@@ -76,16 +78,16 @@ export class TsviewMongoDocuments extends DeesElement {
|
||||
|
||||
.filter-btn {
|
||||
padding: 8px 16px;
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
border: 1px solid #6366f1;
|
||||
color: #818cf8;
|
||||
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(99, 102, 241, 0.3);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.documents-list {
|
||||
@@ -108,13 +110,13 @@ export class TsviewMongoDocuments extends DeesElement {
|
||||
}
|
||||
|
||||
.document-row.selected {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.document-id {
|
||||
font-size: 12px;
|
||||
color: #818cf8;
|
||||
color: #e0e0e0;
|
||||
font-family: monospace;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { apiService, type IMongoIndex } from '../services/index.js';
|
||||
import { themeStyles } from '../styles/index.js';
|
||||
|
||||
const { html, css, cssManager, customElement, property, state, DeesElement } = plugins;
|
||||
|
||||
@@ -31,6 +32,7 @@ export class TsviewMongoIndexes extends DeesElement {
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
@@ -106,8 +108,8 @@ export class TsviewMongoIndexes extends DeesElement {
|
||||
}
|
||||
|
||||
.badge.unique {
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
color: #818cf8;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.badge.sparse {
|
||||
@@ -181,7 +183,7 @@ export class TsviewMongoIndexes extends DeesElement {
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background: #1e1e38;
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #333;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
@@ -219,7 +221,7 @@ export class TsviewMongoIndexes extends DeesElement {
|
||||
|
||||
.dialog-input:focus {
|
||||
outline: none;
|
||||
border-color: #6366f1;
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
.dialog-checkbox {
|
||||
@@ -255,13 +257,13 @@ export class TsviewMongoIndexes extends DeesElement {
|
||||
}
|
||||
|
||||
.dialog-btn.primary {
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
border: 1px solid #6366f1;
|
||||
color: #818cf8;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid #404040;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.dialog-btn.primary:hover {
|
||||
background: rgba(99, 102, 241, 0.3);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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;
|
||||
|
||||
@@ -19,8 +20,12 @@ export class TsviewS3Browser extends DeesElement {
|
||||
@state()
|
||||
private accessor selectedKey: string = '';
|
||||
|
||||
@state()
|
||||
private accessor refreshKey: number = 0;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
@@ -90,19 +95,23 @@ export class TsviewS3Browser extends DeesElement {
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
border-color: #6366f1;
|
||||
color: #818cf8;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: #404040;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 350px;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content.has-preview {
|
||||
grid-template-columns: 1fr 350px;
|
||||
}
|
||||
|
||||
.main-view {
|
||||
overflow: auto;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
@@ -116,7 +125,8 @@ export class TsviewS3Browser extends DeesElement {
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.content {
|
||||
.content,
|
||||
.content.has-preview {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -144,6 +154,20 @@ export class TsviewS3Browser extends DeesElement {
|
||||
this.navigateToPrefix(e.detail.prefix);
|
||||
}
|
||||
|
||||
private handleObjectDeleted(e: CustomEvent) {
|
||||
this.selectedKey = '';
|
||||
// Increment refresh key to trigger re-render of child components
|
||||
this.refreshKey++;
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
if (changedProperties.has('bucketName')) {
|
||||
// Clear selection when bucket changes
|
||||
this.selectedKey = '';
|
||||
this.currentPrefix = '';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const breadcrumbParts = this.currentPrefix
|
||||
? this.currentPrefix.split('/').filter(Boolean)
|
||||
@@ -189,13 +213,14 @@ export class TsviewS3Browser extends DeesElement {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="content ${this.selectedKey ? 'has-preview' : ''}">
|
||||
<div class="main-view">
|
||||
${this.viewType === 'columns'
|
||||
? html`
|
||||
<tsview-s3-columns
|
||||
.bucketName=${this.bucketName}
|
||||
.currentPrefix=${this.currentPrefix}
|
||||
.refreshKey=${this.refreshKey}
|
||||
@key-selected=${this.handleKeySelected}
|
||||
@navigate=${this.handleNavigate}
|
||||
></tsview-s3-columns>
|
||||
@@ -204,18 +229,24 @@ export class TsviewS3Browser extends DeesElement {
|
||||
<tsview-s3-keys
|
||||
.bucketName=${this.bucketName}
|
||||
.currentPrefix=${this.currentPrefix}
|
||||
.refreshKey=${this.refreshKey}
|
||||
@key-selected=${this.handleKeySelected}
|
||||
@navigate=${this.handleNavigate}
|
||||
></tsview-s3-keys>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<div class="preview-panel">
|
||||
<tsview-s3-preview
|
||||
.bucketName=${this.bucketName}
|
||||
.objectKey=${this.selectedKey}
|
||||
></tsview-s3-preview>
|
||||
</div>
|
||||
${this.selectedKey
|
||||
? html`
|
||||
<div class="preview-panel">
|
||||
<tsview-s3-preview
|
||||
.bucketName=${this.bucketName}
|
||||
.objectKey=${this.selectedKey}
|
||||
@object-deleted=${this.handleObjectDeleted}
|
||||
></tsview-s3-preview>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { apiService, type IS3Object } from '../services/index.js';
|
||||
import { getFileName } from '../utilities/index.js';
|
||||
import { themeStyles } from '../styles/index.js';
|
||||
|
||||
const { html, css, cssManager, customElement, property, state, DeesElement } = plugins;
|
||||
|
||||
@@ -19,6 +21,9 @@ export class TsviewS3Columns extends DeesElement {
|
||||
@property({ type: String })
|
||||
public accessor currentPrefix: string = '';
|
||||
|
||||
@property({ type: Number })
|
||||
public accessor refreshKey: number = 0;
|
||||
|
||||
@state()
|
||||
private accessor columns: IColumn[] = [];
|
||||
|
||||
@@ -32,6 +37,7 @@ export class TsviewS3Columns extends DeesElement {
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
@@ -81,7 +87,7 @@ export class TsviewS3Columns extends DeesElement {
|
||||
|
||||
.resize-handle:hover::after,
|
||||
.resize-handle.active::after {
|
||||
background: #6366f1;
|
||||
background: #404040;
|
||||
width: 2px;
|
||||
left: 1px;
|
||||
}
|
||||
@@ -124,8 +130,8 @@ export class TsviewS3Columns extends DeesElement {
|
||||
}
|
||||
|
||||
.column-item.selected {
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
color: #818cf8;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.column-item.folder {
|
||||
@@ -172,9 +178,9 @@ export class TsviewS3Columns extends DeesElement {
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
// Only reset columns when bucket changes
|
||||
// Only reset columns when bucket changes or refresh is triggered
|
||||
// Internal folder navigation is handled by selectFolder() which appends columns
|
||||
if (changedProperties.has('bucketName')) {
|
||||
if (changedProperties.has('bucketName') || changedProperties.has('refreshKey')) {
|
||||
this.loadInitialColumn();
|
||||
}
|
||||
}
|
||||
@@ -298,11 +304,6 @@ export class TsviewS3Columns extends DeesElement {
|
||||
);
|
||||
}
|
||||
|
||||
private getFileName(path: string): string {
|
||||
const parts = path.replace(/\/$/, '').split('/');
|
||||
return parts[parts.length - 1] || path;
|
||||
}
|
||||
|
||||
private getFileIcon(key: string): string {
|
||||
const ext = key.split('.').pop()?.toLowerCase() || '';
|
||||
const iconMap: Record<string, string> = {
|
||||
@@ -343,7 +344,7 @@ export class TsviewS3Columns extends DeesElement {
|
||||
|
||||
private renderColumn(column: IColumn, index: number) {
|
||||
const headerName = column.prefix
|
||||
? this.getFileName(column.prefix)
|
||||
? getFileName(column.prefix)
|
||||
: this.bucketName;
|
||||
|
||||
return html`
|
||||
@@ -364,7 +365,7 @@ export class TsviewS3Columns extends DeesElement {
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
<span class="name">${this.getFileName(prefix)}</span>
|
||||
<span class="name">${getFileName(prefix)}</span>
|
||||
<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
@@ -380,7 +381,7 @@ export class TsviewS3Columns extends DeesElement {
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="${this.getFileIcon(obj.key)}" />
|
||||
</svg>
|
||||
<span class="name">${this.getFileName(obj.key)}</span>
|
||||
<span class="name">${getFileName(obj.key)}</span>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { apiService, type IS3Object } from '../services/index.js';
|
||||
import { formatSize, getFileName } from '../utilities/index.js';
|
||||
import { themeStyles } from '../styles/index.js';
|
||||
|
||||
const { html, css, cssManager, customElement, property, state, DeesElement } = plugins;
|
||||
|
||||
@@ -11,6 +13,9 @@ export class TsviewS3Keys extends DeesElement {
|
||||
@property({ type: String })
|
||||
public accessor currentPrefix: string = '';
|
||||
|
||||
@property({ type: Number })
|
||||
public accessor refreshKey: number = 0;
|
||||
|
||||
@state()
|
||||
private accessor allKeys: IS3Object[] = [];
|
||||
|
||||
@@ -28,6 +33,7 @@ export class TsviewS3Keys extends DeesElement {
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
@@ -58,7 +64,7 @@ export class TsviewS3Keys extends DeesElement {
|
||||
|
||||
.filter-input:focus {
|
||||
outline: none;
|
||||
border-color: #6366f1;
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
.filter-input::placeholder {
|
||||
@@ -78,7 +84,7 @@ export class TsviewS3Keys extends DeesElement {
|
||||
thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #1a1a2e;
|
||||
background: #1a1a1a;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -103,7 +109,7 @@ export class TsviewS3Keys extends DeesElement {
|
||||
}
|
||||
|
||||
tr.selected td {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.key-cell {
|
||||
@@ -148,7 +154,7 @@ export class TsviewS3Keys extends DeesElement {
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
if (changedProperties.has('bucketName') || changedProperties.has('currentPrefix')) {
|
||||
if (changedProperties.has('bucketName') || changedProperties.has('currentPrefix') || changedProperties.has('refreshKey')) {
|
||||
this.loadObjects();
|
||||
}
|
||||
}
|
||||
@@ -193,30 +199,13 @@ export class TsviewS3Keys extends DeesElement {
|
||||
}
|
||||
}
|
||||
|
||||
private getFileName(path: string): string {
|
||||
const parts = path.replace(/\/$/, '').split('/');
|
||||
return parts[parts.length - 1] || path;
|
||||
}
|
||||
|
||||
private formatSize(bytes?: number): string {
|
||||
if (!bytes) return '-';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
return `${size.toFixed(unitIndex > 0 ? 1 : 0)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
private get filteredItems() {
|
||||
const filter = this.filterText.toLowerCase();
|
||||
const folders = this.prefixes
|
||||
.filter((p) => !filter || this.getFileName(p).toLowerCase().includes(filter))
|
||||
.filter((p) => !filter || getFileName(p).toLowerCase().includes(filter))
|
||||
.map((p) => ({ key: p, isFolder: true, size: undefined }));
|
||||
const files = this.allKeys
|
||||
.filter((o) => !filter || this.getFileName(o.key).toLowerCase().includes(filter))
|
||||
.filter((o) => !filter || getFileName(o.key).toLowerCase().includes(filter))
|
||||
.map((o) => ({ key: o.key, isFolder: false, size: o.size }));
|
||||
return [...folders, ...files];
|
||||
}
|
||||
@@ -267,11 +256,11 @@ export class TsviewS3Keys extends DeesElement {
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
</svg>
|
||||
`}
|
||||
<span class="key-name">${this.getFileName(item.key)}</span>
|
||||
<span class="key-name">${getFileName(item.key)}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="size-cell">
|
||||
${item.isFolder ? '-' : this.formatSize(item.size)}
|
||||
${item.isFolder ? '-' : formatSize(item.size)}
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { apiService } from '../services/index.js';
|
||||
import { formatSize, getFileName } from '../utilities/index.js';
|
||||
import { themeStyles } from '../styles/index.js';
|
||||
|
||||
const { html, css, cssManager, customElement, property, state, DeesElement } = plugins;
|
||||
|
||||
@@ -31,6 +33,7 @@ export class TsviewS3Preview extends DeesElement {
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
@@ -104,9 +107,9 @@ export class TsviewS3Preview extends DeesElement {
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: 8px 16px;
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
border: 1px solid #6366f1;
|
||||
color: #818cf8;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid #404040;
|
||||
color: #e0e0e0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
@@ -114,7 +117,7 @@ export class TsviewS3Preview extends DeesElement {
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: rgba(99, 102, 241, 0.3);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
@@ -174,6 +177,7 @@ export class TsviewS3Preview extends DeesElement {
|
||||
} else {
|
||||
this.content = '';
|
||||
this.contentType = '';
|
||||
this.error = ''; // Clear error when no file selected
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,22 +202,6 @@ export class TsviewS3Preview extends DeesElement {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
private getFileName(path: string): string {
|
||||
const parts = path.split('/');
|
||||
return parts[parts.length - 1] || path;
|
||||
}
|
||||
|
||||
private formatSize(bytes: number): string {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
return `${size.toFixed(unitIndex > 0 ? 1 : 0)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
private formatDate(dateStr: string): string {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
@@ -235,7 +223,13 @@ export class TsviewS3Preview extends DeesElement {
|
||||
|
||||
private getTextContent(): string {
|
||||
try {
|
||||
return atob(this.content);
|
||||
// Properly decode base64 to UTF-8 text
|
||||
const binaryString = atob(this.content);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return new TextDecoder('utf-8').decode(bytes);
|
||||
} catch {
|
||||
return 'Unable to decode content';
|
||||
}
|
||||
@@ -249,7 +243,7 @@ export class TsviewS3Preview extends DeesElement {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = this.getFileName(this.objectKey);
|
||||
a.download = getFileName(this.objectKey);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
@@ -260,7 +254,7 @@ export class TsviewS3Preview extends DeesElement {
|
||||
}
|
||||
|
||||
private async handleDelete() {
|
||||
if (!confirm(`Delete "${this.getFileName(this.objectKey)}"?`)) return;
|
||||
if (!confirm(`Delete "${getFileName(this.objectKey)}"?`)) return;
|
||||
|
||||
try {
|
||||
await apiService.deleteObject(this.bucketName, this.objectKey);
|
||||
@@ -310,10 +304,10 @@ export class TsviewS3Preview extends DeesElement {
|
||||
return html`
|
||||
<div class="preview-container">
|
||||
<div class="preview-header">
|
||||
<div class="preview-title">${this.getFileName(this.objectKey)}</div>
|
||||
<div class="preview-title">${getFileName(this.objectKey)}</div>
|
||||
<div class="preview-meta">
|
||||
<span class="meta-item">${this.contentType}</span>
|
||||
<span class="meta-item">${this.formatSize(this.size)}</span>
|
||||
<span class="meta-item">${formatSize(this.size)}</span>
|
||||
<span class="meta-item">${this.formatDate(this.lastModified)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -153,6 +153,22 @@ export class ApiService {
|
||||
return result.databases;
|
||||
}
|
||||
|
||||
async createDatabase(databaseName: string): Promise<boolean> {
|
||||
const result = await this.request<{ databaseName: string }, { success: boolean }>(
|
||||
'createDatabase',
|
||||
{ databaseName }
|
||||
);
|
||||
return result.success;
|
||||
}
|
||||
|
||||
async dropDatabase(databaseName: string): Promise<boolean> {
|
||||
const result = await this.request<{ databaseName: string }, { success: boolean }>(
|
||||
'dropDatabase',
|
||||
{ databaseName }
|
||||
);
|
||||
return result.success;
|
||||
}
|
||||
|
||||
async listCollections(databaseName: string): Promise<IMongoCollection[]> {
|
||||
const result = await this.request<
|
||||
{ databaseName: string },
|
||||
@@ -169,6 +185,14 @@ export class ApiService {
|
||||
return result.success;
|
||||
}
|
||||
|
||||
async dropCollection(databaseName: string, collectionName: string): Promise<boolean> {
|
||||
const result = await this.request<
|
||||
{ databaseName: string; collectionName: string },
|
||||
{ success: boolean }
|
||||
>('dropCollection', { databaseName, collectionName });
|
||||
return result.success;
|
||||
}
|
||||
|
||||
async findDocuments(
|
||||
databaseName: string,
|
||||
collectionName: string,
|
||||
|
||||
1
ts_web/styles/index.ts
Normal file
1
ts_web/styles/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './theme.js';
|
||||
61
ts_web/styles/theme.ts
Normal file
61
ts_web/styles/theme.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { css } from '@design.estate/dees-element';
|
||||
|
||||
/**
|
||||
* CSS custom properties (design tokens) for tsview components
|
||||
* Add themeStyles to your component's static styles array to use these variables
|
||||
*/
|
||||
export const themeStyles = css`
|
||||
:host {
|
||||
/* Background colors */
|
||||
--tsview-bg-primary: #1a1a1a;
|
||||
--tsview-bg-secondary: #1e1e1e;
|
||||
--tsview-bg-tertiary: #141414;
|
||||
--tsview-bg-overlay: rgba(0, 0, 0, 0.2);
|
||||
--tsview-bg-overlay-dark: rgba(0, 0, 0, 0.3);
|
||||
--tsview-bg-dialog-overlay: rgba(0, 0, 0, 0.7);
|
||||
|
||||
/* Border colors */
|
||||
--tsview-border-primary: #333;
|
||||
--tsview-border-secondary: #444;
|
||||
--tsview-border-tertiary: #2a2a3e;
|
||||
|
||||
/* Text colors */
|
||||
--tsview-text-primary: #fff;
|
||||
--tsview-text-secondary: #e0e0e0;
|
||||
--tsview-text-tertiary: #ccc;
|
||||
--tsview-text-muted: #888;
|
||||
--tsview-text-dim: #666;
|
||||
|
||||
/* Interactive states */
|
||||
--tsview-hover-bg: rgba(255, 255, 255, 0.05);
|
||||
--tsview-hover-bg-strong: rgba(255, 255, 255, 0.1);
|
||||
--tsview-selected-bg: rgba(255, 255, 255, 0.08);
|
||||
--tsview-active-bg: rgba(255, 255, 255, 0.1);
|
||||
|
||||
/* Accent colors */
|
||||
--tsview-accent-folder: #fbbf24;
|
||||
|
||||
/* Danger/destructive actions */
|
||||
--tsview-danger: #ef4444;
|
||||
--tsview-danger-text: #f87171;
|
||||
--tsview-danger-bg: rgba(239, 68, 68, 0.2);
|
||||
--tsview-danger-bg-hover: rgba(239, 68, 68, 0.3);
|
||||
|
||||
/* Success states */
|
||||
--tsview-success: #22c55e;
|
||||
--tsview-success-bg: rgba(34, 197, 94, 0.2);
|
||||
|
||||
/* Border radius */
|
||||
--tsview-radius-sm: 4px;
|
||||
--tsview-radius-md: 6px;
|
||||
--tsview-radius-lg: 8px;
|
||||
--tsview-radius-xl: 12px;
|
||||
|
||||
/* Spacing */
|
||||
--tsview-spacing-xs: 4px;
|
||||
--tsview-spacing-sm: 8px;
|
||||
--tsview-spacing-md: 12px;
|
||||
--tsview-spacing-lg: 16px;
|
||||
--tsview-spacing-xl: 24px;
|
||||
}
|
||||
`;
|
||||
46
ts_web/utilities/formatters.ts
Normal file
46
ts_web/utilities/formatters.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Shared formatting utilities for tsview web components
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format a byte size into a human-readable string
|
||||
* @param bytes - Size in bytes (can be undefined for S3 folders)
|
||||
* @returns Formatted size string (e.g., "1.5 MB")
|
||||
*/
|
||||
export function formatSize(bytes?: number): string {
|
||||
if (bytes === undefined || bytes === null) return '-';
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(unitIndex > 0 ? 1 : 0)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a count into a compact human-readable string
|
||||
* @param count - The count number (can be undefined)
|
||||
* @returns Formatted count string (e.g., "1.5K", "2.3M")
|
||||
*/
|
||||
export function formatCount(count?: number): string {
|
||||
if (count === undefined || count === null) return '';
|
||||
if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
|
||||
if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the file name from a path
|
||||
* @param path - Full file path (e.g., "folder/subfolder/file.txt")
|
||||
* @returns File name (e.g., "file.txt")
|
||||
*/
|
||||
export function getFileName(path: string): string {
|
||||
const parts = path.replace(/\/$/, '').split('/');
|
||||
return parts[parts.length - 1] || path;
|
||||
}
|
||||
1
ts_web/utilities/index.ts
Normal file
1
ts_web/utilities/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './formatters.js';
|
||||
Reference in New Issue
Block a user