5 Commits

Author SHA1 Message Date
ad8529cb0f v1.7.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 13m41s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-01-27 15:51:19 +00:00
7cef6f89d9 feat(s3): add drag-and-drop and context-menu file uploads, inline text editing in preview, and increase preview width 2026-01-27 15:51:19 +00:00
81d7ff0722 fix(build): update bundled UI after rebuild 2026-01-26 12:49:23 +00:00
856f13f2ad v1.6.1
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 14m47s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-01-26 12:48:50 +00:00
f7cd43933f fix(ci): add Gitea CI workflows, documentation updates, and packaging metadata tweaks 2026-01-26 12:48:50 +00:00
17 changed files with 694 additions and 160 deletions

View File

@@ -0,0 +1,66 @@
name: Default (not tags)
on:
push:
tags-ignore:
- '**'
env:
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${-{gitea.repository_owner}-}:${-{secrets.GITEA_TOKEN}-}@{{module.githost}}/${-{gitea.repository}-}.git
NPMCI_TOKEN_NPM: ${-{secrets.NPMCI_TOKEN_NPM}-}
NPMCI_TOKEN_NPM2: ${-{secrets.NPMCI_TOKEN_NPM2}-}
NPMCI_GIT_GITHUBTOKEN: ${-{secrets.NPMCI_GIT_GITHUBTOKEN}-}
NPMCI_URL_CLOUDLY: ${-{secrets.NPMCI_URL_CLOUDLY}-}
jobs:
security:
runs-on: ubuntu-latest
continue-on-error: true
container:
image: ${-{ env.IMAGE }-}
steps:
- uses: actions/checkout@v3
- name: Install pnpm and npmci
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
- name: Run npm prepare
run: npmci npm prepare
- name: Audit production dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --prod
continue-on-error: true
- name: Audit development dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --dev
continue-on-error: true
test:
if: ${-{ always() }-}
needs: security
runs-on: ubuntu-latest
container:
image: ${-{ env.IMAGE }-}
steps:
- uses: actions/checkout@v3
- name: Test stable
run: |
npmci node install stable
npmci npm install
npmci npm test
- name: Test build
run: |
npmci node install stable
npmci npm install
npmci npm build

View File

@@ -0,0 +1,124 @@
name: Default (tags)
on:
push:
tags:
- '*'
env:
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${-{gitea.repository_owner}-}:${-{secrets.GITEA_TOKEN}-}@{{module.githost}}/${-{gitea.repository}-}.git
NPMCI_TOKEN_NPM: ${-{secrets.NPMCI_TOKEN_NPM}-}
NPMCI_TOKEN_NPM2: ${-{secrets.NPMCI_TOKEN_NPM2}-}
NPMCI_GIT_GITHUBTOKEN: ${-{secrets.NPMCI_GIT_GITHUBTOKEN}-}
NPMCI_URL_CLOUDLY: ${-{secrets.NPMCI_URL_CLOUDLY}-}
jobs:
security:
runs-on: ubuntu-latest
continue-on-error: true
container:
image: ${-{ env.IMAGE }-}
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Audit production dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --prod
continue-on-error: true
- name: Audit development dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --dev
continue-on-error: true
test:
if: ${-{ always() }-}
needs: security
runs-on: ubuntu-latest
container:
image: ${-{ env.IMAGE }-}
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Test stable
run: |
npmci node install stable
npmci npm install
npmci npm test
- name: Test build
run: |
npmci node install stable
npmci npm install
npmci npm build
release:
needs: test
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
container:
image: ${-{ env.IMAGE }-}
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Release
run: |
npmci node install stable
npmci npm publish
metadata:
needs: test
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
container:
image: ${-{ env.IMAGE }-}
continue-on-error: true
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Code quality
run: |
npmci command npm install -g typescript
npmci npm install
- name: Trigger
run: npmci trigger
- name: Build docs and upload artifacts
run: |
npmci node install stable
npmci npm install
pnpm install -g @git.zone/tsdoc
npmci command tsdoc
continue-on-error: true

2
.gitignore vendored
View File

@@ -20,4 +20,4 @@ dist_*/
.claude/
.serena/
#------# custom
#------# custom

11
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "npm test",
"name": "Run npm test",
"request": "launch",
"type": "node-terminal"
}
]
}

26
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,26 @@
{
"json.schemas": [
{
"fileMatch": ["/npmextra.json"],
"schema": {
"type": "object",
"properties": {
"npmci": {
"type": "object",
"description": "settings for npmci"
},
"gitzone": {
"type": "object",
"description": "settings for gitzone",
"properties": {
"projectType": {
"type": "string",
"enum": ["website", "element", "service", "npm", "wcc"]
}
}
}
}
}
}
]
}

View File

@@ -1,6 +1,27 @@
# Changelog
## 2026-01-27 - 1.7.0 - feat(s3)
add drag-and-drop and context-menu file uploads, inline text editing in preview, and increase preview width
- Add drag-and-drop uploading into columns and folders with visual drag-over / drag-target states and a delayed folder-hover target
- Add context-menu 'Upload Files' action and hidden file input to trigger multi-file uploads; show upload overlay with progress and refresh column after upload
- Implement upload handling using apiService.putObject and base64 file reads, with upload progress tracking and error logging
- Add inline text editing in the preview (Edit / Save / Discard flow) using dees-input-code for editing and dees-preview for read-only display
- Increase preview default width from 350px to 700px and raise max resize limit from 600px to 1000px
- Bump dependencies: @git.zone/tstest -> ^3.1.8, @design.estate/dees-catalog -> ^3.41.1, @design.estate/dees-element -> ^2.1.6
## 2026-01-26 - 1.6.1 - fix(ci)
add Gitea CI workflows, documentation updates, and packaging metadata tweaks
- Add .gitea workflow files for tag and non-tag pushes (security, test, release, metadata jobs)
- Add buildDocs script (tsdoc) and add bugs/homepage/pnpm.overrides entries to package.json
- Update README and readme.hints.md with real-time streaming docs, formatting/table fixes, and examples punctuation fixes
- Tidy npmextra.json formatting and array inline style changes
- Update changelog.md entries and correct trailing newline/formatting
- Minor .gitignore whitespace fix
## 2026-01-25 - 1.6.0 - feat(readme)
document real-time change streaming and expand README with features, architecture, and configuration updates
- Add Real-Time Change Streaming section: MongoDB change streams, S3 polling (ETag), activity stream, WebSocket subscriptions, and auto-reconnect behavior
@@ -10,6 +31,7 @@ document real-time change streaming and expand README with features, architectur
- Minor wording, formatting, and installation clarifications (prefer pnpm examples, fix LICENSE filename case)
## 2026-01-25 - 1.5.0 - feat(streaming)
add real-time streaming (MongoDB change streams & S3 bucket watchers) with WebSocket subscriptions and activity stream UI
- Server: add ChangeStreamManager to manage MongoDB change streams and S3 BucketWatcher subscriptions, handle subscription lifecycle, activity ring buffer and push events via TypedSocket.
@@ -20,6 +42,7 @@ add real-time streaming (MongoDB change streams & S3 bucket watchers) with WebSo
- Docs: update readme.hints.md with Real-Time Streaming architecture, interfaces and dependency notes.
## 2026-01-25 - 1.4.0 - feat(web)
add database overview panel, collection overview and resizable panels; show/hide system databases; use code editor with change-tracking in document view; add getDatabaseStats API and typings; enable overwrite for S3 uploads
- Add backend handler getDatabaseStats + request/response typings (IReq_GetDatabaseStats, IDatabaseStats) and ApiService.getDatabaseStats()
@@ -32,6 +55,7 @@ add database overview panel, collection overview and resizable panels; show/hide
- Minor dependency bumps: @git.zone/tstest and @design.estate/dees-catalog
## 2026-01-25 - 1.3.0 - feat(s3)
add S3 create file/folder dialogs and in-place text editor; export mongodb plugin
- Add mongodb dependency and export mongodb in ts/plugins.ts so ObjectId can be reused from plugins.
@@ -42,6 +66,7 @@ add S3 create file/folder dialogs and in-place text editor; export mongodb plugi
- Various styling and UX improvements for dialogs, buttons, and editor states.
## 2026-01-25 - 1.2.0 - feat(s3,web-ui)
add S3 deletePrefix and getObjectUrl endpoints and add context menus in UI for S3 and Mongo views
- Add server-side TypedHandlers: deletePrefix and getObjectUrl (ts/api/handlers.s3.ts)
@@ -51,24 +76,28 @@ add S3 deletePrefix and getObjectUrl endpoints and add context menus in UI for S
- Switch from inline delete buttons to contextual menus for safer UX; implement downloads via data URLs returned by getObjectUrl and deletion of S3 prefixes (folders)
## 2026-01-25 - 1.1.3 - fix(package)
update package metadata
- metadata-only change; no source code changes
- current version 1.1.2 → recommended patch bump to 1.1.3
## 2026-01-25 - 1.1.2 - fix(package)
apply minor metadata-only change (one-line edit)
- Change affects 1 file with a +1 -1 (metadata-only) — no behavioral changes
- Recommended bump of patch version from 1.1.1 to 1.1.2
## 2026-01-25 - 1.1.1 - fix(tsview)
fix bad build commit - remove accidental include
- Removed an accidental include that caused a bad build and unintended files to be part of the commit
- Patch release recommended from 1.1.0 to 1.1.1
## 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)
@@ -80,6 +109,7 @@ add database and S3 handlers, tswatch/watch scripts, web utilities, assets and r
- 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
@@ -107,4 +137,4 @@ Initial public release introducing the new column-based UI with resizable column
- Bump @api.global/typedserver to v8.3.0 (includes noCache feature)
- chore: initial project scaffold
- Initial commit and project scaffolding (summary)
- Initial commit and project scaffolding (summary)

View File

@@ -6,9 +6,7 @@
"to": "./ts/bundled_ui.ts",
"outputMode": "base64ts",
"bundler": "esbuild",
"includeFiles": [
"html/**/*"
]
"includeFiles": ["html/**/*"]
}
]
},
@@ -29,19 +27,14 @@
]
},
"@git.zone/tsview": {
"port": 3010,
"killIfBusy": true,
"port": 3010,
"killIfBusy": true,
"openBrowser": false
},
"@git.zone/cli": {
"services": [
"mongodb",
"minio"
],
"services": ["mongodb", "minio"],
"release": {
"registries": [
"https://verdaccio.lossless.digital"
],
"registries": ["https://verdaccio.lossless.digital"],
"accessLevel": "public"
},
"projectType": "npm",
@@ -55,4 +48,4 @@
}
},
"@ship.zone/szci": {}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@git.zone/tsview",
"version": "1.6.0",
"version": "1.7.0",
"private": false,
"description": "A CLI tool for viewing S3 and MongoDB data with a web UI",
"main": "dist_ts/index.js",
@@ -13,7 +13,8 @@
"build": "pnpm run bundle && tsbuild --allowimplicitany",
"bundle": "tsbundle",
"startTs": "node cli.ts.js",
"watch": "tswatch"
"watch": "tswatch",
"buildDocs": "tsdoc"
},
"bin": {
"tsview": "cli.js"
@@ -23,7 +24,7 @@
"@git.zone/tsbuild": "^4.1.2",
"@git.zone/tsbundle": "^2.8.3",
"@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.7",
"@git.zone/tstest": "^3.1.8",
"@git.zone/tswatch": "3.0.1",
"@push.rocks/smartrx": "^3.0.10",
"@types/node": "^25.0.10"
@@ -33,8 +34,8 @@
"@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedserver": "^8.3.0",
"@aws-sdk/client-s3": "^3.975.0",
"@design.estate/dees-catalog": "^3.37.1",
"@design.estate/dees-element": "^2.1.5",
"@design.estate/dees-catalog": "^3.41.1",
"@design.estate/dees-element": "^2.1.6",
"@push.rocks/early": "^4.0.4",
"@push.rocks/npmextra": "^5.3.3",
"@push.rocks/smartbucket": "^4.4.1",
@@ -67,5 +68,12 @@
"repository": {
"type": "git",
"url": "https://code.foss.global/git.zone/tsview.git"
},
"bugs": {
"url": "https://code.foss.global/git.zone/tsview/issues"
},
"homepage": "https://code.foss.global/git.zone/tsview#readme",
"pnpm": {
"overrides": {}
}
}

76
pnpm-lock.yaml generated
View File

@@ -21,11 +21,11 @@ importers:
specifier: ^3.975.0
version: 3.975.0
'@design.estate/dees-catalog':
specifier: ^3.37.1
version: 3.37.1(@tiptap/pm@2.27.2)
specifier: ^3.41.1
version: 3.41.1(@tiptap/pm@2.27.2)
'@design.estate/dees-element':
specifier: ^2.1.5
version: 2.1.5
specifier: ^2.1.6
version: 2.1.6
'@push.rocks/early':
specifier: ^4.0.4
version: 4.0.4
@@ -79,8 +79,8 @@ importers:
specifier: ^2.0.1
version: 2.0.1
'@git.zone/tstest':
specifier: ^3.1.7
version: 3.1.7(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)
specifier: ^3.1.8
version: 3.1.8(@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)
@@ -325,26 +325,26 @@ packages:
'@cloudflare/workers-types@4.20260123.0':
resolution: {integrity: sha512-pQccZ8IDLFKkvdKBXZRPkbMtWtS7vVz1giJGkAAZ5cZH2RHK5Bs6p1OoVZA8Z2Sry8Q0tZbZ5Yjud4R7SrG3KQ==}
'@cloudflare/workers-types@4.20260124.0':
resolution: {integrity: sha512-h6TJlew6AtGuEXFc+k5ifalk+tg3fkg0lla6XbMAb2AKKfJGwlFNTwW2xyT/Ha92KY631CIJ+Ace08DPdFohdA==}
'@cloudflare/workers-types@4.20260127.0':
resolution: {integrity: sha512-4M1HLcWViSdT/pAeDGEB5x5P3sqW7UIi34QrBRnxXbqjAY9if8vBU/lWRWnM+UqKzxWGB2LYjEVOzZrp0jZL+w==}
'@configvault.io/interfaces@1.0.17':
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
'@design.estate/dees-catalog@3.37.1':
resolution: {integrity: sha512-NCgzzCG3NJVF7C7aa1nExCMhB+7nA6glFgZpsff32CpvdtbAuBQiuOngU0suVw65uK7Y0a2r/y2CEPGNNmj3TA==}
'@design.estate/dees-catalog@3.41.1':
resolution: {integrity: sha512-AMD0VNLQEWXYRUYWwjLA8K8KEKAiUO7GiriWQm+ld7cj+LrCMsJO0jjVfOCsd4G7fURMqmab9ereBJyxqjoFgQ==}
'@design.estate/dees-comms@1.0.30':
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
'@design.estate/dees-domtools@2.3.7':
resolution: {integrity: sha512-MXoDBrP7JTOpni8b12aFXHJKnKBoQppM8cYBuL9cesRmCVGdB7p39XMRQ7dRyMhmmyr66L3cOczhiCV6febCwg==}
'@design.estate/dees-domtools@2.3.8':
resolution: {integrity: sha512-jUG9GMvPxKMwmRIZ9oLTL3c8hHvHuiwIk8cTrYnuZzGO/uJJ5/czk9o6LRXUuCOOG7TRLtqgOpK8EEQgaadfZA==}
'@design.estate/dees-element@2.1.5':
resolution: {integrity: sha512-czUOFvBiUKi34I+/keDRDc71fuORZS0NfbSuD2jJ4D1ODiTPjaZ6A6SkdQ2QqCEzVsx73XF99Pu8pxPnaOLnHg==}
'@design.estate/dees-element@2.1.6':
resolution: {integrity: sha512-7zyHkUjB8UEQgT9VbB2IJtc/yuPt9CI5JGel3b6BxA1kecY64ceIjFvof1uIkc0QP8q2fMLLY45r1c+9zDTjzg==}
'@design.estate/dees-wcctools@3.7.1':
resolution: {integrity: sha512-BiNWghUoC05RTQOGVCTK+wis6d18LyLY+2p8vHC0q2OBw9hrPoY8k9EplpQgY40MvP0sTXWUwaa7VPXra8ASjA==}
'@design.estate/dees-wcctools@3.8.0':
resolution: {integrity: sha512-CC14iVKUrguzD9jIrdPBd9fZ4egVJEZMxl5y8iy0l7WLumeoYvGsoXj5INVkRPLRVLqziIdi4Je1hXqHt2NU+g==}
'@emnapi/core@1.8.1':
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
@@ -547,8 +547,8 @@ packages:
resolution: {integrity: sha512-NEcnsjvlC1o3Z6SS3VhKCf6Ev+Sh4EAinmggslrIR/ppMrvjDbXNFXoyr3PB+GLeSAR0JRZ1fGvVYjpEzjBdIg==}
hasBin: true
'@git.zone/tstest@3.1.7':
resolution: {integrity: sha512-YCDA+65LJhoY3WJxrNduKlpGf37aq4bFe+fdRqE0dZ2W1f7j3sUunBaBzckShSHKRjkMdPZKr0W0sXFXUK/PcA==}
'@git.zone/tstest@3.1.8':
resolution: {integrity: sha512-nmiLGeOkKMkLDyIk5BUBLx5ExskFbKHKlPdrWCARPVFkU4cAAiuIyJWVfLwISoS0TO/zSInLqArPwIc76yvaNw==}
hasBin: true
'@git.zone/tswatch@3.0.1':
@@ -1924,8 +1924,8 @@ packages:
bare-abort-controller:
optional: true
bare-fs@4.5.2:
resolution: {integrity: sha512-veTnRzkb6aPHOvSKIOy60KzURfBdUflr5VReI+NSaPL6xf+XLdONQgZgpYvUuZLVQ8dCqxpBAudaOM1+KpAUxw==}
bare-fs@4.5.3:
resolution: {integrity: sha512-9+kwVx8QYvt3hPWnmb19tPnh38c6Nihz8Lx3t0g9+4GoIf3/fTgYwM4Z6NxgI+B9elLQA7mLE9PpqcWtOMRDiQ==}
engines: {bare: '>=1.16.0'}
peerDependencies:
bare-buffer: '*'
@@ -4017,7 +4017,7 @@ snapshots:
'@api.global/typedrequest': 3.2.5
'@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 3.1.1(@push.rocks/smartserve@2.0.1)
'@cloudflare/workers-types': 4.20260124.0
'@cloudflare/workers-types': 4.20260127.0
'@design.estate/dees-comms': 1.0.30
'@push.rocks/lik': 6.2.2
'@push.rocks/smartchok': 1.2.0
@@ -4066,7 +4066,7 @@ snapshots:
'@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1)
'@cloudflare/workers-types': 4.20260123.0
'@design.estate/dees-catalog': 3.37.1(@tiptap/pm@2.27.2)
'@design.estate/dees-catalog': 3.41.1(@tiptap/pm@2.27.2)
'@design.estate/dees-comms': 1.0.30
'@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5
@@ -4688,17 +4688,17 @@ snapshots:
'@cloudflare/workers-types@4.20260123.0': {}
'@cloudflare/workers-types@4.20260124.0': {}
'@cloudflare/workers-types@4.20260127.0': {}
'@configvault.io/interfaces@1.0.17':
dependencies:
'@api.global/typedrequest-interfaces': 3.0.19
'@design.estate/dees-catalog@3.37.1(@tiptap/pm@2.27.2)':
'@design.estate/dees-catalog@3.41.1(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-domtools': 2.3.7
'@design.estate/dees-element': 2.1.5
'@design.estate/dees-wcctools': 3.7.1
'@design.estate/dees-domtools': 2.3.8
'@design.estate/dees-element': 2.1.6
'@design.estate/dees-wcctools': 3.8.0
'@fortawesome/fontawesome-svg-core': 7.1.0
'@fortawesome/free-brands-svg-icons': 7.1.0
'@fortawesome/free-regular-svg-icons': 7.1.0
@@ -4736,7 +4736,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5
broadcast-channel: 7.3.0
'@design.estate/dees-domtools@2.3.7':
'@design.estate/dees-domtools@2.3.8':
dependencies:
'@api.global/typedrequest': 3.2.5
'@design.estate/dees-comms': 1.0.30
@@ -4762,9 +4762,9 @@ snapshots:
- supports-color
- vue
'@design.estate/dees-element@2.1.5':
'@design.estate/dees-element@2.1.6':
dependencies:
'@design.estate/dees-domtools': 2.3.7
'@design.estate/dees-domtools': 2.3.8
'@push.rocks/isounique': 1.0.5
'@push.rocks/smartrx': 3.0.10
lit: 3.3.2
@@ -4774,10 +4774,10 @@ snapshots:
- supports-color
- vue
'@design.estate/dees-wcctools@3.7.1':
'@design.estate/dees-wcctools@3.8.0':
dependencies:
'@design.estate/dees-domtools': 2.3.7
'@design.estate/dees-element': 2.1.5
'@design.estate/dees-domtools': 2.3.8
'@design.estate/dees-element': 2.1.6
'@push.rocks/smartdelay': 3.0.5
lit: 3.3.2
transitivePeerDependencies:
@@ -4973,7 +4973,7 @@ snapshots:
'@push.rocks/smartshell': 3.3.0
tsx: 4.21.0
'@git.zone/tstest@3.1.7(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)':
'@git.zone/tstest@3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)':
dependencies:
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
'@git.zone/tsbundle': 2.8.3
@@ -5882,7 +5882,7 @@ snapshots:
'@push.rocks/smartntml@2.0.8':
dependencies:
'@design.estate/dees-element': 2.1.5
'@design.estate/dees-element': 2.1.6
'@happy-dom/global-registrator': 15.11.7
'@push.rocks/smartpromise': 4.2.3
fake-indexeddb: 6.2.5
@@ -6130,7 +6130,7 @@ snapshots:
'@push.rocks/taskbuffer@3.5.0':
dependencies:
'@design.estate/dees-element': 2.1.5
'@design.estate/dees-element': 2.1.6
'@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.10
@@ -7139,7 +7139,7 @@ snapshots:
bare-events@2.8.2: {}
bare-fs@4.5.2:
bare-fs@4.5.3:
dependencies:
bare-events: 2.8.2
bare-path: 3.0.0
@@ -9319,7 +9319,7 @@ snapshots:
pump: 3.0.3
tar-stream: 3.1.7
optionalDependencies:
bare-fs: 4.5.2
bare-fs: 4.5.3
bare-path: 3.0.0
transitivePeerDependencies:
- bare-abort-controller

View File

@@ -1,37 +1,44 @@
# tsview - Project Hints
## Overview
tsview is a CLI tool for viewing S3 and MongoDB data through a web UI.
## Key Patterns
### Configuration
- Reads from `.nogit/env.json` (created by `gitzone service`)
- Environment variables: MONGODB_URL, S3_HOST, S3_ACCESSKEY, etc.
### CLI Commands
- `tsview` - Start viewer (auto-finds free port from 3010+)
- `tsview --port 3000` - Force specific port
- `tsview s3` - S3 viewer only
- `tsview mongo` - MongoDB viewer only
### Dependencies
- Uses `@push.rocks/smartbucket` for S3 operations
- Uses `@push.rocks/smartdata` for MongoDB operations
- Uses `@api.global/typedserver` + `@api.global/typedrequest` for API
- Uses `@design.estate/dees-catalog` for UI components
### Build Process
- 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
export interface IReq_ListBuckets extends plugins.typedrequest.implementsTR<
@@ -57,27 +64,32 @@ typedrouter.addTypedHandler(
## Real-Time Streaming (v1.5.0+)
### Architecture
- `ts/streaming/` - Backend streaming infrastructure
- `classes.changestream-manager.ts` - Manages MongoDB and S3 watchers
- `interfaces.streaming.ts` - TypedRequest interfaces for subscriptions
- `ts_web/services/changestream.service.ts` - Frontend WebSocket client
### MongoDB Change Streams
- Uses native MongoDB Change Streams via `SmartdataDb.mongoDbClient`
- Subscription per collection: `db/collection`
- Events: insert, update, delete, replace, drop
### S3 Change Detection
- Uses `@push.rocks/smartbucket` BucketWatcher (polling-based)
- Polling interval: 5 seconds
- Events: add, modify, delete
### Frontend Components
- `tsview-activity-stream.ts` - Combined activity view with filtering
- MongoDB/S3 browsers auto-subscribe to current resource
- Visual indicators for "Live" status and recent change count
### Key Streaming Interfaces
```typescript
// Subscribe to collection changes
IReq_SubscribeMongo: { database, collection } -> { success, subscriptionId }
@@ -95,5 +107,6 @@ IReq_PushActivityEvent: { event: IActivityEvent } -> { received }
```
### Dependencies Added
- `@api.global/typedsocket` - WebSocket client/server
- `@push.rocks/smartrx` - RxJS utilities

View File

@@ -19,6 +19,7 @@ pnpm add @git.zone/tsview
## Features ✨
### 🗄️ S3 Storage Browser
- **Column View Navigation** — Mac Finder-style interface with resizable columns
- **List View** — Traditional key-based view with hierarchical navigation
- **Real-time Preview** — View images, JSON, text files, code, and more directly in the browser
@@ -29,6 +30,7 @@ pnpm add @git.zone/tsview
- **Breadcrumb Navigation** — Clickable path traversal
### 🍃 MongoDB Browser
- **Database Explorer** — Hierarchical navigation through databases and collections
- **Database Overview** — Collection counts, data sizes, index stats at a glance
- **Document Viewer** — Paginated table view with JSON filter support
@@ -39,6 +41,7 @@ pnpm add @git.zone/tsview
- **Show/Hide System Databases** — Toggle visibility of `admin`, `local`, `config`
### ⚡ Real-Time Change Streaming
- **MongoDB Change Streams** — Live updates via native MongoDB change streams
- **S3 Change Detection** — Polling-based bucket monitoring with ETag comparison (5s intervals)
- **Activity Stream** — Combined timeline of all changes from both sources, filterable by type
@@ -47,6 +50,7 @@ pnpm add @git.zone/tsview
- **Auto-Reconnect** — Subscriptions automatically restored after connection loss
### 🎨 Modern Web UI
- 🌙 Dark theme designed for developer comfort
- 📱 Responsive layout with resizable panels
- ⌨️ Context menus for quick actions
@@ -114,12 +118,12 @@ viewer.setS3Config({
port: 9000,
accessKey: 'minioadmin',
accessSecret: 'minioadmin',
useSsl: false
useSsl: false,
});
viewer.setMongoConfig({
mongoDbUrl: 'mongodb://localhost:27017',
mongoDbName: 'mydb'
mongoDbName: 'mydb',
});
// Option 3: Cloud services
@@ -128,12 +132,12 @@ viewer.setS3Config({
accessKey: 'AKIAXXXXXXX',
accessSecret: 'your-secret-key',
useSsl: true,
region: 'us-east-1'
region: 'us-east-1',
});
viewer.setMongoConfig({
mongoDbUrl: 'mongodb+srv://user:pass@cluster.mongodb.net',
mongoDbName: 'production'
mongoDbName: 'production',
});
// Start the server
@@ -161,11 +165,11 @@ await viewer.stop();
}
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `port` | `number` | auto | Fixed port (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 |
| Option | Type | Default | Description |
| ------------- | --------- | ------- | -------------------------------------------- |
| `port` | `number` | auto | Fixed port (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 |
**Port priority:** CLI `--port` flag → `npmextra.json` → auto-detect
@@ -173,54 +177,54 @@ await viewer.stop();
#### S3
| Variable | Description |
|----------|-------------|
| `S3_ENDPOINT` | S3-compatible server hostname |
| `S3_PORT` | Server port (optional) |
| `S3_ACCESSKEY` | Access key ID |
| `S3_SECRETKEY` | Secret access key |
| `S3_USESSL` | Use HTTPS (`true`/`false`) |
| Variable | Description |
| -------------- | ----------------------------- |
| `S3_ENDPOINT` | S3-compatible server hostname |
| `S3_PORT` | Server port (optional) |
| `S3_ACCESSKEY` | Access key ID |
| `S3_SECRETKEY` | Secret access key |
| `S3_USESSL` | Use HTTPS (`true`/`false`) |
#### MongoDB
| Variable | Description |
|----------|-------------|
| `MONGODB_URL` | Full connection string (preferred) |
| `MONGODB_NAME` | Default database name |
| Variable | Description |
| -------------- | ---------------------------------- |
| `MONGODB_URL` | Full connection string (preferred) |
| `MONGODB_NAME` | Default database name |
Or use individual variables:
| Variable | Description |
|----------|-------------|
| `MONGODB_HOST` | Hostname |
| `MONGODB_PORT` | Port |
| `MONGODB_USER` | Username |
| `MONGODB_PASS` | Password |
| Variable | Description |
| -------------- | ------------- |
| `MONGODB_HOST` | Hostname |
| `MONGODB_PORT` | 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 dev |
| **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 |
| Provider | Status |
| ----------------------- | --------------------------- |
| **MinIO** | ✅ Perfect for local dev |
| **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` |
| 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` |
## Architecture

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/tsview',
version: '1.6.0',
version: '1.7.0',
description: 'A CLI tool for viewing S3 and MongoDB data with a web UI'
}

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/tsview',
version: '1.6.0',
version: '1.7.0',
description: 'A CLI tool for viewing S3 and MongoDB data with a web UI'
}

View File

@@ -24,7 +24,7 @@ export class TsviewS3Browser extends DeesElement {
private accessor refreshKey: number = 0;
@state()
private accessor previewWidth: number = 350;
private accessor previewWidth: number = 700;
@state()
private accessor isResizingPreview: boolean = false;
@@ -123,7 +123,7 @@ export class TsviewS3Browser extends DeesElement {
}
.content.has-preview {
grid-template-columns: 1fr 4px var(--preview-width, 350px);
grid-template-columns: 1fr 4px var(--preview-width, 700px);
}
.resize-divider {
@@ -305,7 +305,7 @@ export class TsviewS3Browser extends DeesElement {
const contentEl = this.shadowRoot?.querySelector('.content');
if (!contentEl) return;
const containerRect = contentEl.getBoundingClientRect();
const newWidth = Math.min(Math.max(containerRect.right - e.clientX, 250), 600);
const newWidth = Math.min(Math.max(containerRect.right - e.clientX, 250), 1000);
this.previewWidth = newWidth;
};

View File

@@ -43,10 +43,25 @@ export class TsviewS3Columns extends DeesElement {
@state()
private accessor createDialogName: string = '';
@state()
private accessor dragOverColumnIndex: number = -1;
@state()
private accessor dragOverFolderPrefix: string | null = null;
@state()
private accessor uploading: boolean = false;
@state()
private accessor uploadProgress: { current: number; total: number } | null = null;
private resizing: { columnIndex: number; startX: number; startWidth: number } | null = null;
private readonly DEFAULT_COLUMN_WIDTH = 250;
private readonly MIN_COLUMN_WIDTH = 150;
private readonly MAX_COLUMN_WIDTH = 500;
private dragCounters: Map<number, number> = new Map();
private folderHoverTimer: ReturnType<typeof setTimeout> | null = null;
private fileInputElement: HTMLInputElement | null = null;
public static styles = [
cssManager.defaultStyles,
@@ -77,6 +92,7 @@ export class TsviewS3Columns extends DeesElement {
height: 100%;
flex-shrink: 0;
overflow: hidden;
position: relative;
}
.resize-handle {
@@ -183,6 +199,58 @@ export class TsviewS3Columns extends DeesElement {
color: #666;
}
.column.drag-over {
background: rgba(59, 130, 246, 0.08);
outline: 2px dashed rgba(59, 130, 246, 0.4);
outline-offset: -2px;
}
.column-item.folder.drag-target {
background: rgba(59, 130, 246, 0.2) !important;
outline: 1px solid rgba(59, 130, 246, 0.5);
}
.upload-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
z-index: 10;
border-radius: 4px;
}
.upload-overlay .upload-text {
color: #e0e0e0;
font-size: 13px;
}
.upload-overlay .upload-progress {
color: #888;
font-size: 11px;
}
.drag-hint {
position: absolute;
bottom: 8px;
left: 50%;
transform: translateX(-50%);
background: rgba(59, 130, 246, 0.9);
color: white;
padding: 4px 12px;
border-radius: 4px;
font-size: 11px;
pointer-events: none;
white-space: nowrap;
z-index: 5;
}
.dialog-overlay {
position: fixed;
top: 0;
@@ -288,6 +356,13 @@ export class TsviewS3Columns extends DeesElement {
await this.loadInitialColumn();
}
disconnectedCallback() {
super.disconnectedCallback();
this.clearFolderHover();
this.dragCounters.clear();
if (this.fileInputElement) { this.fileInputElement.remove(); this.fileInputElement = null; }
}
updated(changedProperties: Map<string, unknown>) {
// Only reset columns when bucket changes or refresh is triggered
// Internal folder navigation is handled by selectFolder() which appends columns
@@ -457,6 +532,11 @@ export class TsviewS3Columns extends DeesElement {
iconName: 'lucide:filePlus',
action: async () => this.openCreateDialog('file', prefix),
},
{
name: 'Upload Files',
iconName: 'lucide:upload',
action: async () => this.triggerFileUpload(prefix),
},
{ divider: true },
{
name: 'Delete Folder',
@@ -535,6 +615,11 @@ export class TsviewS3Columns extends DeesElement {
iconName: 'lucide:filePlus',
action: async () => this.openCreateDialog('file', prefix),
},
{
name: 'Upload Files',
iconName: 'lucide:upload',
action: async () => this.triggerFileUpload(prefix),
},
]);
}
@@ -572,6 +657,145 @@ export class TsviewS3Columns extends DeesElement {
return defaults[ext] || '';
}
// --- File upload helpers ---
private ensureFileInput(): HTMLInputElement {
if (!this.fileInputElement) {
this.fileInputElement = document.createElement('input');
this.fileInputElement.type = 'file';
this.fileInputElement.multiple = true;
this.fileInputElement.style.display = 'none';
this.shadowRoot!.appendChild(this.fileInputElement);
}
return this.fileInputElement;
}
private triggerFileUpload(targetPrefix: string) {
const input = this.ensureFileInput();
input.value = '';
const handler = async () => {
input.removeEventListener('change', handler);
if (input.files && input.files.length > 0) {
await this.uploadFiles(Array.from(input.files), targetPrefix);
}
};
input.addEventListener('change', handler);
input.click();
}
private async uploadFiles(files: File[], targetPrefix: string) {
this.uploading = true;
this.uploadProgress = { current: 0, total: files.length };
for (let i = 0; i < files.length; i++) {
const file = files[i];
this.uploadProgress = { current: i + 1, total: files.length };
try {
const base64 = await this.readFileAsBase64(file);
const key = targetPrefix + file.name;
const contentType = file.type || this.getContentType(file.name.split('.').pop()?.toLowerCase() || '');
await apiService.putObject(this.bucketName, key, base64, contentType);
} catch (err) {
console.error(`Failed to upload ${file.name}:`, err);
}
}
this.uploading = false;
this.uploadProgress = null;
await this.refreshColumnByPrefix(targetPrefix);
}
private readFileAsBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result as string;
resolve(result.split(',')[1] || '');
};
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
}
private async refreshColumnByPrefix(prefix: string) {
const columnIndex = this.columns.findIndex(col => col.prefix === prefix);
if (columnIndex === -1) {
await this.loadInitialColumn();
return;
}
try {
const result = await apiService.listObjects(this.bucketName, prefix, '/');
this.columns = this.columns.map((col, i) =>
i === columnIndex ? { ...col, objects: result.objects, prefixes: result.prefixes } : col
);
} catch {
await this.loadInitialColumn();
}
}
// --- Drag-and-drop handlers ---
private handleColumnDragEnter(e: DragEvent, columnIndex: number) {
e.preventDefault();
e.stopPropagation();
const count = (this.dragCounters.get(columnIndex) || 0) + 1;
this.dragCounters.set(columnIndex, count);
this.dragOverColumnIndex = columnIndex;
}
private handleColumnDragOver(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
}
private handleColumnDragLeave(e: DragEvent, columnIndex: number) {
e.stopPropagation();
const count = (this.dragCounters.get(columnIndex) || 1) - 1;
this.dragCounters.set(columnIndex, count);
if (count <= 0) {
this.dragCounters.delete(columnIndex);
if (this.dragOverColumnIndex === columnIndex) this.dragOverColumnIndex = -1;
this.clearFolderHover();
}
}
private handleColumnDrop(e: DragEvent, columnIndex: number) {
e.preventDefault();
e.stopPropagation();
this.dragCounters.clear();
this.dragOverColumnIndex = -1;
const files = e.dataTransfer?.files;
if (!files || files.length === 0) return;
const targetPrefix = this.dragOverFolderPrefix ?? this.columns[columnIndex].prefix;
this.clearFolderHover();
this.uploadFiles(Array.from(files), targetPrefix);
}
private handleFolderDragEnter(e: DragEvent, folderPrefix: string) {
e.stopPropagation();
this.clearFolderHover();
this.folderHoverTimer = setTimeout(() => {
this.dragOverFolderPrefix = folderPrefix;
}, 500);
}
private handleFolderDragLeave(e: DragEvent, folderPrefix: string) {
e.stopPropagation();
if (this.dragOverFolderPrefix === folderPrefix) this.dragOverFolderPrefix = null;
if (this.folderHoverTimer) {
clearTimeout(this.folderHoverTimer);
this.folderHoverTimer = null;
}
}
private clearFolderHover() {
if (this.folderHoverTimer) { clearTimeout(this.folderHoverTimer); this.folderHoverTimer = null; }
this.dragOverFolderPrefix = null;
}
private async handleCreate() {
if (!this.createDialogName.trim()) return;
@@ -675,7 +899,14 @@ export class TsviewS3Columns extends DeesElement {
: this.bucketName;
return html`
<div class="column" style="width: ${column.width}px">
<div
class="column ${this.dragOverColumnIndex === index ? 'drag-over' : ''}"
style="width: ${column.width}px"
@dragenter=${(e: DragEvent) => this.handleColumnDragEnter(e, index)}
@dragover=${(e: DragEvent) => this.handleColumnDragOver(e)}
@dragleave=${(e: DragEvent) => this.handleColumnDragLeave(e, index)}
@drop=${(e: DragEvent) => this.handleColumnDrop(e, index)}
>
<div class="column-header" title=${column.prefix || this.bucketName}>
${headerName}
</div>
@@ -686,9 +917,11 @@ export class TsviewS3Columns extends DeesElement {
${column.prefixes.map(
(prefix) => html`
<div
class="column-item folder ${column.selectedItem === prefix ? 'selected' : ''}"
class="column-item folder ${column.selectedItem === prefix ? 'selected' : ''} ${this.dragOverFolderPrefix === prefix ? 'drag-target' : ''}"
@click=${() => this.selectFolder(index, prefix)}
@contextmenu=${(e: MouseEvent) => this.handleFolderContextMenu(e, index, prefix)}
@dragenter=${(e: DragEvent) => this.handleFolderDragEnter(e, prefix)}
@dragleave=${(e: DragEvent) => this.handleFolderDragLeave(e, prefix)}
>
<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" />
@@ -715,6 +948,21 @@ export class TsviewS3Columns extends DeesElement {
`
)}
</div>
${this.dragOverColumnIndex === index ? html`
<div class="drag-hint">
${this.dragOverFolderPrefix
? `Drop to upload into ${getFileName(this.dragOverFolderPrefix)}`
: 'Drop to upload here'}
</div>
` : ''}
${this.uploading ? html`
<div class="upload-overlay">
<div class="upload-text">Uploading...</div>
${this.uploadProgress ? html`
<div class="upload-progress">${this.uploadProgress.current} / ${this.uploadProgress.total} files</div>
` : ''}
</div>
` : ''}
</div>
`;
}

View File

@@ -28,6 +28,9 @@ export class TsviewS3Preview extends DeesElement {
@state()
private accessor hasChanges: boolean = false;
@state()
private accessor editing: boolean = false;
@state()
private accessor contentType: string = '';
@@ -83,8 +86,12 @@ export class TsviewS3Preview extends DeesElement {
.preview-content {
flex: 1;
overflow: auto;
padding: 12px;
overflow: hidden;
}
.preview-content dees-preview {
width: 100%;
height: 100%;
}
.preview-content.code-editor {
@@ -96,25 +103,6 @@ export class TsviewS3Preview extends DeesElement {
height: 100%;
}
.preview-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 4px;
}
.preview-text {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
color: #ccc;
background: rgba(0, 0, 0, 0.3);
padding: 12px;
border-radius: 6px;
}
.preview-actions {
padding: 12px;
border-top: 1px solid #333;
@@ -225,11 +213,6 @@ export class TsviewS3Preview extends DeesElement {
text-align: center;
}
.binary-preview {
text-align: center;
color: #888;
padding: 24px;
}
`,
];
@@ -243,6 +226,7 @@ export class TsviewS3Preview extends DeesElement {
this.error = '';
this.originalTextContent = '';
this.hasChanges = false;
this.editing = false;
}
}
}
@@ -253,6 +237,7 @@ export class TsviewS3Preview extends DeesElement {
this.loading = true;
this.error = '';
this.hasChanges = false;
this.editing = false;
try {
const result = await apiService.getObject(this.bucketName, this.objectKey);
@@ -395,12 +380,17 @@ export class TsviewS3Preview extends DeesElement {
this.hasChanges = newValue !== this.originalTextContent;
}
private handleEdit() {
this.editing = true;
}
private handleDiscard() {
const codeEditor = this.shadowRoot?.querySelector('dees-input-code') as any;
if (codeEditor) {
codeEditor.value = this.originalTextContent;
}
this.hasChanges = false;
this.editing = false;
}
private async handleSave() {
@@ -428,6 +418,7 @@ export class TsviewS3Preview extends DeesElement {
if (success) {
this.originalTextContent = currentContent;
this.hasChanges = false;
this.editing = false;
// Update the stored content as well
this.content = base64Content;
}
@@ -486,40 +477,60 @@ export class TsviewS3Preview extends DeesElement {
</div>
</div>
<div class="preview-content ${this.isText() ? 'code-editor' : ''}">
${this.isImage()
? html`<img class="preview-image" src="data:${this.contentType};base64,${this.content}" />`
<div class="preview-content ${this.editing ? 'code-editor' : ''}">
${this.editing
? html`
<dees-input-code
.value=${this.originalTextContent}
.language=${this.getLanguage()}
height="100%"
@content-change=${(e: CustomEvent) => this.handleContentChange(e)}
></dees-input-code>
`
: this.isText()
? html`
<dees-input-code
.value=${this.originalTextContent}
<dees-preview
.textContent=${this.originalTextContent}
.filename=${getFileName(this.objectKey)}
.language=${this.getLanguage()}
height="100%"
@content-change=${(e: CustomEvent) => this.handleContentChange(e)}
></dees-input-code>
.showToolbar=${true}
.showFilename=${false}
></dees-preview>
`
: html`
<div class="binary-preview">
<p>Binary file preview not available</p>
<p>Download to view</p>
</div>
`}
<dees-preview
.base64=${this.content}
.mimeType=${this.contentType}
.filename=${getFileName(this.objectKey)}
.showToolbar=${true}
.showFilename=${false}
></dees-preview>
`
}
</div>
<div class="preview-actions">
${this.hasChanges ? html`
<button class="action-btn secondary" @click=${this.handleDiscard}>Discard</button>
<button
class="action-btn primary"
@click=${this.handleSave}
?disabled=${this.saving}
>
${this.saving ? 'Saving...' : 'Save'}
</button>
` : html`
<button class="action-btn" @click=${this.handleDownload}>Download</button>
<button class="action-btn danger" @click=${this.handleDelete}>Delete</button>
`}
${this.editing
? html`
<button class="action-btn secondary" @click=${this.handleDiscard}>
${this.hasChanges ? 'Discard' : 'Cancel'}
</button>
<button
class="action-btn primary"
@click=${this.handleSave}
?disabled=${this.saving || !this.hasChanges}
>
${this.saving ? 'Saving...' : 'Save'}
</button>
`
: html`
${this.isText()
? html`<button class="action-btn" @click=${this.handleEdit}>Edit</button>`
: ''}
<button class="action-btn" @click=${this.handleDownload}>Download</button>
<button class="action-btn danger" @click=${this.handleDelete}>Delete</button>
`
}
</div>
</div>
`;