Compare commits
112 Commits
Author | SHA1 | Date | |
---|---|---|---|
4b98bc035a | |||
58b2e17a8b | |||
8e13b01b2d | |||
19498eae75 | |||
21089009b4 | |||
1551a5073b | |||
e54a39bb3e | |||
949d609167 | |||
3ac85342bd | |||
c14894fb31 | |||
27ebbbc898 | |||
9ce6a58afc | |||
ca5cdc203a | |||
132df17ff7 | |||
8a1f170121 | |||
b04d0b324a | |||
4e68bf324c | |||
3a325fe064 | |||
9f9ba2de6a | |||
42993b28e9 | |||
6af1f4a028 | |||
fca08479e8 | |||
d90d5bcf63 | |||
421bde13f5 | |||
fd1340f9e4 | |||
426b0b8468 | |||
990c1d3d00 | |||
79417fcaa3 | |||
7687b094c8 | |||
773bcf65fd | |||
82b8f1e300 | |||
3a5bee4a81 | |||
ef9318576a | |||
208db82ba7 | |||
|
ddb3fefae5 | ||
5b35172db9 | |||
7f2adb2df2 | |||
2040a356fd | |||
9eac9ae935 | |||
d1b1c7daa5 | |||
|
db5a526e07 | ||
621af884b5 | |||
a01c56ab69 | |||
1b834de091 | |||
70245bd38b | |||
9be36fda7f | |||
4370672d60 | |||
7cebafeff3 | |||
5b15782d7d | |||
f33cac7f5c | |||
b49faea073 | |||
239af3590d | |||
0e376866a7 | |||
d677b825e1 | |||
3ca561b4b4 | |||
7e5e2127ff | |||
24b3180eb5 | |||
|
3827ae3482 | ||
3556098bc5 | |||
5deab93162 | |||
3d13dd974e | |||
43fce1f27e | |||
77747c10c2 | |||
9d577ac429 | |||
ae5002d356 | |||
6027ab155a | |||
5476df5fe9 | |||
64fb9ed173 | |||
770a204be2 | |||
7a5354c5f4 | |||
61142844fa | |||
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 | |||
8bebdfccd8 | |||
f0ab5288f7 | |||
aaed7a59f7 | |||
f5366a9ad5 | |||
67b820ad47 | |||
cb89b5923f | |||
|
b73b5f4485 | ||
8ccd1a4ce0 | |||
eef74b306b | |||
3e293910ea | |||
8f81e66d17 |
6
.gitattributes
vendored
@ -1,5 +1,9 @@
|
||||
# Let GitHub Linguist ignore documentation and Wails generated files
|
||||
# https://github.com/github-linguist/linguist/blob/master/docs/overrides.md
|
||||
/frontend/src/components/icon.svelte linguist-vendored
|
||||
/frontend/wailsjs/**/* linguist-generated
|
||||
/website/**/* linguist-documentation
|
||||
|
||||
# Exclude certain files from exports
|
||||
/.github/**/* export-ignore
|
||||
/.vscode/**/* export-ignore
|
||||
/website/**/* export-ignore
|
||||
|
32
.github/workflows/ci.yml
vendored
@ -1,10 +1,8 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@ -16,22 +14,21 @@ jobs:
|
||||
platform:
|
||||
- windows-2019
|
||||
- windows-2022
|
||||
- macos-11
|
||||
- macos-12
|
||||
- macos-13
|
||||
- ubuntu-20.04
|
||||
- macos-14
|
||||
- ubuntu-22.04
|
||||
go-version: [1.18]
|
||||
node-version: [16]
|
||||
- ubuntu-24.04
|
||||
go-version: [ 1.23.5 ]
|
||||
node-version: [ 22 ]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache-dependency-path: go.sum
|
||||
@ -45,10 +42,10 @@ jobs:
|
||||
|
||||
- name: Install build dependencies for Linux
|
||||
if: contains(matrix.platform, 'ubuntu')
|
||||
run: sudo apt-get install gtk+-3.0 webkit2gtk-4.0
|
||||
run: sudo apt-get update && sudo apt-get install libgtk-3-0 libwebkit2gtk-4.0-dev gcc-aarch64-linux-gnu
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
@ -67,14 +64,17 @@ jobs:
|
||||
run: ./build/linux/ci_generate.sh "${{ matrix.platform }}"
|
||||
|
||||
- name: Upload generated binaries
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
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
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build
|
||||
if: ${{ always() }}
|
||||
|
||||
@ -91,7 +91,7 @@ jobs:
|
||||
run: build/ci_bundle.sh
|
||||
|
||||
- name: Upload the bundle as an artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rolens-bundle
|
||||
path: bundle
|
||||
|
16
.github/workflows/docs.yml
vendored
@ -22,24 +22,26 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "16"
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd website; npm ci
|
||||
run: npm ci
|
||||
working-directory: website
|
||||
|
||||
- name: Build site
|
||||
run: cd website; npm run build
|
||||
run: npm run build
|
||||
working-directory: website
|
||||
|
||||
- name: Upload built website
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./website-dist
|
||||
|
||||
@ -54,4 +56,4 @@ jobs:
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
uses: actions/deploy-pages@v4
|
||||
|
10
.github/workflows/lint.yml
vendored
@ -1,19 +1,17 @@
|
||||
name: Linter
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
eslint:
|
||||
name: Run ESlint
|
||||
name: Run ESLint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
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/
|
||||
|
2
.vscode/settings.json
vendored
@ -6,7 +6,7 @@
|
||||
"editor.tabSize": 4,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"editor.suggest.snippetsPreventQuickSuggestions": false
|
||||
},
|
||||
|
36
CHANGELOG.md
@ -1,3 +1,36 @@
|
||||
## [v0.3.0]
|
||||
|
||||
New features:
|
||||
|
||||
* Added log view (#53, #54).
|
||||
* Added a shell script editor (#37), plus export/import feature.
|
||||
* Added collection duplication feature (#63).
|
||||
* Find view: paste ID and press Enter (#55).
|
||||
|
||||
Patches:
|
||||
|
||||
* Preserve state after switching to another tab (#56).
|
||||
* Find view: ask for confirmation before negligently deleting documents when the user has clicked the '-' button (#58).
|
||||
* Set a deadline for counting documents, and added a button to count documents if the deadline has been exceeded.
|
||||
* Changed os.ModePerm (777) file permissions to 644 and 755.
|
||||
* UI improvements.
|
||||
* Bumped minimum reqiured Go version from 1.18 to 1.20.
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Build script recreates the build output directory after it has been removed (#62).
|
||||
* After dropping a collection or database, the hosttree was not reloaded correctly. Now, dropped databases and collections are correctly removed from the host tree.
|
||||
|
||||
## [v0.2.2]
|
||||
|
||||
* 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 +50,9 @@
|
||||
|
||||
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
|
||||
[v0.3.0]: https://github.com/garraflavatra/rolens/releases/tag/v0.3.0
|
||||
|
36
README.md
@ -8,7 +8,7 @@ Robust, blazing-fast, comprehensive, yet simple [MongoDB](https://www.mongodb.co
|
||||
|
||||
## Why another MongoDB client?
|
||||
|
||||
This project arose from all flaws of similar tools many of which are slow, complicated, heavy, and fairly unwieldy. They mostly require a reasonably high level of knowledge on how to operate the program.
|
||||
This project arose from all flaws of similar tools many of which are slow, complicated, heavy, and fairly unwieldy. They mostly require quite a high level of knowledge on how to operate the program.
|
||||
|
||||
**Rolens aims to be the intuitive, lightweight counterpart of these overengineered tools.**
|
||||
|
||||
@ -17,7 +17,7 @@ This project arose from all flaws of similar tools many of which are slow, compl
|
||||
- [x] **Low overhead**: Typical query results against a local database are returned whithin milliseconds.
|
||||
- [x] **Intuitive interface**: You know MongoDB? You know Rolens.
|
||||
|
||||

|
||||

|
||||
|
||||
This project is heavily inspired by the excellent [MongoHub](https://github.com/bububa/MongoHub-Mac) application, which sadly has not been updated since 2011.
|
||||
|
||||
@ -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,12 +52,38 @@ Rolens is designed to be as intuitive as possible. But if something is unclear n
|
||||
|
||||
Feel free to contact me if you have questions! [Send an e-mail.](mailto:romein@vburen.nl)
|
||||
|
||||
## 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`
|
||||
- Write and execute shell scripts
|
||||
* 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
|
||||
|
||||
## Author and license
|
||||
|
||||
© [Romein van Buren](mailto:romein@vburen.nl) 2023. The source code and compiled binaries are released under the GNU GPLv3 license — see [`LICENSE`](./LICENSE) for the full license text.
|
||||
© [Romein van Buren](mailto:romein@vburen.nl) 2022-2025. The source code and compiled binaries are released under the GNU GPLv3 license — see [`LICENSE`](./LICENSE) for the full license text.
|
||||
|
||||
## 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/).
|
||||
|
184
build.js
Executable file
@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { execSync, spawn } = require('child_process');
|
||||
const { readFileSync, statSync, rmdirSync, mkdirSync } = 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.20 is installed.
|
||||
|
||||
try {
|
||||
const goMinorVersion = /go1\.([0-9][0-9])/.exec(
|
||||
execSync('go version').toString()
|
||||
)?.pop();
|
||||
|
||||
if (isNullish(goMinorVersion) || (parseInt(goMinorVersion) < 20)) {
|
||||
throw new Error();
|
||||
}
|
||||
} catch {
|
||||
missingDependencies.push({ name: 'Go ^1.20', 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 {}
|
||||
try {
|
||||
mkdirSync('./build/bin');
|
||||
}
|
||||
catch (err) {
|
||||
console.log('Failed to create build output directory!');
|
||||
}
|
||||
|
||||
// Build Rolens.
|
||||
|
||||
console.log(`Building Rolens ${version}...`);
|
||||
console.log();
|
||||
|
||||
const proc = spawn('wails', [ 'build', '-clean', isWindows ? '-nsis' : '' ]);
|
||||
|
||||
if (!quiet) {
|
||||
const suppressMessages = [
|
||||
'Wails CLI',
|
||||
'If Wails is useful',
|
||||
'https://github.com/sponsors/leaanthony',
|
||||
];
|
||||
|
||||
proc.stdout.on('data', data => {
|
||||
for (let i = 0; i < suppressMessages.length; i++) {
|
||||
if (data.toString().indexOf(suppressMessages[i]) !== -1) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
process.stdout.write(data);
|
||||
});
|
||||
|
||||
proc.stderr.on('data', data => process.stderr.write(data));
|
||||
}
|
||||
|
||||
proc.on('exit', code => {
|
||||
console.log();
|
||||
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.
|
||||
#
|
||||
@ -13,12 +12,12 @@
|
||||
# - ubuntu-22.04
|
||||
#
|
||||
# Bundles to choose from:
|
||||
# - rolens-macos-11-amd64.tar.gz
|
||||
# - rolens-macos-11-arm64.tar.gz
|
||||
# - rolens-macos-12-amd64.tar.gz
|
||||
# - rolens-macos-12-arm64.tar.gz
|
||||
# - rolens-macos-13-amd64.tar.gz
|
||||
# - rolens-macos-13-arm64.tar.gz
|
||||
# - rolens-macos-11-amd64.zip
|
||||
# - rolens-macos-11-arm64.zip
|
||||
# - rolens-macos-12-amd64.zip
|
||||
# - rolens-macos-12-arm64.zip
|
||||
# - rolens-macos-13-amd64.zip
|
||||
# - rolens-macos-13-arm64.zip
|
||||
# - rolens-ubuntu-20.04-amd64.tar.gz
|
||||
# - rolens-ubuntu-22.04-amd64.tar.gz
|
||||
# - rolens-windows-2019-amd64.zip
|
||||
@ -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.zip "bundle/rolens-$version-macos-11+-amd64.zip"
|
||||
mv artifacts/*/rolens-macos-11-arm64.zip "bundle/rolens-$version-macos-11+-arm64.zip"
|
||||
|
||||
# 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"
|
||||
|
@ -13,48 +13,27 @@ cat > build/darwin/dmg_settings.json << EOF
|
||||
"background": "$(pwd)/build/darwin/dmg_background.png",
|
||||
"icon-size": 100,
|
||||
"window": {
|
||||
"size": { "width": 155, "height": 250 },
|
||||
"position": { "x": 360, "y": 360 }
|
||||
"size": { "width": 750, "height": 400 }
|
||||
},
|
||||
"contents": [
|
||||
{ "x": 750, "y": 500, "type": "link", "path": "/Applications" },
|
||||
{ "x": 595, "y": 250, "type": "file", "path": "$(pwd)/build/bin/Rolens.app" }
|
||||
{ "x": 600, "y": 175, "type": "link", "path": "/Applications" },
|
||||
{ "x": 150, "y": 175, "type": "file", "path": "$(pwd)/build/bin/Rolens.app" }
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
# AMD/Intel
|
||||
wails build -platform darwin/amd64
|
||||
# create-dmg \
|
||||
# --volname Rolens \
|
||||
# --window-size 155 250 \
|
||||
# --volicon build/appicon.png \
|
||||
# --eula LICENSE \
|
||||
# --app-drop-link 750 500 \
|
||||
# --icon-size 100 \
|
||||
# --background build/darwin/dmg_background.png \
|
||||
# --add-file Rolens.app build/bin/Rolens.app 595 250 \
|
||||
# build/bin/Rolens.dmg emptydir
|
||||
# appdmg build/darwin/dmg_settings.json build/bin/Rolens.dmg
|
||||
tar -czvf releases/rolens-$1-amd64.tar.gz --directory build/bin Rolens.app
|
||||
appdmg build/darwin/dmg_settings.json build/bin/Rolens.dmg
|
||||
zip -j releases/rolens-$1-amd64.zip build/bin/Rolens.dmg
|
||||
|
||||
# Cleanup
|
||||
rm -rf build/bin/Rolens.app
|
||||
rm -rf build/bin/Rolens.dmg
|
||||
|
||||
# ARM/AppleM1
|
||||
wails build -platform darwin/arm64
|
||||
# create-dmg \
|
||||
# --volname Rolens \
|
||||
# --window-size 155 250 \
|
||||
# --volicon build/appicon.png \
|
||||
# --eula LICENSE \
|
||||
# --app-drop-link 750 500 \
|
||||
# --icon-size 100 \
|
||||
# --background build/darwin/dmg_background.png \
|
||||
# --add-file Rolens.app build/bin/Rolens.app 595 250 \
|
||||
# build/bin/Rolens.dmg emptydir
|
||||
# appdmg build/darwin/dmg_settings.json build/bin/Rolens.dmg
|
||||
tar -czvf releases/rolens-$1-arm64.tar.gz --directory build/bin Rolens.app
|
||||
appdmg build/darwin/dmg_settings.json build/bin/Rolens.dmg
|
||||
zip -j releases/rolens-$1-arm64.zip build/bin/Rolens.dmg
|
||||
|
||||
# Cleanup
|
||||
rm -rf build/bin/Rolens.app
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 16 KiB |
@ -28,5 +28,5 @@ Feel free to contact me if you have questions! Send an e-mail to romein@vburen.n
|
||||
Author and license
|
||||
------------------
|
||||
|
||||
© Romein van Buren 2023. The source code and compiled binaries are released
|
||||
© Romein van Buren 2022-2025. The source code and compiled binaries are released
|
||||
under the GNU GPLv3 license — see LICENSE for the full license text.
|
||||
|
@ -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
After Width: | Height: | Size: 25 KiB |
BIN
build/windows/installer/banner_v.bmp
Normal file
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
|
||||
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
|
||||
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
|
||||
## from outside of Wails for debugging and development of the installer.
|
||||
##
|
||||
|
||||
## For development first make a wails nsis build to populate the "wails_tools.nsh":
|
||||
## > wails build --target windows/amd64 --nsis
|
||||
## Then you can call makensis on this file with specifying the path to your binary:
|
||||
@ -16,28 +15,24 @@ Unicode true
|
||||
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
|
||||
## For a installer with both architectures:
|
||||
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
|
||||
####
|
||||
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
|
||||
####
|
||||
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
|
||||
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
|
||||
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
|
||||
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
|
||||
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
|
||||
###
|
||||
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||
####
|
||||
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
|
||||
####
|
||||
## Include the wails tools
|
||||
####
|
||||
|
||||
## The following information is taken from the ProjectInfo file, but can be overwritten here.
|
||||
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
|
||||
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
|
||||
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
|
||||
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
|
||||
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
|
||||
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
|
||||
|
||||
!include "wails_tools.nsh"
|
||||
|
||||
# The version information for this two must consist of 4 parts
|
||||
VIProductVersion "${INFO_PRODUCTVERSION}.0"
|
||||
VIFileVersion "${INFO_PRODUCTVERSION}.0"
|
||||
|
||||
# Product information
|
||||
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
|
||||
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
|
||||
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
|
||||
@ -45,23 +40,36 @@ VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
|
||||
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
|
||||
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
|
||||
|
||||
!include "MUI.nsh"
|
||||
|
||||
!include "MUI2.nsh"
|
||||
!define MUI_ICON "..\icon.ico"
|
||||
!define MUI_UNICON "..\icon.ico"
|
||||
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
|
||||
|
||||
# Bitmap on the left side of the welcome page. Must be 164x314 pixels in size.
|
||||
!define MUI_HEADERIMAGE
|
||||
!define MUI_HEADERIMAGE_BITMAP ".\banner_h.bmp"
|
||||
!define MUI_WELCOMEFINISHPAGE_BITMAP ".\banner_v.bmp"
|
||||
!define MUI_WELCOMEPAGE_TITLE "Welcome to the Rolens installer!"
|
||||
|
||||
# Finish page information
|
||||
!define MUI_FINISHPAGE_RUN "$INSTDIR\${INFO_PROJECTNAME}.exe"
|
||||
!define MUI_FINISHPAGE_RUN_TEXT "Start Rolens when finished"
|
||||
!define MUI_FINISHPAGE_TITLE "Thanks for installing!"
|
||||
!define MUI_FINISHPAGE_LINK "Visit Rolens on the Web!"
|
||||
!define MUI_FINISHPAGE_LINK_LOCATION "https://garraflavatra.github.io/rolens/"
|
||||
!define MUI_FINISHPAGE_LINK_COLOR 880000
|
||||
|
||||
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
|
||||
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
|
||||
|
||||
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
|
||||
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
|
||||
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
|
||||
!insertmacro MUI_PAGE_INSTFILES # Installing page.
|
||||
!insertmacro MUI_PAGE_FINISH # Finished installation page.
|
||||
!insertmacro MUI_PAGE_WELCOME
|
||||
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt"
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
|
||||
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
|
||||
#!uninstfinalize 'signtool --file "%1"'
|
||||
@ -69,7 +77,7 @@ VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
|
||||
|
||||
Name "${INFO_PRODUCTNAME}"
|
||||
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
|
||||
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
|
||||
InstallDir "$PROGRAMFILES64\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
|
||||
ShowInstDetails show # This will always show the installation details.
|
||||
|
||||
Function .onInit
|
||||
@ -80,7 +88,7 @@ Section
|
||||
!insertmacro wails.webview2runtime
|
||||
|
||||
SetOutPath $INSTDIR
|
||||
|
||||
|
||||
!insertmacro wails.files
|
||||
|
||||
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
@ -89,7 +97,7 @@ Section
|
||||
!insertmacro wails.writeUninstaller
|
||||
SectionEnd
|
||||
|
||||
Section "uninstall"
|
||||
Section "uninstall"
|
||||
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
|
||||
|
||||
RMDir /r $INSTDIR
|
||||
|
@ -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,12 +0,0 @@
|
||||
---
|
||||
title: Colophon
|
||||
order: 900
|
||||
---
|
||||
|
||||
Rolens is © [Romein van Buren](mailto:romein@vburen.nl) 2023. The source code and compiled binaries are released under the GNU GPLv3 license — see [`LICENSE`](https://github.com/garraflavatra/rolens/blob/main/LICENSE) for the full license text.
|
||||
|
||||
## Credits
|
||||
|
||||
* [Wails](https://wails.io/) facilitates the build process for multiple OSes.
|
||||
* Icons are from [Feather Icons](https://feathericons.com/) by [Cole Bemis](https://github.com/colebemis).
|
||||
* Vector drawings come from [unDraw](https://undraw.co/).
|
@ -1,6 +1,7 @@
|
||||
---
|
||||
title: Changelog
|
||||
parent: Development
|
||||
summary: The development history of Rolens.
|
||||
parent: Colophon
|
||||
order: 90
|
||||
---
|
||||
|
13
docs/colophon/index.md
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
title: Colophon
|
||||
order: 900
|
||||
---
|
||||
|
||||
Rolens is © [Romein van Buren](mailto:romein@vburen.nl) 2022-2025. The source code and compiled binaries are released under the GNU GPLv3 license — see [`LICENSE`](https://github.com/garraflavatra/rolens/blob/main/LICENSE) for the full license text.
|
||||
|
||||
## 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,38 +1,27 @@
|
||||
# Installation procedure
|
||||
---
|
||||
title: Advanced build process
|
||||
parent: Development
|
||||
order: 10
|
||||
---
|
||||
|
||||
## System requirements
|
||||
If you just want to install Rolens, please refer to the [installation document](/installation/). You can read this guide to get a detailed overview of the build procedure.
|
||||
|
||||
Rolens can run on the following operating systems:
|
||||
## Prerequisites
|
||||
|
||||
* Windows 10/11 amd64/arm64
|
||||
* Linux amd64/arm64
|
||||
* macOS 10.13+ amd64 (Intel)
|
||||
* macOS 11.0+ arm64 (Apple Silicon)
|
||||
Rolens is written in Go, so you should download the Go compiler from [the download page](https://go.dev/dl/). The minimum version required is 1.20. You can confirm whether it's installed correctly by running `go version` and checking that it outputs something similar to `go1.20.4` or higher.
|
||||
|
||||
## Pre-compiled binaries
|
||||
|
||||
You can obtain a pre-compiled Rolens binary for macOS or installer for Windows from the [release page](https://github.com/garraflavatra/rolens/releases/latest).
|
||||
|
||||
## From source
|
||||
|
||||
Rolens is open-source software, which means that you can compile it from source on your own machine by cloning [the repository](https://github.com/garraflavatra/rolens).
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Rolens is written in Go, so you should download the Go compiler from [the download page](https://go.dev/dl/). The minimum version required is 1.18. You can confirm whether it's installed correctly by running `go version` and checking that it outputs something similar to `go1.18.2`.
|
||||
|
||||
Furthermore, you need to have [Wails ^3.1](https://wails.io/docs/gettingstarted/installation) installed: `go install github.com/wailsapp/wails/v2/cmd/wails@latest`. Wails may have platform-specific dependencies; you can consult `wails doctor` to find out what dependencies Wails needs and how to install them.
|
||||
Furthermore, you need to have [Wails ^3.1](https://wails.io/docs/gettingstarted/installation) installed: `go install github.com/wailsapp/wails/v2/cmd/wails@latest`. Wails may have platform-specific dependencies; you can consult [`wails doctor`](https://wails.io/docs/reference/cli#doctor) to find out what dependencies Wails needs and how to install them.
|
||||
|
||||
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
@ -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.
|
Before Width: | Height: | Size: 451 KiB |
BIN
docs/images/home-impression.webp
Normal file
After Width: | Height: | Size: 145 KiB |
BIN
docs/images/shell.webp
Normal file
After Width: | Height: | Size: 117 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:
|
||||
@ -15,6 +15,14 @@ This project arose from all flaws of similar tools many of which are slow, compl
|
||||
- **Low overhead**: Typical query results against a local database are returned whithin milliseconds.
|
||||
- **Intuitive interface**: You know MongoDB? You know Rolens.
|
||||
|
||||

|
||||

|
||||
|
||||
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:
|
||||
|
||||
|
17
docs/user-guide/shell.md
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Shell
|
||||
summary: "Write and execute MongoDB shell scripts within Rolens"
|
||||
parent: User guide
|
||||
order: 60
|
||||
stub: true
|
||||
---
|
||||
|
||||
Rolens has a shell feature: it provides an editor for writing shell scripts and executing them against a local or external host, database, or even a single collection. You can find it under the _Shell_ tab.
|
||||
|
||||
_Note: this feature is currently in development and will be shipped with version 0.3.0. You can [follow the development on GitHub](https://github.com/garraflavatra/rolens)._
|
||||
|
||||

|
||||
|
||||
## Requirements
|
||||
|
||||
To use the script editor, you need to install the official [`mongosh` tool](https://www.mongodb.com/docs/mongodb-shell/) from MongoDB. You can [download it](https://www.mongodb.com/try/download/shell) from the MongoDB web site, or run `brew install mongosh` if you use a Mac.
|
17
docs/user-guide/shortcuts.liquid
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Shortcut reference
|
||||
parent: User guide
|
||||
order: 900
|
||||
---
|
||||
|
||||
<p>You can use the following shortcuts to manage hosts and connections.</p>
|
||||
|
||||
<p>
|
||||
<a href="javascript:window.print()">Print this page</a> if you would like to
|
||||
mount it on your wall for quick reference!
|
||||
</p>
|
||||
|
||||
{% for item in shortcuts %}
|
||||
<h2>{{ item[0] }}</h2>
|
||||
{% render "shortcuts", shortcuts: item[1] %}
|
||||
{% endfor %}
|
@ -7,14 +7,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="app-loading">
|
||||
<div class="ring">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dialogoutlets"></div>
|
||||
<script src="./src/main.js" type="module"></script>
|
||||
</body>
|
||||
|
2521
frontend/package-lock.json
generated
@ -17,23 +17,20 @@
|
||||
"date-fns": "^2.30.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.1",
|
||||
"@garraflavatra/yeslint": "^1.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-svelte3": "github:johbog/eslint-config-svelte3",
|
||||
"svelte": "^3.59.1",
|
||||
"vite": "^3.2.7"
|
||||
"svelte": "^4.0.0",
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "svelte3",
|
||||
"ignorePatterns": ["dist", "wailsjs"],
|
||||
"extends": "./node_modules/@garraflavatra/yeslint/configs/svelte.js",
|
||||
"ignorePatterns": [
|
||||
"dist",
|
||||
"wailsjs"
|
||||
],
|
||||
"rules": {
|
||||
"svelte/html-quotes": [
|
||||
"warn",
|
||||
{
|
||||
"prefer": "double",
|
||||
"dynamic": { "quoted": false }
|
||||
}
|
||||
],
|
||||
"svelte/no-useless-mustaches": "off",
|
||||
"svelte/no-extra-reactive-curlies": "off"
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
7fc6d7b66151030191ded7136d96970e
|
||||
015746ba33749cd2864cd336088387ef
|
@ -1,17 +1,15 @@
|
||||
<script>
|
||||
import BlankState from '$components/blankstate.svelte';
|
||||
import ContextMenu from '$components/contextmenu.svelte';
|
||||
import dialogs from '$lib/dialogs';
|
||||
import contextMenu from '$lib/stores/contextmenu';
|
||||
import environment from '$lib/stores/environment';
|
||||
import hostTree from '$lib/stores/hosttree';
|
||||
import applicationInited from '$lib/stores/inited';
|
||||
import windowTitle from '$lib/stores/windowtitle';
|
||||
import Connection from '$organisms/connection/index.svelte';
|
||||
import { EventsOn } from '$wails/runtime';
|
||||
import { tick } from 'svelte';
|
||||
import AboutDialog from './dialogs/about.svelte';
|
||||
import SettingsDialog from './dialogs/settings/index.svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
import BlankState from '$components/blankstate.svelte';
|
||||
import Connection from '$organisms/connection/index.svelte';
|
||||
import ContextMenu from '$components/contextmenu.svelte';
|
||||
|
||||
import contextMenu from '$lib/stores/contextmenu.js';
|
||||
import hostTree from '$lib/stores/hosttree.js';
|
||||
import applicationInited from '$lib/stores/inited.js';
|
||||
import windowTitle from '$lib/stores/windowtitle.js';
|
||||
|
||||
let showWelcomeScreen = undefined;
|
||||
|
||||
@ -28,29 +26,18 @@
|
||||
await tick();
|
||||
hostTree.newHost();
|
||||
}
|
||||
|
||||
function showAboutDialog() {
|
||||
dialogs.new(AboutDialog);
|
||||
}
|
||||
|
||||
function showSettings() {
|
||||
dialogs.new(SettingsDialog);
|
||||
}
|
||||
|
||||
EventsOn('OpenPreferences', showSettings);
|
||||
EventsOn('OpenAboutModal', showAboutDialog);
|
||||
</script>
|
||||
|
||||
<svelte:window on:contextmenu|preventDefault />
|
||||
|
||||
<div id="root" class="platform-{$environment?.platform}">
|
||||
<div id="root">
|
||||
<div class="titlebar">{$windowTitle}</div>
|
||||
|
||||
{#if $applicationInited && (showWelcomeScreen !== undefined)}
|
||||
<main class:empty={showWelcomeScreen}>
|
||||
<main class:empty={showWelcomeScreen} in:fly={{ y: 10 }}>
|
||||
{#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 />
|
||||
@ -65,29 +52,23 @@
|
||||
|
||||
<style>
|
||||
.titlebar {
|
||||
height: 0;
|
||||
background-color: #00002a;
|
||||
height: var(--titlebar-height);
|
||||
--wails-draggable: drag;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
#root.platform-darwin .titlebar {
|
||||
height: var(--darwin-titlebar-height);
|
||||
overflow: hidden;
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
main {
|
||||
height: 100vh;
|
||||
height: calc(100vh - var(--titlebar-height));
|
||||
display: grid;
|
||||
grid-template: 1fr / minmax(300px, 0.3fr) 1fr;
|
||||
}
|
||||
main.empty {
|
||||
grid-template: 1fr / 1fr;
|
||||
}
|
||||
#root.platform-darwin main {
|
||||
height: calc(100vh - var(--darwin-titlebar-height));
|
||||
}
|
||||
|
||||
main > :global(*) {
|
||||
overflow: auto;
|
||||
@ -97,12 +78,4 @@
|
||||
main > :global(.addressbar) {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
|
||||
.databaselist {
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.btn.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;
|
||||
}
|
||||
|
@ -50,7 +50,7 @@
|
||||
<svelte:window on:keydown={keydown} />
|
||||
|
||||
{#if items && position}
|
||||
<div class="backdrop" on:pointerdown={close}></div>
|
||||
<div class="backdrop" on:pointerdown={close} />
|
||||
<nav>
|
||||
<ul class="contextmenu" role="" style:left="{position[0]}px" style:top="{position[1]}px">
|
||||
{#each items as item, index}
|
||||
@ -101,7 +101,7 @@
|
||||
text-align: left;
|
||||
}
|
||||
button.selected {
|
||||
background-color: #00008b;
|
||||
background-color: var(--selection);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
<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';
|
||||
@ -9,6 +8,7 @@
|
||||
|
||||
export let text = '';
|
||||
export let editor = undefined;
|
||||
export let extensions = [];
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let editorParent;
|
||||
@ -18,7 +18,6 @@
|
||||
extensions: [
|
||||
basicSetup,
|
||||
keymap.of([ indentWithTab, indentOnInput ]),
|
||||
javascript(),
|
||||
EditorState.tabSize.of(4),
|
||||
EditorView.updateListener.of(e => {
|
||||
if (!e.docChanged) {
|
||||
@ -27,6 +26,7 @@
|
||||
text = e.state.doc.toString();
|
||||
dispatch('updated', { text });
|
||||
}),
|
||||
...extensions,
|
||||
],
|
||||
});
|
||||
|
||||
@ -36,19 +36,11 @@
|
||||
state: editorState,
|
||||
});
|
||||
|
||||
editor.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: editor.state.doc.length,
|
||||
insert: text,
|
||||
},
|
||||
});
|
||||
|
||||
dispatch('inited', { editor });
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={editorParent} class="editor"></div>
|
||||
<div bind:this={editorParent} class="editor" />
|
||||
|
||||
<style>
|
||||
.editor {
|
@ -1,10 +1,10 @@
|
||||
<script>
|
||||
import { daysAbbr, months } from '$lib/constants';
|
||||
import { daysAbbr, months } from '$lib/constants.js';
|
||||
import { addDays, getWeek, isDate, isSameDay, startOfWeek } from 'date-fns';
|
||||
import { onMount } from 'svelte';
|
||||
import Clock from './clock.svelte';
|
||||
import Icon from './icon.svelte';
|
||||
import Modal from './modal.svelte';
|
||||
import Clock from '../clock.svelte';
|
||||
import Icon from '../icon.svelte';
|
||||
import Modal from '../modal.svelte';
|
||||
|
||||
export let value;
|
||||
export let show = false;
|
||||
@ -97,7 +97,7 @@
|
||||
<table class="calendar">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th />
|
||||
{#each daysAbbr as dayName}
|
||||
<th>{dayName}</th>
|
||||
{/each}
|
||||
@ -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>
|
||||
@ -173,7 +173,7 @@
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.calendar .day button.active {
|
||||
background-color: #00008b;
|
||||
background-color: var(--selection);
|
||||
color: #fff;
|
||||
}
|
||||
.calendar .day button.notinmonth {
|
@ -1,16 +1,22 @@
|
||||
<script>
|
||||
import { OpenDirectory } from '$wails/go/ui/UI';
|
||||
import { ChooseDirectory } from '$wails/go/app/App.js';
|
||||
|
||||
export let value = '';
|
||||
export let id = '';
|
||||
export let title = 'Choose a directory';
|
||||
|
||||
async function selectDir() {
|
||||
value = await OpenDirectory(title) || value;
|
||||
value = await ChooseDirectory(title) || value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<input type="text" on:pointerdown={selectDir} readonly {id} {value} />
|
||||
<input
|
||||
type="text"
|
||||
on:pointerdown={selectDir}
|
||||
readonly
|
||||
{id}
|
||||
{value}
|
||||
/>
|
||||
|
||||
<style>
|
||||
input {
|
@ -1,10 +1,10 @@
|
||||
<script>
|
||||
import input from '$lib/actions/input';
|
||||
import { canBeObjectId, numericInputTypes } from '$lib/mongo';
|
||||
import input from '$lib/actions/input.js';
|
||||
import { canBeObjectId, numericInputTypes } from '$lib/mongo/index.js';
|
||||
import { ObjectId } from 'bson';
|
||||
import { onMount } from 'svelte';
|
||||
import Datepicker from './datepicker.svelte';
|
||||
import Icon from './icon.svelte';
|
||||
import Icon from '../icon.svelte';
|
||||
|
||||
export let column = {};
|
||||
export let value = undefined;
|
||||
@ -70,7 +70,12 @@
|
||||
<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"
|
||||
@ -79,7 +84,11 @@
|
||||
use:input={{ type, onValid, onInvalid, mandatory, autofocus }}
|
||||
/>
|
||||
{:else if numericInputTypes.includes(type)}
|
||||
<input type="number" bind:value use:input={{ type, onValid, onInvalid, mandatory, autofocus }} />
|
||||
<input
|
||||
type="number"
|
||||
bind:value
|
||||
use:input={{ type, onValid, onInvalid, mandatory, autofocus }}
|
||||
/>
|
||||
{:else if type === 'bool'}
|
||||
<select bind:value on:change={selectChange} bind:this={selectInput}>
|
||||
<option value={undefined} disabled={mandatory}>Unset</option>
|
||||
@ -98,18 +107,38 @@
|
||||
<Icon name="edit" />
|
||||
</button>
|
||||
{/if}
|
||||
<button class="button-small" type="button" title="Generate random object id" on:click={generateObjectId}>
|
||||
<button
|
||||
class="button-small"
|
||||
type="button"
|
||||
title="Generate random object id"
|
||||
on:click={generateObjectId}
|
||||
>
|
||||
<Icon name="reload" />
|
||||
</button>
|
||||
{:else if type === 'date'}
|
||||
<button class="button-small" type="button" title="Edit date" on:click={() => showDatepicker = true}>
|
||||
<button
|
||||
class="button-small"
|
||||
type="button"
|
||||
title="Edit date"
|
||||
on:click={() => showDatepicker = true}
|
||||
>
|
||||
<Icon name="edit" />
|
||||
</button>
|
||||
<button class="button-small" type="button" title="Set date to now" on:click={() => value = new Date()}>
|
||||
<button
|
||||
class="button-small"
|
||||
type="button"
|
||||
title="Set date to now"
|
||||
on:click={() => value = new Date()}
|
||||
>
|
||||
<Icon name="o" />
|
||||
</button>
|
||||
{/if}
|
||||
<button class="button-small" type="button" title="Reset field to default value" on:click={() => value = undefined}>
|
||||
<button
|
||||
class="button-small"
|
||||
type="button"
|
||||
title="Reset field to default value"
|
||||
on:click={() => value = undefined}
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
30
frontend/src/components/editors/objecteditor.svelte
Normal file
@ -0,0 +1,30 @@
|
||||
<script>
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { onMount } from 'svelte';
|
||||
import CodeEditor from './codeeditor.svelte';
|
||||
|
||||
export let text = '';
|
||||
export let editor = undefined;
|
||||
export let readonly = false;
|
||||
|
||||
const extensions = [ javascript() ];
|
||||
|
||||
onMount(() => {
|
||||
editor.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: editor.state.doc.length,
|
||||
insert: text,
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<CodeEditor
|
||||
bind:editor
|
||||
bind:text
|
||||
on:inited
|
||||
on:updated
|
||||
{extensions}
|
||||
{readonly}
|
||||
/>
|
@ -1,9 +1,9 @@
|
||||
<script>
|
||||
import { pathsAreEqual, resolveKeypath, setValue } from '$lib/objects';
|
||||
import contextMenu from '$lib/stores/contextmenu';
|
||||
import { pathsAreEqual, resolveKeypath, setValue } from '$lib/objects.js';
|
||||
import contextMenu from '$lib/stores/contextmenu.js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import FormInput from './forminput.svelte';
|
||||
import Icon from './icon.svelte';
|
||||
import FormInput from '$components/editors/forminput.svelte';
|
||||
import Icon from '$components/icon.svelte';
|
||||
|
||||
export let items = [];
|
||||
export let columns = [];
|
||||
@ -30,7 +30,9 @@
|
||||
|
||||
function refresh(hideObjectIndicators, items) {
|
||||
_items = objectToArray(items).map(item => {
|
||||
item.children = objectToArray(item.children);
|
||||
if (item.children) {
|
||||
item.children = objectToArray(item.children);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
@ -50,7 +52,10 @@
|
||||
return obj;
|
||||
}
|
||||
else if ((typeof obj === 'object') && (obj !== null)) {
|
||||
return Object.entries(obj).map(([ k, item ]) => {
|
||||
return Object.entries(obj).map(([
|
||||
k,
|
||||
item,
|
||||
]) => {
|
||||
return { ...item, [key]: k };
|
||||
});
|
||||
}
|
||||
@ -65,7 +70,10 @@
|
||||
}
|
||||
|
||||
activeKey = itemKey;
|
||||
activePath = [ ...path.slice(0, level), itemKey ];
|
||||
activePath = [
|
||||
...path.slice(0, level),
|
||||
itemKey,
|
||||
];
|
||||
dispatch('select', { level, itemKey, index, path: activePath });
|
||||
}
|
||||
|
||||
@ -124,12 +132,17 @@
|
||||
</script>
|
||||
|
||||
{#each _items as item, index}
|
||||
{@const selected = canSelect && pathsAreEqual(activePath, [
|
||||
...path,
|
||||
item[key],
|
||||
])}
|
||||
|
||||
<tr
|
||||
on:click={() => select(item[key], index)}
|
||||
on:dblclick={() => doubleClick(item[key], index)}
|
||||
on:contextmenu|preventDefault={evt => showContextMenu(evt, item)}
|
||||
class:selectable={canSelect}
|
||||
class:selected={canSelect && pathsAreEqual(activePath, [ ...path, item[key] ])}
|
||||
class:selected
|
||||
class:striped
|
||||
>
|
||||
{#if !hideChildrenToggles}
|
||||
@ -143,6 +156,10 @@
|
||||
<Icon name={childrenOpen[item[key]] ? 'chev-d' : 'chev-r'} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if item.loading}
|
||||
<span class="spinner" style:margin-left="{level * 10}px" />
|
||||
{/if}
|
||||
</td>
|
||||
{/if}
|
||||
|
||||
@ -155,7 +172,11 @@
|
||||
{#each columns as column, columnIndex}
|
||||
<td class:right={column.right} title={keypathProxies[index][column.key]}>
|
||||
{#if column.inputType}
|
||||
<FormInput {column} bind:value={keypathProxies[index][column.key]} bind:valid={validity[columnIndex]} />
|
||||
<FormInput
|
||||
{column}
|
||||
bind:value={keypathProxies[index][column.key]}
|
||||
bind:valid={validity[columnIndex]}
|
||||
/>
|
||||
{:else}
|
||||
<div class="value" style:margin-left="{level * 10}px">
|
||||
{formatValue(keypathProxies[index][column.key])}
|
||||
@ -166,7 +187,12 @@
|
||||
|
||||
{#if canRemoveItems}
|
||||
<td class="has-button">
|
||||
<button class="button-small" type="button" on:click|stopPropagation={() => removeItem(index, item[key])} on:dblclick|stopPropagation>
|
||||
<button
|
||||
class="button-small"
|
||||
type="button"
|
||||
on:click|stopPropagation={() => removeItem(index, item[key])}
|
||||
on:dblclick|stopPropagation
|
||||
>
|
||||
<Icon name="x" />
|
||||
</button>
|
||||
</td>
|
||||
@ -182,7 +208,10 @@
|
||||
{hideChildrenToggles}
|
||||
{canSelect}
|
||||
{canRemoveItems}
|
||||
path={[ ...path, item[key] ]}
|
||||
path={[
|
||||
...path,
|
||||
item[key],
|
||||
]}
|
||||
items={item.children}
|
||||
level={level + 1}
|
||||
bind:activePath
|
||||
@ -201,28 +230,41 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
tr.selectable.selected td {
|
||||
background-color: #00008b !important;
|
||||
color: #fff;
|
||||
background-color: var(--selection) !important;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 2px;
|
||||
padding: 4px 2px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
td.has-toggle {
|
||||
width: 20px;
|
||||
position: relative;
|
||||
width: 1.5em;
|
||||
}
|
||||
td.has-toggle .spinner {
|
||||
position: absolute;
|
||||
top: 0.3em;
|
||||
left: 0.22em;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: 50px;
|
||||
border-top-color: transparent;
|
||||
border-left-color: transparent;
|
||||
animation: .6s linear 0 spin;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
td.has-icon {
|
||||
padding: 0;
|
||||
width: 17px;
|
||||
width: 1.5em;
|
||||
}
|
||||
td.has-icon :global(svg) {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
td .value {
|
||||
height: 15px;
|
||||
height: 1.2em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@ -230,14 +272,13 @@
|
||||
}
|
||||
|
||||
button.toggle {
|
||||
color: inherit;
|
||||
margin: 2px 0 0 3px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
vertical-align: top;
|
||||
color: inherit;
|
||||
}
|
||||
button.toggle :global(svg) {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
width: 0.9em;
|
||||
height: 0.9em;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
<script>
|
||||
import { onDestroy } from 'svelte';
|
||||
import BlankState from './blankstate.svelte';
|
||||
import BlankState from '../blankstate.svelte';
|
||||
import GridItems from './grid-items.svelte';
|
||||
import Icon from './icon.svelte';
|
||||
import Icon from '../icon.svelte';
|
||||
|
||||
export let columns = [];
|
||||
export let items = [];
|
||||
@ -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>
|
||||
@ -57,17 +57,17 @@
|
||||
<thead>
|
||||
<tr>
|
||||
{#if !hideChildrenToggles}
|
||||
<th class="has-toggle"></th>
|
||||
<th class="has-toggle" />
|
||||
{/if}
|
||||
|
||||
<th class="has-icon"></th>
|
||||
<th class="has-icon" />
|
||||
|
||||
{#each columns as column}
|
||||
<th scope="col">{column.title || ''}</th>
|
||||
{/each}
|
||||
|
||||
{#if canRemoveItems}
|
||||
<th class="has-button"></th>
|
||||
<th class="has-button" />
|
||||
{/if}
|
||||
</tr>
|
||||
</thead>
|
||||
@ -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) {
|
@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { isBsonBuiltin } from '$lib/mongo';
|
||||
import { isBsonBuiltin } from '$lib/mongo/index.js';
|
||||
import { isDate } from 'date-fns';
|
||||
import Grid from './grid.svelte';
|
||||
|
||||
@ -11,17 +11,17 @@
|
||||
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 = [];
|
||||
|
||||
if (Array.isArray(data)) {
|
@ -1,27 +1,40 @@
|
||||
<script>
|
||||
import icons from '$lib/icons.json';
|
||||
|
||||
export let name = '';
|
||||
export let spin = false;
|
||||
export let rotation = 0;
|
||||
|
||||
if (name === 'loading') {
|
||||
spin = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:global(.field) svg {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
margin-right: 2px;
|
||||
svg {
|
||||
transition: transform 0.25s;
|
||||
will-change: transform;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
}
|
||||
svg.spinning {
|
||||
animation: spin 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
}
|
||||
|
||||
:global(.btn) svg {
|
||||
height: 13px;
|
||||
:global(.field) svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-right: 2px;
|
||||
}
|
||||
:global(.button) svg {
|
||||
height: 1em;
|
||||
width: auto;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
@keyframes spinning {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
svg.spinning {
|
||||
animation: spinning 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
:global(.blankstate .button) svg {
|
||||
height: 1.25em;
|
||||
vertical-align: -3px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -35,112 +48,8 @@
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class:spinning={name === 'loading'}
|
||||
class:spinning={spin}
|
||||
style:transform="rotate({rotation}deg)"
|
||||
>
|
||||
{#if name === 'radio'}
|
||||
<circle cx="12" cy="12" r="2"></circle><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path>
|
||||
{:else if name === 'chev-l'}
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
{:else if name === 'chev-r'}
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
{:else if name === 'chev-d'}
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
{:else if name === 'chev-u'}
|
||||
<path d="m18 15-6-6-6 6" />
|
||||
{:else if name === 'chevs-l'}
|
||||
<path d="m11 17-5-5 5-5M18 17l-5-5 5-5" />
|
||||
{:else if name === 'chevs-r'}
|
||||
<path d="m13 17 5-5-5-5M6 17l5-5-5-5" />
|
||||
{:else if name === 'arr-d'}
|
||||
<path d="M12 5v14M19 12l-7 7-7-7" />
|
||||
{:else if name === 'db'}
|
||||
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
|
||||
{:else if name === 'x'}
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
{:else if name === '+'}
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
{:else if name === '-'}
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
{:else if name === 'reload'}
|
||||
<polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
|
||||
{:else if name === 'clipboard'}
|
||||
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8"
|
||||
y="2"
|
||||
width="8"
|
||||
height="4"
|
||||
rx="1"
|
||||
ry="1"></rect>
|
||||
{:else if name === 'edit'}
|
||||
<path d="M12 20h9"></path><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
|
||||
{:else if name === 'check'}
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||
{:else if name === 'list'}
|
||||
<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" />
|
||||
{:else if name === 'table'}
|
||||
<path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18" />
|
||||
{:else if name === 'form'}
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" />
|
||||
{:else if name === 'cog'}
|
||||
<circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||
{:else if name === 'zap'}
|
||||
<path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z" />
|
||||
{:else if name === 'server'}
|
||||
<rect x="2"
|
||||
y="2"
|
||||
width="20"
|
||||
height="8"
|
||||
rx="2"
|
||||
ry="2" /><rect x="2"
|
||||
y="14"
|
||||
width="20"
|
||||
height="8"
|
||||
rx="2"
|
||||
ry="2" /><path d="M6 6h.01M6 18h.01" />
|
||||
{:else if name === 'text'}
|
||||
<path d="M4 7V4h16v3M9 20h6M12 4v16" />
|
||||
{:else if name === 'hash'}
|
||||
<path d="M4 9h16M4 15h16M10 3 8 21M16 3l-2 18" />
|
||||
{:else if name === 'toggle-l'}
|
||||
<rect x="1"
|
||||
y="5"
|
||||
width="22"
|
||||
height="14"
|
||||
rx="7"
|
||||
ry="7" /><circle cx="8" cy="12" r="3" />
|
||||
{:else if name === 'cal'}
|
||||
<rect x="3"
|
||||
y="4"
|
||||
width="18"
|
||||
height="18"
|
||||
rx="2"
|
||||
ry="2" /><path d="M16 2v4M8 2v4M3 10h18" />
|
||||
{:else if name === 'code'}
|
||||
<path d="m16 18 6-6-6-6M8 6l-6 6 6 6" />
|
||||
{:else if name === 'target'}
|
||||
<circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" />
|
||||
{:else if name === 'trash'}
|
||||
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M10 11v6M14 11v6" />
|
||||
{:else if name === 'anchor'}
|
||||
<circle cx="12" cy="5" r="3" /><path d="M12 22V8M5 12H2a10 10 0 0 0 20 0h-3" />
|
||||
{:else if name === 'o'}
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
{:else if name === 'info'}
|
||||
<circle cx="12" cy="12" r="10" /><path d="M12 16v-4M12 8h.01" />
|
||||
{:else if name === 'play'}
|
||||
<path d="m5 3 14 9-14 9V3z" />
|
||||
{:else if name === 'upload'}
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" />
|
||||
{:else if name === 'save'}
|
||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" /><path d="M17 21v-8H7v8M7 3v5h8" />
|
||||
{:else if name === 're'}
|
||||
<path d="m17 1 4 4-4 4" /><path d="M3 11V9a4 4 0 0 1 4-4h14M7 23l-4-4 4-4" /><path d="M21 13v2a4 4 0 0 1-4 4H3" />
|
||||
{:else if name === 'chart'}
|
||||
<path d="M18 20V10M12 20V4M6 20v-6" />
|
||||
{:else if name === '?'}
|
||||
<circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||
{:else if name === '!'}
|
||||
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM12 9v4M12 17h.01" />
|
||||
{:else if name === 'loading'}
|
||||
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
|
||||
{/if}
|
||||
{@html icons[name] || ''}
|
||||
</svg>
|
||||
|
@ -3,9 +3,9 @@
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { Beep } from '$wails/go/ui/UI';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import { Beep } from '$wails/go/ui/UI.js';
|
||||
import Icon from './icon.svelte';
|
||||
|
||||
export let show = true;
|
||||
@ -36,6 +36,7 @@
|
||||
}
|
||||
|
||||
function close() {
|
||||
show = false;
|
||||
dispatch('close');
|
||||
}
|
||||
</script>
|
||||
@ -43,12 +44,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} transition:fade={{ duration: 200 }}>
|
||||
<div class="inner" style:max-width={width || '80vw'} transition:fly={{ y: 20, duration: 200 }}>
|
||||
{#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>
|
||||
@ -74,19 +75,14 @@
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
margin: 0;
|
||||
padding-top: 50px;
|
||||
padding: 2rem;
|
||||
--wails-draggable: drag;
|
||||
}
|
||||
:global(#root.platform-darwin) .outer {
|
||||
margin-top: var(--darwin-titlebar-height);
|
||||
}
|
||||
|
||||
.inner {
|
||||
max-height: 80vh;
|
||||
background-color: #fff;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: auto;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
border-radius: var(--radius);
|
||||
display: flex;
|
||||
|
@ -1,12 +1,13 @@
|
||||
<script>
|
||||
import { looseJsonIsValid } from '$lib/strings';
|
||||
import { looseJsonIsValid } from '$lib/strings.js';
|
||||
import { EJSON } from 'bson';
|
||||
import { createEventDispatcher, onDestroy } from 'svelte';
|
||||
import Icon from './icon.svelte';
|
||||
import Modal from './modal.svelte';
|
||||
import ObjectEditor from './objecteditor.svelte';
|
||||
import ObjectEditor from './editors/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>
|
||||
|
||||
|
@ -1,40 +1,61 @@
|
||||
<script>
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { tweened } from 'svelte/motion';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import Icon from './icon.svelte';
|
||||
|
||||
export let tabs = [];
|
||||
export let selectedKey = {};
|
||||
export let selectedKey = '';
|
||||
export let canAddTab = false;
|
||||
export let multiline = false;
|
||||
export let compact = true;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const maxPixelsPerMultilineTab = 120;
|
||||
const activeIndicatorLeft = tweened(0, { easing: cubicOut, duration: 400 });
|
||||
const activeIndicatorRight = tweened(0, { easing: cubicOut, duration: 400 });
|
||||
const liElements = {};
|
||||
let navEl;
|
||||
let pixelsPerTab = 0;
|
||||
$: tabs && navEl && updateMeasurements();
|
||||
|
||||
function updateMeasurements() {
|
||||
pixelsPerTab = (navEl.offsetWidth ?? 0) / tabs.length;
|
||||
}
|
||||
|
||||
function select(tabKey) {
|
||||
selectedKey = tabKey;
|
||||
dispatch('select', tabKey);
|
||||
}
|
||||
|
||||
function moveActiveIndicator(target = liElements[selectedKey]) {
|
||||
if (!compact) {
|
||||
return;
|
||||
}
|
||||
|
||||
const navRect = navEl.getBoundingClientRect();
|
||||
const itemRect = target.getBoundingClientRect();
|
||||
$activeIndicatorLeft = itemRect.x - navRect.x;
|
||||
$activeIndicatorRight = navRect.right - itemRect.right;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('resize', updateMeasurements);
|
||||
if (selectedKey) {
|
||||
moveActiveIndicator(liElements[selectedKey]);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<nav class="tabs" class:multiline={multiline || (pixelsPerTab < maxPixelsPerMultilineTab)} bind:this={navEl}>
|
||||
<svelte:window on:resize={() => moveActiveIndicator()} />
|
||||
|
||||
<nav class="tabs" class:compact bind:this={navEl}>
|
||||
<ul>
|
||||
{#each tabs as tab (tab.key)}
|
||||
<li class:active={tab.key === selectedKey}>
|
||||
<li
|
||||
class="tab"
|
||||
class:active={tab.key === selectedKey}
|
||||
class:closable={tab.closable}
|
||||
bind:this={liElements[tab.key]}
|
||||
on:mouseenter={event => moveActiveIndicator(event.target)}
|
||||
on:mouseleave={() => moveActiveIndicator()}
|
||||
>
|
||||
<button class="tab" on:click={() => select(tab.key)}>
|
||||
{#if tab.icon} <Icon name={tab.icon} /> {/if}
|
||||
<span class="label">{tab.title}</span>
|
||||
</button>
|
||||
|
||||
{#if tab.closable}
|
||||
<button class="button-small" on:click={() => dispatch('closeTab', tab.key)}>
|
||||
<Icon name="x" />
|
||||
@ -44,38 +65,41 @@
|
||||
{/each}
|
||||
|
||||
{#if canAddTab}
|
||||
<li class="tab add">
|
||||
<button class="tab" on:click={() => dispatch('addTab')}>
|
||||
<li>
|
||||
<button class="button-small" on:click={() => dispatch('addTab')}>
|
||||
<Icon name="+" />
|
||||
</button>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
|
||||
{#if compact}
|
||||
<span
|
||||
class="activeindicator"
|
||||
style:left="{$activeIndicatorLeft}px"
|
||||
style:right="{$activeIndicatorRight}px"
|
||||
/>
|
||||
{/if}
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
ul {
|
||||
overflow-x: scroll;
|
||||
display: flex;
|
||||
list-style: none;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
flex: 1;
|
||||
nav {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
li.add {
|
||||
flex: 0 1;
|
||||
ul {
|
||||
overflow-x: auto;
|
||||
display: flex;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.tabs :global(svg) {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
vertical-align: bottom;
|
||||
li {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
li.active :global(svg) {
|
||||
color: #fff;
|
||||
|
||||
nav.tabs :global(svg) {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
button.tab {
|
||||
@ -96,20 +120,50 @@
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
li.active button.tab {
|
||||
color: #fff;
|
||||
background-color: #00008b;
|
||||
border-color: #00008b;
|
||||
background-color: var(--selection);
|
||||
border-color: var(--primary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
nav.tabs.multiline button.tab .label {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
button.tab .label {
|
||||
margin-top: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.button-small {
|
||||
nav.tabs .button-small {
|
||||
margin: 12px 0 0 4px;
|
||||
}
|
||||
|
||||
li.closable .button-small {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 7px;
|
||||
top: 7px;
|
||||
}
|
||||
li.closable.active {
|
||||
padding-right: 20px;
|
||||
}
|
||||
li.closable.active .button-small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
nav.tabs.compact {
|
||||
border-bottom: 1px solid #aaa;
|
||||
}
|
||||
nav.tabs.compact li {
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
nav.tabs.compact button.tab {
|
||||
border: none;
|
||||
color: inherit;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.activeindicator {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
height: 2px;
|
||||
background-color: var(--primary);
|
||||
}
|
||||
</style>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import Modal from '$components/modal.svelte';
|
||||
import alink from '$lib/actions/alink';
|
||||
import environment from '$lib/stores/environment';
|
||||
import alink from '$lib/actions/alink.js';
|
||||
import environment from '$lib/stores/environment.js';
|
||||
</script>
|
||||
|
||||
<Modal width="400px" title=" " on:close>
|
||||
@ -19,7 +19,7 @@
|
||||
<hr />
|
||||
|
||||
<div class="info">
|
||||
<p class="copy">© Romein van Buren, 2023.</p>
|
||||
<p class="copy">© Romein van Buren, 2022-2025.</p>
|
||||
<p>
|
||||
<a href="https://garraflavatra.github.io/rolens/" use:alink>Documentation</a> |
|
||||
<a href="https://github.com/garraflavatra/rolens" use:alink>GitHub</a> |
|
||||
@ -30,6 +30,10 @@
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.brand, .info {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -40,9 +44,9 @@
|
||||
flex: 0 1 125px;
|
||||
}
|
||||
.brand .title {
|
||||
font-size: 2.25rem;
|
||||
font-size: 2.25em;
|
||||
font-weight: 600;
|
||||
line-height: 2.5rem;
|
||||
margin-bottom: .25em;
|
||||
}
|
||||
.brand .title .version {
|
||||
font-size: 80%;
|
||||
@ -51,7 +55,6 @@
|
||||
}
|
||||
.brand .description {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.6rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
@ -60,7 +63,6 @@
|
||||
|
||||
.info {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.25rem;
|
||||
margin: 0 1rem 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
<script>
|
||||
import DirectoryChooser from '$components/directorychooser.svelte';
|
||||
import DirectoryChooser from '$components/editors/directorychooser.svelte';
|
||||
import Modal from '$components/modal.svelte';
|
||||
import input from '$lib/actions/input';
|
||||
import settings from '$lib/stores/settings';
|
||||
import input from '$lib/actions/input.js';
|
||||
import settings from '$lib/stores/settings.js';
|
||||
</script>
|
||||
|
||||
<Modal title="Preferences" on:close>
|
||||
@ -15,7 +15,13 @@
|
||||
|
||||
<label for="defaultSort">Default sort query</label>
|
||||
<label class="field">
|
||||
<input type="text" class="code" bind:value={$settings.defaultSort} id="defaultSort" use:input={{ type: 'json' }} />
|
||||
<input
|
||||
type="text"
|
||||
class="code"
|
||||
bind:value={$settings.defaultSort}
|
||||
id="defaultSort"
|
||||
use:input={{ type: 'json' }}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label for="autosubmitQuery">Autosubmit query</label>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { BrowserOpenURL } from '$wails/runtime/runtime';
|
||||
import { BrowserOpenURL } from '$wails/runtime/runtime.js';
|
||||
|
||||
export default function alink(node) {
|
||||
node.addEventListener('click', e => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import environment from '$lib/stores/environment';
|
||||
import environment from '$lib/stores/environment.js';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export function controlKeyDown(event) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { isInt } from '$lib/math';
|
||||
import { canBeObjectId, int32, int64, uint64 } from '$lib/mongo';
|
||||
import { jsonLooseParse } from '$lib/strings';
|
||||
import { isInt } from '$lib/math.js';
|
||||
import { canBeObjectId, int32, int64, uint64 } from '$lib/mongo/index.js';
|
||||
import { jsonLooseParse } from '$lib/strings.js';
|
||||
|
||||
export default function input(node, { autofocus, type, onValid, onInvalid, mandatory } = {
|
||||
autofocus: false,
|
||||
@ -10,6 +10,9 @@ export default function input(node, { autofocus, type, onValid, onInvalid, manda
|
||||
mandatory: false,
|
||||
}) {
|
||||
|
||||
node.setAttribute('spellcheck', false);
|
||||
node.setAttribute('autocomplete', false);
|
||||
|
||||
const getMessage = () => {
|
||||
const checkInteger = () => (isInt(node.value) ? false : 'Value must be an integer');
|
||||
const checkNumberBoundaries = boundaries => {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { AskConfirmation } from '$wails/go/app/App.js';
|
||||
import InputDialog from '../dialogs/input.svelte';
|
||||
|
||||
function newDialog(dialogComponent, data = {}) {
|
||||
@ -5,11 +6,17 @@ function newDialog(dialogComponent, data = {}) {
|
||||
outlet.className = 'dialogoutlet';
|
||||
document.getElementById('dialogoutlets').appendChild(outlet);
|
||||
|
||||
const instance = new dialogComponent({ target: outlet, props: data });
|
||||
const instance = new dialogComponent({
|
||||
target: outlet,
|
||||
intro: true,
|
||||
props: data,
|
||||
});
|
||||
|
||||
instance.$close = function() {
|
||||
instance.$destroy();
|
||||
outlet.remove();
|
||||
setTimeout(() => {
|
||||
instance.$destroy();
|
||||
outlet.remove();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
instance.$on('close', instance.$close);
|
||||
@ -29,6 +36,10 @@ function enterText(title = '', description = '', value = '') {
|
||||
});
|
||||
}
|
||||
|
||||
const dialogs = { new: newDialog, enterText };
|
||||
function confirm(message = '') {
|
||||
return AskConfirmation(message);
|
||||
}
|
||||
|
||||
const dialogs = { new: newDialog, enterText, confirm };
|
||||
|
||||
export default dialogs;
|
||||
|
46
frontend/src/lib/icons.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"radio": "<circle cx=\"12\" cy=\"12\" r=\"2\"></circle><path d=\"M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14\"></path>",
|
||||
"chev-l": "<polyline points=\"15 18 9 12 15 6\"></polyline>",
|
||||
"chev-r": "<polyline points=\"9 18 15 12 9 6\"></polyline>",
|
||||
"chev-d": "<polyline points=\"6 9 12 15 18 9\"></polyline>",
|
||||
"chev-u": "<path d=\"m18 15-6-6-6 6\"/>",
|
||||
"chevs-l": "<path d=\"m11 17-5-5 5-5M18 17l-5-5 5-5\"/>",
|
||||
"chevs-r": "<path d=\"m13 17 5-5-5-5M6 17l5-5-5-5\"/>",
|
||||
"arr-d": "<path d=\"M12 5v14M19 12l-7 7-7-7\"/>",
|
||||
"arr-r": "<path d=\"M5 12h14M12 5l7 7-7 7\"/>",
|
||||
"db": "<ellipse cx=\"12\" cy=\"5\" rx=\"9\" ry=\"3\"></ellipse><path d=\"M21 12c0 1.66-4 3-9 3s-9-1.34-9-3\"></path><path d=\"M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5\"></path>",
|
||||
"x": "<line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>",
|
||||
"+": "<line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\"></line><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"></line>",
|
||||
"-": "<line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"></line>",
|
||||
"reload": "<polyline points=\"23 4 23 10 17 10\"></polyline><polyline points=\"1 20 1 14 7 14\"></polyline><path d=\"M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15\"></path>",
|
||||
"clipboard": "<path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"></path><rect x=\"8\" y=\"2\" width=\"8\" height=\"4\" rx=\"1\" ry=\"1\"></rect>",
|
||||
"edit": "<path d=\"M12 20h9\"></path><path d=\"M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z\"></path>",
|
||||
"check": "<path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"></path><polyline points=\"22 4 12 14.01 9 11.01\"></polyline>",
|
||||
"list": "<path d=\"M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01\"/>",
|
||||
"table": "<path d=\"M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18\"/>",
|
||||
"form": "<path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"/><path d=\"M14 2v6h6M16 13H8M16 17H8M10 9H8\"/>",
|
||||
"cog": "<circle cx=\"12\" cy=\"12\" r=\"3\"/><path d=\"M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z\"/>",
|
||||
"zap": "<path d=\"M13 2 3 14h9l-1 8 10-12h-9l1-8z\"/>",
|
||||
"server": "<rect x=\"2\" y=\"2\" width=\"20\" height=\"8\" rx=\"2\" ry=\"2\"/><rect x=\"2\" y=\"14\" width=\"20\" height=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M6 6h.01M6 18h.01\"/>",
|
||||
"text": "<path d=\"M4 7V4h16v3M9 20h6M12 4v16\"/>",
|
||||
"hash": "<path d=\"M4 9h16M4 15h16M10 3 8 21M16 3l-2 18\"/>",
|
||||
"toggle-l": "<rect x=\"1\" y=\"5\" width=\"22\" height=\"14\" rx=\"7\" ry=\"7\"/><circle cx=\"8\" cy=\"12\" r=\"3\"/>",
|
||||
"cal": "<rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"/><path d=\"M16 2v4M8 2v4M3 10h18\"/>",
|
||||
"code": "<path d=\"m16 18 6-6-6-6M8 6l-6 6 6 6\"/>",
|
||||
"target": "<circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/>",
|
||||
"trash": "<path d=\"M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M10 11v6M14 11v6\"/>",
|
||||
"anchor": "<circle cx=\"12\" cy=\"5\" r=\"3\"/><path d=\"M12 22V8M5 12H2a10 10 0 0 0 20 0h-3\"/>",
|
||||
"o": "<circle cx=\"12\" cy=\"12\" r=\"10\"/>",
|
||||
"info": "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4M12 8h.01\"/>",
|
||||
"play": "<path d=\"m5 3 14 9-14 9V3z\"/>",
|
||||
"upload": "<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12\"/>",
|
||||
"save": "<path d=\"M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z\"/><path d=\"M17 21v-8H7v8M7 3v5h8\"/>",
|
||||
"re": "<path d=\"m17 1 4 4-4 4\"/><path d=\"M3 11V9a4 4 0 0 1 4-4h14M7 23l-4-4 4-4\"/><path d=\"M21 13v2a4 4 0 0 1-4 4H3\"/>",
|
||||
"chart": "<path d=\"M18 20V10M12 20V4M6 20v-6\"/>",
|
||||
"?": "<circle cx=\"12\" cy=\"12\" r=\"10\"></circle><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\"></path><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"></line>",
|
||||
"!": "<path d=\"M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM12 9v4M12 17h.01\"/>",
|
||||
"loading": "<path d=\"M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83\"/>",
|
||||
"shell": "<path d=\"m4 17 6-6-6-6M12 19h8\"/>",
|
||||
"doc": "<path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"/><path d=\"M14 2v6h6M16 13H8M16 17H8M10 9H8\"/>",
|
||||
"columns": "<path d=\"M12 3h7a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-7m0-18H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7m0-18v18\"/>"
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { locales } from '$lib/mongo';
|
||||
import { locales } from './index.js';
|
||||
|
||||
const defaultCollation = {
|
||||
locale: 'en_US',
|
||||
|
@ -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
@ -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
@ -0,0 +1,7 @@
|
||||
{
|
||||
"F": "Fatal",
|
||||
"E": "Error",
|
||||
"W": "Warning",
|
||||
"I": "Info",
|
||||
"D": "Debug"
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
import { StartProgressBar, StopProgressBar } from '$wails/go/ui/UI';
|
||||
|
||||
let taskCounter = 0;
|
||||
|
||||
export function startProgress(taskDescription = 'Loading…') {
|
||||
const taskIndex = ++taskCounter;
|
||||
let started = false;
|
||||
|
||||
const debouncer = setTimeout(() => {
|
||||
StartProgressBar(taskIndex, taskDescription);
|
||||
started = true;
|
||||
}, 150);
|
||||
|
||||
const task = {
|
||||
id: taskIndex,
|
||||
description: taskDescription,
|
||||
|
||||
end: () => {
|
||||
clearTimeout(debouncer);
|
||||
if (started) {
|
||||
StopProgressBar(taskIndex);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return task;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { Environment } from '$wails/go/app/App';
|
||||
import { Environment } from '$wails/go/app/App.js';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const { set, subscribe } = writable({});
|
||||
@ -11,5 +11,10 @@ async function reload() {
|
||||
|
||||
reload();
|
||||
|
||||
subscribe(env => {
|
||||
// @ts-ignore
|
||||
document.body.dataset.platform = env?.platform;
|
||||
});
|
||||
|
||||
const environment = { reload, subscribe };
|
||||
export default environment;
|
||||
|
@ -1,22 +1,25 @@
|
||||
import dialogs from '$lib/dialogs';
|
||||
import { startProgress } from '$lib/progress';
|
||||
import dialogs from '$lib/dialogs.js';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import applicationInited from './inited';
|
||||
import queries from './queries';
|
||||
import windowTitle from './windowtitle';
|
||||
import applicationInited from './inited.js';
|
||||
import queries from './queries.js';
|
||||
import windowTitle from './windowtitle.js';
|
||||
|
||||
import ExportDialog from '$organisms/connection/collection/dialogs/export.svelte';
|
||||
import IndexDetailDialog from '$organisms/connection/collection/dialogs/indexdetail.svelte';
|
||||
import QueryChooserDialog from '$organisms/connection/collection/dialogs/querychooser.svelte';
|
||||
import DumpDialog from '$organisms/connection/database/dialogs/dump.svelte';
|
||||
import HostDetailDialog from '$organisms/connection/host/dialogs/hostdetail.svelte';
|
||||
import DuplicateDialog from '$organisms/connection/collection/dialogs/duplicate.svelte';
|
||||
|
||||
import {
|
||||
CreateIndex,
|
||||
DropCollection,
|
||||
DropDatabase,
|
||||
DropIndex,
|
||||
DuplicateCollection,
|
||||
ExecuteShellScript,
|
||||
GetIndexes,
|
||||
HostLogs,
|
||||
Hosts,
|
||||
OpenCollection,
|
||||
OpenConnection,
|
||||
@ -26,7 +29,7 @@ import {
|
||||
RemoveHost,
|
||||
RenameCollection,
|
||||
TruncateCollection
|
||||
} from '$wails/go/app/App';
|
||||
} from '$wails/go/app/App.js';
|
||||
|
||||
const { set, subscribe } = writable({});
|
||||
const getValue = () => get({ subscribe });
|
||||
@ -36,7 +39,11 @@ async function refresh() {
|
||||
const hosts = await Hosts();
|
||||
const hostTree = getValue();
|
||||
|
||||
for (const [ hostKey, hostDetails ] of Object.entries(hosts)) {
|
||||
for (const [
|
||||
hostKey,
|
||||
hostDetails,
|
||||
] of Object.entries(hosts)) {
|
||||
|
||||
hostTree[hostKey] = hostTree[hostKey] || {};
|
||||
const host = hostTree[hostKey];
|
||||
host.key = hostKey;
|
||||
@ -44,7 +51,17 @@ async function refresh() {
|
||||
host.uri = hostDetails.uri;
|
||||
|
||||
host.open = async function() {
|
||||
const { databases: dbNames, status, statusError, systemInfo, systemInfoError } = await OpenConnection(hostKey);
|
||||
host.loading = true;
|
||||
set(hostTree);
|
||||
|
||||
const {
|
||||
databases: dbNames,
|
||||
status,
|
||||
statusError,
|
||||
systemInfo,
|
||||
systemInfoError,
|
||||
} = await OpenConnection(hostKey);
|
||||
|
||||
host.status = status;
|
||||
host.statusError = statusError;
|
||||
host.systemInfo = systemInfo;
|
||||
@ -59,7 +76,10 @@ async function refresh() {
|
||||
host.databases[dbKey] = host.databases[dbKey] || {};
|
||||
}
|
||||
|
||||
for (const [ dbKey, database ] of Object.entries(host.databases)) {
|
||||
for (const [
|
||||
dbKey,
|
||||
database,
|
||||
] of Object.entries(host.databases)) {
|
||||
if (!database.new && !dbNames.includes(dbKey)) {
|
||||
delete host.databases[dbKey];
|
||||
continue;
|
||||
@ -72,6 +92,9 @@ async function refresh() {
|
||||
delete database.new;
|
||||
|
||||
database.open = async function() {
|
||||
database.loading = true;
|
||||
set(hostTree);
|
||||
|
||||
const { collections: collNames, stats, statsError } = await OpenDatabase(hostKey, dbKey);
|
||||
database.stats = stats;
|
||||
database.statsError = statsError;
|
||||
@ -84,7 +107,10 @@ async function refresh() {
|
||||
database.collections[collKey] = database.collections[collKey] || {};
|
||||
}
|
||||
|
||||
for (const [ collKey, collection ] of Object.entries(database.collections)) {
|
||||
for (const [
|
||||
collKey,
|
||||
collection,
|
||||
] of Object.entries(database.collections)) {
|
||||
if (!collection.new && !collNames.includes(collKey)) {
|
||||
delete database.collections[collKey];
|
||||
continue;
|
||||
@ -93,6 +119,7 @@ async function refresh() {
|
||||
collection.key = collKey;
|
||||
collection.dbKey = dbKey;
|
||||
collection.hostKey = hostKey;
|
||||
collection.viewKey = 'list';
|
||||
collection.indexes = collection.indexes || [];
|
||||
|
||||
delete collection.new;
|
||||
@ -109,20 +136,45 @@ async function refresh() {
|
||||
collection.rename = async function() {
|
||||
const newCollKey = await dialogs.enterText('Rename collection', `Enter a new name for collection ${collKey}.`, collKey);
|
||||
if (newCollKey && (newCollKey !== collKey)) {
|
||||
const progress = startProgress(`Renaming collection "${collKey}" to "${newCollKey}"…`);
|
||||
const ok = await RenameCollection(hostKey, dbKey, collKey, newCollKey);
|
||||
await database.open();
|
||||
progress.end();
|
||||
return ok;
|
||||
}
|
||||
};
|
||||
|
||||
collection.duplicate = async function() {
|
||||
const dialog = dialogs.new(DuplicateDialog, { host, dbKey, collKey });
|
||||
return new Promise(resolve => {
|
||||
dialog.$on('duplicate', async event => {
|
||||
const success = await DuplicateCollection(
|
||||
hostKey,
|
||||
dbKey,
|
||||
collKey,
|
||||
event.detail.newHost,
|
||||
event.detail.newDb,
|
||||
event.detail.newColl
|
||||
);
|
||||
|
||||
if (success) {
|
||||
await refresh();
|
||||
dialog.$close();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
collection.export = function(query) {
|
||||
const dialog = dialogs.new(ExportDialog, { collection, query });
|
||||
|
||||
return new Promise(resolve => {
|
||||
dialog.$on('export', async event => {
|
||||
const success = await PerformFindExport(hostKey, dbKey, collKey, JSON.stringify(event.detail.exportInfo));
|
||||
const success = await PerformFindExport(
|
||||
hostKey,
|
||||
dbKey,
|
||||
collKey,
|
||||
JSON.stringify(event.detail.exportInfo)
|
||||
);
|
||||
|
||||
if (success) {
|
||||
dialog.$close();
|
||||
resolve();
|
||||
@ -150,30 +202,23 @@ 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) {
|
||||
delete database.collections[collKey];
|
||||
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 +232,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 +249,17 @@ 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));
|
||||
const newIndexName = await CreateIndex(
|
||||
collection.hostKey,
|
||||
collection.dbKey,
|
||||
collection.key,
|
||||
JSON.stringify(event.detail.index)
|
||||
);
|
||||
|
||||
if (newIndexName) {
|
||||
dialog.$close();
|
||||
}
|
||||
|
||||
progress.end();
|
||||
resolve(newIndexName);
|
||||
});
|
||||
});
|
||||
@ -239,10 +283,17 @@ async function refresh() {
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
collection.executeShellScript = async function(script) {
|
||||
const result = await ExecuteShellScript(hostKey, dbKey, collKey, script);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
await refresh();
|
||||
windowTitle.setSegments(dbKey, host.name, 'Rolens');
|
||||
database.loading = false;
|
||||
set(hostTree);
|
||||
};
|
||||
|
||||
database.dump = function() {
|
||||
@ -260,14 +311,11 @@ async function refresh() {
|
||||
};
|
||||
|
||||
database.drop = async function() {
|
||||
const progress = startProgress(`Dropping database "${dbKey}"…`);
|
||||
const success = await DropDatabase(hostKey, dbKey);
|
||||
|
||||
if (success) {
|
||||
delete host.databases[dbKey];
|
||||
await refresh();
|
||||
}
|
||||
|
||||
progress.end();
|
||||
};
|
||||
|
||||
database.newCollection = async function() {
|
||||
@ -277,17 +325,42 @@ async function refresh() {
|
||||
await database.open();
|
||||
}
|
||||
};
|
||||
|
||||
database.executeShellScript = async function(script) {
|
||||
const result = await ExecuteShellScript(hostKey, dbKey, '', script);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
host.newDatabase = async function() {
|
||||
const name = await dialogs.enterText('Create a database', 'Enter the database name. Note: databases in MongoDB do not exist until they have a collection and an item. Your new database will not persist on the server; fill it to have it created.', '');
|
||||
if (name) {
|
||||
host.databases[name] = { key: name, new: true };
|
||||
await host.open();
|
||||
}
|
||||
};
|
||||
|
||||
await refresh();
|
||||
host.loading = false;
|
||||
set(hostTree);
|
||||
};
|
||||
|
||||
host.executeShellScript = async function(script) {
|
||||
const result = await ExecuteShellScript(hostKey, '', '', script);
|
||||
return result;
|
||||
};
|
||||
|
||||
host.newDatabase = async function() {
|
||||
const name = await dialogs.enterText('Create a database', 'Enter the database name. Note: databases in MongoDB do not exist until they have a collection and an item. Your new database will not persist on the server; fill it to have it created.', '');
|
||||
if (name) {
|
||||
host.databases[name] = { key: name, new: true };
|
||||
await host.open();
|
||||
}
|
||||
};
|
||||
|
||||
host.edit = async function() {
|
||||
const dialog = dialogs.new(HostDetailDialog, { hostKey });
|
||||
return new Promise(resolve => {
|
||||
dialog.$on('close', () => {
|
||||
refresh().then(resolve);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
host.getLogs = async function(filter = 'global') {
|
||||
return await HostLogs(hostKey, filter);
|
||||
};
|
||||
|
||||
host.remove = async function() {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { derived } from 'svelte/store';
|
||||
import environment from './environment';
|
||||
import applicationSettings from './settings';
|
||||
import environment from './environment.js';
|
||||
import applicationSettings from './settings.js';
|
||||
|
||||
let alreadyInited = false;
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { RemoveQuery, SavedQueries, SaveQuery, UpdateQueries } from '$wails/go/app/App';
|
||||
import { RemoveQuery, SavedQueries, SaveQuery, UpdateQueries } from '$wails/go/app/App.js';
|
||||
import { get, writable } from 'svelte/store';
|
||||
|
||||
const { set, subscribe } = writable({});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Settings, UpdateSettings } from '$wails/go/app/App';
|
||||
import { Settings, UpdateSettings } from '$wails/go/app/App.js';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const { set, subscribe } = writable({});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReportSharedStateVariable } from '$wails/go/app/App';
|
||||
import { ReportSharedStateVariable } from '$wails/go/app/App.js';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
function sharedStateStore(name) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import dialogs from '$lib/dialogs';
|
||||
import dialogs from '$lib/dialogs.js';
|
||||
import ViewConfigDialog from '$organisms/connection/collection/dialogs/viewconfig.svelte';
|
||||
import { UpdateViewStore, Views } from '$wails/go/app/App';
|
||||
import { UpdateViewStore, Views } from '$wails/go/app/App.js';
|
||||
import { get, writable } from 'svelte/store';
|
||||
|
||||
const { set, subscribe } = writable({});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { WindowSetTitle } from '$wails/runtime/runtime';
|
||||
import { WindowSetTitle } from '$wails/runtime/runtime.js';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const { set, subscribe } = writable('Rolens');
|
||||
|
@ -22,3 +22,7 @@ export function looseJsonIsValid(json) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function stringCouldBeID(string) {
|
||||
return /^[a-zA-Z0-9_-]{1,}$/.test(string);
|
||||
}
|
||||
|
@ -1,15 +1,20 @@
|
||||
import './styles/loading.css';
|
||||
import './styles/reset.css';
|
||||
import './styles/style.css';
|
||||
|
||||
import { LogError } from '$wails/runtime';
|
||||
import { EventsOn, LogError } from '$wails/runtime/runtime.js';
|
||||
import dialogs from '$lib/dialogs.js';
|
||||
|
||||
import App from './app.svelte';
|
||||
import AboutDialog from './dialogs/about.svelte';
|
||||
import SettingsDialog from './dialogs/settings/index.svelte';
|
||||
|
||||
window.addEventListener('unhandledrejection', event => {
|
||||
LogError('Unhandled JS rejection: ' + event.reason);
|
||||
});
|
||||
|
||||
// @ts-ignore Argument IS correct.
|
||||
EventsOn('global.about', () => dialogs.new(AboutDialog));
|
||||
EventsOn('global.settings', () => dialogs.new(SettingsDialog));
|
||||
|
||||
const app = new App({ target: document.getElementById('app') });
|
||||
|
||||
export default app;
|
||||
|
@ -2,12 +2,12 @@
|
||||
import Details from '$components/details.svelte';
|
||||
import Icon from '$components/icon.svelte';
|
||||
import Modal from '$components/modal.svelte';
|
||||
import ObjectEditor from '$components/objecteditor.svelte';
|
||||
import { aggregationStageDocumentationURL, aggregationStages } from '$lib/mongo';
|
||||
import ObjectEditor from '$components/editors/objecteditor.svelte';
|
||||
import { aggregationStageDocumentationURL, aggregationStages } from '$lib/mongo/index.js';
|
||||
import Collation from '$lib/mongo/collation.svelte';
|
||||
import { jsonLooseParse, looseJsonIsValid } from '$lib/strings';
|
||||
import { Aggregate } from '$wails/go/app/App';
|
||||
import { BrowserOpenURL } from '$wails/runtime/runtime';
|
||||
import { jsonLooseParse, looseJsonIsValid } from '$lib/strings.js';
|
||||
import { Aggregate } from '$wails/go/app/App.js';
|
||||
import { BrowserOpenURL } from '$wails/runtime/runtime.js';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let collection;
|
||||
@ -61,14 +61,15 @@
|
||||
<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>
|
||||
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label class="field">
|
||||
<ObjectEditor bind:text={stage.data}
|
||||
<ObjectEditor
|
||||
bind:text={stage.data}
|
||||
on:inited={e => {
|
||||
e.detail.editor.dispatch({
|
||||
changes: {
|
||||
@ -80,7 +81,6 @@
|
||||
anchor: 3,
|
||||
},
|
||||
});
|
||||
e.detail.editor.focus();
|
||||
}} />
|
||||
</label>
|
||||
</Details>
|
||||
@ -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">
|
||||
|
@ -1,9 +1,9 @@
|
||||
<script>
|
||||
import FormInput from '$components/forminput.svelte';
|
||||
import FormInput from '$components/editors/forminput.svelte';
|
||||
import Hint from '$components/hint.svelte';
|
||||
import Icon from '$components/icon.svelte';
|
||||
import { inputTypes } from '$lib/mongo';
|
||||
import { resolveKeypath, setValue } from '$lib/objects';
|
||||
import { inputTypes } from '$lib/mongo/index.js';
|
||||
import { resolveKeypath, setValue } from '$lib/objects.js';
|
||||
|
||||
export let item = {};
|
||||
export let view = {};
|
||||
@ -50,7 +50,12 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="input">
|
||||
<FormInput {column} bind:value={keypathProxy[column.key]} bind:valid={validity[column.key]} autofocus={index === 0} />
|
||||
<FormInput
|
||||
{column}
|
||||
bind:value={keypathProxy[column.key]}
|
||||
bind:valid={validity[column.key]}
|
||||
autofocus={index === 0}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
{:else}
|
||||
|
@ -0,0 +1,90 @@
|
||||
<script>
|
||||
import Icon from '$components/icon.svelte';
|
||||
import Modal from '$components/modal.svelte';
|
||||
import input from '$lib/actions/input.js';
|
||||
import hostTree from '$lib/stores/hosttree.js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let host = {};
|
||||
export let dbKey = '';
|
||||
export let collKey = '';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let newHost = host.key;
|
||||
let newDb = dbKey;
|
||||
let newColl = `${collKey}-duplicate`;
|
||||
|
||||
function duplicate() {
|
||||
dispatch('duplicate', { newHost, newDb, newColl });
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal title="Duplicate collection" width="500px" on:close>
|
||||
<div class="duplicate">
|
||||
<div class="origin">
|
||||
<div class="field">
|
||||
<span class="label">{host.name || host.uri}</span>
|
||||
<!-- <input type="text" readonly value={host.name || host.uri} /> -->
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="label">{dbKey}</span>
|
||||
<!-- <input type="text" readonly value={dbKey} /> -->
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="label">{collKey}</span>
|
||||
<!-- <input type="text" readonly value={collKey} /> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow">
|
||||
<Icon name="arr-r" />
|
||||
</div>
|
||||
|
||||
<div class="destination">
|
||||
<label class="field">
|
||||
<span class="label">Host</span>
|
||||
<select bind:value={newHost}>
|
||||
{#each Object.values($hostTree) as { key, name }}
|
||||
<option value={key} selected={key === host.key}>{name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="label">Database</span>
|
||||
<input type="text" bind:value={newDb} use:input />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="label">Collection</span>
|
||||
<input type="text" bind:value={newColl} use:input={{ autofocus: true }} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<button class="button" on:click={duplicate}>
|
||||
<Icon name="play" /> Duplicate
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.duplicate {
|
||||
display: grid;
|
||||
grid-template: auto / 1fr auto 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.field:not(:last-child) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.origin .field .label {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
@ -1,11 +1,11 @@
|
||||
<script>
|
||||
import Icon from '$components/icon.svelte';
|
||||
import Modal from '$components/modal.svelte';
|
||||
import views from '$lib/stores/views';
|
||||
import views from '$lib/stores/views.js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let collection;
|
||||
export let query = undefined;
|
||||
export let query = {};
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const exportInfo = { ...query, viewKey: collection.viewKey };
|
||||
@ -16,7 +16,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal title="Export results" width="450px" on:close>
|
||||
<Modal title="Export results" width="500px" on:close>
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<label class="field">
|
||||
<span class="label">Export</span>
|
||||
@ -30,27 +30,31 @@
|
||||
<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>
|
||||
|
||||
<label class="field">
|
||||
<span class="label">View to use</span>
|
||||
<select bind:value={exportInfo.viewKey}>
|
||||
{#each Object.entries(views.forCollection(collection.hostKey, collection.dbKey, collection.key)) as [ key, { name } ]}
|
||||
{#each Object.entries(views.forCollection(collection.hostKey, collection.dbKey, collection.key)) as [
|
||||
key,
|
||||
{ name },
|
||||
]}
|
||||
<option value={key}>{name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<button class="btn" type="button" on:click={() => dispatch('openViewConfig')} title="Edit view">
|
||||
<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>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import Icon from '$components/icon.svelte';
|
||||
import Modal from '$components/modal.svelte';
|
||||
import input from '$lib/actions/input';
|
||||
import input from '$lib/actions/input.js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let collection;
|
||||
@ -27,7 +27,12 @@
|
||||
<form on:submit|preventDefault={create}>
|
||||
<label class="field name">
|
||||
<span class="label">Name</span>
|
||||
<input type="text" placeholder="Optional" bind:value={index.name} use:input={{ autofocus: true }} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Optional"
|
||||
bind:value={index.name}
|
||||
use:input={{ autofocus: true }}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div class="toggles">
|
||||
@ -62,8 +67,9 @@
|
||||
<div class="row">
|
||||
<label class="field">
|
||||
<span class="label">Key</span>
|
||||
<input type="text" placeholder="_id" bind:value={rule.key}>
|
||||
<input type="text" placeholder="_id" bind:value={rule.key} />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<select bind:value={rule.sort}>
|
||||
<option value={1}>Ascending</option>
|
||||
@ -71,7 +77,8 @@
|
||||
<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 +89,11 @@
|
||||
</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>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<script>
|
||||
import Grid from '$components/grid.svelte';
|
||||
import Grid from '$components/grid/grid.svelte';
|
||||
import Hint from '$components/hint.svelte';
|
||||
import Icon from '$components/icon.svelte';
|
||||
import Modal from '$components/modal.svelte';
|
||||
import input from '$lib/actions/input';
|
||||
import hostTree from '$lib/stores/hosttree';
|
||||
import queries from '$lib/stores/queries';
|
||||
import input from '$lib/actions/input.js';
|
||||
import hostTree from '$lib/stores/hosttree.js';
|
||||
import queries from '$lib/stores/queries.js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let queryToSave = undefined;
|
||||
@ -71,7 +71,7 @@
|
||||
<input type="text" bind:value={queryToSave.name} use:input={{ autofocus: true }} />
|
||||
</label>
|
||||
<label class="field">
|
||||
<textarea bind:value={queryToSave.remarks} placeholder="Remarks…" use:input></textarea>
|
||||
<textarea bind:value={queryToSave.remarks} placeholder="Remarks…" use:input />
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
@ -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>
|
||||
|
@ -2,9 +2,9 @@
|
||||
import Icon from '$components/icon.svelte';
|
||||
import Modal from '$components/modal.svelte';
|
||||
import TabBar from '$components/tabbar.svelte';
|
||||
import input from '$lib/actions/input';
|
||||
import { randomString } from '$lib/math';
|
||||
import views from '$lib/stores/views';
|
||||
import input from '$lib/actions/input.js';
|
||||
import { randomString } from '$lib/math.js';
|
||||
import views from '$lib/stores/views.js';
|
||||
|
||||
export let collection;
|
||||
export let firstItem = {};
|
||||
@ -114,7 +114,12 @@
|
||||
{#key collection.viewKey}
|
||||
<label class="field">
|
||||
<span class="label">View name</span>
|
||||
<input type="text" use:input={{ autofocus: true }} bind:value={$views[collection.viewKey].name} disabled={collection.viewKey === 'list'} />
|
||||
<input
|
||||
type="text"
|
||||
use:input={{ autofocus: true }}
|
||||
bind:value={$views[collection.viewKey].name}
|
||||
disabled={collection.viewKey === 'list'}
|
||||
/>
|
||||
</label>
|
||||
{/key}
|
||||
<label class="field">
|
||||
@ -128,7 +133,11 @@
|
||||
|
||||
{#if $views[collection.viewKey].type === 'list'}
|
||||
<div class="flex">
|
||||
<input type="checkbox" id="hideObjectIndicators" bind:checked={$views[collection.viewKey].hideObjectIndicators} />
|
||||
<input
|
||||
type="checkbox"
|
||||
id="hideObjectIndicators"
|
||||
bind:checked={$views[collection.viewKey].hideObjectIndicators}
|
||||
/>
|
||||
<label for="hideObjectIndicators">
|
||||
Hide object indicators ({'{...}'} and [...]) in list view and show nothing instead
|
||||
</label>
|
||||
@ -185,16 +194,41 @@
|
||||
</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 +236,16 @@
|
||||
<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}
|
||||
|
@ -1,19 +1,19 @@
|
||||
<script>
|
||||
import Grid from '$components/grid.svelte';
|
||||
import Grid from '$components/grid/grid.svelte';
|
||||
import Icon from '$components/icon.svelte';
|
||||
import ObjectGrid from '$components/objectgrid.svelte';
|
||||
import ObjectGrid from '$components/grid/objectgrid.svelte';
|
||||
import ObjectViewer from '$components/objectviewer.svelte';
|
||||
import input from '$lib/actions/input';
|
||||
import { deepClone } from '$lib/objects';
|
||||
import { startProgress } from '$lib/progress';
|
||||
import applicationSettings from '$lib/stores/settings';
|
||||
import views from '$lib/stores/views';
|
||||
import { convertLooseJson } from '$lib/strings';
|
||||
import { FindItems, RemoveItemById, UpdateFoundDocument } from '$wails/go/app/App';
|
||||
import input from '$lib/actions/input.js';
|
||||
import dialogs from '$lib/dialogs.js';
|
||||
import { deepClone } from '$lib/objects.js';
|
||||
import applicationSettings from '$lib/stores/settings.js';
|
||||
import views from '$lib/stores/views.js';
|
||||
import { convertLooseJson, stringCouldBeID } from '$lib/strings.js';
|
||||
import { CountItems, FindItems, RemoveItemById, UpdateFoundDocument } from '$wails/go/app/App.js';
|
||||
import { EJSON } from 'bson';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let collection;
|
||||
export let visible = false;
|
||||
|
||||
const defaults = {
|
||||
query: '{}',
|
||||
@ -25,15 +25,21 @@
|
||||
|
||||
let form = { ...defaults };
|
||||
let result = {};
|
||||
let countResult = {};
|
||||
let submittedForm = {};
|
||||
let queryField;
|
||||
let activePath = [];
|
||||
let objectViewerData;
|
||||
let querying = false;
|
||||
let counting = false;
|
||||
let objectViewerSuccessMessage = '';
|
||||
let viewsForCollection = {};
|
||||
|
||||
// $: code = `db.${collection.key}.find(${form.query || '{}'}${form.fields && form.fields !== '{}' ? `, ${form.fields}` : ''}).sort(${form.sort})${form.skip ? `.skip(${form.skip})` : ''}${form.limit ? `.limit(${form.limit})` : ''};`;
|
||||
// $: code = `db.${collection.key}.find(${form.query || '{}'}${form.fields &&
|
||||
// form.fields !== '{}' ? `, ${form.fields}` : ''}).sort(${form.sort})
|
||||
// ${form.skip ? `.skip(${form.skip})` : ''}${form.limit ? `
|
||||
// .limit(${form.limit})` : ''};`;
|
||||
|
||||
$: lastPage = (submittedForm.limit && result?.results?.length) ? Math.max(0, Math.ceil((result.total - submittedForm.limit) / submittedForm.limit)) : 0;
|
||||
$: activePage = (submittedForm.limit && submittedForm.skip && result?.results?.length) ? submittedForm.skip / submittedForm.limit : 0;
|
||||
|
||||
@ -42,13 +48,27 @@
|
||||
}
|
||||
|
||||
async function submitQuery() {
|
||||
if (querying) {
|
||||
if (querying || !visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stringCouldBeID(form.query)) {
|
||||
form.query = `{ "_id": "${form.query}" }`;
|
||||
}
|
||||
|
||||
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 }));
|
||||
@ -60,6 +80,17 @@
|
||||
querying = false;
|
||||
}
|
||||
|
||||
async function countItems() {
|
||||
counting = true;
|
||||
countResult = await CountItems(
|
||||
collection.hostKey,
|
||||
collection.dbKey,
|
||||
collection.key,
|
||||
convertLooseJson(form.query) || defaults.query
|
||||
);
|
||||
counting = false;
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
if ($applicationSettings.autosubmitQuery) {
|
||||
await submitQuery();
|
||||
@ -109,7 +140,18 @@
|
||||
if (!activePath[0]) {
|
||||
return;
|
||||
}
|
||||
const ok = await RemoveItemById(collection.hostKey, collection.dbKey, collection.key, activePath[0]);
|
||||
const sure = await dialogs.confirm('Are you sure you wish to delete this item?');
|
||||
if (!sure) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ok = await RemoveItemById(
|
||||
collection.hostKey,
|
||||
collection.dbKey,
|
||||
collection.key,
|
||||
activePath[0]
|
||||
);
|
||||
|
||||
if (ok) {
|
||||
await submitQuery();
|
||||
}
|
||||
@ -135,7 +177,6 @@
|
||||
}
|
||||
|
||||
async function saveDocument(event) {
|
||||
const progress = startProgress('Performing update…');
|
||||
const success = await UpdateFoundDocument(
|
||||
collection.hostKey,
|
||||
collection.dbKey,
|
||||
@ -148,72 +189,94 @@
|
||||
objectViewerSuccessMessage = 'Document has been saved!';
|
||||
submitQuery();
|
||||
}
|
||||
|
||||
progress.end();
|
||||
}
|
||||
|
||||
$: collection && refresh();
|
||||
onMount(refresh);
|
||||
$: visible && refresh();
|
||||
</script>
|
||||
|
||||
<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"
|
||||
<input
|
||||
type="text"
|
||||
class="code"
|
||||
placeholder={defaults.query}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
use:input
|
||||
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">
|
||||
<span class="label">Skip</span>
|
||||
<input type="number"
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
bind:value={form.skip}
|
||||
use:input
|
||||
placeholder={defaults.skip}
|
||||
list="skipstops" />
|
||||
list="skipstops"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span class="label">Limit</span>
|
||||
<input type="number"
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
bind:value={form.limit}
|
||||
use:input
|
||||
placeholder={defaults.limit}
|
||||
list="limits" />
|
||||
list="limits"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-row actions">
|
||||
<button type="submit" class="btn" title="Run query">
|
||||
<div class="formrow actions">
|
||||
<button type="submit" class="button" title="Run query">
|
||||
<Icon name="play" /> Run
|
||||
</button>
|
||||
<button class="btn secondary" type="button" on:click={() => collection.export(form)}>
|
||||
<button class="button secondary" type="button" on:click={() => collection.export(form)}>
|
||||
<Icon name="save" /> Export results…
|
||||
</button>
|
||||
<div class="field">
|
||||
<button class="btn secondary" type="button" on:click={loadQuery}>
|
||||
<button class="button secondary" type="button" on:click={loadQuery}>
|
||||
<Icon name="upload" /> Load query…
|
||||
</button>
|
||||
<button class="btn secondary" type="button" on:click={saveQuery}>
|
||||
<button class="button secondary" type="button" on:click={saveQuery}>
|
||||
<Icon name="save" /> Save as…
|
||||
</button>
|
||||
</div>
|
||||
@ -236,9 +299,11 @@
|
||||
{:else}
|
||||
<Grid
|
||||
key="_id"
|
||||
columns={$views[collection.viewKey]?.columns?.map(c => {
|
||||
return { key: c.key, title: c.key };
|
||||
}) || []}
|
||||
columns={$views[collection.viewKey]?.columns
|
||||
?.filter(c => c.showInTable)
|
||||
.map(c => {
|
||||
return { key: c.key, title: c.key };
|
||||
}) || []}
|
||||
showHeaders={true}
|
||||
items={result.results ? result.results.map(r => EJSON.deserialize(r)) : []}
|
||||
bind:activePath
|
||||
@ -252,35 +317,83 @@
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div>
|
||||
{#key result}
|
||||
<span class="flash-green">Results: {result.total || 0}</span>
|
||||
{/key}
|
||||
<div class="count">
|
||||
{#if counting}
|
||||
<span>Counting items…</span>
|
||||
{:else if countResult?.error}
|
||||
<span>{countResult.error}</span>
|
||||
{:else if countResult?.total === -1}
|
||||
<span>Something went wrong</span>
|
||||
{:else if countResult?.total}
|
||||
<!-- svelte-ignore a11y-invalid-attribute -->
|
||||
<a href="" on:click|preventDefault={countItems}>Results: {countResult.total}</a>
|
||||
{:else if result?.total === -1}
|
||||
<!-- svelte-ignore a11y-invalid-attribute -->
|
||||
<a href="" on:click|preventDefault={countItems}>Count items</a>
|
||||
{:else if result?.total}
|
||||
{#key result}
|
||||
<span class="flash-green">Results: {result.total || 0}</span>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="field inline">
|
||||
<select bind:value={collection.viewKey}>
|
||||
{#each Object.entries(viewsForCollection) as [ key, view ]}
|
||||
{#each Object.entries(viewsForCollection) as [
|
||||
key,
|
||||
view,
|
||||
]}
|
||||
<option value={key}>{view.name}</option>
|
||||
{/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,12 +402,24 @@
|
||||
</div>
|
||||
|
||||
{#if objectViewerData}
|
||||
<!-- @todo Implement save -->
|
||||
<ObjectViewer bind:data={objectViewerData} saveable on:save={saveDocument} bind:successMessage={objectViewerSuccessMessage} />
|
||||
<ObjectViewer
|
||||
bind:data={objectViewerData}
|
||||
saveable
|
||||
on:save={saveDocument}
|
||||
bind:successMessage={objectViewerSuccessMessage}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<datalist id="limits">
|
||||
{#each [ 1, 5, 10, 25, 50, 100, 200 ] as value}
|
||||
{#each [
|
||||
1,
|
||||
5,
|
||||
10,
|
||||
25,
|
||||
50,
|
||||
100,
|
||||
200,
|
||||
] as value}
|
||||
<option {value} />
|
||||
{/each}
|
||||
</datalist>
|
||||
@ -314,18 +439,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;
|
||||
@ -351,4 +476,8 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.count {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,7 +1,6 @@
|
||||
<script>
|
||||
import BlankState from '$components/blankstate.svelte';
|
||||
import TabBar from '$components/tabbar.svelte';
|
||||
import { EventsOn } from '$wails/runtime/runtime';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
import Aggregate from './aggregate.svelte';
|
||||
@ -9,61 +8,53 @@
|
||||
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';
|
||||
|
||||
export let host;
|
||||
export let database;
|
||||
export let collection;
|
||||
export let hostKey;
|
||||
export let dbKey;
|
||||
export let collKey;
|
||||
export let tab = 'find';
|
||||
|
||||
let tab = 'find';
|
||||
let find;
|
||||
const tabs = {
|
||||
stats: { icon: 'chart', title: 'Stats', component: Stats },
|
||||
find: { icon: 'db', title: 'Find', component: Find },
|
||||
insert: { icon: '+', title: 'Insert', component: Insert },
|
||||
update: { icon: 'edit', title: 'Update', component: Update },
|
||||
remove: { icon: 'trash', title: 'Remove', component: Remove },
|
||||
indexes: { icon: 'list', title: 'Indexes', component: Indexes },
|
||||
aggregate: { icon: 're', title: 'Aggregate', component: Aggregate },
|
||||
shell: { icon: 'shell', title: 'Shell', component: Shell },
|
||||
};
|
||||
|
||||
$: if (collection) {
|
||||
collection.hostKey = hostKey;
|
||||
collection.dbKey = dbKey;
|
||||
collection.key = collKey;
|
||||
for (const key of Object.keys(tabs)) {
|
||||
tabs[key].key = key;
|
||||
}
|
||||
|
||||
$: if (hostKey || dbKey || collKey) {
|
||||
tab = 'find';
|
||||
}
|
||||
|
||||
EventsOn('OpenCollectionTab', name => (tab = name || tab));
|
||||
|
||||
async function catchQuery(event) {
|
||||
tab = 'find';
|
||||
await tick();
|
||||
find.performQuery(event.detail);
|
||||
tabs.find.instance.performQuery(event.detail);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="view" class:empty={!collection}>
|
||||
{#if collection}
|
||||
{#key collection}
|
||||
<TabBar tabs={[
|
||||
{ key: 'stats', icon: 'chart', title: 'Stats' },
|
||||
{ key: 'find', icon: 'db', title: 'Find' },
|
||||
{ key: 'insert', icon: '+', title: 'Insert' },
|
||||
{ key: 'update', icon: 'edit', title: 'Update' },
|
||||
{ key: 'remove', icon: 'trash', title: 'Remove' },
|
||||
{ key: 'indexes', icon: 'list', title: 'Indexes' },
|
||||
{ key: 'aggregate', icon: 're', title: 'Aggregate' },
|
||||
]}
|
||||
bind:selectedKey={tab} />
|
||||
<TabBar tabs={Object.values(tabs)} bind:selectedKey={tab} />
|
||||
|
||||
<div class="container">
|
||||
{#if tab === 'stats'} <Stats {collection} />
|
||||
{:else if tab === 'find'} <Find {collection} bind:this={find} />
|
||||
{:else if tab === 'insert'} <Insert {collection} on:performFind={catchQuery} />
|
||||
{:else if tab === 'update'} <Update {collection} on:performFind={catchQuery} />
|
||||
{:else if tab === 'remove'} <Remove {collection} />
|
||||
{:else if tab === 'indexes'} <Indexes {collection} />
|
||||
{:else if tab === 'aggregate'} <Aggregate {collection} />
|
||||
{/if}
|
||||
{#each Object.values(tabs) as view}
|
||||
<div class="container" class:hidden={tab !== view.key}>
|
||||
<svelte:component
|
||||
this={view.component}
|
||||
visible={tab === view.key}
|
||||
on:performFind={catchQuery}
|
||||
{host}
|
||||
{database}
|
||||
{collection}
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{/each}
|
||||
{:else}
|
||||
<BlankState label="Select a collection to continue" />
|
||||
{/if}
|
||||
@ -87,6 +78,9 @@
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.container.hidden {
|
||||
display: none;
|
||||
}
|
||||
.container > :global(*) {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -1,16 +1,23 @@
|
||||
<script>
|
||||
import Icon from '$components/icon.svelte';
|
||||
import ObjectGrid from '$components/objectgrid.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import ObjectGrid from '$components/grid/objectgrid.svelte';
|
||||
|
||||
export let collection;
|
||||
export let visible = false;
|
||||
|
||||
let activePath = [];
|
||||
let _indexes = [];
|
||||
let error = '';
|
||||
let busy = false;
|
||||
|
||||
async function refresh() {
|
||||
if (!visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
busy = 'Fetching indexes…';
|
||||
error = await collection.getIndexes();
|
||||
|
||||
if (!error) {
|
||||
_indexes = collection.indexes.map(idx => {
|
||||
return {
|
||||
@ -22,6 +29,8 @@
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
busy = false;
|
||||
}
|
||||
|
||||
async function createIndex() {
|
||||
@ -44,7 +53,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
onMount(refresh);
|
||||
$: visible && refresh();
|
||||
</script>
|
||||
|
||||
<div class="indexes">
|
||||
@ -56,17 +65,20 @@
|
||||
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>
|
||||
|
@ -1,19 +1,20 @@
|
||||
<script>
|
||||
import Details from '$components/details.svelte';
|
||||
import Grid from '$components/grid.svelte';
|
||||
import Grid from '$components/grid/grid.svelte';
|
||||
import Icon from '$components/icon.svelte';
|
||||
import ObjectEditor from '$components/objecteditor.svelte';
|
||||
import ObjectEditor from '$components/editors/objecteditor.svelte';
|
||||
import ObjectViewer from '$components/objectviewer.svelte';
|
||||
import { randomString } from '$lib/math';
|
||||
import { inputTypes } from '$lib/mongo';
|
||||
import views from '$lib/stores/views';
|
||||
import { capitalise, convertLooseJson, jsonLooseParse } from '$lib/strings';
|
||||
import { InsertItems } from '$wails/go/app/App';
|
||||
import { randomString } from '$lib/math.js';
|
||||
import { inputTypes } from '$lib/mongo/index.js';
|
||||
import views from '$lib/stores/views.js';
|
||||
import { capitalise, convertLooseJson, jsonLooseParse } from '$lib/strings.js';
|
||||
import { InsertItems } from '$wails/go/app/App.js';
|
||||
import { EJSON } from 'bson';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import Form from './components/form.svelte';
|
||||
|
||||
export let collection;
|
||||
export let visible = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const formValidity = {};
|
||||
@ -73,12 +74,7 @@
|
||||
}
|
||||
|
||||
function showJson() {
|
||||
if (viewType === 'form') {
|
||||
objectViewerData = { ...(newItems[0] || {}) };
|
||||
}
|
||||
else if (viewType === 'table') {
|
||||
objectViewerData = [ ...newItems ];
|
||||
}
|
||||
objectViewerData = [ ...newItems ];
|
||||
}
|
||||
|
||||
function addRow(beforeIndex = -1) {
|
||||
@ -103,6 +99,8 @@
|
||||
views.openConfig(collection);
|
||||
}
|
||||
|
||||
$: visible && editor.focus();
|
||||
|
||||
onMount(() => {
|
||||
if (collection.viewKey === 'list') {
|
||||
editor.dispatch({
|
||||
@ -115,7 +113,6 @@
|
||||
anchor: 3,
|
||||
},
|
||||
});
|
||||
editor.focus();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -151,7 +148,6 @@
|
||||
<div class="table">
|
||||
<Grid
|
||||
key="id"
|
||||
items={newItems}
|
||||
columns={$views[collection.viewKey]?.columns
|
||||
?.filter(c => inputTypes.includes(c.inputType))
|
||||
.map(c => {
|
||||
@ -162,7 +158,9 @@
|
||||
canRemoveItems={true}
|
||||
hideChildrenToggles={true}
|
||||
on:addRow={addRow}
|
||||
on:removeItem={() => deleteRow()}
|
||||
bind:inputsValid={allValid}
|
||||
bind:items={newItems}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@ -182,13 +180,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 +196,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 {
|
||||
|
@ -1,11 +1,12 @@
|
||||
<script>
|
||||
import Icon from '$components/icon.svelte';
|
||||
import ObjectEditor from '$components/objecteditor.svelte';
|
||||
import { convertLooseJson } from '$lib/strings';
|
||||
import { RemoveItems } from '$wails/go/app/App';
|
||||
import ObjectEditor from '$components/editors/objecteditor.svelte';
|
||||
import { convertLooseJson } from '$lib/strings.js';
|
||||
import { RemoveItems } from '$wails/go/app/App.js';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let collection;
|
||||
export let visible = false;
|
||||
|
||||
let json = '';
|
||||
let many = true;
|
||||
@ -23,6 +24,8 @@
|
||||
);
|
||||
}
|
||||
|
||||
$: visible && editor.focus();
|
||||
|
||||
onMount(() => {
|
||||
editor.dispatch({
|
||||
changes: {
|
||||
@ -34,7 +37,6 @@
|
||||
anchor: 3,
|
||||
},
|
||||
});
|
||||
editor.focus();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -45,7 +47,7 @@
|
||||
</label>
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" class="btn danger">
|
||||
<button type="submit" class="button danger">
|
||||
<Icon name="-" /> Remove
|
||||
</button>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import Icon from '$components/icon.svelte';
|
||||
import ObjectGrid from '$components/objectgrid.svelte';
|
||||
import ObjectGrid from '$components/grid/objectgrid.svelte';
|
||||
|
||||
export let collection;
|
||||
|
||||
@ -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>
|
||||
|
@ -1,10 +1,10 @@
|
||||
<script>
|
||||
import Icon from '$components/icon.svelte';
|
||||
import input from '$lib/actions/input';
|
||||
import { atomicUpdateOperators } from '$lib/mongo';
|
||||
import { deepClone } from '$lib/objects';
|
||||
import { convertLooseJson, jsonLooseParse } from '$lib/strings';
|
||||
import { UpdateItems } from '$wails/go/app/App';
|
||||
import input from '$lib/actions/input.js';
|
||||
import { atomicUpdateOperators } from '$lib/mongo/index.js';
|
||||
import { deepClone } from '$lib/objects.js';
|
||||
import { convertLooseJson, jsonLooseParse } from '$lib/strings.js';
|
||||
import { UpdateItems } from '$wails/go/app/App.js';
|
||||
|
||||
export let collection = {};
|
||||
|
||||
@ -27,7 +27,8 @@
|
||||
});
|
||||
|
||||
// function buildCode(form) {
|
||||
// let operation = '{ ' + form.parameters.filter(p => p.type).map(p => `${p.type}: ${p.value || '{}'}`).join(', ') + ' }';
|
||||
// let operation = '{ ' + form.parameters.filter(p => p.type).map(p =>
|
||||
// `${p.type}: ${p.value || '{}'}`).join(', ') + ' }';
|
||||
// if (operation === '{ }') {
|
||||
// operation = '{}';
|
||||
// }
|
||||
@ -38,7 +39,8 @@
|
||||
// form.many && (options += 'multi: true');
|
||||
// (form.upsert || form.many) && (options += ' }');
|
||||
|
||||
// const code = `db.${collection.key}.update(${form.query || '{}'}, ${operation}${options});`;
|
||||
// const code = `db.${collection.key}.update(${form.query || '{}'},
|
||||
// ${operation}${options});`;
|
||||
// return code;
|
||||
// }
|
||||
|
||||
@ -48,7 +50,13 @@
|
||||
f.parameters = f.parameters.map(param => {
|
||||
return { ...param, value: convertLooseJson(param.value) };
|
||||
});
|
||||
updatedCount = await UpdateItems(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(f));
|
||||
|
||||
updatedCount = await UpdateItems(
|
||||
collection.hostKey,
|
||||
collection.dbKey,
|
||||
collection.key,
|
||||
JSON.stringify(f)
|
||||
);
|
||||
}
|
||||
|
||||
function removeParam(index) {
|
||||
@ -107,14 +115,19 @@
|
||||
{/if}
|
||||
{/key}
|
||||
|
||||
<button class="btn" type="submit" disabled={invalid}>
|
||||
<button class="button" type="submit" disabled={invalid}>
|
||||
<Icon name="check" /> Update
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label class="field">
|
||||
<span class="label">Filter</span>
|
||||
<input type="text" class="code" bind:value={form.query} use:input={{ type: 'json', autofocus: true }} placeholder={'{}'} />
|
||||
<input
|
||||
type="text"
|
||||
class="code"
|
||||
bind:value={form.query}
|
||||
use:input={{ type: 'json', autofocus: true }}
|
||||
placeholder={'{}'} />
|
||||
</label>
|
||||
|
||||
<fieldset class="parameters">
|
||||
@ -132,14 +145,20 @@
|
||||
</optgroup>
|
||||
{/each}
|
||||
</select>
|
||||
<input type="text" class="code" bind:value={param.value} placeholder={'{}'} use:input={{ type: 'json' }} />
|
||||
<input
|
||||
type="text"
|
||||
class="code"
|
||||
bind:value={param.value}
|
||||
placeholder={'{}'}
|
||||
use:input={{ type: 'json' }}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<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>
|
||||
|
@ -1,11 +1,10 @@
|
||||
<script>
|
||||
import DirectoryChooser from '$components/directorychooser.svelte';
|
||||
import Grid from '$components/grid.svelte';
|
||||
import DirectoryChooser from '$components/editors/directorychooser.svelte';
|
||||
import Grid from '$components/grid/grid.svelte';
|
||||
import Modal from '$components/modal.svelte';
|
||||
import { startProgress } from '$lib/progress';
|
||||
import hostTree from '$lib/stores/hosttree';
|
||||
import applicationSettings from '$lib/stores/settings';
|
||||
import { OpenConnection, OpenDatabase } from '$wails/go/app/App';
|
||||
import hostTree from '$lib/stores/hosttree.js';
|
||||
import applicationSettings from '$lib/stores/settings.js';
|
||||
import { OpenConnection, OpenDatabase } from '$wails/go/app/App.js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let info = {};
|
||||
@ -23,7 +22,6 @@
|
||||
info.collKeys = [];
|
||||
|
||||
if (hostKey) {
|
||||
const progress = startProgress(`Opening connection to host "${hostKey}"`);
|
||||
const databases = await OpenConnection(hostKey);
|
||||
|
||||
if (databases && !$hostTree[hostKey]) {
|
||||
@ -32,8 +30,6 @@
|
||||
$hostTree[hostKey].databases[dbKey] = $hostTree[hostKey].databases[dbKey] || { collections: {} };
|
||||
});
|
||||
}
|
||||
|
||||
progress.end();
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,14 +38,11 @@
|
||||
info.dbKey = dbKey;
|
||||
|
||||
if (dbKey) {
|
||||
const progress = startProgress(`Opening database "${dbKey}"`);
|
||||
const collections = await OpenDatabase(info.hostKey, dbKey);
|
||||
|
||||
for (const collKey of collections?.sort() || []) {
|
||||
$hostTree[info.hostKey].databases[dbKey].collections[collKey] = {};
|
||||
}
|
||||
|
||||
progress.end();
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,7 +119,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>
|
||||
|
||||
|
@ -1,22 +1,22 @@
|
||||
<script>
|
||||
import BlankState from '$components/blankstate.svelte';
|
||||
import TabBar from '$components/tabbar.svelte';
|
||||
import { EventsOn } from '$wails/runtime/runtime';
|
||||
import { EventsOn } from '$wails/runtime/runtime.js';
|
||||
|
||||
import Shell from '../shell.svelte';
|
||||
import Stats from './stats.svelte';
|
||||
|
||||
export let host;
|
||||
export let database;
|
||||
export let hostKey;
|
||||
export let dbKey;
|
||||
export let tab = 'stats';
|
||||
|
||||
let tab = 'stats';
|
||||
const tabs = {
|
||||
stats: { icon: 'chart', title: 'Database stats', component: Stats },
|
||||
shell: { icon: 'shell', title: 'Shell', component: Shell },
|
||||
};
|
||||
|
||||
$: if (database) {
|
||||
database.hostKey = hostKey;
|
||||
database.dbKey = dbKey;
|
||||
}
|
||||
|
||||
$: if (hostKey || dbKey) {
|
||||
tab = 'stats';
|
||||
for (const key of Object.keys(tabs)) {
|
||||
tabs[key].key = key;
|
||||
}
|
||||
|
||||
EventsOn('OpenStatsTab', name => (tab = name || tab));
|
||||
@ -25,11 +25,13 @@
|
||||
<div class="view" class:empty={!database}>
|
||||
{#if database}
|
||||
{#key database}
|
||||
<TabBar tabs={[ { key: 'stats', icon: 'chart', title: 'Database stats' } ]} bind:selectedKey={tab} />
|
||||
<div class="container">
|
||||
{#if tab === 'stats'} <Stats {database} />
|
||||
{/if}
|
||||
</div>
|
||||
<TabBar tabs={Object.values(tabs)} bind:selectedKey={tab} />
|
||||
|
||||
{#each Object.values(tabs) as view}
|
||||
<div class="container" class:hidden={tab !== view.key}>
|
||||
<svelte:component this={view.component} visible={tab === view.key} {host} {database} />
|
||||
</div>
|
||||
{/each}
|
||||
{/key}
|
||||
{:else}
|
||||
<BlankState label="Select a database to continue" />
|
||||
@ -54,6 +56,9 @@
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.container.hidden {
|
||||
display: none;
|
||||
}
|
||||
.container > :global(*) {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import Icon from '$components/icon.svelte';
|
||||
import ObjectGrid from '$components/objectgrid.svelte';
|
||||
import ObjectGrid from '$components/grid/objectgrid.svelte';
|
||||
|
||||
export let database;
|
||||
|
||||
@ -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>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<script>
|
||||
import Modal from '$components/modal.svelte';
|
||||
import input from '$lib/actions/input';
|
||||
import hostTree from '$lib/stores/hosttree';
|
||||
import { AddHost, UpdateHost } from '$wails/go/app/App';
|
||||
import input from '$lib/actions/input.js';
|
||||
import hostTree from '$lib/stores/hosttree.js';
|
||||
import { AddHost, UpdateHost } from '$wails/go/app/App.js';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
|
||||
export let hostKey = '';
|
||||
@ -54,7 +54,13 @@
|
||||
|
||||
<label class="field">
|
||||
<span class="label">Connection string</span>
|
||||
<input type="text" placeholder="mongodb://..." bind:value={form.uri} spellcheck="false" use:input />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="mongodb://..."
|
||||
bind:value={form.uri}
|
||||
spellcheck="false"
|
||||
use:input
|
||||
/>
|
||||
</label>
|
||||
</form>
|
||||
|
||||
@ -64,7 +70,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>
|
||||
|
@ -1,21 +1,25 @@
|
||||
<script>
|
||||
import BlankState from '$components/blankstate.svelte';
|
||||
import TabBar from '$components/tabbar.svelte';
|
||||
import { EventsOn } from '$wails/runtime/runtime';
|
||||
import { EventsOn } from '$wails/runtime/runtime.js';
|
||||
|
||||
import Logs from './logs.svelte';
|
||||
import Shell from '../shell.svelte';
|
||||
import Status from './status.svelte';
|
||||
import SystemInfo from './systeminfo.svelte';
|
||||
|
||||
export let host;
|
||||
export let hostKey;
|
||||
export let tab = 'status';
|
||||
|
||||
let tab = 'status';
|
||||
const tabs = {
|
||||
status: { icon: 'chart', title: 'Host status', component: Status },
|
||||
shell: { icon: 'shell', title: 'Shell', component: Shell },
|
||||
logs: { icon: 'doc', title: 'Logs', component: Logs },
|
||||
systemInfo: { icon: 'server', title: 'System info', component: SystemInfo },
|
||||
};
|
||||
|
||||
$: if (host) {
|
||||
host.hostKey = hostKey;
|
||||
}
|
||||
|
||||
$: if (hostKey) {
|
||||
tab = 'status';
|
||||
for (const key of Object.keys(tabs)) {
|
||||
tabs[key].key = key;
|
||||
}
|
||||
|
||||
EventsOn('OpenStatusTab', name => (tab = name || tab));
|
||||
@ -24,17 +28,13 @@
|
||||
<div class="view" class:empty={!host}>
|
||||
{#if host}
|
||||
{#key host}
|
||||
<TabBar tabs={[
|
||||
{ key: 'status', icon: 'chart', title: 'Host status' },
|
||||
{ key: 'systemInfo', icon: 'server', title: 'System info' },
|
||||
]}
|
||||
bind:selectedKey={tab} />
|
||||
<TabBar tabs={Object.values(tabs)} bind:selectedKey={tab} />
|
||||
|
||||
<div class="container">
|
||||
{#if tab === 'status'} <Status {host} />
|
||||
{:else if tab === 'systemInfo'} <SystemInfo {host} />
|
||||
{/if}
|
||||
</div>
|
||||
{#each Object.values(tabs) as view}
|
||||
<div class="container" class:hidden={tab !== view.key}>
|
||||
<svelte:component this={view.component} visible={tab === view.key} {host} />
|
||||
</div>
|
||||
{/each}
|
||||
{/key}
|
||||
{:else}
|
||||
<BlankState label="Select a host to continue" />
|
||||
@ -59,6 +59,9 @@
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.container.hidden {
|
||||
display: none;
|
||||
}
|
||||
.container > :global(*) {
|
||||
width: 100%;
|
||||
}
|
||||
|