mirror of
https://github.com/garraflavatra/rolens.git
synced 2025-06-28 13:35:12 +00:00
Compare commits
50 Commits
v0.2.1
...
shell_quer
Author | SHA1 | Date | |
---|---|---|---|
0965344f35
|
|||
62f4b88ea6
|
|||
04221bf63f | |||
24b0df95df
|
|||
0b9f23365b
|
|||
84f1b85356
|
|||
1ecfa0ab20
|
|||
823ae1328b
|
|||
eba7e84fc8
|
|||
3dbf8fc98b
|
|||
88a5b961b1
|
|||
ce9934faa7
|
|||
e53019a4b0
|
|||
05bfd54fb5
|
|||
1c95b86475
|
|||
aa53d0c400
|
|||
8a7518532d
|
|||
0e4c5d474b
|
|||
685ea78376
|
|||
7cd68d3280 | |||
cab19e8a8b
|
|||
cae29d9a71
|
|||
3b4be3ebf6
|
|||
efcc78e3bb
|
|||
9acb89205d
|
|||
5191086d74
|
|||
755b96762d
|
|||
e6b0040821
|
|||
3055f8173e
|
|||
6cb6e209eb
|
|||
79568bc5f4
|
|||
86639f766d
|
|||
d1ce48c773
|
|||
b0d3e31378
|
|||
8bebdfccd8
|
|||
f0ab5288f7
|
|||
aaed7a59f7
|
|||
f5366a9ad5
|
|||
67b820ad47
|
|||
cb89b5923f
|
|||
b73b5f4485 | |||
8ccd1a4ce0
|
|||
eef74b306b
|
|||
3e293910ea
|
|||
8f81e66d17
|
|||
48b26c9df5
|
|||
958a0197a4
|
|||
3893c8dd06
|
|||
51897adf8d
|
|||
c284cb4cfc
|
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -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
4
.gitignore
vendored
@ -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/
|
||||
|
16
CHANGELOG.md
16
CHANGELOG.md
@ -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
|
||||
|
30
README.md
30
README.md
@ -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
161
build.js
Executable 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));
|
@ -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.
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
BIN
build/windows/installer/banner_h.bmp
Normal file
BIN
build/windows/installer/banner_h.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
BIN
build/windows/installer/banner_v.bmp
Normal file
BIN
build/windows/installer/banner_v.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 151 KiB |
@ -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.
|
||||
####
|
||||
|
||||
## 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 the wails tools
|
||||
####
|
||||
|
||||
!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
|
||||
|
@ -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
|
@ -1,6 +1,7 @@
|
||||
---
|
||||
title: Changelog
|
||||
parent: Development
|
||||
summary: The development history of Rolens.
|
||||
parent: Colophon
|
||||
order: 90
|
||||
---
|
||||
|
@ -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/).
|
@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Development
|
||||
summary: In-depth information about Rolens source code.
|
||||
order: 50
|
||||
---
|
||||
|
||||
|
@ -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.
|
7
docs/development/build-directory.md
Normal file
7
docs/development/build-directory.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Build directory
|
||||
parent: Development
|
||||
order: 30
|
||||
---
|
||||
|
||||
{% filecontent "../build/README.md", 2 %}
|
@ -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 |
@ -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
|
||||

|
||||
|
||||
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", "## " %}
|
||||
|
@ -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.
|
||||
|
@ -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"] %}
|
||||
|
@ -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"] %}
|
||||
|
@ -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"] %}
|
||||
|
@ -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:
|
||||
|
||||
|
12
docs/user-guide/shortcuts.liquid
Normal file
12
docs/user-guide/shortcuts.liquid
Normal 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 %}
|
@ -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>
|
||||
|
@ -53,7 +53,7 @@
|
||||
margin-bottom: -1.85rem;
|
||||
}
|
||||
|
||||
.blankstate :global(.btn) {
|
||||
.blankstate :global(.button) {
|
||||
font-size: 1.35rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
57
frontend/src/components/codeeditor.svelte
Normal file
57
frontend/src/components/codeeditor.svelte
Normal 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>
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -30,7 +30,9 @@
|
||||
|
||||
function refresh(hideObjectIndicators, items) {
|
||||
_items = objectToArray(items).map(item => {
|
||||
if (item.children) {
|
||||
item.children = objectToArray(item.children);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
|
@ -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) {
|
||||
|
13
frontend/src/components/icon.svelte
vendored
13
frontend/src/components/icon.svelte
vendored
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 ]),
|
||||
const extensions = [
|
||||
javascript(),
|
||||
EditorState.tabSize.of(4),
|
||||
EditorView.updateListener.of(e => {
|
||||
if (!e.docChanged) {
|
||||
return;
|
||||
}
|
||||
text = e.state.doc.toString();
|
||||
dispatch('updated', { text });
|
||||
}),
|
||||
],
|
||||
});
|
||||
];
|
||||
|
||||
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}
|
||||
/>
|
||||
|
@ -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 = [];
|
||||
|
@ -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>
|
||||
|
||||
|
@ -55,7 +55,7 @@
|
||||
|
||||
<style>
|
||||
ul {
|
||||
overflow-x: scroll;
|
||||
overflow-x: auto;
|
||||
display: flex;
|
||||
list-style: none;
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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;
|
||||
|
36
frontend/src/lib/mongo/log-components.json
Normal file
36
frontend/src/lib/mongo/log-components.json
Normal 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",
|
||||
"-"
|
||||
]
|
7
frontend/src/lib/mongo/loglevels.json
Normal file
7
frontend/src/lib/mongo/loglevels.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"F": "Fatal",
|
||||
"E": "Error",
|
||||
"W": "Warning",
|
||||
"I": "Info",
|
||||
"D": "Debug"
|
||||
}
|
@ -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,8 +268,21 @@ async function refresh() {
|
||||
await database.open();
|
||||
}
|
||||
};
|
||||
|
||||
database.executeShellScript = async function(script) {
|
||||
const result = await ExecuteShellScript(hostKey, dbKey, '', script);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
await refresh();
|
||||
};
|
||||
|
||||
host.executeShellScript = async function(script) {
|
||||
const result = await ExecuteShellScript(hostKey, '', '', script);
|
||||
return result;
|
||||
};
|
||||
|
||||
host.newDatabase = async function() {
|
||||
const name = await dialogs.enterText('Create a database', 'Enter the database name. Note: databases in MongoDB do not exist until they have a collection and an item. Your new database will not persist on the server; fill it to have it created.', '');
|
||||
if (name) {
|
||||
@ -287,7 +291,17 @@ async function refresh() {
|
||||
}
|
||||
};
|
||||
|
||||
await refresh();
|
||||
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() {
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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,7 +263,9 @@
|
||||
{:else}
|
||||
<Grid
|
||||
key="_id"
|
||||
columns={$views[collection.viewKey]?.columns?.map(c => {
|
||||
columns={$views[collection.viewKey]?.columns
|
||||
?.filter(c => c.showInTable)
|
||||
.map(c => {
|
||||
return { key: c.key, title: c.key };
|
||||
}) || []}
|
||||
showHeaders={true}
|
||||
@ -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;
|
||||
|
@ -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,7 +43,8 @@
|
||||
<div class="view" class:empty={!collection}>
|
||||
{#if collection}
|
||||
{#key collection}
|
||||
<TabBar tabs={[
|
||||
<TabBar
|
||||
tabs={[
|
||||
{ key: 'stats', icon: 'chart', title: 'Stats' },
|
||||
{ key: 'find', icon: 'db', title: 'Find' },
|
||||
{ key: 'insert', icon: '+', title: 'Insert' },
|
||||
@ -50,8 +52,10 @@
|
||||
{ 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} />
|
||||
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}
|
||||
|
@ -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>
|
||||
|
@ -73,13 +73,8 @@
|
||||
}
|
||||
|
||||
function showJson() {
|
||||
if (viewType === 'form') {
|
||||
objectViewerData = { ...(newItems[0] || {}) };
|
||||
}
|
||||
else if (viewType === 'table') {
|
||||
objectViewerData = [ ...newItems ];
|
||||
}
|
||||
}
|
||||
|
||||
function addRow(beforeIndex = -1) {
|
||||
if ((beforeIndex === -1) || (typeof beforeIndex !== 'number')) {
|
||||
@ -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 {
|
||||
|
@ -45,7 +45,7 @@
|
||||
</label>
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" class="btn danger">
|
||||
<button type="submit" class="button danger">
|
||||
<Icon name="-" /> Remove
|
||||
</button>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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={[
|
||||
<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} />
|
||||
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}
|
||||
|
194
frontend/src/organisms/connection/host/logs.svelte
Normal file
194
frontend/src/organisms/connection/host/logs.svelte
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
||||
|
146
frontend/src/organisms/connection/shell.svelte
Normal file
146
frontend/src/organisms/connection/shell.svelte
Normal 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>
|
@ -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
4
frontend/wailsjs/go/app/App.d.ts
generated
vendored
@ -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>;
|
||||
|
8
frontend/wailsjs/go/app/App.js
generated
8
frontend/wailsjs/go/app/App.js
generated
@ -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']();
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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,23 +274,63 @@ 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:
|
||||
if columnKeys == nil {
|
||||
columnKeys = make([]string, 0)
|
||||
|
||||
if settings.ViewKey == "list" {
|
||||
els, err := cur.Current.Elements()
|
||||
if err != nil {
|
||||
zenity.Error(err.Error(), zenity.Title("BSON invalid"), zenity.ErrorIcon)
|
||||
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
|
||||
Title: "BSON is invalid",
|
||||
Message: err.Error(),
|
||||
Type: runtime.ErrorDialog,
|
||||
})
|
||||
}
|
||||
|
||||
csvItem := make([]string, 0)
|
||||
|
||||
switch settings.ViewKey {
|
||||
case "list":
|
||||
if csvColumnKeys == nil {
|
||||
csvColumnKeys = make([]string, 0)
|
||||
|
||||
for _, el := range els {
|
||||
if el.Key() == "" {
|
||||
continue
|
||||
@ -220,19 +349,42 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo
|
||||
bsontype.Symbol,
|
||||
bsontype.Timestamp,
|
||||
bsontype.Undefined:
|
||||
csvColumnKeys = append(csvColumnKeys, el.Key())
|
||||
columnKeys = append(columnKeys, el.Key())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, col := range view.Columns {
|
||||
columnKeys = append(columnKeys, col.Key)
|
||||
}
|
||||
}
|
||||
|
||||
runtime.LogDebugf(a.ctx, "Export csvColumnKeys: %v", csvColumnKeys)
|
||||
runtime.LogDebugf(a.ctx, "Export column keys: %v", columnKeys)
|
||||
|
||||
if err := csvWriter.Write(csvColumnKeys); err != nil {
|
||||
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())
|
||||
zenity.Error(err.Error(), zenity.Title("Unable to write item %d to CSV"), zenity.ErrorIcon)
|
||||
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>"))
|
||||
}
|
||||
}
|
||||
|
||||
for _, k := range csvColumnKeys {
|
||||
switch settings.Format {
|
||||
case ExportFormatCsv:
|
||||
csvItem := make([]string, 0)
|
||||
|
||||
for _, k := range columnKeys {
|
||||
r, err := cur.Current.LookupErr(k)
|
||||
if err != nil {
|
||||
csvItem = append(csvItem, "")
|
||||
@ -241,7 +393,11 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
@ -249,13 +405,13 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo
|
||||
csvItem = append(csvItem, fmt.Sprintf("%v", v))
|
||||
}
|
||||
|
||||
default:
|
||||
// @todo
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
if settings.Format == ExportFormatJsonArray {
|
||||
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
|
||||
}
|
||||
|
||||
if pgr != nil {
|
||||
pgr.Complete()
|
||||
pgr.Close()
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
a.ui.Reveal(settings.OutFile)
|
||||
runtime.LogInfo(a.ctx, "Export succeeded")
|
||||
runtime.LogInfof(a.ctx, "Export succeeded: %d items", count)
|
||||
return true
|
||||
}
|
||||
|
31
internal/app/collection_find_export_excel/app.xml
Normal file
31
internal/app/collection_find_export_excel/app.xml
Normal 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>
|
29
internal/app/collection_find_export_excel/contenttypes.xml
Normal file
29
internal/app/collection_find_export_excel/contenttypes.xml
Normal 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>
|
18
internal/app/collection_find_export_excel/core.xml
Normal file
18
internal/app/collection_find_export_excel/core.xml
Normal 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>
|
6
internal/app/collection_find_export_excel/dotrels.xml
Normal file
6
internal/app/collection_find_export_excel/dotrels.xml
Normal 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>
|
20
internal/app/collection_find_export_excel/metadata.xml
Normal file
20
internal/app/collection_find_export_excel/metadata.xml
Normal 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>
|
7
internal/app/collection_find_export_excel/rels.xml
Normal file
7
internal/app/collection_find_export_excel/rels.xml
Normal 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>
|
76
internal/app/collection_find_export_excel/styles.xml
Normal file
76
internal/app/collection_find_export_excel/styles.xml
Normal 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>
|
BIN
internal/app/collection_find_export_excel/template.xlsx
Normal file
BIN
internal/app/collection_find_export_excel/template.xlsx
Normal file
Binary file not shown.
269
internal/app/collection_find_export_excel/theme.xml
Normal file
269
internal/app/collection_find_export_excel/theme.xml
Normal 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>
|
10
internal/app/collection_find_export_excel/workbook.xml
Normal file
10
internal/app/collection_find_export_excel/workbook.xml
Normal 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>
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
46
internal/app/host_logs.go
Normal 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
105
internal/app/host_shell.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
Reference in New Issue
Block a user