feat(tsview): add database and S3 handlers, tswatch/watch scripts, web utilities, assets and release config
BIN
.playwright-mcp/column-test-final.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
.playwright-mcp/column-view-state-2.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
.playwright-mcp/column-view-state-3.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
.playwright-mcp/column-view-state.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
.playwright-mcp/columns-working.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
.playwright-mcp/page-2026-01-24T00-39-11-574Z.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
.playwright-mcp/resizable-columns.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
.playwright-mcp/resized-column.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
.playwright-mcp/scroll-test.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
.playwright-mcp/tsview-columns-before.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
.playwright-mcp/tsview-columns-fixed.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
.playwright-mcp/tsview-columns-navigation.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
.playwright-mcp/tsview-columns-sample-data.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
.playwright-mcp/tsview-fixed-single-instance.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
.playwright-mcp/tsview-mongodb-data.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
.playwright-mcp/tsview-mongodb-view.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
.playwright-mcp/tsview-mongodb.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
.playwright-mcp/tsview-s3-buckets.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
.playwright-mcp/tsview-sample-data-bucket.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
.playwright-mcp/tsview-working.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
.playwright-mcp/two-columns.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
42
changelog.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-01-25 - 1.1.0 - feat(tsview)
|
||||
add database and S3 handlers, tswatch/watch scripts, web utilities, assets and release config
|
||||
|
||||
- Add MongoDB management handlers: createDatabase, dropDatabase, dropCollection (ts/api/handlers.mongodb.ts)
|
||||
- Enhance S3 handlers: logging, listBuckets improvements, expanded content-type map (ts/api/handlers.s3.ts)
|
||||
- Add tswatch support and watcher config in npmextra.json and package.json (scripts: watch, startTs); add @git.zone/tswatch dep
|
||||
- Add web exports for styles and utilities (ts_web/styles/index.ts, ts_web/utilities/index.ts) and minor UI assets (.playwright-mcp images)
|
||||
- Update test to use toEqual and small test cleanup (test/test.tsview.ts)
|
||||
- Update package.json: add startTs/watch scripts and bump @push.rocks/smartbucket patch
|
||||
- Add release/registry and project metadata in npmextra.json for publishing
|
||||
|
||||
## 2026-01-23 - 1.0.0 - initial release: column view UI, S3 integration, and API fixes
|
||||
Initial public release introducing the new column-based UI with resizable columns and horizontal navigation, plus backend fixes for S3 bucket listing and API endpoint handling.
|
||||
|
||||
- feat: Add resizable columns and horizontal scrolling
|
||||
- Columns can be resized by dragging the border between them
|
||||
- Column widths persist during navigation (150px min, 500px max)
|
||||
- Container scrolls horizontally when columns exceed available space
|
||||
- Auto-scrolls to show newly opened columns
|
||||
- Resize handle highlights on hover/active state
|
||||
|
||||
- fix: Column view navigation and state preservation
|
||||
- Preserve previous columns during navigation; only reset columns when the bucket changes, not when the prefix changes
|
||||
- Column navigation now expands horizontally instead of resetting the column stack
|
||||
- Remove navigate event dispatch from column folder selection to preserve column state
|
||||
|
||||
- fix: Duplicate app instance rendering
|
||||
- Prevent duplicate tsview-app rendering by checking if the element already exists in the DOM
|
||||
|
||||
- fix / feat: S3 bucket listing
|
||||
- Implement listBuckets to actually query S3 (was previously hardcoded to return an empty array)
|
||||
- Add @aws-sdk/client-s3 dependency to perform direct S3 operations and return real bucket names
|
||||
|
||||
- fix: Resolve API 404s and improve dev caching
|
||||
- Update ApiService baseUrl to include the /typedrequest path expected by TypedServer
|
||||
- Add noCache option to ViewServer to prevent client caching issues during development
|
||||
- Bump @api.global/typedserver to v8.3.0 (includes noCache feature)
|
||||
|
||||
- chore: initial project scaffold
|
||||
- Initial commit and project scaffolding (summary)
|
||||
2
license
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Lossless GmbH (https://lossless.com)
|
||||
Copyright (c) 2024 Task Venture Capital GmbH (https://task.vc)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -6,18 +6,53 @@
|
||||
"to": "./ts/bundled_ui.ts",
|
||||
"outputMode": "base64ts",
|
||||
"bundler": "esbuild",
|
||||
"includeFiles": ["html/**/*"]
|
||||
"includeFiles": [
|
||||
"html/**/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"@git.zone/tswatch": {
|
||||
"watchers": [
|
||||
{
|
||||
"name": "backend",
|
||||
"watch": ["./ts/**/*", "./cli.ts.js"],
|
||||
"command": "tsbuild && pnpm run startTs",
|
||||
"restart": true
|
||||
},
|
||||
{
|
||||
"name": "frontend",
|
||||
"watch": ["./ts_web/**/*", "./html/**/*"],
|
||||
"command": "tsbundle",
|
||||
"restart": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"@git.zone/tsview": {
|
||||
"port": 3010,
|
||||
"openBrowser": true
|
||||
"killIfBusy": true,
|
||||
"openBrowser": false
|
||||
},
|
||||
"@git.zone/cli": {
|
||||
"services": [
|
||||
"mongodb",
|
||||
"minio"
|
||||
]
|
||||
}
|
||||
],
|
||||
"release": {
|
||||
"registries": [
|
||||
"https://verdaccio.lossless.digital"
|
||||
],
|
||||
"accessLevel": "public"
|
||||
},
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "git.zone",
|
||||
"gitrepo": "tsview",
|
||||
"description": "a dev viewer for mongodb and s3",
|
||||
"npmPackagename": "@git.zone/tsview",
|
||||
"license": "MIT"
|
||||
}
|
||||
},
|
||||
"@ship.zone/szci": {}
|
||||
}
|
||||
@@ -11,7 +11,9 @@
|
||||
"scripts": {
|
||||
"test": "pnpm run build && tstest test/ --verbose",
|
||||
"build": "pnpm run bundle && tsbuild --allowimplicitany",
|
||||
"bundle": "tsbundle"
|
||||
"bundle": "tsbundle",
|
||||
"startTs": "node cli.ts.js",
|
||||
"watch": "tswatch"
|
||||
},
|
||||
"bin": {
|
||||
"tsview": "cli.js"
|
||||
@@ -21,6 +23,7 @@
|
||||
"@git.zone/tsbundle": "^2.8.3",
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tstest": "^3.1.6",
|
||||
"@git.zone/tswatch": "3.0.1",
|
||||
"@types/node": "^25.0.10"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -32,7 +35,7 @@
|
||||
"@design.estate/dees-element": "^2.1.5",
|
||||
"@push.rocks/early": "^4.0.4",
|
||||
"@push.rocks/npmextra": "^5.3.3",
|
||||
"@push.rocks/smartbucket": "^4.3.0",
|
||||
"@push.rocks/smartbucket": "^4.3.1",
|
||||
"@push.rocks/smartcli": "^4.0.20",
|
||||
"@push.rocks/smartdata": "^7.0.15",
|
||||
"@push.rocks/smartfile": "^13.1.2",
|
||||
|
||||
46
pnpm-lock.yaml
generated
@@ -33,8 +33,8 @@ importers:
|
||||
specifier: ^5.3.3
|
||||
version: 5.3.3
|
||||
'@push.rocks/smartbucket':
|
||||
specifier: ^4.3.0
|
||||
version: 4.3.0
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1
|
||||
'@push.rocks/smartcli':
|
||||
specifier: ^4.0.20
|
||||
version: 4.0.20
|
||||
@@ -75,6 +75,9 @@ importers:
|
||||
'@git.zone/tstest':
|
||||
specifier: ^3.1.6
|
||||
version: 3.1.6(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)
|
||||
'@git.zone/tswatch':
|
||||
specifier: 3.0.1
|
||||
version: 3.0.1(@tiptap/pm@2.27.2)
|
||||
'@types/node':
|
||||
specifier: ^25.0.10
|
||||
version: 25.0.10
|
||||
@@ -536,6 +539,10 @@ packages:
|
||||
resolution: {integrity: sha512-xRGc6wO4rJ6mohPCMIBDRH+oNjiIvX6Jeo8v/Y5o5VyKSHFmqol7FCKSBrojMcqgBpESnLHFPJAAOmT9W3JV8Q==}
|
||||
hasBin: true
|
||||
|
||||
'@git.zone/tswatch@3.0.1':
|
||||
resolution: {integrity: sha512-vrAkKM5ff/e1BLNkrIRXnTIkMyjl/uW49c1cYaw2nYGloM6/wT1FSwYjwh6BcDkHIYMnzS30SOy9jSYRptW/iw==}
|
||||
hasBin: true
|
||||
|
||||
'@happy-dom/global-registrator@15.11.7':
|
||||
resolution: {integrity: sha512-mfOoUlIw8VBiJYPrl5RZfMzkXC/z7gbSpi2ecycrj/gRWLq2CMV+Q+0G+JPjeOmuNFgg0skEIzkVFzVYFP6URw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@@ -813,8 +820,8 @@ packages:
|
||||
'@push.rocks/smartbucket@3.3.10':
|
||||
resolution: {integrity: sha512-0H2MioALspC8Aj0Q1FPCs2w4k2u9oJg7Q5yM8+1TZo7aRfrdxgM5HQ7z3apUaqC3ZEDewW6vSlttjHFHhMEC3A==}
|
||||
|
||||
'@push.rocks/smartbucket@4.3.0':
|
||||
resolution: {integrity: sha512-4nstzEduCKou4R5ekKH6kUjDZXWfrtjA1hIQ4MJmTbtncmm2+4+ixjaFThS2nS8Aa+fHcBgOtKkBv8wTsgvK/Q==}
|
||||
'@push.rocks/smartbucket@4.3.1':
|
||||
resolution: {integrity: sha512-fMA8w98/E+usaaLkLm6wDj1XSpR0shTtG8AxTdwWIlH1YemQj/aCf4wReezDxUFVoUpC3HMzzV2RTFtQvHndeQ==}
|
||||
|
||||
'@push.rocks/smartbuffer@3.0.5':
|
||||
resolution: {integrity: sha512-pWYF08Mn8s/KF/9nHRk7pZPzuMjmYVQay2c5gGexdayxn1W4eCSYYhWH73vR2JBfGeGq/izbRNuUuEaIEeTIKA==}
|
||||
@@ -5001,6 +5008,33 @@ snapshots:
|
||||
- utf-8-validate
|
||||
- vue
|
||||
|
||||
'@git.zone/tswatch@3.0.1(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@api.global/typedserver': 8.3.0(@tiptap/pm@2.27.2)
|
||||
'@git.zone/tsbundle': 2.8.3
|
||||
'@git.zone/tsrun': 2.0.1
|
||||
'@push.rocks/early': 4.0.4
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/npmextra': 5.3.3
|
||||
'@push.rocks/smartcli': 4.0.20
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartfs': 1.3.1
|
||||
'@push.rocks/smartinteract': 2.0.16
|
||||
'@push.rocks/smartlog': 3.1.10
|
||||
'@push.rocks/smartlog-destination-local': 9.0.2
|
||||
'@push.rocks/smartshell': 3.3.0
|
||||
'@push.rocks/smartwatch': 6.3.0
|
||||
'@push.rocks/taskbuffer': 3.5.0
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- '@swc/helpers'
|
||||
- '@tiptap/pm'
|
||||
- bufferutil
|
||||
- react
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
- vue
|
||||
|
||||
'@happy-dom/global-registrator@15.11.7':
|
||||
dependencies:
|
||||
happy-dom: 15.11.7
|
||||
@@ -5485,7 +5519,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@push.rocks/smartbucket@4.3.0':
|
||||
'@push.rocks/smartbucket@4.3.1':
|
||||
dependencies:
|
||||
'@aws-sdk/client-s3': 3.975.0
|
||||
'@push.rocks/smartmime': 2.0.4
|
||||
@@ -5941,7 +5975,7 @@ snapshots:
|
||||
|
||||
'@push.rocks/smarts3@3.0.3':
|
||||
dependencies:
|
||||
'@push.rocks/smartbucket': 4.3.0
|
||||
'@push.rocks/smartbucket': 4.3.1
|
||||
'@push.rocks/smartfs': 1.3.1
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
'@push.rocks/smartxml': 2.0.0
|
||||
|
||||
@@ -25,6 +25,12 @@ tsview is a CLI tool for viewing S3 and MongoDB data through a web UI.
|
||||
- Run `pnpm build` to compile TypeScript and bundle web UI
|
||||
- UI is bundled from `ts_web/` to `ts/bundled_ui.ts` as base64
|
||||
|
||||
### Web UI Structure
|
||||
- `ts_web/elements/` - Web components (LitElement-based)
|
||||
- `ts_web/services/` - API service for backend communication
|
||||
- `ts_web/utilities/` - Shared formatting functions (formatSize, formatCount, getFileName)
|
||||
- `ts_web/styles/` - Shared CSS custom properties (themeStyles)
|
||||
|
||||
### TypedRequest Pattern
|
||||
```typescript
|
||||
// Interface definition
|
||||
|
||||
240
readme.md
@@ -1,36 +1,56 @@
|
||||
# @git.zone/tsview
|
||||
|
||||
A CLI tool for viewing S3 and MongoDB data with a web UI.
|
||||
A powerful developer tool for browsing and managing S3-compatible storage and MongoDB databases through a sleek web UI. Built with TypeScript, designed for developers who need quick, visual access to their data stores during development. 🚀
|
||||
|
||||
## Installation
|
||||
## Issue Reporting and Security
|
||||
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
# Global installation (recommended for CLI usage)
|
||||
npm install -g @git.zone/tsview
|
||||
# or
|
||||
pnpm add -g @git.zone/tsview
|
||||
|
||||
# Local installation (for programmatic usage)
|
||||
npm install @git.zone/tsview
|
||||
# or
|
||||
pnpm add @git.zone/tsview
|
||||
```
|
||||
|
||||
## Usage
|
||||
## Features ✨
|
||||
|
||||
### CLI
|
||||
### 🗄️ S3 Storage Browser
|
||||
- **Column View Navigation** - Mac Finder-style interface with resizable columns for intuitive file browsing
|
||||
- **List View** - Traditional key-based view with hierarchical navigation
|
||||
- **Real-time Preview** - View images, JSON, text files, and more directly in the browser
|
||||
- **Bucket Management** - Create, delete, and switch between buckets
|
||||
- **File Operations** - Upload, download, delete objects with ease
|
||||
- **Smart Content Type Detection** - Automatic content type recognition for 20+ file types
|
||||
- **Breadcrumb Navigation** - Easy path traversal with clickable breadcrumbs
|
||||
|
||||
```bash
|
||||
# Start viewer (auto-finds free port from 3010+)
|
||||
tsview
|
||||
### 🍃 MongoDB Browser
|
||||
- **Database Explorer** - Hierarchical navigation through databases and collections
|
||||
- **Document Viewer** - Paginated table view with sorting and filtering
|
||||
- **Document Editor** - Full CRUD operations with JSON syntax highlighting
|
||||
- **Index Management** - View, create, and drop indexes
|
||||
- **Aggregation Pipeline** - Run custom aggregation queries (coming soon)
|
||||
- **Collection Stats** - View document counts, sizes, and storage metrics
|
||||
- **Server Status** - Monitor connection info and server health
|
||||
|
||||
# Force specific port
|
||||
tsview --port 3000
|
||||
### 🎨 Modern Web UI
|
||||
- 🌙 Dark theme designed for developer comfort
|
||||
- 📱 Responsive layout with resizable panels
|
||||
- ⌨️ Keyboard-friendly navigation
|
||||
- 🔌 Zero external runtime dependencies in the browser
|
||||
|
||||
# S3 viewer only
|
||||
tsview s3
|
||||
## Quick Start 🚀
|
||||
|
||||
# MongoDB viewer only
|
||||
tsview mongo
|
||||
```
|
||||
### 1. Configure Your Connection
|
||||
|
||||
### Configuration
|
||||
|
||||
tsview reads configuration from `.nogit/env.json` (the same format used by `gitzone service`):
|
||||
Create a `.nogit/env.json` file in your project root:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -44,17 +64,67 @@ tsview reads configuration from `.nogit/env.json` (the same format used by `gitz
|
||||
}
|
||||
```
|
||||
|
||||
### Programmatic API
|
||||
### 2. Launch the Viewer
|
||||
|
||||
```bash
|
||||
tsview
|
||||
```
|
||||
|
||||
That's it! 🎉 Your browser will automatically open to the viewer interface.
|
||||
|
||||
## CLI Usage
|
||||
|
||||
```bash
|
||||
# Start viewer with auto-detected port (starts from 3010)
|
||||
tsview
|
||||
|
||||
# Force a specific port
|
||||
tsview --port 3000
|
||||
|
||||
# S3 browser only
|
||||
tsview s3
|
||||
|
||||
# MongoDB browser only
|
||||
tsview mongo
|
||||
# or
|
||||
tsview mongodb
|
||||
```
|
||||
|
||||
## Configuration via npmextra.json
|
||||
|
||||
For project-level configuration, add a `@git.zone/tsview` section to your `npmextra.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"@git.zone/tsview": {
|
||||
"port": 3015,
|
||||
"killIfBusy": true,
|
||||
"openBrowser": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `port` | `number` | auto | Fixed port to use (auto-finds from 3010 if not set) |
|
||||
| `killIfBusy` | `boolean` | `false` | Kill existing process if port is busy |
|
||||
| `openBrowser` | `boolean` | `true` | Automatically open browser on start |
|
||||
|
||||
**Priority order:** CLI `--port` flag > `npmextra.json` config > auto-detect
|
||||
|
||||
## Programmatic API
|
||||
|
||||
Use tsview as a library in your own tools:
|
||||
|
||||
```typescript
|
||||
import { TsView } from '@git.zone/tsview';
|
||||
|
||||
const viewer = new TsView();
|
||||
|
||||
// Option 1: Load from env.json (gitzone service)
|
||||
// Option 1: Load from .nogit/env.json (gitzone service format)
|
||||
await viewer.loadConfigFromEnv();
|
||||
|
||||
// Option 2: Custom local config (MinIO + local MongoDB)
|
||||
// Option 2: Configure programmatically for local development
|
||||
viewer.setS3Config({
|
||||
endpoint: 'localhost',
|
||||
port: 9000,
|
||||
@@ -63,61 +133,145 @@ viewer.setS3Config({
|
||||
useSsl: false
|
||||
});
|
||||
|
||||
// Option 3: Cloud config (AWS S3 + MongoDB Atlas)
|
||||
viewer.setMongoConfig({
|
||||
mongoDbUrl: 'mongodb://localhost:27017',
|
||||
mongoDbName: 'mydb'
|
||||
});
|
||||
|
||||
// Option 3: Configure for cloud services
|
||||
viewer.setS3Config({
|
||||
endpoint: 's3.amazonaws.com',
|
||||
accessKey: 'AKIAXXXXXXX',
|
||||
accessSecret: 'secret',
|
||||
accessSecret: 'your-secret-key',
|
||||
useSsl: true,
|
||||
region: 'us-east-1'
|
||||
});
|
||||
|
||||
viewer.setMongoConfig({
|
||||
mongoDbUrl: 'mongodb+srv://user:pass@cluster.mongodb.net',
|
||||
mongoDbName: 'mydb'
|
||||
mongoDbName: 'production'
|
||||
});
|
||||
|
||||
// Start on auto-found port
|
||||
// Start the server
|
||||
const port = await viewer.start();
|
||||
console.log(`Viewer running on http://localhost:${port}`);
|
||||
|
||||
// Or force specific port
|
||||
// Or specify a port
|
||||
await viewer.start(3500);
|
||||
|
||||
// Stop when done
|
||||
// Graceful shutdown
|
||||
await viewer.stop();
|
||||
```
|
||||
|
||||
## Features
|
||||
## Environment Variables
|
||||
|
||||
### S3 Browser
|
||||
- **Column View**: Mac Finder-style navigation with horizontal columns
|
||||
- **List View**: Flat list of all keys with filtering
|
||||
- **Preview Panel**: View images, text, and JSON files
|
||||
- **Operations**: Download, delete files
|
||||
The following environment variables are supported in `.nogit/env.json`:
|
||||
|
||||
### MongoDB Browser
|
||||
- **Database/Collection Navigation**: Hierarchical sidebar
|
||||
- **Documents Table**: Paginated view with filtering
|
||||
- **Document Editor**: Edit documents with JSON syntax highlighting
|
||||
- **Index Management**: View, create, and drop indexes
|
||||
### S3 Configuration
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `S3_ENDPOINT` | S3 server hostname |
|
||||
| `S3_PORT` | S3 server port (optional) |
|
||||
| `S3_ACCESSKEY` | Access key ID |
|
||||
| `S3_SECRETKEY` | Secret access key |
|
||||
| `S3_USESSL` | Use HTTPS (`true`/`false`) |
|
||||
|
||||
### MongoDB Configuration
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `MONGODB_URL` | Full MongoDB connection string |
|
||||
| `MONGODB_NAME` | Default database name |
|
||||
|
||||
Or use individual MongoDB variables:
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `MONGODB_HOST` | MongoDB hostname |
|
||||
| `MONGODB_PORT` | MongoDB port |
|
||||
| `MONGODB_USER` | Username |
|
||||
| `MONGODB_PASS` | Password |
|
||||
| `MONGODB_NAME` | Database name |
|
||||
|
||||
## Supported S3 Providers
|
||||
|
||||
tsview works with any S3-compatible storage:
|
||||
|
||||
| Provider | Status |
|
||||
|----------|--------|
|
||||
| **MinIO** | ✅ Perfect for local development |
|
||||
| **AWS S3** | ✅ Amazon's object storage |
|
||||
| **DigitalOcean Spaces** | ✅ Simple object storage |
|
||||
| **Backblaze B2** | ✅ S3-compatible API |
|
||||
| **Cloudflare R2** | ✅ Zero egress fees |
|
||||
| **Wasabi** | ✅ Hot cloud storage |
|
||||
| **Self-hosted** | ✅ Any S3-compatible server |
|
||||
|
||||
## Supported File Types for Preview
|
||||
|
||||
| Category | Extensions |
|
||||
|----------|------------|
|
||||
| **Images** | `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.svg` |
|
||||
| **Text** | `.txt`, `.md`, `.log`, `.sh`, `.env` |
|
||||
| **Code** | `.json`, `.js`, `.ts`, `.tsx`, `.jsx`, `.html`, `.css` |
|
||||
| **Data** | `.csv`, `.xml`, `.yaml`, `.yml` |
|
||||
| **Documents** | `.pdf` |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://code.foss.global/git.zone/tsview.git
|
||||
cd tsview
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Build (bundles UI + compiles TypeScript)
|
||||
# Build (bundles frontend + compiles TypeScript)
|
||||
pnpm build
|
||||
|
||||
# Development mode with hot reload
|
||||
pnpm run watch
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
|
||||
# Run in development mode
|
||||
./cli.ts.js
|
||||
```
|
||||
|
||||
## License
|
||||
## Architecture
|
||||
|
||||
MIT License - see [license](./license) for details.
|
||||
```
|
||||
tsview/
|
||||
├── ts/ # Backend TypeScript source
|
||||
│ ├── api/ # TypedRequest API handlers
|
||||
│ │ ├── handlers.s3.ts
|
||||
│ │ └── handlers.mongodb.ts
|
||||
│ ├── config/ # Configuration management
|
||||
│ ├── server/ # Web server (TypedServer)
|
||||
│ ├── interfaces/ # Shared TypeScript interfaces
|
||||
│ └── tsview.classes.tsview.ts # Main class
|
||||
├── ts_web/ # Frontend TypeScript source
|
||||
│ ├── elements/ # Web components (LitElement)
|
||||
│ ├── services/ # API client service
|
||||
│ ├── styles/ # Shared theme styles
|
||||
│ └── utilities/ # Helper functions
|
||||
└── cli.ts.js # CLI entry point
|
||||
```
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||
|
||||
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
|
||||
@@ -37,7 +37,7 @@ tap.test('should have config methods', async () => {
|
||||
|
||||
tap.test('should have runCli export', async () => {
|
||||
expect(tsview.runCli).toBeDefined();
|
||||
expect(typeof tsview.runCli).toBe('function');
|
||||
expect(typeof tsview.runCli).toEqual('function');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* autocreated commitance data by @push.rocks/commitinfo
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tsview',
|
||||
version: '1.0.0',
|
||||
description: 'A CLI tool for viewing S3 and MongoDB data with a web UI',
|
||||
};
|
||||
version: '1.1.0',
|
||||
description: 'A CLI tool for viewing S3 and MongoDB data with a web UI'
|
||||
}
|
||||
|
||||
@@ -87,6 +87,43 @@ export async function registerMongoHandlers(
|
||||
)
|
||||
);
|
||||
|
||||
// Create database (by creating a placeholder collection)
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.IReq_CreateDatabase>(
|
||||
'createDatabase',
|
||||
async (reqData) => {
|
||||
try {
|
||||
const client = await getMongoClient();
|
||||
const db = client.db(reqData.databaseName);
|
||||
// Create a placeholder collection to materialize the database
|
||||
await db.createCollection('_tsview_init');
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Error creating database:', err);
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Drop database
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.IReq_DropDatabase>(
|
||||
'dropDatabase',
|
||||
async (reqData) => {
|
||||
try {
|
||||
const client = await getMongoClient();
|
||||
const db = client.db(reqData.databaseName);
|
||||
await db.dropDatabase();
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Error dropping database:', err);
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Create collection
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.IReq_CreateCollection>(
|
||||
@@ -105,6 +142,24 @@ export async function registerMongoHandlers(
|
||||
)
|
||||
);
|
||||
|
||||
// Drop collection
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.IReq_DropCollection>(
|
||||
'dropCollection',
|
||||
async (reqData) => {
|
||||
try {
|
||||
const client = await getMongoClient();
|
||||
const db = client.db(reqData.databaseName);
|
||||
await db.dropCollection(reqData.collectionName);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Error dropping collection:', err);
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Find documents
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.IReq_FindDocuments>(
|
||||
|
||||
@@ -9,20 +9,29 @@ export async function registerS3Handlers(
|
||||
typedrouter: plugins.typedrequest.TypedRouter,
|
||||
tsview: TsView
|
||||
): Promise<void> {
|
||||
console.log('Registering S3 handlers...');
|
||||
|
||||
// List all buckets
|
||||
console.log('Registering listBuckets handler');
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.IReq_ListBuckets>(
|
||||
'listBuckets',
|
||||
async () => {
|
||||
console.log('listBuckets handler called');
|
||||
const smartbucket = await tsview.getSmartBucket();
|
||||
console.log('smartbucket:', smartbucket ? 'initialized' : 'null');
|
||||
if (!smartbucket) {
|
||||
console.log('returning empty buckets');
|
||||
return { buckets: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
const command = new plugins.s3.ListBucketsCommand({});
|
||||
console.log('sending ListBucketsCommand...');
|
||||
const response = await smartbucket.s3Client.send(command) as plugins.s3.ListBucketsCommandOutput;
|
||||
console.log('response:', response);
|
||||
const buckets = response.Buckets?.map(b => b.Name).filter((name): name is string => !!name) || [];
|
||||
console.log('returning buckets:', buckets);
|
||||
return { buckets };
|
||||
} catch (err) {
|
||||
console.error('Error listing buckets:', err);
|
||||
@@ -169,10 +178,21 @@ export async function registerS3Handlers(
|
||||
'html': 'text/html',
|
||||
'css': 'text/css',
|
||||
'js': 'application/javascript',
|
||||
'ts': 'text/plain',
|
||||
'tsx': 'text/plain',
|
||||
'jsx': 'text/plain',
|
||||
'md': 'text/markdown',
|
||||
'csv': 'text/csv',
|
||||
'yaml': 'text/yaml',
|
||||
'yml': 'text/yaml',
|
||||
'log': 'text/plain',
|
||||
'sh': 'text/plain',
|
||||
'env': 'text/plain',
|
||||
'png': 'image/png',
|
||||
'jpg': 'image/jpeg',
|
||||
'jpeg': 'image/jpeg',
|
||||
'gif': 'image/gif',
|
||||
'webp': 'image/webp',
|
||||
'svg': 'image/svg+xml',
|
||||
'pdf': 'application/pdf',
|
||||
'xml': 'application/xml',
|
||||
@@ -216,11 +236,26 @@ export async function registerS3Handlers(
|
||||
'json': 'application/json',
|
||||
'txt': 'text/plain',
|
||||
'html': 'text/html',
|
||||
'css': 'text/css',
|
||||
'js': 'application/javascript',
|
||||
'ts': 'text/plain',
|
||||
'tsx': 'text/plain',
|
||||
'jsx': 'text/plain',
|
||||
'md': 'text/markdown',
|
||||
'csv': 'text/csv',
|
||||
'yaml': 'text/yaml',
|
||||
'yml': 'text/yaml',
|
||||
'log': 'text/plain',
|
||||
'sh': 'text/plain',
|
||||
'env': 'text/plain',
|
||||
'png': 'image/png',
|
||||
'jpg': 'image/jpeg',
|
||||
'jpeg': 'image/jpeg',
|
||||
'gif': 'image/gif',
|
||||
'webp': 'image/webp',
|
||||
'svg': 'image/svg+xml',
|
||||
'pdf': 'application/pdf',
|
||||
'xml': 'application/xml',
|
||||
};
|
||||
const contentType = contentTypeMap[ext] || 'application/octet-stream';
|
||||
|
||||
|
||||
@@ -28,6 +28,15 @@ export interface ITsViewConfig {
|
||||
mongo?: IMongoConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration from npmextra.json for @git.zone/tsview
|
||||
*/
|
||||
export interface INpmextraConfig {
|
||||
port?: number; // Fixed port to use (optional)
|
||||
killIfBusy?: boolean; // Kill process on port if busy (default: false)
|
||||
openBrowser?: boolean; // Open browser on start (default: true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Environment configuration from .nogit/env.json (gitzone service format)
|
||||
*/
|
||||
@@ -229,6 +238,32 @@ export interface IReq_ListCollections extends plugins.typedrequestInterfaces.imp
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_CreateDatabase extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_CreateDatabase
|
||||
> {
|
||||
method: 'createDatabase';
|
||||
request: {
|
||||
databaseName: string;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_DropDatabase extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_DropDatabase
|
||||
> {
|
||||
method: 'dropDatabase';
|
||||
request: {
|
||||
databaseName: string;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_CreateCollection extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_CreateCollection
|
||||
@@ -243,6 +278,20 @@ export interface IReq_CreateCollection extends plugins.typedrequestInterfaces.im
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_DropCollection extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_DropCollection
|
||||
> {
|
||||
method: 'dropCollection';
|
||||
request: {
|
||||
databaseName: string;
|
||||
collectionName: string;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_FindDocuments extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_FindDocuments
|
||||
|
||||
@@ -23,15 +23,6 @@ export class ViewServer {
|
||||
* Start the server
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
// Register API handlers
|
||||
if (this.tsview.config.hasS3()) {
|
||||
await registerS3Handlers(this.typedrouter, this.tsview);
|
||||
}
|
||||
|
||||
if (this.tsview.config.hasMongo()) {
|
||||
await registerMongoHandlers(this.typedrouter, this.tsview);
|
||||
}
|
||||
|
||||
// Create typed server with bundled content
|
||||
this.typedServer = new plugins.typedserver.TypedServer({
|
||||
cors: true,
|
||||
@@ -41,8 +32,14 @@ export class ViewServer {
|
||||
noCache: true,
|
||||
});
|
||||
|
||||
// Add the router
|
||||
this.typedServer.typedrouter.addTypedRouter(this.typedrouter);
|
||||
// Register API handlers directly to server's router
|
||||
if (this.tsview.config.hasS3()) {
|
||||
await registerS3Handlers(this.typedServer.typedrouter, this.tsview);
|
||||
}
|
||||
|
||||
if (this.tsview.config.hasMongo()) {
|
||||
await registerMongoHandlers(this.typedServer.typedrouter, this.tsview);
|
||||
}
|
||||
|
||||
// Start server
|
||||
await this.typedServer.start();
|
||||
|
||||
@@ -3,6 +3,10 @@ import * as paths from './paths.js';
|
||||
import type * as interfaces from './interfaces/index.js';
|
||||
import { TsViewConfig } from './config/index.js';
|
||||
import { ViewServer } from './server/index.js';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
/**
|
||||
* Main TsView class.
|
||||
@@ -99,25 +103,88 @@ export class TsView {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the viewer server
|
||||
* @param port - Optional port number (if not provided, finds a free port from 3010+)
|
||||
* Load configuration from npmextra.json
|
||||
*/
|
||||
public async start(port?: number): Promise<number> {
|
||||
const actualPort = port ?? await this.findFreePort(3010);
|
||||
private loadNpmextraConfig(cwd?: string): interfaces.INpmextraConfig {
|
||||
const npmextra = new plugins.npmextra.Npmextra(cwd || process.cwd());
|
||||
const config = npmextra.dataFor<interfaces.INpmextraConfig>('@git.zone/tsview', {});
|
||||
return config || {};
|
||||
}
|
||||
|
||||
this.server = new ViewServer(this, actualPort);
|
||||
await this.server.start();
|
||||
|
||||
console.log(`TsView server started on http://localhost:${actualPort}`);
|
||||
|
||||
// Open browser
|
||||
/**
|
||||
* Kill process running on the specified port
|
||||
*/
|
||||
private async killProcessOnPort(port: number): Promise<void> {
|
||||
try {
|
||||
await plugins.smartopen.openUrl(`http://localhost:${actualPort}`);
|
||||
} catch (err) {
|
||||
// Ignore browser open errors
|
||||
// Get PID using lsof (works on Linux and macOS)
|
||||
const { stdout } = await execAsync(`lsof -ti :${port}`);
|
||||
const pids = stdout.trim().split('\n').filter(Boolean);
|
||||
for (const pid of pids) {
|
||||
console.log(`Killing process ${pid} on port ${port}`);
|
||||
await execAsync(`kill -9 ${pid}`);
|
||||
}
|
||||
// Brief wait for port to be released
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
} catch (e) {
|
||||
// No process on port or lsof not available, ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the viewer server
|
||||
* @param cliPort - Optional port number from CLI (highest priority)
|
||||
*/
|
||||
public async start(cliPort?: number): Promise<number> {
|
||||
const npmextraConfig = await this.loadNpmextraConfig();
|
||||
|
||||
let port: number;
|
||||
let portWasExplicitlySet = false;
|
||||
|
||||
if (cliPort) {
|
||||
// CLI has highest priority
|
||||
port = cliPort;
|
||||
portWasExplicitlySet = true;
|
||||
} else if (npmextraConfig.port) {
|
||||
// Config port specified
|
||||
port = npmextraConfig.port;
|
||||
portWasExplicitlySet = true;
|
||||
} else {
|
||||
// Auto-find free port
|
||||
port = await this.findFreePort(3010);
|
||||
}
|
||||
|
||||
return actualPort;
|
||||
// Check if port is busy and handle accordingly
|
||||
const network = new plugins.smartnetwork.SmartNetwork();
|
||||
const isFree = await network.isLocalPortUnused(port);
|
||||
|
||||
if (!isFree) {
|
||||
if (npmextraConfig.killIfBusy) {
|
||||
console.log(`Port ${port} is busy. Killing existing process...`);
|
||||
await this.killProcessOnPort(port);
|
||||
} else if (portWasExplicitlySet) {
|
||||
throw new Error(`Port ${port} is busy. Set "killIfBusy": true in npmextra.json to auto-kill, or use a different port.`);
|
||||
} else {
|
||||
// Auto port was already free, shouldn't happen, but fallback
|
||||
port = await this.findFreePort(port + 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.server = new ViewServer(this, port);
|
||||
await this.server.start();
|
||||
|
||||
console.log(`TsView server started on http://localhost:${port}`);
|
||||
|
||||
// Open browser (default: true, can be disabled via config)
|
||||
const shouldOpenBrowser = npmextraConfig.openBrowser !== false;
|
||||
if (shouldOpenBrowser) {
|
||||
try {
|
||||
await plugins.smartopen.openUrl(`http://localhost:${port}`);
|
||||
} catch (err) {
|
||||
// Ignore browser open errors
|
||||
}
|
||||
}
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
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
@@ -0,0 +1 @@
|
||||
export * from './theme.js';
|
||||
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
@@ -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
@@ -0,0 +1 @@
|
||||
export * from './formatters.js';
|
||||