1
0
mirror of https://github.com/garraflavatra/rolens.git synced 2025-06-29 14:05:12 +00:00

9 Commits

121 changed files with 1754 additions and 3586 deletions

6
.gitattributes vendored
View File

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

View File

@ -1,8 +1,10 @@
name: CI name: CI
on: on:
- push push:
- pull_request branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs: jobs:
build: build:
@ -14,21 +16,22 @@ jobs:
platform: platform:
- windows-2019 - windows-2019
- windows-2022 - windows-2022
- macos-11
- macos-12
- macos-13 - macos-13
- macos-14 - ubuntu-20.04
- ubuntu-22.04 - ubuntu-22.04
- ubuntu-24.04 go-version: [1.18]
go-version: [ 1.23.5 ] node-version: [16]
node-version: [ 22 ]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v3
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
cache-dependency-path: go.sum cache-dependency-path: go.sum
@ -42,10 +45,10 @@ jobs:
- name: Install build dependencies for Linux - name: Install build dependencies for Linux
if: contains(matrix.platform, 'ubuntu') if: contains(matrix.platform, 'ubuntu')
run: sudo apt-get update && sudo apt-get install libgtk-3-0 libwebkit2gtk-4.0-dev gcc-aarch64-linux-gnu run: sudo apt-get install gtk+-3.0 webkit2gtk-4.0
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: npm cache: npm
@ -64,7 +67,7 @@ jobs:
run: ./build/linux/ci_generate.sh "${{ matrix.platform }}" run: ./build/linux/ci_generate.sh "${{ matrix.platform }}"
- name: Upload generated binaries - name: Upload generated binaries
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v2
with: with:
name: rolens-${{ matrix.platform }} name: rolens-${{ matrix.platform }}
path: releases/* path: releases/*
@ -74,7 +77,7 @@ jobs:
bundle: bundle:
name: Bundle artifacts name: Bundle artifacts
runs-on: ubuntu-24.04 runs-on: ubuntu-22.04
needs: build needs: build
if: ${{ always() }} if: ${{ always() }}
@ -91,7 +94,7 @@ jobs:
run: build/ci_bundle.sh run: build/ci_bundle.sh
- name: Upload the bundle as an artifact - name: Upload the bundle as an artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: rolens-bundle name: rolens-bundle
path: bundle path: bundle

View File

@ -22,26 +22,24 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4 uses: actions/setup-node@v3
with: with:
node-version: "16" node-version: "16"
- name: Setup Pages - name: Setup Pages
uses: actions/configure-pages@v5 uses: actions/configure-pages@v3
- name: Install dependencies - name: Install dependencies
run: npm ci run: cd website; npm ci
working-directory: website
- name: Build site - name: Build site
run: npm run build run: cd website; npm run build
working-directory: website
- name: Upload built website - name: Upload built website
uses: actions/upload-pages-artifact@v3 uses: actions/upload-pages-artifact@v1
with: with:
path: ./website-dist path: ./website-dist
@ -56,4 +54,4 @@ jobs:
steps: steps:
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
id: deployment id: deployment
uses: actions/deploy-pages@v4 uses: actions/deploy-pages@v2

View File

@ -1,17 +1,19 @@
name: Linter name: Linter
on: on:
- push push:
- pull_request branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs: jobs:
eslint: eslint:
name: Run ESLint name: Run ESlint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci

View File

@ -6,7 +6,7 @@
"editor.tabSize": 4, "editor.tabSize": 4,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports": "explicit" "source.organizeImports": true
}, },
"editor.suggest.snippetsPreventQuickSuggestions": false "editor.suggest.snippetsPreventQuickSuggestions": false
}, },

View File

@ -1,25 +1,6 @@
## [v0.3.0] ## [Unreleased]
New features:
* Added log view (#53, #54). * Added log view (#53, #54).
* Added a shell script editor (#37), plus export/import feature.
* Added collection duplication feature (#63).
* Find view: paste ID and press Enter (#55).
Patches:
* Preserve state after switching to another tab (#56).
* Find view: ask for confirmation before negligently deleting documents when the user has clicked the '-' button (#58).
* Set a deadline for counting documents, and added a button to count documents if the deadline has been exceeded.
* Changed os.ModePerm (777) file permissions to 644 and 755.
* UI improvements.
* Bumped minimum reqiured Go version from 1.18 to 1.20.
Bugfixes:
* Build script recreates the build output directory after it has been removed (#62).
* After dropping a collection or database, the hosttree was not reloaded correctly. Now, dropped databases and collections are correctly removed from the host tree.
## [v0.2.2] ## [v0.2.2]
@ -55,4 +36,3 @@ Initial release.
[v0.2.0]: https://github.com/garraflavatra/rolens/releases/tag/v0.2.0 [v0.2.0]: https://github.com/garraflavatra/rolens/releases/tag/v0.2.0
[v0.2.1]: https://github.com/garraflavatra/rolens/releases/tag/v0.2.1 [v0.2.1]: https://github.com/garraflavatra/rolens/releases/tag/v0.2.1
[v0.2.2]: https://github.com/garraflavatra/rolens/releases/tag/v0.2.2 [v0.2.2]: https://github.com/garraflavatra/rolens/releases/tag/v0.2.2
[v0.3.0]: https://github.com/garraflavatra/rolens/releases/tag/v0.3.0

View File

@ -8,7 +8,7 @@ Robust, blazing-fast, comprehensive, yet simple [MongoDB](https://www.mongodb.co
## Why another MongoDB client? ## Why another MongoDB client?
This project arose from all flaws of similar tools many of which are slow, complicated, heavy, and fairly unwieldy. They mostly require quite a high level of knowledge on how to operate the program. This project arose from all flaws of similar tools many of which are slow, complicated, heavy, and fairly unwieldy. They mostly require a reasonably high level of knowledge on how to operate the program.
**Rolens aims to be the intuitive, lightweight counterpart of these overengineered tools.** **Rolens aims to be the intuitive, lightweight counterpart of these overengineered tools.**
@ -17,7 +17,7 @@ This project arose from all flaws of similar tools many of which are slow, compl
- [x] **Low overhead**: Typical query results against a local database are returned whithin milliseconds. - [x] **Low overhead**: Typical query results against a local database are returned whithin milliseconds.
- [x] **Intuitive interface**: You know MongoDB? You know Rolens. - [x] **Intuitive interface**: You know MongoDB? You know Rolens.
![Impression of Rolens's interface](./docs/images/home-impression.webp) ![Impression of Rolens's interface](./docs/images/home-impression.png)
This project is heavily inspired by the excellent [MongoHub](https://github.com/bububa/MongoHub-Mac) application, which sadly has not been updated since 2011. This project is heavily inspired by the excellent [MongoHub](https://github.com/bububa/MongoHub-Mac) application, which sadly has not been updated since 2011.
@ -62,7 +62,6 @@ At this point, Rolens is comparable to MongoHub regarding features. It cannot ha
* Database management * Database management
- See stats - See stats
- Create dumps with `mongodump` - Create dumps with `mongodump`
- Write and execute shell scripts
* Collections * Collections
- See stats - See stats
- Find, insert, update, & remove - Find, insert, update, & remove
@ -76,10 +75,11 @@ At this point, Rolens is comparable to MongoHub regarding features. It cannot ha
## Wishlist ## Wishlist
* User management * User management
* Shell _([under development](https://github.com/garraflavatra/rolens/pull/44))_
## Author and license ## Author and license
© [Romein van Buren](mailto:romein@vburen.nl) 2022-2025. The source code and compiled binaries are released under the GNU GPLv3 license — see [`LICENSE`](./LICENSE) for the full license text. © [Romein van Buren](mailto:romein@vburen.nl) 2023. The source code and compiled binaries are released under the GNU GPLv3 license — see [`LICENSE`](./LICENSE) for the full license text.
## Credits ## Credits

View File

@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
const { execSync, spawn } = require('child_process'); const { execSync, spawn } = require('child_process');
const { readFileSync, statSync, rmdirSync, mkdirSync } = require('fs'); const { readFileSync, statSync, rmdirSync } = require('fs');
// Check that the script is run from the root. // Check that the script is run from the root.
@ -50,18 +50,18 @@ function isNullish(val) {
return val === undefined || val === null; return val === undefined || val === null;
} }
// Check that Go ^1.20 is installed. // Check that Go ^1.18 is installed.
try { try {
const goMinorVersion = /go1\.([0-9][0-9])/.exec( const goMinorVersion = /go1\.([0-9][0-9])/.exec(
execSync('go version').toString() execSync('go version').toString()
)?.pop(); )?.pop();
if (isNullish(goMinorVersion) || (parseInt(goMinorVersion) < 20)) { if (isNullish(goMinorVersion) || (parseInt(goMinorVersion) < 18)) {
throw new Error(); throw new Error();
} }
} catch { } catch {
missingDependencies.push({ name: 'Go ^1.20', url: 'https://go.dev/doc/install' }); missingDependencies.push({ name: 'Go ^1.18 ^16', url: 'https://go.dev/doc/install' });
} }
// Check that Node.js ^16 is installed. // Check that Node.js ^16 is installed.
@ -145,12 +145,6 @@ if (missingDependencies.length > 0) {
console.log('Cleaning output directory...'); console.log('Cleaning output directory...');
try { rmdirSync('./build/bin'); } catch {} try { rmdirSync('./build/bin'); } catch {}
try {
mkdirSync('./build/bin');
}
catch (err) {
console.log('Failed to create build output directory!');
}
// Build Rolens. // Build Rolens.
@ -160,25 +154,8 @@ console.log();
const proc = spawn('wails', [ 'build', '-clean', isWindows ? '-nsis' : '' ]); const proc = spawn('wails', [ 'build', '-clean', isWindows ? '-nsis' : '' ]);
if (!quiet) { if (!quiet) {
const suppressMessages = [ proc.stdout.on('data', data => process.stdout.write(data));
'Wails CLI',
'If Wails is useful',
'https://github.com/sponsors/leaanthony',
];
proc.stdout.on('data', data => {
for (let i = 0; i < suppressMessages.length; i++) {
if (data.toString().indexOf(suppressMessages[i]) !== -1) {
return;
}
}
process.stdout.write(data);
});
proc.stderr.on('data', data => process.stderr.write(data)); proc.stderr.on('data', data => process.stderr.write(data));
} }
proc.on('exit', code => { proc.on('exit', code => process.exit(code));
console.log();
process.exit(code);
});

View File

@ -12,12 +12,12 @@
# - ubuntu-22.04 # - ubuntu-22.04
# #
# Bundles to choose from: # Bundles to choose from:
# - rolens-macos-11-amd64.zip # - rolens-macos-11-amd64.tar.gz
# - rolens-macos-11-arm64.zip # - rolens-macos-11-arm64.tar.gz
# - rolens-macos-12-amd64.zip # - rolens-macos-12-amd64.tar.gz
# - rolens-macos-12-arm64.zip # - rolens-macos-12-arm64.tar.gz
# - rolens-macos-13-amd64.zip # - rolens-macos-13-amd64.tar.gz
# - rolens-macos-13-arm64.zip # - rolens-macos-13-arm64.tar.gz
# - rolens-ubuntu-20.04-amd64.tar.gz # - rolens-ubuntu-20.04-amd64.tar.gz
# - rolens-ubuntu-22.04-amd64.tar.gz # - rolens-ubuntu-22.04-amd64.tar.gz
# - rolens-windows-2019-amd64.zip # - rolens-windows-2019-amd64.zip
@ -32,8 +32,8 @@ version=$(<./build/version.txt)
mkdir bundle mkdir bundle
# macOS apps # macOS apps
mv artifacts/*/rolens-macos-11-amd64.zip "bundle/rolens-$version-macos-11+-amd64.zip" mv artifacts/*/rolens-macos-11-amd64.tar.gz "bundle/rolens-$version-macos-11+-amd64.tar.gz"
mv artifacts/*/rolens-macos-11-arm64.zip "bundle/rolens-$version-macos-11+-arm64.zip" mv artifacts/*/rolens-macos-11-arm64.tar.gz "bundle/rolens-$version-macos-11+-arm64.tar.gz"
# Windows installers # Windows installers
mv artifacts/*/rolens-windows-2019-amd64-installer.zip "bundle/rolens-$version-windows-10+-amd64-installer.zip" mv artifacts/*/rolens-windows-2019-amd64-installer.zip "bundle/rolens-$version-windows-10+-amd64-installer.zip"

View File

@ -13,27 +13,48 @@ cat > build/darwin/dmg_settings.json << EOF
"background": "$(pwd)/build/darwin/dmg_background.png", "background": "$(pwd)/build/darwin/dmg_background.png",
"icon-size": 100, "icon-size": 100,
"window": { "window": {
"size": { "width": 750, "height": 400 } "size": { "width": 155, "height": 250 },
"position": { "x": 360, "y": 360 }
}, },
"contents": [ "contents": [
{ "x": 600, "y": 175, "type": "link", "path": "/Applications" }, { "x": 750, "y": 500, "type": "link", "path": "/Applications" },
{ "x": 150, "y": 175, "type": "file", "path": "$(pwd)/build/bin/Rolens.app" } { "x": 595, "y": 250, "type": "file", "path": "$(pwd)/build/bin/Rolens.app" }
] ]
} }
EOF EOF
# AMD/Intel # AMD/Intel
wails build -platform darwin/amd64 wails build -platform darwin/amd64
appdmg build/darwin/dmg_settings.json build/bin/Rolens.dmg # create-dmg \
zip -j releases/rolens-$1-amd64.zip build/bin/Rolens.dmg # --volname Rolens \
# --window-size 155 250 \
# Cleanup # --volicon build/appicon.png \
rm -rf build/bin/Rolens.dmg # --eula LICENSE \
# --app-drop-link 750 500 \
# ARM/AppleM1 # --icon-size 100 \
wails build -platform darwin/arm64 # --background build/darwin/dmg_background.png \
appdmg build/darwin/dmg_settings.json build/bin/Rolens.dmg # --add-file Rolens.app build/bin/Rolens.app 595 250 \
zip -j releases/rolens-$1-arm64.zip build/bin/Rolens.dmg # 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 # Cleanup
rm -rf build/bin/Rolens.app rm -rf build/bin/Rolens.app

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -28,5 +28,5 @@ Feel free to contact me if you have questions! Send an e-mail to romein@vburen.n
Author and license Author and license
------------------ ------------------
© Romein van Buren 2022-2025. The source code and compiled binaries are released © Romein van Buren 2023. The source code and compiled binaries are released
under the GNU GPLv3 license — see LICENSE for the full license text. under the GNU GPLv3 license — see LICENSE for the full license text.

View File

@ -3,7 +3,7 @@ title: Colophon
order: 900 order: 900
--- ---
Rolens is © [Romein van Buren](mailto:romein@vburen.nl) 2022-2025. The source code and compiled binaries are released under the GNU GPLv3 license — see [`LICENSE`](https://github.com/garraflavatra/rolens/blob/main/LICENSE) for the full license text. Rolens is © [Romein van Buren](mailto:romein@vburen.nl) 2023. The source code and compiled binaries are released under the GNU GPLv3 license — see [`LICENSE`](https://github.com/garraflavatra/rolens/blob/main/LICENSE) for the full license text.
## Credits ## Credits

View File

@ -8,9 +8,9 @@ If you just want to install Rolens, please refer to the [installation document](
## Prerequisites ## Prerequisites
Rolens is written in Go, so you should download the Go compiler from [the download page](https://go.dev/dl/). The minimum version required is 1.20. You can confirm whether it's installed correctly by running `go version` and checking that it outputs something similar to `go1.20.4` or higher. Rolens is written in Go, so you should download the Go compiler from [the download page](https://go.dev/dl/). The minimum version required is 1.18. You can confirm whether it's installed correctly by running `go version` and checking that it outputs something similar to `go1.18.2`.
Furthermore, you need to have [Wails ^3.1](https://wails.io/docs/gettingstarted/installation) installed: `go install github.com/wailsapp/wails/v2/cmd/wails@latest`. Wails may have platform-specific dependencies; you can consult [`wails doctor`](https://wails.io/docs/reference/cli#doctor) to find out what dependencies Wails needs and how to install them. Furthermore, you need to have [Wails ^3.1](https://wails.io/docs/gettingstarted/installation) installed: `go install github.com/wailsapp/wails/v2/cmd/wails@latest`. Wails may have platform-specific dependencies; you can consult `wails doctor` to find out what dependencies Wails needs and how to install them.
In order to compile the frontend, [Node.js](https://nodejs.org/en/download) ^16.0 and the [npm](https://npmjs.com) package manager ^8.0 (included in Node.js) are required. To confirm the installed versions of those tools, execute `node -v` and `npm -v`. In order to compile the frontend, [Node.js](https://nodejs.org/en/download) ^16.0 and the [npm](https://npmjs.com) package manager ^8.0 (included in Node.js) are required. To confirm the installed versions of those tools, execute `node -v` and `npm -v`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

View File

@ -15,7 +15,7 @@ This project arose from all flaws of similar tools many of which are slow, compl
- **Low overhead**: Typical query results against a local database are returned whithin milliseconds. - **Low overhead**: Typical query results against a local database are returned whithin milliseconds.
- **Intuitive interface**: You know MongoDB? You know Rolens. - **Intuitive interface**: You know MongoDB? You know Rolens.
![Impression of Rolens's interface](./images/home-impression.webp) ![Impression of Rolens's interface](./images/home-impression.png)
This project is heavily inspired by the excellent [MongoHub](https://github.com/bububa/MongoHub-Mac) application, which sadly has not been updated since 2011. This project is heavily inspired by the excellent [MongoHub](https://github.com/bububa/MongoHub-Mac) application, which sadly has not been updated since 2011.

View File

@ -1,17 +0,0 @@
---
title: Shell
summary: "Write and execute MongoDB shell scripts within Rolens"
parent: User guide
order: 60
stub: true
---
Rolens has a shell feature: it provides an editor for writing shell scripts and executing them against a local or external host, database, or even a single collection. You can find it under the _Shell_ tab.
_Note: this feature is currently in development and will be shipped with version 0.3.0. You can [follow the development on GitHub](https://github.com/garraflavatra/rolens)._
![The shell tab](/images/shell.png)
## Requirements
To use the script editor, you need to install the official [`mongosh` tool](https://www.mongodb.com/docs/mongodb-shell/) from MongoDB. You can [download it](https://www.mongodb.com/try/download/shell) from the MongoDB web site, or run `brew install mongosh` if you use a Mac.

View File

@ -6,11 +6,6 @@ order: 900
<p>You can use the following shortcuts to manage hosts and connections.</p> <p>You can use the following shortcuts to manage hosts and connections.</p>
<p>
<a href="javascript:window.print()">Print this page</a> if you would like to
mount it on your wall for quick reference!
</p>
{% for item in shortcuts %} {% for item in shortcuts %}
<h2>{{ item[0] }}</h2> <h2>{{ item[0] }}</h2>
{% render "shortcuts", shortcuts: item[1] %} {% render "shortcuts", shortcuts: item[1] %}

View File

@ -7,6 +7,14 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<div id="app-loading">
<div class="ring">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
<div id="dialogoutlets"></div> <div id="dialogoutlets"></div>
<script src="./src/main.js" type="module"></script> <script src="./src/main.js" type="module"></script>
</body> </body>

File diff suppressed because it is too large Load Diff

View File

@ -17,20 +17,23 @@
"date-fns": "^2.30.0" "date-fns": "^2.30.0"
}, },
"devDependencies": { "devDependencies": {
"@garraflavatra/yeslint": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^1.0.1",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"eslint": "^8.43.0", "eslint": "^8.43.0",
"eslint-config-svelte3": "github:johbog/eslint-config-svelte3", "eslint-config-svelte3": "github:johbog/eslint-config-svelte3",
"svelte": "^4.0.0", "svelte": "^3.59.1",
"vite": "^5.0.0" "vite": "^3.2.7"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "./node_modules/@garraflavatra/yeslint/configs/svelte.js", "extends": "svelte3",
"ignorePatterns": [ "ignorePatterns": ["dist", "wailsjs"],
"dist",
"wailsjs"
],
"rules": { "rules": {
"svelte/html-quotes": [
"warn",
{
"prefer": "double",
"dynamic": { "quoted": false }
}
],
"svelte/no-useless-mustaches": "off", "svelte/no-useless-mustaches": "off",
"svelte/no-extra-reactive-curlies": "off" "svelte/no-extra-reactive-curlies": "off"
} }

View File

@ -1 +1 @@
015746ba33749cd2864cd336088387ef 7fc6d7b66151030191ded7136d96970e

View File

@ -1,15 +1,17 @@
<script> <script>
import { tick } from 'svelte';
import { fly } from 'svelte/transition';
import BlankState from '$components/blankstate.svelte'; import BlankState from '$components/blankstate.svelte';
import Connection from '$organisms/connection/index.svelte';
import ContextMenu from '$components/contextmenu.svelte'; import ContextMenu from '$components/contextmenu.svelte';
import dialogs from '$lib/dialogs';
import contextMenu from '$lib/stores/contextmenu.js'; import contextMenu from '$lib/stores/contextmenu';
import hostTree from '$lib/stores/hosttree.js'; import environment from '$lib/stores/environment';
import applicationInited from '$lib/stores/inited.js'; import hostTree from '$lib/stores/hosttree';
import windowTitle from '$lib/stores/windowtitle.js'; import applicationInited from '$lib/stores/inited';
import windowTitle from '$lib/stores/windowtitle';
import Connection from '$organisms/connection/index.svelte';
import { EventsOn } from '$wails/runtime';
import { tick } from 'svelte';
import AboutDialog from './dialogs/about.svelte';
import SettingsDialog from './dialogs/settings/index.svelte';
let showWelcomeScreen = undefined; let showWelcomeScreen = undefined;
@ -26,15 +28,26 @@
await tick(); await tick();
hostTree.newHost(); hostTree.newHost();
} }
function showAboutDialog() {
dialogs.new(AboutDialog);
}
function showSettings() {
dialogs.new(SettingsDialog);
}
EventsOn('OpenPreferences', showSettings);
EventsOn('OpenAboutModal', showAboutDialog);
</script> </script>
<svelte:window on:contextmenu|preventDefault /> <svelte:window on:contextmenu|preventDefault />
<div id="root"> <div id="root" class="platform-{$environment?.platform}">
<div class="titlebar">{$windowTitle}</div> <div class="titlebar">{$windowTitle}</div>
{#if $applicationInited && (showWelcomeScreen !== undefined)} {#if $applicationInited && (showWelcomeScreen !== undefined)}
<main class:empty={showWelcomeScreen} in:fly={{ y: 10 }}> <main class:empty={showWelcomeScreen}>
{#if showWelcomeScreen} {#if showWelcomeScreen}
<BlankState label="Welcome to Rolens!" image="/logo.png" pale={false} big={true}> <BlankState label="Welcome to Rolens!" image="/logo.png" pale={false} big={true}>
<button class="button" on:click={createFirstHost}>Add your first host</button> <button class="button" on:click={createFirstHost}>Add your first host</button>
@ -52,23 +65,30 @@
<style> <style>
.titlebar { .titlebar {
height: var(--titlebar-height); height: 0;
background-color: #00002a;
--wails-draggable: drag; --wails-draggable: drag;
color: #fff;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
overflow: hidden; overflow: hidden;
background-color: #ddd; }
#root.platform-darwin .titlebar {
height: var(--darwin-titlebar-height);
} }
main { main {
height: calc(100vh - var(--titlebar-height)); height: 100vh;
display: grid; display: grid;
grid-template: 1fr / minmax(300px, 0.3fr) 1fr; grid-template: 1fr / minmax(300px, 0.3fr) 1fr;
} }
main.empty { main.empty {
grid-template: 1fr / 1fr; grid-template: 1fr / 1fr;
} }
#root.platform-darwin main {
height: calc(100vh - var(--darwin-titlebar-height));
}
main > :global(*) { main > :global(*) {
overflow: auto; overflow: auto;
@ -78,4 +98,12 @@
main > :global(.addressbar) { main > :global(.addressbar) {
grid-column: 1 / 3; grid-column: 1 / 3;
} }
.databaselist {
overflow: scroll;
}
.button.create {
margin-top: 0.5rem;
}
</style> </style>

View File

@ -40,7 +40,7 @@
}); });
</script> </script>
<div bind:this={editorParent} class="editor" /> <div bind:this={editorParent} class="editor"></div>
<style> <style>
.editor { .editor {

View File

@ -50,7 +50,7 @@
<svelte:window on:keydown={keydown} /> <svelte:window on:keydown={keydown} />
{#if items && position} {#if items && position}
<div class="backdrop" on:pointerdown={close} /> <div class="backdrop" on:pointerdown={close}></div>
<nav> <nav>
<ul class="contextmenu" role="" style:left="{position[0]}px" style:top="{position[1]}px"> <ul class="contextmenu" role="" style:left="{position[0]}px" style:top="{position[1]}px">
{#each items as item, index} {#each items as item, index}
@ -101,7 +101,7 @@
text-align: left; text-align: left;
} }
button.selected { button.selected {
background-color: var(--selection); background-color: #00008b;
color: #fff; color: #fff;
} }

View File

@ -1,10 +1,10 @@
<script> <script>
import { daysAbbr, months } from '$lib/constants.js'; import { daysAbbr, months } from '$lib/constants';
import { addDays, getWeek, isDate, isSameDay, startOfWeek } from 'date-fns'; import { addDays, getWeek, isDate, isSameDay, startOfWeek } from 'date-fns';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Clock from '../clock.svelte'; import Clock from './clock.svelte';
import Icon from '../icon.svelte'; import Icon from './icon.svelte';
import Modal from '../modal.svelte'; import Modal from './modal.svelte';
export let value; export let value;
export let show = false; export let show = false;
@ -97,7 +97,7 @@
<table class="calendar"> <table class="calendar">
<thead> <thead>
<tr> <tr>
<th /> <th></th>
{#each daysAbbr as dayName} {#each daysAbbr as dayName}
<th>{dayName}</th> <th>{dayName}</th>
{/each} {/each}
@ -173,7 +173,7 @@
padding-bottom: 8px; padding-bottom: 8px;
} }
.calendar .day button.active { .calendar .day button.active {
background-color: var(--selection); background-color: #00008b;
color: #fff; color: #fff;
} }
.calendar .day button.notinmonth { .calendar .day button.notinmonth {

View File

@ -1,22 +1,16 @@
<script> <script>
import { ChooseDirectory } from '$wails/go/app/App.js'; import { OpenDirectory } from '$wails/go/ui/UI';
export let value = ''; export let value = '';
export let id = ''; export let id = '';
export let title = 'Choose a directory'; export let title = 'Choose a directory';
async function selectDir() { async function selectDir() {
value = await ChooseDirectory(title) || value; value = await OpenDirectory(title) || value;
} }
</script> </script>
<input <input type="text" on:pointerdown={selectDir} readonly {id} {value} />
type="text"
on:pointerdown={selectDir}
readonly
{id}
{value}
/>
<style> <style>
input { input {

View File

@ -1,10 +1,10 @@
<script> <script>
import input from '$lib/actions/input.js'; import input from '$lib/actions/input';
import { canBeObjectId, numericInputTypes } from '$lib/mongo/index.js'; import { canBeObjectId, numericInputTypes } from '$lib/mongo';
import { ObjectId } from 'bson'; import { ObjectId } from 'bson';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Datepicker from './datepicker.svelte'; import Datepicker from './datepicker.svelte';
import Icon from '../icon.svelte'; import Icon from './icon.svelte';
export let column = {}; export let column = {};
export let value = undefined; export let value = undefined;
@ -70,12 +70,7 @@
<div class="forminput {type}"> <div class="forminput {type}">
<div class="field"> <div class="field">
{#if type === 'string'} {#if type === 'string'}
<input <input type="text" bind:value use:input={{ type, onValid, onInvalid, mandatory, autofocus }} autocomplete="off" spellcheck="false" />
type="text"
bind:value
use:input={{ type, onValid, onInvalid, mandatory, autofocus }}
autocomplete="off"
spellcheck="false" />
{:else if type === 'objectid'} {:else if type === 'objectid'}
<input <input
type="text" type="text"
@ -84,11 +79,7 @@
use:input={{ type, onValid, onInvalid, mandatory, autofocus }} use:input={{ type, onValid, onInvalid, mandatory, autofocus }}
/> />
{:else if numericInputTypes.includes(type)} {:else if numericInputTypes.includes(type)}
<input <input type="number" bind:value use:input={{ type, onValid, onInvalid, mandatory, autofocus }} />
type="number"
bind:value
use:input={{ type, onValid, onInvalid, mandatory, autofocus }}
/>
{:else if type === 'bool'} {:else if type === 'bool'}
<select bind:value on:change={selectChange} bind:this={selectInput}> <select bind:value on:change={selectChange} bind:this={selectInput}>
<option value={undefined} disabled={mandatory}>Unset</option> <option value={undefined} disabled={mandatory}>Unset</option>
@ -107,38 +98,18 @@
<Icon name="edit" /> <Icon name="edit" />
</button> </button>
{/if} {/if}
<button <button class="button-small" type="button" title="Generate random object id" on:click={generateObjectId}>
class="button-small"
type="button"
title="Generate random object id"
on:click={generateObjectId}
>
<Icon name="reload" /> <Icon name="reload" />
</button> </button>
{:else if type === 'date'} {:else if type === 'date'}
<button <button class="button-small" type="button" title="Edit date" on:click={() => showDatepicker = true}>
class="button-small"
type="button"
title="Edit date"
on:click={() => showDatepicker = true}
>
<Icon name="edit" /> <Icon name="edit" />
</button> </button>
<button <button class="button-small" type="button" title="Set date to now" on:click={() => value = new Date()}>
class="button-small"
type="button"
title="Set date to now"
on:click={() => value = new Date()}
>
<Icon name="o" /> <Icon name="o" />
</button> </button>
{/if} {/if}
<button <button class="button-small" type="button" title="Reset field to default value" on:click={() => value = undefined}>
class="button-small"
type="button"
title="Reset field to default value"
on:click={() => value = undefined}
>
<Icon name="trash" /> <Icon name="trash" />
</button> </button>
</div> </div>

View File

@ -1,9 +1,9 @@
<script> <script>
import { pathsAreEqual, resolveKeypath, setValue } from '$lib/objects.js'; import { pathsAreEqual, resolveKeypath, setValue } from '$lib/objects';
import contextMenu from '$lib/stores/contextmenu.js'; import contextMenu from '$lib/stores/contextmenu';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import FormInput from '$components/editors/forminput.svelte'; import FormInput from './forminput.svelte';
import Icon from '$components/icon.svelte'; import Icon from './icon.svelte';
export let items = []; export let items = [];
export let columns = []; export let columns = [];
@ -52,10 +52,7 @@
return obj; return obj;
} }
else if ((typeof obj === 'object') && (obj !== null)) { else if ((typeof obj === 'object') && (obj !== null)) {
return Object.entries(obj).map(([ return Object.entries(obj).map(([ k, item ]) => {
k,
item,
]) => {
return { ...item, [key]: k }; return { ...item, [key]: k };
}); });
} }
@ -70,10 +67,7 @@
} }
activeKey = itemKey; activeKey = itemKey;
activePath = [ activePath = [ ...path.slice(0, level), itemKey ];
...path.slice(0, level),
itemKey,
];
dispatch('select', { level, itemKey, index, path: activePath }); dispatch('select', { level, itemKey, index, path: activePath });
} }
@ -132,17 +126,12 @@
</script> </script>
{#each _items as item, index} {#each _items as item, index}
{@const selected = canSelect && pathsAreEqual(activePath, [
...path,
item[key],
])}
<tr <tr
on:click={() => select(item[key], index)} on:click={() => select(item[key], index)}
on:dblclick={() => doubleClick(item[key], index)} on:dblclick={() => doubleClick(item[key], index)}
on:contextmenu|preventDefault={evt => showContextMenu(evt, item)} on:contextmenu|preventDefault={evt => showContextMenu(evt, item)}
class:selectable={canSelect} class:selectable={canSelect}
class:selected class:selected={canSelect && pathsAreEqual(activePath, [ ...path, item[key] ])}
class:striped class:striped
> >
{#if !hideChildrenToggles} {#if !hideChildrenToggles}
@ -156,10 +145,6 @@
<Icon name={childrenOpen[item[key]] ? 'chev-d' : 'chev-r'} /> <Icon name={childrenOpen[item[key]] ? 'chev-d' : 'chev-r'} />
</button> </button>
{/if} {/if}
{#if item.loading}
<span class="spinner" style:margin-left="{level * 10}px" />
{/if}
</td> </td>
{/if} {/if}
@ -172,11 +157,7 @@
{#each columns as column, columnIndex} {#each columns as column, columnIndex}
<td class:right={column.right} title={keypathProxies[index][column.key]}> <td class:right={column.right} title={keypathProxies[index][column.key]}>
{#if column.inputType} {#if column.inputType}
<FormInput <FormInput {column} bind:value={keypathProxies[index][column.key]} bind:valid={validity[columnIndex]} />
{column}
bind:value={keypathProxies[index][column.key]}
bind:valid={validity[columnIndex]}
/>
{:else} {:else}
<div class="value" style:margin-left="{level * 10}px"> <div class="value" style:margin-left="{level * 10}px">
{formatValue(keypathProxies[index][column.key])} {formatValue(keypathProxies[index][column.key])}
@ -187,12 +168,7 @@
{#if canRemoveItems} {#if canRemoveItems}
<td class="has-button"> <td class="has-button">
<button <button class="button-small" type="button" on:click|stopPropagation={() => removeItem(index, item[key])} on:dblclick|stopPropagation>
class="button-small"
type="button"
on:click|stopPropagation={() => removeItem(index, item[key])}
on:dblclick|stopPropagation
>
<Icon name="x" /> <Icon name="x" />
</button> </button>
</td> </td>
@ -208,10 +184,7 @@
{hideChildrenToggles} {hideChildrenToggles}
{canSelect} {canSelect}
{canRemoveItems} {canRemoveItems}
path={[ path={[ ...path, item[key] ]}
...path,
item[key],
]}
items={item.children} items={item.children}
level={level + 1} level={level + 1}
bind:activePath bind:activePath
@ -230,41 +203,28 @@
cursor: pointer; cursor: pointer;
} }
tr.selectable.selected td { tr.selectable.selected td {
background-color: var(--selection) !important; background-color: #00008b !important;
color: #fff;
} }
td { td {
padding: 4px 2px; padding: 2px;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
td.has-toggle { td.has-toggle {
position: relative; width: 20px;
width: 1.5em;
}
td.has-toggle .spinner {
position: absolute;
top: 0.3em;
left: 0.22em;
width: 1em;
height: 1em;
border: 1px solid var(--primary);
border-radius: 50px;
border-top-color: transparent;
border-left-color: transparent;
animation: .6s linear 0 spin;
animation-iteration-count: infinite;
} }
td.has-icon { td.has-icon {
padding: 0; padding: 0;
width: 1.5em; width: 17px;
} }
td.has-icon :global(svg) { td.has-icon :global(svg) {
width: 1em; width: 13px;
height: 1em; height: 13px;
} }
td .value { td .value {
height: 1.2em; height: 15px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -272,13 +232,14 @@
} }
button.toggle { button.toggle {
margin: 2px 0 0 3px;
padding: 0;
color: inherit; color: inherit;
padding: 0;
margin: 0;
vertical-align: top;
} }
button.toggle :global(svg) { button.toggle :global(svg) {
width: 0.9em; width: 13px;
height: 0.9em; height: 13px;
vertical-align: top; vertical-align: top;
} }

View File

@ -1,8 +1,8 @@
<script> <script>
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import BlankState from '../blankstate.svelte'; import BlankState from './blankstate.svelte';
import GridItems from './grid-items.svelte'; import GridItems from './grid-items.svelte';
import Icon from '../icon.svelte'; import Icon from './icon.svelte';
export let columns = []; export let columns = [];
export let items = []; export let items = [];
@ -57,17 +57,17 @@
<thead> <thead>
<tr> <tr>
{#if !hideChildrenToggles} {#if !hideChildrenToggles}
<th class="has-toggle" /> <th class="has-toggle"></th>
{/if} {/if}
<th class="has-icon" /> <th class="has-icon"></th>
{#each columns as column} {#each columns as column}
<th scope="col">{column.title || ''}</th> <th scope="col">{column.title || ''}</th>
{/each} {/each}
{#if canRemoveItems} {#if canRemoveItems}
<th class="has-button" /> <th class="has-button"></th>
{/if} {/if}
</tr> </tr>
</thead> </thead>

View File

@ -1,9 +1,6 @@
<script> <script>
import icons from '$lib/icons.json';
export let name = ''; export let name = '';
export let spin = false; export let spin = false;
export let rotation = 0;
if (name === 'loading') { if (name === 'loading') {
spin = true; spin = true;
@ -11,30 +8,25 @@
</script> </script>
<style> <style>
svg {
transition: transform 0.25s;
will-change: transform;
width: 1.2em;
height: 1.2em;
}
svg.spinning {
animation: spin 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
}
:global(.field) svg { :global(.field) svg {
width: 1em; width: 13px;
height: 1em; height: 13px;
margin-right: 2px; margin-right: 2px;
} }
:global(.button) svg { :global(.button) svg {
height: 1em; height: 13px;
width: auto; width: auto;
vertical-align: bottom; vertical-align: bottom;
} }
:global(.blankstate .button) svg {
height: 1.25em; @keyframes spinning {
vertical-align: -3px; 0% { transform: rotate(0deg); }
margin-right: 4px; 100% { transform: rotate(360deg); }
}
svg.spinning {
animation: spinning 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
} }
</style> </style>
@ -49,7 +41,115 @@
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class:spinning={spin} class:spinning={spin}
style:transform="rotate({rotation}deg)"
> >
{@html icons[name] || ''} {#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'}
<polyline points="15 18 9 12 15 6"></polyline>
{:else if name === 'chev-r'}
<polyline points="9 18 15 12 9 6"></polyline>
{:else if name === 'chev-d'}
<polyline points="6 9 12 15 18 9"></polyline>
{:else if name === 'chev-u'}
<path d="m18 15-6-6-6 6" />
{:else if name === 'chevs-l'}
<path d="m11 17-5-5 5-5M18 17l-5-5 5-5" />
{:else if name === 'chevs-r'}
<path d="m13 17 5-5-5-5M6 17l5-5-5-5" />
{:else if name === 'arr-d'}
<path d="M12 5v14M19 12l-7 7-7-7" />
{:else if name === 'db'}
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
{:else if name === 'x'}
<line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line>
{:else if name === '+'}
<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>
{:else if name === '-'}
<line x1="5" y1="12" x2="19" y2="12"></line>
{: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>
{: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'}
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline>
{:else if name === 'list'}
<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" />
{:else if name === 'table'}
<path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18" />
{:else if name === 'form'}
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" />
{:else if name === 'cog'}
<circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
{: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" />
{: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" />
{:else if name === 'cal'}
<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'}
<circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" />
{:else if name === 'trash'}
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M10 11v6M14 11v6" />
{:else if name === 'anchor'}
<circle cx="12" cy="5" r="3" /><path d="M12 22V8M5 12H2a10 10 0 0 0 20 0h-3" />
{:else if name === 'o'}
<circle cx="12" cy="12" r="10" />
{:else if name === 'info'}
<circle cx="12" cy="12" r="10" /><path d="M12 16v-4M12 8h.01" />
{:else if name === 'play'}
<path d="m5 3 14 9-14 9V3z" />
{:else if name === 'upload'}
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" />
{:else if name === 'save'}
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" /><path d="M17 21v-8H7v8M7 3v5h8" />
{:else if name === 're'}
<path d="m17 1 4 4-4 4" /><path d="M3 11V9a4 4 0 0 1 4-4h14M7 23l-4-4 4-4" /><path d="M21 13v2a4 4 0 0 1-4 4H3" />
{:else if name === 'chart'}
<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" />
{:else if name === 'shell'}
<path d="m4 17 6-6-6-6M12 19h8" />
{:else if name === 'doc'}
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" />
{/if}
</svg> </svg>

View File

@ -3,9 +3,8 @@
</script> </script>
<script> <script>
import { Beep } from '$wails/go/ui/UI';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { fade, fly } from 'svelte/transition';
import { Beep } from '$wails/go/ui/UI.js';
import Icon from './icon.svelte'; import Icon from './icon.svelte';
export let show = true; export let show = true;
@ -36,7 +35,6 @@
} }
function close() { function close() {
show = false;
dispatch('close'); dispatch('close');
} }
</script> </script>
@ -44,8 +42,8 @@
<svelte:window on:keydown={keydown} /> <svelte:window on:keydown={keydown} />
{#if show} {#if show}
<div class="modal outer" on:pointerdown|self={Beep} transition:fade={{ duration: 200 }}> <div class="modal outer" on:pointerdown|self={Beep}>
<div class="inner" style:max-width={width || '80vw'} transition:fly={{ y: 20, duration: 200 }}> <div class="inner" style:max-width={width || '80vw'}>
{#if title} {#if title}
<header> <header>
<div class="title">{title}</div> <div class="title">{title}</div>
@ -75,14 +73,19 @@
height: 100vh; height: 100vh;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
margin: 0; margin: 0;
padding: 2rem; padding-top: 50px;
--wails-draggable: drag; --wails-draggable: drag;
} }
:global(#root.platform-darwin) .outer {
margin-top: var(--darwin-titlebar-height);
}
.inner { .inner {
max-height: 80vh; max-height: 80vh;
background-color: #fff; background-color: #fff;
margin: auto; margin-left: auto;
margin-right: auto;
margin-bottom: auto;
width: 100%; width: 100%;
border-radius: var(--radius); border-radius: var(--radius);
display: flex; display: flex;

View File

@ -7,7 +7,9 @@
export let editor = undefined; export let editor = undefined;
export let readonly = false; export let readonly = false;
const extensions = [ javascript() ]; const extensions = [
javascript(),
];
onMount(() => { onMount(() => {
editor.dispatch({ editor.dispatch({
@ -20,8 +22,7 @@
}); });
</script> </script>
<CodeEditor <CodeEditor bind:editor
bind:editor
bind:text bind:text
on:inited on:inited
on:updated on:updated

View File

@ -1,5 +1,5 @@
<script> <script>
import { isBsonBuiltin } from '$lib/mongo/index.js'; import { isBsonBuiltin } from '$lib/mongo';
import { isDate } from 'date-fns'; import { isDate } from 'date-fns';
import Grid from './grid.svelte'; import Grid from './grid.svelte';
@ -22,6 +22,7 @@
].filter(c => !!c); ].filter(c => !!c);
$: if (data) { $: if (data) {
// items = dissectObject(data).map(item => ({ ...item, menu: getRootMenu(item.key, item) }));
items = []; items = [];
if (Array.isArray(data)) { if (Array.isArray(data)) {

View File

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

View File

@ -1,61 +1,40 @@
<script> <script>
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import { tweened } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
import Icon from './icon.svelte'; import Icon from './icon.svelte';
export let tabs = []; export let tabs = [];
export let selectedKey = ''; export let selectedKey = {};
export let canAddTab = false; export let canAddTab = false;
export let compact = true; export let multiline = false;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const activeIndicatorLeft = tweened(0, { easing: cubicOut, duration: 400 }); const maxPixelsPerMultilineTab = 120;
const activeIndicatorRight = tweened(0, { easing: cubicOut, duration: 400 });
const liElements = {};
let navEl; let navEl;
let pixelsPerTab = 0;
$: tabs && navEl && updateMeasurements();
function updateMeasurements() {
pixelsPerTab = (navEl.offsetWidth ?? 0) / tabs.length;
}
function select(tabKey) { function select(tabKey) {
selectedKey = tabKey; selectedKey = tabKey;
dispatch('select', tabKey); dispatch('select', tabKey);
} }
function moveActiveIndicator(target = liElements[selectedKey]) {
if (!compact) {
return;
}
const navRect = navEl.getBoundingClientRect();
const itemRect = target.getBoundingClientRect();
$activeIndicatorLeft = itemRect.x - navRect.x;
$activeIndicatorRight = navRect.right - itemRect.right;
}
onMount(() => { onMount(() => {
if (selectedKey) { window.addEventListener('resize', updateMeasurements);
moveActiveIndicator(liElements[selectedKey]);
}
}); });
</script> </script>
<svelte:window on:resize={() => moveActiveIndicator()} /> <nav class="tabs" class:multiline={multiline || (pixelsPerTab < maxPixelsPerMultilineTab)} bind:this={navEl}>
<nav class="tabs" class:compact bind:this={navEl}>
<ul> <ul>
{#each tabs as tab (tab.key)} {#each tabs as tab (tab.key)}
<li <li class:active={tab.key === selectedKey}>
class="tab"
class:active={tab.key === selectedKey}
class:closable={tab.closable}
bind:this={liElements[tab.key]}
on:mouseenter={event => moveActiveIndicator(event.target)}
on:mouseleave={() => moveActiveIndicator()}
>
<button class="tab" on:click={() => select(tab.key)}> <button class="tab" on:click={() => select(tab.key)}>
{#if tab.icon} <Icon name={tab.icon} /> {/if} {#if tab.icon} <Icon name={tab.icon} /> {/if}
<span class="label">{tab.title}</span> <span class="label">{tab.title}</span>
</button> </button>
{#if tab.closable} {#if tab.closable}
<button class="button-small" on:click={() => dispatch('closeTab', tab.key)}> <button class="button-small" on:click={() => dispatch('closeTab', tab.key)}>
<Icon name="x" /> <Icon name="x" />
@ -65,42 +44,39 @@
{/each} {/each}
{#if canAddTab} {#if canAddTab}
<li> <li class="tab add">
<button class="button-small" on:click={() => dispatch('addTab')}> <button class="tab" on:click={() => dispatch('addTab')}>
<Icon name="+" /> <Icon name="+" />
</button> </button>
</li> </li>
{/if} {/if}
</ul> </ul>
{#if compact}
<span
class="activeindicator"
style:left="{$activeIndicatorLeft}px"
style:right="{$activeIndicatorRight}px"
/>
{/if}
</nav> </nav>
<style> <style>
nav {
position: relative;
}
ul { ul {
overflow-x: auto; overflow-x: auto;
display: flex; display: flex;
list-style: none; list-style: none;
} }
li { li {
display: inline-block; display: inline-block;
flex: 1;
position: relative; position: relative;
} }
nav.tabs :global(svg) { li.add {
flex: 0 1;
}
.tabs :global(svg) {
width: 13px;
height: 13px;
vertical-align: bottom; vertical-align: bottom;
} }
li.active :global(svg) {
color: #fff;
}
button.tab { button.tab {
width: 100%; width: 100%;
@ -120,50 +96,20 @@
border-right: 1px solid #ccc; border-right: 1px solid #ccc;
} }
li.active button.tab { li.active button.tab {
background-color: var(--selection); color: #fff;
border-color: var(--primary); background-color: #00008b;
border-color: #00008b;
cursor: not-allowed; cursor: not-allowed;
} }
button.tab .label { nav.tabs.multiline button.tab .label {
margin-top: 5px; display: block;
display: inline-block; margin-top: 4px;
} }
nav.tabs .button-small { .button-small {
margin: 12px 0 0 4px;
}
li.closable .button-small {
display: none;
position: absolute; position: absolute;
top: 0;
right: 7px; right: 7px;
} top: 7px;
li.closable.active {
padding-right: 20px;
}
li.closable.active .button-small {
display: block;
}
nav.tabs.compact {
border-bottom: 1px solid #aaa;
}
nav.tabs.compact li {
border-bottom: 2px solid transparent;
}
nav.tabs.compact button.tab {
border: none;
color: inherit;
background-color: transparent;
}
.activeindicator {
display: block;
position: absolute;
bottom: -1px;
height: 2px;
background-color: var(--primary);
} }
</style> </style>

View File

@ -1,7 +1,7 @@
<script> <script>
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import alink from '$lib/actions/alink.js'; import alink from '$lib/actions/alink';
import environment from '$lib/stores/environment.js'; import environment from '$lib/stores/environment';
</script> </script>
<Modal width="400px" title=" " on:close> <Modal width="400px" title=" " on:close>
@ -19,7 +19,7 @@
<hr /> <hr />
<div class="info"> <div class="info">
<p class="copy">© Romein van Buren, 2022-2025.</p> <p class="copy">© Romein van Buren, 2023.</p>
<p> <p>
<a href="https://garraflavatra.github.io/rolens/" use:alink>Documentation</a> | <a href="https://garraflavatra.github.io/rolens/" use:alink>Documentation</a> |
<a href="https://github.com/garraflavatra/rolens" use:alink>GitHub</a> | <a href="https://github.com/garraflavatra/rolens" use:alink>GitHub</a> |
@ -30,10 +30,6 @@
</Modal> </Modal>
<style> <style>
.brand, .info {
line-height: 1.2;
}
.brand { .brand {
display: flex; display: flex;
align-items: center; align-items: center;
@ -44,9 +40,9 @@
flex: 0 1 125px; flex: 0 1 125px;
} }
.brand .title { .brand .title {
font-size: 2.25em; font-size: 2.25rem;
font-weight: 600; font-weight: 600;
margin-bottom: .25em; line-height: 2.5rem;
} }
.brand .title .version { .brand .title .version {
font-size: 80%; font-size: 80%;
@ -55,6 +51,7 @@
} }
.brand .description { .brand .description {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 1.6rem;
} }
hr { hr {
@ -63,6 +60,7 @@
.info { .info {
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1.25rem;
margin: 0 1rem 1rem; margin: 0 1rem 1rem;
text-align: center; text-align: center;
} }

View File

@ -1,8 +1,8 @@
<script> <script>
import DirectoryChooser from '$components/editors/directorychooser.svelte'; import DirectoryChooser from '$components/directorychooser.svelte';
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import input from '$lib/actions/input.js'; import input from '$lib/actions/input';
import settings from '$lib/stores/settings.js'; import settings from '$lib/stores/settings';
</script> </script>
<Modal title="Preferences" on:close> <Modal title="Preferences" on:close>
@ -15,13 +15,7 @@
<label for="defaultSort">Default sort query</label> <label for="defaultSort">Default sort query</label>
<label class="field"> <label class="field">
<input <input type="text" class="code" bind:value={$settings.defaultSort} id="defaultSort" use:input={{ type: 'json' }} />
type="text"
class="code"
bind:value={$settings.defaultSort}
id="defaultSort"
use:input={{ type: 'json' }}
/>
</label> </label>
<label for="autosubmitQuery">Autosubmit query</label> <label for="autosubmitQuery">Autosubmit query</label>

View File

@ -1,4 +1,4 @@
import { BrowserOpenURL } from '$wails/runtime/runtime.js'; import { BrowserOpenURL } from '$wails/runtime/runtime';
export default function alink(node) { export default function alink(node) {
node.addEventListener('click', e => { node.addEventListener('click', e => {

View File

@ -1,4 +1,4 @@
import environment from '$lib/stores/environment.js'; import environment from '$lib/stores/environment';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
export function controlKeyDown(event) { export function controlKeyDown(event) {

View File

@ -1,6 +1,6 @@
import { isInt } from '$lib/math.js'; import { isInt } from '$lib/math';
import { canBeObjectId, int32, int64, uint64 } from '$lib/mongo/index.js'; import { canBeObjectId, int32, int64, uint64 } from '$lib/mongo';
import { jsonLooseParse } from '$lib/strings.js'; import { jsonLooseParse } from '$lib/strings';
export default function input(node, { autofocus, type, onValid, onInvalid, mandatory } = { export default function input(node, { autofocus, type, onValid, onInvalid, mandatory } = {
autofocus: false, autofocus: false,
@ -10,9 +10,6 @@ export default function input(node, { autofocus, type, onValid, onInvalid, manda
mandatory: false, mandatory: false,
}) { }) {
node.setAttribute('spellcheck', false);
node.setAttribute('autocomplete', false);
const getMessage = () => { const getMessage = () => {
const checkInteger = () => (isInt(node.value) ? false : 'Value must be an integer'); const checkInteger = () => (isInt(node.value) ? false : 'Value must be an integer');
const checkNumberBoundaries = boundaries => { const checkNumberBoundaries = boundaries => {

View File

@ -1,4 +1,3 @@
import { AskConfirmation } from '$wails/go/app/App.js';
import InputDialog from '../dialogs/input.svelte'; import InputDialog from '../dialogs/input.svelte';
function newDialog(dialogComponent, data = {}) { function newDialog(dialogComponent, data = {}) {
@ -6,17 +5,11 @@ function newDialog(dialogComponent, data = {}) {
outlet.className = 'dialogoutlet'; outlet.className = 'dialogoutlet';
document.getElementById('dialogoutlets').appendChild(outlet); document.getElementById('dialogoutlets').appendChild(outlet);
const instance = new dialogComponent({ const instance = new dialogComponent({ target: outlet, props: data });
target: outlet,
intro: true,
props: data,
});
instance.$close = function() { instance.$close = function() {
setTimeout(() => {
instance.$destroy(); instance.$destroy();
outlet.remove(); outlet.remove();
}, 200);
}; };
instance.$on('close', instance.$close); instance.$on('close', instance.$close);
@ -36,10 +29,6 @@ function enterText(title = '', description = '', value = '') {
}); });
} }
function confirm(message = '') { const dialogs = { new: newDialog, enterText };
return AskConfirmation(message);
}
const dialogs = { new: newDialog, enterText, confirm };
export default dialogs; export default dialogs;

View File

@ -1,46 +0,0 @@
{
"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>",
"chev-l": "<polyline points=\"15 18 9 12 15 6\"></polyline>",
"chev-r": "<polyline points=\"9 18 15 12 9 6\"></polyline>",
"chev-d": "<polyline points=\"6 9 12 15 18 9\"></polyline>",
"chev-u": "<path d=\"m18 15-6-6-6 6\"/>",
"chevs-l": "<path d=\"m11 17-5-5 5-5M18 17l-5-5 5-5\"/>",
"chevs-r": "<path d=\"m13 17 5-5-5-5M6 17l5-5-5-5\"/>",
"arr-d": "<path d=\"M12 5v14M19 12l-7 7-7-7\"/>",
"arr-r": "<path d=\"M5 12h14M12 5l7 7-7 7\"/>",
"db": "<ellipse cx=\"12\" cy=\"5\" rx=\"9\" ry=\"3\"></ellipse><path d=\"M21 12c0 1.66-4 3-9 3s-9-1.34-9-3\"></path><path d=\"M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5\"></path>",
"x": "<line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>",
"+": "<line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\"></line><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"></line>",
"-": "<line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"></line>",
"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>",
"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>",
"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>",
"check": "<path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"></path><polyline points=\"22 4 12 14.01 9 11.01\"></polyline>",
"list": "<path d=\"M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01\"/>",
"table": "<path d=\"M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18\"/>",
"form": "<path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"/><path d=\"M14 2v6h6M16 13H8M16 17H8M10 9H8\"/>",
"cog": "<circle cx=\"12\" cy=\"12\" r=\"3\"/><path d=\"M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z\"/>",
"zap": "<path d=\"M13 2 3 14h9l-1 8 10-12h-9l1-8z\"/>",
"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\"/>",
"text": "<path d=\"M4 7V4h16v3M9 20h6M12 4v16\"/>",
"hash": "<path d=\"M4 9h16M4 15h16M10 3 8 21M16 3l-2 18\"/>",
"toggle-l": "<rect x=\"1\" y=\"5\" width=\"22\" height=\"14\" rx=\"7\" ry=\"7\"/><circle cx=\"8\" cy=\"12\" r=\"3\"/>",
"cal": "<rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"/><path d=\"M16 2v4M8 2v4M3 10h18\"/>",
"code": "<path d=\"m16 18 6-6-6-6M8 6l-6 6 6 6\"/>",
"target": "<circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/>",
"trash": "<path d=\"M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M10 11v6M14 11v6\"/>",
"anchor": "<circle cx=\"12\" cy=\"5\" r=\"3\"/><path d=\"M12 22V8M5 12H2a10 10 0 0 0 20 0h-3\"/>",
"o": "<circle cx=\"12\" cy=\"12\" r=\"10\"/>",
"info": "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4M12 8h.01\"/>",
"play": "<path d=\"m5 3 14 9-14 9V3z\"/>",
"upload": "<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12\"/>",
"save": "<path d=\"M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z\"/><path d=\"M17 21v-8H7v8M7 3v5h8\"/>",
"re": "<path d=\"m17 1 4 4-4 4\"/><path d=\"M3 11V9a4 4 0 0 1 4-4h14M7 23l-4-4 4-4\"/><path d=\"M21 13v2a4 4 0 0 1-4 4H3\"/>",
"chart": "<path d=\"M18 20V10M12 20V4M6 20v-6\"/>",
"?": "<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>",
"!": "<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\"/>",
"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\"/>",
"shell": "<path d=\"m4 17 6-6-6-6M12 19h8\"/>",
"doc": "<path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"/><path d=\"M14 2v6h6M16 13H8M16 17H8M10 9H8\"/>",
"columns": "<path d=\"M12 3h7a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-7m0-18H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7m0-18v18\"/>"
}

View File

@ -1,5 +1,5 @@
<script> <script>
import { locales } from './index.js'; import { locales } from '$lib/mongo';
const defaultCollation = { const defaultCollation = {
locale: 'en_US', locale: 'en_US',

View File

@ -0,0 +1,27 @@
import { StartProgressBar, StopProgressBar } from '$wails/go/ui/UI';
let taskCounter = 0;
export function startProgress(taskDescription = 'Loading…') {
const taskIndex = ++taskCounter;
let started = false;
const debouncer = setTimeout(() => {
StartProgressBar(taskIndex, taskDescription);
started = true;
}, 150);
const task = {
id: taskIndex,
description: taskDescription,
end: () => {
clearTimeout(debouncer);
if (started) {
StopProgressBar(taskIndex);
}
},
};
return task;
}

View File

@ -1,4 +1,4 @@
import { Environment } from '$wails/go/app/App.js'; import { Environment } from '$wails/go/app/App';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
const { set, subscribe } = writable({}); const { set, subscribe } = writable({});
@ -11,10 +11,5 @@ async function reload() {
reload(); reload();
subscribe(env => {
// @ts-ignore
document.body.dataset.platform = env?.platform;
});
const environment = { reload, subscribe }; const environment = { reload, subscribe };
export default environment; export default environment;

View File

@ -1,22 +1,21 @@
import dialogs from '$lib/dialogs.js'; import dialogs from '$lib/dialogs';
import { startProgress } from '$lib/progress';
import { get, writable } from 'svelte/store'; import { get, writable } from 'svelte/store';
import applicationInited from './inited.js'; import applicationInited from './inited';
import queries from './queries.js'; import queries from './queries';
import windowTitle from './windowtitle.js'; import windowTitle from './windowtitle';
import ExportDialog from '$organisms/connection/collection/dialogs/export.svelte'; import ExportDialog from '$organisms/connection/collection/dialogs/export.svelte';
import IndexDetailDialog from '$organisms/connection/collection/dialogs/indexdetail.svelte'; import IndexDetailDialog from '$organisms/connection/collection/dialogs/indexdetail.svelte';
import QueryChooserDialog from '$organisms/connection/collection/dialogs/querychooser.svelte'; import QueryChooserDialog from '$organisms/connection/collection/dialogs/querychooser.svelte';
import DumpDialog from '$organisms/connection/database/dialogs/dump.svelte'; import DumpDialog from '$organisms/connection/database/dialogs/dump.svelte';
import HostDetailDialog from '$organisms/connection/host/dialogs/hostdetail.svelte'; import HostDetailDialog from '$organisms/connection/host/dialogs/hostdetail.svelte';
import DuplicateDialog from '$organisms/connection/collection/dialogs/duplicate.svelte';
import { import {
CreateIndex, CreateIndex,
DropCollection, DropCollection,
DropDatabase, DropDatabase,
DropIndex, DropIndex,
DuplicateCollection,
ExecuteShellScript, ExecuteShellScript,
GetIndexes, GetIndexes,
HostLogs, HostLogs,
@ -29,7 +28,7 @@ import {
RemoveHost, RemoveHost,
RenameCollection, RenameCollection,
TruncateCollection TruncateCollection
} from '$wails/go/app/App.js'; } from '$wails/go/app/App';
const { set, subscribe } = writable({}); const { set, subscribe } = writable({});
const getValue = () => get({ subscribe }); const getValue = () => get({ subscribe });
@ -39,11 +38,7 @@ async function refresh() {
const hosts = await Hosts(); const hosts = await Hosts();
const hostTree = getValue(); const hostTree = getValue();
for (const [ for (const [ hostKey, hostDetails ] of Object.entries(hosts)) {
hostKey,
hostDetails,
] of Object.entries(hosts)) {
hostTree[hostKey] = hostTree[hostKey] || {}; hostTree[hostKey] = hostTree[hostKey] || {};
const host = hostTree[hostKey]; const host = hostTree[hostKey];
host.key = hostKey; host.key = hostKey;
@ -51,17 +46,7 @@ async function refresh() {
host.uri = hostDetails.uri; host.uri = hostDetails.uri;
host.open = async function() { host.open = async function() {
host.loading = true; const { databases: dbNames, status, statusError, systemInfo, systemInfoError } = await OpenConnection(hostKey);
set(hostTree);
const {
databases: dbNames,
status,
statusError,
systemInfo,
systemInfoError,
} = await OpenConnection(hostKey);
host.status = status; host.status = status;
host.statusError = statusError; host.statusError = statusError;
host.systemInfo = systemInfo; host.systemInfo = systemInfo;
@ -76,10 +61,7 @@ async function refresh() {
host.databases[dbKey] = host.databases[dbKey] || {}; host.databases[dbKey] = host.databases[dbKey] || {};
} }
for (const [ for (const [ dbKey, database ] of Object.entries(host.databases)) {
dbKey,
database,
] of Object.entries(host.databases)) {
if (!database.new && !dbNames.includes(dbKey)) { if (!database.new && !dbNames.includes(dbKey)) {
delete host.databases[dbKey]; delete host.databases[dbKey];
continue; continue;
@ -92,9 +74,6 @@ async function refresh() {
delete database.new; delete database.new;
database.open = async function() { database.open = async function() {
database.loading = true;
set(hostTree);
const { collections: collNames, stats, statsError } = await OpenDatabase(hostKey, dbKey); const { collections: collNames, stats, statsError } = await OpenDatabase(hostKey, dbKey);
database.stats = stats; database.stats = stats;
database.statsError = statsError; database.statsError = statsError;
@ -107,10 +86,7 @@ async function refresh() {
database.collections[collKey] = database.collections[collKey] || {}; database.collections[collKey] = database.collections[collKey] || {};
} }
for (const [ for (const [ collKey, collection ] of Object.entries(database.collections)) {
collKey,
collection,
] of Object.entries(database.collections)) {
if (!collection.new && !collNames.includes(collKey)) { if (!collection.new && !collNames.includes(collKey)) {
delete database.collections[collKey]; delete database.collections[collKey];
continue; continue;
@ -119,7 +95,6 @@ async function refresh() {
collection.key = collKey; collection.key = collKey;
collection.dbKey = dbKey; collection.dbKey = dbKey;
collection.hostKey = hostKey; collection.hostKey = hostKey;
collection.viewKey = 'list';
collection.indexes = collection.indexes || []; collection.indexes = collection.indexes || [];
delete collection.new; delete collection.new;
@ -136,45 +111,20 @@ async function refresh() {
collection.rename = async function() { collection.rename = async function() {
const newCollKey = await dialogs.enterText('Rename collection', `Enter a new name for collection ${collKey}.`, collKey); const newCollKey = await dialogs.enterText('Rename collection', `Enter a new name for collection ${collKey}.`, collKey);
if (newCollKey && (newCollKey !== collKey)) { if (newCollKey && (newCollKey !== collKey)) {
const progress = startProgress(`Renaming collection "${collKey}" to "${newCollKey}"…`);
const ok = await RenameCollection(hostKey, dbKey, collKey, newCollKey); const ok = await RenameCollection(hostKey, dbKey, collKey, newCollKey);
await database.open(); await database.open();
progress.end();
return ok; return ok;
} }
}; };
collection.duplicate = async function() {
const dialog = dialogs.new(DuplicateDialog, { host, dbKey, collKey });
return new Promise(resolve => {
dialog.$on('duplicate', async event => {
const success = await DuplicateCollection(
hostKey,
dbKey,
collKey,
event.detail.newHost,
event.detail.newDb,
event.detail.newColl
);
if (success) {
await refresh();
dialog.$close();
resolve();
}
});
});
};
collection.export = function(query) { collection.export = function(query) {
const dialog = dialogs.new(ExportDialog, { collection, query }); const dialog = dialogs.new(ExportDialog, { collection, query });
return new Promise(resolve => { return new Promise(resolve => {
dialog.$on('export', async event => { dialog.$on('export', async event => {
const success = await PerformFindExport( const success = await PerformFindExport(hostKey, dbKey, collKey, JSON.stringify(event.detail.exportInfo));
hostKey,
dbKey,
collKey,
JSON.stringify(event.detail.exportInfo)
);
if (success) { if (success) {
dialog.$close(); dialog.$close();
resolve(); resolve();
@ -209,7 +159,6 @@ async function refresh() {
collection.drop = async function() { collection.drop = async function() {
const success = await DropCollection(hostKey, dbKey, collKey); const success = await DropCollection(hostKey, dbKey, collKey);
if (success) { if (success) {
delete database.collections[collKey];
await refresh(); await refresh();
} }
}; };
@ -249,17 +198,10 @@ async function refresh() {
return new Promise(resolve => { return new Promise(resolve => {
dialog.$on('create', async event => { dialog.$on('create', async event => {
const newIndexName = await CreateIndex( const newIndexName = await CreateIndex(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(event.detail.index));
collection.hostKey,
collection.dbKey,
collection.key,
JSON.stringify(event.detail.index)
);
if (newIndexName) { if (newIndexName) {
dialog.$close(); dialog.$close();
} }
resolve(newIndexName); resolve(newIndexName);
}); });
}); });
@ -292,8 +234,6 @@ async function refresh() {
await refresh(); await refresh();
windowTitle.setSegments(dbKey, host.name, 'Rolens'); windowTitle.setSegments(dbKey, host.name, 'Rolens');
database.loading = false;
set(hostTree);
}; };
database.dump = function() { database.dump = function() {
@ -311,11 +251,14 @@ async function refresh() {
}; };
database.drop = async function() { database.drop = async function() {
const progress = startProgress(`Dropping database "${dbKey}"…`);
const success = await DropDatabase(hostKey, dbKey); const success = await DropDatabase(hostKey, dbKey);
if (success) { if (success) {
delete host.databases[dbKey];
await refresh(); await refresh();
} }
progress.end();
}; };
database.newCollection = async function() { database.newCollection = async function() {
@ -333,8 +276,6 @@ async function refresh() {
} }
await refresh(); await refresh();
host.loading = false;
set(hostTree);
}; };
host.executeShellScript = async function(script) { host.executeShellScript = async function(script) {

View File

@ -1,6 +1,6 @@
import { derived } from 'svelte/store'; import { derived } from 'svelte/store';
import environment from './environment.js'; import environment from './environment';
import applicationSettings from './settings.js'; import applicationSettings from './settings';
let alreadyInited = false; let alreadyInited = false;

View File

@ -1,4 +1,4 @@
import { RemoveQuery, SavedQueries, SaveQuery, UpdateQueries } from '$wails/go/app/App.js'; import { RemoveQuery, SavedQueries, SaveQuery, UpdateQueries } from '$wails/go/app/App';
import { get, writable } from 'svelte/store'; import { get, writable } from 'svelte/store';
const { set, subscribe } = writable({}); const { set, subscribe } = writable({});

View File

@ -1,4 +1,4 @@
import { Settings, UpdateSettings } from '$wails/go/app/App.js'; import { Settings, UpdateSettings } from '$wails/go/app/App';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
const { set, subscribe } = writable({}); const { set, subscribe } = writable({});

View File

@ -1,4 +1,4 @@
import { ReportSharedStateVariable } from '$wails/go/app/App.js'; import { ReportSharedStateVariable } from '$wails/go/app/App';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
function sharedStateStore(name) { function sharedStateStore(name) {

View File

@ -1,6 +1,6 @@
import dialogs from '$lib/dialogs.js'; import dialogs from '$lib/dialogs';
import ViewConfigDialog from '$organisms/connection/collection/dialogs/viewconfig.svelte'; import ViewConfigDialog from '$organisms/connection/collection/dialogs/viewconfig.svelte';
import { UpdateViewStore, Views } from '$wails/go/app/App.js'; import { UpdateViewStore, Views } from '$wails/go/app/App';
import { get, writable } from 'svelte/store'; import { get, writable } from 'svelte/store';
const { set, subscribe } = writable({}); const { set, subscribe } = writable({});

View File

@ -1,4 +1,4 @@
import { WindowSetTitle } from '$wails/runtime/runtime.js'; import { WindowSetTitle } from '$wails/runtime/runtime';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
const { set, subscribe } = writable('Rolens'); const { set, subscribe } = writable('Rolens');

View File

@ -22,7 +22,3 @@ export function looseJsonIsValid(json) {
return false; return false;
} }
} }
export function stringCouldBeID(string) {
return /^[a-zA-Z0-9_-]{1,}$/.test(string);
}

View File

@ -1,20 +1,15 @@
import './styles/loading.css';
import './styles/reset.css'; import './styles/reset.css';
import './styles/style.css'; import './styles/style.css';
import { EventsOn, LogError } from '$wails/runtime/runtime.js'; import { LogError } from '$wails/runtime';
import dialogs from '$lib/dialogs.js';
import App from './app.svelte'; import App from './app.svelte';
import AboutDialog from './dialogs/about.svelte';
import SettingsDialog from './dialogs/settings/index.svelte';
window.addEventListener('unhandledrejection', event => { window.addEventListener('unhandledrejection', event => {
LogError('Unhandled JS rejection: ' + event.reason); LogError('Unhandled JS rejection: ' + event.reason);
}); });
EventsOn('global.about', () => dialogs.new(AboutDialog)); // @ts-ignore Argument IS correct.
EventsOn('global.settings', () => dialogs.new(SettingsDialog));
const app = new App({ target: document.getElementById('app') }); const app = new App({ target: document.getElementById('app') });
export default app; export default app;

View File

@ -2,12 +2,12 @@
import Details from '$components/details.svelte'; import Details from '$components/details.svelte';
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import ObjectEditor from '$components/editors/objecteditor.svelte'; import ObjectEditor from '$components/objecteditor.svelte';
import { aggregationStageDocumentationURL, aggregationStages } from '$lib/mongo/index.js'; import { aggregationStageDocumentationURL, aggregationStages } from '$lib/mongo';
import Collation from '$lib/mongo/collation.svelte'; import Collation from '$lib/mongo/collation.svelte';
import { jsonLooseParse, looseJsonIsValid } from '$lib/strings.js'; import { jsonLooseParse, looseJsonIsValid } from '$lib/strings';
import { Aggregate } from '$wails/go/app/App.js'; import { Aggregate } from '$wails/go/app/App';
import { BrowserOpenURL } from '$wails/runtime/runtime.js'; import { BrowserOpenURL } from '$wails/runtime/runtime';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
export let collection; export let collection;
@ -68,8 +68,7 @@
<!-- svelte-ignore a11y-label-has-associated-control --> <!-- svelte-ignore a11y-label-has-associated-control -->
<label class="field"> <label class="field">
<ObjectEditor <ObjectEditor bind:text={stage.data}
bind:text={stage.data}
on:inited={e => { on:inited={e => {
e.detail.editor.dispatch({ e.detail.editor.dispatch({
changes: { changes: {
@ -81,6 +80,7 @@
anchor: 3, anchor: 3,
}, },
}); });
e.detail.editor.focus();
}} /> }} />
</label> </label>
</Details> </Details>

View File

@ -1,9 +1,9 @@
<script> <script>
import FormInput from '$components/editors/forminput.svelte'; import FormInput from '$components/forminput.svelte';
import Hint from '$components/hint.svelte'; import Hint from '$components/hint.svelte';
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import { inputTypes } from '$lib/mongo/index.js'; import { inputTypes } from '$lib/mongo';
import { resolveKeypath, setValue } from '$lib/objects.js'; import { resolveKeypath, setValue } from '$lib/objects';
export let item = {}; export let item = {};
export let view = {}; export let view = {};
@ -50,12 +50,7 @@
</span> </span>
</div> </div>
<div class="input"> <div class="input">
<FormInput <FormInput {column} bind:value={keypathProxy[column.key]} bind:valid={validity[column.key]} autofocus={index === 0} />
{column}
bind:value={keypathProxy[column.key]}
bind:valid={validity[column.key]}
autofocus={index === 0}
/>
</div> </div>
</label> </label>
{:else} {:else}

View File

@ -1,90 +0,0 @@
<script>
import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte';
import input from '$lib/actions/input.js';
import hostTree from '$lib/stores/hosttree.js';
import { createEventDispatcher } from 'svelte';
export let host = {};
export let dbKey = '';
export let collKey = '';
const dispatch = createEventDispatcher();
let newHost = host.key;
let newDb = dbKey;
let newColl = `${collKey}-duplicate`;
function duplicate() {
dispatch('duplicate', { newHost, newDb, newColl });
}
</script>
<Modal title="Duplicate collection" width="500px" on:close>
<div class="duplicate">
<div class="origin">
<div class="field">
<span class="label">{host.name || host.uri}</span>
<!-- <input type="text" readonly value={host.name || host.uri} /> -->
</div>
<div class="field">
<span class="label">{dbKey}</span>
<!-- <input type="text" readonly value={dbKey} /> -->
</div>
<div class="field">
<span class="label">{collKey}</span>
<!-- <input type="text" readonly value={collKey} /> -->
</div>
</div>
<div class="arrow">
<Icon name="arr-r" />
</div>
<div class="destination">
<label class="field">
<span class="label">Host</span>
<select bind:value={newHost}>
{#each Object.values($hostTree) as { key, name }}
<option value={key} selected={key === host.key}>{name}</option>
{/each}
</select>
</label>
<label class="field">
<span class="label">Database</span>
<input type="text" bind:value={newDb} use:input />
</label>
<label class="field">
<span class="label">Collection</span>
<input type="text" bind:value={newColl} use:input={{ autofocus: true }} />
</label>
</div>
</div>
<svelte:fragment slot="footer">
<button class="button" on:click={duplicate}>
<Icon name="play" /> Duplicate
</button>
</svelte:fragment>
</Modal>
<style>
.duplicate {
display: grid;
grid-template: auto / 1fr auto 1fr;
gap: 0.5rem;
}
.arrow {
align-self: center;
}
.field:not(:last-child) {
margin-bottom: 0.5rem;
}
.origin .field .label {
width: 100%;
display: inline-block;
background-color: #fff;
}
</style>

View File

@ -1,7 +1,7 @@
<script> <script>
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import views from '$lib/stores/views.js'; import views from '$lib/stores/views';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
export let collection; export let collection;
@ -16,7 +16,7 @@
} }
</script> </script>
<Modal title="Export results" width="500px" on:close> <Modal title="Export results" width="450px" on:close>
<form on:submit|preventDefault={submit}> <form on:submit|preventDefault={submit}>
<label class="field"> <label class="field">
<span class="label">Export</span> <span class="label">Export</span>
@ -40,10 +40,7 @@
<label class="field"> <label class="field">
<span class="label">View to use</span> <span class="label">View to use</span>
<select bind:value={exportInfo.viewKey}> <select bind:value={exportInfo.viewKey}>
{#each Object.entries(views.forCollection(collection.hostKey, collection.dbKey, collection.key)) as [ {#each Object.entries(views.forCollection(collection.hostKey, collection.dbKey, collection.key)) as [ key, { name } ]}
key,
{ name },
]}
<option value={key}>{name}</option> <option value={key}>{name}</option>
{/each} {/each}
</select> </select>

View File

@ -1,7 +1,7 @@
<script> <script>
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import input from '$lib/actions/input.js'; import input from '$lib/actions/input';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
export let collection; export let collection;
@ -27,12 +27,7 @@
<form on:submit|preventDefault={create}> <form on:submit|preventDefault={create}>
<label class="field name"> <label class="field name">
<span class="label">Name</span> <span class="label">Name</span>
<input <input type="text" placeholder="Optional" bind:value={index.name} use:input={{ autofocus: true }} />
type="text"
placeholder="Optional"
bind:value={index.name}
use:input={{ autofocus: true }}
/>
</label> </label>
<div class="toggles"> <div class="toggles">
@ -67,9 +62,8 @@
<div class="row"> <div class="row">
<label class="field"> <label class="field">
<span class="label">Key</span> <span class="label">Key</span>
<input type="text" placeholder="_id" bind:value={rule.key} /> <input type="text" placeholder="_id" bind:value={rule.key}>
</label> </label>
<label class="field"> <label class="field">
<select bind:value={rule.sort}> <select bind:value={rule.sort}>
<option value={1}>Ascending</option> <option value={1}>Ascending</option>
@ -77,7 +71,6 @@
<option value="hashed" disabled={index.model.length > 1}>Hashed</option> <option value="hashed" disabled={index.model.length > 1}>Hashed</option>
</select> </select>
</label> </label>
<button type="button" class="button danger" on:click={() => removeRule(ruleIndex)} disabled={index.model.length < 2}> <button type="button" class="button danger" on:click={() => removeRule(ruleIndex)} disabled={index.model.length < 2}>
<Icon name="-" /> <Icon name="-" />
</button> </button>
@ -92,7 +85,6 @@
<button class="button" on:click={addRule} disabled={index.model.some(r => r.sort === 'hashed')}> <button class="button" on:click={addRule} disabled={index.model.some(r => r.sort === 'hashed')}>
<Icon name="+" /> Add rule <Icon name="+" /> Add rule
</button> </button>
<button class="button" on:click={create} disabled={!index.model.length || index.model.some(r => !r.key)}> <button class="button" on:click={create} disabled={!index.model.length || index.model.some(r => !r.key)}>
<Icon name="+" /> Create index <Icon name="+" /> Create index
</button> </button>

View File

@ -1,11 +1,11 @@
<script> <script>
import Grid from '$components/grid/grid.svelte'; import Grid from '$components/grid.svelte';
import Hint from '$components/hint.svelte'; import Hint from '$components/hint.svelte';
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import input from '$lib/actions/input.js'; import input from '$lib/actions/input';
import hostTree from '$lib/stores/hosttree.js'; import hostTree from '$lib/stores/hosttree';
import queries from '$lib/stores/queries.js'; import queries from '$lib/stores/queries';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
export let queryToSave = undefined; export let queryToSave = undefined;
@ -71,7 +71,7 @@
<input type="text" bind:value={queryToSave.name} use:input={{ autofocus: true }} /> <input type="text" bind:value={queryToSave.name} use:input={{ autofocus: true }} />
</label> </label>
<label class="field"> <label class="field">
<textarea bind:value={queryToSave.remarks} placeholder="Remarks…" use:input /> <textarea bind:value={queryToSave.remarks} placeholder="Remarks…" use:input></textarea>
</label> </label>
{/if} {/if}

View File

@ -2,9 +2,9 @@
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import TabBar from '$components/tabbar.svelte'; import TabBar from '$components/tabbar.svelte';
import input from '$lib/actions/input.js'; import input from '$lib/actions/input';
import { randomString } from '$lib/math.js'; import { randomString } from '$lib/math';
import views from '$lib/stores/views.js'; import views from '$lib/stores/views';
export let collection; export let collection;
export let firstItem = {}; export let firstItem = {};
@ -114,12 +114,7 @@
{#key collection.viewKey} {#key collection.viewKey}
<label class="field"> <label class="field">
<span class="label">View name</span> <span class="label">View name</span>
<input <input type="text" use:input={{ autofocus: true }} bind:value={$views[collection.viewKey].name} disabled={collection.viewKey === 'list'} />
type="text"
use:input={{ autofocus: true }}
bind:value={$views[collection.viewKey].name}
disabled={collection.viewKey === 'list'}
/>
</label> </label>
{/key} {/key}
<label class="field"> <label class="field">
@ -133,11 +128,7 @@
{#if $views[collection.viewKey].type === 'list'} {#if $views[collection.viewKey].type === 'list'}
<div class="flex"> <div class="flex">
<input <input type="checkbox" id="hideObjectIndicators" bind:checked={$views[collection.viewKey].hideObjectIndicators} />
type="checkbox"
id="hideObjectIndicators"
bind:checked={$views[collection.viewKey].hideObjectIndicators}
/>
<label for="hideObjectIndicators"> <label for="hideObjectIndicators">
Hide object indicators ({'{...}'} and [...]) in list view and show nothing instead Hide object indicators ({'{...}'} and [...]) in list view and show nothing instead
</label> </label>
@ -194,41 +185,16 @@
</span> </span>
</label> </label>
<button <button class="button" type="button" on:click={() => addColumn(columnIndex)} title="Add column before this one">
class="button"
type="button"
on:click={() => addColumn(columnIndex)}
title="Add column before this one"
>
<Icon name="+" /> <Icon name="+" />
</button> </button>
<button class="button" type="button" on:click={() => moveColumn(columnIndex, -1)} disabled={columnIndex === 0} title="Move column one position up">
<button
class="button"
type="button"
on:click={() => moveColumn(columnIndex, -1)}
disabled={columnIndex === 0}
title="Move column one position up"
>
<Icon name="chev-u" /> <Icon name="chev-u" />
</button> </button>
<button class="button" type="button" on:click={() => moveColumn(columnIndex, 1)} disabled={columnIndex === $views[collection.viewKey].columns.length - 1} title="Move column one position down">
<button
class="button"
type="button"
on:click={() => moveColumn(columnIndex, 1)}
disabled={columnIndex === $views[collection.viewKey].columns.length - 1}
title="Move column one position down"
>
<Icon name="chev-d" /> <Icon name="chev-d" />
</button> </button>
<button class="button danger" type="button" on:click={() => removeColumn(columnIndex)} title="Remove this column">
<button
class="button danger"
type="button"
on:click={() => removeColumn(columnIndex)}
title="Remove this column"
>
<Icon name="x" /> <Icon name="x" />
</button> </button>
</div> </div>
@ -236,16 +202,10 @@
<p>No columns yet</p> <p>No columns yet</p>
{/each} {/each}
</div> </div>
<button class="button" on:click={addColumn}> <button class="button" on:click={addColumn}>
<Icon name="+" /> Add column <Icon name="+" /> Add column
</button> </button>
<button class="button" on:click={addSuggestedColumns} disabled={!Object.keys(firstItem || {}).length}>
<button
class="button"
on:click={addSuggestedColumns}
disabled={!Object.keys(firstItem || {}).length}
>
<Icon name="zap" /> Add suggested columns <Icon name="zap" /> Add suggested columns
</button> </button>
{/if} {/if}

View File

@ -1,19 +1,19 @@
<script> <script>
import Grid from '$components/grid/grid.svelte'; import Grid from '$components/grid.svelte';
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import ObjectGrid from '$components/grid/objectgrid.svelte'; import ObjectGrid from '$components/objectgrid.svelte';
import ObjectViewer from '$components/objectviewer.svelte'; import ObjectViewer from '$components/objectviewer.svelte';
import input from '$lib/actions/input.js'; import input from '$lib/actions/input';
import dialogs from '$lib/dialogs.js'; import { deepClone } from '$lib/objects';
import { deepClone } from '$lib/objects.js'; import { startProgress } from '$lib/progress';
import applicationSettings from '$lib/stores/settings.js'; import applicationSettings from '$lib/stores/settings';
import views from '$lib/stores/views.js'; import views from '$lib/stores/views';
import { convertLooseJson, stringCouldBeID } from '$lib/strings.js'; import { convertLooseJson } from '$lib/strings';
import { CountItems, FindItems, RemoveItemById, UpdateFoundDocument } from '$wails/go/app/App.js'; import { FindItems, RemoveItemById, UpdateFoundDocument } from '$wails/go/app/App';
import { EJSON } from 'bson'; import { EJSON } from 'bson';
import { onMount } from 'svelte';
export let collection; export let collection;
export let visible = false;
const defaults = { const defaults = {
query: '{}', query: '{}',
@ -25,21 +25,15 @@
let form = { ...defaults }; let form = { ...defaults };
let result = {}; let result = {};
let countResult = {};
let submittedForm = {}; let submittedForm = {};
let queryField; let queryField;
let activePath = []; let activePath = [];
let objectViewerData; let objectViewerData;
let querying = false; let querying = false;
let counting = false;
let objectViewerSuccessMessage = ''; let objectViewerSuccessMessage = '';
let viewsForCollection = {}; let viewsForCollection = {};
// $: code = `db.${collection.key}.find(${form.query || '{}'}${form.fields && // $: 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})` : ''};`;
// form.fields !== '{}' ? `, ${form.fields}` : ''}).sort(${form.sort})
// ${form.skip ? `.skip(${form.skip})` : ''}${form.limit ? `
// .limit(${form.limit})` : ''};`;
$: lastPage = (submittedForm.limit && result?.results?.length) ? Math.max(0, Math.ceil((result.total - submittedForm.limit) / submittedForm.limit)) : 0; $: lastPage = (submittedForm.limit && result?.results?.length) ? Math.max(0, Math.ceil((result.total - submittedForm.limit) / submittedForm.limit)) : 0;
$: activePage = (submittedForm.limit && submittedForm.skip && result?.results?.length) ? submittedForm.skip / submittedForm.limit : 0; $: activePage = (submittedForm.limit && submittedForm.skip && result?.results?.length) ? submittedForm.skip / submittedForm.limit : 0;
@ -48,27 +42,19 @@
} }
async function submitQuery() { async function submitQuery() {
if (querying || !visible) { if (querying) {
return; return;
} }
if (stringCouldBeID(form.query)) {
form.query = `{ "_id": "${form.query}" }`;
}
querying = `Querying ${collection.key}…`; querying = `Querying ${collection.key}…`;
activePath = []; activePath = [];
const newResult = await FindItems( const newResult = await FindItems(collection.hostKey, collection.dbKey, collection.key, JSON.stringify({
collection.hostKey,
collection.dbKey,
collection.key, JSON.stringify({
fields: convertLooseJson(form.fields || defaults.fields), fields: convertLooseJson(form.fields || defaults.fields),
limit: form.limit ?? defaults.limit, limit: form.limit ?? defaults.limit,
query: convertLooseJson(form.query) || defaults.query, query: convertLooseJson(form.query) || defaults.query,
skip: form.skip ?? defaults.skip, skip: form.skip ?? defaults.skip,
sort: convertLooseJson(form.sort) || defaults.sort, sort: convertLooseJson(form.sort) || defaults.sort,
}) }));
);
if (newResult) { if (newResult) {
newResult.results = newResult.results?.map(s => EJSON.parse(s, { relaxed: false })); newResult.results = newResult.results?.map(s => EJSON.parse(s, { relaxed: false }));
@ -80,17 +66,6 @@
querying = false; querying = false;
} }
async function countItems() {
counting = true;
countResult = await CountItems(
collection.hostKey,
collection.dbKey,
collection.key,
convertLooseJson(form.query) || defaults.query
);
counting = false;
}
async function refresh() { async function refresh() {
if ($applicationSettings.autosubmitQuery) { if ($applicationSettings.autosubmitQuery) {
await submitQuery(); await submitQuery();
@ -140,18 +115,7 @@
if (!activePath[0]) { if (!activePath[0]) {
return; return;
} }
const sure = await dialogs.confirm('Are you sure you wish to delete this item?'); const ok = await RemoveItemById(collection.hostKey, collection.dbKey, collection.key, activePath[0]);
if (!sure) {
return;
}
const ok = await RemoveItemById(
collection.hostKey,
collection.dbKey,
collection.key,
activePath[0]
);
if (ok) { if (ok) {
await submitQuery(); await submitQuery();
} }
@ -177,6 +141,7 @@
} }
async function saveDocument(event) { async function saveDocument(event) {
const progress = startProgress('Performing update…');
const success = await UpdateFoundDocument( const success = await UpdateFoundDocument(
collection.hostKey, collection.hostKey,
collection.dbKey, collection.dbKey,
@ -189,10 +154,12 @@
objectViewerSuccessMessage = 'Document has been saved!'; objectViewerSuccessMessage = 'Document has been saved!';
submitQuery(); submitQuery();
} }
progress.end();
} }
$: collection && refresh(); $: collection && refresh();
$: visible && refresh(); onMount(refresh);
</script> </script>
<div class="find"> <div class="find">
@ -200,13 +167,12 @@
<div class="formrow one"> <div class="formrow one">
<label class="field"> <label class="field">
<span class="label">Query or id</span> <span class="label">Query or id</span>
<input <input type="text"
type="text"
class="code" class="code"
placeholder={defaults.query} placeholder={defaults.query}
autocomplete="off" autocomplete="off"
spellcheck="false" spellcheck="false"
use:input use:input={{ type: 'json', autofocus: true }}
bind:this={queryField} bind:this={queryField}
bind:value={form.query} bind:value={form.query}
/> />
@ -242,8 +208,7 @@
<label class="field"> <label class="field">
<span class="label">Skip</span> <span class="label">Skip</span>
<input <input type="number"
type="number"
min="0" min="0"
bind:value={form.skip} bind:value={form.skip}
use:input use:input
@ -254,8 +219,7 @@
<label class="field"> <label class="field">
<span class="label">Limit</span> <span class="label">Limit</span>
<input <input type="number"
type="number"
min="0" min="0"
bind:value={form.limit} bind:value={form.limit}
use:input use:input
@ -317,33 +281,15 @@
</div> </div>
<div class="controls"> <div class="controls">
<div class="count"> <div>
{#if counting}
<span>Counting items…</span>
{:else if countResult?.error}
<span>{countResult.error}</span>
{:else if countResult?.total === -1}
<span>Something went wrong</span>
{:else if countResult?.total}
<!-- svelte-ignore a11y-invalid-attribute -->
<a href="" on:click|preventDefault={countItems}>Results: {countResult.total}</a>
{:else if result?.total === -1}
<!-- svelte-ignore a11y-invalid-attribute -->
<a href="" on:click|preventDefault={countItems}>Count items</a>
{:else if result?.total}
{#key result} {#key result}
<span class="flash-green">Results: {result.total || 0}</span> <span class="flash-green">Results: {result.total || 0}</span>
{/key} {/key}
{/if}
</div> </div>
<div> <div>
<label class="field inline"> <label class="field inline">
<select bind:value={collection.viewKey}> <select bind:value={collection.viewKey}>
{#each Object.entries(viewsForCollection) as [ {#each Object.entries(viewsForCollection) as [ key, view ]}
key,
view,
]}
<option value={key}>{view.name}</option> <option value={key}>{view.name}</option>
{/each} {/each}
</select> </select>
@ -351,49 +297,19 @@
<Icon name="cog" /> <Icon name="cog" />
</button> </button>
</label> </label>
<button class="button danger" on:click={removeActive} disabled={!activePath?.length} title="Drop selected item">
<button
class="button danger"
on:click={removeActive}
disabled={!activePath?.length}
title="Drop selected item"
>
<Icon name="-" /> <Icon name="-" />
</button> </button>
<button class="button" on:click={first} disabled={!submittedForm.limit || (submittedForm.skip <= 0) || !result?.results || (activePage === 0)} title="First page">
<button
class="button"
on:click={first}
disabled={!submittedForm.limit || (submittedForm.skip <= 0) || !result?.results || (activePage === 0)}
title="First page"
>
<Icon name="chevs-l" /> <Icon name="chevs-l" />
</button> </button>
<button class="button" on:click={prev} disabled={!submittedForm.limit || (submittedForm.skip <= 0) || !result?.results || (activePage === 0)} title="Previous {submittedForm.limit} items">
<button
class="button"
on:click={prev}
disabled={!submittedForm.limit || (submittedForm.skip <= 0) || !result?.results || (activePage === 0)}
title="Previous {submittedForm.limit} items"
>
<Icon name="chev-l" /> <Icon name="chev-l" />
</button> </button>
<button class="button" on:click={next} disabled={!submittedForm.limit || ((result?.results?.length || 0) < submittedForm.limit) || !result?.results || !lastPage || (activePage >= lastPage)} title="Next {submittedForm.limit} items">
<button
class="button"
on:click={next}
disabled={!submittedForm.limit || ((result?.results?.length || 0) < submittedForm.limit) || !result?.results || !lastPage || (activePage >= lastPage)}
title="Next {submittedForm.limit} items"
>
<Icon name="chev-r" /> <Icon name="chev-r" />
</button> </button>
<button class="button" on:click={last} disabled={!submittedForm.limit || ((result?.results?.length || 0) < submittedForm.limit) || !result?.results || !lastPage || (activePage >= lastPage)} title="Last page">
<button
class="button"
on:click={last}
disabled={!submittedForm.limit || ((result?.results?.length || 0) < submittedForm.limit) || !result?.results || !lastPage || (activePage >= lastPage)}
title="Last page"
>
<Icon name="chevs-r" /> <Icon name="chevs-r" />
</button> </button>
</div> </div>
@ -402,24 +318,11 @@
</div> </div>
{#if objectViewerData} {#if objectViewerData}
<ObjectViewer <ObjectViewer bind:data={objectViewerData} saveable on:save={saveDocument} bind:successMessage={objectViewerSuccessMessage} />
bind:data={objectViewerData}
saveable
on:save={saveDocument}
bind:successMessage={objectViewerSuccessMessage}
/>
{/if} {/if}
<datalist id="limits"> <datalist id="limits">
{#each [ {#each [ 1, 5, 10, 25, 50, 100, 200 ] as value}
1,
5,
10,
25,
50,
100,
200,
] as value}
<option {value} /> <option {value} />
{/each} {/each}
</datalist> </datalist>
@ -476,8 +379,4 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.count {
text-overflow: ellipsis;
}
</style> </style>

View File

@ -1,6 +1,7 @@
<script> <script>
import BlankState from '$components/blankstate.svelte'; import BlankState from '$components/blankstate.svelte';
import TabBar from '$components/tabbar.svelte'; import TabBar from '$components/tabbar.svelte';
import { EventsOn } from '$wails/runtime/runtime';
import { tick } from 'svelte'; import { tick } from 'svelte';
import Aggregate from './aggregate.svelte'; import Aggregate from './aggregate.svelte';
@ -12,49 +13,62 @@
import Stats from './stats.svelte'; import Stats from './stats.svelte';
import Update from './update.svelte'; import Update from './update.svelte';
export let host;
export let database;
export let collection; export let collection;
export let hostKey;
export let dbKey;
export let collKey;
export let tab = 'find'; export let tab = 'find';
const tabs = { let find;
stats: { icon: 'chart', title: 'Stats', component: Stats },
find: { icon: 'db', title: 'Find', component: Find },
insert: { icon: '+', title: 'Insert', component: Insert },
update: { icon: 'edit', title: 'Update', component: Update },
remove: { icon: 'trash', title: 'Remove', component: Remove },
indexes: { icon: 'list', title: 'Indexes', component: Indexes },
aggregate: { icon: 're', title: 'Aggregate', component: Aggregate },
shell: { icon: 'shell', title: 'Shell', component: Shell },
};
for (const key of Object.keys(tabs)) { $: if (collection) {
tabs[key].key = key; collection.hostKey = hostKey;
collection.dbKey = dbKey;
collection.key = collKey;
} }
$: if (hostKey || dbKey || collKey) {
tab = 'find';
}
EventsOn('OpenCollectionTab', name => (tab = name || tab));
async function catchQuery(event) { async function catchQuery(event) {
tab = 'find'; tab = 'find';
await tick(); await tick();
tabs.find.instance.performQuery(event.detail); find.performQuery(event.detail);
} }
</script> </script>
<div class="view" class:empty={!collection}> <div class="view" class:empty={!collection}>
{#if collection} {#if collection}
<TabBar tabs={Object.values(tabs)} bind:selectedKey={tab} /> {#key collection}
<TabBar
{#each Object.values(tabs) as view} tabs={[
<div class="container" class:hidden={tab !== view.key}> { key: 'stats', icon: 'chart', title: 'Stats' },
<svelte:component { key: 'find', icon: 'db', title: 'Find' },
this={view.component} { key: 'insert', icon: '+', title: 'Insert' },
visible={tab === view.key} { key: 'update', icon: 'edit', title: 'Update' },
on:performFind={catchQuery} { key: 'remove', icon: 'trash', title: 'Remove' },
{host} { key: 'indexes', icon: 'list', title: 'Indexes' },
{database} { key: 'aggregate', icon: 're', title: 'Aggregate' },
{collection} { key: 'shell', icon: 'shell', title: 'Shell' },
]}
bind:selectedKey={tab}
/> />
<div class="container">
{#if tab === 'stats'} <Stats {collection} />
{: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} />
{:else if tab === 'aggregate'} <Aggregate {collection} />
{:else if tab === 'shell'} <Shell {collection} />
{/if}
</div> </div>
{/each} {/key}
{:else} {:else}
<BlankState label="Select a collection to continue" /> <BlankState label="Select a collection to continue" />
{/if} {/if}
@ -78,9 +92,6 @@
min-height: 0; min-height: 0;
min-width: 0; min-width: 0;
} }
.container.hidden {
display: none;
}
.container > :global(*) { .container > :global(*) {
width: 100%; width: 100%;
} }

View File

@ -1,9 +1,9 @@
<script> <script>
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import ObjectGrid from '$components/grid/objectgrid.svelte'; import ObjectGrid from '$components/objectgrid.svelte';
import { onMount } from 'svelte';
export let collection; export let collection;
export let visible = false;
let activePath = []; let activePath = [];
let _indexes = []; let _indexes = [];
@ -11,10 +11,6 @@
let busy = false; let busy = false;
async function refresh() { async function refresh() {
if (!visible) {
return;
}
busy = 'Fetching indexes…'; busy = 'Fetching indexes…';
error = await collection.getIndexes(); error = await collection.getIndexes();
@ -53,7 +49,7 @@
} }
} }
$: visible && refresh(); onMount(refresh);
</script> </script>
<div class="indexes"> <div class="indexes">
@ -73,11 +69,9 @@
<button class="button" on:click={refresh}> <button class="button" on:click={refresh}>
<Icon name="reload" spin={busy} /> Reload <Icon name="reload" spin={busy} /> Reload
</button> </button>
<button class="button" on:click={createIndex}> <button class="button" on:click={createIndex}>
<Icon name="+" /> Create index… <Icon name="+" /> Create index…
</button> </button>
<button class="button danger" on:click={dropIndex} disabled={!_indexes.length || !activePath[0]}> <button class="button danger" on:click={dropIndex} disabled={!_indexes.length || !activePath[0]}>
<Icon name="x" /> Drop selected <Icon name="x" /> Drop selected
</button> </button>

View File

@ -1,20 +1,19 @@
<script> <script>
import Details from '$components/details.svelte'; import Details from '$components/details.svelte';
import Grid from '$components/grid/grid.svelte'; import Grid from '$components/grid.svelte';
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import ObjectEditor from '$components/editors/objecteditor.svelte'; import ObjectEditor from '$components/objecteditor.svelte';
import ObjectViewer from '$components/objectviewer.svelte'; import ObjectViewer from '$components/objectviewer.svelte';
import { randomString } from '$lib/math.js'; import { randomString } from '$lib/math';
import { inputTypes } from '$lib/mongo/index.js'; import { inputTypes } from '$lib/mongo';
import views from '$lib/stores/views.js'; import views from '$lib/stores/views';
import { capitalise, convertLooseJson, jsonLooseParse } from '$lib/strings.js'; import { capitalise, convertLooseJson, jsonLooseParse } from '$lib/strings';
import { InsertItems } from '$wails/go/app/App.js'; import { InsertItems } from '$wails/go/app/App';
import { EJSON } from 'bson'; import { EJSON } from 'bson';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import Form from './components/form.svelte'; import Form from './components/form.svelte';
export let collection; export let collection;
export let visible = false;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const formValidity = {}; const formValidity = {};
@ -99,8 +98,6 @@
views.openConfig(collection); views.openConfig(collection);
} }
$: visible && editor.focus();
onMount(() => { onMount(() => {
if (collection.viewKey === 'list') { if (collection.viewKey === 'list') {
editor.dispatch({ editor.dispatch({
@ -113,6 +110,7 @@
anchor: 3, anchor: 3,
}, },
}); });
editor.focus();
} }
}); });
</script> </script>

View File

@ -1,12 +1,11 @@
<script> <script>
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import ObjectEditor from '$components/editors/objecteditor.svelte'; import ObjectEditor from '$components/objecteditor.svelte';
import { convertLooseJson } from '$lib/strings.js'; import { convertLooseJson } from '$lib/strings';
import { RemoveItems } from '$wails/go/app/App.js'; import { RemoveItems } from '$wails/go/app/App';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
export let collection; export let collection;
export let visible = false;
let json = ''; let json = '';
let many = true; let many = true;
@ -24,8 +23,6 @@
); );
} }
$: visible && editor.focus();
onMount(() => { onMount(() => {
editor.dispatch({ editor.dispatch({
changes: { changes: {
@ -37,6 +34,7 @@
anchor: 3, anchor: 3,
}, },
}); });
editor.focus();
}); });
</script> </script>

View File

@ -1,6 +1,6 @@
<script> <script>
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import ObjectGrid from '$components/grid/objectgrid.svelte'; import ObjectGrid from '$components/objectgrid.svelte';
export let collection; export let collection;

View File

@ -1,10 +1,10 @@
<script> <script>
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import input from '$lib/actions/input.js'; import input from '$lib/actions/input';
import { atomicUpdateOperators } from '$lib/mongo/index.js'; import { atomicUpdateOperators } from '$lib/mongo';
import { deepClone } from '$lib/objects.js'; import { deepClone } from '$lib/objects';
import { convertLooseJson, jsonLooseParse } from '$lib/strings.js'; import { convertLooseJson, jsonLooseParse } from '$lib/strings';
import { UpdateItems } from '$wails/go/app/App.js'; import { UpdateItems } from '$wails/go/app/App';
export let collection = {}; export let collection = {};
@ -27,8 +27,7 @@
}); });
// function buildCode(form) { // function buildCode(form) {
// let operation = '{ ' + form.parameters.filter(p => p.type).map(p => // let operation = '{ ' + form.parameters.filter(p => p.type).map(p => `${p.type}: ${p.value || '{}'}`).join(', ') + ' }';
// `${p.type}: ${p.value || '{}'}`).join(', ') + ' }';
// if (operation === '{ }') { // if (operation === '{ }') {
// operation = '{}'; // operation = '{}';
// } // }
@ -39,8 +38,7 @@
// form.many && (options += 'multi: true'); // form.many && (options += 'multi: true');
// (form.upsert || form.many) && (options += ' }'); // (form.upsert || form.many) && (options += ' }');
// const code = `db.${collection.key}.update(${form.query || '{}'}, // const code = `db.${collection.key}.update(${form.query || '{}'}, ${operation}${options});`;
// ${operation}${options});`;
// return code; // return code;
// } // }
@ -50,13 +48,7 @@
f.parameters = f.parameters.map(param => { f.parameters = f.parameters.map(param => {
return { ...param, value: convertLooseJson(param.value) }; return { ...param, value: convertLooseJson(param.value) };
}); });
updatedCount = await UpdateItems(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(f));
updatedCount = await UpdateItems(
collection.hostKey,
collection.dbKey,
collection.key,
JSON.stringify(f)
);
} }
function removeParam(index) { function removeParam(index) {
@ -122,12 +114,7 @@
<label class="field"> <label class="field">
<span class="label">Filter</span> <span class="label">Filter</span>
<input <input type="text" class="code" bind:value={form.query} use:input={{ type: 'json', autofocus: true }} placeholder={'{}'} />
type="text"
class="code"
bind:value={form.query}
use:input={{ type: 'json', autofocus: true }}
placeholder={'{}'} />
</label> </label>
<fieldset class="parameters"> <fieldset class="parameters">
@ -145,13 +132,7 @@
</optgroup> </optgroup>
{/each} {/each}
</select> </select>
<input <input type="text" class="code" bind:value={param.value} placeholder={'{}'} use:input={{ type: 'json' }} />
type="text"
class="code"
bind:value={param.value}
placeholder={'{}'}
use:input={{ type: 'json' }}
/>
</label> </label>
<button class="button" disabled={form.parameters.length >= allOperators.length} on:click={() => addParameter()} type="button"> <button class="button" disabled={form.parameters.length >= allOperators.length} on:click={() => addParameter()} type="button">

View File

@ -1,10 +1,11 @@
<script> <script>
import DirectoryChooser from '$components/editors/directorychooser.svelte'; import DirectoryChooser from '$components/directorychooser.svelte';
import Grid from '$components/grid/grid.svelte'; import Grid from '$components/grid.svelte';
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import hostTree from '$lib/stores/hosttree.js'; import { startProgress } from '$lib/progress';
import applicationSettings from '$lib/stores/settings.js'; import hostTree from '$lib/stores/hosttree';
import { OpenConnection, OpenDatabase } from '$wails/go/app/App.js'; import applicationSettings from '$lib/stores/settings';
import { OpenConnection, OpenDatabase } from '$wails/go/app/App';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
export let info = {}; export let info = {};
@ -22,6 +23,7 @@
info.collKeys = []; info.collKeys = [];
if (hostKey) { if (hostKey) {
const progress = startProgress(`Opening connection to host "${hostKey}"`);
const databases = await OpenConnection(hostKey); const databases = await OpenConnection(hostKey);
if (databases && !$hostTree[hostKey]) { if (databases && !$hostTree[hostKey]) {
@ -30,6 +32,8 @@
$hostTree[hostKey].databases[dbKey] = $hostTree[hostKey].databases[dbKey] || { collections: {} }; $hostTree[hostKey].databases[dbKey] = $hostTree[hostKey].databases[dbKey] || { collections: {} };
}); });
} }
progress.end();
} }
} }
@ -38,11 +42,14 @@
info.dbKey = dbKey; info.dbKey = dbKey;
if (dbKey) { if (dbKey) {
const progress = startProgress(`Opening database "${dbKey}"`);
const collections = await OpenDatabase(info.hostKey, dbKey); const collections = await OpenDatabase(info.hostKey, dbKey);
for (const collKey of collections?.sort() || []) { for (const collKey of collections?.sort() || []) {
$hostTree[info.hostKey].databases[dbKey].collections[collKey] = {}; $hostTree[info.hostKey].databases[dbKey].collections[collKey] = {};
} }
progress.end();
} }
} }

View File

@ -1,22 +1,23 @@
<script> <script>
import BlankState from '$components/blankstate.svelte'; import BlankState from '$components/blankstate.svelte';
import TabBar from '$components/tabbar.svelte'; import TabBar from '$components/tabbar.svelte';
import { EventsOn } from '$wails/runtime/runtime.js'; import { EventsOn } from '$wails/runtime/runtime';
import Shell from '../shell.svelte'; import Shell from '../shell.svelte';
import Stats from './stats.svelte'; import Stats from './stats.svelte';
export let host;
export let database; export let database;
export let hostKey;
export let dbKey;
export let tab = 'stats'; export let tab = 'stats';
const tabs = { $: if (database) {
stats: { icon: 'chart', title: 'Database stats', component: Stats }, database.hostKey = hostKey;
shell: { icon: 'shell', title: 'Shell', component: Shell }, database.dbKey = dbKey;
}; }
for (const key of Object.keys(tabs)) { $: if (hostKey || dbKey) {
tabs[key].key = key; tab = 'stats';
} }
EventsOn('OpenStatsTab', name => (tab = name || tab)); EventsOn('OpenStatsTab', name => (tab = name || tab));
@ -25,13 +26,17 @@
<div class="view" class:empty={!database}> <div class="view" class:empty={!database}>
{#if database} {#if database}
{#key database} {#key database}
<TabBar tabs={Object.values(tabs)} bind:selectedKey={tab} /> <TabBar
tabs={[
{#each Object.values(tabs) as view} { key: 'stats', icon: 'chart', title: 'Database stats' },
<div class="container" class:hidden={tab !== view.key}> { key: 'shell', icon: 'shell', title: 'Shell' },
<svelte:component this={view.component} visible={tab === view.key} {host} {database} /> ]}
bind:selectedKey={tab} />
<div class="container">
{#if tab === 'stats'} <Stats {database} />
{:else if tab === 'shell'} <Shell {database} />
{/if}
</div> </div>
{/each}
{/key} {/key}
{:else} {:else}
<BlankState label="Select a database to continue" /> <BlankState label="Select a database to continue" />
@ -56,9 +61,6 @@
min-height: 0; min-height: 0;
min-width: 0; min-width: 0;
} }
.container.hidden {
display: none;
}
.container > :global(*) { .container > :global(*) {
width: 100%; width: 100%;
} }

View File

@ -1,6 +1,6 @@
<script> <script>
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import ObjectGrid from '$components/grid/objectgrid.svelte'; import ObjectGrid from '$components/objectgrid.svelte';
export let database; export let database;

View File

@ -1,8 +1,8 @@
<script> <script>
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import input from '$lib/actions/input.js'; import input from '$lib/actions/input';
import hostTree from '$lib/stores/hosttree.js'; import hostTree from '$lib/stores/hosttree';
import { AddHost, UpdateHost } from '$wails/go/app/App.js'; import { AddHost, UpdateHost } from '$wails/go/app/App';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
export let hostKey = ''; export let hostKey = '';
@ -54,13 +54,7 @@
<label class="field"> <label class="field">
<span class="label">Connection string</span> <span class="label">Connection string</span>
<input <input type="text" placeholder="mongodb://..." bind:value={form.uri} spellcheck="false" use:input />
type="text"
placeholder="mongodb://..."
bind:value={form.uri}
spellcheck="false"
use:input
/>
</label> </label>
</form> </form>

View File

@ -1,7 +1,7 @@
<script> <script>
import BlankState from '$components/blankstate.svelte'; import BlankState from '$components/blankstate.svelte';
import TabBar from '$components/tabbar.svelte'; import TabBar from '$components/tabbar.svelte';
import { EventsOn } from '$wails/runtime/runtime.js'; import { EventsOn } from '$wails/runtime/runtime';
import Logs from './logs.svelte'; import Logs from './logs.svelte';
import Shell from '../shell.svelte'; import Shell from '../shell.svelte';
@ -9,17 +9,15 @@
import SystemInfo from './systeminfo.svelte'; import SystemInfo from './systeminfo.svelte';
export let host; export let host;
export let hostKey;
export let tab = 'status'; export let tab = 'status';
const tabs = { $: if (host) {
status: { icon: 'chart', title: 'Host status', component: Status }, host.hostKey = hostKey;
shell: { icon: 'shell', title: 'Shell', component: Shell }, }
logs: { icon: 'doc', title: 'Logs', component: Logs },
systemInfo: { icon: 'server', title: 'System info', component: SystemInfo },
};
for (const key of Object.keys(tabs)) { $: if (hostKey) {
tabs[key].key = key; tab = 'status';
} }
EventsOn('OpenStatusTab', name => (tab = name || tab)); EventsOn('OpenStatusTab', name => (tab = name || tab));
@ -28,13 +26,23 @@
<div class="view" class:empty={!host}> <div class="view" class:empty={!host}>
{#if host} {#if host}
{#key host} {#key host}
<TabBar tabs={Object.values(tabs)} bind:selectedKey={tab} /> <TabBar
tabs={[
{ key: 'status', icon: 'chart', title: 'Host status' },
{ key: 'shell', icon: 'shell', title: 'Shell' },
{ key: 'logs', icon: 'doc', title: 'Logs' },
{ key: 'systemInfo', icon: 'server', title: 'System info' },
]}
bind:selectedKey={tab}
/>
{#each Object.values(tabs) as view} <div class="container">
<div class="container" class:hidden={tab !== view.key}> {#if tab === 'status'} <Status {host} />
<svelte:component this={view.component} visible={tab === view.key} {host} /> {:else if tab === 'logs'} <Logs {host} />
{:else if tab === 'systemInfo'} <SystemInfo {host} />
{:else if tab === 'shell'} <Shell {host} />
{/if}
</div> </div>
{/each}
{/key} {/key}
{:else} {:else}
<BlankState label="Select a host to continue" /> <BlankState label="Select a host to continue" />
@ -59,9 +67,6 @@
min-height: 0; min-height: 0;
min-width: 0; min-width: 0;
} }
.container.hidden {
display: none;
}
.container > :global(*) { .container > :global(*) {
width: 100%; width: 100%;
} }

View File

@ -1,14 +1,13 @@
<script> <script>
import Grid from '$components/grid/grid.svelte'; import Grid from '$components/grid.svelte';
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import ObjectViewer from '$components/objectviewer.svelte'; import ObjectViewer from '$components/objectviewer.svelte';
import input from '$lib/actions/input.js'; import input from '$lib/actions/input';
import { logComponents, logLevels } from '$lib/mongo/index.js'; import { logComponents, logLevels } from '$lib/mongo';
import { BrowserOpenURL } from '$wails/runtime/runtime.js'; import { BrowserOpenURL } from '$wails/runtime/runtime';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
export let host; export let host;
export let visible = false;
const autoReloadIntervals = [ 1, 2, 5, 10, 30, 60 ]; const autoReloadIntervals = [ 1, 2, 5, 10, 30, 60 ];
let filter = 'global'; let filter = 'global';
@ -32,10 +31,6 @@
} }
async function refresh() { async function refresh() {
if (!visible) {
return;
}
let _logs = []; let _logs = [];
({ logs: _logs, total, error } = await host.getLogs(filter)); ({ logs: _logs, total, error } = await host.getLogs(filter));
logs = []; logs = [];
@ -69,8 +64,6 @@
setTimeout(() => copySucceeded = false, 1500); setTimeout(() => copySucceeded = false, 1500);
} }
$: visible && !logs && refresh();
onDestroy(() => { onDestroy(() => {
if (interval) { if (interval) {
clearInterval(interval); clearInterval(interval);
@ -82,13 +75,7 @@
<div class="formrow"> <div class="formrow">
<label class="field"> <label class="field">
<span class="label">Auto reload (seconds)</span> <span class="label">Auto reload (seconds)</span>
<input <input type="number" class="autoreloadinput" bind:value={autoReloadInterval} list="autoreloadintervals" use:input />
type="number"
class="autoreloadinput"
bind:value={autoReloadInterval}
list="autoreloadintervals"
use:input
/>
</label> </label>
<label class="field"> <label class="field">

View File

@ -1,6 +1,6 @@
<script> <script>
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import ObjectGrid from '$components/grid/objectgrid.svelte'; import ObjectGrid from '$components/objectgrid.svelte';
export let host; export let host;

View File

@ -1,6 +1,6 @@
<script> <script>
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import ObjectGrid from '$components/grid/objectgrid.svelte'; import ObjectGrid from '$components/objectgrid.svelte';
export let host; export let host;

View File

@ -1,33 +1,26 @@
<script> <script>
import Grid from '$components/grid/grid.svelte'; import Grid from '$components/grid.svelte';
import hostTree from '$lib/stores/hosttree.js'; import hostTree from '$lib/stores/hosttree';
export let path = []; export let path = [];
</script> </script>
<Grid <Grid
striped={false} striped={false}
columns={[ columns={[ { key: 'name' }, { key: 'count', right: true } ]}
{ key: 'name' },
{ key: 'count', right: true },
]}
items={Object.values($hostTree || {}).map(host => { items={Object.values($hostTree || {}).map(host => {
return { return {
id: host.key, id: host.key,
name: host.name, name: host.name,
loading: host.loading,
icon: 'server', icon: 'server',
children: Object.values(host.databases || {}) children: Object.values(host.databases || {})
.sort((a, b) => a.key.localeCompare(b)) .sort((a, b) => a.key.localeCompare(b))
.map(database => { .map(database => {
return { return {
id: database.key, id: database.key,
name: database.key, name: database.key,
loading: database.loading,
count: Object.keys(database.collections || {}).length || '',
icon: 'db', icon: 'db',
count: Object.keys(database.collections || {}).length || '',
children: Object.values(database.collections) children: Object.values(database.collections)
.sort((a, b) => a.key.localeCompare(b)) .sort((a, b) => a.key.localeCompare(b))
.map(collection => { .map(collection => {
@ -40,8 +33,6 @@
{ label: 'Dump collection (BSON via mongodump)…', fn: collection.dump }, { label: 'Dump collection (BSON via mongodump)…', fn: collection.dump },
{ separator: true }, { separator: true },
{ label: 'Rename collection…', fn: collection.rename }, { label: 'Rename collection…', fn: collection.rename },
{ label: 'Duplicate collection…', fn: collection.duplicate },
{ separator: true },
{ label: 'Truncate collection…', fn: collection.truncate }, { label: 'Truncate collection…', fn: collection.truncate },
{ label: 'Drop collection…', fn: collection.drop }, { label: 'Drop collection…', fn: collection.drop },
{ separator: true }, { separator: true },
@ -49,7 +40,6 @@
], ],
}; };
}) || [], }) || [],
menu: [ menu: [
{ label: 'Dump database (BSON via mongodump)…', fn: database.dump }, { label: 'Dump database (BSON via mongodump)…', fn: database.dump },
{ label: 'Drop database…', fn: database.drop }, { label: 'Drop database…', fn: database.drop },
@ -59,7 +49,6 @@
], ],
}; };
}), }),
menu: [ menu: [
{ label: 'New database…', fn: host.newDatabase }, { label: 'New database…', fn: host.newDatabase },
{ separator: true }, { separator: true },

View File

@ -1,26 +1,18 @@
<script> <script>
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import hostTree from '$lib/stores/hosttree.js'; import hostTree from '$lib/stores/hosttree';
import sharedState from '$lib/stores/sharedstate.js'; import sharedState from '$lib/stores/sharedstate';
import { EventsOn } from '$wails/runtime/runtime.js'; import { EventsOn } from '$wails/runtime/runtime';
import CollectionView from './collection/index.svelte'; import CollectionView from './collection/index.svelte';
import DatabaseView from './database/index.svelte'; import DatabaseView from './database/index.svelte';
import HostView from './host/index.svelte'; import HostView from './host/index.svelte';
import HostTree from './hosttree.svelte'; import HostTree from './hosttree.svelte';
let path = []; let path = [];
let prevPath = '';
let hostTab = ''; let hostTab = '';
let dbTab = ''; let dbTab = '';
let collTab = ''; let collTab = '';
$: if (path.join('.') !== prevPath) {
hostTab = 'status';
dbTab = 'stats';
collTab = 'find';
prevPath = path.join('.');
}
$: activeHostKey = path[0]; $: activeHostKey = path[0];
$: activeDbKey = path[1]; $: activeDbKey = path[1];
$: activeCollKey = path[2]; $: activeCollKey = path[2];
@ -63,26 +55,26 @@
</div> </div>
{#if activeCollKey} {#if activeCollKey}
{#key activeCollKey}
<CollectionView <CollectionView
host={$hostTree[activeHostKey]}
database={$hostTree[activeHostKey]?.databases[activeDbKey]}
collection={$hostTree[activeHostKey]?.databases[activeDbKey]?.collections?.[activeCollKey]} collection={$hostTree[activeHostKey]?.databases[activeDbKey]?.collections?.[activeCollKey]}
hostKey={activeHostKey}
dbKey={activeDbKey}
collKey={activeCollKey}
bind:tab={collTab} bind:tab={collTab}
/> />
{/key}
{:else if activeDbKey} {:else if activeDbKey}
{#key activeDbKey}
<DatabaseView <DatabaseView
host={$hostTree[activeHostKey]}
database={$hostTree[activeHostKey]?.databases[activeDbKey]} database={$hostTree[activeHostKey]?.databases[activeDbKey]}
hostKey={activeHostKey}
dbKey={activeDbKey}
bind:tab={dbTab} bind:tab={dbTab}
/> />
{/key}
{:else if activeHostKey} {:else if activeHostKey}
{#key activeHostKey} <HostView
<HostView host={$hostTree[activeHostKey]} bind:tab={hostTab} /> host={$hostTree[activeHostKey]}
{/key} hostKey={activeHostKey}
bind:tab={hostTab}
/>
{/if} {/if}
<style> <style>

View File

@ -1,33 +1,24 @@
<script> <script>
import BlankState from '$components/blankstate.svelte'; import BlankState from '$components/blankstate.svelte';
import CodeEditor from '$components/editors/codeeditor.svelte'; import CodeEditor from '$components/codeeditor.svelte';
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import environment from '$lib/stores/environment.js';
import { OpenShellScript, SaveShellScript, SaveShellOuput } from '$wails/go/app/App.js';
import { BrowserOpenURL } from '$wails/runtime/runtime.js';
import { javascript } from '@codemirror/lang-javascript'; import { javascript } from '@codemirror/lang-javascript';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
export let host = undefined; export let host = undefined;
export let database = undefined; export let database = undefined;
export let collection = undefined; export let collection = undefined;
export let visible = false;
const placeholder = '// Write your script here...'; const placeholder = '// Write your script here...';
const extensions = [ javascript() ]; const extensions = [ javascript() ];
let script = ''; let script = '';
let result = {}; let result = {};
let horizontal = false;
let copySucceeded = false; let copySucceeded = false;
let timeout; let timeout;
let busy = false; let busy = false;
let editor; let editor;
async function runScript() { async function run() {
if (!$environment.hasMongoShell) {
return;
}
busy = true; busy = true;
if (collection) { if (collection) {
@ -43,56 +34,12 @@
busy = false; busy = false;
} }
async function loadScript() {
const _script = await OpenShellScript();
if (_script) {
script = _script;
editor.dispatch({
changes: {
from: 0,
to: editor.state.doc.length,
insert: script,
},
selection: {
from: 0,
anchor: 0,
to: 0,
head: 0,
},
});
}
}
async function saveScript() {
await SaveShellScript(
host?.key || '',
database?.key || '',
collection?.key || '',
script,
false // not temporary
);
}
async function saveOutput() {
await SaveShellOuput(result.output + '\n' + result.stderr);
}
async function copyErrorDescription() { async function copyErrorDescription() {
await navigator.clipboard.writeText(result.errorDescription); await navigator.clipboard.writeText(result.errorDescription);
copySucceeded = true; copySucceeded = true;
timeout = setTimeout(() => copySucceeded = false, 1500); timeout = setTimeout(() => copySucceeded = false, 1500);
} }
function toggleView() {
horizontal = !horizontal;
}
function openMongoshInstallDocs() {
BrowserOpenURL('https://garraflavatra.github.io/rolens/user-guide/shell/');
}
$: visible && editor.focus();
onMount(() => { onMount(() => {
editor.dispatch({ editor.dispatch({
changes: { changes: {
@ -109,11 +56,10 @@
}); });
editor.focus(); editor.focus();
}); });
onDestroy(() => clearTimeout(timeout)); onDestroy(() => clearTimeout(timeout));
</script> </script>
<div class="shell" class:horizontal> <div class="shell">
<div class="overflow"> <div class="overflow">
<!-- svelte-ignore a11y-label-has-associated-control --> <!-- svelte-ignore a11y-label-has-associated-control -->
<label class="field"> <label class="field">
@ -122,17 +68,7 @@
</div> </div>
<div class="output"> <div class="output">
{#if !$environment.hasMongoShell} {#if busy}
<BlankState
title="mongosh is required to run shell scripts"
label="Please refer to the documentation for more information."
icon="!"
>
<button class="button" on:click={openMongoshInstallDocs}>
<Icon name="info" /> Read the documentation
</button>
</BlankState>
{:else if busy}
<BlankState icon="loading" label="Executing…" /> <BlankState icon="loading" label="Executing…" />
{:else if result.errorTitle || result.errorDescription} {:else if result.errorTitle || result.errorDescription}
<BlankState title={result.errorTitle} label={result.errorDescription} icon="!"> <BlankState title={result.errorTitle} label={result.errorDescription} icon="!">
@ -141,32 +77,11 @@
</button> </button>
</BlankState> </BlankState>
{:else} {:else}
<pre>{result.output || ''}{#if result.stderr}<div class="error">{result.stderr}</div>{/if}</pre> <pre>{result.output || ''}</pre>
{/if} {/if}
</div> </div>
<div class="controls"> <div class="controls">
<button class="button" on:click={runScript}>
<Icon name="play" /> Run
</button>
<div class="field inline">
<button class="button secondary" on:click={loadScript}>
<Icon name="upload" /> Load script…
</button>
<button class="button secondary" on:click={saveScript}>
<Icon name="save" /> Save as…
</button>
</div>
<button class="button secondary" disabled={!result.output} on:click={saveOutput}>
<Icon name="upload" /> Save output as…
</button>
<button class="button viewtoggle" title="Toggle horizontal/vertical view" on:click={toggleView}>
<Icon name="columns" rotation={horizontal ? 90 : 0} />
</button>
{#key result} {#key result}
<div class="status flash-green"> <div class="status flash-green">
{#if result?.status} {#if result?.status}
@ -174,6 +89,10 @@
{/if} {/if}
</div> </div>
{/key} {/key}
<button class="btn" on:click={run}>
<Icon name="play" /> Run
</button>
</div> </div>
</div> </div>
@ -182,9 +101,6 @@
display: grid; display: grid;
grid-template: 1fr auto / 1fr 1fr; grid-template: 1fr auto / 1fr 1fr;
} }
.shell.horizontal {
grid-template: 1fr 1fr auto / 1fr;
}
.overflow { .overflow {
overflow: auto; overflow: auto;
@ -198,7 +114,7 @@
} }
.output { .output {
background-color: #2e3027; background-color: #111;
color: #fff; color: #fff;
overflow: auto; overflow: auto;
display: flex; display: flex;
@ -213,10 +129,6 @@
-webkit-user-select: text; -webkit-user-select: text;
cursor: text; cursor: text;
} }
.output pre .error {
color: #ff8989;
margin-top: 2px;
}
.output :global(.blankstate) { .output :global(.blankstate) {
margin: auto; margin: auto;
padding: 0.5rem; padding: 0.5rem;
@ -225,14 +137,10 @@
.controls { .controls {
margin-top: 0.5rem; margin-top: 0.5rem;
display: flex; display: flex;
gap: 0.2rem;
align-items: center; align-items: center;
grid-column: 1 / 3; grid-column: 1 / 3;
} }
.controls .viewtoggle { .controls .status {
margin-left: auto; margin-right: auto;
}
.shell.horizontal .controls {
grid-column: 1;
} }
</style> </style>

View File

@ -0,0 +1,50 @@
#app-loading {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
display: flex;
}
/* Source of the following CSS: https://loading.io/css/ (modified) */
#app-loading .ring {
display: inline-block;
position: relative;
width: 136px;
height: 136px;
margin: auto;
}
#app-loading .ring div {
box-sizing: border-box;
display: block;
position: absolute;
width: 120px;
height: 120px;
margin: 8px;
border: 8px solid;
border-radius: 50%;
animation: loading-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: #f1dac8 transparent transparent transparent;
}
#app-loading .ring div:nth-child(1) {
animation-delay: -0.45s;
}
#app-loading .ring div:nth-child(2) {
animation-delay: -0.3s;
}
#app-loading .ring div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes loading-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@ -1,9 +1,6 @@
:root { :root {
--darwin-titlebar-height: 36px;
--radius: 4px; --radius: 4px;
--titlebar-height: 36px;
--primary: #00008b;
--selection: rgba(0, 62, 205, 0.3);
} }
html, html,
@ -17,8 +14,8 @@ body {
-webkit-user-select: none; -webkit-user-select: none;
user-select: none; user-select: none;
cursor: default; cursor: default;
font-size: 15px; font-size: 13px;
line-height: 17px; line-height: 13px;
background-color: rgba(255, 255, 255, 0.45); background-color: rgba(255, 255, 255, 0.45);
} }
@ -45,7 +42,7 @@ p strong {
} }
a { a {
color: #0000cc; color: rgb(0, 0, 204);
text-decoration: underline; text-decoration: underline;
text-underline-offset: 2px; text-underline-offset: 2px;
text-decoration-thickness: 1px; text-decoration-thickness: 1px;
@ -69,8 +66,8 @@ select:disabled {
} }
.button { .button {
background-color: var(--primary); background-color: #00008b;
border: 1px solid var(--primary); border: 1px solid #00008b;
padding: 0.5rem; padding: 0.5rem;
border-radius: var(--radius); border-radius: var(--radius);
color: #fff; color: #fff;
@ -145,7 +142,7 @@ select:disabled {
.field > textarea, .field > textarea,
.field > select { .field > select {
flex: 1; flex: 1;
padding: 0.5rem; padding: 0 0.5rem;
border: 1px solid #ccc; border: 1px solid #ccc;
background-color: #fff; background-color: #fff;
appearance: none; appearance: none;
@ -154,8 +151,8 @@ select:disabled {
.field > textarea:focus, .field > textarea:focus,
.field > select:focus { .field > select:focus {
outline: none; outline: none;
border-color: var(--primary); border-color: #00008b;
box-shadow: 0 0 0 1px var(--primary); box-shadow: 0 0 0 3px rgba(0, 0, 139, 0.2);
} }
.field > input.invalid, .field > input.invalid,
.field > textarea.invalid, .field > textarea.invalid,
@ -218,8 +215,3 @@ code,
.flash-green { .flash-green {
animation: 1s ease-out 0s 1 flashGreen; animation: 1s ease-out 0s 1 flashGreen;
} }
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

@ -9,10 +9,10 @@ export default defineConfig({
plugins: [ svelte() ], plugins: [ svelte() ],
resolve: { resolve: {
alias: { alias: {
$components: currentDir + '/src/components', '$components': currentDir + '/src/components',
$organisms: currentDir + '/src/organisms', '$organisms': currentDir + '/src/organisms',
$wails: currentDir + '/wailsjs', '$wails': currentDir + '/wailsjs',
$lib: currentDir + '/src/lib', '$lib': currentDir + '/src/lib',
}, },
}, },
}); });

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

@ -1,6 +1,7 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
import {app} from '../models'; import {app} from '../models';
import {map[string]app} from '../models';
import {menu} from '../models'; import {menu} from '../models';
import {context} from '../models'; import {context} from '../models';
import {ui} from '../models'; import {ui} from '../models';
@ -9,12 +10,6 @@ export function AddHost(arg1:string):Promise<string>;
export function Aggregate(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string):Promise<void>; export function Aggregate(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string):Promise<void>;
export function AskConfirmation(arg1:string):Promise<boolean>;
export function ChooseDirectory(arg1:string):Promise<string>;
export function CountItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise<app.CountItemsResult>;
export function CreateIndex(arg1:string,arg2:string,arg3:string,arg4:string):Promise<string>; export function CreateIndex(arg1:string,arg2:string,arg3:string,arg4:string):Promise<string>;
export function DropCollection(arg1:string,arg2:string,arg3:string):Promise<boolean>; export function DropCollection(arg1:string,arg2:string,arg3:string):Promise<boolean>;
@ -23,8 +18,6 @@ export function DropDatabase(arg1:string,arg2:string):Promise<boolean>;
export function DropIndex(arg1:string,arg2:string,arg3:string,arg4:string):Promise<boolean>; export function DropIndex(arg1:string,arg2:string,arg3:string,arg4:string):Promise<boolean>;
export function DuplicateCollection(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string,arg6:string):Promise<boolean>;
export function Environment():Promise<app.EnvironmentInfo>; export function Environment():Promise<app.EnvironmentInfo>;
export function ExecuteShellScript(arg1:string,arg2:string,arg3:string,arg4:string):Promise<app.ExecuteShellScriptResult>; export function ExecuteShellScript(arg1:string,arg2:string,arg3:string,arg4:string):Promise<app.ExecuteShellScriptResult>;
@ -35,7 +28,7 @@ export function GetIndexes(arg1:string,arg2:string,arg3:string):Promise<app.GetI
export function HostLogs(arg1:string,arg2:string):Promise<app.HostLogsResult>; export function HostLogs(arg1:string,arg2:string):Promise<app.HostLogsResult>;
export function Hosts():Promise<{[key: string]: app.Host}>; export function Hosts():Promise<map[string]app.Host>;
export function InsertItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise<any>; export function InsertItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise<any>;
@ -47,8 +40,6 @@ export function OpenConnection(arg1:string):Promise<app.OpenConnectionResult>;
export function OpenDatabase(arg1:string,arg2:string):Promise<app.OpenDatabaseResult>; export function OpenDatabase(arg1:string,arg2:string):Promise<app.OpenDatabaseResult>;
export function OpenShellScript():Promise<string>;
export function PerformDump(arg1:string):Promise<boolean>; export function PerformDump(arg1:string):Promise<boolean>;
export function PerformFindExport(arg1:string,arg2:string,arg3:string,arg4:string):Promise<boolean>; export function PerformFindExport(arg1:string,arg2:string,arg3:string,arg4:string):Promise<boolean>;
@ -71,11 +62,7 @@ export function ReportSharedStateVariable(arg1:string,arg2:string):Promise<void>
export function SaveQuery(arg1:string):Promise<string>; export function SaveQuery(arg1:string):Promise<string>;
export function SaveShellOuput(arg1:string):Promise<void>; export function SavedQueries():Promise<map[string]app.SavedQuery>;
export function SaveShellScript(arg1:string,arg2:string,arg3:string,arg4:string,arg5:boolean):Promise<app.SaveShellScriptResult>;
export function SavedQueries():Promise<{[key: string]: app.SavedQuery}>;
export function Settings():Promise<app.Settings>; export function Settings():Promise<app.Settings>;

View File

@ -10,18 +10,6 @@ export function Aggregate(arg1, arg2, arg3, arg4, arg5) {
return window['go']['app']['App']['Aggregate'](arg1, arg2, arg3, arg4, arg5); return window['go']['app']['App']['Aggregate'](arg1, arg2, arg3, arg4, arg5);
} }
export function AskConfirmation(arg1) {
return window['go']['app']['App']['AskConfirmation'](arg1);
}
export function ChooseDirectory(arg1) {
return window['go']['app']['App']['ChooseDirectory'](arg1);
}
export function CountItems(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['CountItems'](arg1, arg2, arg3, arg4);
}
export function CreateIndex(arg1, arg2, arg3, arg4) { export function CreateIndex(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['CreateIndex'](arg1, arg2, arg3, arg4); return window['go']['app']['App']['CreateIndex'](arg1, arg2, arg3, arg4);
} }
@ -38,10 +26,6 @@ export function DropIndex(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['DropIndex'](arg1, arg2, arg3, arg4); return window['go']['app']['App']['DropIndex'](arg1, arg2, arg3, arg4);
} }
export function DuplicateCollection(arg1, arg2, arg3, arg4, arg5, arg6) {
return window['go']['app']['App']['DuplicateCollection'](arg1, arg2, arg3, arg4, arg5, arg6);
}
export function Environment() { export function Environment() {
return window['go']['app']['App']['Environment'](); return window['go']['app']['App']['Environment']();
} }
@ -86,10 +70,6 @@ export function OpenDatabase(arg1, arg2) {
return window['go']['app']['App']['OpenDatabase'](arg1, arg2); return window['go']['app']['App']['OpenDatabase'](arg1, arg2);
} }
export function OpenShellScript() {
return window['go']['app']['App']['OpenShellScript']();
}
export function PerformDump(arg1) { export function PerformDump(arg1) {
return window['go']['app']['App']['PerformDump'](arg1); return window['go']['app']['App']['PerformDump'](arg1);
} }
@ -134,14 +114,6 @@ export function SaveQuery(arg1) {
return window['go']['app']['App']['SaveQuery'](arg1); return window['go']['app']['App']['SaveQuery'](arg1);
} }
export function SaveShellOuput(arg1) {
return window['go']['app']['App']['SaveShellOuput'](arg1);
}
export function SaveShellScript(arg1, arg2, arg3, arg4, arg5) {
return window['go']['app']['App']['SaveShellScript'](arg1, arg2, arg3, arg4, arg5);
}
export function SavedQueries() { export function SavedQueries() {
return window['go']['app']['App']['SavedQueries'](); return window['go']['app']['App']['SavedQueries']();
} }

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

@ -4,6 +4,12 @@ import {context} from '../models';
export function Beep():Promise<void>; export function Beep():Promise<void>;
export function OpenDirectory(arg1:string):Promise<string>;
export function Reveal(arg1:string):Promise<void>; export function Reveal(arg1:string):Promise<void>;
export function StartProgressBar(arg1:number,arg2:string):Promise<void>;
export function Startup(arg1:context.Context):Promise<void>; export function Startup(arg1:context.Context):Promise<void>;
export function StopProgressBar(arg1:number):Promise<void>;

View File

@ -6,10 +6,22 @@ export function Beep() {
return window['go']['ui']['UI']['Beep'](); return window['go']['ui']['UI']['Beep']();
} }
export function OpenDirectory(arg1) {
return window['go']['ui']['UI']['OpenDirectory'](arg1);
}
export function Reveal(arg1) { export function Reveal(arg1) {
return window['go']['ui']['UI']['Reveal'](arg1); return window['go']['ui']['UI']['Reveal'](arg1);
} }
export function StartProgressBar(arg1, arg2) {
return window['go']['ui']['UI']['StartProgressBar'](arg1, arg2);
}
export function Startup(arg1) { export function Startup(arg1) {
return window['go']['ui']['UI']['Startup'](arg1); return window['go']['ui']['UI']['Startup'](arg1);
} }
export function StopProgressBar(arg1) {
return window['go']['ui']['UI']['StopProgressBar'](arg1);
}

View File

@ -225,11 +225,3 @@ export function Hide(): void;
// [Show](https://wails.io/docs/reference/runtime/intro#show) // [Show](https://wails.io/docs/reference/runtime/intro#show)
// Shows the application. // Shows the application.
export function Show(): void; export function Show(): void;
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
// Returns the current text stored on clipboard
export function ClipboardGetText(): Promise<string>;
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
// Sets a text on the clipboard
export function ClipboardSetText(text: string): Promise<boolean>;

View File

@ -37,11 +37,11 @@ export function LogFatal(message) {
} }
export function EventsOnMultiple(eventName, callback, maxCallbacks) { export function EventsOnMultiple(eventName, callback, maxCallbacks) {
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
} }
export function EventsOn(eventName, callback) { export function EventsOn(eventName, callback) {
return EventsOnMultiple(eventName, callback, -1); EventsOnMultiple(eventName, callback, -1);
} }
export function EventsOff(eventName, ...additionalEventNames) { export function EventsOff(eventName, ...additionalEventNames) {
@ -49,7 +49,7 @@ export function EventsOff(eventName, ...additionalEventNames) {
} }
export function EventsOnce(eventName, callback) { export function EventsOnce(eventName, callback) {
return EventsOnMultiple(eventName, callback, 1); EventsOnMultiple(eventName, callback, 1);
} }
export function EventsEmit(eventName) { export function EventsEmit(eventName) {
@ -192,11 +192,3 @@ export function Hide() {
export function Show() { export function Show() {
window.runtime.Show(); window.runtime.Show();
} }
export function ClipboardGetText() {
return window.runtime.ClipboardGetText();
}
export function ClipboardSetText(text) {
return window.runtime.ClipboardSetText(text);
}

70
go.mod
View File

@ -1,55 +1,61 @@
module github.com/garraflavatra/rolens module github.com/garraflavatra/rolens
go 1.23.5 go 1.18
require github.com/wailsapp/wails/v2 v2.9.2 require github.com/wailsapp/wails/v2 v2.3.1
require ( require (
github.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4 github.com/gen2brain/beeep v0.0.0-20220909211152-5a9ec94374f6
github.com/google/uuid v1.6.0 github.com/google/uuid v1.3.0
go.mongodb.org/mongo-driver v1.17.2 go.mongodb.org/mongo-driver v1.11.7
) )
require ( require (
github.com/leaanthony/u v1.1.1 // indirect github.com/akavel/rsrc v0.10.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f // indirect
github.com/wailsapp/go-webview2 v1.0.19 // indirect github.com/imdario/mergo v0.3.12 // indirect
github.com/josephspurrier/goversioninfo v1.4.0 // indirect
github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 // indirect
golang.org/x/image v0.7.0 // indirect
) )
require ( require (
github.com/bep/debounce v1.2.1 // indirect github.com/bep/debounce v1.2.1 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.1 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/compress v1.13.6 // indirect
github.com/labstack/echo/v4 v4.13.3 // indirect github.com/labstack/echo/v4 v4.9.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect github.com/labstack/gommon v0.3.1 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect github.com/leaanthony/go-ansi-parser v1.0.1 // indirect
github.com/leaanthony/gosod v1.0.4 // indirect github.com/leaanthony/gosod v1.0.3 // indirect
github.com/leaanthony/slicer v1.6.0 // indirect github.com/leaanthony/slicer v1.5.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.14 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/ncruces/zenity v0.10.9
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/samber/lo v1.49.1 // indirect github.com/samber/lo v1.27.1 // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
github.com/tkrajina/go-reflector v0.5.8 // indirect github.com/tkrajina/go-reflector v0.5.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/scram v1.1.1 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.3 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
golang.org/x/crypto v0.32.0 // indirect golang.org/x/crypto v0.1.0 // indirect
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/net v0.34.0 // indirect golang.org/x/net v0.7.0 // indirect
golang.org/x/sync v0.10.0 golang.org/x/sync v0.3.0
golang.org/x/sys v0.29.0 // indirect golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.9.0 // indirect
) )
// replace github.com/wailsapp/wails/v2 v2.3.1 => /Users/romeinvanburen/go/pkg/mod

170
go.sum
View File

@ -1,186 +1,131 @@
github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw=
github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f h1:OGqDDftRTwrvUoL6pOG7rYTmWsTCvyEWFsMjg+HcOaA=
github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f/go.mod h1:Dv9D0NUlAsaQcGQZa5kc5mqR9ua72SmA8VXi4cd+cBw=
github.com/gen2brain/beeep v0.0.0-20220909211152-5a9ec94374f6 h1:jFEK/SA/7E8lg9T33+y8D4Z0I782+bbiEjmyyklRzRQ= github.com/gen2brain/beeep v0.0.0-20220909211152-5a9ec94374f6 h1:jFEK/SA/7E8lg9T33+y8D4Z0I782+bbiEjmyyklRzRQ=
github.com/gen2brain/beeep v0.0.0-20220909211152-5a9ec94374f6/go.mod h1:/WeFVhhxMOGypVKS0w8DUJxUBbHypnWkUVnW7p5c9Pw= github.com/gen2brain/beeep v0.0.0-20220909211152-5a9ec94374f6/go.mod h1:/WeFVhhxMOGypVKS0w8DUJxUBbHypnWkUVnW7p5c9Pw=
github.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4 h1:ygs9POGDQpQGLJPlq4+0LBUmMBNox1N4JSpw+OETcvI=
github.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4/go.mod h1:0W7dI87PvXJ1Sjs0QPvWXKcQmNERY77e8l7GFhZB/s4=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8=
github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY=
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
github.com/leaanthony/go-ansi-parser v1.6.0 h1:T8TuMhFB6TUMIUm0oRrSbgJudTFw9csT3ZK09w0t4Pg= github.com/leaanthony/go-ansi-parser v1.0.1 h1:97v6c5kYppVsbScf4r/VZdXyQ21KQIfeQOk2DgKxGG4=
github.com/leaanthony/go-ansi-parser v1.6.0/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= github.com/leaanthony/go-ansi-parser v1.0.1/go.mod h1:7arTzgVI47srICYhvgUV4CGd063sGEeoSlych5yeSPM=
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ= github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ=
github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4= github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4=
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI= github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY=
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= 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/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/ncruces/zenity v0.10.9 h1:TYdNwEj9HiDDcpdsIUecBMsQw7L80Aiu/IJMM0Tao1E=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= 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 h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 h1:acNfDZXmm28D2Yg/c3ALnZStzNaZMSagpbr96vY6Zjc=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 h1:GranzK4hv1/pqTIhMTXt2X8MmMOuH3hMeUR0o9SP5yc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844/go.mod h1:T1TLSfyWVBRXVGzWd0o9BI4kfoO9InEgfQe4NV3mLz8=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= github.com/samber/lo v1.27.1 h1:sTXwkRiIFIQG+G0HeAvOEnGjqWeWtI9cg5/n51KrxPg=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/samber/lo v1.27.1/go.mod h1:it33p9UtPMS7z72fP4gw/EIfQB2eI8ke7GR2wc6+Rhg=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk= github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o= github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.1.0 h1:K3hMW5epkdAVwibsQEfR/7Zj0Qgt4DxtNumTq/VloO8=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tkrajina/go-reflector v0.5.5 h1:gwoQFNye30Kk7NrExj8zm3zFtrGPqOkzFMLuQZg1DtQ=
github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE= github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/wailsapp/go-webview2 v1.0.10 h1:PP5Hug6pnQEAhfRzLCoOh2jJaPdrqeRgJKZhyYyDV/w=
github.com/wailsapp/go-webview2 v1.0.10/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.7.1 h1:HAzp2c5ODOzsLC6ZMDVtNOB72ozM7/SJecJPB2Ur+UU= github.com/wailsapp/wails/v2 v2.3.1 h1:ZJz+pyIBKyASkgO8JO31NuHO1gTTHmvwiHYHwei1CqM=
github.com/wailsapp/wails/v2 v2.7.1/go.mod h1:oIJVwwso5fdOgprBYWXBBqtx6PaSvxg8/KTQHNGkadc= github.com/wailsapp/wails/v2 v2.3.1/go.mod h1:zlNLI0E2c2qA6miiuAHtp0Bac8FaGH0tlhA19OssR/8=
github.com/wailsapp/wails/v2 v2.9.2 h1:Xb5YRTos1w5N7DTMyYegWaGukCP2fIaX9WF21kPPF2k=
github.com/wailsapp/wails/v2 v2.9.2/go.mod h1:uehvlCwJSFcBq7rMCGfk4rxca67QQGsbg5Nm4m9UnBs=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E= github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs= github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= 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/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.11.7 h1:LIwYxASDLGUg/8wOhgOOZhX8tQa/9tgZPgzZoVqJvcs= 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.mongodb.org/mongo-driver v1.11.7/go.mod h1:G9TgswdsWjX4tmDA5zfs2+6AEPpYJwqblyjsfuh8oXY=
go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -194,31 +139,30 @@ golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -75,8 +75,8 @@ func NewApp(version string) *App {
panic(errors.New("unsupported platform")) panic(errors.New("unsupported platform"))
} }
os.MkdirAll(a.Env.DataDirectory, 0755) os.MkdirAll(a.Env.DataDirectory, os.ModePerm)
os.MkdirAll(a.Env.LogDirectory, 0755) os.MkdirAll(a.Env.LogDirectory, os.ModePerm)
return a return a
} }
@ -141,50 +141,3 @@ func (a *App) ReportSharedStateVariable(key, value string) {
a.State.Store(key, value) a.State.Store(key, value)
wailsRuntime.LogDebug(a.ctx, fmt.Sprintf("State: %s=\"%s\"", key, value)) wailsRuntime.LogDebug(a.ctx, fmt.Sprintf("State: %s=\"%s\"", key, value))
} }
func (a *App) AskConfirmation(message string) bool {
var title string = ""
if runtime.GOOS == "darwin" {
title = message
message = ""
} else {
title = "Confirm"
}
button, err := wailsRuntime.MessageDialog(a.ctx, wailsRuntime.MessageDialogOptions{
Type: wailsRuntime.QuestionDialog,
Title: title,
Message: message,
Buttons: []string{"Yes", "No"},
DefaultButton: "Yes",
CancelButton: "No",
})
if err == nil {
return button == "Yes"
} else {
return false
}
}
func (a *App) ChooseDirectory(title string) string {
if title == "" {
title = "Choose a directory"
}
dir, err := wailsRuntime.OpenDirectoryDialog(a.ctx, wailsRuntime.OpenDialogOptions{
Title: title,
DefaultDirectory: a.Env.DownloadDirectory,
CanCreateDirectories: true,
})
if err != nil {
wailsRuntime.MessageDialog(a.ctx, wailsRuntime.MessageDialogOptions{
Title: "Error while opening directory",
Message: err.Error(),
Type: wailsRuntime.ErrorDialog,
})
}
return dir
}

View File

@ -24,12 +24,12 @@ func (a *App) Menu() *menu.Menu {
appMenu := menu.NewMenu() appMenu := menu.NewMenu()
aboutMenu := appMenu.AddSubmenu("Rolens") aboutMenu := appMenu.AddSubmenu("Rolens")
aboutMenu.AddText("About Rolens", nil, menuCallbackEmit(a, "global.about")) aboutMenu.AddText("About Rolens", nil, menuCallbackEmit(a, "OpenAboutModal"))
aboutMenu.AddSeparator() aboutMenu.AddSeparator()
aboutMenu.AddText("Preferences…", keys.CmdOrCtrl(","), menuCallbackEmit(a, "global.settings")) aboutMenu.AddText("Preferences…", keys.CmdOrCtrl(","), menuCallbackEmit(a, "OpenPreferences"))
aboutMenu.AddSeparator() aboutMenu.AddSeparator()
aboutMenu.AddText("Open data directory", nil, func(cd *menu.CallbackData) { a.ui.Reveal(a.Env.DataDirectory) }) aboutMenu.AddText("Open data directory", nil, func(cd *menu.CallbackData) { a.ui.Reveal(a.Env.DataDirectory) })
aboutMenu.AddText("Open log directory", nil, func(cd *menu.CallbackData) { a.ui.Reveal(a.Env.LogDirectory) }) aboutMenu.AddText("Open log directory", nil, func(cd *menu.CallbackData) { a.ui.Reveal(a.Env.LogDirectory) })
aboutMenu.AddText("Purge logs…", nil, func(cd *menu.CallbackData) { a.PurgeLogDirectory() }) aboutMenu.AddText("Purge logs…", nil, func(cd *menu.CallbackData) { a.PurgeLogDirectory() })
aboutMenu.AddSeparator() aboutMenu.AddSeparator()
@ -47,10 +47,9 @@ func (a *App) Menu() *menu.Menu {
hostMenu.AddText("New…", keys.OptionOrAlt("C"), menuCallbackEmit(a, "ui.host.new")) hostMenu.AddText("New…", keys.OptionOrAlt("C"), menuCallbackEmit(a, "ui.host.new"))
hostMenu.AddText("Edit host…", keys.OptionOrAlt("H"), menuCallbackEmit(a, "ui.host.edit")) hostMenu.AddText("Edit host…", keys.OptionOrAlt("H"), menuCallbackEmit(a, "ui.host.edit"))
hostMenu.AddSeparator() hostMenu.AddSeparator()
hostMenu.AddText("Host status", nil, menuCallbackEmit(a, "ui.host.tab", "status")) hostMenu.AddText("Server status", nil, menuCallbackEmit(a, "ui.host.tab", "status"))
hostMenu.AddText("Shell", nil, menuCallbackEmit(a, "ui.host.tab", "shell"))
hostMenu.AddText("Logs", nil, menuCallbackEmit(a, "ui.host.tab", "logs"))
hostMenu.AddText("System info", nil, menuCallbackEmit(a, "ui.host.tab", "systemInfo")) hostMenu.AddText("System info", nil, menuCallbackEmit(a, "ui.host.tab", "systemInfo"))
hostMenu.AddText("Shell", nil, menuCallbackEmit(a, "ui.host.tab", "shell"))
hostMenu.AddSeparator() hostMenu.AddSeparator()
hostMenu.AddText("Remove host…", nil, menuCallbackEmit(a, "ui.host.remove")) hostMenu.AddText("Remove host…", nil, menuCallbackEmit(a, "ui.host.remove"))

View File

@ -3,6 +3,7 @@ package app
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"os"
"path" "path"
"github.com/wailsapp/wails/v2/pkg/runtime" "github.com/wailsapp/wails/v2/pkg/runtime"
@ -76,7 +77,7 @@ func (a *App) UpdateSettings(jsonData string) Settings {
} }
filePath := path.Join(a.Env.DataDirectory, "settings.json") filePath := path.Join(a.Env.DataDirectory, "settings.json")
err = ioutil.WriteFile(filePath, newJson, 0644) err = ioutil.WriteFile(filePath, newJson, os.ModePerm)
if err != nil { if err != nil {
runtime.LogErrorf(a.ctx, "Could not update host list: %s", err.Error()) runtime.LogErrorf(a.ctx, "Could not update host list: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{

View File

@ -1,12 +1,10 @@
package app package app
import ( import (
"context"
"fmt" "fmt"
"github.com/wailsapp/wails/v2/pkg/runtime" "github.com/wailsapp/wails/v2/pkg/runtime"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
) )
type OpenCollectionResult struct { type OpenCollectionResult struct {
@ -57,77 +55,6 @@ func (a *App) RenameCollection(hostKey, dbKey, collKey, newCollKey string) bool
return true return true
} }
func (a *App) DuplicateCollection(origHostKey, origDbKey, origCollKey, destHostKey, destDbKey, destCollKey string) bool {
runtime.LogDebugf(a.ctx, "Duplicating collection %s:%s.%s to %s:%s.%s", origHostKey, origDbKey, origCollKey, destHostKey, destDbKey, destCollKey)
if (origHostKey == destHostKey) && (origDbKey == destDbKey) && (origCollKey == destCollKey) {
runtime.LogInfof(a.ctx, "Duplicating collection: cannot duplicate to the same collection")
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Type: runtime.WarningDialog,
Title: "Error while duplicating collection",
Message: "The new collection name cannot equal the old one!",
})
return false
}
origHost, origCtx, origClose, err := a.connectToHost(origHostKey)
if err != nil {
return false
}
defer origClose()
var destHost *mongo.Client
var destCtx context.Context
if origHostKey != destHostKey {
var destClose func()
destHost, destCtx, destClose, err = a.connectToHost(destHostKey)
if err != nil {
return false
}
defer destClose()
} else {
destHost = origHost
destCtx = origCtx
}
destColl := destHost.Database(destDbKey).Collection(destCollKey)
origCur, err := origHost.Database(origDbKey).Collection(origCollKey).Find(origCtx, bson.D{})
if err != nil {
runtime.LogInfof(a.ctx, "Duplicating collection: could not create origin cursor: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Type: runtime.WarningDialog,
Title: "Error while duplicating collection",
Message: "Could not create origin cursor: " + err.Error(),
})
return false
}
var n int64 = 0
var ok = true
for origCur.Next(origCtx) {
_, err := destColl.InsertOne(destCtx, origCur.Current)
if err != nil {
ok = false
runtime.LogInfof(a.ctx, "Duplicating collection: could not insert item %d: %s", n, err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Type: runtime.WarningDialog,
Title: "Error while duplicating collection",
Message: fmt.Sprintf("Could not insert item %d: %s", n, err.Error()),
})
}
n += 1
}
return ok
}
func (a *App) TruncateCollection(hostKey, dbKey, collKey string) bool { func (a *App) TruncateCollection(hostKey, dbKey, collKey string) bool {
choice, _ := runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ choice, _ := runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Confirm", Title: "Confirm",

View File

@ -1,9 +1,7 @@
package app package app
import ( import (
"context"
"encoding/json" "encoding/json"
"time"
"github.com/wailsapp/wails/v2/pkg/runtime" "github.com/wailsapp/wails/v2/pkg/runtime"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
@ -25,11 +23,6 @@ type FindItemsResult struct {
ErrorDescription string `json:"errorDescription"` ErrorDescription string `json:"errorDescription"`
} }
type CountItemsResult struct {
Total int64 `json:"total"`
Error string `json:"error"`
}
func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) (result FindItemsResult) { func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) (result FindItemsResult) {
var form Query var form Query
@ -82,14 +75,12 @@ func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) (result FindIt
Sort: sort, Sort: sort,
} }
ctx2, cancel := context.WithDeadline(ctx, time.Now().Add(time.Second)) total, err := client.Database(dbKey).Collection(collKey).CountDocuments(ctx, query, nil)
defer cancel() if err != nil {
total, err := client.Database(dbKey).Collection(collKey).CountDocuments(ctx2, query, nil)
if err == nil {
result.Total = total
} else {
result.Total = -1
runtime.LogWarningf(a.ctx, "Encountered an error while counting documents: %s", err.Error()) runtime.LogWarningf(a.ctx, "Encountered an error while counting documents: %s", err.Error())
result.ErrorTitle = "Error while counting documents"
result.ErrorDescription = err.Error()
return
} }
cur, err := client.Database(dbKey).Collection(collKey).Find(ctx, query, &opt) cur, err := client.Database(dbKey).Collection(collKey).Find(ctx, query, &opt)
@ -111,6 +102,7 @@ func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) (result FindIt
return return
} }
result.Total = total
result.Results = make([]string, 0) result.Results = make([]string, 0)
for _, r := range results { for _, r := range results {
@ -127,34 +119,6 @@ func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) (result FindIt
return return
} }
func (a *App) CountItems(hostKey, dbKey, collKey, query string) (result CountItemsResult) {
var q bson.M
err := bson.UnmarshalExtJSON([]byte(query), true, &q)
if err != nil {
runtime.LogInfof(a.ctx, "Invalid count query: %s", err.Error())
result.Error = "Invalid query"
return
}
client, ctx, close, err := a.connectToHost(hostKey)
if err != nil {
return
}
defer close()
ctx, _ = context.WithDeadline(ctx, time.Now().Add(2*time.Minute))
total, err := client.Database(dbKey).Collection(collKey).CountDocuments(ctx, q, nil)
if err == nil {
result.Total = total
} else {
result.Total = -1
result.Error = err.Error()
runtime.LogWarningf(a.ctx, "Encountered an error while counting documents: %s", err.Error())
}
return
}
func (a *App) UpdateFoundDocument(hostKey, dbKey, collKey, idJson, newDocJson string) bool { func (a *App) UpdateFoundDocument(hostKey, dbKey, collKey, idJson, newDocJson string) bool {
var id bson.M var id bson.M
if err := bson.UnmarshalExtJSON([]byte(idJson), true, &id); err != nil { if err := bson.UnmarshalExtJSON([]byte(idJson), true, &id); err != nil {

View File

@ -3,6 +3,7 @@ package app
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"os"
"path" "path"
"github.com/wailsapp/wails/v2/pkg/runtime" "github.com/wailsapp/wails/v2/pkg/runtime"
@ -24,7 +25,7 @@ func updateQueryFile(a *App, newData map[string]SavedQuery) error {
return err return err
} }
err = ioutil.WriteFile(filePath, jsonData, 0644) err = ioutil.WriteFile(filePath, jsonData, os.ModePerm)
return err return err
} }

View File

@ -6,7 +6,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"path" "path"
"strings"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/wailsapp/wails/v2/pkg/runtime" "github.com/wailsapp/wails/v2/pkg/runtime"
@ -14,19 +13,11 @@ import (
type ExecuteShellScriptResult struct { type ExecuteShellScriptResult struct {
Output string `json:"output"` Output string `json:"output"`
Stderr string `json:"stderr"`
Status int `json:"status"` Status int `json:"status"`
ErrorTitle string `json:"errorTitle"` ErrorTitle string `json:"errorTitle"`
ErrorDescription string `json:"errorDescription"` ErrorDescription string `json:"errorDescription"`
} }
type SaveShellScriptResult struct {
Host Host `json:"host"`
Fname string `json:"filename"`
ErrorTitle string `json:"errorTitle"`
ErrorDescription string `json:"errorDescription"`
}
func (a *App) ExecuteShellScript(hostKey, dbKey, collKey, script string) (result ExecuteShellScriptResult) { func (a *App) ExecuteShellScript(hostKey, dbKey, collKey, script string) (result ExecuteShellScriptResult) {
if !a.Env.HasMongoShell { if !a.Env.HasMongoShell {
result.ErrorTitle = "mongosh not found" result.ErrorTitle = "mongosh not found"
@ -34,40 +25,8 @@ func (a *App) ExecuteShellScript(hostKey, dbKey, collKey, script string) (result
return return
} }
saveRes := a.SaveShellScript(hostKey, dbKey, collKey, script, true)
if (saveRes.ErrorTitle != "") || (saveRes.ErrorDescription != "") {
result.ErrorTitle = saveRes.ErrorTitle
result.ErrorDescription = saveRes.ErrorDescription
return
}
var outbuf, errbuf strings.Builder
cmd := exec.Command("mongosh", "--file", saveRes.Fname, saveRes.Host.URI)
cmd.Stdout = &outbuf
cmd.Stderr = &errbuf
err := cmd.Run()
if exiterr, ok := err.(*exec.ExitError); ok {
result.Status = exiterr.ExitCode()
} else if err != nil {
runtime.LogWarningf(a.ctx, "Shell: failed to execute: mongosh --file %s: %s", saveRes.Fname, err.Error())
result.ErrorTitle = "mongosh failure"
result.ErrorDescription = err.Error()
return
} else {
result.Status = 0
}
os.Remove(saveRes.Fname)
result.Output = outbuf.String()
result.Stderr = errbuf.String()
return
}
func (a *App) SaveShellScript(hostKey, dbKey, collKey, script string, temp bool) (result SaveShellScriptResult) {
hosts, err := a.Hosts() hosts, err := a.Hosts()
if err != nil { if err != nil {
runtime.LogWarningf(a.ctx, "Shell: could not get hosts: %s", err.Error())
result.ErrorTitle = "Could not get hosts" result.ErrorTitle = "Could not get hosts"
result.ErrorDescription = err.Error() result.ErrorDescription = err.Error()
return return
@ -75,12 +34,10 @@ func (a *App) SaveShellScript(hostKey, dbKey, collKey, script string, temp bool)
host, hostFound := hosts[hostKey] host, hostFound := hosts[hostKey]
if !hostFound { if !hostFound {
runtime.LogWarningf(a.ctx, "Shell: host %s does not exist", host)
result.ErrorTitle = "The specified host does not seem to exist" result.ErrorTitle = "The specified host does not seem to exist"
return return
} }
result.Host = host
id, err := uuid.NewRandom() id, err := uuid.NewRandom()
if err != nil { if err != nil {
runtime.LogErrorf(a.ctx, "Shell: failed to generate a UUID: %s", err.Error()) runtime.LogErrorf(a.ctx, "Shell: failed to generate a UUID: %s", err.Error())
@ -89,36 +46,15 @@ func (a *App) SaveShellScript(hostKey, dbKey, collKey, script string, temp bool)
return return
} }
if temp { dirname := path.Join(a.Env.DataDirectory, "Shell Scripts")
dirname, err := os.MkdirTemp(os.TempDir(), "rolens-script") fname := path.Join(dirname, fmt.Sprintf("%s.mongosh.js", id.String()))
if err != nil {
runtime.LogErrorf(a.ctx, "Shell: failed to create temporary directory: %s", err.Error())
result.ErrorTitle = "Could not generate temporary directory for script"
result.ErrorDescription = err.Error()
return
}
result.Fname = path.Join(dirname, fmt.Sprintf("%s.mongosh.js", id.String()))
} else {
result.Fname, err = runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
DefaultFilename: "New Script.js",
DefaultDirectory: path.Join(a.Env.DataDirectory, "Shell Scripts"),
Title: "Save MongoDB Shell Script",
CanCreateDirectories: true,
Filters: []runtime.FileFilter{
{
DisplayName: "MongoDB Shell Script (*.js)",
Pattern: "*.js",
},
},
})
if err != nil { if err := os.MkdirAll(dirname, os.ModePerm); err != nil {
runtime.LogErrorf(a.ctx, "Shell: failed to save script: %s", err.Error()) runtime.LogWarningf(a.ctx, "Shell: failed to mkdir %s", err.Error())
result.ErrorTitle = "Could not save shell script" result.ErrorTitle = "Could not create temporary directory"
result.ErrorDescription = err.Error() result.ErrorDescription = err.Error()
return return
} }
}
scriptHeader := fmt.Sprintf("// Namespace: %s.%s\n", dbKey, collKey) scriptHeader := fmt.Sprintf("// Namespace: %s.%s\n", dbKey, collKey)
@ -139,73 +75,31 @@ func (a *App) SaveShellScript(hostKey, dbKey, collKey, script string, temp bool)
scriptHeader = scriptHeader + fmt.Sprintf("coll = db.getCollection('%s');\n", collKey) scriptHeader = scriptHeader + fmt.Sprintf("coll = db.getCollection('%s');\n", collKey)
} }
scriptHeader = scriptHeader + "\n" scriptHeader = scriptHeader + "\n// Start of user script\n"
script = scriptHeader + strings.TrimLeft(strings.TrimRight(script, " \t\n"), "\n") script = scriptHeader + script
if err := os.WriteFile(result.Fname, []byte(script), 0755); err != nil { if err := os.WriteFile(fname, []byte(script), os.ModePerm); err != nil {
runtime.LogWarningf(a.ctx, "Shell: failed to write script to %s: %s", result.Fname, err.Error()) runtime.LogWarningf(a.ctx, "Shell: failed to write script to %s", err.Error())
result.ErrorTitle = "Could not create temporary script file" result.ErrorTitle = "Could not create temporary script file"
result.ErrorDescription = err.Error() result.ErrorDescription = err.Error()
return return
} }
cmd := exec.Command("mongosh", "--file", fname, host.URI)
stdout, err := cmd.Output()
if exiterr, ok := err.(*exec.ExitError); ok {
result.Status = exiterr.ExitCode()
} else if err != nil {
runtime.LogWarningf(a.ctx, "Shell: failed to execute: mongosh --file %s: %s", fname, err.Error())
result.ErrorTitle = "Could not execute script"
result.ErrorDescription = err.Error()
return
} else {
result.Status = 0
}
os.Remove(fname)
result.Output = string(stdout)
return return
} }
func (a *App) OpenShellScript() string {
dir := path.Join(a.Env.DataDirectory, "Shell Scripts")
os.MkdirAll(dir, 0755)
fname, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
DefaultDirectory: path.Join(a.Env.DataDirectory, "Shell Scripts"),
Title: "Load a MongoDB Shell Script",
CanCreateDirectories: true,
Filters: []runtime.FileFilter{
{
DisplayName: "MongoDB Shell Script (*.js)",
Pattern: "*.js",
},
},
})
if err != nil {
runtime.LogWarningf(a.ctx, "Shell: error opening script: %s", err.Error())
return ""
}
script, err := os.ReadFile(fname)
if err != nil {
runtime.LogWarningf(a.ctx, "Shell: error reading script %s: %s", fname, err.Error())
return ""
}
return string(script)
}
func (a *App) SaveShellOuput(output string) {
fname, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
DefaultFilename: "mongosh-output.txt",
DefaultDirectory: a.Env.DownloadDirectory,
Title: "Save mongosh output",
CanCreateDirectories: true,
})
if err != nil {
runtime.LogWarningf(a.ctx, "Shell: error exporting output to %s: %s", fname, err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error exporting output",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
}
if err := os.WriteFile(fname, []byte(output), 0755); err != nil {
runtime.LogWarningf(a.ctx, "Shell: error writing shell output to %s: %s", fname, err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error writing shell output",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
}
}

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