1
0
mirror of https://github.com/garraflavatra/rolens.git synced 2025-07-07 09:34:05 +00:00

68 Commits

Author SHA1 Message Date
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
90 changed files with 3649 additions and 1197 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/wailsjs/**/* linguist-generated
/website/**/* linguist-documentation

View File

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

View File

@ -13,7 +13,14 @@ jobs:
strategy:
fail-fast: false
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]
node-version: [16]
@ -32,8 +39,12 @@ jobs:
- name: Install Wails
run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
- name: Install Wails dependencies for Linux
if: matrix.platform == 'ubuntu-22.04'
- name: Install build dependencies for macOS
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
- name: Set up Node.js
@ -43,14 +54,44 @@ jobs:
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies
run: cd frontend && npm ci && cd ..
- name: Cross-compile Rolens for Windows
if: contains(matrix.platform, 'windows')
run: ./build/windows/ci_generate.ps1 -platform "${{ matrix.platform }}"
- name: Build Rolens
run: wails build
- name: Cross-compile Rolens for Darwin
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
with:
name: rolens-${{ matrix.platform }}
path: build/bin/*
path: releases/*
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

4
.gitignore vendored
View File

@ -1,7 +1,11 @@
.DS_Store
/build/version.txt
/build/bin/
/build/windows/installer/tmp/
/build/darwin/dmg_settings.json
/releases
/frontend/node_modules/
/frontend/dist/

View File

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

View File

@ -1,9 +1,22 @@
## [v0.2.0](https://github.com/garraflavatra/rolens/releases/tag/v0.2.0)
## [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.
* 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.
## [v0.1.0](https://github.com/garraflavatra/rolens/releases/tag/v0.1.0)
## [v0.1.0]
Initial release.
[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

View File

@ -37,6 +37,6 @@ To obtain a copy of the source code, do either of the following:
`cd` into the root directory of the source code and run either:
* `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).

View File

@ -2,7 +2,9 @@
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?
@ -54,5 +56,6 @@ Feel free to contact me if you have questions! [Send an e-mail.](mailto:romein@v
## Credits
* [Wails](https://wails.io/) facilitates the build process for multiple OSes.
* Icons are from [Feather Icons](https://feathericons.com/) by [Cole Bemis](https://github.com/colebemis).
* Vector drawings come from [unDraw](https://undraw.co/).

View File

@ -10,10 +10,7 @@ The structure is:
## Mac
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`.
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`.
The directory contains the following files:
@ -22,14 +19,9 @@ The directory contains the following files:
## Windows
The `windows` directory contains the manifest and rc files used when building 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`.
The `windows` directory contains the manifest and rc files used when building 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
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.
- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to 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`.
- `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)
- `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)
- `wails.exe.manifest` - The main application manifest file.

41
build/ci_bundle.sh Executable file
View File

@ -0,0 +1,41 @@
#!/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 binaries
mv artifacts/*/rolens-macos-11-amd64.tar.gz bundle/rolens-$version-macos-amd64.tar.gz
mv artifacts/*/rolens-macos-11-arm64.tar.gz bundle/rolens-$version-macos-arm64.tar.gz
# Windows binaries
mv artifacts/*/rolens-windows-2019-amd64.zip bundle/rolens-$version-windows-amd64.zip
mv artifacts/*/rolens-windows-2019-arm64.zip bundle/rolens-$version-windows-arm64.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
);

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

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 623 KiB

After

Width:  |  Height:  |  Size: 451 KiB

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 id="dialogoutlets"></div>
<script src="./src/main.js" type="module"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -9,17 +9,33 @@
"preview": "vite preview"
},
"dependencies": {
"@codemirror/lang-javascript": "^6.1.8",
"@codemirror/language": "^6.7.0",
"@codemirror/view": "^6.12.0",
"@codemirror/lang-javascript": "^6.1.9",
"@codemirror/language": "^6.8.0",
"@codemirror/view": "^6.13.2",
"bson": "^4.7.2",
"codemirror": "^6.0.1",
"date-fns": "^2.30.0"
},
"devDependencies": {
"@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",
"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>
import BlankState from '$components/blankstate.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 environment from '$lib/stores/environment';
import hosts from '$lib/stores/hosts';
import hostTree from '$lib/stores/hosttree';
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 Settings from '$organisms/settings/index.svelte';
import { EventsEmit, EventsOn } from '$wails/runtime';
import { EventsOn } from '$wails/runtime';
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;
$: host = hosts[activeHostKey];
$: connection = $connections[activeHostKey];
hosts.subscribe(h => {
if (h && (showWelcomeScreen === undefined)) {
showWelcomeScreen = !Object.keys($hosts || {}).length;
applicationInited.defer(() => {
hostTree.subscribe(hosts => {
if (hostTree.hasBeenInited() && (showWelcomeScreen === undefined)) {
showWelcomeScreen = !Object.keys(hosts || {}).length;
}
});
});
async function createFirstHost() {
showWelcomeScreen = false;
await tick();
connectionManager.createHost();
hostTree.newHost();
}
EventsOn('OpenPreferences', () => settingsModalOpen = true);
EventsOn('OpenAboutModal', () => aboutModalOpen = true);
function showAboutDialog() {
dialogs.new(AboutDialog);
}
function showSettings() {
dialogs.new(SettingsDialog);
}
EventsOn('OpenPreferences', showSettings);
EventsOn('OpenAboutModal', showAboutDialog);
</script>
<svelte:window on:contextmenu|preventDefault />
<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}>
{#if showWelcomeScreen}
<BlankState label="Welcome to Rolens!" image="/logo.png" pale={false} big={true}>
<button class="btn" on:click={createFirstHost}>Add your first host</button>
</BlankState>
{:else}
<Connection {activeHostKey} bind:activeCollKey bind:activeDbKey bind:this={connectionManager} />
<Connection />
{/if}
</main>
{#key $contextMenu}
<ContextMenu {...$contextMenu} on:close={contextMenu.hide} />
{/key}
<Settings bind:show={settingsModalOpen} />
<About bind:show={aboutModalOpen} />
{/if}
</div>
@ -69,6 +68,10 @@
height: 0;
background-color: #00002a;
--wails-draggable: drag;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
}
#root.platform-darwin .titlebar {
height: var(--darwin-titlebar-height);

View File

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

View File

@ -1,5 +1,5 @@
<script>
import { resolveKeypath, setValue } from '$lib/objects';
import { pathsAreEqual, resolveKeypath, setValue } from '$lib/objects';
import contextMenu from '$lib/stores/contextmenu';
import { createEventDispatcher } from 'svelte';
import FormInput from './forminput.svelte';
@ -50,7 +50,9 @@
return obj;
}
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 {
return obj;
@ -62,18 +64,9 @@
return false;
}
toggleChildren(itemKey, false);
if (activeKey !== itemKey) {
activeKey = itemKey;
if (level === 0) {
activePath = [ itemKey ];
}
else {
activePath = [ ...path, itemKey ];
}
dispatch('select', { level, itemKey, index });
}
activePath = [ ...path.slice(0, level), itemKey ];
dispatch('select', { level, itemKey, index, path: activePath });
}
function closeAll() {
@ -89,7 +82,7 @@
}
function doubleClick(itemKey, index) {
// toggleChildren(itemKey, false);
toggleChildren(itemKey, false);
dispatch('trigger', { level, itemKey, index });
childrenOpen[itemKey] = true;
}
@ -136,7 +129,7 @@
on:dblclick={() => doubleClick(item[key], index)}
on:contextmenu|preventDefault={evt => showContextMenu(evt, item)}
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
>
{#if !hideChildrenToggles}

View File

@ -1,5 +1,8 @@
<script>
import { onDestroy } from 'svelte';
import BlankState from './blankstate.svelte';
import GridItems from './grid-items.svelte';
import Icon from './icon.svelte';
export let columns = [];
export let items = [];
@ -12,7 +15,20 @@
export let canSelect = true;
export let canRemoveItems = 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>
<div class="grid">
@ -27,6 +43,15 @@
</div>
{/if} -->
{#if busy}
<BlankState label={(busy === true) ? 'Loading…' : busy} icon="loading" />
{:else if errorTitle || errorDescription}
<BlankState title={errorTitle} label={errorDescription} icon="!">
<button class="button-small" on:click={copyErrorDescription}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} /> Copy error message
</button>
</BlankState>
{:else}
<table>
{#if showHeaders && columns.some(col => col.title)}
<thead>
@ -66,6 +91,7 @@
/>
</tbody>
</table>
{/if}
</div>
<style>
@ -75,15 +101,6 @@
background-color: #fff;
}
/* .actions {
margin-bottom: 0.5rem;
padding: 0.5rem;
border-bottom: 1px solid #ccc;
}
.actions button {
margin-right: 0.2rem;
} */
table {
border-collapse: collapse;
width: 100%;
@ -99,7 +116,20 @@
padding: 2px;
}
.grid :global(.blankstate) {
height: 100%;
padding: 1rem;
}
/* tfoot button {
margin-top: 0.5rem;
}
.actions {
margin-bottom: 0.5rem;
padding: 0.5rem;
border-bottom: 1px solid #ccc;
}
.actions button {
margin-right: 0.2rem;
} */
</style>

View File

@ -14,9 +14,29 @@
width: auto;
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>
<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={name === 'loading'}
>
{#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>
{:else if name === 'chev-l'}
@ -44,7 +64,12 @@
{: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>
{: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'}
<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'}
@ -60,15 +85,35 @@
{:else if name === 'zap'}
<path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z" />
{: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'}
<path d="M4 7V4h16v3M9 20h6M12 4v16" />
{:else if name === 'hash'}
<path d="M4 9h16M4 15h16M10 3 8 21M16 3l-2 18" />
{: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'}
<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'}
<path d="m16 18 6-6-6-6M8 6l-6 6 6 6" />
{:else if name === 'target'}
@ -93,5 +138,9 @@
<path d="M18 20V10M12 20V4M6 20v-6" />
{: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>
{: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" />
{/if}
</svg>

View File

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

View File

@ -8,6 +8,9 @@
export let activePath = [];
export let hideObjectIndicators = false;
export let getRootMenu = () => undefined;
export let errorTitle = '';
export let errorDescription = '';
export let busy = false;
const columns = [
{ key: 'key', label: 'Key' },
@ -116,4 +119,7 @@
{columns}
{items}
{hideObjectIndicators}
{errorTitle}
{errorDescription}
{busy}
/>

View File

@ -1,10 +1,10 @@
<script>
import { jsonLooseParse, looseJsonIsValid } from '$lib/strings';
import { looseJsonIsValid } from '$lib/strings';
import { EJSON } from 'bson';
import { createEventDispatcher, onDestroy } from 'svelte';
import Icon from './icon.svelte';
import Modal from './modal.svelte';
import ObjectEditor from './objecteditor.svelte';
import { EJSON } from 'bson';
export let data;
export let saveable = false;

View File

@ -1,26 +1,39 @@
<script>
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher, onMount } from 'svelte';
import Icon from './icon.svelte';
export let tabs = [];
export let selectedKey = {};
export let canAddTab = false;
export let multiline = false;
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) {
selectedKey = tabKey;
dispatch('select', tabKey);
}
onMount(() => {
window.addEventListener('resize', updateMeasurements);
});
</script>
<nav class="tabs">
<nav class="tabs" class:multiline={multiline || (pixelsPerTab < maxPixelsPerMultilineTab)} bind:this={navEl}>
<ul>
{#each tabs as tab (tab.key)}
<li class:active={tab.key === selectedKey}>
<button class="tab" on:click={() => select(tab.key)}>
{#if tab.icon} <Icon name={tab.icon} /> {/if}
{tab.title}
<span class="label">{tab.title}</span>
</button>
{#if tab.closable}
<button class="button-small" on:click={() => dispatch('closeTab', tab.key)}>
@ -48,7 +61,7 @@
}
li {
display: inline-block;
flex-grow: 1;
flex: 1;
position: relative;
}
@ -89,6 +102,11 @@
cursor: not-allowed;
}
nav.tabs.multiline button.tab .label {
display: block;
margin-top: 4px;
}
.button-small {
position: absolute;
right: 7px;

View File

@ -1,15 +1,17 @@
<script>
import Modal from '$components/modal.svelte';
import alink from '$lib/actions/alink';
export let show = true;
import environment from '$lib/stores/environment';
</script>
<Modal bind:show width="400px" title=" ">
<Modal width="400px" title=" " on:close>
<div class="brand">
<img src="/logo.png" alt="Rolens logo" />
<div>
<div class="title">Rolens</div>
<div class="title">
Rolens
<span class="version">{$environment.version}</span>
</div>
<div class="description">Intuitive MongoDB <br /> administration tool</div>
</div>
</div>
@ -19,7 +21,7 @@
<div class="info">
<p class="copy">© Romein van Buren, 2023.</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/issues/new" use:alink>Report a bug</a> |
<a href="https://github.com/garraflavatra/rolens/blob/main/LICENSE" use:alink>License</a>
@ -42,6 +44,11 @@
font-weight: 600;
line-height: 2.5rem;
}
.brand .title .version {
font-size: 80%;
font-weight: 300;
opacity: 0.65;
}
.brand .description {
font-size: 1.5rem;
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="btn">OK</button>
<button on:click={close} class="btn 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 input from '$lib/actions/input';
import settings from '$lib/stores/settings';
export let show = false;
</script>
<Modal title="Preferences" bind:show>
<Modal title="Preferences" on:close>
<div class="prefs">
<label for="defaultLimit">Initial number of items to retrieve using one query (limit):</label>
<label class="field">
@ -27,7 +25,7 @@
</span>
<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">
<DirectoryChooser id="defaultExportDirectory" bind:value={$settings.defaultExportDirectory} />
</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

@ -58,6 +58,18 @@ export function setValue(object, path, value) {
}
export function deepClone(obj) {
// Room for improvement below
// @todo: Room for improvement below
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,322 @@
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,
GetIndexes,
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() {
const progress = startProgress(`Truncating collection "${collKey}"…`);
await TruncateCollection(hostKey, dbKey, collKey);
await refresh();
progress.end();
};
collection.drop = async function() {
const progress = startProgress(`Dropping collection "${collKey}"…`);
const success = await DropCollection(hostKey, dbKey, collKey);
if (success) {
await refresh();
}
progress.end();
};
collection.getIndexes = async function() {
const progress = startProgress(`Retrieving indexes of "${collKey}"…`);
collection.indexes = [];
const { indexes, error } = await GetIndexes(hostKey, dbKey, collKey);
if (error) {
progress.end();
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 progress = startProgress(`Dropping index ${index.name}`);
const hasBeenDropped = await DropIndex(hostKey, dbKey, collKey, index.name);
progress.end();
return hasBeenDropped;
};
collection.indexes.push(index);
}
progress.end();
};
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 progress = startProgress('Creating index…');
const newIndexName = await CreateIndex(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(event.detail.index));
if (newIndexName) {
dialog.$close();
}
progress.end();
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);
}
});
});
};
}
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();
}
};
}
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();
}
};
await refresh();
};
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();
}
else {
listeners.push(listener)
listeners.push(listener);
}
};
@ -18,16 +18,12 @@ const { subscribe } = derived([ environment, applicationSettings ], ([ env, sett
if (alreadyInited) {
return;
}
if (env && settings) {
else if (env && settings) {
Promise.all(listeners.map(l => l())).then(() => {
set(true);
alreadyInited = true;
// Remove loading spinner.
document.getElementById('app-loading')?.remove();
// Call hooks
listeners.forEach(l => l());
});
}
}, 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 { get, writable } from 'svelte/store';
@ -23,6 +25,11 @@ function forCollection(hostKey, dbKey, collKey) {
return Object.fromEntries(entries);
}
function openConfig(collection, firstItem = {}) {
const dialog = dialogs.new(ViewConfigDialog, { collection, firstItem });
return dialog;
}
reload();
subscribe(newViewStore => {
if (skipUpdate) {
@ -32,5 +39,5 @@ subscribe(newViewStore => {
UpdateViewStore(JSON.stringify(newViewStore));
});
const views = { reload, set, subscribe, forCollection };
const views = { reload, set, subscribe, forCollection, openConfig };
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 Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte';
import Collation from '$lib/mongo/collation.svelte';
import ObjectEditor from '$components/objecteditor.svelte';
import { aggregationStageDocumentationURL, aggregationStages } from '$lib/mongo';
import Collation from '$lib/mongo/collation.svelte';
import { jsonLooseParse, looseJsonIsValid } from '$lib/strings';
import { Aggregate } from '$wails/go/app/App';
import { BrowserOpenURL } from '$wails/runtime/runtime';
@ -31,7 +31,9 @@
}
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(
collection.hostKey,
collection.dbKey,
@ -66,7 +68,8 @@
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="field">
<ObjectEditor bind:text={stage.data} on:inited={e => {
<ObjectEditor bind:text={stage.data}
on:inited={e => {
e.detail.editor.dispatch({
changes: {
from: 0,

View File

@ -1,9 +1,9 @@
<script>
import FormInput from '$components/forminput.svelte';
import Hint from '$components/hint.svelte';
import Icon from '$components/icon.svelte';
import { inputTypes } from '$lib/mongo';
import { resolveKeypath, setValue } from '$lib/objects';
import Hint from '$components/hint.svelte';
export let item = {};
export let view = {};
@ -38,7 +38,7 @@
{#if item && view}
{#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">
<div class="label">
<Icon name={iconMap[column.inputType]} />

View File

@ -2,43 +2,34 @@
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;
export let query = undefined;
const dispatch = createEventDispatcher();
let viewKey = collection.viewKey;
$: viewKey = collection.viewKey;
$: if (info) {
info.viewKey = viewKey;
}
const exportInfo = { ...query, viewKey: collection.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;
}
function submit() {
exportInfo.view = $views[exportInfo.viewKey];
dispatch('export', { exportInfo });
}
</script>
<Modal bind:show={info} title="Export results" width="450px">
<form on:submit|preventDefault={performExport}>
<Modal title="Export results" width="450px" on:close>
<form on:submit|preventDefault={submit}>
<label class="field">
<span class="label">Export</span>
<select bind:value={info.contents}>
<select bind:value={exportInfo.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>
<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={info.format}>
<select bind:value={exportInfo.format}>
<option value="jsonarray">JSON array</option>
<option value="ndjson">Newline delimited JSON</option>
<option value="csv">CSV</option>
@ -47,7 +38,7 @@
<label class="field">
<span class="label">View to use</span>
<select bind:value={viewKey}>
<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}
@ -59,7 +50,7 @@
</form>
<svelte:fragment slot="footer">
<button class="btn" on:click={performExport}>
<button class="btn" on:click={submit}>
<Icon name="play" /> Start export
</button>
</svelte:fragment>

View File

@ -2,14 +2,12 @@
import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte';
import input from '$lib/actions/input';
import { CreateIndex } from '$wails/go/app/App';
import { createEventDispatcher } from 'svelte';
export let collection = {};
export let creatingNewIndex = false;
export let collection;
const dispatch = createEventDispatcher();
let index = { model: [] };
const index = { model: [] };
function addRule() {
index.model = [ ...index.model, {} ];
@ -21,16 +19,11 @@
}
async function create() {
const newIndexName = await CreateIndex(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(index));
if (newIndexName) {
creatingNewIndex = false;
index = { model: [] };
dispatch('reload');
}
dispatch('create', { index });
}
</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}>
<label class="field name">
<span class="label">Name</span>

View File

@ -4,13 +4,12 @@
import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte';
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 { createEventDispatcher } from 'svelte';
export let queryToSave = undefined;
export let collection = {};
export let show = false;
const dispatch = createEventDispatcher();
let gridSelectedPath = [];
@ -22,23 +21,16 @@
queryToSave.dbKey = collection.dbKey;
queryToSave.collKey = collection.key;
const newId = queries.create(queryToSave);
if (newId) {
dispatch('created', newId);
queryToSave = undefined;
selectedKey = newId;
select();
}
dispatch('create', { query: queryToSave });
selectedKey = queryToSave.name;
}
else {
select();
selectActive();
}
}
function select() {
dispatch('select', selectedKey);
show = false;
function selectActive() {
dispatch('select', { query: $queries[selectedKey] });
}
function gridSelect(event) {
@ -53,7 +45,7 @@
function gridTrigger(event) {
gridSelect(event);
select();
selectActive();
}
async function gridRemove(event) {
@ -71,7 +63,7 @@
}
</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}>
{#if queryToSave}
<label class="field queryname">
@ -88,7 +80,11 @@
columns={[ { key: 'n', title: 'Query name' }, { key: 'h', title: 'Host' }, { key: 'ns', title: 'Namespace' } ]}
key="n"
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;
}, {})}
showHeaders={true}

View File

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

View File

@ -6,18 +6,15 @@
import input from '$lib/actions/input';
import { deepClone } from '$lib/objects';
import { startProgress } from '$lib/progress';
import queries from '$lib/stores/queries';
import applicationSettings from '$lib/stores/settings';
import views from '$lib/stores/views';
import { convertLooseJson } from '$lib/strings';
import { FindItems, RemoveItemById, UpdateFoundDocument } from '$wails/go/app/App';
import { EJSON } from 'bson';
import { createEventDispatcher, onMount } from 'svelte';
import ExportInfo from './components/export.svelte';
import QueryChooser from './components/querychooser.svelte';
import { onMount } from 'svelte';
export let collection;
const dispatch = createEventDispatcher();
const defaults = {
query: '{}',
sort: $applicationSettings.defaultSort || '{ "_id": 1 }',
@ -32,24 +29,24 @@
let queryField;
let activePath = [];
let objectViewerData;
let queryToSave;
let showQueryChooser = false;
let exportInfo;
let querying = false;
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;
$: 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() {
if (querying) {
return;
}
querying = true;
const progress = startProgress('Performing query…');
querying = `Querying ${collection.key}…`;
activePath = [];
const newResult = await FindItems(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(form));
@ -59,7 +56,6 @@
submittedForm = deepClone(form);
}
progress.end();
resetFocus();
querying = false;
}
@ -70,19 +66,18 @@
}
}
function loadQuery() {
queryToSave = undefined;
showQueryChooser = true;
async function loadQuery() {
const query = await collection.openQueryChooser();
if (query) {
form = { ...query };
submitQuery();
}
}
function saveQuery() {
queryToSave = form;
showQueryChooser = true;
}
function queryChosen(event) {
if ($queries[event?.detail]) {
form = { ...$queries[event?.detail] };
async function saveQuery() {
const query = await collection.openQueryChooser(form);
if (query) {
form = { ...query };
submitQuery();
}
}
@ -130,6 +125,10 @@
objectViewerData = item;
}
function openViewConfig() {
views.openConfig(collection, result.results?.[0] || {});
}
export function performQuery(q) {
form = { ...defaults, ...q };
submitQuery();
@ -142,7 +141,7 @@
collection.dbKey,
collection.key,
EJSON.stringify({ _id: event.detail.originalData._id }),
event.detail.text
convertLooseJson(event.detail.text)
);
if (success) {
@ -162,7 +161,12 @@
<div class="form-row one">
<label class="field">
<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"
bind:this={queryField}
bind:value={form.query}
use:input={{ type: 'json', autofocus: true }}
placeholder={defaults.query} />
</label>
<label class="field">
@ -179,12 +183,22 @@
<label class="field">
<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 class="field">
<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>
</div>
@ -192,7 +206,7 @@
<button type="submit" class="btn" title="Run query">
<Icon name="play" /> Run
</button>
<button class="btn secondary" type="button" on:click={() => exportInfo = {}}>
<button class="btn secondary" type="button" on:click={() => collection.export(form)}>
<Icon name="save" /> Export results…
</button>
<div class="field">
@ -215,15 +229,23 @@
hideObjectIndicators={$views[collection.viewKey]?.hideObjectIndicators}
bind:activePath
on:trigger={e => openJson(e.detail?.index)}
errorTitle={result.errorTitle}
errorDescription={result.errorDescription}
busy={querying}
/>
{:else}
<Grid
key="_id"
columns={$views[collection.viewKey]?.columns?.map(c => ({ key: c.key, title: c.key })) || []}
columns={$views[collection.viewKey]?.columns?.map(c => {
return { key: c.key, title: c.key };
}) || []}
showHeaders={true}
items={result.results ? result.results.map(r => EJSON.deserialize(r)) : []}
bind:activePath
on:trigger={e => openJson(e.detail?.index)}
errorTitle={result.errorTitle}
errorDescription={result.errorDescription}
busy={querying}
/>
{/if}
{/key}
@ -242,7 +264,7 @@
<option value={key}>{view.name}</option>
{/each}
</select>
<button class="btn" on:click={() => dispatch('openViewConfig', { firstItem: result.results?.[0] })} title="Configure view">
<button class="btn" on:click={openViewConfig} title="Configure view">
<Icon name="cog" />
</button>
</label>
@ -266,15 +288,6 @@
</div>
</div>
<QueryChooser
bind:queryToSave
bind:show={showQueryChooser}
on:select={queryChosen}
{collection}
/>
<ExportInfo on:openViewConfig bind:collection bind:info={exportInfo} />
{#if objectViewerData}
<!-- @todo Implement save -->
<ObjectViewer bind:data={objectViewerData} saveable on:save={saveDocument} bind:successMessage={objectViewerSuccessMessage} />

View File

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

View File

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

View File

@ -2,6 +2,7 @@
import Details from '$components/details.svelte';
import Grid from '$components/grid.svelte';
import Icon from '$components/icon.svelte';
import ObjectEditor from '$components/objecteditor.svelte';
import ObjectViewer from '$components/objectviewer.svelte';
import { randomString } from '$lib/math';
import { inputTypes } from '$lib/mongo';
@ -11,7 +12,6 @@
import { EJSON } from 'bson';
import { createEventDispatcher, onMount } from 'svelte';
import Form from './components/form.svelte';
import ObjectEditor from '$components/objecteditor.svelte';
export let collection;
@ -25,8 +25,8 @@
let objectViewerData = '';
let viewType = 'form';
let allValid = false;
let viewsForCollection = {};
$: viewsForCollection = views.forCollection(collection.hostKey, collection.dbKey, collection.key);
$: oppositeViewType = viewType === 'table' ? 'form' : 'table';
$: allValid = Object.values(formValidity).every(v => v !== false);
@ -46,6 +46,10 @@
newItems = [ {} ];
}
$: if ($views) {
viewsForCollection = views.forCollection(collection.hostKey, collection.dbKey, collection.key);
}
async function insert() {
insertedIds = await InsertItems(
collection.hostKey,
@ -60,9 +64,7 @@
function showDocs() {
dispatch('performFind', {
query: insertedIds.length === 1
? `{ "_id": ${JSON.stringify(insertedIds[0])} }`
: `{ "_id": { "$in": [ ${insertedIds.map(id => JSON.stringify(id)).join(', ')} ] } }`,
query: insertedIds.length === 1 ? `{ "_id": ${JSON.stringify(insertedIds[0])} }` : `{ "_id": { "$in": [ ${insertedIds.map(id => JSON.stringify(id)).join(', ')} ] } }`,
});
}
@ -97,6 +99,10 @@
newItems = newItems;
}
function openViewConfig() {
views.openConfig(collection);
}
onMount(() => {
if (collection.viewKey === 'list') {
editor.dispatch({
@ -146,11 +152,11 @@
<Grid
key="id"
items={newItems}
columns={
$views[collection.viewKey]?.columns
columns={$views[collection.viewKey]?.columns
?.filter(c => inputTypes.includes(c.inputType))
.map(c => ({ ...c, id: randomString(8), title: c.key })) || []
}
.map(c => {
return { ...c, id: randomString(8), title: c.key };
}) || []}
showHeaders={true}
canSelect={false}
canRemoveItems={true}
@ -192,7 +198,7 @@
<option value={key}>{key === 'list' ? 'Raw JSON' : view.name}</option>
{/each}
</select>
<button class="btn" type="button" on:click={() => dispatch('openViewConfig')} title="Configure view">
<button class="btn" type="button" on:click={openViewConfig} title="Configure view">
<Icon name="cog" />
</button>
</label>

View File

@ -11,7 +11,7 @@
let many = true;
let result = undefined;
let editor;
$: code = `db.${collection.key}.remove(${json});`;
// $: code = `db.${collection.key}.remove(${json});`;
async function removeItems() {
result = await RemoveItems(

View File

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

View File

@ -11,7 +11,7 @@
const allOperators = Object.values(atomicUpdateOperators).map(Object.keys).flat();
const form = { query: '{}', parameters: [ { type: '$set' } ] };
let updatedCount;
$: code = buildCode(form);
// $: code = buildCode(form);
$: invalid = !form.query || form.parameters?.some(param => {
if (!param.value) {
return true;
@ -26,26 +26,28 @@
}
});
function buildCode(form) {
let operation = '{ ' + form.parameters.filter(p => p.type).map(p => `${p.type}: ${p.value || '{}'}`).join(', ') + ' }';
if (operation === '{ }') {
operation = '{}';
}
// function buildCode(form) {
// let operation = '{ ' + form.parameters.filter(p => p.type).map(p => `${p.type}: ${p.value || '{}'}`).join(', ') + ' }';
// if (operation === '{ }') {
// operation = '{}';
// }
let options = (form.upsert || form.many) ? ', { ' : '';
form.upsert && (options += 'upsert: true');
form.upsert && form.many && (options += ', ');
form.many && (options += 'multi: true');
(form.upsert || form.many) && (options += ' }');
// let options = (form.upsert || form.many) ? ', { ' : '';
// form.upsert && (options += 'upsert: true');
// form.upsert && form.many && (options += ', ');
// form.many && (options += 'multi: true');
// (form.upsert || form.many) && (options += ' }');
const code = `db.${collection.key}.update(${form.query || '{}'}, ${operation}${options});`;
return code;
}
// const code = `db.${collection.key}.update(${form.query || '{}'}, ${operation}${options});`;
// return code;
// }
async function submitQuery() {
const f = deepClone(form);
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));
}

View File

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

View File

@ -0,0 +1,60 @@
<script>
import BlankState from '$components/blankstate.svelte';
import TabBar from '$components/tabbar.svelte';
import { EventsOn } from '$wails/runtime/runtime';
import Stats from './stats.svelte';
export let database;
export let hostKey;
export let dbKey;
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' } ]} bind:selectedKey={tab} />
<div class="container">
{#if tab === 'stats'} <Stats {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,50 @@
<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}
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="btn 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>
import Modal from '$components/modal.svelte';
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 { createEventDispatcher } from 'svelte';
import { createEventDispatcher, onMount } from 'svelte';
export let show = false;
export let hostKey = '';
const dispatch = createEventDispatcher();
let form = {};
let error = '';
$: valid = validate(form);
$: host = $hosts[hostKey];
$: if (show || !show) {
init();
}
function init() {
form = { ...(host || {}) };
}
$: host = $hostTree[hostKey];
function validate(form) {
return form.name && form.uri && true;
@ -41,16 +32,20 @@
hostKey = newHostKey;
}
}
show = false;
dispatch('reload');
dispatch('updated', form);
dispatch('close');
}
catch (e) {
error = e;
}
}
onMount(() => {
form = { ...(host || {}) };
});
</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}>
<label class="field">
<span class="label">Label</span>

View File

@ -0,0 +1,65 @@
<script>
import BlankState from '$components/blankstate.svelte';
import TabBar from '$components/tabbar.svelte';
import { EventsOn } from '$wails/runtime/runtime';
import Status from './status.svelte';
import SystemInfo from './systeminfo.svelte';
export let host;
export let hostKey;
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: 'systemInfo', icon: 'server', title: 'System info' },
]}
bind:selectedKey={tab} />
<div class="container">
{#if tab === 'status'} <Status {host} />
{:else if tab === 'systemInfo'} <SystemInfo {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,50 @@
<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 || {}}
errorTitle={host.statusError ? 'Error fetching server status' : ''}
errorDescription={host.statusError}
busy={!host.status && !host.statusError && 'Fetching server status…'}
/>
</div>
<div class="buttons">
<button class="btn 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,50 @@
<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}
errorTitle={host.systemInfoError ? 'Error fetching system info' : ''}
errorDescription={host.systemInfoError}
busy={!host.systemInfo && !host.systemInfoError && 'Fetching system info…'}
/>
</div>
<div class="buttons">
<button class="btn 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>
import Grid from '$components/grid.svelte';
import { startProgress } from '$lib/progress';
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';
import hostTree from '$lib/stores/hosttree';
export let activeHostKey = '';
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();
}
export let path = [];
</script>
<Grid
striped={false}
columns={[ { key: 'name' }, { key: 'count', right: true } ]}
items={Object.keys($hosts).map(hostKey => ({
id: hostKey,
name: $hosts[hostKey].name,
items={Object.values($hostTree || {}).map(host => {
return {
id: host.key,
name: host.name,
icon: 'server',
children: Object.keys(connection?.databases || {}).sort().map(dbKey => ({
id: dbKey,
name: dbKey,
children: Object.values(host.databases || {})
.sort((a, b) => a.key.localeCompare(b))
.map(database => {
return {
id: database.key,
name: database.key,
icon: 'db',
count: Object.keys(connection.databases[dbKey].collections || {}).length || '',
children: Object.keys(connection.databases[dbKey].collections).sort().map(collKey => ({
id: collKey,
name: collKey,
count: Object.keys(database.collections || {}).length || '',
children: Object.values(database.collections)
.sort((a, b) => a.key.localeCompare(b))
.map(collection => {
return {
id: collection.key,
name: collection.key,
icon: 'list',
menu: [
{ label: 'Export collection (JSON, CSV)…', fn: () => dispatch('exportCollection', collKey) },
{ label: 'Dump collection (BSON via mongodump)…', fn: () => dispatch('dumpCollection', collKey) },
{ label: 'Export collection…', fn: collection.export },
{ label: 'Dump collection (BSON via mongodump)…', fn: collection.dump },
{ separator: true },
{ label: 'Rename collection…', fn: () => dispatch('renameCollection', collKey) },
{ label: 'Truncate collection…', fn: () => truncateCollection(dbKey, collKey) },
{ label: 'Drop collection…', fn: () => dropCollection(dbKey, collKey) },
{ label: 'Rename collection…', fn: collection.rename },
{ label: 'Truncate collection…', fn: collection.truncate },
{ label: 'Drop collection…', fn: collection.drop },
{ separator: true },
{ label: 'New collection…', fn: () => dispatch('newCollection') },
{ label: 'New collection…', fn: database.newCollection },
],
})) || [],
};
}) || [],
menu: [
{ label: 'Drop database…', fn: () => dropDatabase(dbKey) },
{ label: 'Dump database (BSON via mongodump)…', fn: database.dump },
{ label: 'Drop database…', fn: database.drop },
{ separator: true },
{ label: 'New database…', fn: () => dispatch('newDatabase') },
{ label: 'New collection…', fn: () => dispatch('newCollection') },
{ label: 'New database…', fn: host.newDatabase },
{ label: 'New collection…', fn: database.newCollection },
],
})),
};
}),
menu: [
{ label: 'New database…', fn: () => dispatch('newDatabase') },
{ label: 'New database…', fn: host.newDatabase },
{ separator: true },
{ label: `Edit host ${$hosts[hostKey].name}`, fn: () => dispatch('editHost', hostKey) },
{ label: `Remove host…`, fn: () => removeHost(hostKey) },
{ label: `Edit host ${host.name}`, fn: host.edit },
{ label: 'Remove host…', fn: host.remove },
],
}))}
bind:activePath={activeGridPath}
};
})}
on:select={e => {
const key = e.detail.itemKey;
switch (e.detail?.level) {
case 0: return openConnection(key);
case 1: return openDatabase(key);
case 2: return openCollection(key);
let level;
({ path, level } = e.detail);
switch (level) {
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,52 @@
<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 hosts from '$lib/stores/hosts';
import hostTree from '$lib/stores/hosttree';
import sharedState from '$lib/stores/sharedstate';
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 = '';
export let activeDbKey = '';
export let activeCollKey = '';
let path = [];
let hostTree;
let showHostDetail = false;
let hostDetailKey = '';
let exportInfo;
$: activeHostKey = path[0];
$: activeDbKey = path[1];
$: activeCollKey = path[2];
$: sharedState.currentHost.set(activeHostKey);
$: sharedState.currentDb.set(activeDbKey);
$: sharedState.currentColl.set(activeCollKey);
export function createHost() {
hostDetailKey = '';
showHostDetail = true;
}
function editHost(hostKey) {
hostDetailKey = hostKey;
showHostDetail = true;
}
export async function createDatabase() {
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.');
if (name) {
$connections[activeHostKey].databases[name] = { collections: {} };
}
}
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>
<div class="tree">
<div class="tree-buttons">
<button class="button-small" on:click={createHost}>
<button class="button-small" on:click={hostTree.newHost}>
<Icon name="+" /> New host
</button>
</div>
<HostTree
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)}
/>
<HostTree bind:path />
</div>
<CollectionDetail
collection={$connections[activeHostKey]?.databases[activeDbKey]?.collections?.[activeCollKey]}
{#if activeCollKey}
<CollectionView
collection={$hostTree[activeHostKey]?.databases[activeDbKey]?.collections?.[activeCollKey]}
hostKey={activeHostKey}
dbKey={activeDbKey}
collectionKey={activeCollKey}
collKey={activeCollKey}
/>
<HostDetail
bind:show={showHostDetail}
on:reload={hosts.update}
{:else if activeDbKey}
<DatabaseView
database={$hostTree[activeHostKey]?.databases[activeDbKey]}
hostKey={activeHostKey}
dbKey={activeDbKey}
/>
{:else if activeHostKey}
<HostView
host={$hostTree[activeHostKey]}
hostKey={activeHostKey}
/>
<DumpInfo bind:info={exportInfo} />
{/if}
<style>
.tree {

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>

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

@ -1,7 +1,6 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {app} from '../models';
import {primitive} from '../models';
import {map[string]app} from '../models';
import {menu} from '../models';
import {context} from '../models';
@ -21,9 +20,9 @@ export function DropIndex(arg1:string,arg2:string,arg3:string,arg4:string):Promi
export function Environment():Promise<app.EnvironmentInfo>;
export function FindItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise<app.QueryResult>;
export function FindItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise<app.FindItemsResult>;
export function GetIndexes(arg1:string,arg2:string,arg3:string):Promise<Array<primitive.M>>;
export function GetIndexes(arg1:string,arg2:string,arg3:string):Promise<app.GetIndexesResult>;
export function Hosts():Promise<map[string]app.Host>;
@ -31,11 +30,11 @@ export function InsertItems(arg1:string,arg2:string,arg3:string,arg4:string):Pro
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>;

View File

@ -1,65 +0,0 @@
export namespace app {
export class EnvironmentInfo {
arch: string;
buildType: string;
platform: string;
hasMongoExport: boolean;
hasMongoDump: boolean;
homeDirectory: string;
dataDirectory: string;
logDirectory: string;
downloadDirectory: string;
static createFrom(source: any = {}) {
return new EnvironmentInfo(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.arch = source["arch"];
this.buildType = source["buildType"];
this.platform = source["platform"];
this.hasMongoExport = source["hasMongoExport"];
this.hasMongoDump = source["hasMongoDump"];
this.homeDirectory = source["homeDirectory"];
this.dataDirectory = source["dataDirectory"];
this.logDirectory = source["logDirectory"];
this.downloadDirectory = source["downloadDirectory"];
}
}
export class QueryResult {
total: number;
results: string[];
static createFrom(source: any = {}) {
return new QueryResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.total = source["total"];
this.results = source["results"];
}
}
export class Settings {
defaultLimit: number;
defaultSort: string;
autosubmitQuery: boolean;
defaultExportDirectory: string;
static createFrom(source: any = {}) {
return new Settings(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.defaultLimit = source["defaultLimit"];
this.defaultSort = source["defaultSort"];
this.autosubmitQuery = source["autosubmitQuery"];
this.defaultExportDirectory = source["defaultExportDirectory"];
}
}
}

2
frontend/wailsjs/go/ui/UI.d.ts generated vendored
View File

@ -4,8 +4,6 @@ import {context} from '../models';
export function Beep():Promise<void>;
export function EnterText(arg1:string,arg2:string,arg3:string):Promise<string>;
export function OpenDirectory(arg1:string):Promise<string>;
export function Reveal(arg1:string):Promise<void>;

View File

@ -6,10 +6,6 @@ export function Beep() {
return window['go']['ui']['UI']['Beep']();
}
export function EnterText(arg1, arg2, arg3) {
return window['go']['ui']['UI']['EnterText'](arg1, arg2, arg3);
}
export function OpenDirectory(arg1) {
return window['go']['ui']['UI']['OpenDirectory'](arg1);
}

8
go.mod
View File

@ -7,7 +7,7 @@ require github.com/wailsapp/wails/v2 v2.3.1
require (
github.com/gen2brain/beeep v0.0.0-20220909211152-5a9ec94374f6
github.com/google/uuid v1.3.0
go.mongodb.org/mongo-driver v1.11.6
go.mongodb.org/mongo-driver v1.11.7
)
require (
@ -35,7 +35,7 @@ require (
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/ncruces/zenity v0.10.8
github.com/ncruces/zenity v0.10.9
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 // indirect
github.com/pkg/errors v0.9.1 // indirect
@ -53,8 +53,8 @@ require (
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sync v0.1.0
golang.org/x/sys v0.7.0 // indirect
golang.org/x/sync v0.3.0
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
)

15
go.sum
View File

@ -54,8 +54,8 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/ncruces/zenity v0.10.8 h1:vNwTJazgxj47VOBXqik14fN8ML5eLMhl/Nw646Ln/7o=
github.com/ncruces/zenity v0.10.8/go.mod h1:gPHbGF/lkwfJS3wdVG9sXYgK8c0vLAXkA2xHacOiFZY=
github.com/ncruces/zenity v0.10.9 h1:TYdNwEj9HiDDcpdsIUecBMsQw7L80Aiu/IJMM0Tao1E=
github.com/ncruces/zenity v0.10.9/go.mod h1:FzjqP1loicusCFJTdIt5Oqbmoj2zySHpM0RsgJeeCbk=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 h1:acNfDZXmm28D2Yg/c3ALnZStzNaZMSagpbr96vY6Zjc=
@ -98,8 +98,8 @@ github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgk
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.11.6 h1:XM7G6PjiGAO5betLF13BIa5TlLUUE3uJ/2Ox3Lz1K+o=
go.mongodb.org/mongo-driver v1.11.6/go.mod h1:G9TgswdsWjX4tmDA5zfs2+6AEPpYJwqblyjsfuh8oXY=
go.mongodb.org/mongo-driver v1.11.7 h1:LIwYxASDLGUg/8wOhgOOZhX8tQa/9tgZPgzZoVqJvcs=
go.mongodb.org/mongo-driver v1.11.7/go.mod h1:G9TgswdsWjX4tmDA5zfs2+6AEPpYJwqblyjsfuh8oXY=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@ -123,8 +123,9 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -139,8 +140,8 @@ golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=

View File

@ -10,6 +10,7 @@ import (
"path"
"path/filepath"
"runtime"
"strings"
"github.com/garraflavatra/rolens/internal/ui"
"github.com/ncruces/zenity"
@ -21,6 +22,7 @@ type EnvironmentInfo struct {
Arch string `json:"arch"`
BuildType string `json:"buildType"`
Platform string `json:"platform"`
Version string `json:"version"`
HasMongoExport bool `json:"hasMongoExport"`
HasMongoDump bool `json:"hasMongoDump"`
@ -38,8 +40,9 @@ type App struct {
ui *ui.UI
}
func NewApp() *App {
func NewApp(version string) *App {
a := &App{}
a.Env.Version = strings.TrimSpace(version)
_, err := exec.LookPath("mongodump")
a.Env.HasMongoDump = err == nil

View File

@ -36,8 +36,6 @@ func (a *App) Menu() *menu.Menu {
fileMenu := appMenu.AddSubmenu("File")
fileMenu.AddText("New host…", keys.CmdOrCtrl("y"), menuCallbackEmit(a, "CreateHost"))
fileMenu.AddText("New database", keys.CmdOrCtrl("y"), menuCallbackEmit(a, "CreateDatabase"))
fileMenu.AddText("New collection…", keys.CmdOrCtrl("i"), menuCallbackEmit(a, "CreateCollection"))
fileMenu.AddSeparator()
fileMenu.AddText("Stats", keys.Combo("h", keys.CmdOrCtrlKey, keys.OptionOrAltKey), menuCallbackEmit(a, "OpenCollectionTab", "stats"))
fileMenu.AddText("Find", keys.Combo("f", keys.CmdOrCtrlKey, keys.OptionOrAltKey), menuCallbackEmit(a, "OpenCollectionTab", "find"))

View File

@ -8,23 +8,27 @@ import (
"go.mongodb.org/mongo-driver/bson"
)
func (a *App) OpenCollection(hostKey, dbKey, collKey string) (result bson.M) {
client, ctx, close, err := a.connectToHost(hostKey)
if err != nil {
return nil
type OpenCollectionResult struct {
Stats bson.M `json:"stats"`
StatsError string `json:"statsError"`
}
func (a *App) OpenCollection(hostKey, dbKey, collKey string) (result OpenCollectionResult) {
client, ctx, close, err := a.connectToHost(hostKey)
if err != nil {
return
}
defer close()
command := bson.M{"collStats": collKey}
err = client.Database(dbKey).RunCommand(ctx, command).Decode(&result)
err = client.Database(dbKey).RunCommand(ctx, command).Decode(&result.Stats)
if err != nil {
runtime.LogWarning(a.ctx, "Could not retrieve collection stats for "+collKey)
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Could not get stats"), zenity.ErrorIcon)
return nil
result.StatsError = err.Error()
}
defer close()
return result
return
}
func (a *App) RenameCollection(hostKey, dbKey, collKey, newCollKey string) bool {
@ -32,6 +36,7 @@ func (a *App) RenameCollection(hostKey, dbKey, collKey, newCollKey string) bool
if err != nil {
return false
}
defer close()
var result bson.M
command := bson.D{
@ -46,7 +51,6 @@ func (a *App) RenameCollection(hostKey, dbKey, collKey, newCollKey string) bool
return false
}
defer close()
return true
}
@ -60,6 +64,7 @@ func (a *App) TruncateCollection(hostKey, dbKey, collKey string) bool {
if err != nil {
return false
}
defer close()
_, err = client.Database(dbKey).Collection(collKey).DeleteMany(ctx, bson.D{})
if err != nil {
@ -69,7 +74,6 @@ func (a *App) TruncateCollection(hostKey, dbKey, collKey string) bool {
return false
}
defer close()
return true
}
@ -83,6 +87,7 @@ func (a *App) DropCollection(hostKey, dbKey, collKey string) bool {
if err != nil {
return false
}
defer close()
err = client.Database(dbKey).Collection(collKey).Drop(ctx)
if err != nil {
@ -92,6 +97,5 @@ func (a *App) DropCollection(hostKey, dbKey, collKey string) bool {
return false
}
defer close()
return true
}

View File

@ -17,13 +17,14 @@ type Query struct {
Sort string `json:"sort"`
}
type QueryResult struct {
type FindItemsResult struct {
Total int64 `json:"total"`
Results []string `json:"results"`
ErrorTitle string `json:"errorTitle"`
ErrorDescription string `json:"errorDescription"`
}
func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) QueryResult {
var out QueryResult
func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) (result FindItemsResult) {
var form Query
err := json.Unmarshal([]byte(formJson), &form)
@ -31,15 +32,15 @@ func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) QueryResult {
runtime.LogError(a.ctx, "Could not parse find form:")
runtime.LogError(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Could not parse form"), zenity.ErrorIcon)
return out
return
}
client, ctx, close, err := a.connectToHost(hostKey)
if err != nil {
return out
return
}
defer close()
var query bson.M
var projection bson.M
var sort bson.M
@ -47,22 +48,25 @@ func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) QueryResult {
err = bson.UnmarshalExtJSON([]byte(form.Query), true, &query)
if err != nil {
runtime.LogInfof(a.ctx, "Invalid find query: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Invalid query"), zenity.ErrorIcon)
return out
result.ErrorTitle = "Invalid query"
result.ErrorDescription = err.Error()
return
}
err = json.Unmarshal([]byte(form.Fields), &projection)
if err != nil {
runtime.LogInfof(a.ctx, "Invalid find projection: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Invalid projection"), zenity.ErrorIcon)
return out
result.ErrorTitle = "Invalid projection"
result.ErrorDescription = err.Error()
return
}
err = json.Unmarshal([]byte(form.Sort), &sort)
if err != nil {
runtime.LogInfof(a.ctx, "Invalid find sort: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Invalid sort"), zenity.ErrorIcon)
return out
result.ErrorTitle = "Invalid sort"
result.ErrorDescription = err.Error()
return
}
opt := mongoOptions.FindOptions{
@ -75,15 +79,17 @@ func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) QueryResult {
total, err := client.Database(dbKey).Collection(collKey).CountDocuments(ctx, query, nil)
if err != nil {
runtime.LogWarningf(a.ctx, "Encountered an error while counting documents: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Error while counting docs"), zenity.ErrorIcon)
return out
result.ErrorTitle = "Error while counting documents"
result.ErrorDescription = err.Error()
return
}
cur, err := client.Database(dbKey).Collection(collKey).Find(ctx, query, &opt)
if err != nil {
runtime.LogWarningf(a.ctx, "Encountered an error while performing query: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Error while querying"), zenity.ErrorIcon)
return out
result.ErrorTitle = "Error while querying"
result.ErrorDescription = err.Error()
return
}
defer cur.Close(ctx)
@ -92,25 +98,27 @@ func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) QueryResult {
if err != nil {
runtime.LogWarningf(a.ctx, "Encountered an error while performing query: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Error while querying"), zenity.ErrorIcon)
return out
result.ErrorTitle = "Error while querying"
result.ErrorDescription = err.Error()
return
}
out.Total = total
out.Results = make([]string, 0)
result.Total = total
result.Results = make([]string, 0)
for _, r := range results {
marshalled, err := bson.MarshalExtJSON(r, true, true)
if err != nil {
runtime.LogError(a.ctx, "Failed to marshal find BSON:")
runtime.LogError(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Failed to marshal JSON"), zenity.ErrorIcon)
return out
result.ErrorTitle = "Failed to marshal JSON"
result.ErrorDescription = err.Error()
return
}
out.Results = append(out.Results, string(marshalled))
result.Results = append(result.Results, string(marshalled))
}
return out
return
}
func (a *App) UpdateFoundDocument(hostKey, dbKey, collKey, idJson, newDocJson string) bool {
@ -134,9 +142,9 @@ func (a *App) UpdateFoundDocument(hostKey, dbKey, collKey, idJson, newDocJson st
}
defer close()
if _, err := client.Database(dbKey).Collection(collKey).UpdateOne(ctx, id, bson.M{"$set": newDoc}); err != nil {
runtime.LogInfof(a.ctx, "Error while performing find/update: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Unable to perform update"), zenity.ErrorIcon)
if _, err := client.Database(dbKey).Collection(collKey).ReplaceOne(ctx, id, newDoc); err != nil {
runtime.LogInfof(a.ctx, "Error while replacing document: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Unable to replace document"), zenity.ErrorIcon)
return false
}

View File

@ -246,7 +246,7 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo
continue
}
csvItem = append(csvItem, string(v.(string)))
csvItem = append(csvItem, fmt.Sprintf("%v", v))
}
default:

View File

@ -11,10 +11,15 @@ import (
"go.mongodb.org/mongo-driver/mongo/options"
)
func (a *App) GetIndexes(hostKey, dbKey, collKey string) []bson.M {
type GetIndexesResult struct {
Indexes []bson.M `json:"indexes"`
Error string `json:"error"`
}
func (a *App) GetIndexes(hostKey, dbKey, collKey string) (result GetIndexesResult) {
client, ctx, close, err := a.connectToHost(hostKey)
if err != nil {
return nil
return
}
defer close()
@ -22,20 +27,18 @@ func (a *App) GetIndexes(hostKey, dbKey, collKey string) []bson.M {
if err != nil {
runtime.LogWarning(a.ctx, "Encountered an error while creating index cursor:")
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while creating cursor"), zenity.ErrorIcon)
return nil
result.Error = err.Error()
return
}
var results []bson.M
err = cur.All(ctx, &results)
err = cur.All(ctx, &result.Indexes)
if err != nil {
runtime.LogWarning(a.ctx, "Encountered an error while executing index cursor:")
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while running cursor"), zenity.ErrorIcon)
return nil
result.Error = err.Error()
}
return results
return
}
func (a *App) CreateIndex(hostKey, dbKey, collKey, jsonData string) string {

View File

@ -28,8 +28,8 @@ func (a *App) InsertItems(hostKey, dbKey, collKey, jsonData string) interface{}
if err != nil {
return nil
}
defer close()
res, err := client.Database(dbKey).Collection(collKey).InsertMany(ctx, data)
if err != nil {
runtime.LogWarning(a.ctx, "Encountered an error while performing insert:")

View File

@ -12,16 +12,25 @@ import (
mongoOptions "go.mongodb.org/mongo-driver/mongo/options"
)
type OpenConnectionResult struct {
Databases []string `json:"databases"`
Status bson.M `json:"status"`
StatusError string `json:"statusError"`
SystemInfo bson.M `json:"systemInfo"`
SystemInfoError string `json:"systemInfoError"`
}
func (a *App) connectToHost(hostKey string) (*mongo.Client, context.Context, func(), error) {
hosts, err := a.Hosts()
if err != nil {
runtime.LogInfof(a.ctx, "Error while getting hosts: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Error while getting hosts"), zenity.ErrorIcon)
return nil, nil, nil, errors.New("could not retrieve hosts")
}
h := hosts[hostKey]
if len(h.URI) == 0 {
runtime.LogInfo(a.ctx, "Invalid URI (len == 0) for host "+hostKey)
runtime.LogInfof(a.ctx, "Invalid URI (len == 0) for host %s", hostKey)
zenity.Warning("You haven't specified a valid uri for the selected host.", zenity.Title("Invalid query"), zenity.WarningIcon)
return nil, nil, nil, errors.New("invalid uri")
}
@ -29,8 +38,7 @@ func (a *App) connectToHost(hostKey string) (*mongo.Client, context.Context, fun
client, err := mongo.NewClient(mongoOptions.Client().ApplyURI(h.URI))
if err != nil {
runtime.LogWarning(a.ctx, "Could not connect to host "+hostKey)
runtime.LogWarning(a.ctx, err.Error())
runtime.LogWarningf(a.ctx, "Could not connect to host %s: %s", hostKey, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while connecting to "+h.Name), zenity.ErrorIcon)
return nil, nil, nil, errors.New("could not establish a connection with " + h.Name)
}
@ -43,18 +51,35 @@ func (a *App) connectToHost(hostKey string) (*mongo.Client, context.Context, fun
}, nil
}
func (a *App) OpenConnection(hostKey string) (databases []string) {
func (a *App) OpenConnection(hostKey string) (result OpenConnectionResult) {
client, ctx, close, err := a.connectToHost(hostKey)
if err != nil {
return nil
return
}
databases, err = client.ListDatabaseNames(ctx, bson.M{})
defer close()
result.Databases, err = client.ListDatabaseNames(ctx, bson.M{})
if err != nil {
runtime.LogWarning(a.ctx, "Could not retrieve database names for host "+hostKey)
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while getting databases"), zenity.ErrorIcon)
return nil
}
defer close()
return databases
command := bson.M{"serverStatus": 1}
err = client.Database("admin").RunCommand(ctx, command).Decode(&result.Status)
if err != nil {
runtime.LogWarning(a.ctx, "Could not retrieve server status")
runtime.LogWarning(a.ctx, err.Error())
result.StatusError = err.Error()
}
command = bson.M{"hostInfo": 1}
err = client.Database("admin").RunCommand(ctx, command).Decode(&result.SystemInfo)
if err != nil {
runtime.LogWarning(a.ctx, "Could not retrieve system info")
runtime.LogWarning(a.ctx, err.Error())
result.SystemInfoError = err.Error()
}
return
}

View File

@ -6,22 +6,35 @@ import (
"go.mongodb.org/mongo-driver/bson"
)
func (a *App) OpenDatabase(hostKey, dbKey string) (collections []string) {
client, ctx, close, err := a.connectToHost(hostKey)
if err != nil {
return nil
type OpenDatabaseResult struct {
Collections []string `json:"collections"`
Stats bson.M `json:"stats"`
StatsError string `json:"statsError"`
}
collections, err = client.Database(dbKey).ListCollectionNames(ctx, bson.D{})
func (a *App) OpenDatabase(hostKey, dbKey string) (result OpenDatabaseResult) {
client, ctx, close, err := a.connectToHost(hostKey)
if err != nil {
return
}
defer close()
command := bson.M{"dbStats": 1}
err = client.Database(dbKey).RunCommand(ctx, command).Decode(&result.Stats)
if err != nil {
runtime.LogWarning(a.ctx, "Could not retrieve database stats for "+dbKey)
runtime.LogWarning(a.ctx, err.Error())
result.StatsError = err.Error()
}
result.Collections, err = client.Database(dbKey).ListCollectionNames(ctx, bson.D{})
if err != nil {
runtime.LogWarning(a.ctx, "Could not retrieve collection list for db "+dbKey)
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while getting collections"), zenity.ErrorIcon)
return nil
}
defer close()
return collections
return
}
func (a *App) DropDatabase(hostKey, dbKey string) bool {
@ -34,6 +47,7 @@ func (a *App) DropDatabase(hostKey, dbKey string) bool {
if err != nil {
return false
}
defer close()
err = client.Database(dbKey).Drop(ctx)
if err != nil {
@ -43,6 +57,5 @@ func (a *App) DropDatabase(hostKey, dbKey string) bool {
return false
}
defer close()
return true
}

View File

@ -15,16 +15,3 @@ func (u *UI) OpenDirectory(title string) string {
return dir
}
func (u *UI) EnterText(title, info, defaultEntry string) string {
input, err := zenity.Entry(info, zenity.Title(title), zenity.EntryText(defaultEntry), zenity.Modal())
if err == zenity.ErrCanceled {
return ""
} else if err != nil {
zenity.Error(err.Error(), zenity.Title("Encountered an error!"), zenity.ErrorIcon)
return ""
} else {
return input
}
}

12
main.go
View File

@ -7,7 +7,6 @@ import (
"github.com/garraflavatra/rolens/internal"
"github.com/garraflavatra/rolens/internal/app"
uictrl "github.com/garraflavatra/rolens/internal/ui"
"github.com/ncruces/zenity"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/options"
@ -22,10 +21,13 @@ var (
//go:embed build/appicon.png
appIcon []byte
//go:embed build/version.txt
version string
)
func main() {
app := app.NewApp()
app := app.NewApp(version)
ui := uictrl.New()
err := wails.Run(&options.App{
@ -45,7 +47,11 @@ func main() {
defer func() {
if p := recover(); p != nil {
runtime.LogFatalf(ctx, "Application panicked: %v", p)
zenity.Error("A fatal error occured.")
runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
Type: runtime.ErrorDialog,
Title: "A fatal error occured!",
Message: "Please try to restart the application, or consult the logs for more details.",
})
}
}()

View File

@ -13,8 +13,11 @@
"info": {
"productName": "Rolens",
"companyName": "Romein van Buren",
"productVersion": "0.2.0",
"productVersion": "0.2.1",
"comments": "The intuitive MongoDB administration tool",
"copyright": "© Romein van Buren 2023 (GNU GPL 3.0)."
},
"preBuildHooks": {
"*/*": "node ../version_to_file.js"
}
}

View File

@ -2,7 +2,7 @@ const { default: fetch } = require("node-fetch")
module.exports = async function() {
try {
const res = await fetch('https://api.github.com/repos/garraflavatra/alphabets');
const res = await fetch('https://api.github.com/repos/garraflavatra/rolens');
const json = await res.json();
return { stars: json.stargazers_count || 0 };
}

View File

@ -3,9 +3,6 @@
<svg class="icon" viewBox="0 0 16 16"><path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/></svg>
</span>
{% if github.stars %}
<span class="count">{{ github.stars }}</span>
{% endif %}
<span class="count">{{ stars }}</span>
Star on GitHub!
</a>

View File

@ -56,8 +56,8 @@ navigationOptions:
on GitHub.
</p>
<p>
{% render "githubstars" %}
{% render "mastodon" %}
{% render "githubstars", stars: github.stars %}
{% # render "mastodon" %}
</p>
<p>
© <a href="mailto:romein@vburen.nl">Romein van Buren</a> 2023.