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

50 Commits

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

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

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

    Add filter functionality

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

    Renamed `form-row` class to `formrow`

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

    Hide object types in object grid

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

    Make auto reload interval input smaller

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

    Implement logs autoreload

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

    Return on error

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

    Add log error handling

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

    Update log tab icon

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

    Add host log panel
2023-07-01 20:30:43 +02:00
0b9f23365b Add app name and version to mongo connections 2023-07-01 10:35:41 +02:00
84f1b85356 Attempt to fix build.js 2023-06-30 20:09:36 +02:00
1ecfa0ab20 Prepared 0.2.1 2023-06-29 14:40:48 +02:00
823ae1328b Added some PR talk 2023-06-29 14:37:39 +02:00
eba7e84fc8 Fix "edit this page on GH" link on documentation website 2023-06-29 13:58:39 +02:00
3dbf8fc98b Update some links in documentation 2023-06-29 10:16:29 +02:00
88a5b961b1 Fixed heading levels in advanced build document 2023-06-29 10:12:18 +02:00
ce9934faa7 Update shortcuts (fixes #51) 2023-06-29 09:50:18 +02:00
e53019a4b0 Hide title bar overflow (fixes #52) 2023-06-28 18:00:50 +02:00
05bfd54fb5 Added a build script to simplify compiling Rolens from source 2023-06-28 17:58:03 +02:00
1c95b86475 Patched a flaw in ci_bundle.sh 2023-06-28 15:41:57 +02:00
aa53d0c400 Generate installer for Windows using NSIS 2023-06-28 12:44:56 +02:00
8a7518532d Exclude wails_tools.nsh from version control 2023-06-27 17:32:09 +02:00
0e4c5d474b CSS: renamed .btn to .button 2023-06-27 17:21:54 +02:00
685ea78376 Updated changelog 2023-06-26 21:22:23 +02:00
7cd68d3280 Excel export feature (#46) 2023-06-26 21:20:12 +02:00
cab19e8a8b Debug Excel export script 2023-06-26 21:18:29 +02:00
cae29d9a71 Fixed export to Excel feature, it works now 2023-06-26 21:18:29 +02:00
3b4be3ebf6 Experimental Excel export feature (WIP) 2023-06-26 21:18:29 +02:00
efcc78e3bb Use new loading interface for collection indexes 2023-06-26 21:09:28 +02:00
9acb89205d Improved the app menu 2023-06-26 21:05:05 +02:00
5191086d74 Disabled Dependabot for a while, generates too many PRs 2023-06-26 17:34:38 +02:00
755b96762d Disabled Dependabot for a while, generates too many PRs 2023-06-26 17:33:19 +02:00
e6b0040821 Debug Excel export script 2023-06-26 11:16:04 +02:00
3055f8173e Fixed export to Excel feature, it works now 2023-06-25 22:00:53 +02:00
6cb6e209eb Experimental Excel export feature (WIP) 2023-06-25 20:29:28 +02:00
79568bc5f4 Make table headers stick to the top 2023-06-25 16:11:51 +02:00
86639f766d Remove temporary script file when it has been used 2023-06-25 15:38:06 +02:00
d1ce48c773 Changed script file extension 2023-06-25 10:21:30 +02:00
b0d3e31378 Made shell available for hosts and databases (besides collections) 2023-06-25 08:49:57 +02:00
8bebdfccd8 Small frontend fixes 2023-06-25 08:16:31 +02:00
f0ab5288f7 Hide tabs scrollbar when not necessary 2023-06-24 22:10:47 +02:00
aaed7a59f7 Add window menu for Mac 2023-06-24 22:10:25 +02:00
f5366a9ad5 Added ulimit note 2023-06-24 22:00:29 +02:00
67b820ad47 Fixed host editing bug 2023-06-24 20:43:02 +02:00
cb89b5923f Improved error logging and dialogs 2023-06-24 20:27:48 +02:00
b73b5f4485 [ImgBot] Optimize images (#45)
Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
2023-06-24 17:57:57 +02:00
8ccd1a4ce0 Updated readme screenshot once again 2023-06-24 16:06:47 +02:00
eef74b306b Made it possible to input loose JSON into find view inputs
I.e. `{ key: 'val' }` or `{ 'num': 2 }` besides `{ "strict": "json" }`.
2023-06-24 16:05:33 +02:00
3e293910ea Open dump in Explorer/Finder when finished (fixes #43) 2023-06-24 15:50:46 +02:00
8f81e66d17 Renamed dump.go to database_dump.go 2023-06-24 15:40:15 +02:00
48b26c9df5 Parse connection URI correctly 2023-06-24 11:26:02 +02:00
958a0197a4 Connect to mongosh using the right credentials 2023-06-24 11:05:19 +02:00
3893c8dd06 CSS tweaks for shell 2023-06-24 11:05:02 +02:00
51897adf8d Show spinner 2023-06-24 10:28:49 +02:00
c284cb4cfc Started working on shell feature (WIP) 2023-06-24 10:11:32 +02:00
107 changed files with 2550 additions and 784 deletions

View File

@ -72,6 +72,9 @@ jobs:
name: rolens-${{ matrix.platform }}
path: releases/*
- name: Test build script for users
run: node ./build.js
bundle:
name: Bundle artifacts
runs-on: ubuntu-22.04

4
.gitignore vendored
View File

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

View File

@ -1,3 +1,17 @@
## [Unreleased]
* Added log view (#53, #54).
## [v0.2.2]
* Added Excel export format (#46).
* Improved the application menu.
* Improved error logging and dialogs.
* Made table headers stick to the top.
* Made it possible (again) to input loose JSON into find view inputs, i.e. `{ key: 'val' }` or `{ 'num': 2 }` besides `{ "strict": "json" }`.
* Fixed host editing bug.
* Open dump in Explorer/Finder when finished (#43).
## [v0.2.1]
* Display host and database statistics generated by the corresponding diagnostic MongoDB commands in addition to collection stats (#15).
@ -17,6 +31,8 @@
Initial release.
[Unreleased]: https://github.com/garraflavatra/rolens/tree/main
[v0.1.0]: https://github.com/garraflavatra/rolens/releases/tag/v0.1.0
[v0.2.0]: https://github.com/garraflavatra/rolens/releases/tag/v0.2.0
[v0.2.1]: https://github.com/garraflavatra/rolens/releases/tag/v0.2.1
[v0.2.2]: https://github.com/garraflavatra/rolens/releases/tag/v0.2.2

View File

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

View File

@ -36,7 +36,9 @@ You can obtain a pre-compiled Rolens binary for macOS or installer for Windows f
### Compiling from source
Please refer to [the documentation](https://garraflavatra.github.io/rolens/installation/) for detailed compilation instructions.
If you have Node.js installed, just download the source from GitHub, and run `node ./build.js`. The install script will check that dependencies are present and build Rolens for you.
If you want to build it yourself, please refer to the [advanced build process documentation](https://garraflavatra.github.io/rolens/development/advanced-build/) for detailed compilation instructions.
## User guide
@ -50,6 +52,31 @@ Rolens is designed to be as intuitive as possible. But if something is unclear n
Feel free to contact me if you have questions! [Send an e-mail.](mailto:romein@vburen.nl)
## Feature list
At this point, Rolens is comparable to MongoHub regarding features. It cannot handle things like user management _yet_, but it _does_ have:
* Connecting to hosts
- Host status
- System info
* Database management
- See stats
- Create dumps with `mongodump`
* Collections
- See stats
- Find, insert, update, & remove
- Save queries to reuse them
- Customizable table view for query results
- Versatile forms to enter data in a standardized format
- Aggregation pipeline editor
- Fully customizable export to a number of formats like JSON, CSV, and Excel
- Index editor
## Wishlist
* User management
* Shell _([under development](https://github.com/garraflavatra/rolens/pull/44))_
## Author and license
© [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.
@ -57,5 +84,6 @@ Feel free to contact me if you have questions! [Send an e-mail.](mailto:romein@v
## Credits
* [Wails](https://wails.io/) facilitates the build process for multiple OSes.
* The installer for Windows is generated by [NSIS](https://nsis.sourceforge.io/Main_Page).
* Icons are from [Feather Icons](https://feathericons.com/) by [Cole Bemis](https://github.com/colebemis).
* Vector drawings come from [unDraw](https://undraw.co/).

161
build.js Executable file
View File

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

View File

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

View File

@ -1,5 +1,4 @@
#!/bin/sh
#
# This script bundles the binaries generated by GitHub Actions.
#
@ -32,10 +31,10 @@ version=$(<./build/version.txt)
mkdir bundle
# macOS binaries
mv artifacts/*/rolens-macos-11-amd64.tar.gz bundle/rolens-$version-macos-amd64.tar.gz
mv artifacts/*/rolens-macos-11-arm64.tar.gz bundle/rolens-$version-macos-arm64.tar.gz
# macOS apps
mv artifacts/*/rolens-macos-11-amd64.tar.gz "bundle/rolens-$version-macos-11+-amd64.tar.gz"
mv artifacts/*/rolens-macos-11-arm64.tar.gz "bundle/rolens-$version-macos-11+-arm64.tar.gz"
# Windows binaries
mv artifacts/*/rolens-windows-2019-amd64.zip bundle/rolens-$version-windows-amd64.zip
mv artifacts/*/rolens-windows-2019-arm64.zip bundle/rolens-$version-windows-arm64.zip
# Windows installers
mv artifacts/*/rolens-windows-2019-amd64-installer.zip "bundle/rolens-$version-windows-10+-amd64-installer.zip"
mv artifacts/*/rolens-windows-2019-arm64-installer.zip "bundle/rolens-$version-windows-10+-arm64-installer.zip"

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 451 KiB

After

Width:  |  Height:  |  Size: 462 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@
<main class:empty={showWelcomeScreen}>
{#if showWelcomeScreen}
<BlankState label="Welcome to Rolens!" image="/logo.png" pale={false} big={true}>
<button class="btn" on:click={createFirstHost}>Add your first host</button>
<button class="button" on:click={createFirstHost}>Add your first host</button>
</BlankState>
{:else}
<Connection />
@ -72,6 +72,7 @@
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
#root.platform-darwin .titlebar {
height: var(--darwin-titlebar-height);
@ -102,7 +103,7 @@
overflow: scroll;
}
.btn.create {
.button.create {
margin-top: 0.5rem;
}
</style>

View File

@ -53,7 +53,7 @@
margin-bottom: -1.85rem;
}
.blankstate :global(.btn) {
.blankstate :global(.button) {
font-size: 1.35rem;
padding: 1rem;
}

View File

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

View File

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

View File

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

View File

@ -30,7 +30,9 @@
function refresh(hideObjectIndicators, items) {
_items = objectToArray(items).map(item => {
item.children = objectToArray(item.children);
if (item.children) {
item.children = objectToArray(item.children);
}
return item;
});

View File

@ -35,7 +35,7 @@
<!-- {#if actions?.length}
<div class="actions">
{#each actions as action}
<button class="btn" on:click={action.fn} disabled={action.disabled}>
<button class="button" on:click={action.fn} disabled={action.disabled}>
{#if action.icon}<Icon name={action.icon} />{/if}
{action.label || ''}
</button>
@ -107,13 +107,15 @@
background-color: #fff;
}
table thead {
border-bottom: 2px solid #ccc;
}
th {
thead th {
font-weight: 600;
text-align: left;
padding: 2px;
/* border-bottom: 2px solid #ccc; */
box-shadow: 0 2px #ccc;
background-color: #fff;
position: sticky;
top: 0;
}
.grid :global(.blankstate) {

View File

@ -1,5 +1,10 @@
<script>
export let name = '';
export let spin = false;
if (name === 'loading') {
spin = true;
}
</script>
<style>
@ -9,7 +14,7 @@
margin-right: 2px;
}
:global(.btn) svg {
:global(.button) svg {
height: 13px;
width: auto;
vertical-align: bottom;
@ -35,7 +40,7 @@
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class:spinning={name === 'loading'}
class:spinning={spin}
>
{#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>
@ -142,5 +147,9 @@
<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>

View File

@ -5,7 +5,6 @@
<script>
import { Beep } from '$wails/go/ui/UI';
import { createEventDispatcher } from 'svelte';
import { fade, fly } from 'svelte/transition';
import Icon from './icon.svelte';
export let show = true;
@ -43,12 +42,12 @@
<svelte:window on:keydown={keydown} />
{#if show}
<div class="modal outer" transition:fade on:pointerdown|self={Beep}>
<div class="inner" style:max-width={width || '80vw'} transition:fly={{ y: -100 }}>
<div class="modal outer" on:pointerdown|self={Beep}>
<div class="inner" style:max-width={width || '80vw'}>
{#if title}
<header>
<div class="title">{title}</div>
<button class="btn close" on:click={close} title="close" type="button">
<button class="button close" on:click={close} title="close" type="button">
<Icon name="x" />
</button>
</header>

View File

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

View File

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

View File

@ -7,6 +7,7 @@
import ObjectEditor from './objecteditor.svelte';
export let data;
export let readonly = false;
export let saveable = false;
export let successMessage = '';
@ -37,21 +38,21 @@
{#if data}
<Modal bind:show={data} contentPadding={false}>
<div class="objectviewer">
<ObjectEditor bind:text on:updated={() => successMessage = ''} />
<ObjectEditor bind:text on:updated={() => successMessage = ''} {readonly} />
</div>
<svelte:fragment slot="footer">
{#if saveable}
<button class="btn" on:click={save} disabled={invalid}>
<button class="button" on:click={save} disabled={invalid}>
<Icon name="save" /> Save
</button>
{/if}
<button class="btn secondary" on:click={close}>
<button class="button secondary" on:click={close}>
<Icon name="x" /> Close
</button>
<button class="btn secondary" on:click={copy}>
<button class="button secondary" on:click={copy}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} /> Copy
</button>

View File

@ -55,7 +55,7 @@
<style>
ul {
overflow-x: scroll;
overflow-x: auto;
display: flex;
list-style: none;
}

View File

@ -32,8 +32,8 @@
</form>
<svelte:fragment slot="footer">
<button on:click={submit} class="btn">OK</button>
<button on:click={close} class="btn secondary">Cancel</button>
<button on:click={submit} class="button">OK</button>
<button on:click={close} class="button secondary">Cancel</button>
</svelte:fragment>
</Modal>

View File

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

View File

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

View File

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

View File

@ -16,7 +16,9 @@ import {
DropCollection,
DropDatabase,
DropIndex,
ExecuteShellScript,
GetIndexes,
HostLogs,
Hosts,
OpenCollection,
OpenConnection,
@ -150,30 +152,22 @@ async function refresh() {
};
collection.truncate = async function() {
const progress = startProgress(`Truncating collection "${collKey}"…`);
await TruncateCollection(hostKey, dbKey, collKey);
await refresh();
progress.end();
};
collection.drop = async function() {
const progress = startProgress(`Dropping collection "${collKey}"…`);
const success = await DropCollection(hostKey, dbKey, collKey);
if (success) {
await refresh();
}
progress.end();
};
collection.getIndexes = async function() {
const progress = startProgress(`Retrieving indexes of "${collKey}"…`);
collection.indexes = [];
const { indexes, error } = await GetIndexes(hostKey, dbKey, collKey);
if (error) {
progress.end();
return error;
}
@ -187,16 +181,12 @@ async function refresh() {
};
index.drop = async function() {
const progress = startProgress(`Dropping index ${index.name}`);
const hasBeenDropped = await DropIndex(hostKey, dbKey, collKey, index.name);
progress.end();
return hasBeenDropped;
};
collection.indexes.push(index);
}
progress.end();
};
collection.getIndexByName = function(indesName) {
@ -208,14 +198,10 @@ async function refresh() {
return new Promise(resolve => {
dialog.$on('create', async event => {
const progress = startProgress('Creating index…');
const newIndexName = await CreateIndex(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(event.detail.index));
if (newIndexName) {
dialog.$close();
}
progress.end();
resolve(newIndexName);
});
});
@ -239,6 +225,11 @@ async function refresh() {
});
});
};
collection.executeShellScript = async function(script) {
const result = await ExecuteShellScript(hostKey, dbKey, collKey, script);
return result;
};
}
await refresh();
@ -277,19 +268,42 @@ async function refresh() {
await database.open();
}
};
database.executeShellScript = async function(script) {
const result = await ExecuteShellScript(hostKey, dbKey, '', script);
return result;
};
}
host.newDatabase = async function() {
const name = await dialogs.enterText('Create a database', 'Enter the database name. Note: databases in MongoDB do not exist until they have a collection and an item. Your new database will not persist on the server; fill it to have it created.', '');
if (name) {
host.databases[name] = { key: name, new: true };
await host.open();
}
};
await refresh();
};
host.executeShellScript = async function(script) {
const result = await ExecuteShellScript(hostKey, '', '', script);
return result;
};
host.newDatabase = async function() {
const name = await dialogs.enterText('Create a database', 'Enter the database name. Note: databases in MongoDB do not exist until they have a collection and an item. Your new database will not persist on the server; fill it to have it created.', '');
if (name) {
host.databases[name] = { key: name, new: true };
await host.open();
}
};
host.edit = async function() {
const dialog = dialogs.new(HostDetailDialog, { hostKey });
return new Promise(resolve => {
dialog.$on('close', () => {
refresh().then(resolve);
});
});
};
host.getLogs = async function(filter = 'global') {
return await HostLogs(hostKey, filter);
};
host.remove = async function() {
await RemoveHost(hostKey);
await refresh();

View File

@ -61,7 +61,7 @@
<option {value}>{value}</option>
{/each}
</select>
<button class="btn secondary" type="button" on:click={() => openStageDocs(stage.type)} title="Open documentation about {stage.type || 'this stage'} on mongodb.org">
<button class="button secondary" type="button" on:click={() => openStageDocs(stage.type)} title="Open documentation about {stage.type || 'this stage'} on mongodb.org">
<Icon name="?" />
</button>
</label>
@ -93,17 +93,17 @@
<div class="controls">
<div>
<button class="btn" type="submit" disabled={invalid}>
<button class="button" type="submit" disabled={invalid}>
<Icon name="play" /> Run pipeline
</button>
<button class="btn" type="button" on:click={() => settingsModalOpen = true}>
<button class="button" type="button" on:click={() => settingsModalOpen = true}>
<Icon name="cog" /> Settings
</button>
</div>
</div>
</form>
<Modal title="Advanced aggregation settings" bind:show={settingsModalOpen}>
<Modal title="Advanced aggregation settings" show={settingsModalOpen} on:close={() => settingsModalOpen = false}>
<div class="settinggrid">
<label for="allowDiskUse">Allow disk use</label>
<div class="field">

View File

@ -5,7 +5,7 @@
import { createEventDispatcher } from 'svelte';
export let collection;
export let query = undefined;
export let query = {};
const dispatch = createEventDispatcher();
const exportInfo = { ...query, viewKey: collection.viewKey };
@ -30,9 +30,10 @@
<label class="field">
<span class="label">Format</span>
<select bind:value={exportInfo.format}>
<option value="jsonarray">JSON array</option>
<option value="ndjson">Newline delimited JSON</option>
<option value="csv">CSV</option>
<option value="jsonarray">JSON array (*.json)</option>
<option value="ndjson">Newline delimited JSON (*.ndjson)</option>
<option value="csv">CSV (*.csv)</option>
<option value="excel">Excel (*.xlsx)</option>
</select>
</label>
@ -43,14 +44,14 @@
<option value={key}>{name}</option>
{/each}
</select>
<button class="btn" type="button" on:click={() => dispatch('openViewConfig')} title="Edit view">
<button class="button" type="button" on:click={() => dispatch('openViewConfig')} title="Edit view">
<Icon name="cog" />
</button>
</label>
</form>
<svelte:fragment slot="footer">
<button class="btn" on:click={submit}>
<button class="button" on:click={submit}>
<Icon name="play" /> Start export
</button>
</svelte:fragment>

View File

@ -71,7 +71,7 @@
<option value="hashed" disabled={index.model.length > 1}>Hashed</option>
</select>
</label>
<button type="button" class="btn danger" on:click={() => removeRule(ruleIndex)} disabled={index.model.length < 2}>
<button type="button" class="button danger" on:click={() => removeRule(ruleIndex)} disabled={index.model.length < 2}>
<Icon name="-" />
</button>
</div>
@ -82,10 +82,10 @@
</form>
<div class="buttons" slot="footer">
<button class="btn" on:click={addRule} disabled={index.model.some(r => r.sort === 'hashed')}>
<button class="button" on:click={addRule} disabled={index.model.some(r => r.sort === 'hashed')}>
<Icon name="+" /> Add rule
</button>
<button class="btn" on:click={create} disabled={!index.model.length || index.model.some(r => !r.key)}>
<button class="button" on:click={create} disabled={!index.model.length || index.model.some(r => !r.key)}>
<Icon name="+" /> Create index
</button>
</div>

View File

@ -106,11 +106,11 @@
<svelte:fragment slot="footer">
{#if queryToSave}
<button class="btn" on:click={submit}>
<button class="button" on:click={submit}>
<Icon name="save" /> Save query
</button>
{:else}
<button class="btn" on:click={submit} disabled={!selectedKey}>
<button class="button" on:click={submit} disabled={!selectedKey}>
<Icon name="upload" /> Load query
</button>
{/if}
@ -131,7 +131,7 @@
min-height: 200px;
}
.btn + :global(.hint) {
.button + :global(.hint) {
margin-top: 0.5rem;
}
</style>

View File

@ -185,16 +185,16 @@
</span>
</label>
<button class="btn" type="button" on:click={() => addColumn(columnIndex)} title="Add column before this one">
<button class="button" type="button" on:click={() => addColumn(columnIndex)} title="Add column before this one">
<Icon name="+" />
</button>
<button class="btn" type="button" on:click={() => moveColumn(columnIndex, -1)} disabled={columnIndex === 0} title="Move column one position up">
<button class="button" type="button" on:click={() => moveColumn(columnIndex, -1)} disabled={columnIndex === 0} title="Move column one position up">
<Icon name="chev-u" />
</button>
<button class="btn" 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" />
</button>
<button class="btn danger" type="button" on:click={() => removeColumn(columnIndex)} title="Remove this column">
<button class="button danger" type="button" on:click={() => removeColumn(columnIndex)} title="Remove this column">
<Icon name="x" />
</button>
</div>
@ -202,10 +202,10 @@
<p>No columns yet</p>
{/each}
</div>
<button class="btn" on:click={addColumn}>
<button class="button" on:click={addColumn}>
<Icon name="+" /> Add column
</button>
<button class="btn" on:click={addSuggestedColumns} disabled={!Object.keys(firstItem || {}).length}>
<button class="button" on:click={addSuggestedColumns} disabled={!Object.keys(firstItem || {}).length}>
<Icon name="zap" /> Add suggested columns
</button>
{/if}

View File

@ -48,7 +48,13 @@
querying = `Querying ${collection.key}…`;
activePath = [];
const newResult = await FindItems(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(form));
const newResult = await FindItems(collection.hostKey, collection.dbKey, collection.key, JSON.stringify({
fields: convertLooseJson(form.fields || defaults.fields),
limit: form.limit ?? defaults.limit,
query: convertLooseJson(form.query) || defaults.query,
skip: form.skip ?? defaults.skip,
sort: convertLooseJson(form.sort) || defaults.sort,
}));
if (newResult) {
newResult.results = newResult.results?.map(s => EJSON.parse(s, { relaxed: false }));
@ -158,27 +164,46 @@
<div class="find">
<form on:submit|preventDefault={submitQuery}>
<div class="form-row one">
<div class="formrow one">
<label class="field">
<span class="label">Query or id</span>
<input type="text"
class="code"
placeholder={defaults.query}
autocomplete="off"
spellcheck="false"
use:input={{ type: 'json', autofocus: true }}
bind:this={queryField}
bind:value={form.query}
use:input={{ type: 'json', autofocus: true }}
placeholder={defaults.query} />
/>
</label>
<label class="field">
<span class="label">Sort</span>
<input type="text" class="code" bind:value={form.sort} use:input={{ type: 'json' }} placeholder={defaults.sort} />
<input
type="text"
class="code"
placeholder={defaults.sort}
autocomplete="off"
spellcheck="false"
bind:value={form.sort}
use:input={{ type: 'json' }}
/>
</label>
</div>
<div class="form-row two">
<div class="formrow two">
<label class="field">
<span class="label">Fields</span>
<input type="text" class="code" bind:value={form.fields} use:input={{ type: 'json' }} placeholder={defaults.fields} />
<input
type="text"
class="code"
placeholder={defaults.fields}
autocomplete="off"
spellcheck="false"
bind:value={form.fields}
use:input={{ type: 'json' }}
/>
</label>
<label class="field">
@ -188,7 +213,8 @@
bind:value={form.skip}
use:input
placeholder={defaults.skip}
list="skipstops" />
list="skipstops"
/>
</label>
<label class="field">
@ -198,22 +224,23 @@
bind:value={form.limit}
use:input
placeholder={defaults.limit}
list="limits" />
list="limits"
/>
</label>
</div>
<div class="form-row actions">
<button type="submit" class="btn" title="Run query">
<div class="formrow actions">
<button type="submit" class="button" title="Run query">
<Icon name="play" /> Run
</button>
<button class="btn secondary" type="button" on:click={() => collection.export(form)}>
<button class="button secondary" type="button" on:click={() => collection.export(form)}>
<Icon name="save" /> Export results…
</button>
<div class="field">
<button class="btn secondary" type="button" on:click={loadQuery}>
<button class="button secondary" type="button" on:click={loadQuery}>
<Icon name="upload" /> Load query…
</button>
<button class="btn secondary" type="button" on:click={saveQuery}>
<button class="button secondary" type="button" on:click={saveQuery}>
<Icon name="save" /> Save as…
</button>
</div>
@ -236,9 +263,11 @@
{:else}
<Grid
key="_id"
columns={$views[collection.viewKey]?.columns?.map(c => {
return { key: c.key, title: c.key };
}) || []}
columns={$views[collection.viewKey]?.columns
?.filter(c => c.showInTable)
.map(c => {
return { key: c.key, title: c.key };
}) || []}
showHeaders={true}
items={result.results ? result.results.map(r => EJSON.deserialize(r)) : []}
bind:activePath
@ -264,23 +293,23 @@
<option value={key}>{view.name}</option>
{/each}
</select>
<button class="btn" on:click={openViewConfig} title="Configure view">
<button class="button" on:click={openViewConfig} title="Configure view">
<Icon name="cog" />
</button>
</label>
<button class="btn danger" on:click={removeActive} disabled={!activePath?.length} title="Drop selected item">
<button class="button danger" on:click={removeActive} disabled={!activePath?.length} title="Drop selected item">
<Icon name="-" />
</button>
<button class="btn" on:click={first} disabled={!submittedForm.limit || (submittedForm.skip <= 0) || !result?.results || (activePage === 0)} title="First page">
<button class="button" on:click={first} disabled={!submittedForm.limit || (submittedForm.skip <= 0) || !result?.results || (activePage === 0)} title="First page">
<Icon name="chevs-l" />
</button>
<button class="btn" on:click={prev} disabled={!submittedForm.limit || (submittedForm.skip <= 0) || !result?.results || (activePage === 0)} title="Previous {submittedForm.limit} items">
<button class="button" on:click={prev} disabled={!submittedForm.limit || (submittedForm.skip <= 0) || !result?.results || (activePage === 0)} title="Previous {submittedForm.limit} items">
<Icon name="chev-l" />
</button>
<button class="btn" on:click={next} disabled={!submittedForm.limit || ((result?.results?.length || 0) < submittedForm.limit) || !result?.results || !lastPage || (activePage >= lastPage)} title="Next {submittedForm.limit} items">
<button class="button" on:click={next} disabled={!submittedForm.limit || ((result?.results?.length || 0) < submittedForm.limit) || !result?.results || !lastPage || (activePage >= lastPage)} title="Next {submittedForm.limit} items">
<Icon name="chev-r" />
</button>
<button class="btn" on:click={last} disabled={!submittedForm.limit || ((result?.results?.length || 0) < submittedForm.limit) || !result?.results || !lastPage || (activePage >= lastPage)} title="Last page">
<button class="button" on:click={last} disabled={!submittedForm.limit || ((result?.results?.length || 0) < submittedForm.limit) || !result?.results || !lastPage || (activePage >= lastPage)} title="Last page">
<Icon name="chevs-r" />
</button>
</div>
@ -289,7 +318,6 @@
</div>
{#if objectViewerData}
<!-- @todo Implement save -->
<ObjectViewer bind:data={objectViewerData} saveable on:save={saveDocument} bind:successMessage={objectViewerSuccessMessage} />
{/if}
@ -314,18 +342,18 @@
grid-template: auto 1fr / 1fr;
}
.form-row {
.formrow {
display: grid;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.form-row.one {
.formrow.one {
grid-template: 1fr / 3fr 2fr;
}
.form-row.two {
.formrow.two {
grid-template: 1fr / 5fr 1fr 1fr;
}
.form-row.actions {
.formrow.actions {
margin-bottom: 0rem;
grid-template: 1fr / repeat(4, auto);
justify-content: start;

View File

@ -9,6 +9,7 @@
import Indexes from './indexes.svelte';
import Insert from './insert.svelte';
import Remove from './remove.svelte';
import Shell from '../shell.svelte';
import Stats from './stats.svelte';
import Update from './update.svelte';
@ -16,8 +17,8 @@
export let hostKey;
export let dbKey;
export let collKey;
export let tab = 'find';
let tab = 'find';
let find;
$: if (collection) {
@ -42,16 +43,19 @@
<div class="view" class:empty={!collection}>
{#if collection}
{#key collection}
<TabBar tabs={[
{ key: 'stats', icon: 'chart', title: 'Stats' },
{ key: 'find', icon: 'db', title: 'Find' },
{ key: 'insert', icon: '+', title: 'Insert' },
{ key: 'update', icon: 'edit', title: 'Update' },
{ key: 'remove', icon: 'trash', title: 'Remove' },
{ key: 'indexes', icon: 'list', title: 'Indexes' },
{ key: 'aggregate', icon: 're', title: 'Aggregate' },
]}
bind:selectedKey={tab} />
<TabBar
tabs={[
{ key: 'stats', icon: 'chart', title: 'Stats' },
{ key: 'find', icon: 'db', title: 'Find' },
{ key: 'insert', icon: '+', title: 'Insert' },
{ key: 'update', icon: 'edit', title: 'Update' },
{ key: 'remove', icon: 'trash', title: 'Remove' },
{ key: 'indexes', icon: 'list', title: 'Indexes' },
{ key: 'aggregate', icon: 're', title: 'Aggregate' },
{ key: 'shell', icon: 'shell', title: 'Shell' },
]}
bind:selectedKey={tab}
/>
<div class="container">
{#if tab === 'stats'} <Stats {collection} />
@ -61,6 +65,7 @@
{: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>
{/key}

View File

@ -8,9 +8,12 @@
let activePath = [];
let _indexes = [];
let error = '';
let busy = false;
async function refresh() {
busy = 'Fetching indexes…';
error = await collection.getIndexes();
if (!error) {
_indexes = collection.indexes.map(idx => {
return {
@ -22,6 +25,8 @@
};
});
}
busy = false;
}
async function createIndex() {
@ -56,17 +61,18 @@
errorTitle={error ? 'Error while getting indexes' : ''}
errorDescription={error}
bind:activePath
{busy}
/>
</div>
<div class="actions">
<button class="btn" on:click={refresh}>
<Icon name="reload" /> Reload
<button class="button" on:click={refresh}>
<Icon name="reload" spin={busy} /> Reload
</button>
<button class="btn" on:click={createIndex}>
<button class="button" on:click={createIndex}>
<Icon name="+" /> Create index…
</button>
<button class="btn 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
</button>
</div>

View File

@ -73,12 +73,7 @@
}
function showJson() {
if (viewType === 'form') {
objectViewerData = { ...(newItems[0] || {}) };
}
else if (viewType === 'table') {
objectViewerData = [ ...newItems ];
}
objectViewerData = [ ...newItems ];
}
function addRow(beforeIndex = -1) {
@ -151,7 +146,6 @@
<div class="table">
<Grid
key="id"
items={newItems}
columns={$views[collection.viewKey]?.columns
?.filter(c => inputTypes.includes(c.inputType))
.map(c => {
@ -162,7 +156,9 @@
canRemoveItems={true}
hideChildrenToggles={true}
on:addRow={addRow}
on:removeItem={() => deleteRow()}
bind:inputsValid={allValid}
bind:items={newItems}
/>
</div>
{/if}
@ -182,13 +178,13 @@
</div>
<div>
{#if insertedIds}
<button class="btn" type="button" on:click={showDocs}>View inserted docs</button>
<button class="button" type="button" on:click={showDocs}>View inserted docs</button>
{/if}
{#if collection.viewKey !== 'list'}
<button class="btn" type="button" on:click={showJson} title="Show JSON">
<button class="button" type="button" on:click={showJson} title="Show JSON">
<Icon name="code" />
</button>
<button class="btn" type="button" on:click={switchViewType} title="Edit as {oppositeViewType}">
<button class="button" type="button" on:click={switchViewType} title="Edit as {oppositeViewType}">
<Icon name={oppositeViewType} /> {capitalise(oppositeViewType)}
</button>
{/if}
@ -198,18 +194,20 @@
<option value={key}>{key === 'list' ? 'Raw JSON' : view.name}</option>
{/each}
</select>
<button class="btn" type="button" on:click={openViewConfig} title="Configure view">
<button class="button" type="button" on:click={openViewConfig} title="Configure view">
<Icon name="cog" />
</button>
</label>
<button type="submit" class="btn" disabled={$views[collection.viewKey]?.type === 'list' ? !json : !allValid}>
<button type="submit" class="button" disabled={$views[collection.viewKey]?.type === 'list' ? !json : !allValid}>
<Icon name="+" /> Insert
</button>
</div>
</div>
</form>
<ObjectViewer data={objectViewerData} />
{#if objectViewerData}
<ObjectViewer bind:data={objectViewerData} />
{/if}
<style>
form {

View File

@ -45,7 +45,7 @@
</label>
<div class="actions">
<button type="submit" class="btn danger">
<button type="submit" class="button danger">
<Icon name="-" /> Remove
</button>

View File

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

View File

@ -107,7 +107,7 @@
{/if}
{/key}
<button class="btn" type="submit" disabled={invalid}>
<button class="button" type="submit" disabled={invalid}>
<Icon name="check" /> Update
</button>
</div>
@ -135,11 +135,11 @@
<input type="text" class="code" bind:value={param.value} placeholder={'{}'} use:input={{ type: 'json' }} />
</label>
<button class="btn" disabled={form.parameters.length >= allOperators.length} on:click={() => addParameter()} type="button">
<button class="button" disabled={form.parameters.length >= allOperators.length} on:click={() => addParameter()} type="button">
<Icon name="+" />
</button>
<button class="btn" disabled={form.parameters.length < 2} on:click={() => removeParam(index)} type="button">
<button class="button" disabled={form.parameters.length < 2} on:click={() => removeParam(index)} type="button">
<Icon name="-" />
</button>
</fieldset>

View File

@ -126,7 +126,7 @@
</form>
<svelte:fragment slot="footer">
<button class="btn" on:click={performDump}>Perform dump</button>
<button class="button" on:click={performDump}>Perform dump</button>
</svelte:fragment>
</Modal>

View File

@ -2,13 +2,14 @@
import BlankState from '$components/blankstate.svelte';
import TabBar from '$components/tabbar.svelte';
import { EventsOn } from '$wails/runtime/runtime';
import Shell from '../shell.svelte';
import Stats from './stats.svelte';
export let database;
export let hostKey;
export let dbKey;
let tab = 'stats';
export let tab = 'stats';
$: if (database) {
database.hostKey = hostKey;
@ -25,9 +26,15 @@
<div class="view" class:empty={!database}>
{#if database}
{#key database}
<TabBar tabs={[ { key: 'stats', icon: 'chart', title: 'Database stats' } ]} bind:selectedKey={tab} />
<TabBar
tabs={[
{ key: 'stats', icon: 'chart', title: 'Database stats' },
{ key: 'shell', icon: 'shell', title: 'Shell' },
]}
bind:selectedKey={tab} />
<div class="container">
{#if tab === 'stats'} <Stats {database} />
{:else if tab === 'shell'} <Shell {database} />
{/if}
</div>
{/key}

View File

@ -20,6 +20,7 @@
<div class="grid">
<ObjectGrid
data={database.stats}
showTypes={false}
errorTitle={database.statsError ? 'Error fetching database stats' : ''}
errorDescription={database.statsError}
busy={!database.stats && !database.statsError && `Fetching stats for ${database.key}`}
@ -27,7 +28,7 @@
</div>
<div class="buttons">
<button class="btn secondary" on:click={copy} disabled={!database.stats}>
<button class="button secondary" on:click={copy} disabled={!database.stats}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} />
Copy JSON
</button>

View File

@ -64,7 +64,7 @@
<div class="error">{error}</div>
{/if}
</div>
<button class="btn" disabled={!valid} on:click={submit}>
<button class="button" disabled={!valid} on:click={submit}>
{host ? 'Save' : 'Create'}
</button>
</div>

View File

@ -2,13 +2,15 @@
import BlankState from '$components/blankstate.svelte';
import TabBar from '$components/tabbar.svelte';
import { EventsOn } from '$wails/runtime/runtime';
import Logs from './logs.svelte';
import Shell from '../shell.svelte';
import Status from './status.svelte';
import SystemInfo from './systeminfo.svelte';
export let host;
export let hostKey;
let tab = 'status';
export let tab = 'status';
$: if (host) {
host.hostKey = hostKey;
@ -24,15 +26,21 @@
<div class="view" class:empty={!host}>
{#if host}
{#key host}
<TabBar tabs={[
{ key: 'status', icon: 'chart', title: 'Host status' },
{ key: 'systemInfo', icon: 'server', title: 'System info' },
]}
bind:selectedKey={tab} />
<TabBar
tabs={[
{ key: 'status', icon: 'chart', title: 'Host status' },
{ key: 'shell', icon: 'shell', title: 'Shell' },
{ key: 'logs', icon: 'doc', title: 'Logs' },
{ key: 'systemInfo', icon: 'server', title: 'System info' },
]}
bind:selectedKey={tab}
/>
<div class="container">
{#if tab === 'status'} <Status {host} />
{:else if tab === 'logs'} <Logs {host} />
{:else if tab === 'systemInfo'} <SystemInfo {host} />
{:else if tab === 'shell'} <Shell {host} />
{/if}
</div>
{/key}

View File

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

View File

@ -20,6 +20,7 @@
<div class="grid">
<ObjectGrid
data={host.status || {}}
showTypes={false}
errorTitle={host.statusError ? 'Error fetching server status' : ''}
errorDescription={host.statusError}
busy={!host.status && !host.statusError && 'Fetching server status…'}
@ -27,7 +28,7 @@
</div>
<div class="buttons">
<button class="btn secondary" on:click={copy} disabled={!host.status}>
<button class="button secondary" on:click={copy} disabled={!host.status}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} />
Copy JSON
</button>

View File

@ -20,6 +20,7 @@
<div class="grid">
<ObjectGrid
data={host.systemInfo}
showTypes={false}
errorTitle={host.systemInfoError ? 'Error fetching system info' : ''}
errorDescription={host.systemInfoError}
busy={!host.systemInfo && !host.systemInfoError && 'Fetching system info…'}
@ -27,7 +28,7 @@
</div>
<div class="buttons">
<button class="btn secondary" on:click={copy} disabled={!host.systemInfo}>
<button class="button secondary" on:click={copy} disabled={!host.systemInfo}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} />
Copy JSON
</button>

View File

@ -2,12 +2,16 @@
import Icon from '$components/icon.svelte';
import hostTree from '$lib/stores/hosttree';
import sharedState from '$lib/stores/sharedstate';
import { EventsOn } from '$wails/runtime/runtime';
import CollectionView from './collection/index.svelte';
import DatabaseView from './database/index.svelte';
import HostView from './host/index.svelte';
import HostTree from './hosttree.svelte';
let path = [];
let hostTab = '';
let dbTab = '';
let collTab = '';
$: activeHostKey = path[0];
$: activeDbKey = path[1];
@ -16,6 +20,28 @@
$: sharedState.currentHost.set(activeHostKey);
$: sharedState.currentDb.set(activeDbKey);
$: sharedState.currentColl.set(activeCollKey);
EventsOn('ui.host.new', () => hostTree.newHost());
EventsOn('ui.host.edit', () => $hostTree[activeHostKey]?.edit());
EventsOn('ui.host.remove', () => $hostTree[activeHostKey]?.remove());
EventsOn('ui.host.tab', tab => {
path = path.slice(0, 1);
hostTab = tab;
});
EventsOn('ui.db.new', () => $hostTree[activeHostKey]?.newDatabase());
EventsOn('ui.db.dump', () => $hostTree[activeHostKey]?.databases[activeDbKey]?.dump());
EventsOn('ui.db.drop', () => $hostTree[activeHostKey]?.databases[activeDbKey]?.drop());
EventsOn('ui.db.tab', tab => {
path = path.slice(0, 2);
dbTab = tab;
});
EventsOn('ui.coll.new', () => $hostTree[activeHostKey]?.databases[activeDbKey]?.newCollection());
EventsOn('ui.coll.export', () => $hostTree[activeHostKey]?.databases[activeDbKey]?.collections[activeCollKey]?.export());
EventsOn('ui.coll.truncate', () => $hostTree[activeHostKey]?.databases[activeDbKey]?.collections[activeCollKey]?.truncate());
EventsOn('ui.coll.drop', () => $hostTree[activeHostKey]?.databases[activeDbKey]?.collections[activeCollKey]?.drop());
EventsOn('ui.coll.tab', tab => collTab = tab);
</script>
<div class="tree">
@ -34,17 +60,20 @@
hostKey={activeHostKey}
dbKey={activeDbKey}
collKey={activeCollKey}
bind:tab={collTab}
/>
{:else if activeDbKey}
<DatabaseView
database={$hostTree[activeHostKey]?.databases[activeDbKey]}
hostKey={activeHostKey}
dbKey={activeDbKey}
bind:tab={dbTab}
/>
{:else if activeHostKey}
<HostView
host={$hostTree[activeHostKey]}
hostKey={activeHostKey}
bind:tab={hostTab}
/>
{/if}

View File

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

View File

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

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

@ -20,10 +20,14 @@ export function DropIndex(arg1:string,arg2:string,arg3:string,arg4:string):Promi
export function Environment():Promise<app.EnvironmentInfo>;
export function ExecuteShellScript(arg1:string,arg2:string,arg3:string,arg4:string):Promise<app.ExecuteShellScriptResult>;
export function FindItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise<app.FindItemsResult>;
export function GetIndexes(arg1:string,arg2:string,arg3:string):Promise<app.GetIndexesResult>;
export function HostLogs(arg1:string,arg2:string):Promise<app.HostLogsResult>;
export function Hosts():Promise<map[string]app.Host>;
export function InsertItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise<any>;

View File

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

View File

@ -13,7 +13,6 @@ import (
"strings"
"github.com/garraflavatra/rolens/internal/ui"
"github.com/ncruces/zenity"
wailsRuntime "github.com/wailsapp/wails/v2/pkg/runtime"
"golang.org/x/sync/syncmap"
)
@ -26,6 +25,7 @@ type EnvironmentInfo struct {
HasMongoExport bool `json:"hasMongoExport"`
HasMongoDump bool `json:"hasMongoDump"`
HasMongoShell bool `json:"hasMongoShell"`
HomeDirectory string `json:"homeDirectory"`
DataDirectory string `json:"dataDirectory"`
@ -50,6 +50,9 @@ func NewApp(version string) *App {
_, err = exec.LookPath("mongoexport")
a.Env.HasMongoExport = err == nil
_, err = exec.LookPath("mongosh")
a.Env.HasMongoShell = err == nil
a.Env.HomeDirectory, err = os.UserHomeDir()
if err != nil {
panic(errors.New("encountered an error while getting home directory"))
@ -107,16 +110,30 @@ func (a *App) Environment() EnvironmentInfo {
}
func (a *App) PurgeLogDirectory() {
err := zenity.Question("Are you sure you want to remove all logfiles?", zenity.Title("Confirm"), zenity.WarningIcon)
if err == zenity.ErrCanceled {
choice, _ := wailsRuntime.MessageDialog(a.ctx, wailsRuntime.MessageDialogOptions{
Title: "Confirm",
Message: "Are you sure you want to remove all logfiles?",
Buttons: []string{"Yes", "Cancel"},
DefaultButton: "Yes",
CancelButton: "Cancel",
})
if choice != "Yes" {
return
}
err = os.RemoveAll(a.Env.LogDirectory)
err := os.RemoveAll(a.Env.LogDirectory)
if err == nil {
zenity.Info("Successfully purged log directory.", zenity.InfoIcon)
wailsRuntime.MessageDialog(a.ctx, wailsRuntime.MessageDialogOptions{
Title: "Success",
Message: "Successfully purged log directory",
Type: wailsRuntime.InfoDialog,
})
} else {
zenity.Error(err.Error(), zenity.Title("Encountered an error while purging log directory."), zenity.WarningIcon)
wailsRuntime.MessageDialog(a.ctx, wailsRuntime.MessageDialogOptions{
Title: "Error while purging log directory",
Message: err.Error(),
Type: wailsRuntime.ErrorDialog,
})
}
}

View File

@ -14,7 +14,7 @@ func menuCallbackEmit(a *App, eventName string, data ...interface{}) func(cd *me
}
}
func menuCallbackURL(a *App, url string) func(cd *menu.CallbackData) {
func menuCallbackOpenURL(a *App, url string) func(cd *menu.CallbackData) {
return func(cd *menu.CallbackData) {
wailsRuntime.BrowserOpenURL(a.ctx, url)
}
@ -30,36 +30,65 @@ func (a *App) Menu() *menu.Menu {
aboutMenu.AddSeparator()
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("Purge logs", nil, func(cd *menu.CallbackData) { a.PurgeLogDirectory() })
aboutMenu.AddText("Purge logs", nil, func(cd *menu.CallbackData) { a.PurgeLogDirectory() })
aboutMenu.AddSeparator()
aboutMenu.AddText("Quit Rolens", keys.CmdOrCtrl("q"), func(cd *menu.CallbackData) { wailsRuntime.Quit(a.ctx) })
fileMenu := appMenu.AddSubmenu("File")
fileMenu.AddText("New host…", keys.CmdOrCtrl("y"), menuCallbackEmit(a, "CreateHost"))
fileMenu.AddSeparator()
fileMenu.AddText("Stats", keys.Combo("h", keys.CmdOrCtrlKey, keys.OptionOrAltKey), menuCallbackEmit(a, "OpenCollectionTab", "stats"))
fileMenu.AddText("Find", keys.Combo("f", keys.CmdOrCtrlKey, keys.OptionOrAltKey), menuCallbackEmit(a, "OpenCollectionTab", "find"))
fileMenu.AddText("Insert", keys.Combo("i", keys.CmdOrCtrlKey, keys.OptionOrAltKey), menuCallbackEmit(a, "OpenCollectionTab", "insert"))
fileMenu.AddText("Update", keys.Combo("u", keys.CmdOrCtrlKey, keys.OptionOrAltKey), menuCallbackEmit(a, "OpenCollectionTab", "update"))
fileMenu.AddText("Remove", keys.Combo("r", keys.CmdOrCtrlKey, keys.OptionOrAltKey), menuCallbackEmit(a, "OpenCollectionTab", "remove"))
fileMenu.AddText("Indexes", keys.Combo("x", keys.CmdOrCtrlKey, keys.OptionOrAltKey), menuCallbackEmit(a, "OpenCollectionTab", "indexes"))
fileMenu.AddText("Aggregate", keys.Combo("a", keys.CmdOrCtrlKey, keys.OptionOrAltKey), menuCallbackEmit(a, "OpenCollectionTab", "aggregate"))
if runtime.GOOS == "darwin" {
aboutMenu.AddText("Minimize", keys.CmdOrCtrl("M"), func(cd *menu.CallbackData) { wailsRuntime.WindowMinimise(a.ctx) })
aboutMenu.AddText("Hide Rolens", keys.CmdOrCtrl("H"), func(cd *menu.CallbackData) { wailsRuntime.WindowHide(a.ctx) })
aboutMenu.AddSeparator()
appMenu.Append(menu.EditMenu())
}
aboutMenu.AddText("Quit Rolens", keys.CmdOrCtrl("Q"), func(cd *menu.CallbackData) { wailsRuntime.Quit(a.ctx) })
hostMenu := appMenu.AddSubmenu("Host")
hostMenu.AddText("New…", keys.OptionOrAlt("C"), menuCallbackEmit(a, "ui.host.new"))
hostMenu.AddText("Edit host…", keys.OptionOrAlt("H"), menuCallbackEmit(a, "ui.host.edit"))
hostMenu.AddSeparator()
hostMenu.AddText("Server status", nil, menuCallbackEmit(a, "ui.host.tab", "status"))
hostMenu.AddText("System info", nil, menuCallbackEmit(a, "ui.host.tab", "systemInfo"))
hostMenu.AddText("Shell", nil, menuCallbackEmit(a, "ui.host.tab", "shell"))
hostMenu.AddSeparator()
hostMenu.AddText("Remove host…", nil, menuCallbackEmit(a, "ui.host.remove"))
dbMenu := appMenu.AddSubmenu("Database")
dbMenu.AddText("New…", keys.OptionOrAlt("D"), menuCallbackEmit(a, "ui.db.new"))
dbMenu.AddSeparator()
dbMenu.AddText("Database statistics", nil, menuCallbackEmit(a, "ui.db.tab", "stats"))
dbMenu.AddText("Shell", nil, menuCallbackEmit(a, "ui.db.tab", "shell"))
dbMenu.AddSeparator()
dbMenu.AddText("Dump…", nil, menuCallbackEmit(a, "ui.db.dump"))
dbMenu.AddText("Drop…", nil, menuCallbackEmit(a, "ui.db.drop"))
collMenu := appMenu.AddSubmenu("Collection")
collMenu.AddText("New…", keys.OptionOrAlt("T"), menuCallbackEmit(a, "ui.coll.new"))
collMenu.AddSeparator()
collMenu.AddText("Collection statistics", keys.Combo("S", keys.CmdOrCtrlKey, keys.ShiftKey), menuCallbackEmit(a, "ui.coll.tab", "stats"))
collMenu.AddText("Find", keys.Combo("F", keys.CmdOrCtrlKey, keys.ShiftKey), menuCallbackEmit(a, "ui.coll.tab", "find"))
collMenu.AddText("Insert", keys.Combo("I", keys.CmdOrCtrlKey, keys.ShiftKey), menuCallbackEmit(a, "ui.coll.tab", "insert"))
collMenu.AddText("Update", keys.Combo("P", keys.CmdOrCtrlKey, keys.ShiftKey), menuCallbackEmit(a, "ui.coll.tab", "update"))
collMenu.AddText("Remove", keys.Combo("R", keys.CmdOrCtrlKey, keys.ShiftKey), menuCallbackEmit(a, "ui.coll.tab", "remove"))
collMenu.AddText("Indexes", keys.Combo("X", keys.CmdOrCtrlKey, keys.ShiftKey), menuCallbackEmit(a, "ui.coll.tab", "indexes"))
collMenu.AddText("Aggregate", keys.Combo("A", keys.CmdOrCtrlKey, keys.ShiftKey), menuCallbackEmit(a, "ui.coll.tab", "aggregate"))
collMenu.AddText("Shell", keys.Combo("H", keys.CmdOrCtrlKey, keys.ShiftKey), menuCallbackEmit(a, "ui.coll.tab", "shell"))
collMenu.AddSeparator()
collMenu.AddText("Export…", keys.OptionOrAlt("E"), menuCallbackEmit(a, "ui.coll.exort"))
collMenu.AddText("Truncate…", nil, menuCallbackEmit(a, "ui.coll.truncate"))
collMenu.AddText("Drop…", nil, menuCallbackEmit(a, "ui.coll.drop"))
helpMenu := appMenu.AddSubmenu("Help")
helpMenu.AddText("User guide", nil, menuCallbackURL(a, "https://garraflavatra.github.io/rolens/user-guide/"))
helpMenu.AddText("Website", nil, menuCallbackURL(a, "https://garraflavatra.github.io/rolens/"))
helpMenu.AddText("Discussion board", nil, menuCallbackURL(a, "https://github.com/garraflavatra/rolens/discussions"))
helpMenu.AddText("User guide", keys.CmdOrCtrl("/"), menuCallbackOpenURL(a, "https://garraflavatra.github.io/rolens/user-guide/"))
helpMenu.AddText("Website", nil, menuCallbackOpenURL(a, "https://garraflavatra.github.io/rolens/"))
helpMenu.AddText("Discussion board", nil, menuCallbackOpenURL(a, "https://github.com/garraflavatra/rolens/discussions"))
helpMenu.AddSeparator()
helpMenu.AddText("Report a problem", nil, menuCallbackURL(a, "https://github.com/garraflavatra/rolens/issues/new"))
helpMenu.AddText("Ask a question", nil, menuCallbackURL(a, "https://github.com/garraflavatra/rolens/discussions/new?category=questions"))
helpMenu.AddText("Report a problem", nil, menuCallbackOpenURL(a, "https://github.com/garraflavatra/rolens/issues/new"))
helpMenu.AddText("Ask a question", nil, menuCallbackOpenURL(a, "https://github.com/garraflavatra/rolens/discussions/new?category=questions"))
helpMenu.AddSeparator()
helpMenu.AddText("Star Rolens on GitHub", nil, menuCallbackURL(a, "https://github.com/garraflavatra/rolens"))
helpMenu.AddText("Changelog", nil, menuCallbackURL(a, "https://garraflavatra.github.io/rolens/development/changelog/"))
helpMenu.AddText("License", nil, menuCallbackURL(a, "https://github.com/garraflavatra/rolens/blob/main/LICENSE"))
helpMenu.AddText("Star Rolens on GitHub", nil, menuCallbackOpenURL(a, "https://github.com/garraflavatra/rolens"))
helpMenu.AddText("Changelog", nil, menuCallbackOpenURL(a, "https://garraflavatra.github.io/rolens/colophon/changelog/"))
helpMenu.AddText("License", nil, menuCallbackOpenURL(a, "https://github.com/garraflavatra/rolens/blob/main/LICENSE"))
return appMenu
}

View File

@ -6,7 +6,6 @@ import (
"os"
"path"
"github.com/ncruces/zenity"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
@ -34,20 +33,20 @@ func (a *App) Settings() Settings {
if err != nil {
// It's ok if the file cannot be opened, for example if it is not accessible.
// Therefore no error is returned.
runtime.LogInfo(a.ctx, "Cannot open settings.json:")
runtime.LogInfo(a.ctx, err.Error())
runtime.LogInfof(a.ctx, "Cannot open settings.json: %s", err.Error())
return s
}
if len(jsonData) == 0 {
return s
} else {
err = json.Unmarshal(jsonData, &s)
if err != nil {
runtime.LogWarning(a.ctx, "Cannot unmarshal settings.json:")
runtime.LogWarning(a.ctx, err.Error())
zenity.Warning("Could not retrieve application settings, using defaults!", zenity.WarningIcon)
if err := json.Unmarshal(jsonData, &s); err != nil {
runtime.LogWarningf(a.ctx, "Cannot unmarshal settings.json: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Settings malformed",
Message: "Could not retrieve application settings: using defaults!",
Type: runtime.WarningDialog,
})
}
return s
}
@ -57,26 +56,35 @@ func (a *App) UpdateSettings(jsonData string) Settings {
s := a.Settings()
err := json.Unmarshal([]byte(jsonData), &s)
if err != nil {
runtime.LogError(a.ctx, "Malformed JSON for settings file:")
runtime.LogError(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Malformed JSON"), zenity.ErrorIcon)
runtime.LogErrorf(a.ctx, "Malformed JSON for settings file: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Settings malformed",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return s
}
newJson, err := json.MarshalIndent(s, "", "\t")
if err != nil {
runtime.LogError(a.ctx, "Could not marshal settings into JSON:")
runtime.LogError(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Malformed JSON"), zenity.ErrorIcon)
runtime.LogErrorf(a.ctx, "Could not marshal settings into JSON: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "JSON is being awkward",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return s
}
filePath := path.Join(a.Env.DataDirectory, "settings.json")
err = ioutil.WriteFile(filePath, newJson, os.ModePerm)
if err != nil {
runtime.LogError(a.ctx, "Could not update host list:")
runtime.LogError(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Could not update host list"), zenity.ErrorIcon)
runtime.LogErrorf(a.ctx, "Could not update host list: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Could not update host list",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
}
return s

View File

@ -3,7 +3,6 @@ package app
import (
"fmt"
"github.com/ncruces/zenity"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go.mongodb.org/mongo-driver/bson"
)
@ -23,8 +22,7 @@ func (a *App) OpenCollection(hostKey, dbKey, collKey string) (result OpenCollect
command := bson.M{"collStats": collKey}
err = client.Database(dbKey).RunCommand(ctx, command).Decode(&result.Stats)
if err != nil {
runtime.LogWarning(a.ctx, "Could not retrieve collection stats for "+collKey)
runtime.LogWarning(a.ctx, err.Error())
runtime.LogWarningf(a.ctx, "Could not retrieve collection stats for %s: %s", collKey, err.Error())
result.StatsError = err.Error()
}
@ -45,9 +43,12 @@ func (a *App) RenameCollection(hostKey, dbKey, collKey, newCollKey string) bool
}
err = client.Database("admin").RunCommand(ctx, command).Decode(&result)
if err != nil {
runtime.LogWarning(a.ctx, "Could not rename collection "+collKey)
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while renaming collection"), zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Could not rename collection %s: %s", collKey, err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error renaming collection",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
@ -55,8 +56,14 @@ func (a *App) RenameCollection(hostKey, dbKey, collKey, newCollKey string) bool
}
func (a *App) TruncateCollection(hostKey, dbKey, collKey string) bool {
err := zenity.Question("Are you sure you want to remove all items from "+collKey+"?", zenity.Title("Confirm"), zenity.WarningIcon)
if err == zenity.ErrCanceled {
choice, _ := runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Confirm",
Message: "Are you sure you want to remove all items in " + collKey + "?",
Buttons: []string{"Yes", "Cancel"},
DefaultButton: "Yes",
CancelButton: "Cancel",
})
if choice != "Yes" {
return false
}
@ -68,9 +75,12 @@ func (a *App) TruncateCollection(hostKey, dbKey, collKey string) bool {
_, err = client.Database(dbKey).Collection(collKey).DeleteMany(ctx, bson.D{})
if err != nil {
runtime.LogWarning(a.ctx, "Could not truncate collection "+collKey)
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while truncating collection"), zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Could not truncate collection %s: %s", collKey, err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error truncating collection",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
@ -78,8 +88,14 @@ func (a *App) TruncateCollection(hostKey, dbKey, collKey string) bool {
}
func (a *App) DropCollection(hostKey, dbKey, collKey string) bool {
err := zenity.Question("Are you sure you want to drop "+collKey+"?", zenity.Title("Confirm"), zenity.WarningIcon)
if err == zenity.ErrCanceled {
choice, _ := runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Confirm",
Message: "Are you sure you want to drop " + collKey + "?",
Buttons: []string{"Yes", "Cancel"},
DefaultButton: "Yes",
CancelButton: "Cancel",
})
if choice != "Yes" {
return false
}
@ -91,9 +107,12 @@ func (a *App) DropCollection(hostKey, dbKey, collKey string) bool {
err = client.Database(dbKey).Collection(collKey).Drop(ctx)
if err != nil {
runtime.LogWarning(a.ctx, "Could not drop collection "+collKey)
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while dropping collection"), zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Could not drop collection %s: %s", collKey, err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error dropping collection",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}

View File

@ -2,9 +2,7 @@ package app
import (
"encoding/json"
"fmt"
"github.com/ncruces/zenity"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
@ -14,17 +12,23 @@ import (
func (a *App) Aggregate(hostKey, dbKey, collKey, pipelineJson, settingsJson string) {
var settings *options.AggregateOptions
if err := json.Unmarshal([]byte(settingsJson), &settings); err != nil {
runtime.LogError(a.ctx, "Could not parse aggregation settings:")
runtime.LogError(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Couldn't parse aggregation settings"), zenity.ErrorIcon)
runtime.LogErrorf(a.ctx, "Could not parse aggregation settings: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Couldn't parse aggregation settings",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return
}
var pipeline mongo.Pipeline
if err := bson.UnmarshalExtJSON([]byte(pipelineJson), true, &pipeline); err != nil {
runtime.LogWarning(a.ctx, "Could not parse aggregation pipeline:")
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Couldn't parse aggregation pipeline"), zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Could not parse aggregation pipeline: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Couldn't parse aggregation pipeline",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return
}
@ -37,19 +41,23 @@ func (a *App) Aggregate(hostKey, dbKey, collKey, pipelineJson, settingsJson stri
cursor, err := client.Database(dbKey).Collection(collKey).Aggregate(ctx, pipeline, settings)
if err != nil {
runtime.LogWarning(a.ctx, "Could not get aggregation cursor:")
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Couldn't get aggregation cursor"), zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Could not get aggregation cursor: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Couldn't get aggregation cursor",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return
}
var results []bson.M
if err := cursor.All(ctx, &results); err != nil {
runtime.LogInfo(a.ctx, "Error while running aggregation pipeline:")
runtime.LogInfo(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while running aggregation pipeline"), zenity.ErrorIcon)
runtime.LogInfof(a.ctx, "Error while running aggregation pipeline: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error while running aggregation pipeline",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return
}
fmt.Println(results)
}

View File

@ -3,7 +3,6 @@ package app
import (
"encoding/json"
"github.com/ncruces/zenity"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go.mongodb.org/mongo-driver/bson"
mongoOptions "go.mongodb.org/mongo-driver/mongo/options"
@ -29,9 +28,9 @@ func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) (result FindIt
err := json.Unmarshal([]byte(formJson), &form)
if err != nil {
runtime.LogError(a.ctx, "Could not parse find form:")
runtime.LogError(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Could not parse form"), zenity.ErrorIcon)
runtime.LogErrorf(a.ctx, "Could not parse find form: %s", err.Error())
result.ErrorTitle = "Could not parse form"
result.ErrorDescription = err.Error()
return
}
@ -109,8 +108,7 @@ func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) (result FindIt
for _, r := range results {
marshalled, err := bson.MarshalExtJSON(r, true, true)
if err != nil {
runtime.LogError(a.ctx, "Failed to marshal find BSON:")
runtime.LogError(a.ctx, err.Error())
runtime.LogErrorf(a.ctx, "Failed to marshal find BSON: %s", err.Error())
result.ErrorTitle = "Failed to marshal JSON"
result.ErrorDescription = err.Error()
return
@ -125,14 +123,22 @@ func (a *App) UpdateFoundDocument(hostKey, dbKey, collKey, idJson, newDocJson st
var id bson.M
if err := bson.UnmarshalExtJSON([]byte(idJson), true, &id); err != nil {
runtime.LogWarningf(a.ctx, "Could not parse find/update query: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Couldn't parse update query"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error parsing update query",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
var newDoc bson.M
if err := bson.UnmarshalExtJSON([]byte(newDocJson), true, &newDoc); err != nil {
runtime.LogWarningf(a.ctx, "Could not parse new find/update document: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Couldn't parse document"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error parsing document",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
@ -144,7 +150,11 @@ func (a *App) UpdateFoundDocument(hostKey, dbKey, collKey, idJson, newDocJson st
if _, err := client.Database(dbKey).Collection(collKey).ReplaceOne(ctx, id, newDoc); err != nil {
runtime.LogInfof(a.ctx, "Error while replacing document: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Unable to replace document"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error replacing document",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}

View File

@ -1,12 +1,15 @@
package app
import (
"archive/zip"
_ "embed"
"encoding/csv"
"encoding/json"
"fmt"
"html"
"os"
"strings"
"github.com/ncruces/zenity"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsontype"
@ -24,6 +27,30 @@ const (
ExportFormatJsonArray ExportFormat = "jsonarray"
ExportFormatNdJson ExportFormat = "ndjson"
ExportFormatCsv ExportFormat = "csv"
ExportFormatExcel ExportFormat = "excel"
)
var (
//go:embed collection_find_export_excel/app.xml
excelAppXml string
//go:embed collection_find_export_excel/contenttypes.xml
excelContentTypesXml string
//go:embed collection_find_export_excel/core.xml
excelCoreXml string
//go:embed collection_find_export_excel/dotrels.xml
excelDotRelsXml string
//go:embed collection_find_export_excel/metadata.xml
excelMetadataXml string
//go:embed collection_find_export_excel/rels.xml
excelRelsXml string
//go:embed collection_find_export_excel/styles.xml
excelStylesXml string
//go:embed collection_find_export_excel/theme.xml
excelThemeXml string
//go:embed collection_find_export_excel/workbook.xml
excelWorkbookXml string
alphabet = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
)
type ExportSettings struct {
@ -40,13 +67,40 @@ func getptr[T any](v T) *T {
return &v
}
func excelColIndex(idx int) string {
str := make([]rune, 0)
for idx > 0 {
rem := idx % 26
if rem == 0 {
str = append(str, 'Z')
idx = (idx / 26) - 1
} else {
str = append(str, alphabet[rem-1])
idx = (idx / 26)
}
}
// Reverse string
for i, j := 0, len(str)-1; i < j; i, j = i+1, j-1 {
str[i], str[j] = str[j], str[i]
}
return string(str)
}
func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bool {
runtime.LogInfof(a.ctx, "Export started for %s/%s/%s. Settings: %s", hostKey, dbKey, collKey, settingsJson)
var settings ExportSettings
if err := json.Unmarshal([]byte(settingsJson), &settings); err != nil {
runtime.LogWarningf(a.ctx, "Could not parse export settings: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Couldn't parse export settings!"), zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Export: Could not parse settings: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Couldn't parse export settings!",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
@ -69,8 +123,11 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo
view, found := views[settings.ViewKey]
if !found {
zenity.Error(fmt.Sprintf("View %s is not known", settings.ViewKey), zenity.ErrorIcon)
runtime.LogDebugf(a.ctx, "Export: unknown view %s", settings.ViewKey)
runtime.LogWarningf(a.ctx, "Export: unknown view %s", settings.ViewKey)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Message: fmt.Sprintf("View %s is not known", settings.ViewKey),
Type: runtime.ErrorDialog,
})
return false
}
@ -84,18 +141,27 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo
DisplayName: "CSV files (*.csv)",
Pattern: "*.csv",
}
case ExportFormatJsonArray:
defaultFilename = "export.json"
fileFilter = runtime.FileFilter{
DisplayName: "JSON files (*.json)",
Pattern: "*.json",
}
case ExportFormatNdJson:
defaultFilename = "export.ndjson"
fileFilter = runtime.FileFilter{
DisplayName: "Newline delimited JSON files (*.ndjson)",
Pattern: "*.ndjson",
}
case ExportFormatExcel:
defaultFilename = "export.xlsx"
fileFilter = runtime.FileFilter{
DisplayName: "Microsoft Excel Workbook (*.xlsx)",
Pattern: "*.xlsx",
}
}
settings.OutFile, err = runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
@ -106,27 +172,40 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo
Filters: []runtime.FileFilter{fileFilter},
})
if err != nil {
zenity.Error("An error occured while choosing the export destination", zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Export: error while choosing export destination: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error while choosing export destination",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
if settings.OutFile == "" {
zenity.Error("You must specify an export destination.", zenity.ErrorIcon)
runtime.LogDebug(a.ctx, "Export: no destination specified")
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Message: "Please specify an export destination.",
Type: runtime.ErrorDialog,
})
return false
}
if _, err := os.Stat(settings.OutFile); err == nil {
zenity.Error(fmt.Sprintf("File %s already exists, export aborted.", settings.OutFile), zenity.ErrorIcon)
runtime.LogDebugf(a.ctx, "Export: destination %s already exists", settings.OutFile)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Message: fmt.Sprintf("File %s already exists, export aborted.", settings.OutFile),
Type: runtime.ErrorDialog,
})
return false
}
var query bson.M
if settings.Contents != ExportContentsAll {
if err = bson.UnmarshalExtJSON([]byte(settings.QueryJson), true, &query); err != nil {
runtime.LogDebugf(a.ctx, "Invalid find query (exporting): %s", settings.QueryJson)
zenity.Error(err.Error(), zenity.Title("Invalid query"), zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Export: invalid find query: %s", settings.QueryJson)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Invalid query",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
}
@ -137,12 +216,10 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo
}
defer close()
pgr, _ := zenity.Progress(zenity.Title("Performing export…"))
projection := bson.M{}
if settings.ViewKey != "list" {
for _, col := range view.Columns {
projection[col.Key] = ""
projection[col.Key] = 1
}
}
@ -162,22 +239,34 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo
Projection: projection,
})
if err != nil {
runtime.LogInfof(a.ctx, "Export: unable to get cursor while exporting: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Unable to get cursor"), zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Export: unable to get cursor while exporting: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Couldn't get cursor",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
file, err := os.OpenFile(settings.OutFile, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
zenity.Error(fmt.Sprintf(err.Error(), zenity.Title("Error while opening file"), settings.OutFile), zenity.ErrorIcon)
runtime.LogDebugf(a.ctx, "Export: unable to open file %s", settings.OutFile)
runtime.LogInfof(a.ctx, "Export: unable to open file %s", settings.OutFile)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error opening file",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
defer file.Close()
var csvWriter *csv.Writer
var csvColumnKeys []string
var index uint = 0
var columnKeys []string
var csvWriter *csv.Writer
var excelZipWriter *zip.Writer
var excelSheetWriter strings.Builder
switch settings.Format {
case ExportFormatJsonArray:
@ -185,77 +274,144 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo
case ExportFormatCsv:
csvWriter = csv.NewWriter(file)
case ExportFormatExcel:
excelZipWriter = zip.NewWriter(file)
files := map[string]string{
"_rels/.rels": excelDotRelsXml,
"docProps/app.xml": excelAppXml,
"docProps/core.xml": strings.Replace(excelCoreXml, "{TITLE}", fmt.Sprintf("%s.%s Export", dbKey, collKey), 1),
"xl/_rels/workbook.xml.rels": excelRelsXml,
"xl/theme/theme1.xml": excelThemeXml,
"xl/metadata.xml": excelMetadataXml,
"xl/styles.xml": excelStylesXml,
"xl/workbook.xml": excelWorkbookXml,
"[Content_Types].xml": excelContentTypesXml,
}
for fname, body := range files {
f, err := excelZipWriter.Create(fname)
if err != nil {
runtime.LogErrorf(a.ctx, "Export: Excel zip.Create error: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "ZIP error!",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
_, err = f.Write([]byte(body))
if err != nil {
runtime.LogErrorf(a.ctx, "Export: Excel zip.Create.Write error: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "ZIP error!",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
}
excelSheetWriter = strings.Builder{}
}
for cur.Next(ctx) {
switch settings.Format {
case ExportFormatCsv:
els, err := cur.Current.Elements()
if err != nil {
zenity.Error(err.Error(), zenity.Title("BSON invalid"), zenity.ErrorIcon)
if columnKeys == nil {
columnKeys = make([]string, 0)
if settings.ViewKey == "list" {
els, err := cur.Current.Elements()
if err != nil {
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "BSON is invalid",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
}
for _, el := range els {
if el.Key() == "" {
continue
}
switch el.Value().Type {
case bsontype.Boolean,
bsontype.Decimal128,
bsontype.Double,
bsontype.Int32,
bsontype.Int64,
bsontype.Null,
bsontype.ObjectID,
bsontype.Regex,
bsontype.String,
bsontype.Symbol,
bsontype.Timestamp,
bsontype.Undefined:
columnKeys = append(columnKeys, el.Key())
}
}
} else {
for _, col := range view.Columns {
columnKeys = append(columnKeys, col.Key)
}
}
runtime.LogDebugf(a.ctx, "Export column keys: %v", columnKeys)
switch settings.Format {
case ExportFormatCsv:
if err := csvWriter.Write(columnKeys); err != nil {
runtime.LogInfof(a.ctx, "Unable to write item %d to CSV while exporting: %s", index, err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: fmt.Sprintf("Unable to write item %d to CSV", index),
Message: err.Error(),
Type: runtime.ErrorDialog,
})
}
case ExportFormatExcel:
excelSheetWriter.Write([]byte(fmt.Sprintf(`<row r="1" spans="1:%d" s="1" customFormat="1" x14ac:dyDescent="0.2">`, len(columnKeys))))
for idx, key := range columnKeys {
excelSheetWriter.Write([]byte(fmt.Sprintf(`<c r="%s1" s="1" t="str"><v>%s</v></c>`, excelColIndex(idx+1), key)))
}
excelSheetWriter.Write([]byte("</row>"))
}
}
switch settings.Format {
case ExportFormatCsv:
csvItem := make([]string, 0)
switch settings.ViewKey {
case "list":
if csvColumnKeys == nil {
csvColumnKeys = make([]string, 0)
for _, el := range els {
if el.Key() == "" {
continue
}
switch el.Value().Type {
case bsontype.Boolean,
bsontype.Decimal128,
bsontype.Double,
bsontype.Int32,
bsontype.Int64,
bsontype.Null,
bsontype.ObjectID,
bsontype.Regex,
bsontype.String,
bsontype.Symbol,
bsontype.Timestamp,
bsontype.Undefined:
csvColumnKeys = append(csvColumnKeys, el.Key())
}
}
runtime.LogDebugf(a.ctx, "Export csvColumnKeys: %v", csvColumnKeys)
if err := csvWriter.Write(csvColumnKeys); err != nil {
runtime.LogInfof(a.ctx, "Unable to write item %d to CSV while exporting: %s", index, err.Error())
zenity.Error(err.Error(), zenity.Title("Unable to write item %d to CSV"), zenity.ErrorIcon)
}
for _, k := range columnKeys {
r, err := cur.Current.LookupErr(k)
if err != nil {
csvItem = append(csvItem, "")
continue
}
for _, k := range csvColumnKeys {
r, err := cur.Current.LookupErr(k)
if err != nil {
csvItem = append(csvItem, "")
continue
}
var v any
if err := r.Unmarshal(&v); err != nil {
zenity.Error(err.Error(), zenity.Title(fmt.Sprintf("Unable to unmarshal field %s", k)), zenity.ErrorIcon)
csvItem = append(csvItem, "")
continue
}
csvItem = append(csvItem, fmt.Sprintf("%v", v))
var v any
if err := r.Unmarshal(&v); err != nil {
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: fmt.Sprintf("Unable to unmarshal field %s", k),
Message: err.Error(),
Type: runtime.ErrorDialog,
})
csvItem = append(csvItem, "")
continue
}
default:
// @todo
csvItem = append(csvItem, fmt.Sprintf("%v", v))
}
if err := csvWriter.Write(csvItem); err != nil {
runtime.LogInfof(a.ctx, "Unable to write item %d to CSV while exporting: %s", index, err.Error())
zenity.Error(err.Error(), zenity.Title("Unable to write item %d to CSV"), zenity.ErrorIcon)
runtime.LogInfof(a.ctx, "Export: Unable to write item %d to CSV while exporting: %s", index, err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: fmt.Sprintf("Unable to write item %d to CSV", index),
Message: err.Error(),
Type: runtime.ErrorDialog,
})
}
csvWriter.Flush()
@ -263,8 +419,12 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo
case ExportFormatJsonArray, ExportFormatNdJson:
itemJson, err := bson.MarshalExtJSON(cur.Current, true, false)
if err != nil {
runtime.LogInfof(a.ctx, "Unable to marshal item %d to JSON while exporting: %s", index, err.Error())
zenity.Error(err.Error(), zenity.Title("Unable to marshal item %d to JSON"), zenity.ErrorIcon)
runtime.LogInfof(a.ctx, "Export: Unable to marshal item %d to JSON while exporting: %s", index, err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: fmt.Sprintf("Unable to write item %d to CSV", index),
Message: err.Error(),
Type: runtime.ErrorDialog,
})
}
if (settings.Format == ExportFormatJsonArray) && (index != 0) {
@ -276,26 +436,87 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo
if settings.Format == ExportFormatNdJson {
file.WriteString("\n")
}
case ExportFormatExcel:
excelRow := make([]string, 0)
for _, k := range columnKeys {
r, err := cur.Current.LookupErr(k)
if err != nil {
excelRow = append(excelRow, "")
continue
}
var v any
if err := r.Unmarshal(&v); err != nil {
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: fmt.Sprintf("Unable to unmarshal field %s", k),
Message: err.Error(),
Type: runtime.ErrorDialog,
})
excelRow = append(excelRow, "")
continue
}
excelRow = append(excelRow, html.EscapeString(fmt.Sprintf("%v", v)))
}
excelSheetWriter.Write([]byte(fmt.Sprintf(`<row r="%d" spans="1:%d" s="1" x14ac:dyDescent="0.2">`, index+2, len(columnKeys))))
for idx, val := range excelRow {
excelSheetWriter.Write([]byte(fmt.Sprintf(`<c r="%s%d" t="str"><v>%s</v></c>`, excelColIndex(idx+1), index+2, val)))
}
excelSheetWriter.Write([]byte("</row>"))
}
index++
}
if count != 0 && pgr != nil {
p := (float32(index) / float32(count)) * 100.0
pgr.Value(int(p))
switch settings.Format {
case ExportFormatJsonArray:
file.WriteString("]\n")
case ExportFormatExcel:
sw, err := excelZipWriter.Create("xl/worksheets/sheet1.xml")
if err != nil {
runtime.LogErrorf(a.ctx, "Export: Excel ZIP error creating worksheet: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "ZIP error!",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
sw.Write([]byte(strings.ReplaceAll(fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet
xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x14ac xr xr2 xr3"
xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac"
xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision"
xmlns:xr2="http://schemas.microsoft.com/office/spreadsheetml/2015/revision2"
xmlns:xr3="http://schemas.microsoft.com/office/spreadsheetml/2016/revision3" xr:uid="{17671867-E8D5-A14C-B382-03A6AA54A004}">
<dimension ref="A1:%s%d"/>
<sheetViews>
<sheetView workbookViewId="0"/>
</sheetViews>
<sheetFormatPr baseColWidth="25" defaultRowHeight="16" x14ac:dyDescent="0.2"/>
<sheetData>`, excelColIndex(len(columnKeys)), index+1), "\n", "\r\n")))
sw.Write([]byte(excelSheetWriter.String()))
sw.Write([]byte(`</sheetData><pageMargins left="0.7" right="0.7" top="0.75" bottom="0.75" header="0.3" footer="0.3"/></worksheet>`))
err = excelZipWriter.Close()
if err != nil {
runtime.LogErrorf(a.ctx, "Export: Excel ZIP error while closing: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "ZIP error!",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
}
}
if settings.Format == ExportFormatJsonArray {
file.WriteString("]\n")
}
if pgr != nil {
pgr.Complete()
pgr.Close()
}
a.ui.Reveal(settings.OutFile)
runtime.LogInfo(a.ctx, "Export succeeded")
runtime.LogInfof(a.ctx, "Export succeeded: %d items", count)
return true
}

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Properties
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
>
<Application>RolensExport</Application>
<DocSecurity>0</DocSecurity>
<ScaleCrop>false</ScaleCrop>
<HeadingPairs>
<vt:vector size="2" baseType="variant">
<vt:variant>
<vt:lpstr>Sheets</vt:lpstr>
</vt:variant>
<vt:variant>
<vt:i4>1</vt:i4>
</vt:variant>
</vt:vector>
</HeadingPairs>
<TitlesOfParts>
<vt:vector size="1" baseType="lpstr">
<vt:lpstr>RolensExport</vt:lpstr>
</vt:vector>
</TitlesOfParts>
<Manager></Manager>
<Company></Company>
<LinksUpToDate>false</LinksUpToDate>
<SharedDoc>false</SharedDoc>
<HyperlinkBase></HyperlinkBase>
<HyperlinksChanged>false</HyperlinksChanged>
<AppVersion>16.0300</AppVersion>
</Properties>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types
xmlns="http://schemas.openxmlformats.org/package/2006/content-types"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
>
<Default Extension="xml" ContentType="application/xml"/>
<Default Extension="bin" ContentType="application/vnd.ms-excel.sheet.binary.macroEnabled.main"/>
<Default Extension="vml" ContentType="application/vnd.openxmlformats-officedocument.vmlDrawing"/>
<Default Extension="data" ContentType="application/vnd.openxmlformats-officedocument.model+data"/>
<Default Extension="bmp" ContentType="image/bmp"/>
<Default Extension="png" ContentType="image/png"/>
<Default Extension="gif" ContentType="image/gif"/>
<Default Extension="emf" ContentType="image/x-emf"/>
<Default Extension="wmf" ContentType="image/x-wmf"/>
<Default Extension="jpg" ContentType="image/jpeg"/>
<Default Extension="jpeg" ContentType="image/jpeg"/>
<Default Extension="tif" ContentType="image/tiff"/>
<Default Extension="tiff" ContentType="image/tiff"/>
<Default Extension="pdf" ContentType="application/pdf"/>
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
<Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
<Override PartName="/xl/metadata.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml"/>
</Types>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<cp:coreProperties
xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:dcterms="http://purl.org/dc/terms/"
xmlns:dcmitype="http://purl.org/dc/dcmitype/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
>
<dc:title>{TITLE}</dc:title>
<dc:subject></dc:subject>
<dc:creator>Rolens</dc:creator>
<cp:keywords></cp:keywords>
<dc:description></dc:description>
<cp:lastModifiedBy></cp:lastModifiedBy>
<dcterms:created xsi:type="dcterms:W3CDTF">2020-01-01T00:00:00Z</dcterms:created>
<dcterms:modified xsi:type="dcterms:W3CDTF">2020-01-01T00:00:00Z</dcterms:modified>
<cp:category></cp:category>
</cp:coreProperties>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
</Relationships>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<metadata xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xlrd="http://schemas.microsoft.com/office/spreadsheetml/2017/richdata" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">
<metadataTypes count="1">
<metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1" pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1" clearComments="1" assign="1" coerce="1" cellMeta="1"/>
</metadataTypes>
<futureMetadata name="XLDAPR" count="1">
<bk>
<extLst>
<ext uri="{bdbb8cdc-fa1e-496e-a857-3c3f30c029c3}">
<xda:dynamicArrayProperties fDynamic="1" fCollapsed="0"/>
</ext>
</extLst>
</bk>
</futureMetadata>
<cellMetadata count="1">
<bk>
<rc t="1" v="0"/>
</bk>
</cellMetadata>
</metadata>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
<Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>
</Relationships>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<styleSheet
xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x14ac x16r2 xr"
xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac"
xmlns:x16r2="http://schemas.microsoft.com/office/spreadsheetml/2015/02/main"
xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision"
>
<fonts count="1" x14ac:knownFonts="1">
<font>
<sz val="12"/>
<color theme="1"/>
<name val="Calibri"/>
<family val="2"/>
<scheme val="minor"/>
</font>
</fonts>
<fills count="3">
<fill>
<patternFill patternType="none"/>
</fill>
<fill>
<patternFill patternType="gray125"/>
</fill>
<fill>
<patternFill patternType="solid">
<fgColor rgb="FFDCDCDC"/>
<bgColor indexed="64"/>
</patternFill>
</fill>
</fills>
<borders count="2">
<border>
<left/>
<right/>
<top/>
<bottom/>
<diagonal/>
</border>
<border>
<left/>
<right/>
<top/>
<bottom style="thin">
<color indexed="64"/>
</bottom>
<diagonal/>
</border>
</borders>
<cellStyleXfs count="1">
<xf numFmtId="0" fontId="0" fillId="0" borderId="0"/>
</cellStyleXfs>
<cellXfs count="2">
<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
<xf numFmtId="0" fontId="0" fillId="2" borderId="1" xfId="0" applyFill="1" applyBorder="1"/>
</cellXfs>
<cellStyles count="1">
<cellStyle name="Standard" xfId="0" builtinId="0"/>
</cellStyles>
<dxfs count="0"/>
<tableStyles count="0" defaultTableStyle="TableStyleMedium2" defaultPivotStyle="PivotStyleLight16"/>
<colors>
<mruColors>
<color rgb="FFDCDCDC"/>
<color rgb="FFABABAB"/>
</mruColors>
</colors>
<extLst>
<ext uri="{EB79DEF2-80B8-43e5-95BD-54CBDDF9020C}" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main">
<x14:slicerStyles defaultSlicerStyle="SlicerStyleLight1"/>
</ext>
<ext uri="{9260A510-F301-46a8-8635-F512D64BE5F5}" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main">
<x15:timelineStyles defaultTimelineStyle="TimeSlicerStyleLight1"/>
</ext>
</extLst>
</styleSheet>

View File

@ -0,0 +1,269 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Kantoorthema">
<a:themeElements>
<a:clrScheme name="Office">
<a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
<a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
<a:dk2><a:srgbClr val="44546A"/></a:dk2>
<a:lt2><a:srgbClr val="E7E6E6"/></a:lt2>
<a:accent1><a:srgbClr val="4472C4"/></a:accent1>
<a:accent2><a:srgbClr val="ED7D31"/></a:accent2>
<a:accent3><a:srgbClr val="A5A5A5"/></a:accent3>
<a:accent4><a:srgbClr val="FFC000"/></a:accent4>
<a:accent5><a:srgbClr val="5B9BD5"/></a:accent5>
<a:accent6><a:srgbClr val="70AD47"/></a:accent6>
<a:hlink><a:srgbClr val="0563C1"/></a:hlink>
<a:folHlink><a:srgbClr val="954F72"/></a:folHlink>
</a:clrScheme>
<a:fontScheme name="Office">
<a:majorFont>
<a:latin typeface="Calibri Light" panose="020F0302020204030204"/>
<a:ea typeface=""/>
<a:cs typeface=""/>
<a:font script="Jpan" typeface="游ゴシック Light"/>
<a:font script="Hang" typeface="맑은 고딕"/>
<a:font script="Hans" typeface="等线 Light"/>
<a:font script="Hant" typeface="新細明體"/>
<a:font script="Arab" typeface="Times New Roman"/>
<a:font script="Hebr" typeface="Times New Roman"/>
<a:font script="Thai" typeface="Tahoma"/>
<a:font script="Ethi" typeface="Nyala"/>
<a:font script="Beng" typeface="Vrinda"/>
<a:font script="Gujr" typeface="Shruti"/>
<a:font script="Khmr" typeface="MoolBoran"/>
<a:font script="Knda" typeface="Tunga"/>
<a:font script="Guru" typeface="Raavi"/>
<a:font script="Cans" typeface="Euphemia"/>
<a:font script="Cher" typeface="Plantagenet Cherokee"/>
<a:font script="Yiii" typeface="Microsoft Yi Baiti"/>
<a:font script="Tibt" typeface="Microsoft Himalaya"/>
<a:font script="Thaa" typeface="MV Boli"/>
<a:font script="Deva" typeface="Mangal"/>
<a:font script="Telu" typeface="Gautami"/>
<a:font script="Taml" typeface="Latha"/>
<a:font script="Syrc" typeface="Estrangelo Edessa"/>
<a:font script="Orya" typeface="Kalinga"/>
<a:font script="Mlym" typeface="Kartika"/>
<a:font script="Laoo" typeface="DokChampa"/>
<a:font script="Sinh" typeface="Iskoola Pota"/>
<a:font script="Mong" typeface="Mongolian Baiti"/>
<a:font script="Viet" typeface="Times New Roman"/>
<a:font script="Uigh" typeface="Microsoft Uighur"/>
<a:font script="Geor" typeface="Sylfaen"/>
<a:font script="Armn" typeface="Arial"/>
<a:font script="Bugi" typeface="Leelawadee UI"/>
<a:font script="Bopo" typeface="Microsoft JhengHei"/>
<a:font script="Java" typeface="Javanese Text"/>
<a:font script="Lisu" typeface="Segoe UI"/>
<a:font script="Mymr" typeface="Myanmar Text"/>
<a:font script="Nkoo" typeface="Ebrima"/>
<a:font script="Olck" typeface="Nirmala UI"/>
<a:font script="Osma" typeface="Ebrima"/>
<a:font script="Phag" typeface="Phagspa"/>
<a:font script="Syrn" typeface="Estrangelo Edessa"/>
<a:font script="Syrj" typeface="Estrangelo Edessa"/>
<a:font script="Syre" typeface="Estrangelo Edessa"/>
<a:font script="Sora" typeface="Nirmala UI"/>
<a:font script="Tale" typeface="Microsoft Tai Le"/>
<a:font script="Talu" typeface="Microsoft New Tai Lue"/>
<a:font script="Tfng" typeface="Ebrima"/>
</a:majorFont>
<a:minorFont>
<a:latin typeface="Calibri" panose="020F0502020204030204"/>
<a:ea typeface=""/>
<a:cs typeface=""/>
<a:font script="Jpan" typeface="游ゴシック"/>
<a:font script="Hang" typeface="맑은 고딕"/>
<a:font script="Hans" typeface="等线"/>
<a:font script="Hant" typeface="新細明體"/>
<a:font script="Arab" typeface="Arial"/>
<a:font script="Hebr" typeface="Arial"/>
<a:font script="Thai" typeface="Tahoma"/>
<a:font script="Ethi" typeface="Nyala"/>
<a:font script="Beng" typeface="Vrinda"/>
<a:font script="Gujr" typeface="Shruti"/>
<a:font script="Khmr" typeface="DaunPenh"/>
<a:font script="Knda" typeface="Tunga"/>
<a:font script="Guru" typeface="Raavi"/>
<a:font script="Cans" typeface="Euphemia"/>
<a:font script="Cher" typeface="Plantagenet Cherokee"/>
<a:font script="Yiii" typeface="Microsoft Yi Baiti"/>
<a:font script="Tibt" typeface="Microsoft Himalaya"/>
<a:font script="Thaa" typeface="MV Boli"/>
<a:font script="Deva" typeface="Mangal"/>
<a:font script="Telu" typeface="Gautami"/>
<a:font script="Taml" typeface="Latha"/>
<a:font script="Syrc" typeface="Estrangelo Edessa"/>
<a:font script="Orya" typeface="Kalinga"/>
<a:font script="Mlym" typeface="Kartika"/>
<a:font script="Laoo" typeface="DokChampa"/>
<a:font script="Sinh" typeface="Iskoola Pota"/>
<a:font script="Mong" typeface="Mongolian Baiti"/>
<a:font script="Viet" typeface="Arial"/>
<a:font script="Uigh" typeface="Microsoft Uighur"/>
<a:font script="Geor" typeface="Sylfaen"/>
<a:font script="Armn" typeface="Arial"/>
<a:font script="Bugi" typeface="Leelawadee UI"/>
<a:font script="Bopo" typeface="Microsoft JhengHei"/>
<a:font script="Java" typeface="Javanese Text"/>
<a:font script="Lisu" typeface="Segoe UI"/>
<a:font script="Mymr" typeface="Myanmar Text"/>
<a:font script="Nkoo" typeface="Ebrima"/>
<a:font script="Olck" typeface="Nirmala UI"/>
<a:font script="Osma" typeface="Ebrima"/>
<a:font script="Phag" typeface="Phagspa"/>
<a:font script="Syrn" typeface="Estrangelo Edessa"/>
<a:font script="Syrj" typeface="Estrangelo Edessa"/>
<a:font script="Syre" typeface="Estrangelo Edessa"/>
<a:font script="Sora" typeface="Nirmala UI"/>
<a:font script="Tale" typeface="Microsoft Tai Le"/>
<a:font script="Talu" typeface="Microsoft New Tai Lue"/>
<a:font script="Tfng" typeface="Ebrima"/>
</a:minorFont>
</a:fontScheme>
<a:fmtScheme name="Office">
<a:fillStyleLst>
<a:solidFill>
<a:schemeClr val="phClr"/>
</a:solidFill>
<a:gradFill rotWithShape="1">
<a:gsLst>
<a:gs pos="0">
<a:schemeClr val="phClr">
<a:lumMod val="110000"/>
<a:satMod val="105000"/>
<a:tint val="67000"/>
</a:schemeClr>
</a:gs>
<a:gs pos="50000">
<a:schemeClr val="phClr">
<a:lumMod val="105000"/>
<a:satMod val="103000"/>
<a:tint val="73000"/>
</a:schemeClr>
</a:gs>
<a:gs pos="100000">
<a:schemeClr val="phClr">
<a:lumMod val="105000"/>
<a:satMod val="109000"/>
<a:tint val="81000"/>
</a:schemeClr>
</a:gs>
</a:gsLst>
<a:lin ang="5400000" scaled="0"/>
</a:gradFill>
<a:gradFill rotWithShape="1">
<a:gsLst>
<a:gs pos="0">
<a:schemeClr val="phClr">
<a:satMod val="103000"/>
<a:lumMod val="102000"/>
<a:tint val="94000"/>
</a:schemeClr>
</a:gs>
<a:gs pos="50000">
<a:schemeClr val="phClr">
<a:satMod val="110000"/>
<a:lumMod val="100000"/>
<a:shade val="100000"/>
</a:schemeClr>
</a:gs>
<a:gs pos="100000">
<a:schemeClr val="phClr">
<a:lumMod val="99000"/>
<a:satMod val="120000"/>
<a:shade val="78000"/>
</a:schemeClr>
</a:gs>
</a:gsLst>
<a:lin ang="5400000" scaled="0"/>
</a:gradFill>
</a:fillStyleLst>
<a:lnStyleLst>
<a:ln w="6350" cap="flat" cmpd="sng" algn="ctr">
<a:solidFill>
<a:schemeClr val="phClr"/>
</a:solidFill>
<a:prstDash val="solid"/>
<a:miter lim="800000"/>
</a:ln>
<a:ln w="12700" cap="flat" cmpd="sng" algn="ctr">
<a:solidFill>
<a:schemeClr val="phClr"/>
</a:solidFill>
<a:prstDash val="solid"/>
<a:miter lim="800000"/>
</a:ln>
<a:ln w="19050" cap="flat" cmpd="sng" algn="ctr">
<a:solidFill>
<a:schemeClr val="phClr"/>
</a:solidFill>
<a:prstDash val="solid"/>
<a:miter lim="800000"/>
</a:ln>
</a:lnStyleLst>
<a:effectStyleLst>
<a:effectStyle>
<a:effectLst/>
</a:effectStyle>
<a:effectStyle>
<a:effectLst/>
</a:effectStyle>
<a:effectStyle>
<a:effectLst>
<a:outerShdw blurRad="57150" dist="19050" dir="5400000" algn="ctr" rotWithShape="0">
<a:srgbClr val="000000">
<a:alpha val="63000"/>
</a:srgbClr>
</a:outerShdw>
</a:effectLst>
</a:effectStyle>
</a:effectStyleLst>
<a:bgFillStyleLst>
<a:solidFill>
<a:schemeClr val="phClr"/>
</a:solidFill>
<a:solidFill>
<a:schemeClr val="phClr">
<a:tint val="95000"/>
<a:satMod val="170000"/>
</a:schemeClr>
</a:solidFill>
<a:gradFill rotWithShape="1">
<a:gsLst>
<a:gs pos="0">
<a:schemeClr val="phClr">
<a:tint val="93000"/>
<a:satMod val="150000"/>
<a:shade val="98000"/>
<a:lumMod val="102000"/>
</a:schemeClr>
</a:gs>
<a:gs pos="50000">
<a:schemeClr val="phClr">
<a:tint val="98000"/>
<a:satMod val="130000"/>
<a:shade val="90000"/>
<a:lumMod val="103000"/>
</a:schemeClr>
</a:gs>
<a:gs pos="100000">
<a:schemeClr val="phClr">
<a:shade val="63000"/>
<a:satMod val="120000"/>
</a:schemeClr>
</a:gs>
</a:gsLst>
<a:lin ang="5400000" scaled="0"/>
</a:gradFill>
</a:bgFillStyleLst>
</a:fmtScheme>
</a:themeElements>
<a:objectDefaults/>
<a:extraClrSchemeLst/>
<a:extLst>
<a:ext uri="{05A4C25C-085E-4340-85A3-A5531E510DB2}">
<thm15:themeFamily xmlns:thm15="http://schemas.microsoft.com/office/thememl/2012/main" name="Office Theme" id="{62F939B6-93AF-4DB8-9C6B-D6C7DFDC589F}" vid="{4A3C46E8-61CC-4603-A589-7422A47A8E4A}"/>
</a:ext>
</a:extLst>
</a:theme>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook
xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
>
<workbookPr codeName="ThisWorkbook"/>
<sheets>
<sheet name="RolensExport" sheetId="1" r:id="rId1"/>
</sheets>
</workbook>

View File

@ -6,7 +6,6 @@ import (
"os"
"path"
"github.com/ncruces/zenity"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
@ -37,8 +36,7 @@ func (a *App) SavedQueries() map[string]SavedQuery {
if err != nil {
// It's ok if the file cannot be opened, for example if it is not accessible.
// Therefore no error is returned.
runtime.LogInfo(a.ctx, "Could not open queries.json")
runtime.LogInfo(a.ctx, err.Error())
runtime.LogInfof(a.ctx, "Could not open queries.json: %s", err.Error())
return make(map[string]SavedQuery, 0)
}
@ -49,8 +47,7 @@ func (a *App) SavedQueries() map[string]SavedQuery {
err = json.Unmarshal(jsonData, &queries)
if err != nil {
runtime.LogInfo(a.ctx, "queries.json file contains malformatted JSON data")
runtime.LogInfo(a.ctx, err.Error())
runtime.LogInfof(a.ctx, "queries.json file contains malformatted JSON data: %s", err.Error())
return nil
}
@ -62,9 +59,12 @@ func (a *App) SaveQuery(jsonData string) string {
var query SavedQuery
err := json.Unmarshal([]byte(jsonData), &query)
if err != nil {
runtime.LogError(a.ctx, "Add query: malformed form")
runtime.LogError(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Malformed JSON"), zenity.ErrorIcon)
runtime.LogErrorf(a.ctx, "Add query: malformed form: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Malformed JSON",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return ""
}
@ -72,7 +72,11 @@ func (a *App) SaveQuery(jsonData string) string {
queries[query.Name] = query
err = updateQueryFile(a, queries)
if err != nil {
zenity.Error(err.Error(), zenity.Title("Could not update query list"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Could not update query list",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return ""
}
@ -83,7 +87,11 @@ func (a *App) RemoveQuery(queryName string) {
queries := a.SavedQueries()
delete(queries, queryName)
if err := updateQueryFile(a, queries); err != nil {
zenity.Error(err.Error(), zenity.Title("Could not update query list"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Could not update query list",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
}
}
@ -91,15 +99,22 @@ func (a *App) UpdateQueries(jsonData string) bool {
var queries map[string]SavedQuery
err := json.Unmarshal([]byte(jsonData), &queries)
if err != nil {
runtime.LogError(a.ctx, "Update queries: malformed form")
runtime.LogError(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Malformed JSON"), zenity.ErrorIcon)
runtime.LogErrorf(a.ctx, "Update queries: malformed form: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Malformed JSON",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
err = updateQueryFile(a, queries)
if err != nil {
zenity.Error(err.Error(), zenity.Title("Could not save queries"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Could not save queries",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}

View File

@ -4,7 +4,6 @@ import (
"encoding/json"
"math"
"github.com/ncruces/zenity"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
@ -25,16 +24,14 @@ func (a *App) GetIndexes(hostKey, dbKey, collKey string) (result GetIndexesResul
cur, err := client.Database(dbKey).Collection(collKey).Indexes().List(ctx)
if err != nil {
runtime.LogWarning(a.ctx, "Encountered an error while creating index cursor:")
runtime.LogWarning(a.ctx, err.Error())
runtime.LogWarningf(a.ctx, "Encountered an error while creating index cursor: %s", err.Error())
result.Error = err.Error()
return
}
err = cur.All(ctx, &result.Indexes)
if err != nil {
runtime.LogWarning(a.ctx, "Encountered an error while executing index cursor:")
runtime.LogWarning(a.ctx, err.Error())
runtime.LogWarningf(a.ctx, "Encountered an error while executing index cursor: %s", err.Error())
result.Error = err.Error()
}
@ -64,9 +61,12 @@ func (a *App) CreateIndex(hostKey, dbKey, collKey, jsonData string) string {
err = json.Unmarshal([]byte(jsonData), &form)
if err != nil {
runtime.LogError(a.ctx, "Could not parse index JSON:")
runtime.LogError(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Could not parse JSON"), zenity.ErrorIcon)
runtime.LogErrorf(a.ctx, "Could not parse index JSON: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Malformed JSON",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return ""
}
@ -94,9 +94,12 @@ func (a *App) CreateIndex(hostKey, dbKey, collKey, jsonData string) string {
name, err := client.Database(dbKey).Collection(collKey).Indexes().CreateOne(ctx, indexModel)
if err != nil {
runtime.LogWarning(a.ctx, "Encountered an error while creating index:")
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while creating index"), zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Encountered an error while creating index: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error creating index",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return ""
}
@ -112,9 +115,12 @@ func (a *App) DropIndex(hostKey, dbKey, collKey, indexName string) bool {
_, err = client.Database(dbKey).Collection(collKey).Indexes().DropOne(ctx, indexName, &options.DropIndexesOptions{})
if err != nil {
runtime.LogError(a.ctx, "Encountered an error while creating index drop cursor:")
runtime.LogError(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while creating drop cursor"), zenity.ErrorIcon)
runtime.LogErrorf(a.ctx, "Encountered an error while creating index drop cursor: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error creating drop cursor",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}

View File

@ -3,7 +3,6 @@ package app
import (
"strings"
"github.com/ncruces/zenity"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go.mongodb.org/mongo-driver/bson"
)
@ -18,9 +17,12 @@ func (a *App) InsertItems(hostKey, dbKey, collKey, jsonData string) interface{}
err := bson.UnmarshalExtJSON([]byte(jsonData), true, &data)
if err != nil {
runtime.LogError(a.ctx, "Could not parse insert JSON:")
runtime.LogError(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Could not parse JSON"), zenity.ErrorIcon)
runtime.LogErrorf(a.ctx, "Could not parse insert JSON: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Malformed JSON",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return nil
}
@ -32,9 +34,12 @@ func (a *App) InsertItems(hostKey, dbKey, collKey, jsonData string) interface{}
res, err := client.Database(dbKey).Collection(collKey).InsertMany(ctx, data)
if err != nil {
runtime.LogWarning(a.ctx, "Encountered an error while performing insert:")
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while performing insert"), zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Encountered an error while performing insert: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error performing insert",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return nil
}

View File

@ -3,7 +3,6 @@ package app
import (
"strings"
"github.com/ncruces/zenity"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
@ -15,16 +14,25 @@ func (a *App) RemoveItems(hostKey, dbKey, collKey, jsonData string, many bool) i
jsonData = strings.TrimSpace(jsonData)
if len(jsonData) == 0 {
err := zenity.Question("Are you sure you want to drop all items in "+collKey+"?", zenity.Title("Confirm"), zenity.WarningIcon)
if err == zenity.ErrCanceled {
choice, _ := runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Confirm",
Message: "Are you sure you want to drop all items in " + collKey + "?",
Buttons: []string{"Yes", "Cancel"},
DefaultButton: "Yes",
CancelButton: "Cancel",
})
if choice != "Yes" {
return 0
}
} else {
err = bson.UnmarshalExtJSON([]byte(jsonData), true, &filter)
if err != nil {
runtime.LogError(a.ctx, "Could not parse remove query:")
runtime.LogError(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Could not parse JSON"), zenity.ErrorIcon)
runtime.LogErrorf(a.ctx, "Could not parse remove query: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Malformed JSON",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return 0
}
}
@ -45,9 +53,12 @@ func (a *App) RemoveItems(hostKey, dbKey, collKey, jsonData string, many bool) i
}
if err != nil {
runtime.LogWarning(a.ctx, "Encountered an error while performing remove:")
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while performing remove"), zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Encountered an error while performing remove: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error performing remove query",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return 0
}
@ -66,10 +77,12 @@ func (a *App) RemoveItemById(hostKey, dbKey, collKey, itemId string) bool {
err = client.Database(dbKey).Collection(collKey).FindOneAndDelete(ctx, filter).Err()
if err != nil && err != mongo.ErrNoDocuments {
runtime.LogWarning(a.ctx, "Encountered an error while performing remove by id:")
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while performing remove"), zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Encountered an error while performing remove by id: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error performing remove query",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}

View File

@ -3,7 +3,6 @@ package app
import (
"encoding/json"
"github.com/ncruces/zenity"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go.mongodb.org/mongo-driver/bson"
@ -24,9 +23,12 @@ func (a *App) UpdateItems(hostKey, dbKey, collKey string, formJson string) int64
err := json.Unmarshal([]byte(formJson), &form)
if err != nil {
runtime.LogError(a.ctx, "Could not parse update form:")
runtime.LogError(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Could not parse form"), zenity.ErrorIcon)
runtime.LogErrorf(a.ctx, "Could not parse update form: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Malformed JSON",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return 0
}
@ -41,10 +43,12 @@ func (a *App) UpdateItems(hostKey, dbKey, collKey string, formJson string) int64
err = bson.UnmarshalExtJSON([]byte(form.Query), true, &query)
if err != nil {
runtime.LogWarning(a.ctx, "Invalid update query:")
runtime.LogWarning(a.ctx, form.Query)
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Invalid update query"), zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Invalid update query %v: %s", form.Query, err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Invalid update query",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return 0
}
@ -54,10 +58,12 @@ func (a *App) UpdateItems(hostKey, dbKey, collKey string, formJson string) int64
if err == nil {
update[param.Type] = unmarshalled
} else {
runtime.LogWarning(a.ctx, "Invalid update parameter value:")
runtime.LogWarning(a.ctx, param.Value)
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Invalid update query"), zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Invalid update parameter value %v: %s", param.Value, err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Invalid update query",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return 0
}
}
@ -72,9 +78,12 @@ func (a *App) UpdateItems(hostKey, dbKey, collKey string, formJson string) int64
}
if err != nil {
runtime.LogWarning(a.ctx, "Encountered an error while performing update:")
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while performing update"), zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Encountered an error while performing update: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error performing update query",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return 0
}

View File

@ -5,7 +5,6 @@ import (
"errors"
"time"
"github.com/ncruces/zenity"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
@ -24,22 +23,38 @@ func (a *App) connectToHost(hostKey string) (*mongo.Client, context.Context, fun
hosts, err := a.Hosts()
if err != nil {
runtime.LogInfof(a.ctx, "Error while getting hosts: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Error while getting hosts"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error getting hosts",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return nil, nil, nil, errors.New("could not retrieve hosts")
}
h := hosts[hostKey]
if len(h.URI) == 0 {
runtime.LogInfof(a.ctx, "Invalid URI (len == 0) for host %s", hostKey)
zenity.Warning("You haven't specified a valid uri for the selected host.", zenity.Title("Invalid query"), zenity.WarningIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Invalid host information",
Message: "You haven't specified a valid uri for the selected host.",
Type: runtime.ErrorDialog,
})
return nil, nil, nil, errors.New("invalid uri")
}
client, err := mongo.NewClient(mongoOptions.Client().ApplyURI(h.URI))
appName := "Rolens v" + a.Env.Version
opts := mongoOptions.Client()
opts.AppName = &appName
opts.ApplyURI(h.URI)
client, err := mongo.NewClient(opts)
if err != nil {
runtime.LogWarningf(a.ctx, "Could not connect to host %s: %s", hostKey, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while connecting to "+h.Name), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error while connecting to " + h.Name,
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return nil, nil, nil, errors.New("could not establish a connection with " + h.Name)
}
@ -60,24 +75,25 @@ func (a *App) OpenConnection(hostKey string) (result OpenConnectionResult) {
result.Databases, err = client.ListDatabaseNames(ctx, bson.M{})
if err != nil {
runtime.LogWarning(a.ctx, "Could not retrieve database names for host "+hostKey)
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while getting databases"), zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Could not retrieve database names for host %s: %s", hostKey, err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error getting database list",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
}
command := bson.M{"serverStatus": 1}
err = client.Database("admin").RunCommand(ctx, command).Decode(&result.Status)
if err != nil {
runtime.LogWarning(a.ctx, "Could not retrieve server status")
runtime.LogWarning(a.ctx, err.Error())
runtime.LogWarningf(a.ctx, "Could not retrieve server status: %s", err.Error())
result.StatusError = err.Error()
}
command = bson.M{"hostInfo": 1}
err = client.Database("admin").RunCommand(ctx, command).Decode(&result.SystemInfo)
if err != nil {
runtime.LogWarning(a.ctx, "Could not retrieve system info")
runtime.LogWarning(a.ctx, err.Error())
runtime.LogWarningf(a.ctx, "Could not retrieve system info: %s", err.Error())
result.SystemInfoError = err.Error()
}

View File

@ -1,7 +1,6 @@
package app
import (
"github.com/ncruces/zenity"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go.mongodb.org/mongo-driver/bson"
)
@ -22,24 +21,32 @@ func (a *App) OpenDatabase(hostKey, dbKey string) (result OpenDatabaseResult) {
command := bson.M{"dbStats": 1}
err = client.Database(dbKey).RunCommand(ctx, command).Decode(&result.Stats)
if err != nil {
runtime.LogWarning(a.ctx, "Could not retrieve database stats for "+dbKey)
runtime.LogWarning(a.ctx, err.Error())
runtime.LogWarningf(a.ctx, "Could not retrieve database stats for %s: %s", dbKey, err.Error())
result.StatsError = err.Error()
}
result.Collections, err = client.Database(dbKey).ListCollectionNames(ctx, bson.D{})
if err != nil {
runtime.LogWarning(a.ctx, "Could not retrieve collection list for db "+dbKey)
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while getting collections"), zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Could not retrieve collection list for db %s: %s", dbKey, err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error getting collection list",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
}
return
}
func (a *App) DropDatabase(hostKey, dbKey string) bool {
err := zenity.Question("Are you sure you want to drop "+dbKey+"?", zenity.Title("Confirm"), zenity.WarningIcon)
if err == zenity.ErrCanceled {
choice, _ := runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Confirm",
Message: "Are you sure you want to drop " + dbKey + "?",
Buttons: []string{"Yes", "Cancel"},
DefaultButton: "Yes",
CancelButton: "Cancel",
})
if choice != "Yes" {
return false
}
@ -51,9 +58,12 @@ func (a *App) DropDatabase(hostKey, dbKey string) bool {
err = client.Database(dbKey).Drop(ctx)
if err != nil {
runtime.LogWarning(a.ctx, "Could not drop db "+dbKey)
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while dropping database"), zenity.ErrorIcon)
runtime.LogWarningf(a.ctx, "Could not drop db %s: %s", dbKey, err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error dropping database",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}

View File

@ -7,7 +7,6 @@ import (
"path"
"strings"
"github.com/ncruces/zenity"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
@ -23,26 +22,38 @@ func (a *App) PerformDump(jsonData string) bool {
var info DumpInfo
err := json.Unmarshal([]byte(jsonData), &info)
if err != nil {
runtime.LogError(a.ctx, "Could not unmarshal dump form")
runtime.LogError(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Could not parse JSON"), zenity.ErrorIcon)
runtime.LogErrorf(a.ctx, "Could not unmarshal dump form: %s", err.Error())
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Malformed JSON",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
hosts, err := a.Hosts()
if err != nil {
zenity.Error(err.Error(), zenity.Title("Error while getting hosts"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error getting hosts",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
host := hosts[info.HostKey]
if !a.Env.HasMongoDump {
zenity.Error("You need to install mongodump to perform a dump.", zenity.Title("Additional tooling required"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Additional tooling required",
Message: "You need to install mongodump to perform a dump.",
Type: runtime.ErrorDialog,
})
return false
}
args := make([]string, 0)
args = append(args, fmt.Sprintf(`--uri="%v"`, host.URI))
fname := path.Join(info.OutDir, info.Filename)
if info.DbKey != "" {
args = append(args, fmt.Sprintf(`--db="%v"`, info.DbKey))
@ -52,7 +63,7 @@ func (a *App) PerformDump(jsonData string) bool {
}
}
args = append(args, fmt.Sprintf(`--out="%v"`, path.Join(info.OutDir, info.Filename)))
args = append(args, fmt.Sprintf(`--out="%v"`, fname))
cmd := exec.Command("mongodump", args...)
var stdout strings.Builder
var stderr strings.Builder
@ -63,9 +74,14 @@ func (a *App) PerformDump(jsonData string) bool {
runtime.LogInfo(a.ctx, "Performing dump, executing command: mongodump "+strings.Join(args, " "))
runtime.LogInfo(a.ctx, "mongodump stdout: "+stdout.String())
runtime.LogInfo(a.ctx, "mongodump sterr: "+stderr.String())
if err != nil {
runtime.LogWarning(a.ctx, "Error while executing mongodump: "+err.Error())
return false
}
return err == nil
println(fname)
a.ui.Reveal(fname)
return true
}

46
internal/app/host_logs.go Normal file
View File

@ -0,0 +1,46 @@
package app
import (
"github.com/wailsapp/wails/v2/pkg/runtime"
"go.mongodb.org/mongo-driver/bson"
)
type HostLogsResult struct {
Total int32 `json:"total"`
Logs []string `json:"logs"`
Error string `json:"error"`
}
func (a *App) HostLogs(hostKey, filter string) (result HostLogsResult) {
client, ctx, close, err := a.connectToHost(hostKey)
if err != nil {
result.Error = "Could not connect to host"
return
}
defer close()
var res bson.M
err = client.Database("admin").RunCommand(ctx, bson.M{"getLog": filter}).Decode(&res)
if err != nil {
runtime.LogWarningf(a.ctx, "Could not get %s logs for %s: %s", filter, hostKey, err.Error())
result.Error = err.Error()
return
}
if res["totalLinesWritten"] != nil {
result.Total = res["totalLinesWritten"].(int32)
} else {
result.Total = 0
}
result.Logs = make([]string, 0)
switch res["log"].(type) {
case bson.A:
for _, v := range res["log"].(bson.A) {
result.Logs = append(result.Logs, v.(string))
}
}
return
}

105
internal/app/host_shell.go Normal file
View File

@ -0,0 +1,105 @@
package app
import (
"fmt"
"net/url"
"os"
"os/exec"
"path"
"github.com/google/uuid"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
type ExecuteShellScriptResult struct {
Output string `json:"output"`
Status int `json:"status"`
ErrorTitle string `json:"errorTitle"`
ErrorDescription string `json:"errorDescription"`
}
func (a *App) ExecuteShellScript(hostKey, dbKey, collKey, script string) (result ExecuteShellScriptResult) {
if !a.Env.HasMongoShell {
result.ErrorTitle = "mongosh not found"
result.ErrorDescription = "The mongosh executable is required to run a shell script. Please see https://www.mongodb.com/docs/mongodb-shell/install/"
return
}
hosts, err := a.Hosts()
if err != nil {
result.ErrorTitle = "Could not get hosts"
result.ErrorDescription = err.Error()
return
}
host, hostFound := hosts[hostKey]
if !hostFound {
result.ErrorTitle = "The specified host does not seem to exist"
return
}
id, err := uuid.NewRandom()
if err != nil {
runtime.LogErrorf(a.ctx, "Shell: failed to generate a UUID: %s", err.Error())
result.ErrorTitle = "Could not generate UUID"
result.ErrorDescription = err.Error()
return
}
dirname := path.Join(a.Env.DataDirectory, "Shell Scripts")
fname := path.Join(dirname, fmt.Sprintf("%s.mongosh.js", id.String()))
if err := os.MkdirAll(dirname, os.ModePerm); err != nil {
runtime.LogWarningf(a.ctx, "Shell: failed to mkdir %s", err.Error())
result.ErrorTitle = "Could not create temporary directory"
result.ErrorDescription = err.Error()
return
}
scriptHeader := fmt.Sprintf("// Namespace: %s.%s\n", dbKey, collKey)
if dbKey != "" {
url, err := url.Parse(host.URI)
if err != nil {
runtime.LogWarningf(a.ctx, "Shell: failed to parse host URI %s: %s", host.URI, err.Error())
result.ErrorTitle = "Could parse host URI"
result.ErrorDescription = err.Error()
return
}
url.Path = "/" + dbKey
scriptHeader = scriptHeader + fmt.Sprintf("db = connect('%s');\n", url.String())
}
if collKey != "" {
scriptHeader = scriptHeader + fmt.Sprintf("coll = db.getCollection('%s');\n", collKey)
}
scriptHeader = scriptHeader + "\n// Start of user script\n"
script = scriptHeader + script
if err := os.WriteFile(fname, []byte(script), os.ModePerm); err != nil {
runtime.LogWarningf(a.ctx, "Shell: failed to write script to %s", err.Error())
result.ErrorTitle = "Could not create temporary script file"
result.ErrorDescription = err.Error()
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
}

View File

@ -8,7 +8,6 @@ import (
"path"
"github.com/google/uuid"
"github.com/ncruces/zenity"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
@ -55,7 +54,11 @@ func (a *App) Hosts() (map[string]Host, error) {
func (a *App) AddHost(jsonData string) string {
hosts, err := a.Hosts()
if err != nil {
zenity.Error(err.Error(), zenity.Title("Error while retrieving hosts"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error getting hosts",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return ""
}
@ -63,21 +66,33 @@ func (a *App) AddHost(jsonData string) string {
err = json.Unmarshal([]byte(jsonData), &newHost)
if err != nil {
runtime.LogErrorf(a.ctx, "Add host: malformed form: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Could not parse JSON"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Malformed JSON",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return ""
}
id, err := uuid.NewRandom()
if err != nil {
runtime.LogErrorf(a.ctx, "Add host: failed to generate a UUID: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Error while generating UUID"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error generating UUID",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return ""
}
hosts[id.String()] = newHost
err = updateHostsFile(a, hosts)
if err != nil {
zenity.Error(err.Error(), zenity.Title("Error while updating host list"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error updating host list",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return ""
}
@ -87,7 +102,11 @@ func (a *App) AddHost(jsonData string) string {
func (a *App) UpdateHost(hostKey string, jsonData string) bool {
hosts, err := a.Hosts()
if err != nil {
zenity.Error(err.Error(), zenity.Title("Error while getting hosts"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error getting host list",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
@ -95,14 +114,22 @@ func (a *App) UpdateHost(hostKey string, jsonData string) bool {
err = json.Unmarshal([]byte(jsonData), &host)
if err != nil {
runtime.LogErrorf(a.ctx, "Could not parse update host JSON: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Could not parse JSON"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Malformed JSON",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
hosts[hostKey] = host
err = updateHostsFile(a, hosts)
if err != nil {
zenity.Error(err.Error(), zenity.Title("Error while updating hosts"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error updating hosts",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
@ -112,12 +139,22 @@ func (a *App) UpdateHost(hostKey string, jsonData string) bool {
func (a *App) RemoveHost(key string) bool {
hosts, err := a.Hosts()
if err != nil {
zenity.Error(err.Error(), zenity.Title("Error while retrieving hosts"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error getting host list",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}
err = zenity.Question("Are you sure you want to remove "+hosts[key].Name+"?", zenity.Title("Confirm"), zenity.WarningIcon)
if err == zenity.ErrCanceled {
choice, _ := runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Confirm",
Message: "Are you sure you want to remove " + hosts[key].Name + "?",
Buttons: []string{"Yes", "Cancel"},
DefaultButton: "Yes",
CancelButton: "Cancel",
})
if choice != "Yes" {
return false
}
@ -125,7 +162,11 @@ func (a *App) RemoveHost(key string) bool {
err = updateHostsFile(a, hosts)
if err != nil {
zenity.Error(err.Error(), zenity.Title("Error while updating hosts"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error updating host list",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return false
}

View File

@ -7,7 +7,6 @@ import (
"os"
"path"
"github.com/ncruces/zenity"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
@ -77,16 +76,14 @@ func (a *App) Views() (ViewStore, error) {
if err != nil {
// It's ok if the file cannot be opened, for example if it is not accessible.
// Therefore no error is returned.
runtime.LogInfo(a.ctx, "views.json file cannot be opened")
runtime.LogInfo(a.ctx, err.Error())
runtime.LogInfof(a.ctx, "views.json file cannot be opened: %s", err.Error())
return views, nil
}
if len(jsonData) > 0 {
err = json.Unmarshal(jsonData, &views)
if err != nil {
runtime.LogInfo(a.ctx, "views.json file contains malformatted JSON data")
runtime.LogInfo(a.ctx, err.Error())
runtime.LogInfof(a.ctx, "views.json file contains malformatted JSON data: %s", err.Error())
return views, errors.New("views.json file contains malformatted JSON data")
}
}
@ -101,13 +98,21 @@ func (a *App) UpdateViewStore(jsonData string) error {
var viewStore ViewStore
err := json.Unmarshal([]byte(jsonData), &viewStore)
if err != nil {
zenity.Error(err.Error(), zenity.Title("Could not parse JSON"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Malformed JSON",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return errors.New("invalid JSON")
}
err = updateViewStore(a, viewStore)
if err != nil {
zenity.Error(err.Error(), zenity.Title("Error while updating view store"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error updating view store",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return errors.New("could not update view store")
}
@ -117,12 +122,22 @@ func (a *App) UpdateViewStore(jsonData string) error {
func (a *App) RemoveView(viewKey string) error {
views, err := a.Views()
if err != nil {
zenity.Error(err.Error(), zenity.Title("Error while getting views"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error getting views",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return errors.New("could not retrieve existing view store")
}
err = zenity.Question("Are you sure you want to remove "+views[viewKey].Name+"?", zenity.Title("Confirm"), zenity.WarningIcon)
if err == zenity.ErrCanceled {
choice, _ := runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Confirm",
Message: "Are you sure you want to remove " + views[viewKey].Name + "?",
Buttons: []string{"Yes", "Cancel"},
DefaultButton: "Yes",
CancelButton: "Cancel",
})
if choice != "Yes" {
return errors.New("operation aborted")
}
@ -130,7 +145,11 @@ func (a *App) RemoveView(viewKey string) error {
err = updateViewStore(a, views)
if err != nil {
zenity.Error(err.Error(), zenity.Title("Error while updating view store"), zenity.ErrorIcon)
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Title: "Error updating view store",
Message: err.Error(),
Type: runtime.ErrorDialog,
})
return errors.New("could not update view store")
}
return nil

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