1
0
mirror of https://github.com/garraflavatra/rolens.git synced 2025-06-28 13:35:12 +00:00

118 Commits

Author SHA1 Message Date
0965344f35 Merge branch 'shell_queries' of https://github.com/garraflavatra/mongodup into shell_queries 2023-07-01 21:23:42 +02:00
62f4b88ea6 Added log view to changelog 2023-07-01 20:32:41 +02:00
04221bf63f Add ability to see host logs (#54)
This adds a 'Log' view to the host panel using the [`getLog`
command](https://www.mongodb.com/docs/manual/reference/command/getLog/).

Fixes #53.
2023-07-01 20:31:12 +02:00
24b0df95df Add ability to see host logs (#54)
Squashed commit of the following:

commit 93b2d67cef
Author: Romein van Buren <romein@vburen.nl>
Date:   Sat Jul 1 20:07:33 2023 +0200

    Add filter functionality

commit 30b65a198f
Author: Romein van Buren <romein@vburen.nl>
Date:   Sat Jul 1 19:27:20 2023 +0200

    Renamed `form-row` class to `formrow`

commit 21afb01ea1
Author: Romein van Buren <romein@vburen.nl>
Date:   Sat Jul 1 19:26:04 2023 +0200

    Hide object types in object grid

commit 037d5432a4
Author: Romein van Buren <romein@vburen.nl>
Date:   Sat Jul 1 19:21:54 2023 +0200

    Make auto reload interval input smaller

commit 49d5022027
Author: Romein van Buren <romein@vburen.nl>
Date:   Sat Jul 1 15:08:00 2023 +0200

    Implement logs autoreload

commit 1f8984766b
Author: Romein van Buren <romein@vburen.nl>
Date:   Sat Jul 1 15:04:00 2023 +0200

    Return on error

commit 9c85259964
Author: Romein van Buren <romein@vburen.nl>
Date:   Sat Jul 1 15:03:37 2023 +0200

    Add log error handling

commit 7a98a63866
Author: Romein van Buren <romein@vburen.nl>
Date:   Sat Jul 1 14:46:39 2023 +0200

    Update log tab icon

commit f30827ae2e
Author: Romein van Buren <romein@vburen.nl>
Date:   Sat Jul 1 14:41:59 2023 +0200

    Add host log panel
2023-07-01 20:30:43 +02:00
0b9f23365b Add app name and version to mongo connections 2023-07-01 10:35:41 +02:00
84f1b85356 Attempt to fix build.js 2023-06-30 20:09:36 +02:00
1ecfa0ab20 Prepared 0.2.1 2023-06-29 14:40:48 +02:00
823ae1328b Added some PR talk 2023-06-29 14:37:39 +02:00
eba7e84fc8 Fix "edit this page on GH" link on documentation website 2023-06-29 13:58:39 +02:00
3dbf8fc98b Update some links in documentation 2023-06-29 10:16:29 +02:00
88a5b961b1 Fixed heading levels in advanced build document 2023-06-29 10:12:18 +02:00
ce9934faa7 Update shortcuts (fixes #51) 2023-06-29 09:50:18 +02:00
e53019a4b0 Hide title bar overflow (fixes #52) 2023-06-28 18:00:50 +02:00
05bfd54fb5 Added a build script to simplify compiling Rolens from source 2023-06-28 17:58:03 +02:00
1c95b86475 Patched a flaw in ci_bundle.sh 2023-06-28 15:41:57 +02:00
aa53d0c400 Generate installer for Windows using NSIS 2023-06-28 12:44:56 +02:00
8a7518532d Exclude wails_tools.nsh from version control 2023-06-27 17:32:09 +02:00
0e4c5d474b CSS: renamed .btn to .button 2023-06-27 17:21:54 +02:00
685ea78376 Updated changelog 2023-06-26 21:22:23 +02:00
7cd68d3280 Excel export feature (#46) 2023-06-26 21:20:12 +02:00
cab19e8a8b Debug Excel export script 2023-06-26 21:18:29 +02:00
cae29d9a71 Fixed export to Excel feature, it works now 2023-06-26 21:18:29 +02:00
3b4be3ebf6 Experimental Excel export feature (WIP) 2023-06-26 21:18:29 +02:00
efcc78e3bb Use new loading interface for collection indexes 2023-06-26 21:09:28 +02:00
9acb89205d Improved the app menu 2023-06-26 21:05:05 +02:00
5191086d74 Disabled Dependabot for a while, generates too many PRs 2023-06-26 17:34:38 +02:00
755b96762d Disabled Dependabot for a while, generates too many PRs 2023-06-26 17:33:19 +02:00
e6b0040821 Debug Excel export script 2023-06-26 11:16:04 +02:00
3055f8173e Fixed export to Excel feature, it works now 2023-06-25 22:00:53 +02:00
6cb6e209eb Experimental Excel export feature (WIP) 2023-06-25 20:29:28 +02:00
79568bc5f4 Make table headers stick to the top 2023-06-25 16:11:51 +02:00
86639f766d Remove temporary script file when it has been used 2023-06-25 15:38:06 +02:00
d1ce48c773 Changed script file extension 2023-06-25 10:21:30 +02:00
b0d3e31378 Made shell available for hosts and databases (besides collections) 2023-06-25 08:49:57 +02:00
8bebdfccd8 Small frontend fixes 2023-06-25 08:16:31 +02:00
f0ab5288f7 Hide tabs scrollbar when not necessary 2023-06-24 22:10:47 +02:00
aaed7a59f7 Add window menu for Mac 2023-06-24 22:10:25 +02:00
f5366a9ad5 Added ulimit note 2023-06-24 22:00:29 +02:00
67b820ad47 Fixed host editing bug 2023-06-24 20:43:02 +02:00
cb89b5923f Improved error logging and dialogs 2023-06-24 20:27:48 +02:00
b73b5f4485 [ImgBot] Optimize images (#45)
Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
2023-06-24 17:57:57 +02:00
8ccd1a4ce0 Updated readme screenshot once again 2023-06-24 16:06:47 +02:00
eef74b306b Made it possible to input loose JSON into find view inputs
I.e. `{ key: 'val' }` or `{ 'num': 2 }` besides `{ "strict": "json" }`.
2023-06-24 16:05:33 +02:00
3e293910ea Open dump in Explorer/Finder when finished (fixes #43) 2023-06-24 15:50:46 +02:00
8f81e66d17 Renamed dump.go to database_dump.go 2023-06-24 15:40:15 +02:00
48b26c9df5 Parse connection URI correctly 2023-06-24 11:26:02 +02:00
958a0197a4 Connect to mongosh using the right credentials 2023-06-24 11:05:19 +02:00
3893c8dd06 CSS tweaks for shell 2023-06-24 11:05:02 +02:00
51897adf8d Show spinner 2023-06-24 10:28:49 +02:00
c284cb4cfc Started working on shell feature (WIP) 2023-06-24 10:11:32 +02:00
0027a4333b Prepared v0.2.1 2023-06-23 21:32:52 +02:00
2a29f5226c [ImgBot] Optimize images (#42)
Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
2023-06-23 21:30:41 +02:00
a12417d4e9 Added copy button to error messages 2023-06-23 21:27:55 +02:00
58dd9de4ba Fixed star count on website 2023-06-23 18:42:03 +02:00
77ce1ab842 Temporarily removed fosstodon badge 2023-06-23 18:27:54 +02:00
b5bb51c869 CI: trying to generate .dmg (not working yet) 2023-06-23 18:25:42 +02:00
dc0094b27c Made harsh loading/error experience friendlier 2023-06-23 17:22:47 +02:00
964e66e8a3 Updated readme screenshot 2023-06-23 15:59:38 +02:00
7eb41630db Fixed some hosttree bugs 2023-06-23 15:58:23 +02:00
2ecc664e48 Merge branch 'dialogs_to_frontend' 2023-06-23 15:22:10 +02:00
756fa10555 CI: checkout repo in bundle job 2023-06-23 14:34:49 +02:00
bd265d0548 Multiline tabbar 2023-06-23 14:33:39 +02:00
c41572dbb7 CI: inline ci_bundle.sh 2023-06-23 14:21:49 +02:00
9a07954e4c CI: execute ci_bundle script 2023-06-23 13:32:05 +02:00
535b474d65 Fixed version_to_file script 2023-06-22 21:06:20 +02:00
43b059a579 CI: updated bundle script 2023-06-22 11:37:35 +02:00
0365e8e0ec CI: always run bundle job 2023-06-22 11:04:39 +02:00
fcdefedffd Prepared improved CI action 2023-06-22 10:55:59 +02:00
005f23c322 Linguist: ignore website files 2023-06-20 21:42:09 +02:00
030edaa561 Updated changelog 2023-06-20 21:02:03 +02:00
a4262a6ab7 Bump golang.org/x/sync from 0.2.0 to 0.3.0 (#39)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-19 14:44:10 +02:00
982d112665 Bump eslint from 8.42.0 to 8.43.0 in /frontend (#40)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-19 14:43:57 +02:00
e388b2513b Bump @codemirror/view from 6.13.0 to 6.13.2 in /frontend (#41)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-19 14:43:47 +02:00
c751169b7d Use Wails dialog to report fatal errors 2023-06-18 21:46:44 +02:00
0a3f99fa32 Moved text input dialog to frontend 2023-06-18 21:40:39 +02:00
a1456b3987 Implement OOP hosttree (#32)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-18 21:31:55 +02:00
3fe5f09163 Removed triple line break in release readme 2023-06-17 09:15:02 +02:00
29154d04d7 Bump @codemirror/language from 6.7.0 to 6.8.0 in /frontend (#33)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-12 16:20:16 +02:00
cde13d9ec0 Bump github.com/ncruces/zenity from 0.10.8 to 0.10.9 (#34)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-12 16:20:06 +02:00
19d9b8addd Bump github.com/wailsapp/wails/v2 from 2.3.1 to 2.5.1 (#35)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-12 16:19:58 +02:00
67ebaecb31 Bump go.mongodb.org/mongo-driver from 1.11.6 to 1.11.7 (#36)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-12 16:19:49 +02:00
3fd2be55b0 CI: run linter 2023-06-11 09:40:16 +02:00
2ee5c7dadd Fixed frontend coding style problems 2023-06-11 09:34:00 +02:00
6cc329982a Configure ESlint properly 2023-06-11 09:27:57 +02:00
15d30e67c3 Correct doc link in about box (resolves #30) 2023-06-10 09:40:38 +02:00
abc9df0897 Added meaningful window titles 2023-06-09 20:52:15 +02:00
be4e3e778e Display host stats 2023-06-07 21:52:43 +02:00
be7643bd31 Display sb stats generated by dbStats command (#15) 2023-06-07 21:30:22 +02:00
ea376f5ba7 Fix host/database selection bug in grid 2023-06-07 21:12:09 +02:00
b29b534b2f Show version number in about dialog (#28) 2023-06-06 21:52:02 +02:00
775e4d98d8 Bump vite from 3.2.5 to 3.2.7 in /frontend (#29)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-06 07:12:15 +02:00
f44165e41a Removed accidentally comitted dmg 2023-06-05 21:53:25 +02:00
9c1e6fe37a Bump @codemirror/lang-javascript from 6.1.8 to 6.1.9 in /frontend (#22)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-05 21:50:11 +02:00
c0f072b72e Bump github.com/wailsapp/wails/v2 from 2.3.1 to 2.5.1 (#21)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-05 21:50:02 +02:00
8e5beb9a2d Bump golang.org/x/sync from 0.1.0 to 0.2.0 (#20)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-05 21:49:54 +02:00
4e12591fc8 Bump eslint from 8.41.0 to 8.42.0 in /frontend (#23)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-05 21:49:45 +02:00
0ddf172327 Bump @codemirror/view from 6.12.0 to 6.13.0 in /frontend (#24)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-05 21:49:31 +02:00
cc1f67af45 CI: create-dmg doesn't seem to work on Ventura. Disabling it for the time being. 2023-06-04 17:33:20 +02:00
6e7ca49623 CI: correct use of parameters for Linux script 2023-06-04 17:18:59 +02:00
1524cdb244 Extract CI scripts to files. macOS: generate DMG 2023-06-04 17:11:13 +02:00
2fc7607ea7 Added Wails to credits section of readme and colophon 2023-06-03 18:51:05 +02:00
1327714557 Issue template: make docs bug solution a textarea 2023-06-03 18:47:59 +02:00
f2401a07c2 CI: flatten bundle 2023-06-03 18:32:30 +02:00
7ae3fdab53 CI: create comprehensive artifact containing all builds 2023-06-03 17:59:17 +02:00
1308d74967 CI windows: use correct zip path 2023-06-03 17:50:29 +02:00
cc1b4fb04c CI: Create zip file for windows 2023-06-03 17:42:46 +02:00
12d398320d CI: remove double OS version numbers from tar names 2023-06-03 11:45:05 +02:00
c5c3c6f873 Build for numerous platforms 2023-06-03 11:43:54 +02:00
9090f9425f CI: _do_ upload binaries 2023-06-03 11:20:04 +02:00
5240bdefab CI: correct windows Remove-Item usage 2023-06-03 11:19:10 +02:00
d0b5c0616e CI: upload binaries to the same articact 2023-06-03 10:59:08 +02:00
cd946307db CI: Use rmdir /s for Windows 2023-06-03 10:43:01 +02:00
ec99c15ff2 Trying to fix CI 2023-06-03 10:41:59 +02:00
536740259d CI: only run Linux step on Linux ...... 2023-06-03 10:33:14 +02:00
a44f4c8b7e Oops; compile for Linux on Linux machine, not for Windows/Darwin 2023-06-03 10:29:08 +02:00
4b618f9b25 CI: correct upload artifact step 2023-06-03 10:26:17 +02:00
5463b1a2c4 Attemt to have CI cross-compile binaries 2023-06-03 10:18:01 +02:00
7b8efa3022 It isn't necessary to install frontend dependencies manually, as Wails does it 2023-06-03 09:54:51 +02:00
145 changed files with 6172 additions and 1954 deletions

3
.gitattributes vendored
View File

@ -1,2 +1,5 @@
# Let GitHub Linguist ignore documentation and Wails generated files
# https://github.com/github-linguist/linguist/blob/master/docs/overrides.md
/frontend/src/components/icon.svelte linguist-vendored /frontend/src/components/icon.svelte linguist-vendored
/frontend/wailsjs/**/* linguist-generated /frontend/wailsjs/**/* linguist-generated
/website/**/* linguist-documentation

View File

@ -22,10 +22,8 @@ body:
attributes: attributes:
label: Problem label: Problem
description: Why is it wrong? description: Why is it wrong?
validations:
required: true
- type: input - type: textarea
id: solution id: solution
attributes: attributes:
label: Solution label: Solution

View File

@ -13,7 +13,14 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
platform: [windows-2022, macos-12, ubuntu-22.04] platform:
- windows-2019
- windows-2022
- macos-11
- macos-12
- macos-13
- ubuntu-20.04
- ubuntu-22.04
go-version: [1.18] go-version: [1.18]
node-version: [16] node-version: [16]
@ -32,8 +39,12 @@ jobs:
- name: Install Wails - name: Install Wails
run: go install github.com/wailsapp/wails/v2/cmd/wails@latest run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
- name: Install Wails dependencies for Linux - name: Install build dependencies for macOS
if: matrix.platform == 'ubuntu-22.04' if: contains(matrix.platform, 'macos')
run: npm install --global appdmg
- name: Install build dependencies for Linux
if: contains(matrix.platform, 'ubuntu')
run: sudo apt-get install gtk+-3.0 webkit2gtk-4.0 run: sudo apt-get install gtk+-3.0 webkit2gtk-4.0
- name: Set up Node.js - name: Set up Node.js
@ -43,14 +54,47 @@ jobs:
cache: npm cache: npm
cache-dependency-path: frontend/package-lock.json cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies - name: Cross-compile Rolens for Windows
run: cd frontend && npm ci && cd .. if: contains(matrix.platform, 'windows')
run: ./build/windows/ci_generate.ps1 -platform "${{ matrix.platform }}"
- name: Build Rolens - name: Cross-compile Rolens for Darwin
run: wails build if: contains(matrix.platform, 'macos')
run: ./build/darwin/ci_generate.sh "${{ matrix.platform }}"
- name: Upload binary - name: Cross-compile Rolens for Linux
if: contains(matrix.platform, 'ubuntu')
run: ./build/linux/ci_generate.sh "${{ matrix.platform }}"
- name: Upload generated binaries
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: rolens-${{ matrix.platform }} name: rolens-${{ matrix.platform }}
path: build/bin/* path: releases/*
- name: Test build script for users
run: node ./build.js
bundle:
name: Bundle artifacts
runs-on: ubuntu-22.04
needs: build
if: ${{ always() }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Download artifacts
uses: actions/download-artifact@v3
with:
path: artifacts
- name: Place all tarballs in the same directory
run: build/ci_bundle.sh
- name: Upload the bundle as an artifact
uses: actions/upload-artifact@v3
with:
name: rolens-bundle
path: bundle

24
.github/workflows/lint.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: Linter
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
eslint:
name: Run ESlint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
working-directory: ./frontend
- name: Run ESLint
run: npx eslint .
working-directory: ./frontend

6
.gitignore vendored
View File

@ -1,7 +1,13 @@
.DS_Store .DS_Store
Thumbs.db
/build/version.txt
/build/bin/ /build/bin/
build/windows/installer/wails_tools.nsh
/build/windows/installer/tmp/ /build/windows/installer/tmp/
/build/darwin/dmg_settings.json
/releases/
/frontend/node_modules/ /frontend/node_modules/
/frontend/dist/ /frontend/dist/

View File

@ -14,5 +14,12 @@
"[css]": { "[css]": {
"editor.suggest.insertMode": "replace", "editor.suggest.insertMode": "replace",
"editor.tabSize": 2 "editor.tabSize": 2
} },
"eslint.format.enable": true,
"eslint.lintTask.enable": true,
"eslint.lintTask.options": "frontend",
"eslint.workingDirectories": [
"frontend"
]
} }

View File

@ -1,9 +1,38 @@
## [v0.2.0](https://github.com/garraflavatra/rolens/releases/tag/v0.2.0) ## [Unreleased]
* Added log view (#53, #54).
## [v0.2.2]
* Added Excel export format (#46).
* Improved the application menu.
* Improved error logging and dialogs.
* Made table headers stick to the top.
* Made it possible (again) to input loose JSON into find view inputs, i.e. `{ key: 'val' }` or `{ 'num': 2 }` besides `{ "strict": "json" }`.
* Fixed host editing bug.
* Open dump in Explorer/Finder when finished (#43).
## [v0.2.1]
* Display host and database statistics generated by the corresponding diagnostic MongoDB commands in addition to collection stats (#15).
* Added version number of the running Rolens instance in the about dialog (#25, #28).
* Added meaningful window titles, and actually show these in the title bar (macOS).
* Corrected link to documentation in the about box (#30).
* Fixed host/database selection bug in grid (#31, #32), involving a frontend refactoring.
* Replaced (some) harsh loading dialogs with smooth spinners, and replaced (some) capricious error dialogs with friendly error messages.
## [v0.2.0]
* Added some external links related to Rolens to the application menu. * Added some external links related to Rolens to the application menu.
* Fix an infinite loop bug when a document in the find view has been double clicked. * Fix an infinite loop bug when a document in the find view has been double clicked.
* Find view: added the ability to make changes to single documents from within the object editor. * Find view: added the ability to make changes to single documents from within the object editor.
## [v0.1.0](https://github.com/garraflavatra/rolens/releases/tag/v0.1.0) ## [v0.1.0]
Initial release. Initial release.
[Unreleased]: https://github.com/garraflavatra/rolens/tree/main
[v0.1.0]: https://github.com/garraflavatra/rolens/releases/tag/v0.1.0
[v0.2.0]: https://github.com/garraflavatra/rolens/releases/tag/v0.2.0
[v0.2.1]: https://github.com/garraflavatra/rolens/releases/tag/v0.2.1
[v0.2.2]: https://github.com/garraflavatra/rolens/releases/tag/v0.2.2

View File

@ -1,2 +0,0 @@
build: *
wails build

View File

@ -2,7 +2,9 @@
Robust, blazing-fast, comprehensive, yet simple [MongoDB](https://www.mongodb.com/) administration tool for Windows, macOS and Linux. Robust, blazing-fast, comprehensive, yet simple [MongoDB](https://www.mongodb.com/) administration tool for Windows, macOS and Linux.
<a href="https://github.com/garraflavatra/rolens/actions/workflows/ci.yml" target="_blank"><img src="https://github.com/garraflavatra/rolens/actions/workflows/ci.yml/badge.svg" alt="CI" /></a> <a target="_blank" href="https://garraflavatra.github.io/rolens"><img src="./.github/docs-badge.svg" alt="Documentation" /></a> <a href="https://fosstodon.org/@rolens" target="_blank" rel="me"><img src="./.github/fosstodon-badge.svg" alt="Fosstodon" /></a> <a href="https://github.com/garraflavatra/rolens/actions/workflows/ci.yml" target="_blank"><img src="https://github.com/garraflavatra/rolens/actions/workflows/ci.yml/badge.svg" alt="CI" /></a> <a target="_blank" href="https://garraflavatra.github.io/rolens"><img src="./.github/docs-badge.svg" alt="Documentation" /></a>
<!-- <a href="https://fosstodon.org/@rolens" target="_blank" rel="me"><img src="./.github/fosstodon-badge.svg" alt="Fosstodon" /></a> -->
## Why another MongoDB client? ## Why another MongoDB client?
@ -34,7 +36,9 @@ You can obtain a pre-compiled Rolens binary for macOS or installer for Windows f
### Compiling from source ### Compiling from source
Please refer to [the documentation](https://garraflavatra.github.io/rolens/installation/) for detailed compilation instructions. If you have Node.js installed, just download the source from GitHub, and run `node ./build.js`. The install script will check that dependencies are present and build Rolens for you.
If you want to build it yourself, please refer to the [advanced build process documentation](https://garraflavatra.github.io/rolens/development/advanced-build/) for detailed compilation instructions.
## User guide ## User guide
@ -48,11 +52,38 @@ Rolens is designed to be as intuitive as possible. But if something is unclear n
Feel free to contact me if you have questions! [Send an e-mail.](mailto:romein@vburen.nl) Feel free to contact me if you have questions! [Send an e-mail.](mailto:romein@vburen.nl)
## Feature list
At this point, Rolens is comparable to MongoHub regarding features. It cannot handle things like user management _yet_, but it _does_ have:
* Connecting to hosts
- Host status
- System info
* Database management
- See stats
- Create dumps with `mongodump`
* Collections
- See stats
- Find, insert, update, & remove
- Save queries to reuse them
- Customizable table view for query results
- Versatile forms to enter data in a standardized format
- Aggregation pipeline editor
- Fully customizable export to a number of formats like JSON, CSV, and Excel
- Index editor
## Wishlist
* User management
* Shell _([under development](https://github.com/garraflavatra/rolens/pull/44))_
## Author and license ## Author and license
© [Romein van Buren](mailto:romein@vburen.nl) 2023. The source code and compiled binaries are released under the GNU GPLv3 license — see [`LICENSE`](./LICENSE) for the full license text. © [Romein van Buren](mailto:romein@vburen.nl) 2023. The source code and compiled binaries are released under the GNU GPLv3 license — see [`LICENSE`](./LICENSE) for the full license text.
## Credits ## Credits
* [Wails](https://wails.io/) facilitates the build process for multiple OSes.
* The installer for Windows is generated by [NSIS](https://nsis.sourceforge.io/Main_Page).
* Icons are from [Feather Icons](https://feathericons.com/) by [Cole Bemis](https://github.com/colebemis). * Icons are from [Feather Icons](https://feathericons.com/) by [Cole Bemis](https://github.com/colebemis).
* Vector drawings come from [unDraw](https://undraw.co/). * Vector drawings come from [unDraw](https://undraw.co/).

161
build.js Executable file
View File

@ -0,0 +1,161 @@
#!/usr/bin/env node
const { execSync, spawn } = require('child_process');
const { readFileSync, statSync, rmdirSync } = require('fs');
// Check that the script is run from the root.
try {
const wailsJsonFile = statSync('./wails.json');
if (!wailsJsonFile.isFile()) {
throw new Error();
}
} catch {
console.log('Error: please run the build script from the Rolens project root.');
process.exit(1);
}
// Output version.
const version = JSON.parse(readFileSync('./wails.json').toString()).info.productVersion;
if (process.argv.includes('-v') || process.argv.includes('-version') || process.argv.includes('--version')) {
console.log(version);
process.exit(0);
}
// Output help text.
if (process.argv.includes('-h') || process.argv.includes('--help')) {
console.log(`Rolens build script v${version}`);
console.log('');
console.log('This script installs missing dependencies if any, and then compiles Rolens');
console.log('for the current platform.');
console.log('');
console.log('Options:');
console.log(' -h --help Show this help text and exit.');
console.log(' -q --quiet Do not output Wails build log.');
console.log(' -v --version Log the current Rolens version and exit.');
process.exit(0);
}
// Shared objects.
const quiet = process.argv.includes('-q') || process.argv.includes('--quiet');
const isWindows = process.platform === 'win32';
const missingDependencies = [];
function isNullish(val) {
return val === undefined || val === null;
}
// Check that Go ^1.18 is installed.
try {
const goMinorVersion = /go1\.([0-9][0-9])/.exec(
execSync('go version').toString()
)?.pop();
if (isNullish(goMinorVersion) || (parseInt(goMinorVersion) < 18)) {
throw new Error();
}
} catch {
missingDependencies.push({ name: 'Go ^1.18 ^16', url: 'https://go.dev/doc/install' });
}
// Check that Node.js ^16 is installed.
try {
const nodeMajorVersion = /v([0-9]{1,2})\.[0-9]{1,3}\.[0-9]{1,3}/.exec(
execSync('node --version').toString()
)?.pop();
if (isNullish(nodeMajorVersion) || (parseInt(nodeMajorVersion) < 16)) {
throw new Error();
}
} catch {
missingDependencies.push({ name: 'Node.js ^16', url: 'https://go.dev/doc/install' });
}
// Check that Wails is installed.
try {
const wailsMinorVersion = /v2\.([0-9])\.[0-9]/.exec(
execSync('wails version').toString()
)?.pop();
if (isNullish(wailsMinorVersion) || (parseInt(wailsMinorVersion) < 3)) {
throw new Error();
}
} catch {
missingDependencies.push({
name: 'Wails ^2.3',
command: 'go install github.com/wailsapp/wails/v2/cmd/wails@latest',
url: 'https://wails.io/docs/gettingstarted/installation',
});
}
// Check that NSIS is installed on Windows.
if (isWindows) {
try {
const nsisInstalled = /v3\.([0-9][0-9])/.test(execSync('makensis.exe /VERSION').toString());
if (!nsisInstalled) {
throw new Error();
}
} catch {
missingDependencies.push({
name: 'Nullsoft Install System ^3',
command: 'choco install nsis',
url: 'https://nsis.sourceforge.io/Download',
comment: 'Note: you should add makensis.exe to your path:\n setx /M PATH "%PATH%;C:\\Program Files (x86)\\NSIS\\Bin"'
});
}
}
// Report missing dependencies.
if (missingDependencies.length > 0) {
console.log('You are missing the following dependencies:');
for (const dependency of missingDependencies) {
console.log('');
console.log(`- ${dependency.name}`);
if (dependency.command) {
console.log(' Install it by executing:');
console.log(` ${dependency.command}`);
}
if (dependency.url) {
console.log(' Visit the following page for more information:');
console.log(` ${dependency.url}`);
}
if (dependency.comment) {
console.log(` ${dependency.comment}`);
}
}
process.exit(1);
}
// Clean output directory.
console.log('Cleaning output directory...');
try { rmdirSync('./build/bin'); } catch {}
// Build Rolens.
console.log(`Building Rolens ${version}...`);
console.log();
const proc = spawn('wails', [ 'build', '-clean', isWindows ? '-nsis' : '' ]);
if (!quiet) {
proc.stdout.on('data', data => process.stdout.write(data));
proc.stderr.on('data', data => process.stderr.write(data));
}
proc.on('exit', code => process.exit(code));

View File

@ -1,19 +1,10 @@
# Build Directory # Build Directory
The build directory is used to house all the build files and assets for your application. The build directory is used to house all the build files and assets for the application.
The structure is:
* bin - Output directory
* darwin - macOS specific files
* windows - Windows specific files
## Mac ## Mac
The `darwin` directory holds files specific to Mac builds. The `darwin` directory holds files specific to Mac builds. These may be customised and used as part of the build. To return these files to the default state, simply delete them and build with `wails build`.
These may be customised and used as part of the build. To return these files to the default state, simply delete them
and
build with `wails build`.
The directory contains the following files: The directory contains the following files:
@ -22,14 +13,27 @@ The directory contains the following files:
## Windows ## Windows
The `windows` directory contains the manifest and rc files used when building with `wails build`. The `windows` directory contains the manifest and rc files used when building with `wails build`. These may be customised for the application. To return these files to the default state, simply delete them and build with `wails build`.
These may be customised for your application. To return these files to the default state, simply delete them and
build with `wails build`.
- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to - `icon.ico` - The icon used for the application. This is used when building using `wails build`. If it is missing, a new `icon.ico` file will be created using the `appicon.png` file in the build directory.
use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
will be created using the `appicon.png` file in the build directory.
- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`. - `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer, - `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer, as well as the application itself (right click the exe -> properties -> details)
as well as the application itself (right click the exe -> properties -> details) - `wails.exe.manifest` - The main application manifest file.
- `wails.exe.manifest` - The main application manifest file.
### NSIS graphics
When updating the bitmaps for the NSIS installer, make sure you export a 24-bits image without colour space information. Follow [this guide](https://stackoverflow.com/a/26471885):
> These steps worked for me using GIMP 2.8.10:
>
> * Create an image using RGB mode (Image > Mode > RGB) using the appropriate size for whatever you are creating (`164x314` for `MUI_WELCOMEFINISHPAGE_BITMAP`, `150x57` for `MUI_HEADERIMAGE_BITMAP`)
> * File > Export as ...
> * Name your file with a .bmp extension
> * Click "Export"
> * In the window titled "Export Image as BMP" expand "Compatibility Options" and check the box that says **"Do not write color space information"**
> * Also, in the window titled "Export Image as BMP" expand "Advanced Options" and check the radio button under **"24 bits"** next to "R8 G8 B8"
> * Click "Export"
## CI scripts
Each platform folder inside this directory contains a `ci_generate.*` file, which is used by GitHub Actions to build the application. When you want to compile Rolens on your machine, please refer to the installation instructions.

40
build/ci_bundle.sh Executable file
View File

@ -0,0 +1,40 @@
#!/bin/sh
#
# This script bundles the binaries generated by GitHub Actions.
#
# Platforms to choose from:
# - windows-2019
# - windows-2022
# - macos-11
# - macos-12
# - macos-13
# - ubuntu-20.04
# - ubuntu-22.04
#
# Bundles to choose from:
# - rolens-macos-11-amd64.tar.gz
# - rolens-macos-11-arm64.tar.gz
# - rolens-macos-12-amd64.tar.gz
# - rolens-macos-12-arm64.tar.gz
# - rolens-macos-13-amd64.tar.gz
# - rolens-macos-13-arm64.tar.gz
# - rolens-ubuntu-20.04-amd64.tar.gz
# - rolens-ubuntu-22.04-amd64.tar.gz
# - rolens-windows-2019-amd64.zip
# - rolens-windows-2019-arm64.zip
# - rolens-windows-2022-amd64.zip
# - rolens-windows-2022-arm64.zip
#
node ./build/version_to_file.js
version=$(<./build/version.txt)
mkdir bundle
# macOS apps
mv artifacts/*/rolens-macos-11-amd64.tar.gz "bundle/rolens-$version-macos-11+-amd64.tar.gz"
mv artifacts/*/rolens-macos-11-arm64.tar.gz "bundle/rolens-$version-macos-11+-arm64.tar.gz"
# Windows installers
mv artifacts/*/rolens-windows-2019-amd64-installer.zip "bundle/rolens-$version-windows-10+-amd64-installer.zip"
mv artifacts/*/rolens-windows-2019-arm64-installer.zip "bundle/rolens-$version-windows-10+-arm64-installer.zip"

60
build/darwin/ci_generate.sh Executable file
View File

@ -0,0 +1,60 @@
#!/bin/sh
# Cleanup
rm -rf releases
rm -rf build/bin
mkdir releases
mkdir -p build/bin
# Settings
cat > build/darwin/dmg_settings.json << EOF
{
"title": "Rolens",
"background": "$(pwd)/build/darwin/dmg_background.png",
"icon-size": 100,
"window": {
"size": { "width": 155, "height": 250 },
"position": { "x": 360, "y": 360 }
},
"contents": [
{ "x": 750, "y": 500, "type": "link", "path": "/Applications" },
{ "x": 595, "y": 250, "type": "file", "path": "$(pwd)/build/bin/Rolens.app" }
]
}
EOF
# AMD/Intel
wails build -platform darwin/amd64
# create-dmg \
# --volname Rolens \
# --window-size 155 250 \
# --volicon build/appicon.png \
# --eula LICENSE \
# --app-drop-link 750 500 \
# --icon-size 100 \
# --background build/darwin/dmg_background.png \
# --add-file Rolens.app build/bin/Rolens.app 595 250 \
# build/bin/Rolens.dmg emptydir
# appdmg build/darwin/dmg_settings.json build/bin/Rolens.dmg
tar -czvf releases/rolens-$1-amd64.tar.gz --directory build/bin Rolens.app
# Cleanup
rm -rf build/bin/Rolens.app
# ARM/AppleM1
wails build -platform darwin/arm64
# create-dmg \
# --volname Rolens \
# --window-size 155 250 \
# --volicon build/appicon.png \
# --eula LICENSE \
# --app-drop-link 750 500 \
# --icon-size 100 \
# --background build/darwin/dmg_background.png \
# --add-file Rolens.app build/bin/Rolens.app 595 250 \
# build/bin/Rolens.dmg emptydir
# appdmg build/darwin/dmg_settings.json build/bin/Rolens.dmg
tar -czvf releases/rolens-$1-arm64.tar.gz --directory build/bin Rolens.app
# Cleanup
rm -rf build/bin/Rolens.app

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

9
build/linux/ci_generate.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/sh
mkdir releases
wails build -platform linux/amd64
tar -czvf releases/rolens-$1-amd64.tar.gz --directory build/bin Rolens
# rm -rf build/bin
# wails build -platform linux/arm64
# tar -czvf releases/rolens-$1-arm64.tar.gz --directory build/bin Rolens

32
build/release_readme.txt Normal file
View File

@ -0,0 +1,32 @@
Thank you for downloading Rolens!
=================================
Star the project on GitHub:
https://github.com/garraflavatra/rolens
User guide
----------
Rolens is designed to be as intuitive as possible. But if something is unclear
nevertheless, you can consult the user manual:
https://garraflavatra.github.io/rolens/
Questions and bugs
------------------
Did you capture a bug? Please report it — thank you!
https://github.com/garraflavatra/rolens/issues/new?assignees=garraflavatra&labels=bug&projects=&template=bug.yml
Would you like to see a new feature? You can request it:
https://github.com/garraflavatra/rolens/issues/new?assignees=garraflavatra&labels=enhancement&projects=&template=feature.yml
Do you have a question? Ask questions on the discussion board:
https://github.com/garraflavatra/rolens/discussions/new?category=questions
Feel free to contact me if you have questions! Send an e-mail to romein@vburen.nl
Author and license
------------------
© Romein van Buren 2023. The source code and compiled binaries are released
under the GNU GPLv3 license — see LICENSE for the full license text.

13
build/version_to_file.js Normal file
View File

@ -0,0 +1,13 @@
#!/usr/bin/node
// This script extracts the version number from wails.json in the project root
// and writes it to version.txt
const fs = require('fs');
fs.writeFileSync(
__dirname + '/version.txt',
JSON.parse(
fs.readFileSync(__dirname + '/../wails.json')
).info.productVersion
);

11
build/windows/ci_generate.ps1 Executable file
View File

@ -0,0 +1,11 @@
param([string]$platform)
mkdir releases
wails build -platform windows/amd64 -nsis
Remove-Item build\bin\Rolens.exe
Compress-Archive -Path build\bin\* -DestinationPath releases\rolens-$platform-amd64-installer.zip
Remove-Item -Recurse -Confirm:$false .\build\bin
wails build -platform windows/arm64 -nsis
Remove-Item build\bin\Rolens.exe
Compress-Archive -Path build\bin\* -DestinationPath releases\rolens-$platform-arm64-installer.zip

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

View File

@ -1,12 +1,11 @@
Unicode true Unicode true
####
## Please note: Template replacements don't work in this file. They are provided with default defines like ## Please note: Template replacements don't work in this file. They are provided with default defines like
## mentioned underneath. ## mentioned underneath.
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo. ## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually ## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
## from outside of Wails for debugging and development of the installer. ## from outside of Wails for debugging and development of the installer.
##
## For development first make a wails nsis build to populate the "wails_tools.nsh": ## For development first make a wails nsis build to populate the "wails_tools.nsh":
## > wails build --target windows/amd64 --nsis ## > wails build --target windows/amd64 --nsis
## Then you can call makensis on this file with specifying the path to your binary: ## Then you can call makensis on this file with specifying the path to your binary:
@ -16,28 +15,24 @@ Unicode true
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe ## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
## For a installer with both architectures: ## For a installer with both architectures:
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe ## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
####
## The following information is taken from the ProjectInfo file, but they can be overwritten here. ## The following information is taken from the ProjectInfo file, but can be overwritten here.
#### ## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}" ## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}" ## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}" ## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}" ## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}" ## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
### ## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" ## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
####
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
####
## Include the wails tools
####
!include "wails_tools.nsh" !include "wails_tools.nsh"
# The version information for this two must consist of 4 parts # The version information for this two must consist of 4 parts
VIProductVersion "${INFO_PRODUCTVERSION}.0" VIProductVersion "${INFO_PRODUCTVERSION}.0"
VIFileVersion "${INFO_PRODUCTVERSION}.0" VIFileVersion "${INFO_PRODUCTVERSION}.0"
# Product information
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
@ -45,23 +40,36 @@ VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
!include "MUI.nsh" !include "MUI2.nsh"
!define MUI_ICON "..\icon.ico" !define MUI_ICON "..\icon.ico"
!define MUI_UNICON "..\icon.ico" !define MUI_UNICON "..\icon.ico"
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
# Bitmap on the left side of the welcome page. Must be 164x314 pixels in size.
!define MUI_HEADERIMAGE
!define MUI_HEADERIMAGE_BITMAP ".\banner_h.bmp"
!define MUI_WELCOMEFINISHPAGE_BITMAP ".\banner_v.bmp"
!define MUI_WELCOMEPAGE_TITLE "Welcome to the Rolens installer!"
# Finish page information
!define MUI_FINISHPAGE_RUN "$INSTDIR\${INFO_PROJECTNAME}.exe"
!define MUI_FINISHPAGE_RUN_TEXT "Start Rolens when finished"
!define MUI_FINISHPAGE_TITLE "Thanks for installing!"
!define MUI_FINISHPAGE_LINK "Visit Rolens on the Web!"
!define MUI_FINISHPAGE_LINK_LOCATION "https://garraflavatra.github.io/rolens/"
!define MUI_FINISHPAGE_LINK_COLOR 880000
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps !define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. !define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. !insertmacro MUI_PAGE_WELCOME
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer # !insertmacro MUI_PAGE_LICENSE "resources\eula.txt"
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. !insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES # Installing page. !insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH # Finished installation page. !insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page !insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer !insertmacro MUI_LANGUAGE "English"
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 ## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
#!uninstfinalize 'signtool --file "%1"' #!uninstfinalize 'signtool --file "%1"'
@ -69,7 +77,7 @@ VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
Name "${INFO_PRODUCTNAME}" Name "${INFO_PRODUCTNAME}"
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). InstallDir "$PROGRAMFILES64\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
ShowInstDetails show # This will always show the installation details. ShowInstDetails show # This will always show the installation details.
Function .onInit Function .onInit
@ -80,7 +88,7 @@ Section
!insertmacro wails.webview2runtime !insertmacro wails.webview2runtime
SetOutPath $INSTDIR SetOutPath $INSTDIR
!insertmacro wails.files !insertmacro wails.files
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
@ -89,7 +97,7 @@ Section
!insertmacro wails.writeUninstaller !insertmacro wails.writeUninstaller
SectionEnd SectionEnd
Section "uninstall" Section "uninstall"
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
RMDir /r $INSTDIR RMDir /r $INSTDIR

View File

@ -1,171 +0,0 @@
# DO NOT EDIT - Generated automatically by `wails build`
!include "x64.nsh"
!include "WinVer.nsh"
!include "FileFunc.nsh"
!ifndef INFO_PROJECTNAME
!define INFO_PROJECTNAME "Rolens"
!endif
!ifndef INFO_COMPANYNAME
!define INFO_COMPANYNAME "Rolens"
!endif
!ifndef INFO_PRODUCTNAME
!define INFO_PRODUCTNAME "Rolens"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "1.0.0"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "Copyright........."
!endif
!ifndef PRODUCT_EXECUTABLE
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
!endif
!ifndef UNINST_KEY_NAME
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
!endif
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
!ifndef REQUEST_EXECUTION_LEVEL
!define REQUEST_EXECUTION_LEVEL "admin"
!endif
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
!ifdef ARG_WAILS_AMD64_BINARY
!define SUPPORTS_AMD64
!endif
!ifdef ARG_WAILS_ARM64_BINARY
!define SUPPORTS_ARM64
!endif
!ifdef SUPPORTS_AMD64
!ifdef SUPPORTS_ARM64
!define ARCH "amd64_arm64"
!else
!define ARCH "amd64"
!endif
!else
!ifdef SUPPORTS_ARM64
!define ARCH "arm64"
!else
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
!endif
!endif
!macro wails.checkArchitecture
!ifndef WAILS_WIN10_REQUIRED
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
!endif
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
!endif
${If} ${AtLeastWin10}
!ifdef SUPPORTS_AMD64
${if} ${IsNativeAMD64}
Goto ok
${EndIf}
!endif
!ifdef SUPPORTS_ARM64
${if} ${IsNativeARM64}
Goto ok
${EndIf}
!endif
IfSilent silentArch notSilentArch
silentArch:
SetErrorLevel 65
Abort
notSilentArch:
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
Quit
${else}
IfSilent silentWin notSilentWin
silentWin:
SetErrorLevel 64
Abort
notSilentWin:
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
Quit
${EndIf}
ok:
!macroend
!macro wails.files
!ifdef SUPPORTS_AMD64
${if} ${IsNativeAMD64}
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
${EndIf}
!endif
!ifdef SUPPORTS_ARM64
${if} ${IsNativeARM64}
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
${EndIf}
!endif
!macroend
!macro wails.writeUninstaller
WriteUninstaller "$INSTDIR\uninstall.exe"
SetRegView 64
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
!macroend
!macro wails.deleteUninstaller
Delete "$INSTDIR\uninstall.exe"
SetRegView 64
DeleteRegKey HKLM "${UNINST_KEY}"
!macroend
# Install webview2 by launching the bootstrapper
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
!macro wails.webview2runtime
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
!endif
SetRegView 64
# If the admin key exists and is not empty then webview2 is already installed
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${If} $0 != ""
Goto ok
${EndIf}
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${If} $0 != ""
Goto ok
${EndIf}
${EndIf}
SetDetailsPrint both
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
SetDetailsPrint listonly
InitPluginsDir
CreateDirectory "$pluginsdir\webview2bootstrapper"
SetOutPath "$pluginsdir\webview2bootstrapper"
File "tmp\MicrosoftEdgeWebview2Setup.exe"
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
SetDetailsPrint both
ok:
!macroend

View File

@ -1,6 +1,7 @@
--- ---
title: Changelog title: Changelog
parent: Development summary: The development history of Rolens.
parent: Colophon
order: 90 order: 90
--- ---

View File

@ -7,5 +7,7 @@ Rolens is © [Romein van Buren](mailto:romein@vburen.nl) 2023. The source code a
## Credits ## Credits
* [Wails](https://wails.io/) facilitates the build process for multiple OSes.
* The installer for Windows is generated by [NSIS](https://nsis.sourceforge.io/Main_Page).
* Icons are from [Feather Icons](https://feathericons.com/) by [Cole Bemis](https://github.com/colebemis). * Icons are from [Feather Icons](https://feathericons.com/) by [Cole Bemis](https://github.com/colebemis).
* Vector drawings come from [unDraw](https://undraw.co/). * Vector drawings come from [unDraw](https://undraw.co/).

View File

@ -1,5 +1,6 @@
--- ---
title: Development title: Development
summary: In-depth information about Rolens source code.
order: 50 order: 50
--- ---

View File

@ -1,23 +1,12 @@
# Installation procedure ---
title: Advanced build process
parent: Development
order: 10
---
## System requirements If you just want to install Rolens, please refer to the [installation document](/installation/). You can read this guide to get a detailed overview of the build procedure.
Rolens can run on the following operating systems: ## Prerequisites
* Windows 10/11 amd64/arm64
* Linux amd64/arm64
* macOS 10.13+ amd64 (Intel)
* macOS 11.0+ arm64 (Apple Silicon)
## Pre-compiled binaries
You can obtain a pre-compiled Rolens binary for macOS or installer for Windows from the [release page](https://github.com/garraflavatra/rolens/releases/latest).
## From source
Rolens is open-source software, which means that you can compile it from source on your own machine by cloning [the repository](https://github.com/garraflavatra/rolens).
### Prerequisites
Rolens is written in Go, so you should download the Go compiler from [the download page](https://go.dev/dl/). The minimum version required is 1.18. You can confirm whether it's installed correctly by running `go version` and checking that it outputs something similar to `go1.18.2`. Rolens is written in Go, so you should download the Go compiler from [the download page](https://go.dev/dl/). The minimum version required is 1.18. You can confirm whether it's installed correctly by running `go version` and checking that it outputs something similar to `go1.18.2`.
@ -25,18 +14,20 @@ Furthermore, you need to have [Wails ^3.1](https://wails.io/docs/gettingstarted/
In order to compile the frontend, [Node.js](https://nodejs.org/en/download) ^16.0 and the [npm](https://npmjs.com) package manager ^8.0 (included in Node.js) are required. To confirm the installed versions of those tools, execute `node -v` and `npm -v`. In order to compile the frontend, [Node.js](https://nodejs.org/en/download) ^16.0 and the [npm](https://npmjs.com) package manager ^8.0 (included in Node.js) are required. To confirm the installed versions of those tools, execute `node -v` and `npm -v`.
### Download source ## Download source
To obtain a copy of the source code, do either of the following: To obtain a copy of the source code, do either of the following:
* Download a tarball or zip archive from the [release page](https://github.com/garraflavatra/rolens/releases/latest). Make sure you download the source archive, and not a pre-compiled binary. * Download a tarball or zip archive from the [release page](https://github.com/garraflavatra/rolens/releases/latest). Make sure you download the source archive, and not a pre-compiled binary.
* Or clone [the Git repository](https://github.com/garraflavatra/rolens): `git clone https://github.com/garraflavatra/rolens.git`. * Or clone [the Git repository](https://github.com/garraflavatra/rolens): `git clone https://github.com/garraflavatra/rolens.git`.
### Compile ## Compile
`cd` into the root directory of the source code and run either: `cd` into the root directory of the source code and run either:
* `wails build` to generate an executable for your platform. * `wails build` to generate an executable for your platform.
* `wails build -nsis` to generate an [NSIS installer](https://nsis.sourceforge.io/Main_Page). This requires that you have NSIS installed on your machine. * `wails build -nsis` to generate an [NSIS installer](https://nsis.sourceforge.io/Main_Page) for Windows. This requires that you have NSIS installed on your machine.
The generated binary will live in `build/bin`. You may want to run the installer (Windows) or move the app to the Applications folder (Mac). The generated binary will live in `build/bin`. You may want to run the installer (Windows) or move the app to the Applications folder (Mac).
If Wails complains that there are too many open files, you can try to increase the maximum number of open files using [`ulimit -f 1024`](https://www.man7.org/linux/man-pages/man1/ulimit.1p.html) (or whichever value) on *nix systems.

View File

@ -0,0 +1,7 @@
---
title: Build directory
parent: Development
order: 30
---
{% filecontent "../build/README.md", 2 %}

View File

@ -1,7 +1,7 @@
--- ---
title: Logfiles title: Logfiles
parent: User guide parent: Development
order: 120 order: 40
--- ---
Rolens keeps track of log-worthy events and logs them in its log directory. Rolens keeps track of log-worthy events and logs them in its log directory.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 623 KiB

After

Width:  |  Height:  |  Size: 462 KiB

View File

@ -1,5 +1,5 @@
--- ---
title: title: Rolens
order: 1 order: 1
summary: Robust, blazing-fast, comprehensive, yet simple [MongoDB](https://www.mongodb.com/) administration tool for Windows, macOS and Linux. summary: Robust, blazing-fast, comprehensive, yet simple [MongoDB](https://www.mongodb.com/) administration tool for Windows, macOS and Linux.
eleventyNavigation: eleventyNavigation:
@ -18,3 +18,11 @@ This project arose from all flaws of similar tools many of which are slow, compl
![Impression of Rolens's interface](./images/home-impression.png) ![Impression of Rolens's interface](./images/home-impression.png)
This project is heavily inspired by the excellent [MongoHub](https://github.com/bububa/MongoHub-Mac) application, which sadly has not been updated since 2011. This project is heavily inspired by the excellent [MongoHub](https://github.com/bububa/MongoHub-Mac) application, which sadly has not been updated since 2011.
## Features
{% filecontent "../README.md", "## Feature list", "## " %}
## Wishlist
{% filecontent "../README.md", "## Wishlist", "## " %}

View File

@ -1,6 +1,30 @@
--- ---
title: Installation title: Installation
summary: Install Rolens on your machine.
order: 20 order: 20
--- ---
{% filecontent "../INSTALL.md", 2 %} ## System requirements
Rolens can run on the following operating systems:
* Windows 10/11 amd64/arm64
* Linux amd64/arm64
* macOS 10.13+ amd64 (Intel)
* macOS 11.0+ arm64 (Apple Silicon)
## Pre-compiled binaries
You can obtain a pre-compiled Rolens.app for macOS or installer for Windows from the [release page](https://github.com/garraflavatra/rolens/releases/latest).
If you use a Linux-based OS, please continue reading.
## Compile from source in 2 easy steps
Rolens is free and open-source software, which means that you can compile it from source on your own machine by cloning [the repository](https://github.com/garraflavatra/rolens).
If you have Node.js installed, just download the source from GitHub, and run `node ./build.js`. The install script will check that dependencies are present and build Rolens for you. If you want to build it yourself, please continue reading.
## Advanced build
Please see the [advanced build documentation](/development/advanced-build/) for a more profound insight in the build procedure.

View File

@ -4,6 +4,8 @@ parent: User guide
order: 30 order: 30
--- ---
## Shortcuts
You can use the following shortcuts when you have opened a collection. You can use the following shortcuts when you have opened a collection.
{% render "shortcuts", shortcuts: shortcuts.collections %} {% render "shortcuts", shortcuts: shortcuts["Managing collections"] %}

View File

@ -2,5 +2,10 @@
title: Databases title: Databases
parent: User guide parent: User guide
order: 20 order: 20
stub: true
--- ---
## Shortcuts
You can use the following shortcuts when you have opened a database.
{% render "shortcuts", shortcuts: shortcuts["Managing databases"] %}

View File

@ -2,5 +2,10 @@
title: Managing hosts title: Managing hosts
parent: User guide parent: User guide
order: 10 order: 10
stub: true
--- ---
## Shortcuts
You can use the following shortcuts to manage hosts and connections.
{% render "shortcuts", shortcuts: shortcuts["Connecting to hosts"] %}

View File

@ -4,7 +4,7 @@ parent: User guide
order: 100 order: 100
--- ---
You can open the preference panel by using the menu <kbd>Rolens > Settings…</kbd> or by typing {% render "shortcut", shortcut: shortcuts.preferences[0] %}. You can open the preference panel by using the menu <kbd>Rolens > Settings…</kbd> or by pressing <kbd></kbd><kbd>,</kbd>.
Rolens currently offers the following configuration options: Rolens currently offers the following configuration options:

View File

@ -0,0 +1,12 @@
---
title: Shortcut reference
parent: User guide
order: 900
---
<p>You can use the following shortcuts to manage hosts and connections.</p>
{% for item in shortcuts %}
<h2>{{ item[0] }}</h2>
{% render "shortcuts", shortcuts: item[1] %}
{% endfor %}

View File

@ -1,195 +0,0 @@
{
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"env": {
"browser": true,
"node": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:svelte/recommended"
],
"plugins": [
"svelte",
"import"
],
"overrides": [{
"files": "*.svelte",
"rules": {
"no-inner-declarations": 0,
"svelte/no-inner-declarations": [
"error",
"functions"
]
}
}],
"rules": {
"no-undef": [
"error",
{ "typeof": true }
],
"require-atomic-updates": 0,
"indent": [
"error",
2,
{ "SwitchCase": 1 }
],
"strict": 0,
"quotes": [ "error", "single" ],
"semi": [ "warn", "always" ],
"accessor-pairs": "error",
"array-bracket-spacing": [
"error",
"always"
],
"arrow-body-style": [
"error",
"as-needed",
{ "requireReturnForObjectLiteral": true }
],
"arrow-parens": [ "error", "as-needed" ],
"arrow-spacing": "error",
"block-spacing": [ "error", "always" ],
"comma-dangle": [
"warn",
{
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "never",
"exports": "never",
"functions": "never"
}
],
"comma-spacing": "error",
"computed-property-spacing": [
"error",
"never"
],
"generator-star-spacing": "error",
"id-blacklist": "error",
"id-match": "error",
"jsx-quotes": "error",
"keyword-spacing": "error",
"key-spacing": [
"warn",
{ "beforeColon": false }
],
"linebreak-style": [
"error",
"unix"
],
"no-unused-vars": "warn",
"no-alert": "error",
"no-caller": "error",
"no-confusing-arrow": [
"error",
{ "allowParens": true }
],
"no-console": "off",
"no-div-regex": "error",
"no-duplicate-imports": "error",
"no-extend-native": "error",
"no-extra-label": "error",
"no-fallthrough": "off",
"no-floating-decimal": "error",
"no-implicit-coercion": [
"error",
{
"boolean": false,
"number": false,
"string": false
}
],
"no-inner-declarations": [
"error",
"functions"
],
"no-iterator": "error",
"no-label-var": "error",
"no-lone-blocks": "error",
"no-new-object": "error",
"no-new-require": "error",
"no-new-wrappers": "error",
"no-restricted-globals": [
"error",
"event",
"name"
],
"no-restricted-imports": "error",
"no-restricted-modules": "error",
"no-restricted-syntax": "error",
"no-script-url": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow-restricted-names": "error",
"no-spaced-func": "error",
"no-trailing-spaces": "error",
"no-unmodified-loop-condition": "error",
"no-useless-constructor": "error",
"no-whitespace-before-property": "error",
"no-with": "error",
"object-curly-spacing": [
"error",
"always"
],
"prefer-const": "error",
"require-yield": "error",
"semi-spacing": [
"error"
],
"space-before-blocks": "error",
"space-before-function-paren": [
"error",
"never"
],
"space-in-parens": [
"error",
"never"
],
"space-infix-ops": "error",
"space-unary-ops": "error",
"template-curly-spacing": "error",
"curly": 2,
"brace-style": [
"error",
"stroustrup"
],
"wrap-iife": [
"error",
"any"
],
"yield-star-spacing": "error",
"multiline-ternary": [
"warn",
"never"
],
"no-nested-ternary": "error",
"svelte/html-quotes": [
"warn",
{
"prefer": "double",
"dynamic": { "quoted": false }
}
],
"svelte/no-useless-mustaches": "warn",
"svelte/require-store-reactive-access": "warn",
"svelte/no-reactive-literals": "error",
"svelte/html-closing-bracket-spacing": "warn",
"svelte/indent": "warn",
"svelte/max-attributes-per-line": [
"warn",
{ "multiline": 1, "singleline": 5 }
],
"svelte/mustache-spacing": "warn",
"svelte/no-extra-reactive-curlies": "error",
"svelte/no-spaces-around-equal-signs-in-attribute": "warn",
"svelte/prefer-class-directive": "warn",
"svelte/shorthand-attribute": "warn",
"svelte/shorthand-directive": "warn",
"svelte/spaced-html-comment": "warn",
"svelte/no-at-html-tags": 0
}
}

View File

@ -15,6 +15,7 @@
<div></div> <div></div>
</div> </div>
</div> </div>
<div id="dialogoutlets"></div>
<script src="./src/main.js" type="module"></script> <script src="./src/main.js" type="module"></script>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@ -9,17 +9,33 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-javascript": "^6.1.8", "@codemirror/lang-javascript": "^6.1.9",
"@codemirror/language": "^6.7.0", "@codemirror/language": "^6.8.0",
"@codemirror/view": "^6.12.0", "@codemirror/view": "^6.13.2",
"bson": "^4.7.2", "bson": "^4.7.2",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"date-fns": "^2.30.0" "date-fns": "^2.30.0"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1", "@sveltejs/vite-plugin-svelte": "^1.0.1",
"eslint": "^8.41.0", "eslint": "^8.43.0",
"eslint-config-svelte3": "github:johbog/eslint-config-svelte3",
"svelte": "^3.59.1", "svelte": "^3.59.1",
"vite": "^3.0.0" "vite": "^3.2.7"
},
"eslintConfig": {
"extends": "svelte3",
"ignorePatterns": ["dist", "wailsjs"],
"rules": {
"svelte/html-quotes": [
"warn",
{
"prefer": "double",
"dynamic": { "quoted": false }
}
],
"svelte/no-useless-mustaches": "off",
"svelte/no-extra-reactive-curlies": "off"
}
} }
} }

View File

@ -1 +1 @@
d933a4e16d5bb0132361eef3b5d3ba22 7fc6d7b66151030191ded7136d96970e

View File

@ -1 +1 @@
<svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="647.63626" height="632.17383" viewBox="0 0 647.63626 632.17383" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M687.3279,276.08691H512.81813a15.01828,15.01828,0,0,0-15,15v387.85l-2,.61005-42.81006,13.11a8.00676,8.00676,0,0,1-9.98974-5.31L315.678,271.39691a8.00313,8.00313,0,0,1,5.31006-9.99l65.97022-20.2,191.25-58.54,65.96972-20.2a7.98927,7.98927,0,0,1,9.99024,5.3l32.5498,106.32Z" transform="translate(-276.18187 -133.91309)" fill="#f2f2f2"/><path d="M725.408,274.08691l-39.23-128.14a16.99368,16.99368,0,0,0-21.23-11.28l-92.75,28.39L380.95827,221.60693l-92.75,28.4a17.0152,17.0152,0,0,0-11.28028,21.23l134.08008,437.93a17.02661,17.02661,0,0,0,16.26026,12.03,16.78926,16.78926,0,0,0,4.96972-.75l63.58008-19.46,2-.62v-2.09l-2,.61-64.16992,19.65a15.01489,15.01489,0,0,1-18.73-9.95l-134.06983-437.94a14.97935,14.97935,0,0,1,9.94971-18.73l92.75-28.4,191.24024-58.54,92.75-28.4a15.15551,15.15551,0,0,1,4.40966-.66,15.01461,15.01461,0,0,1,14.32032,10.61l39.0498,127.56.62012,2h2.08008Z" transform="translate(-276.18187 -133.91309)" fill="#3f3d56"/><path d="M398.86279,261.73389a9.0157,9.0157,0,0,1-8.61133-6.3667l-12.88037-42.07178a8.99884,8.99884,0,0,1,5.9712-11.24023l175.939-53.86377a9.00867,9.00867,0,0,1,11.24072,5.9707l12.88037,42.07227a9.01029,9.01029,0,0,1-5.9707,11.24072L401.49219,261.33887A8.976,8.976,0,0,1,398.86279,261.73389Z" transform="translate(-276.18187 -133.91309)" fill="#00008b"/><circle cx="190.15351" cy="24.95465" r="20" fill="#00008b"/><circle cx="190.15351" cy="24.95465" r="12.66462" fill="#fff"/><path d="M878.81836,716.08691h-338a8.50981,8.50981,0,0,1-8.5-8.5v-405a8.50951,8.50951,0,0,1,8.5-8.5h338a8.50982,8.50982,0,0,1,8.5,8.5v405A8.51013,8.51013,0,0,1,878.81836,716.08691Z" transform="translate(-276.18187 -133.91309)" fill="#e6e6e6"/><path d="M723.31813,274.08691h-210.5a17.02411,17.02411,0,0,0-17,17v407.8l2-.61v-407.19a15.01828,15.01828,0,0,1,15-15H723.93825Zm183.5,0h-394a17.02411,17.02411,0,0,0-17,17v458a17.0241,17.0241,0,0,0,17,17h394a17.0241,17.0241,0,0,0,17-17v-458A17.02411,17.02411,0,0,0,906.81813,274.08691Zm15,475a15.01828,15.01828,0,0,1-15,15h-394a15.01828,15.01828,0,0,1-15-15v-458a15.01828,15.01828,0,0,1,15-15h394a15.01828,15.01828,0,0,1,15,15Z" transform="translate(-276.18187 -133.91309)" fill="#3f3d56"/><path d="M801.81836,318.08691h-184a9.01015,9.01015,0,0,1-9-9v-44a9.01016,9.01016,0,0,1,9-9h184a9.01016,9.01016,0,0,1,9,9v44A9.01015,9.01015,0,0,1,801.81836,318.08691Z" transform="translate(-276.18187 -133.91309)" fill="#00008b"/><circle cx="433.63626" cy="105.17383" r="20" fill="#00008b"/><circle cx="433.63626" cy="105.17383" r="12.18187" fill="#fff"/></svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="647.636" height="632.174" data-name="Layer 1" viewBox="0 0 647.636 632.174"><path fill="#f2f2f2" d="M687.3279,276.08691H512.81813a15.01828,15.01828,0,0,0-15,15v387.85l-2,.61005-42.81006,13.11a8.00676,8.00676,0,0,1-9.98974-5.31L315.678,271.39691a8.00313,8.00313,0,0,1,5.31006-9.99l65.97022-20.2,191.25-58.54,65.96972-20.2a7.98927,7.98927,0,0,1,9.99024,5.3l32.5498,106.32Z" transform="translate(-276.18187 -133.91309)"/><path fill="#3f3d56" d="M725.408,274.08691l-39.23-128.14a16.99368,16.99368,0,0,0-21.23-11.28l-92.75,28.39L380.95827,221.60693l-92.75,28.4a17.0152,17.0152,0,0,0-11.28028,21.23l134.08008,437.93a17.02661,17.02661,0,0,0,16.26026,12.03,16.78926,16.78926,0,0,0,4.96972-.75l63.58008-19.46,2-.62v-2.09l-2,.61-64.16992,19.65a15.01489,15.01489,0,0,1-18.73-9.95l-134.06983-437.94a14.97935,14.97935,0,0,1,9.94971-18.73l92.75-28.4,191.24024-58.54,92.75-28.4a15.15551,15.15551,0,0,1,4.40966-.66,15.01461,15.01461,0,0,1,14.32032,10.61l39.0498,127.56.62012,2h2.08008Z" transform="translate(-276.18187 -133.91309)"/><path fill="#00008b" d="M398.86279,261.73389a9.0157,9.0157,0,0,1-8.61133-6.3667l-12.88037-42.07178a8.99884,8.99884,0,0,1,5.9712-11.24023l175.939-53.86377a9.00867,9.00867,0,0,1,11.24072,5.9707l12.88037,42.07227a9.01029,9.01029,0,0,1-5.9707,11.24072L401.49219,261.33887A8.976,8.976,0,0,1,398.86279,261.73389Z" transform="translate(-276.18187 -133.91309)"/><circle cx="190.154" cy="24.955" r="20" fill="#00008b"/><circle cx="190.154" cy="24.955" r="12.665" fill="#fff"/><path fill="#e6e6e6" d="M878.81836,716.08691h-338a8.50981,8.50981,0,0,1-8.5-8.5v-405a8.50951,8.50951,0,0,1,8.5-8.5h338a8.50982,8.50982,0,0,1,8.5,8.5v405A8.51013,8.51013,0,0,1,878.81836,716.08691Z" transform="translate(-276.18187 -133.91309)"/><path fill="#3f3d56" d="M723.31813,274.08691h-210.5a17.02411,17.02411,0,0,0-17,17v407.8l2-.61v-407.19a15.01828,15.01828,0,0,1,15-15H723.93825Zm183.5,0h-394a17.02411,17.02411,0,0,0-17,17v458a17.0241,17.0241,0,0,0,17,17h394a17.0241,17.0241,0,0,0,17-17v-458A17.02411,17.02411,0,0,0,906.81813,274.08691Zm15,475a15.01828,15.01828,0,0,1-15,15h-394a15.01828,15.01828,0,0,1-15-15v-458a15.01828,15.01828,0,0,1,15-15h394a15.01828,15.01828,0,0,1,15,15Z" transform="translate(-276.18187 -133.91309)"/><path fill="#00008b" d="M801.81836,318.08691h-184a9.01015,9.01015,0,0,1-9-9v-44a9.01016,9.01016,0,0,1,9-9h184a9.01016,9.01016,0,0,1,9,9v44A9.01015,9.01015,0,0,1,801.81836,318.08691Z" transform="translate(-276.18187 -133.91309)"/><circle cx="433.636" cy="105.174" r="20" fill="#00008b"/><circle cx="433.636" cy="105.174" r="12.182" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,66 +1,65 @@
<script> <script>
import BlankState from '$components/blankstate.svelte'; import BlankState from '$components/blankstate.svelte';
import ContextMenu from '$components/contextmenu.svelte'; import ContextMenu from '$components/contextmenu.svelte';
import connections from '$lib/stores/connections'; import dialogs from '$lib/dialogs';
import contextMenu from '$lib/stores/contextmenu'; import contextMenu from '$lib/stores/contextmenu';
import environment from '$lib/stores/environment'; import environment from '$lib/stores/environment';
import hosts from '$lib/stores/hosts'; import hostTree from '$lib/stores/hosttree';
import applicationInited from '$lib/stores/inited'; import applicationInited from '$lib/stores/inited';
import About from '$organisms/about.svelte'; import windowTitle from '$lib/stores/windowtitle';
import Connection from '$organisms/connection/index.svelte'; import Connection from '$organisms/connection/index.svelte';
import Settings from '$organisms/settings/index.svelte'; import { EventsOn } from '$wails/runtime';
import { EventsEmit, EventsOn } from '$wails/runtime';
import { tick } from 'svelte'; import { tick } from 'svelte';
import AboutDialog from './dialogs/about.svelte';
import SettingsDialog from './dialogs/settings/index.svelte';
const activeHostKey = '';
let activeDbKey = '';
let activeCollKey = '';
let settingsModalOpen = false;
let aboutModalOpen = false;
let connectionManager;
let showWelcomeScreen = undefined; let showWelcomeScreen = undefined;
$: host = hosts[activeHostKey]; applicationInited.defer(() => {
$: connection = $connections[activeHostKey]; hostTree.subscribe(hosts => {
if (hostTree.hasBeenInited() && (showWelcomeScreen === undefined)) {
hosts.subscribe(h => { showWelcomeScreen = !Object.keys(hosts || {}).length;
if (h && (showWelcomeScreen === undefined)) { }
showWelcomeScreen = !Object.keys($hosts || {}).length; });
}
}); });
async function createFirstHost() { async function createFirstHost() {
showWelcomeScreen = false; showWelcomeScreen = false;
await tick(); await tick();
connectionManager.createHost(); hostTree.newHost();
} }
EventsOn('OpenPreferences', () => settingsModalOpen = true); function showAboutDialog() {
EventsOn('OpenAboutModal', () => aboutModalOpen = true); dialogs.new(AboutDialog);
}
function showSettings() {
dialogs.new(SettingsDialog);
}
EventsOn('OpenPreferences', showSettings);
EventsOn('OpenAboutModal', showAboutDialog);
</script> </script>
<svelte:window on:contextmenu|preventDefault /> <svelte:window on:contextmenu|preventDefault />
<div id="root" class="platform-{$environment?.platform}"> <div id="root" class="platform-{$environment?.platform}">
<div class="titlebar"></div> <div class="titlebar">{$windowTitle}</div>
{#if $applicationInited && $hosts && (showWelcomeScreen !== undefined)} {#if $applicationInited && (showWelcomeScreen !== undefined)}
<main class:empty={showWelcomeScreen}> <main class:empty={showWelcomeScreen}>
{#if showWelcomeScreen} {#if showWelcomeScreen}
<BlankState label="Welcome to Rolens!" image="/logo.png" pale={false} big={true}> <BlankState label="Welcome to Rolens!" image="/logo.png" pale={false} big={true}>
<button class="btn" on:click={createFirstHost}>Add your first host</button> <button class="button" on:click={createFirstHost}>Add your first host</button>
</BlankState> </BlankState>
{:else} {:else}
<Connection {activeHostKey} bind:activeCollKey bind:activeDbKey bind:this={connectionManager} /> <Connection />
{/if} {/if}
</main> </main>
{#key $contextMenu} {#key $contextMenu}
<ContextMenu {...$contextMenu} on:close={contextMenu.hide} /> <ContextMenu {...$contextMenu} on:close={contextMenu.hide} />
{/key} {/key}
<Settings bind:show={settingsModalOpen} />
<About bind:show={aboutModalOpen} />
{/if} {/if}
</div> </div>
@ -69,6 +68,11 @@
height: 0; height: 0;
background-color: #00002a; background-color: #00002a;
--wails-draggable: drag; --wails-draggable: drag;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
} }
#root.platform-darwin .titlebar { #root.platform-darwin .titlebar {
height: var(--darwin-titlebar-height); height: var(--darwin-titlebar-height);
@ -99,7 +103,7 @@
overflow: scroll; overflow: scroll;
} }
.btn.create { .button.create {
margin-top: 0.5rem; margin-top: 0.5rem;
} }
</style> </style>

View File

@ -1,14 +1,25 @@
<script> <script>
import Icon from './icon.svelte';
export let title = '';
export let label = 'No items'; export let label = 'No items';
export let image = '/empty.svg'; export let image = '/empty.svg';
export let icon = '';
export let pale = true; export let pale = true;
export let big = false; export let big = false;
</script> </script>
<div class="blankstate" class:pale class:big> <div class="blankstate" class:pale class:big>
<div class="content"> <div class="content">
<img src={image} alt="" /> {#if icon}
<p>{label}</p> <Icon name={icon} />
{:else if image}
<img src={image} alt="" />
{/if}
<p class="title">{title}</p>
<p class="label">{label}</p>
<slot /> <slot />
</div> </div>
</div> </div>
@ -27,14 +38,22 @@
height: 150px; height: 150px;
width: auto; width: auto;
} }
.content > :global(svg) {
height: 40px;
width: auto;
}
p { p {
margin: 2.85rem 0; margin: 2.85rem 0;
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1.25rem; line-height: 1.25rem;
} }
p.title {
font-weight: 700;
margin-bottom: -1.85rem;
}
.blankstate :global(.btn) { .blankstate :global(.button) {
font-size: 1.35rem; font-size: 1.35rem;
padding: 1rem; padding: 1rem;
} }
@ -48,7 +67,7 @@
filter: grayscale(1); filter: grayscale(1);
opacity: 0.4; opacity: 0.4;
} }
.pale p { .pale {
color: #777; color: #777;
} }
</style> </style>

View File

@ -0,0 +1,57 @@
<script>
import { indentWithTab } from '@codemirror/commands';
import { indentOnInput } from '@codemirror/language';
import { EditorState } from '@codemirror/state';
import { EditorView, keymap } from '@codemirror/view';
import { basicSetup } from 'codemirror';
import { createEventDispatcher, onMount } from 'svelte';
export let text = '';
export let editor = undefined;
export let extensions = [];
const dispatch = createEventDispatcher();
let editorParent;
const editorState = EditorState.create({
doc: '',
extensions: [
basicSetup,
keymap.of([ indentWithTab, indentOnInput ]),
EditorState.tabSize.of(4),
EditorView.updateListener.of(e => {
if (!e.docChanged) {
return;
}
text = e.state.doc.toString();
dispatch('updated', { text });
}),
...extensions,
],
});
onMount(() => {
editor = new EditorView({
parent: editorParent,
state: editorState,
});
dispatch('inited', { editor });
});
</script>
<div bind:this={editorParent} class="editor"></div>
<style>
.editor {
width: 100%;
background-color: #fff;
border-radius: var(--radius);
overflow: hidden;
}
.editor :global(.cm-editor) {
overflow: auto;
height: 100%;
}
</style>

View File

@ -136,10 +136,10 @@
</div> </div>
<div slot="footer" class="footer"> <div slot="footer" class="footer">
<button class="btn secondary" type="button" on:click={() => value = new Date()}> <button class="button secondary" type="button" on:click={() => value = new Date()}>
<Icon name="o" /> Set to now <Icon name="o" /> Set to now
</button> </button>
<button class="btn" type="button" on:click={() => show = false}> <button class="button" type="button" on:click={() => show = false}>
<Icon name="check" /> OK <Icon name="check" /> OK
</button> </button>
</div> </div>

View File

@ -70,7 +70,7 @@
<div class="forminput {type}"> <div class="forminput {type}">
<div class="field"> <div class="field">
{#if type === 'string'} {#if type === 'string'}
<input type="text" bind:value use:input={{ type, onValid, onInvalid, mandatory, autofocus }} /> <input type="text" bind:value use:input={{ type, onValid, onInvalid, mandatory, autofocus }} autocomplete="off" spellcheck="false" />
{:else if type === 'objectid'} {:else if type === 'objectid'}
<input <input
type="text" type="text"

View File

@ -1,5 +1,5 @@
<script> <script>
import { resolveKeypath, setValue } from '$lib/objects'; import { pathsAreEqual, resolveKeypath, setValue } from '$lib/objects';
import contextMenu from '$lib/stores/contextmenu'; import contextMenu from '$lib/stores/contextmenu';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import FormInput from './forminput.svelte'; import FormInput from './forminput.svelte';
@ -30,7 +30,9 @@
function refresh(hideObjectIndicators, items) { function refresh(hideObjectIndicators, items) {
_items = objectToArray(items).map(item => { _items = objectToArray(items).map(item => {
item.children = objectToArray(item.children); if (item.children) {
item.children = objectToArray(item.children);
}
return item; return item;
}); });
@ -50,7 +52,9 @@
return obj; return obj;
} }
else if ((typeof obj === 'object') && (obj !== null)) { else if ((typeof obj === 'object') && (obj !== null)) {
return Object.entries(obj).map(([ k, item ]) => ({ ...item, [key]: k })); return Object.entries(obj).map(([ k, item ]) => {
return { ...item, [key]: k };
});
} }
else { else {
return obj; return obj;
@ -62,18 +66,9 @@
return false; return false;
} }
toggleChildren(itemKey, false); activeKey = itemKey;
activePath = [ ...path.slice(0, level), itemKey ];
if (activeKey !== itemKey) { dispatch('select', { level, itemKey, index, path: activePath });
activeKey = itemKey;
if (level === 0) {
activePath = [ itemKey ];
}
else {
activePath = [ ...path, itemKey ];
}
dispatch('select', { level, itemKey, index });
}
} }
function closeAll() { function closeAll() {
@ -89,7 +84,7 @@
} }
function doubleClick(itemKey, index) { function doubleClick(itemKey, index) {
// toggleChildren(itemKey, false); toggleChildren(itemKey, false);
dispatch('trigger', { level, itemKey, index }); dispatch('trigger', { level, itemKey, index });
childrenOpen[itemKey] = true; childrenOpen[itemKey] = true;
} }
@ -136,7 +131,7 @@
on:dblclick={() => doubleClick(item[key], index)} on:dblclick={() => doubleClick(item[key], index)}
on:contextmenu|preventDefault={evt => showContextMenu(evt, item)} on:contextmenu|preventDefault={evt => showContextMenu(evt, item)}
class:selectable={canSelect} class:selectable={canSelect}
class:selected={canSelect && !activePath[level + 1] && activePath.every(k => path.includes(k) || k === item[key]) && (activePath[level] === item[key])} class:selected={canSelect && pathsAreEqual(activePath, [ ...path, item[key] ])}
class:striped class:striped
> >
{#if !hideChildrenToggles} {#if !hideChildrenToggles}

View File

@ -1,5 +1,8 @@
<script> <script>
import { onDestroy } from 'svelte';
import BlankState from './blankstate.svelte';
import GridItems from './grid-items.svelte'; import GridItems from './grid-items.svelte';
import Icon from './icon.svelte';
export let columns = []; export let columns = [];
export let items = []; export let items = [];
@ -12,14 +15,27 @@
export let canSelect = true; export let canSelect = true;
export let canRemoveItems = false; export let canRemoveItems = false;
export let inputsValid = false; export let inputsValid = false;
// export let actions = []; export let errorTitle = '';
export let errorDescription = '';
export let busy = false;
let copySucceeded = false;
let timeout;
async function copyErrorDescription() {
await navigator.clipboard.writeText(errorDescription);
copySucceeded = true;
timeout = setTimeout(() => copySucceeded = false, 1500);
}
onDestroy(() => clearTimeout(timeout));
</script> </script>
<div class="grid"> <div class="grid">
<!-- {#if actions?.length} <!-- {#if actions?.length}
<div class="actions"> <div class="actions">
{#each actions as action} {#each actions as action}
<button class="btn" on:click={action.fn} disabled={action.disabled}> <button class="button" on:click={action.fn} disabled={action.disabled}>
{#if action.icon}<Icon name={action.icon} />{/if} {#if action.icon}<Icon name={action.icon} />{/if}
{action.label || ''} {action.label || ''}
</button> </button>
@ -27,45 +43,55 @@
</div> </div>
{/if} --> {/if} -->
<table> {#if busy}
{#if showHeaders && columns.some(col => col.title)} <BlankState label={(busy === true) ? 'Loading…' : busy} icon="loading" />
<thead> {:else if errorTitle || errorDescription}
<tr> <BlankState title={errorTitle} label={errorDescription} icon="!">
{#if !hideChildrenToggles} <button class="button-small" on:click={copyErrorDescription}>
<th class="has-toggle"></th> <Icon name={copySucceeded ? 'check' : 'clipboard'} /> Copy error message
{/if} </button>
</BlankState>
{:else}
<table>
{#if showHeaders && columns.some(col => col.title)}
<thead>
<tr>
{#if !hideChildrenToggles}
<th class="has-toggle"></th>
{/if}
<th class="has-icon"></th> <th class="has-icon"></th>
{#each columns as column} {#each columns as column}
<th scope="col">{column.title || ''}</th> <th scope="col">{column.title || ''}</th>
{/each} {/each}
{#if canRemoveItems} {#if canRemoveItems}
<th class="has-button"></th> <th class="has-button"></th>
{/if} {/if}
</tr> </tr>
</thead> </thead>
{/if} {/if}
<tbody> <tbody>
<GridItems <GridItems
{items} {items}
{columns} {columns}
{key} {key}
{striped} {striped}
{canSelect} {canSelect}
{canRemoveItems} {canRemoveItems}
{hideObjectIndicators} {hideObjectIndicators}
{hideChildrenToggles} {hideChildrenToggles}
bind:activePath bind:activePath
bind:inputsValid bind:inputsValid
on:select on:select
on:trigger on:trigger
on:removeItem on:removeItem
/> />
</tbody> </tbody>
</table> </table>
{/if}
</div> </div>
<style> <style>
@ -75,7 +101,32 @@
background-color: #fff; background-color: #fff;
} }
/* .actions { table {
border-collapse: collapse;
width: 100%;
background-color: #fff;
}
thead th {
font-weight: 600;
text-align: left;
padding: 2px;
/* border-bottom: 2px solid #ccc; */
box-shadow: 0 2px #ccc;
background-color: #fff;
position: sticky;
top: 0;
}
.grid :global(.blankstate) {
height: 100%;
padding: 1rem;
}
/* tfoot button {
margin-top: 0.5rem;
}
.actions {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
padding: 0.5rem; padding: 0.5rem;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
@ -83,23 +134,4 @@
.actions button { .actions button {
margin-right: 0.2rem; margin-right: 0.2rem;
} */ } */
table {
border-collapse: collapse;
width: 100%;
background-color: #fff;
}
table thead {
border-bottom: 2px solid #ccc;
}
th {
font-weight: 600;
text-align: left;
padding: 2px;
}
/* tfoot button {
margin-top: 0.5rem;
} */
</style> </style>

View File

@ -1,5 +1,10 @@
<script> <script>
export let name = ''; export let name = '';
export let spin = false;
if (name === 'loading') {
spin = true;
}
</script> </script>
<style> <style>
@ -9,14 +14,34 @@
margin-right: 2px; margin-right: 2px;
} }
:global(.btn) svg { :global(.button) svg {
height: 13px; height: 13px;
width: auto; width: auto;
vertical-align: bottom; vertical-align: bottom;
} }
@keyframes spinning {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
svg.spinning {
animation: spinning 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
}
</style> </style>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class:spinning={spin}
>
{#if name === 'radio'} {#if name === 'radio'}
<circle cx="12" cy="12" r="2"></circle><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path> <circle cx="12" cy="12" r="2"></circle><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path>
{:else if name === 'chev-l'} {:else if name === 'chev-l'}
@ -26,13 +51,13 @@
{:else if name === 'chev-d'} {:else if name === 'chev-d'}
<polyline points="6 9 12 15 18 9"></polyline> <polyline points="6 9 12 15 18 9"></polyline>
{:else if name === 'chev-u'} {:else if name === 'chev-u'}
<path d="m18 15-6-6-6 6"/> <path d="m18 15-6-6-6 6" />
{:else if name === 'chevs-l'} {:else if name === 'chevs-l'}
<path d="m11 17-5-5 5-5M18 17l-5-5 5-5"/> <path d="m11 17-5-5 5-5M18 17l-5-5 5-5" />
{:else if name === 'chevs-r'} {:else if name === 'chevs-r'}
<path d="m13 17 5-5-5-5M6 17l5-5-5-5"/> <path d="m13 17 5-5-5-5M6 17l5-5-5-5" />
{:else if name === 'arr-d'} {:else if name === 'arr-d'}
<path d="M12 5v14M19 12l-7 7-7-7"/> <path d="M12 5v14M19 12l-7 7-7-7" />
{:else if name === 'db'} {:else if name === 'db'}
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><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> <ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><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>
{:else if name === 'x'} {:else if name === 'x'}
@ -44,54 +69,87 @@
{:else if name === 'reload'} {:else if name === 'reload'}
<polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path> <polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
{:else if name === 'clipboard'} {:else if name === 'clipboard'}
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect> <path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8"
y="2"
width="8"
height="4"
rx="1"
ry="1"></rect>
{:else if name === 'edit'} {:else if name === 'edit'}
<path d="M12 20h9"></path><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path> <path d="M12 20h9"></path><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
{:else if name === 'check'} {:else if name === 'check'}
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline> <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline>
{:else if name === 'list'} {:else if name === 'list'}
<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/> <path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" />
{:else if name === 'table'} {:else if name === 'table'}
<path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"/> <path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18" />
{:else if name === 'form'} {:else if name === 'form'}
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8"/> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" />
{:else if name === 'cog'} {:else if name === 'cog'}
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/> <circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
{:else if name === 'zap'} {:else if name === 'zap'}
<path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/> <path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z" />
{:else if name === 'server'} {:else if name === 'server'}
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><path d="M6 6h.01M6 18h.01"/> <rect x="2"
y="2"
width="20"
height="8"
rx="2"
ry="2" /><rect x="2"
y="14"
width="20"
height="8"
rx="2"
ry="2" /><path d="M6 6h.01M6 18h.01" />
{:else if name === 'text'} {:else if name === 'text'}
<path d="M4 7V4h16v3M9 20h6M12 4v16"/> <path d="M4 7V4h16v3M9 20h6M12 4v16" />
{:else if name === 'hash'} {:else if name === 'hash'}
<path d="M4 9h16M4 15h16M10 3 8 21M16 3l-2 18"/> <path d="M4 9h16M4 15h16M10 3 8 21M16 3l-2 18" />
{:else if name === 'toggle-l'} {:else if name === 'toggle-l'}
<rect x="1" y="5" width="22" height="14" rx="7" ry="7"/><circle cx="8" cy="12" r="3"/> <rect x="1"
y="5"
width="22"
height="14"
rx="7"
ry="7" /><circle cx="8" cy="12" r="3" />
{:else if name === 'cal'} {:else if name === 'cal'}
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><path d="M16 2v4M8 2v4M3 10h18"/> <rect x="3"
y="4"
width="18"
height="18"
rx="2"
ry="2" /><path d="M16 2v4M8 2v4M3 10h18" />
{:else if name === 'code'} {:else if name === 'code'}
<path d="m16 18 6-6-6-6M8 6l-6 6 6 6"/> <path d="m16 18 6-6-6-6M8 6l-6 6 6 6" />
{:else if name === 'target'} {:else if name === 'target'}
<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/> <circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" />
{:else if name === 'trash'} {:else if name === 'trash'}
<path d="M3 6h18M19 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 2v2M10 11v6M14 11v6"/> <path d="M3 6h18M19 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 2v2M10 11v6M14 11v6" />
{:else if name === 'anchor'} {:else if name === 'anchor'}
<circle cx="12" cy="5" r="3"/><path d="M12 22V8M5 12H2a10 10 0 0 0 20 0h-3"/> <circle cx="12" cy="5" r="3" /><path d="M12 22V8M5 12H2a10 10 0 0 0 20 0h-3" />
{:else if name === 'o'} {:else if name === 'o'}
<circle cx="12" cy="12" r="10"/> <circle cx="12" cy="12" r="10" />
{:else if name === 'info'} {:else if name === 'info'}
<circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/> <circle cx="12" cy="12" r="10" /><path d="M12 16v-4M12 8h.01" />
{:else if name === 'play'} {:else if name === 'play'}
<path d="m5 3 14 9-14 9V3z"/> <path d="m5 3 14 9-14 9V3z" />
{:else if name === 'upload'} {:else if name === 'upload'}
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" />
{:else if name === 'save'} {:else if name === 'save'}
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><path d="M17 21v-8H7v8M7 3v5h8"/> <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" /><path d="M17 21v-8H7v8M7 3v5h8" />
{:else if name === 're'} {:else if name === 're'}
<path d="m17 1 4 4-4 4"/><path d="M3 11V9a4 4 0 0 1 4-4h14M7 23l-4-4 4-4"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/> <path d="m17 1 4 4-4 4" /><path d="M3 11V9a4 4 0 0 1 4-4h14M7 23l-4-4 4-4" /><path d="M21 13v2a4 4 0 0 1-4 4H3" />
{:else if name === 'chart'} {:else if name === 'chart'}
<path d="M18 20V10M12 20V4M6 20v-6"/> <path d="M18 20V10M12 20V4M6 20v-6" />
{:else if name === '?'} {:else if name === '?'}
<circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line> <circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line>
{:else if name === '!'}
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM12 9v4M12 17h.01" />
{:else if name === 'loading'}
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
{:else if name === 'shell'}
<path d="m4 17 6-6-6-6M12 19h8" />
{:else if name === 'doc'}
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" />
{/if} {/if}
</svg> </svg>

View File

@ -4,15 +4,16 @@
<script> <script>
import { Beep } from '$wails/go/ui/UI'; import { Beep } from '$wails/go/ui/UI';
import { fade, fly } from 'svelte/transition'; import { createEventDispatcher } from 'svelte';
import Icon from './icon.svelte'; import Icon from './icon.svelte';
export let show = false; export let show = true;
export let title = undefined; export let title = undefined;
export let contentPadding = true; export let contentPadding = true;
export let width = '80vw'; export let width = '80vw';
export let overflow = true; export let overflow = true;
const dispatch = createEventDispatcher();
const level = numberOfModalsOpen + 1; const level = numberOfModalsOpen + 1;
let isNew = true; let isNew = true;
@ -29,20 +30,24 @@
function keydown(event) { function keydown(event) {
if ((event.key === 'Escape') && (level === numberOfModalsOpen)) { if ((event.key === 'Escape') && (level === numberOfModalsOpen)) {
event.preventDefault(); event.preventDefault();
show = false; close();
} }
} }
function close() {
dispatch('close');
}
</script> </script>
<svelte:window on:keydown={keydown} /> <svelte:window on:keydown={keydown} />
{#if show} {#if show}
<div class="modal outer" transition:fade on:pointerdown|self={Beep}> <div class="modal outer" on:pointerdown|self={Beep}>
<div class="inner" style:max-width={width || '80vw'} transition:fly={{ y: -100 }}> <div class="inner" style:max-width={width || '80vw'}>
{#if title} {#if title}
<header> <header>
<div class="title">{title}</div> <div class="title">{title}</div>
<button class="btn close" on:click={() => show = false} title="close" type="button"> <button class="button close" on:click={close} title="close" type="button">
<Icon name="x" /> <Icon name="x" />
</button> </button>
</header> </header>
@ -69,6 +74,7 @@
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
margin: 0; margin: 0;
padding-top: 50px; padding-top: 50px;
--wails-draggable: drag;
} }
:global(#root.platform-darwin) .outer { :global(#root.platform-darwin) .outer {
margin-top: var(--darwin-titlebar-height); margin-top: var(--darwin-titlebar-height);
@ -86,6 +92,7 @@
flex-flow: column; flex-flow: column;
cursor: auto; cursor: auto;
overflow: hidden; overflow: hidden;
--wails-draggable: nodrag;
} }
.inner > :global(*:first-child) { .inner > :global(*:first-child) {
margin-top: 0; margin-top: 0;

View File

@ -1,41 +1,17 @@
<script> <script>
import { indentWithTab } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript'; import { javascript } from '@codemirror/lang-javascript';
import { indentOnInput } from '@codemirror/language'; import { onMount } from 'svelte';
import { EditorState } from '@codemirror/state'; import CodeEditor from './codeeditor.svelte';
import { EditorView, keymap } from '@codemirror/view';
import { basicSetup } from 'codemirror';
import { createEventDispatcher, onMount } from 'svelte';
export let text = ''; export let text = '';
export let editor = undefined; export let editor = undefined;
export let readonly = false;
const dispatch = createEventDispatcher(); const extensions = [
let editorParent; javascript(),
];
const editorState = EditorState.create({
doc: '',
extensions: [
basicSetup,
keymap.of([ indentWithTab, indentOnInput ]),
javascript(),
EditorState.tabSize.of(4),
EditorView.updateListener.of(e => {
if (!e.docChanged) {
return;
}
text = e.state.doc.toString();
dispatch('updated', { text });
}),
],
});
onMount(() => { onMount(() => {
editor = new EditorView({
parent: editorParent,
state: editorState,
});
editor.dispatch({ editor.dispatch({
changes: { changes: {
from: 0, from: 0,
@ -43,23 +19,13 @@
insert: text, insert: text,
}, },
}); });
dispatch('inited', { editor });
}); });
</script> </script>
<div bind:this={editorParent} class="editor"></div> <CodeEditor bind:editor
bind:text
<style> on:inited
.editor { on:updated
width: 100%; {extensions}
background-color: #fff; {readonly}
border-radius: var(--radius); />
overflow: hidden;
}
.editor :global(.cm-editor) {
overflow: auto;
height: 100%;
}
</style>

View File

@ -8,15 +8,19 @@
export let activePath = []; export let activePath = [];
export let hideObjectIndicators = false; export let hideObjectIndicators = false;
export let getRootMenu = () => undefined; export let getRootMenu = () => undefined;
export let errorTitle = '';
const columns = [ export let errorDescription = '';
{ key: 'key', label: 'Key' }, export let busy = false;
{ key: 'value', label: 'Value' }, export let showTypes = true;
{ key: 'type', label: 'Type' },
];
let items = []; let items = [];
$: columns = [
{ key: 'key', label: 'Key' },
{ key: 'value', label: 'Value' },
showTypes ? { key: 'type', label: 'Type' } : undefined,
].filter(c => !!c);
$: if (data) { $: if (data) {
// items = dissectObject(data).map(item => ({ ...item, menu: getRootMenu(item.key, item) })); // items = dissectObject(data).map(item => ({ ...item, menu: getRootMenu(item.key, item) }));
items = []; items = [];
@ -116,4 +120,7 @@
{columns} {columns}
{items} {items}
{hideObjectIndicators} {hideObjectIndicators}
{errorTitle}
{errorDescription}
{busy}
/> />

View File

@ -1,12 +1,13 @@
<script> <script>
import { jsonLooseParse, looseJsonIsValid } from '$lib/strings'; import { looseJsonIsValid } from '$lib/strings';
import { EJSON } from 'bson';
import { createEventDispatcher, onDestroy } from 'svelte'; import { createEventDispatcher, onDestroy } from 'svelte';
import Icon from './icon.svelte'; import Icon from './icon.svelte';
import Modal from './modal.svelte'; import Modal from './modal.svelte';
import ObjectEditor from './objecteditor.svelte'; import ObjectEditor from './objecteditor.svelte';
import { EJSON } from 'bson';
export let data; export let data;
export let readonly = false;
export let saveable = false; export let saveable = false;
export let successMessage = ''; export let successMessage = '';
@ -37,21 +38,21 @@
{#if data} {#if data}
<Modal bind:show={data} contentPadding={false}> <Modal bind:show={data} contentPadding={false}>
<div class="objectviewer"> <div class="objectviewer">
<ObjectEditor bind:text on:updated={() => successMessage = ''} /> <ObjectEditor bind:text on:updated={() => successMessage = ''} {readonly} />
</div> </div>
<svelte:fragment slot="footer"> <svelte:fragment slot="footer">
{#if saveable} {#if saveable}
<button class="btn" on:click={save} disabled={invalid}> <button class="button" on:click={save} disabled={invalid}>
<Icon name="save" /> Save <Icon name="save" /> Save
</button> </button>
{/if} {/if}
<button class="btn secondary" on:click={close}> <button class="button secondary" on:click={close}>
<Icon name="x" /> Close <Icon name="x" /> Close
</button> </button>
<button class="btn secondary" on:click={copy}> <button class="button secondary" on:click={copy}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} /> Copy <Icon name={copySucceeded ? 'check' : 'clipboard'} /> Copy
</button> </button>

View File

@ -1,26 +1,39 @@
<script> <script>
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import Icon from './icon.svelte'; import Icon from './icon.svelte';
export let tabs = []; export let tabs = [];
export let selectedKey = {}; export let selectedKey = {};
export let canAddTab = false; export let canAddTab = false;
export let multiline = false;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const maxPixelsPerMultilineTab = 120;
let navEl;
let pixelsPerTab = 0;
$: tabs && navEl && updateMeasurements();
function updateMeasurements() {
pixelsPerTab = (navEl.offsetWidth ?? 0) / tabs.length;
}
function select(tabKey) { function select(tabKey) {
selectedKey = tabKey; selectedKey = tabKey;
dispatch('select', tabKey); dispatch('select', tabKey);
} }
onMount(() => {
window.addEventListener('resize', updateMeasurements);
});
</script> </script>
<nav class="tabs"> <nav class="tabs" class:multiline={multiline || (pixelsPerTab < maxPixelsPerMultilineTab)} bind:this={navEl}>
<ul> <ul>
{#each tabs as tab (tab.key)} {#each tabs as tab (tab.key)}
<li class:active={tab.key === selectedKey}> <li class:active={tab.key === selectedKey}>
<button class="tab" on:click={() => select(tab.key)}> <button class="tab" on:click={() => select(tab.key)}>
{#if tab.icon} <Icon name={tab.icon} /> {/if} {#if tab.icon} <Icon name={tab.icon} /> {/if}
{tab.title} <span class="label">{tab.title}</span>
</button> </button>
{#if tab.closable} {#if tab.closable}
<button class="button-small" on:click={() => dispatch('closeTab', tab.key)}> <button class="button-small" on:click={() => dispatch('closeTab', tab.key)}>
@ -42,13 +55,13 @@
<style> <style>
ul { ul {
overflow-x: scroll; overflow-x: auto;
display: flex; display: flex;
list-style: none; list-style: none;
} }
li { li {
display: inline-block; display: inline-block;
flex-grow: 1; flex: 1;
position: relative; position: relative;
} }
@ -89,6 +102,11 @@
cursor: not-allowed; cursor: not-allowed;
} }
nav.tabs.multiline button.tab .label {
display: block;
margin-top: 4px;
}
.button-small { .button-small {
position: absolute; position: absolute;
right: 7px; right: 7px;

View File

@ -1,16 +1,18 @@
<script> <script>
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import alink from '$lib/actions/alink'; import alink from '$lib/actions/alink';
import environment from '$lib/stores/environment';
export let show = true;
</script> </script>
<Modal bind:show width="400px" title=" "> <Modal width="400px" title=" " on:close>
<div class="brand"> <div class="brand">
<img src="/logo.png" alt="Rolens logo" /> <img src="/logo.png" alt="Rolens logo" />
<div> <div>
<div class="title">Rolens</div> <div class="title">
<div class="description">Intuitive MongoDB <br/> administration tool</div> Rolens
<span class="version">{$environment.version}</span>
</div>
<div class="description">Intuitive MongoDB <br /> administration tool</div>
</div> </div>
</div> </div>
@ -19,7 +21,7 @@
<div class="info"> <div class="info">
<p class="copy">© Romein van Buren, 2023.</p> <p class="copy">© Romein van Buren, 2023.</p>
<p> <p>
<a href="" use:alink>Documentation</a> | <a href="https://garraflavatra.github.io/rolens/" use:alink>Documentation</a> |
<a href="https://github.com/garraflavatra/rolens" use:alink>GitHub</a> | <a href="https://github.com/garraflavatra/rolens" use:alink>GitHub</a> |
<a href="https://github.com/garraflavatra/rolens/issues/new" use:alink>Report a bug</a> | <a href="https://github.com/garraflavatra/rolens/issues/new" use:alink>Report a bug</a> |
<a href="https://github.com/garraflavatra/rolens/blob/main/LICENSE" use:alink>License</a> <a href="https://github.com/garraflavatra/rolens/blob/main/LICENSE" use:alink>License</a>
@ -42,6 +44,11 @@
font-weight: 600; font-weight: 600;
line-height: 2.5rem; line-height: 2.5rem;
} }
.brand .title .version {
font-size: 80%;
font-weight: 300;
opacity: 0.65;
}
.brand .description { .brand .description {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 1.6rem; line-height: 1.6rem;

View File

@ -0,0 +1,44 @@
<script>
import Modal from '$components/modal.svelte';
import { createEventDispatcher, onMount, tick } from 'svelte';
export let title = '';
export let description = '';
export let value = '';
const dispatch = createEventDispatcher();
let input;
function submit() {
dispatch('submit', { value });
}
function close() {
dispatch('close');
}
onMount(() => tick().then(() => input.select()));
</script>
<Modal {title} on:close width="350px">
{#if description}
<p>{description}</p>
{/if}
<form on:submit|preventDefault={submit}>
<label class="field">
<input type="text" bind:value bind:this={input} spellcheck="false" />
</label>
</form>
<svelte:fragment slot="footer">
<button on:click={submit} class="button">OK</button>
<button on:click={close} class="button secondary">Cancel</button>
</svelte:fragment>
</Modal>
<style>
p {
line-height: 1.25;
}
</style>

View File

@ -3,11 +3,9 @@
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import input from '$lib/actions/input'; import input from '$lib/actions/input';
import settings from '$lib/stores/settings'; import settings from '$lib/stores/settings';
export let show = false;
</script> </script>
<Modal title="Preferences" bind:show> <Modal title="Preferences" on:close>
<div class="prefs"> <div class="prefs">
<label for="defaultLimit">Initial number of items to retrieve using one query (limit):</label> <label for="defaultLimit">Initial number of items to retrieve using one query (limit):</label>
<label class="field"> <label class="field">
@ -27,7 +25,7 @@
</span> </span>
<label for="defaultExportDirectory">Default export directory</label> <label for="defaultExportDirectory">Default export directory</label>
<!-- svelte-ignore a11y-label-has-associated-control - input is in DirectoryChooser --> <!-- svelte-ignore a11y-label-has-associated-control -->
<label class="field"> <label class="field">
<DirectoryChooser id="defaultExportDirectory" bind:value={$settings.defaultExportDirectory} /> <DirectoryChooser id="defaultExportDirectory" bind:value={$settings.defaultExportDirectory} />
</label> </label>

View File

@ -0,0 +1,34 @@
import InputDialog from '../dialogs/input.svelte';
function newDialog(dialogComponent, data = {}) {
const outlet = document.createElement('div');
outlet.className = 'dialogoutlet';
document.getElementById('dialogoutlets').appendChild(outlet);
const instance = new dialogComponent({ target: outlet, props: data });
instance.$close = function() {
instance.$destroy();
outlet.remove();
};
instance.$on('close', instance.$close);
return instance;
}
function enterText(title = '', description = '', value = '') {
const instance = newDialog(InputDialog, { title, description, value });
return new Promise(resolve => {
instance.$on('submit', event => {
instance.$close();
resolve(event.detail.value);
});
instance.$on('close', () => resolve(undefined));
});
}
const dialogs = { new: newDialog, enterText };
export default dialogs;

View File

@ -20,7 +20,7 @@
<label for="collationLocale">Locale</label> <label for="collationLocale">Locale</label>
<div class="field"> <div class="field">
<select id="collationLocale" bind:value={collation.locale}> <select id="collationLocale" bind:value={collation.locale}>
{#each Object.entries(locales) as [value, name]} {#each Object.entries(locales) as [ value, name ]}
<option {value}>({value}) {name}</option> <option {value}>({value}) {name}</option>
{/each} {/each}
</select> </select>

View File

@ -2,8 +2,10 @@ import { ObjectId } from 'bson';
import aggregationStages from './aggregation-stages.json'; import aggregationStages from './aggregation-stages.json';
import atomicUpdateOperators from './atomic-update-operators.json'; import atomicUpdateOperators from './atomic-update-operators.json';
import locales from './locales.json'; import locales from './locales.json';
import logComponents from './log-components.json';
import logLevels from './loglevels.json';
export { aggregationStages, atomicUpdateOperators, locales }; export { aggregationStages, atomicUpdateOperators, locales, logComponents, logLevels };
// Calculate the min and max values of (un)signed integers with n bits // Calculate the min and max values of (un)signed integers with n bits
export const intMin = bits => Math.pow(2, bits - 1) * -1; export const intMin = bits => Math.pow(2, bits - 1) * -1;

View File

@ -0,0 +1,36 @@
[
"ACCESS",
"COMMAND",
"CONTROL",
"ELECTION",
"FTDC",
"GEO",
"INDEX",
"INITSYNC",
"JOURNAL",
"NETWORK",
"QUERY",
"RECOVERY",
"REPL",
"REPL_HB",
"ROLLBACK",
"SHARDING",
"STORAGE",
"TXN",
"WRITE",
"WT",
"WTBACKUP",
"WTCHKPT",
"WTCMPCT",
"WTEVICT",
"WTHS",
"WTRECOV",
"WTRTS",
"WTSLVG",
"WTTIER",
"WTTS",
"WTTXN",
"WTVRFY",
"WTWRTLOG",
"-"
]

View File

@ -0,0 +1,7 @@
{
"F": "Fatal",
"E": "Error",
"W": "Warning",
"I": "Info",
"D": "Debug"
}

View File

@ -58,6 +58,18 @@ export function setValue(object, path, value) {
} }
export function deepClone(obj) { export function deepClone(obj) {
// Room for improvement below // @todo: Room for improvement below
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj));
} }
export function pathsAreEqual(x, y) {
const lengthOfLongest = (x.length >= y.length) ? x.length : y.length;
for (let i = 0; i < lengthOfLongest; i++) {
if (x[i] !== y[i]) {
return false;
}
}
return true;
}

View File

@ -1,5 +0,0 @@
import { writable } from 'svelte/store';
const connections = writable({});
export default connections;

View File

@ -1,11 +0,0 @@
import { Hosts } from "$wails/go/app/App";
import { writable } from "svelte/store";
import applicationInited from "./inited";
const { set, subscribe } = writable();
const update = async () => set(await Hosts());
applicationInited.defer(update);
const hosts = { update, subscribe };
export default hosts;

View File

@ -0,0 +1,336 @@
import dialogs from '$lib/dialogs';
import { startProgress } from '$lib/progress';
import { get, writable } from 'svelte/store';
import applicationInited from './inited';
import queries from './queries';
import windowTitle from './windowtitle';
import ExportDialog from '$organisms/connection/collection/dialogs/export.svelte';
import IndexDetailDialog from '$organisms/connection/collection/dialogs/indexdetail.svelte';
import QueryChooserDialog from '$organisms/connection/collection/dialogs/querychooser.svelte';
import DumpDialog from '$organisms/connection/database/dialogs/dump.svelte';
import HostDetailDialog from '$organisms/connection/host/dialogs/hostdetail.svelte';
import {
CreateIndex,
DropCollection,
DropDatabase,
DropIndex,
ExecuteShellScript,
GetIndexes,
HostLogs,
Hosts,
OpenCollection,
OpenConnection,
OpenDatabase,
PerformDump,
PerformFindExport,
RemoveHost,
RenameCollection,
TruncateCollection
} from '$wails/go/app/App';
const { set, subscribe } = writable({});
const getValue = () => get({ subscribe });
let hostTreeInited = false;
async function refresh() {
const hosts = await Hosts();
const hostTree = getValue();
for (const [ hostKey, hostDetails ] of Object.entries(hosts)) {
hostTree[hostKey] = hostTree[hostKey] || {};
const host = hostTree[hostKey];
host.key = hostKey;
host.name = hostDetails.name;
host.uri = hostDetails.uri;
host.open = async function() {
const { databases: dbNames, status, statusError, systemInfo, systemInfoError } = await OpenConnection(hostKey);
host.status = status;
host.statusError = statusError;
host.systemInfo = systemInfo;
host.systemInfoError = systemInfoError;
host.databases = host.databases || {};
if (!dbNames) {
return;
}
for (const dbKey of dbNames.sort((a, b) => a.localeCompare(b))) {
host.databases[dbKey] = host.databases[dbKey] || {};
}
for (const [ dbKey, database ] of Object.entries(host.databases)) {
if (!database.new && !dbNames.includes(dbKey)) {
delete host.databases[dbKey];
continue;
}
database.key = dbKey;
database.hostKey = hostKey;
database.collections = database.collections || {};
delete database.new;
database.open = async function() {
const { collections: collNames, stats, statsError } = await OpenDatabase(hostKey, dbKey);
database.stats = stats;
database.statsError = statsError;
if (!collNames) {
return;
}
for (const collKey of collNames.sort((a, b) => a.localeCompare(b))) {
database.collections[collKey] = database.collections[collKey] || {};
}
for (const [ collKey, collection ] of Object.entries(database.collections)) {
if (!collection.new && !collNames.includes(collKey)) {
delete database.collections[collKey];
continue;
}
collection.key = collKey;
collection.dbKey = dbKey;
collection.hostKey = hostKey;
collection.indexes = collection.indexes || [];
delete collection.new;
collection.open = async function() {
const { stats, statsError } = await OpenCollection(hostKey, dbKey, collKey);
collection.stats = stats;
collection.statsError = statsError;
await refresh();
};
collection.rename = async function() {
const newCollKey = await dialogs.enterText('Rename collection', `Enter a new name for collection ${collKey}.`, collKey);
if (newCollKey && (newCollKey !== collKey)) {
const progress = startProgress(`Renaming collection "${collKey}" to "${newCollKey}"…`);
const ok = await RenameCollection(hostKey, dbKey, collKey, newCollKey);
await database.open();
progress.end();
return ok;
}
};
collection.export = function(query) {
const dialog = dialogs.new(ExportDialog, { collection, query });
return new Promise(resolve => {
dialog.$on('export', async event => {
const success = await PerformFindExport(hostKey, dbKey, collKey, JSON.stringify(event.detail.exportInfo));
if (success) {
dialog.$close();
resolve();
}
});
});
};
collection.dump = function() {
const dialog = dialogs.new(DumpDialog, { info: {
hostKey,
dbKey,
collKeys: [ collKey ],
} });
return new Promise(resolve => {
dialog.$on('dump', async event => {
const success = await PerformDump(JSON.stringify(event.detail.info));
if (success) {
dialog.$close();
resolve();
}
});
});
};
collection.truncate = async function() {
await TruncateCollection(hostKey, dbKey, collKey);
await refresh();
};
collection.drop = async function() {
const success = await DropCollection(hostKey, dbKey, collKey);
if (success) {
await refresh();
}
};
collection.getIndexes = async function() {
collection.indexes = [];
const { indexes, error } = await GetIndexes(hostKey, dbKey, collKey);
if (error) {
return error;
}
for (const indexDetails of indexes) {
const index = {
name: indexDetails.name,
background: indexDetails.background || false,
unique: indexDetails.unique || false,
sparse: indexDetails.sparse || false,
model: indexDetails.model,
};
index.drop = async function() {
const hasBeenDropped = await DropIndex(hostKey, dbKey, collKey, index.name);
return hasBeenDropped;
};
collection.indexes.push(index);
}
};
collection.getIndexByName = function(indesName) {
return collection.indexes.find(idx => idx.name = indesName);
};
collection.newIndex = function() {
const dialog = dialogs.new(IndexDetailDialog, { collection });
return new Promise(resolve => {
dialog.$on('create', async event => {
const newIndexName = await CreateIndex(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(event.detail.index));
if (newIndexName) {
dialog.$close();
}
resolve(newIndexName);
});
});
};
collection.openQueryChooser = function(queryToSave = undefined) {
const dialog = dialogs.new(QueryChooserDialog, { collection, queryToSave });
return new Promise(resolve => {
dialog.$on('select', async event => {
dialog.$close();
resolve(event.detail.query);
});
dialog.$on('create', async event => {
const ok = await queries.create(event.detail.query);
if (ok) {
dialog.$close();
resolve(event.detail.query);
}
});
});
};
collection.executeShellScript = async function(script) {
const result = await ExecuteShellScript(hostKey, dbKey, collKey, script);
return result;
};
}
await refresh();
windowTitle.setSegments(dbKey, host.name, 'Rolens');
};
database.dump = function() {
const dialog = dialogs.new(DumpDialog, { info: { hostKey, dbKey } });
return new Promise(resolve => {
dialog.$on('dump', async event => {
const success = await PerformDump(JSON.stringify(event.detail.info));
if (success) {
dialog.$close();
resolve();
}
});
});
};
database.drop = async function() {
const progress = startProgress(`Dropping database "${dbKey}"…`);
const success = await DropDatabase(hostKey, dbKey);
if (success) {
await refresh();
}
progress.end();
};
database.newCollection = async function() {
const name = await dialogs.enterText('Create a collection', 'Note: collections in MongoDB do not exist until they have at least one item. Your new collection will not persist on the server; fill it to have it created.', '');
if (name) {
database.collections[name] = { key: name, new: true };
await database.open();
}
};
database.executeShellScript = async function(script) {
const result = await ExecuteShellScript(hostKey, dbKey, '', script);
return result;
};
}
await refresh();
};
host.executeShellScript = async function(script) {
const result = await ExecuteShellScript(hostKey, '', '', script);
return result;
};
host.newDatabase = async function() {
const name = await dialogs.enterText('Create a database', 'Enter the database name. Note: databases in MongoDB do not exist until they have a collection and an item. Your new database will not persist on the server; fill it to have it created.', '');
if (name) {
host.databases[name] = { key: name, new: true };
await host.open();
}
};
host.edit = async function() {
const dialog = dialogs.new(HostDetailDialog, { hostKey });
return new Promise(resolve => {
dialog.$on('close', () => {
refresh().then(resolve);
});
});
};
host.getLogs = async function(filter = 'global') {
return await HostLogs(hostKey, filter);
};
host.remove = async function() {
await RemoveHost(hostKey);
await refresh();
};
}
hostTreeInited = true;
set(hostTree);
}
function newHost() {
const dialog = dialogs.new(HostDetailDialog, { hostKey: '' });
return new Promise(resolve => {
dialog.$on('close', () => {
refresh().then(resolve);
});
});
}
applicationInited.defer(refresh);
const hostTree = {
refresh,
subscribe,
get: getValue,
newHost,
hasBeenInited: () => hostTreeInited,
};
export default hostTree;

View File

@ -10,7 +10,7 @@ const defer = listener => {
listener(); listener();
} }
else { else {
listeners.push(listener) listeners.push(listener);
} }
}; };
@ -18,16 +18,12 @@ const { subscribe } = derived([ environment, applicationSettings ], ([ env, sett
if (alreadyInited) { if (alreadyInited) {
return; return;
} }
else if (env && settings) {
if (env && settings) { Promise.all(listeners.map(l => l())).then(() => {
set(true); set(true);
alreadyInited = true; alreadyInited = true;
document.getElementById('app-loading')?.remove();
// Remove loading spinner. });
document.getElementById('app-loading')?.remove();
// Call hooks
listeners.forEach(l => l());
} }
}, false); }, false);

View File

@ -1,3 +1,5 @@
import dialogs from '$lib/dialogs';
import ViewConfigDialog from '$organisms/connection/collection/dialogs/viewconfig.svelte';
import { UpdateViewStore, Views } from '$wails/go/app/App'; import { UpdateViewStore, Views } from '$wails/go/app/App';
import { get, writable } from 'svelte/store'; import { get, writable } from 'svelte/store';
@ -23,6 +25,11 @@ function forCollection(hostKey, dbKey, collKey) {
return Object.fromEntries(entries); return Object.fromEntries(entries);
} }
function openConfig(collection, firstItem = {}) {
const dialog = dialogs.new(ViewConfigDialog, { collection, firstItem });
return dialog;
}
reload(); reload();
subscribe(newViewStore => { subscribe(newViewStore => {
if (skipUpdate) { if (skipUpdate) {
@ -32,5 +39,5 @@ subscribe(newViewStore => {
UpdateViewStore(JSON.stringify(newViewStore)); UpdateViewStore(JSON.stringify(newViewStore));
}); });
const views = { reload, set, subscribe, forCollection }; const views = { reload, set, subscribe, forCollection, openConfig };
export default views; export default views;

View File

@ -0,0 +1,14 @@
import { WindowSetTitle } from '$wails/runtime/runtime';
import { writable } from 'svelte/store';
const { set, subscribe } = writable('Rolens');
subscribe(newTitle => WindowSetTitle(newTitle));
const windowTitle = {
set,
setSegments: (...segments) => set(segments.map(s => s.trim()).join(' — ')),
subscribe,
};
export default windowTitle;

View File

@ -2,9 +2,9 @@
import Details from '$components/details.svelte'; import Details from '$components/details.svelte';
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import Collation from '$lib/mongo/collation.svelte';
import ObjectEditor from '$components/objecteditor.svelte'; import ObjectEditor from '$components/objecteditor.svelte';
import { aggregationStageDocumentationURL, aggregationStages } from '$lib/mongo'; import { aggregationStageDocumentationURL, aggregationStages } from '$lib/mongo';
import Collation from '$lib/mongo/collation.svelte';
import { jsonLooseParse, looseJsonIsValid } from '$lib/strings'; import { jsonLooseParse, looseJsonIsValid } from '$lib/strings';
import { Aggregate } from '$wails/go/app/App'; import { Aggregate } from '$wails/go/app/App';
import { BrowserOpenURL } from '$wails/runtime/runtime'; import { BrowserOpenURL } from '$wails/runtime/runtime';
@ -31,7 +31,9 @@
} }
async function run() { async function run() {
const pipeline = stages.map(stage => ({ [stage.type]: jsonLooseParse(stage.data) })); const pipeline = stages.map(stage => {
return { [stage.type]: jsonLooseParse(stage.data) };
});
await Aggregate( await Aggregate(
collection.hostKey, collection.hostKey,
collection.dbKey, collection.dbKey,
@ -59,26 +61,27 @@
<option {value}>{value}</option> <option {value}>{value}</option>
{/each} {/each}
</select> </select>
<button class="btn secondary" type="button" on:click={() => openStageDocs(stage.type)} title="Open documentation about {stage.type || 'this stage'} on mongodb.org"> <button class="button secondary" type="button" on:click={() => openStageDocs(stage.type)} title="Open documentation about {stage.type || 'this stage'} on mongodb.org">
<Icon name="?" /> <Icon name="?" />
</button> </button>
</label> </label>
<!-- svelte-ignore a11y-label-has-associated-control --> <!-- svelte-ignore a11y-label-has-associated-control -->
<label class="field"> <label class="field">
<ObjectEditor bind:text={stage.data} on:inited={e => { <ObjectEditor bind:text={stage.data}
e.detail.editor.dispatch({ on:inited={e => {
changes: { e.detail.editor.dispatch({
from: 0, changes: {
to: e.detail.editor.state.doc.length, from: 0,
insert: '{\n\t\n}', to: e.detail.editor.state.doc.length,
}, insert: '{\n\t\n}',
selection: { },
anchor: 3, selection: {
}, anchor: 3,
}); },
e.detail.editor.focus(); });
}} /> e.detail.editor.focus();
}} />
</label> </label>
</Details> </Details>
{/each} {/each}
@ -90,17 +93,17 @@
<div class="controls"> <div class="controls">
<div> <div>
<button class="btn" type="submit" disabled={invalid}> <button class="button" type="submit" disabled={invalid}>
<Icon name="play" /> Run pipeline <Icon name="play" /> Run pipeline
</button> </button>
<button class="btn" type="button" on:click={() => settingsModalOpen = true}> <button class="button" type="button" on:click={() => settingsModalOpen = true}>
<Icon name="cog" /> Settings <Icon name="cog" /> Settings
</button> </button>
</div> </div>
</div> </div>
</form> </form>
<Modal title="Advanced aggregation settings" bind:show={settingsModalOpen}> <Modal title="Advanced aggregation settings" show={settingsModalOpen} on:close={() => settingsModalOpen = false}>
<div class="settinggrid"> <div class="settinggrid">
<label for="allowDiskUse">Allow disk use</label> <label for="allowDiskUse">Allow disk use</label>
<div class="field"> <div class="field">

View File

@ -1,74 +0,0 @@
<script>
import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte';
import views from '$lib/stores/views';
import { PerformFindExport } from '$wails/go/app/App';
import { createEventDispatcher } from 'svelte';
export let info;
export let collection;
const dispatch = createEventDispatcher();
let viewKey = collection.viewKey;
$: viewKey = collection.viewKey;
$: if (info) {
info.viewKey = viewKey;
}
async function performExport() {
info.view = $views[viewKey];
const success = await PerformFindExport(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(info));
if (success) {
info = undefined;
}
}
</script>
<Modal bind:show={info} title="Export results" width="450px">
<form on:submit|preventDefault={performExport}>
<label class="field">
<span class="label">Export</span>
<select bind:value={info.contents}>
<option value="all">all records</option>
<option value="query">all records matching query</option>
<option value="querylimitskip">all records matching query, considering limit and skip</option>
</select>
</label>
<label class="field">
<span class="label">Format</span>
<select bind:value={info.format}>
<option value="jsonarray">JSON array</option>
<option value="ndjson">Newline delimited JSON</option>
<option value="csv">CSV</option>
</select>
</label>
<label class="field">
<span class="label">View to use</span>
<select bind:value={viewKey}>
{#each Object.entries(views.forCollection(collection.hostKey, collection.dbKey, collection.key)) as [key, { name }]}
<option value={key}>{name}</option>
{/each}
</select>
<button class="btn" type="button" on:click={() => dispatch('openViewConfig')} title="Edit view">
<Icon name="cog" />
</button>
</label>
</form>
<svelte:fragment slot="footer">
<button class="btn" on:click={performExport}>
<Icon name="play" /> Start export
</button>
</svelte:fragment>
</Modal>
<style>
form {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
</style>

View File

@ -1,9 +1,9 @@
<script> <script>
import FormInput from '$components/forminput.svelte'; import FormInput from '$components/forminput.svelte';
import Hint from '$components/hint.svelte';
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import { inputTypes } from '$lib/mongo'; import { inputTypes } from '$lib/mongo';
import { resolveKeypath, setValue } from '$lib/objects'; import { resolveKeypath, setValue } from '$lib/objects';
import Hint from '$components/hint.svelte';
export let item = {}; export let item = {};
export let view = {}; export let view = {};
@ -38,7 +38,7 @@
{#if item && view} {#if item && view}
{#each view?.columns?.filter(c => inputTypes.includes(c.inputType)) || [] as column, index} {#each view?.columns?.filter(c => inputTypes.includes(c.inputType)) || [] as column, index}
<!-- svelte-ignore a11y-label-has-associated-control because FormInput contains one --> <!-- svelte-ignore a11y-label-has-associated-control -->
<label class="column"> <label class="column">
<div class="label"> <div class="label">
<Icon name={iconMap[column.inputType]} /> <Icon name={iconMap[column.inputType]} />

View File

@ -0,0 +1,66 @@
<script>
import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte';
import views from '$lib/stores/views';
import { createEventDispatcher } from 'svelte';
export let collection;
export let query = {};
const dispatch = createEventDispatcher();
const exportInfo = { ...query, viewKey: collection.viewKey };
function submit() {
exportInfo.view = $views[exportInfo.viewKey];
dispatch('export', { exportInfo });
}
</script>
<Modal title="Export results" width="450px" on:close>
<form on:submit|preventDefault={submit}>
<label class="field">
<span class="label">Export</span>
<select bind:value={exportInfo.contents}>
<option value="all">all records</option>
<option value="query" disabled={!query}>all records matching query</option>
<option value="querylimitskip" disabled={!query}>all records matching query, considering limit and skip</option>
</select>
</label>
<label class="field">
<span class="label">Format</span>
<select bind:value={exportInfo.format}>
<option value="jsonarray">JSON array (*.json)</option>
<option value="ndjson">Newline delimited JSON (*.ndjson)</option>
<option value="csv">CSV (*.csv)</option>
<option value="excel">Excel (*.xlsx)</option>
</select>
</label>
<label class="field">
<span class="label">View to use</span>
<select bind:value={exportInfo.viewKey}>
{#each Object.entries(views.forCollection(collection.hostKey, collection.dbKey, collection.key)) as [ key, { name } ]}
<option value={key}>{name}</option>
{/each}
</select>
<button class="button" type="button" on:click={() => dispatch('openViewConfig')} title="Edit view">
<Icon name="cog" />
</button>
</label>
</form>
<svelte:fragment slot="footer">
<button class="button" on:click={submit}>
<Icon name="play" /> Start export
</button>
</svelte:fragment>
</Modal>
<style>
form {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
</style>

View File

@ -2,14 +2,12 @@
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import input from '$lib/actions/input'; import input from '$lib/actions/input';
import { CreateIndex } from '$wails/go/app/App';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
export let collection = {}; export let collection;
export let creatingNewIndex = false;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let index = { model: [] }; const index = { model: [] };
function addRule() { function addRule() {
index.model = [ ...index.model, {} ]; index.model = [ ...index.model, {} ];
@ -21,16 +19,11 @@
} }
async function create() { async function create() {
const newIndexName = await CreateIndex(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(index)); dispatch('create', { index });
if (newIndexName) {
creatingNewIndex = false;
index = { model: [] };
dispatch('reload');
}
} }
</script> </script>
<Modal title="Create new index {collection ? `on collection ${collection.key}` : ''}" bind:show={creatingNewIndex}> <Modal title="Create new index on {collection.key}" on:close>
<form on:submit|preventDefault={create}> <form on:submit|preventDefault={create}>
<label class="field name"> <label class="field name">
<span class="label">Name</span> <span class="label">Name</span>
@ -78,7 +71,7 @@
<option value="hashed" disabled={index.model.length > 1}>Hashed</option> <option value="hashed" disabled={index.model.length > 1}>Hashed</option>
</select> </select>
</label> </label>
<button type="button" class="btn danger" on:click={() => removeRule(ruleIndex)} disabled={index.model.length < 2}> <button type="button" class="button danger" on:click={() => removeRule(ruleIndex)} disabled={index.model.length < 2}>
<Icon name="-" /> <Icon name="-" />
</button> </button>
</div> </div>
@ -89,10 +82,10 @@
</form> </form>
<div class="buttons" slot="footer"> <div class="buttons" slot="footer">
<button class="btn" on:click={addRule} disabled={index.model.some(r => r.sort === 'hashed')}> <button class="button" on:click={addRule} disabled={index.model.some(r => r.sort === 'hashed')}>
<Icon name="+" /> Add rule <Icon name="+" /> Add rule
</button> </button>
<button class="btn" on:click={create} disabled={!index.model.length || index.model.some(r => !r.key)}> <button class="button" on:click={create} disabled={!index.model.length || index.model.some(r => !r.key)}>
<Icon name="+" /> Create index <Icon name="+" /> Create index
</button> </button>
</div> </div>

View File

@ -4,13 +4,12 @@
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import input from '$lib/actions/input'; import input from '$lib/actions/input';
import hosts from '$lib/stores/hosts'; import hostTree from '$lib/stores/hosttree';
import queries from '$lib/stores/queries'; import queries from '$lib/stores/queries';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
export let queryToSave = undefined; export let queryToSave = undefined;
export let collection = {}; export let collection = {};
export let show = false;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let gridSelectedPath = []; let gridSelectedPath = [];
@ -22,23 +21,16 @@
queryToSave.dbKey = collection.dbKey; queryToSave.dbKey = collection.dbKey;
queryToSave.collKey = collection.key; queryToSave.collKey = collection.key;
const newId = queries.create(queryToSave); dispatch('create', { query: queryToSave });
selectedKey = queryToSave.name;
if (newId) {
dispatch('created', newId);
queryToSave = undefined;
selectedKey = newId;
select();
}
} }
else { else {
select(); selectActive();
} }
} }
function select() { function selectActive() {
dispatch('select', selectedKey); dispatch('select', { query: $queries[selectedKey] });
show = false;
} }
function gridSelect(event) { function gridSelect(event) {
@ -53,7 +45,7 @@
function gridTrigger(event) { function gridTrigger(event) {
gridSelect(event); gridSelect(event);
select(); selectActive();
} }
async function gridRemove(event) { async function gridRemove(event) {
@ -71,7 +63,7 @@
} }
</script> </script>
<Modal bind:show title={queryToSave ? 'Save query' : 'Load query'} width="500px"> <Modal title={queryToSave ? 'Save query' : 'Load query'} width="500px" on:close>
<form on:submit|preventDefault={submit}> <form on:submit|preventDefault={submit}>
{#if queryToSave} {#if queryToSave}
<label class="field queryname"> <label class="field queryname">
@ -88,7 +80,11 @@
columns={[ { key: 'n', title: 'Query name' }, { key: 'h', title: 'Host' }, { key: 'ns', title: 'Namespace' } ]} columns={[ { key: 'n', title: 'Query name' }, { key: 'h', title: 'Host' }, { key: 'ns', title: 'Namespace' } ]}
key="n" key="n"
items={Object.entries($queries).reduce((object, [ name, query ]) => { items={Object.entries($queries).reduce((object, [ name, query ]) => {
object[query.name] = { n: name, h: $hosts[query.hostKey]?.name || '?', ns: `${query.dbKey}.${query.collKey}` }; object[query.name] = {
n: name,
h: $hostTree[query.hostKey]?.name || '?',
ns: `${query.dbKey}.${query.collKey}`,
};
return object; return object;
}, {})} }, {})}
showHeaders={true} showHeaders={true}
@ -110,11 +106,11 @@
<svelte:fragment slot="footer"> <svelte:fragment slot="footer">
{#if queryToSave} {#if queryToSave}
<button class="btn" on:click={submit}> <button class="button" on:click={submit}>
<Icon name="save" /> Save query <Icon name="save" /> Save query
</button> </button>
{:else} {:else}
<button class="btn" on:click={submit} disabled={!selectedKey}> <button class="button" on:click={submit} disabled={!selectedKey}>
<Icon name="upload" /> Load query <Icon name="upload" /> Load query
</button> </button>
{/if} {/if}
@ -135,7 +131,7 @@
min-height: 200px; min-height: 200px;
} }
.btn + :global(.hint) { .button + :global(.hint) {
margin-top: 0.5rem; margin-top: 0.5rem;
} }
</style> </style>

View File

@ -7,13 +7,18 @@
import views from '$lib/stores/views'; import views from '$lib/stores/views';
export let collection; export let collection;
export let show = false;
export let activeViewKey = 'list';
export let firstItem = {}; export let firstItem = {};
$: tabs = Object.entries(views.forCollection(collection.hostKey, collection.dbKey, collection.key)) let tabs = [];
.sort((a, b) => sortTabKeys(a[0], b[0])) $: $views && refresh();
.map(([ key, v ]) => ({ key, title: v.name, closable: key !== 'list' }));
function refresh() {
tabs = Object.entries(views.forCollection(collection.hostKey, collection.dbKey, collection.key))
.sort((a, b) => sortTabKeys(a[0], b[0]))
.map(([ key, v ]) => {
return { key, title: v.name, closable: key !== 'list' };
});
}
function sortTabKeys(a, b) { function sortTabKeys(a, b) {
if (a === 'list') { if (a === 'list') {
@ -37,29 +42,29 @@
type: 'table', type: 'table',
columns: [ { key: '_id', showInTable: true, inputType: 'objectid', mandatory: true } ], columns: [ { key: '_id', showInTable: true, inputType: 'objectid', mandatory: true } ],
}; };
activeViewKey = newViewKey; collection.viewKey = newViewKey;
} }
function removeView(viewKey) { function removeView(viewKey) {
const keys = Object.keys($views).sort(sortTabKeys); const keys = Object.keys($views).sort(sortTabKeys);
const oldIndex = keys.indexOf(viewKey); const oldIndex = keys.indexOf(viewKey);
const newKey = keys[oldIndex - 1]; const newKey = keys[oldIndex - 1];
activeViewKey = newKey; collection.viewKey = newKey;
delete $views[viewKey]; delete $views[viewKey];
$views = $views; $views = $views;
} }
function addColumn(before) { function addColumn(before) {
if (typeof before === 'number') { if (typeof before === 'number') {
$views[activeViewKey].columns = [ $views[collection.viewKey].columns = [
...$views[activeViewKey].columns.slice(0, before), ...$views[collection.viewKey].columns.slice(0, before),
{ showInTable: true, inputType: 'none' }, { showInTable: true, inputType: 'none' },
...$views[activeViewKey].columns.slice(before), ...$views[collection.viewKey].columns.slice(before),
]; ];
} }
else { else {
$views[activeViewKey].columns = [ $views[collection.viewKey].columns = [
...$views[activeViewKey].columns, ...$views[collection.viewKey].columns,
{ showInTable: true, inputType: 'none' }, { showInTable: true, inputType: 'none' },
]; ];
} }
@ -70,65 +75,67 @@
return; return;
} }
$views[activeViewKey].columns = Object.keys(firstItem).sort().map(key => ({ $views[collection.viewKey].columns = Object.keys(firstItem).sort().map(key => {
key, return {
showInTable: true, key,
inputType: 'none', showInTable: true,
})); inputType: 'none',
};
});
} }
function moveColumn(oldIndex, delta) { function moveColumn(oldIndex, delta) {
const column = $views[activeViewKey].columns[oldIndex]; const column = $views[collection.viewKey].columns[oldIndex];
const newIndex = oldIndex + delta; const newIndex = oldIndex + delta;
$views[activeViewKey].columns.splice(oldIndex, 1); $views[collection.viewKey].columns.splice(oldIndex, 1);
$views[activeViewKey].columns.splice(newIndex, 0, column); $views[collection.viewKey].columns.splice(newIndex, 0, column);
$views[activeViewKey].columns = $views[activeViewKey].columns; $views[collection.viewKey].columns = $views[collection.viewKey].columns;
} }
function removeColumn(index) { function removeColumn(index) {
$views[activeViewKey].columns.splice(index, 1); $views[collection.viewKey].columns.splice(index, 1);
$views[activeViewKey].columns = $views[activeViewKey].columns; $views[collection.viewKey].columns = $views[collection.viewKey].columns;
} }
</script> </script>
<Modal title="View configuration" bind:show contentPadding={false}> <Modal title="View configuration" contentPadding={false} on:close>
<TabBar <TabBar
{tabs} {tabs}
canAddTab={true} canAddTab={true}
on:addTab={createView} on:addTab={createView}
on:closeTab={e => removeView(e.detail)} on:closeTab={e => removeView(e.detail)}
bind:selectedKey={activeViewKey} bind:selectedKey={collection.viewKey}
/> />
<div class="options"> <div class="options">
{#if $views[activeViewKey]} {#if $views[collection.viewKey]}
<div class="meta"> <div class="meta">
{#key activeViewKey} {#key collection.viewKey}
<label class="field"> <label class="field">
<span class="label">View name</span> <span class="label">View name</span>
<input type="text" use:input={{ autofocus: true }} bind:value={$views[activeViewKey].name} disabled={activeViewKey === 'list'} /> <input type="text" use:input={{ autofocus: true }} bind:value={$views[collection.viewKey].name} disabled={collection.viewKey === 'list'} />
</label> </label>
{/key} {/key}
<label class="field"> <label class="field">
<span class="label">View type</span> <span class="label">View type</span>
<select bind:value={$views[activeViewKey].type} disabled> <select bind:value={$views[collection.viewKey].type} disabled>
<option value="list">List view</option> <option value="list">List view</option>
<option value="table">Table view</option> <option value="table">Table view</option>
</select> </select>
</label> </label>
</div> </div>
{#if $views[activeViewKey].type === 'list'} {#if $views[collection.viewKey].type === 'list'}
<div class="flex"> <div class="flex">
<input type="checkbox" id="hideObjectIndicators" bind:checked={$views[activeViewKey].hideObjectIndicators} /> <input type="checkbox" id="hideObjectIndicators" bind:checked={$views[collection.viewKey].hideObjectIndicators} />
<label for="hideObjectIndicators"> <label for="hideObjectIndicators">
Hide object indicators ({'{...}'} and [...]) in list view and show nothing instead Hide object indicators ({'{...}'} and [...]) in list view and show nothing instead
</label> </label>
</div> </div>
{:else if $views[activeViewKey].type === 'table'} {:else if $views[collection.viewKey].type === 'table'}
<div class="columns"> <div class="columns">
{#each $views[activeViewKey].columns as column, columnIndex} {#each $views[collection.viewKey].columns as column, columnIndex}
<div class="column"> <div class="column">
<label class="field"> <label class="field">
<input type="text" use:input bind:value={column.key} placeholder="Column keypath" /> <input type="text" use:input bind:value={column.key} placeholder="Column keypath" />
@ -178,16 +185,16 @@
</span> </span>
</label> </label>
<button class="btn" type="button" on:click={() => addColumn(columnIndex)} title="Add column before this one"> <button class="button" type="button" on:click={() => addColumn(columnIndex)} title="Add column before this one">
<Icon name="+" /> <Icon name="+" />
</button> </button>
<button class="btn" type="button" on:click={() => moveColumn(columnIndex, -1)} disabled={columnIndex === 0} title="Move column one position up"> <button class="button" type="button" on:click={() => moveColumn(columnIndex, -1)} disabled={columnIndex === 0} title="Move column one position up">
<Icon name="chev-u" /> <Icon name="chev-u" />
</button> </button>
<button class="btn" type="button" on:click={() => moveColumn(columnIndex, 1)} disabled={columnIndex === $views[activeViewKey].columns.length - 1} title="Move column one position down"> <button class="button" type="button" on:click={() => moveColumn(columnIndex, 1)} disabled={columnIndex === $views[collection.viewKey].columns.length - 1} title="Move column one position down">
<Icon name="chev-d" /> <Icon name="chev-d" />
</button> </button>
<button class="btn danger" type="button" on:click={() => removeColumn(columnIndex)} title="Remove this column"> <button class="button danger" type="button" on:click={() => removeColumn(columnIndex)} title="Remove this column">
<Icon name="x" /> <Icon name="x" />
</button> </button>
</div> </div>
@ -195,10 +202,10 @@
<p>No columns yet</p> <p>No columns yet</p>
{/each} {/each}
</div> </div>
<button class="btn" on:click={addColumn}> <button class="button" on:click={addColumn}>
<Icon name="+" /> Add column <Icon name="+" /> Add column
</button> </button>
<button class="btn" on:click={addSuggestedColumns} disabled={!Object.keys(firstItem || {}).length}> <button class="button" on:click={addSuggestedColumns} disabled={!Object.keys(firstItem || {}).length}>
<Icon name="zap" /> Add suggested columns <Icon name="zap" /> Add suggested columns
</button> </button>
{/if} {/if}

View File

@ -6,18 +6,15 @@
import input from '$lib/actions/input'; import input from '$lib/actions/input';
import { deepClone } from '$lib/objects'; import { deepClone } from '$lib/objects';
import { startProgress } from '$lib/progress'; import { startProgress } from '$lib/progress';
import queries from '$lib/stores/queries';
import applicationSettings from '$lib/stores/settings'; import applicationSettings from '$lib/stores/settings';
import views from '$lib/stores/views'; import views from '$lib/stores/views';
import { convertLooseJson } from '$lib/strings';
import { FindItems, RemoveItemById, UpdateFoundDocument } from '$wails/go/app/App'; import { FindItems, RemoveItemById, UpdateFoundDocument } from '$wails/go/app/App';
import { EJSON } from 'bson'; import { EJSON } from 'bson';
import { createEventDispatcher, onMount } from 'svelte'; import { onMount } from 'svelte';
import ExportInfo from './components/export.svelte';
import QueryChooser from './components/querychooser.svelte';
export let collection; export let collection;
const dispatch = createEventDispatcher();
const defaults = { const defaults = {
query: '{}', query: '{}',
sort: $applicationSettings.defaultSort || '{ "_id": 1 }', sort: $applicationSettings.defaultSort || '{ "_id": 1 }',
@ -32,26 +29,32 @@
let queryField; let queryField;
let activePath = []; let activePath = [];
let objectViewerData; let objectViewerData;
let queryToSave;
let showQueryChooser = false;
let exportInfo;
let querying = false; let querying = false;
let objectViewerSuccessMessage = ''; let objectViewerSuccessMessage = '';
let viewsForCollection = {};
$: viewsForCollection = views.forCollection(collection.hostKey, collection.dbKey, collection.key); // $: code = `db.${collection.key}.find(${form.query || '{}'}${form.fields && form.fields !== '{}' ? `, ${form.fields}` : ''}).sort(${form.sort})${form.skip ? `.skip(${form.skip})` : ''}${form.limit ? `.limit(${form.limit})` : ''};`;
$: code = `db.${collection.key}.find(${form.query || '{}'}${form.fields && form.fields !== '{}' ? `, ${form.fields}` : ''}).sort(${form.sort})${form.skip ? `.skip(${form.skip})` : ''}${form.limit ? `.limit(${form.limit})` : ''};`;
$: lastPage = (submittedForm.limit && result?.results?.length) ? Math.max(0, Math.ceil((result.total - submittedForm.limit) / submittedForm.limit)) : 0; $: lastPage = (submittedForm.limit && result?.results?.length) ? Math.max(0, Math.ceil((result.total - submittedForm.limit) / submittedForm.limit)) : 0;
$: activePage = (submittedForm.limit && submittedForm.skip && result?.results?.length) ? submittedForm.skip / submittedForm.limit : 0; $: activePage = (submittedForm.limit && submittedForm.skip && result?.results?.length) ? submittedForm.skip / submittedForm.limit : 0;
$: if ($views) {
viewsForCollection = views.forCollection(collection.hostKey, collection.dbKey, collection.key);
}
async function submitQuery() { async function submitQuery() {
if (querying) { if (querying) {
return; return;
} }
querying = true; querying = `Querying ${collection.key}…`;
const progress = startProgress('Performing query…');
activePath = []; activePath = [];
const newResult = await FindItems(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(form)); const newResult = await FindItems(collection.hostKey, collection.dbKey, collection.key, JSON.stringify({
fields: convertLooseJson(form.fields || defaults.fields),
limit: form.limit ?? defaults.limit,
query: convertLooseJson(form.query) || defaults.query,
skip: form.skip ?? defaults.skip,
sort: convertLooseJson(form.sort) || defaults.sort,
}));
if (newResult) { if (newResult) {
newResult.results = newResult.results?.map(s => EJSON.parse(s, { relaxed: false })); newResult.results = newResult.results?.map(s => EJSON.parse(s, { relaxed: false }));
@ -59,7 +62,6 @@
submittedForm = deepClone(form); submittedForm = deepClone(form);
} }
progress.end();
resetFocus(); resetFocus();
querying = false; querying = false;
} }
@ -70,19 +72,18 @@
} }
} }
function loadQuery() { async function loadQuery() {
queryToSave = undefined; const query = await collection.openQueryChooser();
showQueryChooser = true; if (query) {
form = { ...query };
submitQuery();
}
} }
function saveQuery() { async function saveQuery() {
queryToSave = form; const query = await collection.openQueryChooser(form);
showQueryChooser = true; if (query) {
} form = { ...query };
function queryChosen(event) {
if ($queries[event?.detail]) {
form = { ...$queries[event?.detail] };
submitQuery(); submitQuery();
} }
} }
@ -130,6 +131,10 @@
objectViewerData = item; objectViewerData = item;
} }
function openViewConfig() {
views.openConfig(collection, result.results?.[0] || {});
}
export function performQuery(q) { export function performQuery(q) {
form = { ...defaults, ...q }; form = { ...defaults, ...q };
submitQuery(); submitQuery();
@ -142,7 +147,7 @@
collection.dbKey, collection.dbKey,
collection.key, collection.key,
EJSON.stringify({ _id: event.detail.originalData._id }), EJSON.stringify({ _id: event.detail.originalData._id }),
event.detail.text convertLooseJson(event.detail.text)
); );
if (success) { if (success) {
@ -159,47 +164,83 @@
<div class="find"> <div class="find">
<form on:submit|preventDefault={submitQuery}> <form on:submit|preventDefault={submitQuery}>
<div class="form-row one"> <div class="formrow one">
<label class="field"> <label class="field">
<span class="label">Query or id</span> <span class="label">Query or id</span>
<input type="text" class="code" bind:this={queryField} bind:value={form.query} use:input={{ type: 'json', autofocus: true }} placeholder={defaults.query} /> <input type="text"
class="code"
placeholder={defaults.query}
autocomplete="off"
spellcheck="false"
use:input={{ type: 'json', autofocus: true }}
bind:this={queryField}
bind:value={form.query}
/>
</label> </label>
<label class="field"> <label class="field">
<span class="label">Sort</span> <span class="label">Sort</span>
<input type="text" class="code" bind:value={form.sort} use:input={{ type: 'json' }} placeholder={defaults.sort} /> <input
type="text"
class="code"
placeholder={defaults.sort}
autocomplete="off"
spellcheck="false"
bind:value={form.sort}
use:input={{ type: 'json' }}
/>
</label> </label>
</div> </div>
<div class="form-row two"> <div class="formrow two">
<label class="field"> <label class="field">
<span class="label">Fields</span> <span class="label">Fields</span>
<input type="text" class="code" bind:value={form.fields} use:input={{ type: 'json' }} placeholder={defaults.fields} /> <input
type="text"
class="code"
placeholder={defaults.fields}
autocomplete="off"
spellcheck="false"
bind:value={form.fields}
use:input={{ type: 'json' }}
/>
</label> </label>
<label class="field"> <label class="field">
<span class="label">Skip</span> <span class="label">Skip</span>
<input type="number" min="0" bind:value={form.skip} use:input placeholder={defaults.skip} list="skipstops" /> <input type="number"
min="0"
bind:value={form.skip}
use:input
placeholder={defaults.skip}
list="skipstops"
/>
</label> </label>
<label class="field"> <label class="field">
<span class="label">Limit</span> <span class="label">Limit</span>
<input type="number" min="0" bind:value={form.limit} use:input placeholder={defaults.limit} list="limits" /> <input type="number"
min="0"
bind:value={form.limit}
use:input
placeholder={defaults.limit}
list="limits"
/>
</label> </label>
</div> </div>
<div class="form-row actions"> <div class="formrow actions">
<button type="submit" class="btn" title="Run query"> <button type="submit" class="button" title="Run query">
<Icon name="play" /> Run <Icon name="play" /> Run
</button> </button>
<button class="btn secondary" type="button" on:click={() => exportInfo = {}}> <button class="button secondary" type="button" on:click={() => collection.export(form)}>
<Icon name="save" /> Export results… <Icon name="save" /> Export results…
</button> </button>
<div class="field"> <div class="field">
<button class="btn secondary" type="button" on:click={loadQuery}> <button class="button secondary" type="button" on:click={loadQuery}>
<Icon name="upload" /> Load query… <Icon name="upload" /> Load query…
</button> </button>
<button class="btn secondary" type="button" on:click={saveQuery}> <button class="button secondary" type="button" on:click={saveQuery}>
<Icon name="save" /> Save as… <Icon name="save" /> Save as…
</button> </button>
</div> </div>
@ -215,15 +256,25 @@
hideObjectIndicators={$views[collection.viewKey]?.hideObjectIndicators} hideObjectIndicators={$views[collection.viewKey]?.hideObjectIndicators}
bind:activePath bind:activePath
on:trigger={e => openJson(e.detail?.index)} on:trigger={e => openJson(e.detail?.index)}
errorTitle={result.errorTitle}
errorDescription={result.errorDescription}
busy={querying}
/> />
{:else} {:else}
<Grid <Grid
key="_id" key="_id"
columns={$views[collection.viewKey]?.columns?.map(c => ({ key: c.key, title: c.key })) || []} columns={$views[collection.viewKey]?.columns
?.filter(c => c.showInTable)
.map(c => {
return { key: c.key, title: c.key };
}) || []}
showHeaders={true} showHeaders={true}
items={result.results ? result.results.map(r => EJSON.deserialize(r)) : []} items={result.results ? result.results.map(r => EJSON.deserialize(r)) : []}
bind:activePath bind:activePath
on:trigger={e => openJson(e.detail?.index)} on:trigger={e => openJson(e.detail?.index)}
errorTitle={result.errorTitle}
errorDescription={result.errorDescription}
busy={querying}
/> />
{/if} {/if}
{/key} {/key}
@ -238,27 +289,27 @@
<div> <div>
<label class="field inline"> <label class="field inline">
<select bind:value={collection.viewKey}> <select bind:value={collection.viewKey}>
{#each Object.entries(viewsForCollection) as [key, view]} {#each Object.entries(viewsForCollection) as [ key, view ]}
<option value={key}>{view.name}</option> <option value={key}>{view.name}</option>
{/each} {/each}
</select> </select>
<button class="btn" on:click={() => dispatch('openViewConfig', { firstItem: result.results?.[0] })} title="Configure view"> <button class="button" on:click={openViewConfig} title="Configure view">
<Icon name="cog" /> <Icon name="cog" />
</button> </button>
</label> </label>
<button class="btn danger" on:click={removeActive} disabled={!activePath?.length} title="Drop selected item"> <button class="button danger" on:click={removeActive} disabled={!activePath?.length} title="Drop selected item">
<Icon name="-" /> <Icon name="-" />
</button> </button>
<button class="btn" on:click={first} disabled={!submittedForm.limit || (submittedForm.skip <= 0) || !result?.results || (activePage === 0)} title="First page"> <button class="button" on:click={first} disabled={!submittedForm.limit || (submittedForm.skip <= 0) || !result?.results || (activePage === 0)} title="First page">
<Icon name="chevs-l" /> <Icon name="chevs-l" />
</button> </button>
<button class="btn" on:click={prev} disabled={!submittedForm.limit || (submittedForm.skip <= 0) || !result?.results || (activePage === 0)} title="Previous {submittedForm.limit} items"> <button class="button" on:click={prev} disabled={!submittedForm.limit || (submittedForm.skip <= 0) || !result?.results || (activePage === 0)} title="Previous {submittedForm.limit} items">
<Icon name="chev-l" /> <Icon name="chev-l" />
</button> </button>
<button class="btn" on:click={next} disabled={!submittedForm.limit || ((result?.results?.length || 0) < submittedForm.limit) || !result?.results || !lastPage || (activePage >= lastPage)} title="Next {submittedForm.limit} items"> <button class="button" on:click={next} disabled={!submittedForm.limit || ((result?.results?.length || 0) < submittedForm.limit) || !result?.results || !lastPage || (activePage >= lastPage)} title="Next {submittedForm.limit} items">
<Icon name="chev-r" /> <Icon name="chev-r" />
</button> </button>
<button class="btn" on:click={last} disabled={!submittedForm.limit || ((result?.results?.length || 0) < submittedForm.limit) || !result?.results || !lastPage || (activePage >= lastPage)} title="Last page"> <button class="button" on:click={last} disabled={!submittedForm.limit || ((result?.results?.length || 0) < submittedForm.limit) || !result?.results || !lastPage || (activePage >= lastPage)} title="Last page">
<Icon name="chevs-r" /> <Icon name="chevs-r" />
</button> </button>
</div> </div>
@ -266,17 +317,7 @@
</div> </div>
</div> </div>
<QueryChooser
bind:queryToSave
bind:show={showQueryChooser}
on:select={queryChosen}
{collection}
/>
<ExportInfo on:openViewConfig bind:collection bind:info={exportInfo} />
{#if objectViewerData} {#if objectViewerData}
<!-- @todo Implement save -->
<ObjectViewer bind:data={objectViewerData} saveable on:save={saveDocument} bind:successMessage={objectViewerSuccessMessage} /> <ObjectViewer bind:data={objectViewerData} saveable on:save={saveDocument} bind:successMessage={objectViewerSuccessMessage} />
{/if} {/if}
@ -301,18 +342,18 @@
grid-template: auto 1fr / 1fr; grid-template: auto 1fr / 1fr;
} }
.form-row { .formrow {
display: grid; display: grid;
gap: 0.5rem; gap: 0.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.form-row.one { .formrow.one {
grid-template: 1fr / 3fr 2fr; grid-template: 1fr / 3fr 2fr;
} }
.form-row.two { .formrow.two {
grid-template: 1fr / 5fr 1fr 1fr; grid-template: 1fr / 5fr 1fr 1fr;
} }
.form-row.actions { .formrow.actions {
margin-bottom: 0rem; margin-bottom: 0rem;
grid-template: 1fr / repeat(4, auto); grid-template: 1fr / repeat(4, auto);
justify-content: start; justify-content: start;

View File

@ -3,32 +3,31 @@
import TabBar from '$components/tabbar.svelte'; import TabBar from '$components/tabbar.svelte';
import { EventsOn } from '$wails/runtime/runtime'; import { EventsOn } from '$wails/runtime/runtime';
import { tick } from 'svelte'; import { tick } from 'svelte';
import Aggregate from './aggregate.svelte'; import Aggregate from './aggregate.svelte';
import ViewConfig from './components/viewconfig.svelte';
import Find from './find.svelte'; import Find from './find.svelte';
import Indexes from './indexes.svelte'; import Indexes from './indexes.svelte';
import Insert from './insert.svelte'; import Insert from './insert.svelte';
import Remove from './remove.svelte'; import Remove from './remove.svelte';
import Shell from '../shell.svelte';
import Stats from './stats.svelte'; import Stats from './stats.svelte';
import Update from './update.svelte'; import Update from './update.svelte';
export let collection; export let collection;
export let hostKey; export let hostKey;
export let dbKey; export let dbKey;
export let collectionKey; export let collKey;
export let tab = 'find';
let tab = 'find';
let find; let find;
let viewConfigModalOpen = false;
let firstItem;
$: if (collection) { $: if (collection) {
collection.hostKey = hostKey; collection.hostKey = hostKey;
collection.dbKey = dbKey; collection.dbKey = dbKey;
collection.key = collectionKey; collection.key = collKey;
} }
$: if (hostKey || dbKey || collectionKey) { $: if (hostKey || dbKey || collKey) {
tab = 'find'; tab = 'find';
} }
@ -39,34 +38,34 @@
await tick(); await tick();
find.performQuery(event.detail); find.performQuery(event.detail);
} }
function openViewConfig(event) {
firstItem = event.detail?.firstItem;
viewConfigModalOpen = true;
}
</script> </script>
<div class="collection" class:empty={!collection}> <div class="view" class:empty={!collection}>
{#if collection} {#if collection}
{#key collection} {#key collection}
<TabBar tabs={[ <TabBar
{ key: 'stats', icon: 'chart', title: 'Stats' }, tabs={[
{ key: 'find', icon: 'db', title: 'Find' }, { key: 'stats', icon: 'chart', title: 'Stats' },
{ key: 'insert', icon: '+', title: 'Insert' }, { key: 'find', icon: 'db', title: 'Find' },
{ key: 'update', icon: 'edit', title: 'Update' }, { key: 'insert', icon: '+', title: 'Insert' },
{ key: 'remove', icon: 'trash', title: 'Remove' }, { key: 'update', icon: 'edit', title: 'Update' },
{ key: 'indexes', icon: 'list', title: 'Indexes' }, { key: 'remove', icon: 'trash', title: 'Remove' },
{ key: 'aggregate', icon: 're', title: 'Aggregate' }, { key: 'indexes', icon: 'list', title: 'Indexes' },
]} bind:selectedKey={tab} /> { key: 'aggregate', icon: 're', title: 'Aggregate' },
{ key: 'shell', icon: 'shell', title: 'Shell' },
]}
bind:selectedKey={tab}
/>
<div class="container"> <div class="container">
{#if tab === 'stats'} <Stats {collection} /> {#if tab === 'stats'} <Stats {collection} />
{:else if tab === 'find'} <Find {collection} bind:this={find} on:openViewConfig={openViewConfig} /> {:else if tab === 'find'} <Find {collection} bind:this={find} />
{:else if tab === 'insert'} <Insert {collection} on:performFind={catchQuery} on:openViewConfig={openViewConfig} /> {:else if tab === 'insert'} <Insert {collection} on:performFind={catchQuery} />
{:else if tab === 'update'} <Update {collection} on:performFind={catchQuery} /> {:else if tab === 'update'} <Update {collection} on:performFind={catchQuery} />
{:else if tab === 'remove'} <Remove {collection} /> {:else if tab === 'remove'} <Remove {collection} />
{:else if tab === 'indexes'} <Indexes {collection} /> {:else if tab === 'indexes'} <Indexes {collection} />
{:else if tab === 'aggregate'} <Aggregate {collection} /> {:else if tab === 'aggregate'} <Aggregate {collection} />
{:else if tab === 'shell'} <Shell {collection} />
{/if} {/if}
</div> </div>
{/key} {/key}
@ -75,22 +74,13 @@
{/if} {/if}
</div> </div>
{#if collection}
<ViewConfig
bind:show={viewConfigModalOpen}
bind:activeViewKey={collection.viewKey}
{firstItem}
{collection}
/>
{/if}
<style> <style>
.collection { .view {
height: 100%; height: 100%;
display: grid; display: grid;
grid-template: auto 1fr / 1fr; grid-template: auto 1fr / 1fr;
} }
.collection.empty { .view.empty {
grid-template: 1fr / 1fr; grid-template: 1fr / 1fr;
} }

View File

@ -1,66 +1,83 @@
<script> <script>
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import ObjectGrid from '$components/objectgrid.svelte'; import ObjectGrid from '$components/objectgrid.svelte';
import ObjectViewer from '$components/objectviewer.svelte'; import { onMount } from 'svelte';
import { DropIndex, GetIndexes } from '$wails/go/app/App';
import IndexDetail from './components/indexdetail.svelte';
export let collection; export let collection;
let indexes = [];
let activePath = []; let activePath = [];
let creatingNewIndex = false; let _indexes = [];
let error = '';
let busy = false;
$: collection && getIndexes(); async function refresh() {
busy = 'Fetching indexes…';
error = await collection.getIndexes();
async function getIndexes() { if (!error) {
const result = await GetIndexes(collection.hostKey, collection.dbKey, collection.key); _indexes = collection.indexes.map(idx => {
if (result) { return {
indexes = result; name: idx.name,
background: idx.background || false,
unique: idx.unique || false,
sparse: idx.sparse || false,
model: idx.model,
};
});
}
busy = false;
}
async function createIndex() {
const newIndexName = await collection.newIndex();
if (newIndexName) {
await refresh();
} }
} }
function createIndex() { async function dropIndex(indexName) {
creatingNewIndex = true; if (typeof indexName !== 'string') {
} indexName = activePath[0];
async function drop(key) {
if (typeof key !== 'string') {
key = activePath[0];
} }
const success = await DropIndex(collection.hostKey, collection.dbKey, collection.key, key);
const success = await collection.getIndexByName(indexName).drop();
if (success) { if (success) {
await getIndexes();
activePath[0] = ''; activePath[0] = '';
await refresh();
} }
} }
onMount(refresh);
</script> </script>
<div class="indexes"> <div class="indexes">
<div class="grid"> <div class="grid">
<ObjectGrid <ObjectGrid
key="name" key="name"
data={indexes} data={_indexes}
getRootMenu={(_, idx) => [ { label: 'Drop this index', fn: () => drop(idx.name) } ]} getRootMenu={(_, idx) => [ { label: 'Drop this index', fn: () => dropIndex(idx.name) } ]}
errorTitle={error ? 'Error while getting indexes' : ''}
errorDescription={error}
bind:activePath bind:activePath
{busy}
/> />
</div> </div>
<div class="actions"> <div class="actions">
<button class="btn" on:click={getIndexes}> <button class="button" on:click={refresh}>
<Icon name="reload" /> Reload <Icon name="reload" spin={busy} /> Reload
</button> </button>
<button class="btn" on:click={createIndex}> <button class="button" on:click={createIndex}>
<Icon name="+" /> Create index… <Icon name="+" /> Create index…
</button> </button>
<button class="btn danger" on:click={drop} disabled={!indexes?.length || !activePath[0]}> <button class="button danger" on:click={dropIndex} disabled={!_indexes.length || !activePath[0]}>
<Icon name="x" /> Drop selected <Icon name="x" /> Drop selected
</button> </button>
</div> </div>
</div> </div>
<IndexDetail bind:creatingNewIndex {collection} on:reload={getIndexes} />
<style> <style>
.indexes { .indexes {
display: grid; display: grid;

View File

@ -2,6 +2,7 @@
import Details from '$components/details.svelte'; import Details from '$components/details.svelte';
import Grid from '$components/grid.svelte'; import Grid from '$components/grid.svelte';
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import ObjectEditor from '$components/objecteditor.svelte';
import ObjectViewer from '$components/objectviewer.svelte'; import ObjectViewer from '$components/objectviewer.svelte';
import { randomString } from '$lib/math'; import { randomString } from '$lib/math';
import { inputTypes } from '$lib/mongo'; import { inputTypes } from '$lib/mongo';
@ -11,7 +12,6 @@
import { EJSON } from 'bson'; import { EJSON } from 'bson';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import Form from './components/form.svelte'; import Form from './components/form.svelte';
import ObjectEditor from '$components/objecteditor.svelte';
export let collection; export let collection;
@ -25,8 +25,8 @@
let objectViewerData = ''; let objectViewerData = '';
let viewType = 'form'; let viewType = 'form';
let allValid = false; let allValid = false;
let viewsForCollection = {};
$: viewsForCollection = views.forCollection(collection.hostKey, collection.dbKey, collection.key);
$: oppositeViewType = viewType === 'table' ? 'form' : 'table'; $: oppositeViewType = viewType === 'table' ? 'form' : 'table';
$: allValid = Object.values(formValidity).every(v => v !== false); $: allValid = Object.values(formValidity).every(v => v !== false);
@ -46,6 +46,10 @@
newItems = [ {} ]; newItems = [ {} ];
} }
$: if ($views) {
viewsForCollection = views.forCollection(collection.hostKey, collection.dbKey, collection.key);
}
async function insert() { async function insert() {
insertedIds = await InsertItems( insertedIds = await InsertItems(
collection.hostKey, collection.hostKey,
@ -60,9 +64,7 @@
function showDocs() { function showDocs() {
dispatch('performFind', { dispatch('performFind', {
query: insertedIds.length === 1 query: insertedIds.length === 1 ? `{ "_id": ${JSON.stringify(insertedIds[0])} }` : `{ "_id": { "$in": [ ${insertedIds.map(id => JSON.stringify(id)).join(', ')} ] } }`,
? `{ "_id": ${JSON.stringify(insertedIds[0])} }`
: `{ "_id": { "$in": [ ${insertedIds.map(id => JSON.stringify(id)).join(', ')} ] } }`,
}); });
} }
@ -71,12 +73,7 @@
} }
function showJson() { function showJson() {
if (viewType === 'form') { objectViewerData = [ ...newItems ];
objectViewerData = { ...(newItems[0] || {}) };
}
else if (viewType === 'table') {
objectViewerData = [ ...newItems ];
}
} }
function addRow(beforeIndex = -1) { function addRow(beforeIndex = -1) {
@ -97,6 +94,10 @@
newItems = newItems; newItems = newItems;
} }
function openViewConfig() {
views.openConfig(collection);
}
onMount(() => { onMount(() => {
if (collection.viewKey === 'list') { if (collection.viewKey === 'list') {
editor.dispatch({ editor.dispatch({
@ -145,18 +146,19 @@
<div class="table"> <div class="table">
<Grid <Grid
key="id" key="id"
items={newItems} columns={$views[collection.viewKey]?.columns
columns={ ?.filter(c => inputTypes.includes(c.inputType))
$views[collection.viewKey]?.columns .map(c => {
?.filter(c => inputTypes.includes(c.inputType)) return { ...c, id: randomString(8), title: c.key };
.map(c => ({ ...c, id: randomString(8), title: c.key })) || [] }) || []}
}
showHeaders={true} showHeaders={true}
canSelect={false} canSelect={false}
canRemoveItems={true} canRemoveItems={true}
hideChildrenToggles={true} hideChildrenToggles={true}
on:addRow={addRow} on:addRow={addRow}
on:removeItem={() => deleteRow()}
bind:inputsValid={allValid} bind:inputsValid={allValid}
bind:items={newItems}
/> />
</div> </div>
{/if} {/if}
@ -176,34 +178,36 @@
</div> </div>
<div> <div>
{#if insertedIds} {#if insertedIds}
<button class="btn" type="button" on:click={showDocs}>View inserted docs</button> <button class="button" type="button" on:click={showDocs}>View inserted docs</button>
{/if} {/if}
{#if collection.viewKey !== 'list'} {#if collection.viewKey !== 'list'}
<button class="btn" type="button" on:click={showJson} title="Show JSON"> <button class="button" type="button" on:click={showJson} title="Show JSON">
<Icon name="code" /> <Icon name="code" />
</button> </button>
<button class="btn" type="button" on:click={switchViewType} title="Edit as {oppositeViewType}"> <button class="button" type="button" on:click={switchViewType} title="Edit as {oppositeViewType}">
<Icon name={oppositeViewType} /> {capitalise(oppositeViewType)} <Icon name={oppositeViewType} /> {capitalise(oppositeViewType)}
</button> </button>
{/if} {/if}
<label class="field inline"> <label class="field inline">
<select bind:value={collection.viewKey}> <select bind:value={collection.viewKey}>
{#each Object.entries(viewsForCollection) as [key, view]} {#each Object.entries(viewsForCollection) as [ key, view ]}
<option value={key}>{key === 'list' ? 'Raw JSON' : view.name}</option> <option value={key}>{key === 'list' ? 'Raw JSON' : view.name}</option>
{/each} {/each}
</select> </select>
<button class="btn" type="button" on:click={() => dispatch('openViewConfig')} title="Configure view"> <button class="button" type="button" on:click={openViewConfig} title="Configure view">
<Icon name="cog" /> <Icon name="cog" />
</button> </button>
</label> </label>
<button type="submit" class="btn" disabled={$views[collection.viewKey]?.type === 'list' ? !json : !allValid}> <button type="submit" class="button" disabled={$views[collection.viewKey]?.type === 'list' ? !json : !allValid}>
<Icon name="+" /> Insert <Icon name="+" /> Insert
</button> </button>
</div> </div>
</div> </div>
</form> </form>
<ObjectViewer data={objectViewerData} /> {#if objectViewerData}
<ObjectViewer bind:data={objectViewerData} />
{/if}
<style> <style>
form { form {

View File

@ -11,7 +11,7 @@
let many = true; let many = true;
let result = undefined; let result = undefined;
let editor; let editor;
$: code = `db.${collection.key}.remove(${json});`; // $: code = `db.${collection.key}.remove(${json});`;
async function removeItems() { async function removeItems() {
result = await RemoveItems( result = await RemoveItems(
@ -45,7 +45,7 @@
</label> </label>
<div class="actions"> <div class="actions">
<button type="submit" class="btn danger"> <button type="submit" class="button danger">
<Icon name="-" /> Remove <Icon name="-" /> Remove
</button> </button>

View File

@ -18,11 +18,17 @@
<!-- <CodeExample code="db.stats()" /> --> <!-- <CodeExample code="db.stats()" /> -->
<div class="grid"> <div class="grid">
<ObjectGrid data={collection.stats} /> <ObjectGrid
data={collection.stats}
showTypes={false}
errorTitle={collection.statsError ? 'Error fetching collection stats' : ''}
errorDescription={collection.statsError}
busy={!collection.stats && !collection.statsError && `Fetching stats for ${collection.key}`}
/>
</div> </div>
<div class="buttons"> <div class="buttons">
<button class="btn secondary" on:click={copy}> <button class="button secondary" on:click={copy} disabled={!collection.stats}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} /> <Icon name={copySucceeded ? 'check' : 'clipboard'} />
Copy JSON Copy JSON
</button> </button>

View File

@ -11,7 +11,7 @@
const allOperators = Object.values(atomicUpdateOperators).map(Object.keys).flat(); const allOperators = Object.values(atomicUpdateOperators).map(Object.keys).flat();
const form = { query: '{}', parameters: [ { type: '$set' } ] }; const form = { query: '{}', parameters: [ { type: '$set' } ] };
let updatedCount; let updatedCount;
$: code = buildCode(form); // $: code = buildCode(form);
$: invalid = !form.query || form.parameters?.some(param => { $: invalid = !form.query || form.parameters?.some(param => {
if (!param.value) { if (!param.value) {
return true; return true;
@ -26,26 +26,28 @@
} }
}); });
function buildCode(form) { // function buildCode(form) {
let operation = '{ ' + form.parameters.filter(p => p.type).map(p => `${p.type}: ${p.value || '{}'}`).join(', ') + ' }'; // let operation = '{ ' + form.parameters.filter(p => p.type).map(p => `${p.type}: ${p.value || '{}'}`).join(', ') + ' }';
if (operation === '{ }') { // if (operation === '{ }') {
operation = '{}'; // operation = '{}';
} // }
let options = (form.upsert || form.many) ? ', { ' : ''; // let options = (form.upsert || form.many) ? ', { ' : '';
form.upsert && (options += 'upsert: true'); // form.upsert && (options += 'upsert: true');
form.upsert && form.many && (options += ', '); // form.upsert && form.many && (options += ', ');
form.many && (options += 'multi: true'); // form.many && (options += 'multi: true');
(form.upsert || form.many) && (options += ' }'); // (form.upsert || form.many) && (options += ' }');
const code = `db.${collection.key}.update(${form.query || '{}'}, ${operation}${options});`; // const code = `db.${collection.key}.update(${form.query || '{}'}, ${operation}${options});`;
return code; // return code;
} // }
async function submitQuery() { async function submitQuery() {
const f = deepClone(form); const f = deepClone(form);
f.query = convertLooseJson(f.query); f.query = convertLooseJson(f.query);
f.parameters = f.parameters.map(param => ({ ...param, value: convertLooseJson(param.value) })); f.parameters = f.parameters.map(param => {
return { ...param, value: convertLooseJson(param.value) };
});
updatedCount = await UpdateItems(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(f)); updatedCount = await UpdateItems(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(f));
} }
@ -105,7 +107,7 @@
{/if} {/if}
{/key} {/key}
<button class="btn" type="submit" disabled={invalid}> <button class="button" type="submit" disabled={invalid}>
<Icon name="check" /> Update <Icon name="check" /> Update
</button> </button>
</div> </div>
@ -120,9 +122,9 @@
<fieldset class="parameter"> <fieldset class="parameter">
<label class="field"> <label class="field">
<select bind:value={param.type} class="type"> <select bind:value={param.type} class="type">
{#each Object.entries(atomicUpdateOperators) as [groupName, options]} {#each Object.entries(atomicUpdateOperators) as [ groupName, options ]}
<optgroup label={groupName}> <optgroup label={groupName}>
{#each Object.entries(options) as [key, label]} {#each Object.entries(options) as [ key, label ]}
<option value={key} disabled={form.parameters.some(p => p.type === key) && (key !== param.type)}> <option value={key} disabled={form.parameters.some(p => p.type === key) && (key !== param.type)}>
{label} {label}
</option> </option>
@ -133,11 +135,11 @@
<input type="text" class="code" bind:value={param.value} placeholder={'{}'} use:input={{ type: 'json' }} /> <input type="text" class="code" bind:value={param.value} placeholder={'{}'} use:input={{ type: 'json' }} />
</label> </label>
<button class="btn" disabled={form.parameters.length >= allOperators.length} on:click={() => addParameter()} type="button"> <button class="button" disabled={form.parameters.length >= allOperators.length} on:click={() => addParameter()} type="button">
<Icon name="+" /> <Icon name="+" />
</button> </button>
<button class="btn" disabled={form.parameters.length < 2} on:click={() => removeParam(index)} type="button"> <button class="button" disabled={form.parameters.length < 2} on:click={() => removeParam(index)} type="button">
<Icon name="-" /> <Icon name="-" />
</button> </button>
</fieldset> </fieldset>

View File

@ -3,12 +3,14 @@
import Grid from '$components/grid.svelte'; import Grid from '$components/grid.svelte';
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import { startProgress } from '$lib/progress'; import { startProgress } from '$lib/progress';
import connections from '$lib/stores/connections'; import hostTree from '$lib/stores/hosttree';
import hosts from '$lib/stores/hosts';
import applicationSettings from '$lib/stores/settings'; import applicationSettings from '$lib/stores/settings';
import { OpenConnection, OpenDatabase, PerformDump } from '$wails/go/app/App'; import { OpenConnection, OpenDatabase } from '$wails/go/app/App';
import { createEventDispatcher } from 'svelte';
export let info; export let info = {};
const dispatch = createEventDispatcher();
$: if (info) { $: if (info) {
info.outdir = info.outdir || $applicationSettings.defaultExportDirectory; info.outdir = info.outdir || $applicationSettings.defaultExportDirectory;
@ -24,10 +26,10 @@
const progress = startProgress(`Opening connection to host "${hostKey}"`); const progress = startProgress(`Opening connection to host "${hostKey}"`);
const databases = await OpenConnection(hostKey); const databases = await OpenConnection(hostKey);
if (databases && !$connections[hostKey]) { if (databases && !$hostTree[hostKey]) {
$connections[hostKey] = { databases: {} }; $hostTree[hostKey] = { databases: {} };
databases.sort().forEach(dbKey => { databases.sort().forEach(dbKey => {
$connections[hostKey].databases[dbKey] = $connections[hostKey].databases[dbKey] || { collections: {} }; $hostTree[hostKey].databases[dbKey] = $hostTree[hostKey].databases[dbKey] || { collections: {} };
}); });
} }
@ -44,28 +46,24 @@
const collections = await OpenDatabase(info.hostKey, dbKey); const collections = await OpenDatabase(info.hostKey, dbKey);
for (const collKey of collections?.sort() || []) { for (const collKey of collections?.sort() || []) {
$connections[info.hostKey].databases[dbKey].collections[collKey] = {}; $hostTree[info.hostKey].databases[dbKey].collections[collKey] = {};
} }
progress.end(); progress.end();
} }
} }
async function performDump() {
const ok = await PerformDump(JSON.stringify(info));
if (ok) {
info = undefined;
}
}
function selectCollection(collKey) { function selectCollection(collKey) {
info.collKeys = [ collKey ]; info.collKeys = [ collKey ];
} }
function performDump() {
dispatch('dump', { info });
}
</script> </script>
<Modal bind:show={info} title="Perform dump"> <Modal title="Perform dump" on:close>
<form on:submit|preventDefault={performDump}> <form on:submit|preventDefault={performDump}>
<!-- svelte-ignore a11y-label-has-associated-control - input is in DirectoryChooser -->
<label class="field"> <label class="field">
<span class="label">Output destination:</span> <span class="label">Output destination:</span>
<DirectoryChooser bind:value={info.outdir} /> <DirectoryChooser bind:value={info.outdir} />
@ -83,7 +81,9 @@
hideChildrenToggles hideChildrenToggles
items={[ items={[
{ id: undefined, name: '(localhost)' }, { id: undefined, name: '(localhost)' },
...Object.keys($hosts).map(id => ({ id, name: $hosts[id]?.name })), ...Object.keys($hostTree).map(id => {
return { id, name: $hostTree[id]?.name };
}),
]} ]}
on:select={e => selectHost(e.detail?.itemKey)} on:select={e => selectHost(e.detail?.itemKey)}
/> />
@ -97,9 +97,9 @@
hideChildrenToggles hideChildrenToggles
items={[ items={[
{ id: undefined, name: '(all databases)' }, { id: undefined, name: '(all databases)' },
...($connections[info.hostKey]?.databases ...($hostTree[info.hostKey]?.databases ? Object.keys($hostTree[info.hostKey].databases).map(id => {
? Object.keys($connections[info.hostKey].databases).map(id => ({ id, name: id })) return { id, name: id };
: [] }) : []
), ),
]} ]}
on:select={e => selectDatabase(e.detail?.itemKey)} on:select={e => selectDatabase(e.detail?.itemKey)}
@ -114,9 +114,9 @@
hideChildrenToggles hideChildrenToggles
items={[ items={[
{ id: undefined, name: '(all collections)' }, { id: undefined, name: '(all collections)' },
...($connections[info.hostKey]?.databases[info.dbKey]?.collections ...($hostTree[info.hostKey]?.databases[info.dbKey]?.collections ? Object.keys($hostTree[info.hostKey].databases[info.dbKey].collections).map(id => {
? Object.keys($connections[info.hostKey].databases[info.dbKey].collections).map(id => ({ id, name: id })) return { id, name: id };
: [] }) : []
), ),
]} ]}
on:select={e => selectCollection(e.detail?.itemKey)} on:select={e => selectCollection(e.detail?.itemKey)}
@ -126,7 +126,7 @@
</form> </form>
<svelte:fragment slot="footer"> <svelte:fragment slot="footer">
<button class="btn" on:click={performDump}>Perform dump</button> <button class="button" on:click={performDump}>Perform dump</button>
</svelte:fragment> </svelte:fragment>
</Modal> </Modal>

View File

@ -0,0 +1,67 @@
<script>
import BlankState from '$components/blankstate.svelte';
import TabBar from '$components/tabbar.svelte';
import { EventsOn } from '$wails/runtime/runtime';
import Shell from '../shell.svelte';
import Stats from './stats.svelte';
export let database;
export let hostKey;
export let dbKey;
export let tab = 'stats';
$: if (database) {
database.hostKey = hostKey;
database.dbKey = dbKey;
}
$: if (hostKey || dbKey) {
tab = 'stats';
}
EventsOn('OpenStatsTab', name => (tab = name || tab));
</script>
<div class="view" class:empty={!database}>
{#if database}
{#key database}
<TabBar
tabs={[
{ key: 'stats', icon: 'chart', title: 'Database stats' },
{ key: 'shell', icon: 'shell', title: 'Shell' },
]}
bind:selectedKey={tab} />
<div class="container">
{#if tab === 'stats'} <Stats {database} />
{:else if tab === 'shell'} <Shell {database} />
{/if}
</div>
{/key}
{:else}
<BlankState label="Select a database to continue" />
{/if}
</div>
<style>
.view {
height: 100%;
display: grid;
grid-template: auto 1fr / 1fr;
}
.view.empty {
grid-template: 1fr / 1fr;
}
.container {
padding: 0.5rem;
display: flex;
align-items: stretch;
overflow: auto;
min-height: 0;
min-width: 0;
}
.container > :global(*) {
width: 100%;
}
</style>

View File

@ -0,0 +1,51 @@
<script>
import Icon from '$components/icon.svelte';
import ObjectGrid from '$components/objectgrid.svelte';
export let database;
let copySucceeded = false;
async function copy() {
const json = JSON.stringify(database.stats, undefined, '\t');
await navigator.clipboard.writeText(json);
copySucceeded = true;
setTimeout(() => copySucceeded = false, 1500);
}
</script>
<div class="stats">
<!-- <CodeExample code="db.stats()" /> -->
<div class="grid">
<ObjectGrid
data={database.stats}
showTypes={false}
errorTitle={database.statsError ? 'Error fetching database stats' : ''}
errorDescription={database.statsError}
busy={!database.stats && !database.statsError && `Fetching stats for ${database.key}`}
/>
</div>
<div class="buttons">
<button class="button secondary" on:click={copy} disabled={!database.stats}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} />
Copy JSON
</button>
</div>
</div>
<style>
.stats {
display: grid;
gap: 0.5rem;
grid-template: 1fr auto / 1fr;
}
.stats .grid {
overflow: auto;
min-height: 0;
min-width: 0;
border: 1px solid #ccc;
}
</style>

View File

@ -1,26 +1,17 @@
<script> <script>
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import input from '$lib/actions/input'; import input from '$lib/actions/input';
import hosts from '$lib/stores/hosts'; import hostTree from '$lib/stores/hosttree';
import { AddHost, UpdateHost } from '$wails/go/app/App'; import { AddHost, UpdateHost } from '$wails/go/app/App';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
export let show = false;
export let hostKey = ''; export let hostKey = '';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let form = {}; let form = {};
let error = ''; let error = '';
$: valid = validate(form); $: valid = validate(form);
$: host = $hosts[hostKey]; $: host = $hostTree[hostKey];
$: if (show || !show) {
init();
}
function init() {
form = { ...(host || {}) };
}
function validate(form) { function validate(form) {
return form.name && form.uri && true; return form.name && form.uri && true;
@ -41,16 +32,20 @@
hostKey = newHostKey; hostKey = newHostKey;
} }
} }
show = false; dispatch('updated', form);
dispatch('reload'); dispatch('close');
} }
catch (e) { catch (e) {
error = e; error = e;
} }
} }
onMount(() => {
form = { ...(host || {}) };
});
</script> </script>
<Modal bind:show title={host ? `Edit ${host.name}` : 'Create a new host'}> <Modal title={host ? `Edit ${host.name}` : 'Create a new host'} on:close>
<form on:submit|preventDefault={submit}> <form on:submit|preventDefault={submit}>
<label class="field"> <label class="field">
<span class="label">Label</span> <span class="label">Label</span>
@ -69,7 +64,7 @@
<div class="error">{error}</div> <div class="error">{error}</div>
{/if} {/if}
</div> </div>
<button class="btn" disabled={!valid} on:click={submit}> <button class="button" disabled={!valid} on:click={submit}>
{host ? 'Save' : 'Create'} {host ? 'Save' : 'Create'}
</button> </button>
</div> </div>

View File

@ -0,0 +1,73 @@
<script>
import BlankState from '$components/blankstate.svelte';
import TabBar from '$components/tabbar.svelte';
import { EventsOn } from '$wails/runtime/runtime';
import Logs from './logs.svelte';
import Shell from '../shell.svelte';
import Status from './status.svelte';
import SystemInfo from './systeminfo.svelte';
export let host;
export let hostKey;
export let tab = 'status';
$: if (host) {
host.hostKey = hostKey;
}
$: if (hostKey) {
tab = 'status';
}
EventsOn('OpenStatusTab', name => (tab = name || tab));
</script>
<div class="view" class:empty={!host}>
{#if host}
{#key host}
<TabBar
tabs={[
{ key: 'status', icon: 'chart', title: 'Host status' },
{ key: 'shell', icon: 'shell', title: 'Shell' },
{ key: 'logs', icon: 'doc', title: 'Logs' },
{ key: 'systemInfo', icon: 'server', title: 'System info' },
]}
bind:selectedKey={tab}
/>
<div class="container">
{#if tab === 'status'} <Status {host} />
{:else if tab === 'logs'} <Logs {host} />
{:else if tab === 'systemInfo'} <SystemInfo {host} />
{:else if tab === 'shell'} <Shell {host} />
{/if}
</div>
{/key}
{:else}
<BlankState label="Select a host to continue" />
{/if}
</div>
<style>
.view {
height: 100%;
display: grid;
grid-template: auto 1fr / 1fr;
}
.view.empty {
grid-template: 1fr / 1fr;
}
.container {
padding: 0.5rem;
display: flex;
align-items: stretch;
overflow: auto;
min-height: 0;
min-width: 0;
}
.container > :global(*) {
width: 100%;
}
</style>

View File

@ -0,0 +1,194 @@
<script>
import Grid from '$components/grid.svelte';
import Icon from '$components/icon.svelte';
import ObjectViewer from '$components/objectviewer.svelte';
import input from '$lib/actions/input';
import { logComponents, logLevels } from '$lib/mongo';
import { BrowserOpenURL } from '$wails/runtime/runtime';
import { onDestroy } from 'svelte';
export let host;
const autoReloadIntervals = [ 1, 2, 5, 10, 30, 60 ];
let filter = 'global';
let severityFilter = '';
let componentFilter = '';
let logs;
let total = 0;
let error = '';
let copySucceeded = false;
let autoReloadInterval = 0;
let objectViewerData;
let interval;
$: (filter || severityFilter || componentFilter) && refresh();
$: busy = !logs && !error && 'Requesting logs…';
$: if (autoReloadInterval) {
if (interval) {
clearInterval(interval);
}
interval = setInterval(refresh, autoReloadInterval * 1000);
}
async function refresh() {
let _logs = [];
({ logs: _logs, total, error } = await host.getLogs(filter));
logs = [];
for (let index = 0; index < _logs.length; index++) {
const log = JSON.parse(_logs[index]);
const matchesLevel = severityFilter ? log.s?.startsWith(severityFilter) : true;
const matchesComponent = componentFilter ? (componentFilter === log.c?.toUpperCase()) : true;
if (matchesLevel && matchesComponent) {
log._index = index;
log.s = logLevels[log.s] || log.s;
logs = [ ...logs, log ];
}
}
}
function openFilterDocs() {
BrowserOpenURL('https://www.mongodb.com/docs/manual/reference/command/getLog/#command-fields');
}
function openLogDetail(event) {
objectViewerData = logs[event.detail.index];
}
async function copy() {
const json = JSON.stringify(host.status, undefined, '\t');
await navigator.clipboard.writeText(json);
copySucceeded = true;
setTimeout(() => copySucceeded = false, 1500);
}
onDestroy(() => {
if (interval) {
clearInterval(interval);
}
});
</script>
<div class="stats">
<div class="formrow">
<label class="field">
<span class="label">Auto reload (seconds)</span>
<input type="number" class="autoreloadinput" bind:value={autoReloadInterval} list="autoreloadintervals" use:input />
</label>
<label class="field">
<span class="label">Log type</span>
<select bind:value={filter}>
<option value="global">Global</option>
<option value="startupWarnings">Startup warnings</option>
</select>
<button class="button secondary" on:click={openFilterDocs} title="Documentation">
<Icon name="?" />
</button>
</label>
</div>
<div class="formrow">
<label class="field">
<span class="label">Severity</span>
<select bind:value={severityFilter}>
<option value="">All</option>
{#each Object.entries(logLevels) as [ value, name ]}
<option {value}>{value} ({name})</option>
{/each}
</select>
</label>
<label class="field">
<span class="label">Component</span>
<select bind:value={componentFilter}>
<option value="">All</option>
{#each logComponents as value}
<option {value}>{value}</option>
{/each}
</select>
</label>
</div>
<div class="grid">
<Grid
items={logs || []}
columns={[
{ title: 'Date', key: 't.$date' },
{ title: 'Severity', key: 's' },
{ title: 'ID', key: 'id' },
{ title: 'Component', key: 'c' },
{ title: 'Context', key: 'ctx' },
{ title: 'Message', key: 'msg' },
]}
key="_index"
showHeaders
errorTitle={error ? 'Error fetching server status' : ''}
errorDescription={error}
on:trigger={openLogDetail}
{busy}
/>
</div>
<div class="controls">
<button class="button" on:click={refresh}>
<Icon name="reload" spin={busy} /> Reload
</button>
<button class="button secondary" on:click={copy} disabled={!host.status}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} />
Copy JSON
</button>
{#if total}
<div class="total">
Total: {total}
</div>
{/if}
</div>
</div>
{#if objectViewerData}
<ObjectViewer bind:data={objectViewerData} readonly />
{/if}
<datalist id="autoreloadintervals">
{#each autoReloadIntervals as value}
<option {value} />
{/each}
</datalist>
<style>
.stats {
display: grid;
gap: 0.5rem;
grid-template: auto auto 1fr auto / 1fr;
}
.formrow {
display: grid;
gap: 0.5rem;
grid-template: 1fr / 1fr 1fr;
}
.grid {
overflow: auto;
min-height: 0;
min-width: 0;
border: 1px solid #ccc;
}
.controls {
display: flex;
align-items: center;
gap: 0.2rem;
}
.total {
margin-left: auto;
}
.autoreloadinput {
width: 1.5rem;
}
</style>

View File

@ -0,0 +1,51 @@
<script>
import Icon from '$components/icon.svelte';
import ObjectGrid from '$components/objectgrid.svelte';
export let host;
let copySucceeded = false;
async function copy() {
const json = JSON.stringify(host.status, undefined, '\t');
await navigator.clipboard.writeText(json);
copySucceeded = true;
setTimeout(() => copySucceeded = false, 1500);
}
</script>
<div class="stats">
<!-- <CodeExample code="db.stats()" /> -->
<div class="grid">
<ObjectGrid
data={host.status || {}}
showTypes={false}
errorTitle={host.statusError ? 'Error fetching server status' : ''}
errorDescription={host.statusError}
busy={!host.status && !host.statusError && 'Fetching server status…'}
/>
</div>
<div class="buttons">
<button class="button secondary" on:click={copy} disabled={!host.status}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} />
Copy JSON
</button>
</div>
</div>
<style>
.stats {
display: grid;
gap: 0.5rem;
grid-template: 1fr auto / 1fr;
}
.stats .grid {
overflow: auto;
min-height: 0;
min-width: 0;
border: 1px solid #ccc;
}
</style>

View File

@ -0,0 +1,51 @@
<script>
import Icon from '$components/icon.svelte';
import ObjectGrid from '$components/objectgrid.svelte';
export let host;
let copySucceeded = false;
async function copy() {
const json = JSON.stringify(host.systemInfo, undefined, '\t');
await navigator.clipboard.writeText(json);
copySucceeded = true;
setTimeout(() => copySucceeded = false, 1500);
}
</script>
<div class="stats">
<!-- <CodeExample code="db.stats()" /> -->
<div class="grid">
<ObjectGrid
data={host.systemInfo}
showTypes={false}
errorTitle={host.systemInfoError ? 'Error fetching system info' : ''}
errorDescription={host.systemInfoError}
busy={!host.systemInfo && !host.systemInfoError && 'Fetching system info…'}
/>
</div>
<div class="buttons">
<button class="button secondary" on:click={copy} disabled={!host.systemInfo}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} />
Copy JSON
</button>
</div>
</div>
<style>
.stats {
display: grid;
gap: 0.5rem;
grid-template: 1fr auto / 1fr;
}
.stats .grid {
overflow: auto;
min-height: 0;
min-width: 0;
border: 1px solid #ccc;
}
</style>

View File

@ -1,148 +1,70 @@
<script> <script>
import Grid from '$components/grid.svelte'; import Grid from '$components/grid.svelte';
import { startProgress } from '$lib/progress'; import hostTree from '$lib/stores/hosttree';
import connections from '$lib/stores/connections';
import { WindowSetTitle } from '$wails/runtime/runtime';
import { createEventDispatcher } from 'svelte';
import { DropCollection, DropDatabase, OpenCollection, OpenConnection, OpenDatabase, RemoveHost, TruncateCollection } from '../../../wailsjs/go/app/App';
import hosts from '$lib/stores/hosts';
import { tick } from 'svelte';
export let activeHostKey = ''; export let path = [];
export let activeDbKey = '';
export let activeCollKey = '';
const dispatch = createEventDispatcher();
let activeGridPath = [];
$: activeHostKey = activeGridPath[0] || activeHostKey;
$: activeDbKey = activeGridPath[1];
$: activeCollKey = activeGridPath[2];
$: host = $hosts[activeHostKey];
$: connection = $connections[activeHostKey];
$: database = connection?.databases[activeDbKey];
$: collection = database?.collections?.[activeCollKey];
export async function reload() {
activeHostKey && await openConnection(activeHostKey);
activeDbKey && await openDatabase(activeDbKey);
activeCollKey && await openCollection(activeCollKey);
}
async function openConnection(hostKey) {
const progress = startProgress(`Connecting to "${hostKey}"…`);
const databases = await OpenConnection(hostKey);
if (databases) {
$connections[hostKey] = { databases: {} };
databases.forEach(dbKey => {
$connections[hostKey].databases[dbKey] = { collections: {} };
});
activeHostKey = hostKey;
dispatch('connected', hostKey);
WindowSetTitle(`${$hosts[activeHostKey].name} - Rolens`);
}
progress.end();
}
async function removeHost(hostKey) {
activeCollKey = '';
activeDbKey = '';
activeHostKey = '';
await tick();
await RemoveHost(hostKey);
hosts.update();
}
async function openDatabase(dbKey) {
const progress = startProgress(`Opening database "${dbKey}"…`);
const collections = await OpenDatabase(activeHostKey, dbKey);
for (const collKey of collections || []) {
$connections[activeHostKey].databases[dbKey].collections[collKey] = {};
}
progress.end();
}
async function dropDatabase(dbKey) {
const progress = startProgress(`Dropping database "${dbKey}"…`);
await DropDatabase(activeHostKey, dbKey);
await reload();
progress.end();
}
async function openCollection(collKey) {
const progress = startProgress(`Opening collection "${collKey}"…`);
const stats = await OpenCollection(activeHostKey, activeDbKey, collKey);
$connections[activeHostKey].databases[activeDbKey].collections[collKey] = $connections[activeHostKey].databases[activeDbKey].collections[collKey] || {};
$connections[activeHostKey].databases[activeDbKey].collections[collKey].stats = stats;
progress.end();
}
async function truncateCollection(dbKey, collKey) {
const progress = startProgress(`Truncating collection "${collKey}"…`);
await TruncateCollection(activeHostKey, dbKey, collKey);
await reload();
progress.end();
}
async function dropCollection(dbKey, collKey) {
const progress = startProgress(`Dropping collection "${collKey}"…`);
await DropCollection(activeHostKey, dbKey, collKey);
await reload();
progress.end();
}
</script> </script>
<Grid <Grid
striped={false} striped={false}
columns={[ { key: 'name' }, { key: 'count', right: true } ]} columns={[ { key: 'name' }, { key: 'count', right: true } ]}
items={Object.keys($hosts).map(hostKey => ({ items={Object.values($hostTree || {}).map(host => {
id: hostKey, return {
name: $hosts[hostKey].name, id: host.key,
icon: 'server', name: host.name,
children: Object.keys(connection?.databases || {}).sort().map(dbKey => ({ icon: 'server',
id: dbKey, children: Object.values(host.databases || {})
name: dbKey, .sort((a, b) => a.key.localeCompare(b))
icon: 'db', .map(database => {
count: Object.keys(connection.databases[dbKey].collections || {}).length || '', return {
children: Object.keys(connection.databases[dbKey].collections).sort().map(collKey => ({ id: database.key,
id: collKey, name: database.key,
name: collKey, icon: 'db',
icon: 'list', count: Object.keys(database.collections || {}).length || '',
menu: [ children: Object.values(database.collections)
{ label: 'Export collection (JSON, CSV)…', fn: () => dispatch('exportCollection', collKey) }, .sort((a, b) => a.key.localeCompare(b))
{ label: 'Dump collection (BSON via mongodump)…', fn: () => dispatch('dumpCollection', collKey) }, .map(collection => {
{ separator: true }, return {
{ label: 'Rename collection…', fn: () => dispatch('renameCollection', collKey) }, id: collection.key,
{ label: 'Truncate collection…', fn: () => truncateCollection(dbKey, collKey) }, name: collection.key,
{ label: 'Drop collection…', fn: () => dropCollection(dbKey, collKey) }, icon: 'list',
{ separator: true }, menu: [
{ label: 'New collection…', fn: () => dispatch('newCollection') }, { label: 'Export collection…', fn: collection.export },
], { label: 'Dump collection (BSON via mongodump)…', fn: collection.dump },
})) || [], { separator: true },
{ label: 'Rename collection…', fn: collection.rename },
{ label: 'Truncate collection…', fn: collection.truncate },
{ label: 'Drop collection…', fn: collection.drop },
{ separator: true },
{ label: 'New collection…', fn: database.newCollection },
],
};
}) || [],
menu: [
{ label: 'Dump database (BSON via mongodump)…', fn: database.dump },
{ label: 'Drop database…', fn: database.drop },
{ separator: true },
{ label: 'New database…', fn: host.newDatabase },
{ label: 'New collection…', fn: database.newCollection },
],
};
}),
menu: [ menu: [
{ label: 'Drop database…', fn: () => dropDatabase(dbKey) }, { label: 'New database…', fn: host.newDatabase },
{ separator: true }, { separator: true },
{ label: 'New database…', fn: () => dispatch('newDatabase') }, { label: `Edit host ${host.name}`, fn: host.edit },
{ label: 'New collection…', fn: () => dispatch('newCollection') }, { label: 'Remove host…', fn: host.remove },
], ],
})), };
menu: [ })}
{ label: 'New database…', fn: () => dispatch('newDatabase') },
{ separator: true },
{ label: `Edit host ${$hosts[hostKey].name}`, fn: () => dispatch('editHost', hostKey) },
{ label: `Remove host…`, fn: () => removeHost(hostKey) },
],
}))}
bind:activePath={activeGridPath}
on:select={e => { on:select={e => {
const key = e.detail.itemKey; let level;
switch (e.detail?.level) { ({ path, level } = e.detail);
case 0: return openConnection(key);
case 1: return openDatabase(key); switch (level) {
case 2: return openCollection(key); case 0: return $hostTree[path[0]].open();
case 1: return $hostTree[path[0]].databases[path[1]].open();
case 2: return $hostTree[path[0]].databases[path[1]].collections[path[2]].open();
} }
}} }}
/> />

View File

@ -1,129 +1,81 @@
<script> <script>
import { startProgress } from '$lib/progress';
import connections from '$lib/stores/connections';
import { Hosts, RenameCollection } from '$wails/go/app/App';
import { EnterText } from '$wails/go/ui/UI';
import { EventsOn } from '$wails/runtime/runtime';
import { onMount } from 'svelte';
import CollectionDetail from './collection/index.svelte';
import DumpInfo from './dump.svelte';
import HostDetail from './hostdetail.svelte';
import HostTree from './hosttree.svelte';
import sharedState from '$lib/stores/sharedstate';
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import hosts from '$lib/stores/hosts'; import hostTree from '$lib/stores/hosttree';
import sharedState from '$lib/stores/sharedstate';
import { EventsOn } from '$wails/runtime/runtime';
import CollectionView from './collection/index.svelte';
import DatabaseView from './database/index.svelte';
import HostView from './host/index.svelte';
import HostTree from './hosttree.svelte';
export let activeHostKey = ''; let path = [];
export let activeDbKey = ''; let hostTab = '';
export let activeCollKey = ''; let dbTab = '';
let collTab = '';
let hostTree; $: activeHostKey = path[0];
let showHostDetail = false; $: activeDbKey = path[1];
let hostDetailKey = ''; $: activeCollKey = path[2];
let exportInfo;
$: sharedState.currentHost.set(activeHostKey); $: sharedState.currentHost.set(activeHostKey);
$: sharedState.currentDb.set(activeDbKey); $: sharedState.currentDb.set(activeDbKey);
$: sharedState.currentColl.set(activeCollKey); $: sharedState.currentColl.set(activeCollKey);
export function createHost() { EventsOn('ui.host.new', () => hostTree.newHost());
hostDetailKey = ''; EventsOn('ui.host.edit', () => $hostTree[activeHostKey]?.edit());
showHostDetail = true; EventsOn('ui.host.remove', () => $hostTree[activeHostKey]?.remove());
} EventsOn('ui.host.tab', tab => {
path = path.slice(0, 1);
hostTab = tab;
});
function editHost(hostKey) { EventsOn('ui.db.new', () => $hostTree[activeHostKey]?.newDatabase());
hostDetailKey = hostKey; EventsOn('ui.db.dump', () => $hostTree[activeHostKey]?.databases[activeDbKey]?.dump());
showHostDetail = true; EventsOn('ui.db.drop', () => $hostTree[activeHostKey]?.databases[activeDbKey]?.drop());
} EventsOn('ui.db.tab', tab => {
path = path.slice(0, 2);
dbTab = tab;
});
export async function createDatabase() { EventsOn('ui.coll.new', () => $hostTree[activeHostKey]?.databases[activeDbKey]?.newCollection());
const name = await EnterText('Create a database', 'Enter the database name. Note: databases in MongoDB do not exist until they have a collection and an item. Your new database will not persist on the server; fill it to have it created.'); EventsOn('ui.coll.export', () => $hostTree[activeHostKey]?.databases[activeDbKey]?.collections[activeCollKey]?.export());
if (name) { EventsOn('ui.coll.truncate', () => $hostTree[activeHostKey]?.databases[activeDbKey]?.collections[activeCollKey]?.truncate());
$connections[activeHostKey].databases[name] = { collections: {} }; EventsOn('ui.coll.drop', () => $hostTree[activeHostKey]?.databases[activeDbKey]?.collections[activeCollKey]?.drop());
} EventsOn('ui.coll.tab', tab => collTab = tab);
}
async function renameCollection(oldCollKey) {
const newCollKey = await EnterText('Rename collection', `Enter a new name for collection ${oldCollKey}.`, oldCollKey);
if (newCollKey && (newCollKey !== oldCollKey)) {
const progress = startProgress(`Renaming collection "${oldCollKey}" to "${newCollKey}"…`);
const ok = await RenameCollection(activeHostKey, activeDbKey, oldCollKey, newCollKey);
if (ok) {
activeCollKey = newCollKey;
await hostTree.reload();
}
progress.end();
}
}
export async function createCollection() {
const name = await EnterText('Create a collection', 'Note: collections in MongoDB do not exist until they have at least one item. Your new collection will not persist on the server; fill it to have it created.');
if (name) {
$connections[activeHostKey].databases[activeDbKey].collections[name] = {};
}
}
function exportCollection(collKey) {
exportInfo = {
type: 'export',
filetype: 'json',
hostKey: activeHostKey,
dbKey: activeDbKey,
collKeys: [ collKey ],
};
}
function dumpCollection(collKey) {
exportInfo = {
type: 'dump',
filetype: 'bson',
hostKey: activeHostKey,
dbKey: activeDbKey,
collKeys: [ collKey ],
};
}
EventsOn('CreateHost', createHost);
EventsOn('CreateDatabase', createDatabase);
EventsOn('CreateCollection', createCollection);
</script> </script>
<div class="tree"> <div class="tree">
<div class="tree-buttons"> <div class="tree-buttons">
<button class="button-small" on:click={createHost}> <button class="button-small" on:click={hostTree.newHost}>
<Icon name="+" /> New host <Icon name="+" /> New host
</button> </button>
</div> </div>
<HostTree <HostTree bind:path />
bind:activeHostKey
bind:activeCollKey
bind:activeDbKey
bind:this={hostTree}
on:newHost={createHost}
on:newDatabase={createDatabase}
on:newCollection={createCollection}
on:editHost={e => editHost(e.detail)}
on:renameCollection={e => renameCollection(e.detail)}
on:exportCollection={e => exportCollection(e.detail)}
on:dumpCollection={e => dumpCollection(e.detail)}
/>
</div> </div>
<CollectionDetail {#if activeCollKey}
collection={$connections[activeHostKey]?.databases[activeDbKey]?.collections?.[activeCollKey]} <CollectionView
hostKey={activeHostKey} collection={$hostTree[activeHostKey]?.databases[activeDbKey]?.collections?.[activeCollKey]}
dbKey={activeDbKey} hostKey={activeHostKey}
collectionKey={activeCollKey} dbKey={activeDbKey}
/> collKey={activeCollKey}
bind:tab={collTab}
<HostDetail />
bind:show={showHostDetail} {:else if activeDbKey}
on:reload={hosts.update} <DatabaseView
hostKey={activeHostKey} database={$hostTree[activeHostKey]?.databases[activeDbKey]}
/> hostKey={activeHostKey}
dbKey={activeDbKey}
<DumpInfo bind:info={exportInfo} /> bind:tab={dbTab}
/>
{:else if activeHostKey}
<HostView
host={$hostTree[activeHostKey]}
hostKey={activeHostKey}
bind:tab={hostTab}
/>
{/if}
<style> <style>
.tree { .tree {

View File

@ -0,0 +1,146 @@
<script>
import BlankState from '$components/blankstate.svelte';
import CodeEditor from '$components/codeeditor.svelte';
import Icon from '$components/icon.svelte';
import { javascript } from '@codemirror/lang-javascript';
import { onDestroy, onMount } from 'svelte';
export let host = undefined;
export let database = undefined;
export let collection = undefined;
const placeholder = '// Write your script here...';
const extensions = [ javascript() ];
let script = '';
let result = {};
let copySucceeded = false;
let timeout;
let busy = false;
let editor;
async function run() {
busy = true;
if (collection) {
result = await collection.executeShellScript(script);
}
else if (database) {
result = await database.executeShellScript(script);
}
else if (host) {
result = await host.executeShellScript(script);
}
busy = false;
}
async function copyErrorDescription() {
await navigator.clipboard.writeText(result.errorDescription);
copySucceeded = true;
timeout = setTimeout(() => copySucceeded = false, 1500);
}
onMount(() => {
editor.dispatch({
changes: {
from: 0,
to: editor.state.doc.length,
insert: placeholder,
},
selection: {
from: 0,
anchor: 0,
to: placeholder.length,
head: placeholder.length,
},
});
editor.focus();
});
onDestroy(() => clearTimeout(timeout));
</script>
<div class="shell">
<div class="overflow">
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="field">
<CodeEditor bind:editor bind:text={script} {extensions} />
</label>
</div>
<div class="output">
{#if busy}
<BlankState icon="loading" label="Executing…" />
{:else if result.errorTitle || result.errorDescription}
<BlankState title={result.errorTitle} label={result.errorDescription} icon="!">
<button class="button-small" on:click={copyErrorDescription}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} /> Copy error message
</button>
</BlankState>
{:else}
<pre>{result.output || ''}</pre>
{/if}
</div>
<div class="controls">
{#key result}
<div class="status flash-green">
{#if result?.status}
Exit code: {result.status}
{/if}
</div>
{/key}
<button class="btn" on:click={run}>
<Icon name="play" /> Run
</button>
</div>
</div>
<style>
.shell {
display: grid;
grid-template: 1fr auto / 1fr 1fr;
}
.overflow {
overflow: auto;
}
.field {
height: 100%;
}
.field :global(.editor) {
border-radius: 0;
}
.output {
background-color: #111;
color: #fff;
overflow: auto;
display: flex;
}
.output :global(*) {
color: #fff;
}
.output pre {
font-family: monospace;
padding: 0.5rem;
user-select: text;
-webkit-user-select: text;
cursor: text;
}
.output :global(.blankstate) {
margin: auto;
padding: 0.5rem;
}
.controls {
margin-top: 0.5rem;
display: flex;
align-items: center;
grid-column: 1 / 3;
}
.controls .status {
margin-right: auto;
}
</style>

View File

@ -1,60 +0,0 @@
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function createHost() {
dispatch('createHost');
}
</script>
<div class="welcome">
<div class="brand">
<img src="/logo.png" alt="" class="logo" />
<div class="text">
<div class="name">Welcome to Rolens!</div>
<div class="subtitle">A modest MongoDB client</div>
</div>
</div>
<button class="btn" on:click={createHost}>Create your first host</button>
</div>
<style>
.welcome {
/* transform: translateY(-80px); */
margin-top: -90px;
padding: 2rem;
}
.brand {
display: flex;
}
.brand .logo {
height: 200px;
}
.brand .text {
align-self: flex-end;
margin: 0 0 4rem 1rem;
}
.brand .text .name {
font-size: 2.5rem;
margin-bottom: 1.5rem;
font-weight: 600;
}
.brand .text .subtitle {
font-size: 1.5rem;
}
.logo {
height: 250px;
}
/* .title {
font-weight: 600;
font-size: 1.5rem;
} */
.btn {
margin-top: 2rem;
}
</style>

View File

@ -50,8 +50,8 @@ a {
hr { hr {
border: none; border: none;
height: 1px; border-top: 1px solid #ccc;
background-color: #ccc; width: 100%;
} }
.loading { .loading {
@ -65,38 +65,38 @@ select:disabled {
color: #444; color: #444;
} }
.btn { .button {
background-color: #00008b; background-color: #00008b;
border: 1px solid #00008b; border: 1px solid #00008b;
padding: 0.5rem; padding: 0.5rem;
border-radius: var(--radius); border-radius: var(--radius);
color: #fff; color: #fff;
} }
.btn:focus, .button:focus,
.btn:active { .button:active {
box-shadow: 0 0 0 3px rgba(0, 0, 139, 0.2); box-shadow: 0 0 0 3px rgba(0, 0, 139, 0.2);
outline: none; outline: none;
} }
.btn.danger { .button.danger {
background-color: #c00; background-color: #c00;
border: 1px solid #c00; border: 1px solid #c00;
} }
.btn.danger:active, .button.danger:active,
.btn.danger:focus { .button.danger:focus {
box-shadow: 0 0 0 3px rgba(204, 0, 0, 0.2); box-shadow: 0 0 0 3px rgba(204, 0, 0, 0.2);
} }
.btn.secondary { .button.secondary {
border: 1px solid #ccc; border: 1px solid #ccc;
background-color: #fff; background-color: #fff;
color: inherit; color: inherit;
} }
.btn.secondary:hover { .button.secondary:hover {
background-color: #eee; background-color: #eee;
} }
.btn.secondary:active { .button.secondary:active {
background-color: #ddd; background-color: #ddd;
} }
.btn:disabled { .button:disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
} }
@ -142,7 +142,7 @@ select:disabled {
.field > textarea, .field > textarea,
.field > select { .field > select {
flex: 1; flex: 1;
padding: 0.5rem; padding: 0 0.5rem;
border: 1px solid #ccc; border: 1px solid #ccc;
background-color: #fff; background-color: #fff;
appearance: none; appearance: none;

15
frontend/wailsjs/go/app/App.d.ts generated vendored
View File

@ -1,7 +1,6 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
import {app} from '../models'; import {app} from '../models';
import {primitive} from '../models';
import {map[string]app} from '../models'; import {map[string]app} from '../models';
import {menu} from '../models'; import {menu} from '../models';
import {context} from '../models'; import {context} from '../models';
@ -21,9 +20,13 @@ export function DropIndex(arg1:string,arg2:string,arg3:string,arg4:string):Promi
export function Environment():Promise<app.EnvironmentInfo>; export function Environment():Promise<app.EnvironmentInfo>;
export function FindItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise<app.QueryResult>; export function ExecuteShellScript(arg1:string,arg2:string,arg3:string,arg4:string):Promise<app.ExecuteShellScriptResult>;
export function GetIndexes(arg1:string,arg2:string,arg3:string):Promise<Array<primitive.M>>; export function FindItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise<app.FindItemsResult>;
export function GetIndexes(arg1:string,arg2:string,arg3:string):Promise<app.GetIndexesResult>;
export function HostLogs(arg1:string,arg2:string):Promise<app.HostLogsResult>;
export function Hosts():Promise<map[string]app.Host>; export function Hosts():Promise<map[string]app.Host>;
@ -31,11 +34,11 @@ export function InsertItems(arg1:string,arg2:string,arg3:string,arg4:string):Pro
export function Menu():Promise<menu.Menu>; export function Menu():Promise<menu.Menu>;
export function OpenCollection(arg1:string,arg2:string,arg3:string):Promise<primitive.M>; export function OpenCollection(arg1:string,arg2:string,arg3:string):Promise<app.OpenCollectionResult>;
export function OpenConnection(arg1:string):Promise<Array<string>>; export function OpenConnection(arg1:string):Promise<app.OpenConnectionResult>;
export function OpenDatabase(arg1:string,arg2:string):Promise<Array<string>>; export function OpenDatabase(arg1:string,arg2:string):Promise<app.OpenDatabaseResult>;
export function PerformDump(arg1:string):Promise<boolean>; export function PerformDump(arg1:string):Promise<boolean>;

View File

@ -30,6 +30,10 @@ export function Environment() {
return window['go']['app']['App']['Environment'](); return window['go']['app']['App']['Environment']();
} }
export function ExecuteShellScript(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['ExecuteShellScript'](arg1, arg2, arg3, arg4);
}
export function FindItems(arg1, arg2, arg3, arg4) { export function FindItems(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['FindItems'](arg1, arg2, arg3, arg4); return window['go']['app']['App']['FindItems'](arg1, arg2, arg3, arg4);
} }
@ -38,6 +42,10 @@ export function GetIndexes(arg1, arg2, arg3) {
return window['go']['app']['App']['GetIndexes'](arg1, arg2, arg3); return window['go']['app']['App']['GetIndexes'](arg1, arg2, arg3);
} }
export function HostLogs(arg1, arg2) {
return window['go']['app']['App']['HostLogs'](arg1, arg2);
}
export function Hosts() { export function Hosts() {
return window['go']['app']['App']['Hosts'](); return window['go']['app']['App']['Hosts']();
} }

Some files were not shown because too many files have changed in this diff Show More