Compare commits
180 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 | |||
0027a4333b | |||
|
2a29f5226c | ||
a12417d4e9 | |||
58dd9de4ba | |||
77ce1ab842 | |||
b5bb51c869 | |||
dc0094b27c | |||
964e66e8a3 | |||
7eb41630db | |||
2ecc664e48 | |||
756fa10555 | |||
bd265d0548 | |||
c41572dbb7 | |||
9a07954e4c | |||
535b474d65 | |||
43b059a579 | |||
0365e8e0ec | |||
fcdefedffd | |||
005f23c322 | |||
030edaa561 | |||
|
a4262a6ab7 | ||
|
982d112665 | ||
|
e388b2513b | ||
c751169b7d | |||
0a3f99fa32 | |||
a1456b3987 | |||
3fe5f09163 | |||
|
29154d04d7 | ||
|
cde13d9ec0 | ||
|
19d9b8addd | ||
|
67ebaecb31 | ||
3fd2be55b0 | |||
2ee5c7dadd | |||
6cc329982a | |||
15d30e67c3 | |||
abc9df0897 | |||
be4e3e778e | |||
be7643bd31 | |||
ea376f5ba7 | |||
b29b534b2f | |||
|
775e4d98d8 | ||
f44165e41a | |||
|
9c1e6fe37a | ||
|
c0f072b72e | ||
|
8e5beb9a2d | ||
|
4e12591fc8 | ||
|
0ddf172327 | ||
cc1f67af45 | |||
6e7ca49623 | |||
1524cdb244 | |||
2fc7607ea7 | |||
1327714557 | |||
f2401a07c2 | |||
7ae3fdab53 | |||
1308d74967 | |||
cc1b4fb04c | |||
12d398320d | |||
c5c3c6f873 | |||
9090f9425f | |||
5240bdefab | |||
d0b5c0616e | |||
cd946307db | |||
ec99c15ff2 | |||
536740259d | |||
a44f4c8b7e | |||
4b618f9b25 | |||
5463b1a2c4 | |||
7b8efa3022 |
9
.gitattributes
vendored
@ -1,2 +1,9 @@
|
||||
/frontend/src/components/icon.svelte linguist-vendored
|
||||
# Let GitHub Linguist ignore documentation and Wails generated files
|
||||
# https://github.com/github-linguist/linguist/blob/master/docs/overrides.md
|
||||
/frontend/wailsjs/**/* linguist-generated
|
||||
/website/**/* linguist-documentation
|
||||
|
||||
# Exclude certain files from exports
|
||||
/.github/**/* export-ignore
|
||||
/.vscode/**/* export-ignore
|
||||
/website/**/* export-ignore
|
||||
|
4
.github/ISSUE_TEMPLATE/docs.yml
vendored
@ -22,10 +22,8 @@ body:
|
||||
attributes:
|
||||
label: Problem
|
||||
description: Why is it wrong?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Solution
|
||||
|
81
.github/workflows/ci.yml
vendored
@ -1,10 +1,8 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@ -13,18 +11,24 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [windows-2022, macos-12, ubuntu-22.04]
|
||||
go-version: [1.18]
|
||||
node-version: [16]
|
||||
platform:
|
||||
- windows-2019
|
||||
- windows-2022
|
||||
- macos-13
|
||||
- macos-14
|
||||
- ubuntu-22.04
|
||||
- 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
|
||||
@ -32,25 +36,62 @@ jobs:
|
||||
- name: Install Wails
|
||||
run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||
|
||||
- name: Install Wails dependencies for Linux
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
run: sudo apt-get install gtk+-3.0 webkit2gtk-4.0
|
||||
- name: Install build dependencies for macOS
|
||||
if: contains(matrix.platform, 'macos')
|
||||
run: npm install --global appdmg
|
||||
|
||||
- name: Install build dependencies for Linux
|
||||
if: contains(matrix.platform, 'ubuntu')
|
||||
run: sudo apt-get 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
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: cd frontend && npm ci && cd ..
|
||||
- name: Cross-compile Rolens for Windows
|
||||
if: contains(matrix.platform, 'windows')
|
||||
run: ./build/windows/ci_generate.ps1 -platform "${{ matrix.platform }}"
|
||||
|
||||
- name: Build Rolens
|
||||
run: wails build
|
||||
- name: Cross-compile Rolens for Darwin
|
||||
if: contains(matrix.platform, 'macos')
|
||||
run: ./build/darwin/ci_generate.sh "${{ matrix.platform }}"
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v2
|
||||
- name: Cross-compile Rolens for Linux
|
||||
if: contains(matrix.platform, 'ubuntu')
|
||||
run: ./build/linux/ci_generate.sh "${{ matrix.platform }}"
|
||||
|
||||
- name: Upload generated binaries
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rolens-${{ matrix.platform }}
|
||||
path: build/bin/*
|
||||
path: releases/*
|
||||
|
||||
- name: Test build script for users
|
||||
run: node ./build.js
|
||||
|
||||
bundle:
|
||||
name: Bundle artifacts
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build
|
||||
if: ${{ always() }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Place all tarballs in the same directory
|
||||
run: build/ci_bundle.sh
|
||||
|
||||
- name: Upload the bundle as an artifact
|
||||
uses: actions/upload-artifact@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
|
||||
|
22
.github/workflows/lint.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
name: Linter
|
||||
|
||||
on:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
eslint:
|
||||
name: Run ESLint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
working-directory: ./frontend
|
||||
|
||||
- name: Run ESLint
|
||||
run: npx eslint .
|
||||
working-directory: ./frontend
|
6
.gitignore
vendored
@ -1,7 +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/
|
||||
|
||||
/frontend/node_modules/
|
||||
/frontend/dist/
|
||||
|
11
.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
|
||||
},
|
||||
@ -14,5 +14,12 @@
|
||||
"[css]": {
|
||||
"editor.suggest.insertMode": "replace",
|
||||
"editor.tabSize": 2
|
||||
}
|
||||
},
|
||||
|
||||
"eslint.format.enable": true,
|
||||
"eslint.lintTask.enable": true,
|
||||
"eslint.lintTask.options": "frontend",
|
||||
"eslint.workingDirectories": [
|
||||
"frontend"
|
||||
]
|
||||
}
|
||||
|
53
CHANGELOG.md
@ -1,9 +1,58 @@
|
||||
## [v0.2.0](https://github.com/garraflavatra/rolens/releases/tag/v0.2.0)
|
||||
## [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).
|
||||
* Added version number of the running Rolens instance in the about dialog (#25, #28).
|
||||
* Added meaningful window titles, and actually show these in the title bar (macOS).
|
||||
* Corrected link to documentation in the about box (#30).
|
||||
* Fixed host/database selection bug in grid (#31, #32), involving a frontend refactoring.
|
||||
* Replaced (some) harsh loading dialogs with smooth spinners, and replaced (some) capricious error dialogs with friendly error messages.
|
||||
|
||||
## [v0.2.0]
|
||||
|
||||
* Added some external links related to Rolens to the application menu.
|
||||
* Fix an infinite loop bug when a document in the find view has been double clicked.
|
||||
* Find view: added the ability to make changes to single documents from within the object editor.
|
||||
|
||||
## [v0.1.0](https://github.com/garraflavatra/rolens/releases/tag/v0.1.0)
|
||||
## [v0.1.0]
|
||||
|
||||
Initial release.
|
||||
|
||||
[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
|
||||
|
41
README.md
@ -2,11 +2,13 @@
|
||||
|
||||
Robust, blazing-fast, comprehensive, yet simple [MongoDB](https://www.mongodb.com/) administration tool for Windows, macOS and Linux.
|
||||
|
||||
<a href="https://github.com/garraflavatra/rolens/actions/workflows/ci.yml" target="_blank"><img src="https://github.com/garraflavatra/rolens/actions/workflows/ci.yml/badge.svg" alt="CI" /></a> <a target="_blank" href="https://garraflavatra.github.io/rolens"><img src="./.github/docs-badge.svg" alt="Documentation" /></a> <a href="https://fosstodon.org/@rolens" target="_blank" rel="me"><img src="./.github/fosstodon-badge.svg" alt="Fosstodon" /></a>
|
||||
<a href="https://github.com/garraflavatra/rolens/actions/workflows/ci.yml" target="_blank"><img src="https://github.com/garraflavatra/rolens/actions/workflows/ci.yml/badge.svg" alt="CI" /></a> <a target="_blank" href="https://garraflavatra.github.io/rolens"><img src="./.github/docs-badge.svg" alt="Documentation" /></a>
|
||||
|
||||
<!-- <a href="https://fosstodon.org/@rolens" target="_blank" rel="me"><img src="./.github/fosstodon-badge.svg" alt="Fosstodon" /></a> -->
|
||||
|
||||
## Why another MongoDB client?
|
||||
|
||||
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.**
|
||||
|
||||
@ -15,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.
|
||||
|
||||
@ -34,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
|
||||
|
||||
@ -48,11 +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,19 +1,10 @@
|
||||
# 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
|
||||
|
||||
The `darwin` directory holds files specific to Mac builds.
|
||||
These may be customised and used as part of the build. To return these files to the default state, simply delete them
|
||||
and
|
||||
build with `wails build`.
|
||||
The `darwin` directory holds files specific to Mac builds. These may be customised and used as part of the build. To return these files to the default state, simply delete them and build with `wails build`.
|
||||
|
||||
The directory contains the following files:
|
||||
|
||||
@ -22,14 +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.
|
||||
- `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.
|
||||
|
40
build/ci_bundle.sh
Executable file
@ -0,0 +1,40 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# This script bundles the binaries generated by GitHub Actions.
|
||||
#
|
||||
# Platforms to choose from:
|
||||
# - windows-2019
|
||||
# - windows-2022
|
||||
# - macos-11
|
||||
# - macos-12
|
||||
# - macos-13
|
||||
# - ubuntu-20.04
|
||||
# - ubuntu-22.04
|
||||
#
|
||||
# Bundles to choose from:
|
||||
# - rolens-macos-11-amd64.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
|
||||
# - rolens-windows-2019-arm64.zip
|
||||
# - rolens-windows-2022-amd64.zip
|
||||
# - rolens-windows-2022-arm64.zip
|
||||
#
|
||||
|
||||
node ./build/version_to_file.js
|
||||
version=$(<./build/version.txt)
|
||||
|
||||
mkdir bundle
|
||||
|
||||
# macOS 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 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"
|
39
build/darwin/ci_generate.sh
Executable file
@ -0,0 +1,39 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Cleanup
|
||||
rm -rf releases
|
||||
rm -rf build/bin
|
||||
mkdir releases
|
||||
mkdir -p build/bin
|
||||
|
||||
# Settings
|
||||
cat > build/darwin/dmg_settings.json << EOF
|
||||
{
|
||||
"title": "Rolens",
|
||||
"background": "$(pwd)/build/darwin/dmg_background.png",
|
||||
"icon-size": 100,
|
||||
"window": {
|
||||
"size": { "width": 750, "height": 400 }
|
||||
},
|
||||
"contents": [
|
||||
{ "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
|
||||
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.dmg
|
||||
|
||||
# ARM/AppleM1
|
||||
wails build -platform darwin/arm64
|
||||
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
|
BIN
build/darwin/dmg_background.png
Normal file
After Width: | Height: | Size: 16 KiB |
9
build/linux/ci_generate.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
mkdir releases
|
||||
wails build -platform linux/amd64
|
||||
tar -czvf releases/rolens-$1-amd64.tar.gz --directory build/bin Rolens
|
||||
|
||||
# rm -rf build/bin
|
||||
# wails build -platform linux/arm64
|
||||
# tar -czvf releases/rolens-$1-arm64.tar.gz --directory build/bin Rolens
|
32
build/release_readme.txt
Normal file
@ -0,0 +1,32 @@
|
||||
Thank you for downloading Rolens!
|
||||
=================================
|
||||
|
||||
Star the project on GitHub:
|
||||
https://github.com/garraflavatra/rolens
|
||||
|
||||
User guide
|
||||
----------
|
||||
|
||||
Rolens is designed to be as intuitive as possible. But if something is unclear
|
||||
nevertheless, you can consult the user manual:
|
||||
https://garraflavatra.github.io/rolens/
|
||||
|
||||
Questions and bugs
|
||||
------------------
|
||||
|
||||
Did you capture a bug? Please report it — thank you!
|
||||
https://github.com/garraflavatra/rolens/issues/new?assignees=garraflavatra&labels=bug&projects=&template=bug.yml
|
||||
|
||||
Would you like to see a new feature? You can request it:
|
||||
https://github.com/garraflavatra/rolens/issues/new?assignees=garraflavatra&labels=enhancement&projects=&template=feature.yml
|
||||
|
||||
Do you have a question? Ask questions on the discussion board:
|
||||
https://github.com/garraflavatra/rolens/discussions/new?category=questions
|
||||
|
||||
Feel free to contact me if you have questions! Send an e-mail to romein@vburen.nl
|
||||
|
||||
Author and license
|
||||
------------------
|
||||
|
||||
© Romein van Buren 2022-2025. The source code and compiled binaries are released
|
||||
under the GNU GPLv3 license — see LICENSE for the full license text.
|
13
build/version_to_file.js
Normal file
@ -0,0 +1,13 @@
|
||||
#!/usr/bin/node
|
||||
|
||||
// This script extracts the version number from wails.json in the project root
|
||||
// and writes it to version.txt
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
fs.writeFileSync(
|
||||
__dirname + '/version.txt',
|
||||
JSON.parse(
|
||||
fs.readFileSync(__dirname + '/../wails.json')
|
||||
).info.productVersion
|
||||
);
|
11
build/windows/ci_generate.ps1
Executable file
@ -0,0 +1,11 @@
|
||||
param([string]$platform)
|
||||
|
||||
mkdir releases
|
||||
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 -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,11 +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
|
||||
|
||||
* 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,42 +1,33 @@
|
||||
# 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:
|
||||
|
||||
* `wails build` to generate an executable for your platform.
|
||||
* `wails build -nsis` to generate an [NSIS installer](https://nsis.sourceforge.io/Main_Page). This requires that you have NSIS installed on your machine.
|
||||
* `wails build -nsis` to generate an [NSIS installer](https://nsis.sourceforge.io/Main_Page) for Windows. This requires that you have NSIS installed on your machine.
|
||||
|
||||
The generated binary will live in `build/bin`. You may want to run the installer (Windows) or move the app to the Applications folder (Mac).
|
||||
|
||||
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: 623 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 %}
|
@ -1,195 +0,0 @@
|
||||
{
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:svelte/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"svelte",
|
||||
"import"
|
||||
],
|
||||
"overrides": [{
|
||||
"files": "*.svelte",
|
||||
"rules": {
|
||||
"no-inner-declarations": 0,
|
||||
"svelte/no-inner-declarations": [
|
||||
"error",
|
||||
"functions"
|
||||
]
|
||||
}
|
||||
}],
|
||||
"rules": {
|
||||
"no-undef": [
|
||||
"error",
|
||||
{ "typeof": true }
|
||||
],
|
||||
"require-atomic-updates": 0,
|
||||
"indent": [
|
||||
"error",
|
||||
2,
|
||||
{ "SwitchCase": 1 }
|
||||
],
|
||||
"strict": 0,
|
||||
"quotes": [ "error", "single" ],
|
||||
"semi": [ "warn", "always" ],
|
||||
"accessor-pairs": "error",
|
||||
"array-bracket-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"arrow-body-style": [
|
||||
"error",
|
||||
"as-needed",
|
||||
{ "requireReturnForObjectLiteral": true }
|
||||
],
|
||||
"arrow-parens": [ "error", "as-needed" ],
|
||||
"arrow-spacing": "error",
|
||||
"block-spacing": [ "error", "always" ],
|
||||
"comma-dangle": [
|
||||
"warn",
|
||||
{
|
||||
"arrays": "always-multiline",
|
||||
"objects": "always-multiline",
|
||||
"imports": "never",
|
||||
"exports": "never",
|
||||
"functions": "never"
|
||||
}
|
||||
],
|
||||
"comma-spacing": "error",
|
||||
"computed-property-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"generator-star-spacing": "error",
|
||||
"id-blacklist": "error",
|
||||
"id-match": "error",
|
||||
"jsx-quotes": "error",
|
||||
"keyword-spacing": "error",
|
||||
"key-spacing": [
|
||||
"warn",
|
||||
{ "beforeColon": false }
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"no-unused-vars": "warn",
|
||||
"no-alert": "error",
|
||||
"no-caller": "error",
|
||||
"no-confusing-arrow": [
|
||||
"error",
|
||||
{ "allowParens": true }
|
||||
],
|
||||
"no-console": "off",
|
||||
"no-div-regex": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-extend-native": "error",
|
||||
"no-extra-label": "error",
|
||||
"no-fallthrough": "off",
|
||||
"no-floating-decimal": "error",
|
||||
"no-implicit-coercion": [
|
||||
"error",
|
||||
{
|
||||
"boolean": false,
|
||||
"number": false,
|
||||
"string": false
|
||||
}
|
||||
],
|
||||
"no-inner-declarations": [
|
||||
"error",
|
||||
"functions"
|
||||
],
|
||||
"no-iterator": "error",
|
||||
"no-label-var": "error",
|
||||
"no-lone-blocks": "error",
|
||||
"no-new-object": "error",
|
||||
"no-new-require": "error",
|
||||
"no-new-wrappers": "error",
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
"event",
|
||||
"name"
|
||||
],
|
||||
"no-restricted-imports": "error",
|
||||
"no-restricted-modules": "error",
|
||||
"no-restricted-syntax": "error",
|
||||
"no-script-url": "error",
|
||||
"no-self-compare": "error",
|
||||
"no-sequences": "error",
|
||||
"no-shadow-restricted-names": "error",
|
||||
"no-spaced-func": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-unmodified-loop-condition": "error",
|
||||
"no-useless-constructor": "error",
|
||||
"no-whitespace-before-property": "error",
|
||||
"no-with": "error",
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"prefer-const": "error",
|
||||
"require-yield": "error",
|
||||
"semi-spacing": [
|
||||
"error"
|
||||
],
|
||||
"space-before-blocks": "error",
|
||||
"space-before-function-paren": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"space-in-parens": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"space-infix-ops": "error",
|
||||
"space-unary-ops": "error",
|
||||
"template-curly-spacing": "error",
|
||||
"curly": 2,
|
||||
"brace-style": [
|
||||
"error",
|
||||
"stroustrup"
|
||||
],
|
||||
"wrap-iife": [
|
||||
"error",
|
||||
"any"
|
||||
],
|
||||
"yield-star-spacing": "error",
|
||||
"multiline-ternary": [
|
||||
"warn",
|
||||
"never"
|
||||
],
|
||||
"no-nested-ternary": "error",
|
||||
"svelte/html-quotes": [
|
||||
"warn",
|
||||
{
|
||||
"prefer": "double",
|
||||
"dynamic": { "quoted": false }
|
||||
}
|
||||
],
|
||||
"svelte/no-useless-mustaches": "warn",
|
||||
"svelte/require-store-reactive-access": "warn",
|
||||
"svelte/no-reactive-literals": "error",
|
||||
"svelte/html-closing-bracket-spacing": "warn",
|
||||
"svelte/indent": "warn",
|
||||
"svelte/max-attributes-per-line": [
|
||||
"warn",
|
||||
{ "multiline": 1, "singleline": 5 }
|
||||
],
|
||||
"svelte/mustache-spacing": "warn",
|
||||
"svelte/no-extra-reactive-curlies": "error",
|
||||
"svelte/no-spaces-around-equal-signs-in-attribute": "warn",
|
||||
"svelte/prefer-class-directive": "warn",
|
||||
"svelte/shorthand-attribute": "warn",
|
||||
"svelte/shorthand-directive": "warn",
|
||||
"svelte/spaced-html-comment": "warn",
|
||||
"svelte/no-at-html-tags": 0
|
||||
}
|
||||
}
|
@ -7,14 +7,7 @@
|
||||
</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>
|
||||
</html>
|
||||
|
4284
frontend/package-lock.json
generated
@ -9,17 +9,30 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.1.8",
|
||||
"@codemirror/language": "^6.7.0",
|
||||
"@codemirror/view": "^6.12.0",
|
||||
"@codemirror/lang-javascript": "^6.1.9",
|
||||
"@codemirror/language": "^6.8.0",
|
||||
"@codemirror/view": "^6.13.2",
|
||||
"bson": "^4.7.2",
|
||||
"codemirror": "^6.0.1",
|
||||
"date-fns": "^2.30.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.1",
|
||||
"eslint": "^8.41.0",
|
||||
"svelte": "^3.59.1",
|
||||
"vite": "^3.0.0"
|
||||
"@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": "^4.0.0",
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "./node_modules/@garraflavatra/yeslint/configs/svelte.js",
|
||||
"ignorePatterns": [
|
||||
"dist",
|
||||
"wailsjs"
|
||||
],
|
||||
"rules": {
|
||||
"svelte/no-useless-mustaches": "off",
|
||||
"svelte/no-extra-reactive-curlies": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
d933a4e16d5bb0132361eef3b5d3ba22
|
||||
015746ba33749cd2864cd336088387ef
|
@ -1 +1 @@
|
||||
<svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="647.63626" height="632.17383" viewBox="0 0 647.63626 632.17383" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M687.3279,276.08691H512.81813a15.01828,15.01828,0,0,0-15,15v387.85l-2,.61005-42.81006,13.11a8.00676,8.00676,0,0,1-9.98974-5.31L315.678,271.39691a8.00313,8.00313,0,0,1,5.31006-9.99l65.97022-20.2,191.25-58.54,65.96972-20.2a7.98927,7.98927,0,0,1,9.99024,5.3l32.5498,106.32Z" transform="translate(-276.18187 -133.91309)" fill="#f2f2f2"/><path d="M725.408,274.08691l-39.23-128.14a16.99368,16.99368,0,0,0-21.23-11.28l-92.75,28.39L380.95827,221.60693l-92.75,28.4a17.0152,17.0152,0,0,0-11.28028,21.23l134.08008,437.93a17.02661,17.02661,0,0,0,16.26026,12.03,16.78926,16.78926,0,0,0,4.96972-.75l63.58008-19.46,2-.62v-2.09l-2,.61-64.16992,19.65a15.01489,15.01489,0,0,1-18.73-9.95l-134.06983-437.94a14.97935,14.97935,0,0,1,9.94971-18.73l92.75-28.4,191.24024-58.54,92.75-28.4a15.15551,15.15551,0,0,1,4.40966-.66,15.01461,15.01461,0,0,1,14.32032,10.61l39.0498,127.56.62012,2h2.08008Z" transform="translate(-276.18187 -133.91309)" fill="#3f3d56"/><path d="M398.86279,261.73389a9.0157,9.0157,0,0,1-8.61133-6.3667l-12.88037-42.07178a8.99884,8.99884,0,0,1,5.9712-11.24023l175.939-53.86377a9.00867,9.00867,0,0,1,11.24072,5.9707l12.88037,42.07227a9.01029,9.01029,0,0,1-5.9707,11.24072L401.49219,261.33887A8.976,8.976,0,0,1,398.86279,261.73389Z" transform="translate(-276.18187 -133.91309)" fill="#00008b"/><circle cx="190.15351" cy="24.95465" r="20" fill="#00008b"/><circle cx="190.15351" cy="24.95465" r="12.66462" fill="#fff"/><path d="M878.81836,716.08691h-338a8.50981,8.50981,0,0,1-8.5-8.5v-405a8.50951,8.50951,0,0,1,8.5-8.5h338a8.50982,8.50982,0,0,1,8.5,8.5v405A8.51013,8.51013,0,0,1,878.81836,716.08691Z" transform="translate(-276.18187 -133.91309)" fill="#e6e6e6"/><path d="M723.31813,274.08691h-210.5a17.02411,17.02411,0,0,0-17,17v407.8l2-.61v-407.19a15.01828,15.01828,0,0,1,15-15H723.93825Zm183.5,0h-394a17.02411,17.02411,0,0,0-17,17v458a17.0241,17.0241,0,0,0,17,17h394a17.0241,17.0241,0,0,0,17-17v-458A17.02411,17.02411,0,0,0,906.81813,274.08691Zm15,475a15.01828,15.01828,0,0,1-15,15h-394a15.01828,15.01828,0,0,1-15-15v-458a15.01828,15.01828,0,0,1,15-15h394a15.01828,15.01828,0,0,1,15,15Z" transform="translate(-276.18187 -133.91309)" fill="#3f3d56"/><path d="M801.81836,318.08691h-184a9.01015,9.01015,0,0,1-9-9v-44a9.01016,9.01016,0,0,1,9-9h184a9.01016,9.01016,0,0,1,9,9v44A9.01015,9.01015,0,0,1,801.81836,318.08691Z" transform="translate(-276.18187 -133.91309)" fill="#00008b"/><circle cx="433.63626" cy="105.17383" r="20" fill="#00008b"/><circle cx="433.63626" cy="105.17383" r="12.18187" fill="#fff"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="647.636" height="632.174" data-name="Layer 1" viewBox="0 0 647.636 632.174"><path fill="#f2f2f2" d="M687.3279,276.08691H512.81813a15.01828,15.01828,0,0,0-15,15v387.85l-2,.61005-42.81006,13.11a8.00676,8.00676,0,0,1-9.98974-5.31L315.678,271.39691a8.00313,8.00313,0,0,1,5.31006-9.99l65.97022-20.2,191.25-58.54,65.96972-20.2a7.98927,7.98927,0,0,1,9.99024,5.3l32.5498,106.32Z" transform="translate(-276.18187 -133.91309)"/><path fill="#3f3d56" d="M725.408,274.08691l-39.23-128.14a16.99368,16.99368,0,0,0-21.23-11.28l-92.75,28.39L380.95827,221.60693l-92.75,28.4a17.0152,17.0152,0,0,0-11.28028,21.23l134.08008,437.93a17.02661,17.02661,0,0,0,16.26026,12.03,16.78926,16.78926,0,0,0,4.96972-.75l63.58008-19.46,2-.62v-2.09l-2,.61-64.16992,19.65a15.01489,15.01489,0,0,1-18.73-9.95l-134.06983-437.94a14.97935,14.97935,0,0,1,9.94971-18.73l92.75-28.4,191.24024-58.54,92.75-28.4a15.15551,15.15551,0,0,1,4.40966-.66,15.01461,15.01461,0,0,1,14.32032,10.61l39.0498,127.56.62012,2h2.08008Z" transform="translate(-276.18187 -133.91309)"/><path fill="#00008b" d="M398.86279,261.73389a9.0157,9.0157,0,0,1-8.61133-6.3667l-12.88037-42.07178a8.99884,8.99884,0,0,1,5.9712-11.24023l175.939-53.86377a9.00867,9.00867,0,0,1,11.24072,5.9707l12.88037,42.07227a9.01029,9.01029,0,0,1-5.9707,11.24072L401.49219,261.33887A8.976,8.976,0,0,1,398.86279,261.73389Z" transform="translate(-276.18187 -133.91309)"/><circle cx="190.154" cy="24.955" r="20" fill="#00008b"/><circle cx="190.154" cy="24.955" r="12.665" fill="#fff"/><path fill="#e6e6e6" d="M878.81836,716.08691h-338a8.50981,8.50981,0,0,1-8.5-8.5v-405a8.50951,8.50951,0,0,1,8.5-8.5h338a8.50982,8.50982,0,0,1,8.5,8.5v405A8.51013,8.51013,0,0,1,878.81836,716.08691Z" transform="translate(-276.18187 -133.91309)"/><path fill="#3f3d56" d="M723.31813,274.08691h-210.5a17.02411,17.02411,0,0,0-17,17v407.8l2-.61v-407.19a15.01828,15.01828,0,0,1,15-15H723.93825Zm183.5,0h-394a17.02411,17.02411,0,0,0-17,17v458a17.0241,17.0241,0,0,0,17,17h394a17.0241,17.0241,0,0,0,17-17v-458A17.02411,17.02411,0,0,0,906.81813,274.08691Zm15,475a15.01828,15.01828,0,0,1-15,15h-394a15.01828,15.01828,0,0,1-15-15v-458a15.01828,15.01828,0,0,1,15-15h394a15.01828,15.01828,0,0,1,15,15Z" transform="translate(-276.18187 -133.91309)"/><path fill="#00008b" d="M801.81836,318.08691h-184a9.01015,9.01015,0,0,1-9-9v-44a9.01016,9.01016,0,0,1,9-9h184a9.01016,9.01016,0,0,1,9,9v44A9.01015,9.01015,0,0,1,801.81836,318.08691Z" transform="translate(-276.18187 -133.91309)"/><circle cx="433.636" cy="105.174" r="20" fill="#00008b"/><circle cx="433.636" cy="105.174" r="12.182" fill="#fff"/></svg>
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
@ -1,90 +1,74 @@
|
||||
<script>
|
||||
import BlankState from '$components/blankstate.svelte';
|
||||
import ContextMenu from '$components/contextmenu.svelte';
|
||||
import connections from '$lib/stores/connections';
|
||||
import contextMenu from '$lib/stores/contextmenu';
|
||||
import environment from '$lib/stores/environment';
|
||||
import hosts from '$lib/stores/hosts';
|
||||
import applicationInited from '$lib/stores/inited';
|
||||
import About from '$organisms/about.svelte';
|
||||
import Connection from '$organisms/connection/index.svelte';
|
||||
import Settings from '$organisms/settings/index.svelte';
|
||||
import { EventsEmit, EventsOn } from '$wails/runtime';
|
||||
import { tick } from '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';
|
||||
|
||||
const activeHostKey = '';
|
||||
let activeDbKey = '';
|
||||
let activeCollKey = '';
|
||||
let settingsModalOpen = false;
|
||||
let aboutModalOpen = false;
|
||||
let connectionManager;
|
||||
let showWelcomeScreen = undefined;
|
||||
|
||||
$: host = hosts[activeHostKey];
|
||||
$: connection = $connections[activeHostKey];
|
||||
|
||||
hosts.subscribe(h => {
|
||||
if (h && (showWelcomeScreen === undefined)) {
|
||||
showWelcomeScreen = !Object.keys($hosts || {}).length;
|
||||
}
|
||||
applicationInited.defer(() => {
|
||||
hostTree.subscribe(hosts => {
|
||||
if (hostTree.hasBeenInited() && (showWelcomeScreen === undefined)) {
|
||||
showWelcomeScreen = !Object.keys(hosts || {}).length;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function createFirstHost() {
|
||||
showWelcomeScreen = false;
|
||||
await tick();
|
||||
connectionManager.createHost();
|
||||
hostTree.newHost();
|
||||
}
|
||||
|
||||
EventsOn('OpenPreferences', () => settingsModalOpen = true);
|
||||
EventsOn('OpenAboutModal', () => aboutModalOpen = true);
|
||||
</script>
|
||||
|
||||
<svelte:window on:contextmenu|preventDefault />
|
||||
|
||||
<div id="root" class="platform-{$environment?.platform}">
|
||||
<div class="titlebar"></div>
|
||||
<div id="root">
|
||||
<div class="titlebar">{$windowTitle}</div>
|
||||
|
||||
{#if $applicationInited && $hosts && (showWelcomeScreen !== undefined)}
|
||||
<main class:empty={showWelcomeScreen}>
|
||||
{#if $applicationInited && (showWelcomeScreen !== undefined)}
|
||||
<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 {activeHostKey} bind:activeCollKey bind:activeDbKey bind:this={connectionManager} />
|
||||
<Connection />
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
{#key $contextMenu}
|
||||
<ContextMenu {...$contextMenu} on:close={contextMenu.hide} />
|
||||
{/key}
|
||||
|
||||
<Settings bind:show={settingsModalOpen} />
|
||||
<About bind:show={aboutModalOpen} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.titlebar {
|
||||
height: 0;
|
||||
background-color: #00002a;
|
||||
height: var(--titlebar-height);
|
||||
--wails-draggable: drag;
|
||||
}
|
||||
#root.platform-darwin .titlebar {
|
||||
height: var(--darwin-titlebar-height);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
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;
|
||||
@ -94,12 +78,4 @@
|
||||
main > :global(.addressbar) {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
|
||||
.databaselist {
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.btn.create {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,14 +1,25 @@
|
||||
<script>
|
||||
import Icon from './icon.svelte';
|
||||
|
||||
export let title = '';
|
||||
export let label = 'No items';
|
||||
export let image = '/empty.svg';
|
||||
export let icon = '';
|
||||
export let pale = true;
|
||||
export let big = false;
|
||||
</script>
|
||||
|
||||
<div class="blankstate" class:pale class:big>
|
||||
<div class="content">
|
||||
<img src={image} alt="" />
|
||||
<p>{label}</p>
|
||||
{#if icon}
|
||||
<Icon name={icon} />
|
||||
{:else if image}
|
||||
<img src={image} alt="" />
|
||||
{/if}
|
||||
|
||||
<p class="title">{title}</p>
|
||||
<p class="label">{label}</p>
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
@ -27,14 +38,22 @@
|
||||
height: 150px;
|
||||
width: auto;
|
||||
}
|
||||
.content > :global(svg) {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 2.85rem 0;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
p.title {
|
||||
font-weight: 700;
|
||||
margin-bottom: -1.85rem;
|
||||
}
|
||||
|
||||
.blankstate :global(.btn) {
|
||||
.blankstate :global(.button) {
|
||||
font-size: 1.35rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
@ -48,7 +67,7 @@
|
||||
filter: grayscale(1);
|
||||
opacity: 0.4;
|
||||
}
|
||||
.pale p {
|
||||
.pale {
|
||||
color: #777;
|
||||
}
|
||||
</style>
|
||||
|
@ -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,105 +0,0 @@
|
||||
<script>
|
||||
import GridItems from './grid-items.svelte';
|
||||
|
||||
export let columns = [];
|
||||
export let items = [];
|
||||
export let key = 'id';
|
||||
export let activePath = [];
|
||||
export let striped = true;
|
||||
export let showHeaders = false;
|
||||
export let hideObjectIndicators = false;
|
||||
export let hideChildrenToggles = false;
|
||||
export let canSelect = true;
|
||||
export let canRemoveItems = false;
|
||||
export let inputsValid = false;
|
||||
// export let actions = [];
|
||||
</script>
|
||||
|
||||
<div class="grid">
|
||||
<!-- {#if actions?.length}
|
||||
<div class="actions">
|
||||
{#each actions as action}
|
||||
<button class="btn" on:click={action.fn} disabled={action.disabled}>
|
||||
{#if action.icon}<Icon name={action.icon} />{/if}
|
||||
{action.label || ''}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if} -->
|
||||
|
||||
<table>
|
||||
{#if showHeaders && columns.some(col => col.title)}
|
||||
<thead>
|
||||
<tr>
|
||||
{#if !hideChildrenToggles}
|
||||
<th class="has-toggle"></th>
|
||||
{/if}
|
||||
|
||||
<th class="has-icon"></th>
|
||||
|
||||
{#each columns as column}
|
||||
<th scope="col">{column.title || ''}</th>
|
||||
{/each}
|
||||
|
||||
{#if canRemoveItems}
|
||||
<th class="has-button"></th>
|
||||
{/if}
|
||||
</tr>
|
||||
</thead>
|
||||
{/if}
|
||||
|
||||
<tbody>
|
||||
<GridItems
|
||||
{items}
|
||||
{columns}
|
||||
{key}
|
||||
{striped}
|
||||
{canSelect}
|
||||
{canRemoveItems}
|
||||
{hideObjectIndicators}
|
||||
{hideChildrenToggles}
|
||||
bind:activePath
|
||||
bind:inputsValid
|
||||
on:select
|
||||
on:trigger
|
||||
on:removeItem
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.grid {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* .actions {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
.actions button {
|
||||
margin-right: 0.2rem;
|
||||
} */
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
table thead {
|
||||
border-bottom: 2px solid #ccc;
|
||||
}
|
||||
th {
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* tfoot button {
|
||||
margin-top: 0.5rem;
|
||||
} */
|
||||
</style>
|
@ -1,9 +1,9 @@
|
||||
<script>
|
||||
import { 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,12 @@
|
||||
return obj;
|
||||
}
|
||||
else if ((typeof obj === 'object') && (obj !== null)) {
|
||||
return Object.entries(obj).map(([ k, item ]) => ({ ...item, [key]: k }));
|
||||
return Object.entries(obj).map(([
|
||||
k,
|
||||
item,
|
||||
]) => {
|
||||
return { ...item, [key]: k };
|
||||
});
|
||||
}
|
||||
else {
|
||||
return obj;
|
||||
@ -62,18 +69,12 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
toggleChildren(itemKey, false);
|
||||
|
||||
if (activeKey !== itemKey) {
|
||||
activeKey = itemKey;
|
||||
if (level === 0) {
|
||||
activePath = [ itemKey ];
|
||||
}
|
||||
else {
|
||||
activePath = [ ...path, itemKey ];
|
||||
}
|
||||
dispatch('select', { level, itemKey, index });
|
||||
}
|
||||
activeKey = itemKey;
|
||||
activePath = [
|
||||
...path.slice(0, level),
|
||||
itemKey,
|
||||
];
|
||||
dispatch('select', { level, itemKey, index, path: activePath });
|
||||
}
|
||||
|
||||
function closeAll() {
|
||||
@ -89,7 +90,7 @@
|
||||
}
|
||||
|
||||
function doubleClick(itemKey, index) {
|
||||
// toggleChildren(itemKey, false);
|
||||
toggleChildren(itemKey, false);
|
||||
dispatch('trigger', { level, itemKey, index });
|
||||
childrenOpen[itemKey] = true;
|
||||
}
|
||||
@ -131,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 && !activePath[level + 1] && activePath.every(k => path.includes(k) || k === item[key]) && (activePath[level] === item[key])}
|
||||
class:selected
|
||||
class:striped
|
||||
>
|
||||
{#if !hideChildrenToggles}
|
||||
@ -150,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}
|
||||
|
||||
@ -162,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])}
|
||||
@ -173,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>
|
||||
@ -189,7 +208,10 @@
|
||||
{hideChildrenToggles}
|
||||
{canSelect}
|
||||
{canRemoveItems}
|
||||
path={[ ...path, item[key] ]}
|
||||
path={[
|
||||
...path,
|
||||
item[key],
|
||||
]}
|
||||
items={item.children}
|
||||
level={level + 1}
|
||||
bind:activePath
|
||||
@ -208,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;
|
||||
@ -237,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;
|
||||
}
|
||||
|
137
frontend/src/components/grid/grid.svelte
Normal file
@ -0,0 +1,137 @@
|
||||
<script>
|
||||
import { onDestroy } from 'svelte';
|
||||
import BlankState from '../blankstate.svelte';
|
||||
import GridItems from './grid-items.svelte';
|
||||
import Icon from '../icon.svelte';
|
||||
|
||||
export let columns = [];
|
||||
export let items = [];
|
||||
export let key = 'id';
|
||||
export let activePath = [];
|
||||
export let striped = true;
|
||||
export let showHeaders = false;
|
||||
export let hideObjectIndicators = false;
|
||||
export let hideChildrenToggles = false;
|
||||
export let canSelect = true;
|
||||
export let canRemoveItems = false;
|
||||
export let inputsValid = false;
|
||||
export let errorTitle = '';
|
||||
export let errorDescription = '';
|
||||
export let busy = false;
|
||||
|
||||
let copySucceeded = false;
|
||||
let timeout;
|
||||
|
||||
async function copyErrorDescription() {
|
||||
await navigator.clipboard.writeText(errorDescription);
|
||||
copySucceeded = true;
|
||||
timeout = setTimeout(() => copySucceeded = false, 1500);
|
||||
}
|
||||
|
||||
onDestroy(() => clearTimeout(timeout));
|
||||
</script>
|
||||
|
||||
<div class="grid">
|
||||
<!-- {#if actions?.length}
|
||||
<div class="actions">
|
||||
{#each actions as action}
|
||||
<button class="button" on:click={action.fn} disabled={action.disabled}>
|
||||
{#if action.icon}<Icon name={action.icon} />{/if}
|
||||
{action.label || ''}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if} -->
|
||||
|
||||
{#if busy}
|
||||
<BlankState label={(busy === true) ? 'Loading…' : busy} icon="loading" />
|
||||
{:else if errorTitle || errorDescription}
|
||||
<BlankState title={errorTitle} label={errorDescription} icon="!">
|
||||
<button class="button-small" on:click={copyErrorDescription}>
|
||||
<Icon name={copySucceeded ? 'check' : 'clipboard'} /> Copy error message
|
||||
</button>
|
||||
</BlankState>
|
||||
{:else}
|
||||
<table>
|
||||
{#if showHeaders && columns.some(col => col.title)}
|
||||
<thead>
|
||||
<tr>
|
||||
{#if !hideChildrenToggles}
|
||||
<th class="has-toggle" />
|
||||
{/if}
|
||||
|
||||
<th class="has-icon" />
|
||||
|
||||
{#each columns as column}
|
||||
<th scope="col">{column.title || ''}</th>
|
||||
{/each}
|
||||
|
||||
{#if canRemoveItems}
|
||||
<th class="has-button" />
|
||||
{/if}
|
||||
</tr>
|
||||
</thead>
|
||||
{/if}
|
||||
|
||||
<tbody>
|
||||
<GridItems
|
||||
{items}
|
||||
{columns}
|
||||
{key}
|
||||
{striped}
|
||||
{canSelect}
|
||||
{canRemoveItems}
|
||||
{hideObjectIndicators}
|
||||
{hideChildrenToggles}
|
||||
bind:activePath
|
||||
bind:inputsValid
|
||||
on:select
|
||||
on:trigger
|
||||
on:removeItem
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.grid {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
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) {
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* tfoot button {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.actions {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
.actions button {
|
||||
margin-right: 0.2rem;
|
||||
} */
|
||||
</style>
|
@ -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';
|
||||
|
||||
@ -8,17 +8,20 @@
|
||||
export let activePath = [];
|
||||
export let hideObjectIndicators = false;
|
||||
export let getRootMenu = () => undefined;
|
||||
|
||||
const columns = [
|
||||
{ key: 'key', label: 'Key' },
|
||||
{ key: 'value', label: 'Value' },
|
||||
{ key: 'type', label: 'Type' },
|
||||
];
|
||||
export let errorTitle = '';
|
||||
export let errorDescription = '';
|
||||
export let busy = false;
|
||||
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)) {
|
||||
@ -116,4 +119,7 @@
|
||||
{columns}
|
||||
{items}
|
||||
{hideObjectIndicators}
|
||||
{errorTitle}
|
||||
{errorDescription}
|
||||
{busy}
|
||||
/>
|
@ -1,97 +1,55 @@
|
||||
<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;
|
||||
}
|
||||
:global(.blankstate .button) svg {
|
||||
height: 1.25em;
|
||||
vertical-align: -3px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
{#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>
|
||||
{/if}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class:spinning={spin}
|
||||
style:transform="rotate({rotation}deg)"
|
||||
>
|
||||
{@html icons[name] || ''}
|
||||
</svg>
|
||||
|
@ -3,16 +3,18 @@
|
||||
</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 = false;
|
||||
export let show = true;
|
||||
export let title = undefined;
|
||||
export let contentPadding = true;
|
||||
export let width = '80vw';
|
||||
export let overflow = true;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const level = numberOfModalsOpen + 1;
|
||||
let isNew = true;
|
||||
|
||||
@ -29,20 +31,25 @@
|
||||
function keydown(event) {
|
||||
if ((event.key === 'Escape') && (level === numberOfModalsOpen)) {
|
||||
event.preventDefault();
|
||||
show = false;
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
show = false;
|
||||
dispatch('close');
|
||||
}
|
||||
</script>
|
||||
|
||||
<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={() => show = false} title="close" type="button">
|
||||
<button class="button close" on:click={close} title="close" type="button">
|
||||
<Icon name="x" />
|
||||
</button>
|
||||
</header>
|
||||
@ -68,24 +75,21 @@
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
margin: 0;
|
||||
padding-top: 50px;
|
||||
}
|
||||
:global(#root.platform-darwin) .outer {
|
||||
margin-top: var(--darwin-titlebar-height);
|
||||
padding: 2rem;
|
||||
--wails-draggable: drag;
|
||||
}
|
||||
|
||||
.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;
|
||||
flex-flow: column;
|
||||
cursor: auto;
|
||||
overflow: hidden;
|
||||
--wails-draggable: nodrag;
|
||||
}
|
||||
.inner > :global(*:first-child) {
|
||||
margin-top: 0;
|
||||
|
@ -1,12 +1,13 @@
|
||||
<script>
|
||||
import { jsonLooseParse, 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 { EJSON } from 'bson';
|
||||
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,27 +1,61 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
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 compact = true;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const activeIndicatorLeft = tweened(0, { easing: cubicOut, duration: 400 });
|
||||
const activeIndicatorRight = tweened(0, { easing: cubicOut, duration: 400 });
|
||||
const liElements = {};
|
||||
let navEl;
|
||||
|
||||
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(() => {
|
||||
if (selectedKey) {
|
||||
moveActiveIndicator(liElements[selectedKey]);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<nav class="tabs">
|
||||
<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}
|
||||
{tab.title}
|
||||
<span class="label">{tab.title}</span>
|
||||
</button>
|
||||
|
||||
{#if tab.closable}
|
||||
<button class="button-small" on:click={() => dispatch('closeTab', tab.key)}>
|
||||
<Icon name="x" />
|
||||
@ -31,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-grow: 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 {
|
||||
@ -83,15 +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;
|
||||
}
|
||||
|
||||
.button-small {
|
||||
button.tab .label {
|
||||
margin-top: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
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,25 +1,27 @@
|
||||
<script>
|
||||
import Modal from '$components/modal.svelte';
|
||||
import alink from '$lib/actions/alink';
|
||||
|
||||
export let show = true;
|
||||
import alink from '$lib/actions/alink.js';
|
||||
import environment from '$lib/stores/environment.js';
|
||||
</script>
|
||||
|
||||
<Modal bind:show width="400px" title=" ">
|
||||
<Modal width="400px" title=" " on:close>
|
||||
<div class="brand">
|
||||
<img src="/logo.png" alt="Rolens logo" />
|
||||
<div>
|
||||
<div class="title">Rolens</div>
|
||||
<div class="description">Intuitive MongoDB <br/> administration tool</div>
|
||||
<div class="title">
|
||||
Rolens
|
||||
<span class="version">{$environment.version}</span>
|
||||
</div>
|
||||
<div class="description">Intuitive MongoDB <br /> administration tool</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="info">
|
||||
<p class="copy">© Romein van Buren, 2023.</p>
|
||||
<p class="copy">© Romein van Buren, 2022-2025.</p>
|
||||
<p>
|
||||
<a href="" use:alink>Documentation</a> |
|
||||
<a href="https://garraflavatra.github.io/rolens/" use:alink>Documentation</a> |
|
||||
<a href="https://github.com/garraflavatra/rolens" use:alink>GitHub</a> |
|
||||
<a href="https://github.com/garraflavatra/rolens/issues/new" use:alink>Report a bug</a> |
|
||||
<a href="https://github.com/garraflavatra/rolens/blob/main/LICENSE" use:alink>License</a>
|
||||
@ -28,6 +30,10 @@
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.brand, .info {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -38,13 +44,17 @@
|
||||
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%;
|
||||
font-weight: 300;
|
||||
opacity: 0.65;
|
||||
}
|
||||
.brand .description {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.6rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
@ -53,7 +63,6 @@
|
||||
|
||||
.info {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.25rem;
|
||||
margin: 0 1rem 1rem;
|
||||
text-align: center;
|
||||
}
|
44
frontend/src/dialogs/input.svelte
Normal file
@ -0,0 +1,44 @@
|
||||
<script>
|
||||
import Modal from '$components/modal.svelte';
|
||||
import { createEventDispatcher, onMount, tick } from 'svelte';
|
||||
|
||||
export let title = '';
|
||||
export let description = '';
|
||||
export let value = '';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let input;
|
||||
|
||||
function submit() {
|
||||
dispatch('submit', { value });
|
||||
}
|
||||
|
||||
function close() {
|
||||
dispatch('close');
|
||||
}
|
||||
|
||||
onMount(() => tick().then(() => input.select()));
|
||||
</script>
|
||||
|
||||
<Modal {title} on:close width="350px">
|
||||
{#if description}
|
||||
<p>{description}</p>
|
||||
{/if}
|
||||
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<label class="field">
|
||||
<input type="text" bind:value bind:this={input} spellcheck="false" />
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<button on:click={submit} class="button">OK</button>
|
||||
<button on:click={close} class="button secondary">Cancel</button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
p {
|
||||
line-height: 1.25;
|
||||
}
|
||||
</style>
|
@ -1,13 +1,11 @@
|
||||
<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';
|
||||
|
||||
export let show = false;
|
||||
import input from '$lib/actions/input.js';
|
||||
import settings from '$lib/stores/settings.js';
|
||||
</script>
|
||||
|
||||
<Modal title="Preferences" bind:show>
|
||||
<Modal title="Preferences" on:close>
|
||||
<div class="prefs">
|
||||
<label for="defaultLimit">Initial number of items to retrieve using one query (limit):</label>
|
||||
<label class="field">
|
||||
@ -17,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>
|
||||
@ -27,7 +31,7 @@
|
||||
</span>
|
||||
|
||||
<label for="defaultExportDirectory">Default export directory</label>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control - input is in DirectoryChooser -->
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label class="field">
|
||||
<DirectoryChooser id="defaultExportDirectory" bind:value={$settings.defaultExportDirectory} />
|
||||
</label>
|
@ -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 => {
|
||||
|
45
frontend/src/lib/dialogs.js
Normal file
@ -0,0 +1,45 @@
|
||||
import { AskConfirmation } from '$wails/go/app/App.js';
|
||||
import InputDialog from '../dialogs/input.svelte';
|
||||
|
||||
function newDialog(dialogComponent, data = {}) {
|
||||
const outlet = document.createElement('div');
|
||||
outlet.className = 'dialogoutlet';
|
||||
document.getElementById('dialogoutlets').appendChild(outlet);
|
||||
|
||||
const instance = new dialogComponent({
|
||||
target: outlet,
|
||||
intro: true,
|
||||
props: data,
|
||||
});
|
||||
|
||||
instance.$close = function() {
|
||||
setTimeout(() => {
|
||||
instance.$destroy();
|
||||
outlet.remove();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
instance.$on('close', instance.$close);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
function enterText(title = '', description = '', value = '') {
|
||||
const instance = newDialog(InputDialog, { title, description, value });
|
||||
|
||||
return new Promise(resolve => {
|
||||
instance.$on('submit', event => {
|
||||
instance.$close();
|
||||
resolve(event.detail.value);
|
||||
});
|
||||
instance.$on('close', () => resolve(undefined));
|
||||
});
|
||||
}
|
||||
|
||||
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',
|
||||
@ -20,7 +20,7 @@
|
||||
<label for="collationLocale">Locale</label>
|
||||
<div class="field">
|
||||
<select id="collationLocale" bind:value={collation.locale}>
|
||||
{#each Object.entries(locales) as [value, name]}
|
||||
{#each Object.entries(locales) as [ value, name ]}
|
||||
<option {value}>({value}) {name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
@ -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"
|
||||
}
|
@ -58,6 +58,18 @@ export function setValue(object, path, value) {
|
||||
}
|
||||
|
||||
export function deepClone(obj) {
|
||||
// Room for improvement below
|
||||
// @todo: Room for improvement below
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
export function pathsAreEqual(x, y) {
|
||||
const lengthOfLongest = (x.length >= y.length) ? x.length : y.length;
|
||||
|
||||
for (let i = 0; i < lengthOfLongest; i++) {
|
||||
if (x[i] !== y[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -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,5 +0,0 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const connections = writable({});
|
||||
|
||||
export default connections;
|
@ -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,11 +0,0 @@
|
||||
import { Hosts } from "$wails/go/app/App";
|
||||
import { writable } from "svelte/store";
|
||||
import applicationInited from "./inited";
|
||||
|
||||
const { set, subscribe } = writable();
|
||||
|
||||
const update = async () => set(await Hosts());
|
||||
applicationInited.defer(update);
|
||||
|
||||
const hosts = { update, subscribe };
|
||||
export default hosts;
|
395
frontend/src/lib/stores/hosttree.js
Normal file
@ -0,0 +1,395 @@
|
||||
import dialogs from '$lib/dialogs.js';
|
||||
import { get, writable } from 'svelte/store';
|
||||
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,
|
||||
OpenDatabase,
|
||||
PerformDump,
|
||||
PerformFindExport,
|
||||
RemoveHost,
|
||||
RenameCollection,
|
||||
TruncateCollection
|
||||
} from '$wails/go/app/App.js';
|
||||
|
||||
const { set, subscribe } = writable({});
|
||||
const getValue = () => get({ subscribe });
|
||||
let hostTreeInited = false;
|
||||
|
||||
async function refresh() {
|
||||
const hosts = await Hosts();
|
||||
const hostTree = getValue();
|
||||
|
||||
for (const [
|
||||
hostKey,
|
||||
hostDetails,
|
||||
] of Object.entries(hosts)) {
|
||||
|
||||
hostTree[hostKey] = hostTree[hostKey] || {};
|
||||
const host = hostTree[hostKey];
|
||||
host.key = hostKey;
|
||||
host.name = hostDetails.name;
|
||||
host.uri = hostDetails.uri;
|
||||
|
||||
host.open = async function() {
|
||||
host.loading = true;
|
||||
set(hostTree);
|
||||
|
||||
const {
|
||||
databases: dbNames,
|
||||
status,
|
||||
statusError,
|
||||
systemInfo,
|
||||
systemInfoError,
|
||||
} = await OpenConnection(hostKey);
|
||||
|
||||
host.status = status;
|
||||
host.statusError = statusError;
|
||||
host.systemInfo = systemInfo;
|
||||
host.systemInfoError = systemInfoError;
|
||||
host.databases = host.databases || {};
|
||||
|
||||
if (!dbNames) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const dbKey of dbNames.sort((a, b) => a.localeCompare(b))) {
|
||||
host.databases[dbKey] = host.databases[dbKey] || {};
|
||||
}
|
||||
|
||||
for (const [
|
||||
dbKey,
|
||||
database,
|
||||
] of Object.entries(host.databases)) {
|
||||
if (!database.new && !dbNames.includes(dbKey)) {
|
||||
delete host.databases[dbKey];
|
||||
continue;
|
||||
}
|
||||
|
||||
database.key = dbKey;
|
||||
database.hostKey = hostKey;
|
||||
database.collections = database.collections || {};
|
||||
|
||||
delete database.new;
|
||||
|
||||
database.open = async function() {
|
||||
database.loading = true;
|
||||
set(hostTree);
|
||||
|
||||
const { collections: collNames, stats, statsError } = await OpenDatabase(hostKey, dbKey);
|
||||
database.stats = stats;
|
||||
database.statsError = statsError;
|
||||
|
||||
if (!collNames) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const collKey of collNames.sort((a, b) => a.localeCompare(b))) {
|
||||
database.collections[collKey] = database.collections[collKey] || {};
|
||||
}
|
||||
|
||||
for (const [
|
||||
collKey,
|
||||
collection,
|
||||
] of Object.entries(database.collections)) {
|
||||
if (!collection.new && !collNames.includes(collKey)) {
|
||||
delete database.collections[collKey];
|
||||
continue;
|
||||
}
|
||||
|
||||
collection.key = collKey;
|
||||
collection.dbKey = dbKey;
|
||||
collection.hostKey = hostKey;
|
||||
collection.viewKey = 'list';
|
||||
collection.indexes = collection.indexes || [];
|
||||
|
||||
delete collection.new;
|
||||
|
||||
collection.open = async function() {
|
||||
const { stats, statsError } = await OpenCollection(hostKey, dbKey, collKey);
|
||||
|
||||
collection.stats = stats;
|
||||
collection.statsError = statsError;
|
||||
|
||||
await refresh();
|
||||
};
|
||||
|
||||
collection.rename = async function() {
|
||||
const newCollKey = await dialogs.enterText('Rename collection', `Enter a new name for collection ${collKey}.`, collKey);
|
||||
if (newCollKey && (newCollKey !== collKey)) {
|
||||
const ok = await RenameCollection(hostKey, dbKey, collKey, newCollKey);
|
||||
await database.open();
|
||||
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)
|
||||
);
|
||||
|
||||
if (success) {
|
||||
dialog.$close();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
collection.dump = function() {
|
||||
const dialog = dialogs.new(DumpDialog, { info: {
|
||||
hostKey,
|
||||
dbKey,
|
||||
collKeys: [ collKey ],
|
||||
} });
|
||||
|
||||
return new Promise(resolve => {
|
||||
dialog.$on('dump', async event => {
|
||||
const success = await PerformDump(JSON.stringify(event.detail.info));
|
||||
if (success) {
|
||||
dialog.$close();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
collection.truncate = async function() {
|
||||
await TruncateCollection(hostKey, dbKey, collKey);
|
||||
await refresh();
|
||||
};
|
||||
|
||||
collection.drop = async function() {
|
||||
const success = await DropCollection(hostKey, dbKey, collKey);
|
||||
if (success) {
|
||||
delete database.collections[collKey];
|
||||
await refresh();
|
||||
}
|
||||
};
|
||||
|
||||
collection.getIndexes = async function() {
|
||||
collection.indexes = [];
|
||||
const { indexes, error } = await GetIndexes(hostKey, dbKey, collKey);
|
||||
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
for (const indexDetails of indexes) {
|
||||
const index = {
|
||||
name: indexDetails.name,
|
||||
background: indexDetails.background || false,
|
||||
unique: indexDetails.unique || false,
|
||||
sparse: indexDetails.sparse || false,
|
||||
model: indexDetails.model,
|
||||
};
|
||||
|
||||
index.drop = async function() {
|
||||
const hasBeenDropped = await DropIndex(hostKey, dbKey, collKey, index.name);
|
||||
return hasBeenDropped;
|
||||
};
|
||||
|
||||
collection.indexes.push(index);
|
||||
}
|
||||
};
|
||||
|
||||
collection.getIndexByName = function(indesName) {
|
||||
return collection.indexes.find(idx => idx.name = indesName);
|
||||
};
|
||||
|
||||
collection.newIndex = function() {
|
||||
const dialog = dialogs.new(IndexDetailDialog, { collection });
|
||||
|
||||
return new Promise(resolve => {
|
||||
dialog.$on('create', async event => {
|
||||
const newIndexName = await CreateIndex(
|
||||
collection.hostKey,
|
||||
collection.dbKey,
|
||||
collection.key,
|
||||
JSON.stringify(event.detail.index)
|
||||
);
|
||||
|
||||
if (newIndexName) {
|
||||
dialog.$close();
|
||||
}
|
||||
|
||||
resolve(newIndexName);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
collection.openQueryChooser = function(queryToSave = undefined) {
|
||||
const dialog = dialogs.new(QueryChooserDialog, { collection, queryToSave });
|
||||
|
||||
return new Promise(resolve => {
|
||||
dialog.$on('select', async event => {
|
||||
dialog.$close();
|
||||
resolve(event.detail.query);
|
||||
});
|
||||
|
||||
dialog.$on('create', async event => {
|
||||
const ok = await queries.create(event.detail.query);
|
||||
if (ok) {
|
||||
dialog.$close();
|
||||
resolve(event.detail.query);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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() {
|
||||
const dialog = dialogs.new(DumpDialog, { info: { hostKey, dbKey } });
|
||||
|
||||
return new Promise(resolve => {
|
||||
dialog.$on('dump', async event => {
|
||||
const success = await PerformDump(JSON.stringify(event.detail.info));
|
||||
if (success) {
|
||||
dialog.$close();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
database.drop = async function() {
|
||||
const success = await DropDatabase(hostKey, dbKey);
|
||||
if (success) {
|
||||
delete host.databases[dbKey];
|
||||
await refresh();
|
||||
}
|
||||
};
|
||||
|
||||
database.newCollection = async function() {
|
||||
const name = await dialogs.enterText('Create a collection', 'Note: collections in MongoDB do not exist until they have at least one item. Your new collection will not persist on the server; fill it to have it created.', '');
|
||||
if (name) {
|
||||
database.collections[name] = { key: name, new: true };
|
||||
await database.open();
|
||||
}
|
||||
};
|
||||
|
||||
database.executeShellScript = async function(script) {
|
||||
const result = await ExecuteShellScript(hostKey, dbKey, '', script);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
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() {
|
||||
await RemoveHost(hostKey);
|
||||
await refresh();
|
||||
};
|
||||
}
|
||||
|
||||
hostTreeInited = true;
|
||||
set(hostTree);
|
||||
}
|
||||
|
||||
function newHost() {
|
||||
const dialog = dialogs.new(HostDetailDialog, { hostKey: '' });
|
||||
return new Promise(resolve => {
|
||||
dialog.$on('close', () => {
|
||||
refresh().then(resolve);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
applicationInited.defer(refresh);
|
||||
|
||||
const hostTree = {
|
||||
refresh,
|
||||
subscribe,
|
||||
get: getValue,
|
||||
newHost,
|
||||
hasBeenInited: () => hostTreeInited,
|
||||
};
|
||||
|
||||
export default hostTree;
|
@ -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;
|
||||
|
||||
@ -10,7 +10,7 @@ const defer = listener => {
|
||||
listener();
|
||||
}
|
||||
else {
|
||||
listeners.push(listener)
|
||||
listeners.push(listener);
|
||||
}
|
||||
};
|
||||
|
||||
@ -18,16 +18,12 @@ const { subscribe } = derived([ environment, applicationSettings ], ([ env, sett
|
||||
if (alreadyInited) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (env && settings) {
|
||||
set(true);
|
||||
alreadyInited = true;
|
||||
|
||||
// Remove loading spinner.
|
||||
document.getElementById('app-loading')?.remove();
|
||||
|
||||
// Call hooks
|
||||
listeners.forEach(l => l());
|
||||
else if (env && settings) {
|
||||
Promise.all(listeners.map(l => l())).then(() => {
|
||||
set(true);
|
||||
alreadyInited = true;
|
||||
document.getElementById('app-loading')?.remove();
|
||||
});
|
||||
}
|
||||
}, 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,4 +1,6 @@
|
||||
import { UpdateViewStore, Views } from '$wails/go/app/App';
|
||||
import dialogs from '$lib/dialogs.js';
|
||||
import ViewConfigDialog from '$organisms/connection/collection/dialogs/viewconfig.svelte';
|
||||
import { UpdateViewStore, Views } from '$wails/go/app/App.js';
|
||||
import { get, writable } from 'svelte/store';
|
||||
|
||||
const { set, subscribe } = writable({});
|
||||
@ -23,6 +25,11 @@ function forCollection(hostKey, dbKey, collKey) {
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
|
||||
function openConfig(collection, firstItem = {}) {
|
||||
const dialog = dialogs.new(ViewConfigDialog, { collection, firstItem });
|
||||
return dialog;
|
||||
}
|
||||
|
||||
reload();
|
||||
subscribe(newViewStore => {
|
||||
if (skipUpdate) {
|
||||
@ -32,5 +39,5 @@ subscribe(newViewStore => {
|
||||
UpdateViewStore(JSON.stringify(newViewStore));
|
||||
});
|
||||
|
||||
const views = { reload, set, subscribe, forCollection };
|
||||
const views = { reload, set, subscribe, forCollection, openConfig };
|
||||
export default views;
|
||||
|
14
frontend/src/lib/stores/windowtitle.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { WindowSetTitle } from '$wails/runtime/runtime.js';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const { set, subscribe } = writable('Rolens');
|
||||
|
||||
subscribe(newTitle => WindowSetTitle(newTitle));
|
||||
|
||||
const windowTitle = {
|
||||
set,
|
||||
setSegments: (...segments) => set(segments.map(s => s.trim()).join(' — ')),
|
||||
subscribe,
|
||||
};
|
||||
|
||||
export default windowTitle;
|
@ -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/editors/objecteditor.svelte';
|
||||
import { aggregationStageDocumentationURL, aggregationStages } from '$lib/mongo/index.js';
|
||||
import Collation from '$lib/mongo/collation.svelte';
|
||||
import ObjectEditor from '$components/objecteditor.svelte';
|
||||
import { aggregationStageDocumentationURL, aggregationStages } from '$lib/mongo';
|
||||
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;
|
||||
@ -31,7 +31,9 @@
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const pipeline = stages.map(stage => ({ [stage.type]: jsonLooseParse(stage.data) }));
|
||||
const pipeline = stages.map(stage => {
|
||||
return { [stage.type]: jsonLooseParse(stage.data) };
|
||||
});
|
||||
await Aggregate(
|
||||
collection.hostKey,
|
||||
collection.dbKey,
|
||||
@ -59,26 +61,27 @@
|
||||
<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} on:inited={e => {
|
||||
e.detail.editor.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: e.detail.editor.state.doc.length,
|
||||
insert: '{\n\t\n}',
|
||||
},
|
||||
selection: {
|
||||
anchor: 3,
|
||||
},
|
||||
});
|
||||
e.detail.editor.focus();
|
||||
}} />
|
||||
<ObjectEditor
|
||||
bind:text={stage.data}
|
||||
on:inited={e => {
|
||||
e.detail.editor.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: e.detail.editor.state.doc.length,
|
||||
insert: '{\n\t\n}',
|
||||
},
|
||||
selection: {
|
||||
anchor: 3,
|
||||
},
|
||||
});
|
||||
}} />
|
||||
</label>
|
||||
</Details>
|
||||
{/each}
|
||||
@ -90,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,74 +0,0 @@
|
||||
<script>
|
||||
import Icon from '$components/icon.svelte';
|
||||
import Modal from '$components/modal.svelte';
|
||||
import views from '$lib/stores/views';
|
||||
import { PerformFindExport } from '$wails/go/app/App';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let info;
|
||||
export let collection;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let viewKey = collection.viewKey;
|
||||
$: viewKey = collection.viewKey;
|
||||
$: if (info) {
|
||||
info.viewKey = viewKey;
|
||||
}
|
||||
|
||||
async function performExport() {
|
||||
info.view = $views[viewKey];
|
||||
const success = await PerformFindExport(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(info));
|
||||
|
||||
if (success) {
|
||||
info = undefined;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:show={info} title="Export results" width="450px">
|
||||
<form on:submit|preventDefault={performExport}>
|
||||
<label class="field">
|
||||
<span class="label">Export</span>
|
||||
<select bind:value={info.contents}>
|
||||
<option value="all">all records</option>
|
||||
<option value="query">all records matching query</option>
|
||||
<option value="querylimitskip">all records matching query, considering limit and skip</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span class="label">Format</span>
|
||||
<select bind:value={info.format}>
|
||||
<option value="jsonarray">JSON array</option>
|
||||
<option value="ndjson">Newline delimited JSON</option>
|
||||
<option value="csv">CSV</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span class="label">View to use</span>
|
||||
<select bind:value={viewKey}>
|
||||
{#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">
|
||||
<Icon name="cog" />
|
||||
</button>
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<button class="btn" on:click={performExport}>
|
||||
<Icon name="play" /> Start export
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
@ -1,9 +1,9 @@
|
||||
<script>
|
||||
import FormInput from '$components/forminput.svelte';
|
||||
import Icon from '$components/icon.svelte';
|
||||
import { inputTypes } from '$lib/mongo';
|
||||
import { resolveKeypath, setValue } from '$lib/objects';
|
||||
import FormInput from '$components/editors/forminput.svelte';
|
||||
import Hint from '$components/hint.svelte';
|
||||
import Icon from '$components/icon.svelte';
|
||||
import { inputTypes } from '$lib/mongo/index.js';
|
||||
import { resolveKeypath, setValue } from '$lib/objects.js';
|
||||
|
||||
export let item = {};
|
||||
export let view = {};
|
||||
@ -38,7 +38,7 @@
|
||||
|
||||
{#if item && view}
|
||||
{#each view?.columns?.filter(c => inputTypes.includes(c.inputType)) || [] as column, index}
|
||||
<!-- svelte-ignore a11y-label-has-associated-control because FormInput contains one -->
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label class="column">
|
||||
<div class="label">
|
||||
<Icon name={iconMap[column.inputType]} />
|
||||
@ -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>
|
@ -0,0 +1,69 @@
|
||||
<script>
|
||||
import Icon from '$components/icon.svelte';
|
||||
import Modal from '$components/modal.svelte';
|
||||
import views from '$lib/stores/views.js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let collection;
|
||||
export let query = {};
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const exportInfo = { ...query, viewKey: collection.viewKey };
|
||||
|
||||
function submit() {
|
||||
exportInfo.view = $views[exportInfo.viewKey];
|
||||
dispatch('export', { exportInfo });
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal title="Export results" width="500px" on:close>
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<label class="field">
|
||||
<span class="label">Export</span>
|
||||
<select bind:value={exportInfo.contents}>
|
||||
<option value="all">all records</option>
|
||||
<option value="query" disabled={!query}>all records matching query</option>
|
||||
<option value="querylimitskip" disabled={!query}>all records matching query, considering limit and skip</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span class="label">Format</span>
|
||||
<select bind:value={exportInfo.format}>
|
||||
<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 },
|
||||
]}
|
||||
<option value={key}>{name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<button class="button" type="button" on:click={() => dispatch('openViewConfig')} title="Edit view">
|
||||
<Icon name="cog" />
|
||||
</button>
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<button class="button" on:click={submit}>
|
||||
<Icon name="play" /> Start export
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
@ -1,15 +1,13 @@
|
||||
<script>
|
||||
import Icon from '$components/icon.svelte';
|
||||
import Modal from '$components/modal.svelte';
|
||||
import input from '$lib/actions/input';
|
||||
import { CreateIndex } from '$wails/go/app/App';
|
||||
import input from '$lib/actions/input.js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let collection = {};
|
||||
export let creatingNewIndex = false;
|
||||
export let collection;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let index = { model: [] };
|
||||
const index = { model: [] };
|
||||
|
||||
function addRule() {
|
||||
index.model = [ ...index.model, {} ];
|
||||
@ -21,20 +19,20 @@
|
||||
}
|
||||
|
||||
async function create() {
|
||||
const newIndexName = await CreateIndex(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(index));
|
||||
if (newIndexName) {
|
||||
creatingNewIndex = false;
|
||||
index = { model: [] };
|
||||
dispatch('reload');
|
||||
}
|
||||
dispatch('create', { index });
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal title="Create new index {collection ? `on collection ${collection.key}` : ''}" bind:show={creatingNewIndex}>
|
||||
<Modal title="Create new index on {collection.key}" on:close>
|
||||
<form on:submit|preventDefault={create}>
|
||||
<label class="field name">
|
||||
<span class="label">Name</span>
|
||||
<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">
|
||||
@ -69,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>
|
||||
@ -78,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>
|
||||
@ -89,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,16 +1,15 @@
|
||||
<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 hosts from '$lib/stores/hosts';
|
||||
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;
|
||||
export let collection = {};
|
||||
export let show = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let gridSelectedPath = [];
|
||||
@ -22,23 +21,16 @@
|
||||
queryToSave.dbKey = collection.dbKey;
|
||||
queryToSave.collKey = collection.key;
|
||||
|
||||
const newId = queries.create(queryToSave);
|
||||
|
||||
if (newId) {
|
||||
dispatch('created', newId);
|
||||
queryToSave = undefined;
|
||||
selectedKey = newId;
|
||||
select();
|
||||
}
|
||||
dispatch('create', { query: queryToSave });
|
||||
selectedKey = queryToSave.name;
|
||||
}
|
||||
else {
|
||||
select();
|
||||
selectActive();
|
||||
}
|
||||
}
|
||||
|
||||
function select() {
|
||||
dispatch('select', selectedKey);
|
||||
show = false;
|
||||
function selectActive() {
|
||||
dispatch('select', { query: $queries[selectedKey] });
|
||||
}
|
||||
|
||||
function gridSelect(event) {
|
||||
@ -53,7 +45,7 @@
|
||||
|
||||
function gridTrigger(event) {
|
||||
gridSelect(event);
|
||||
select();
|
||||
selectActive();
|
||||
}
|
||||
|
||||
async function gridRemove(event) {
|
||||
@ -71,7 +63,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:show title={queryToSave ? 'Save query' : 'Load query'} width="500px">
|
||||
<Modal title={queryToSave ? 'Save query' : 'Load query'} width="500px" on:close>
|
||||
<form on:submit|preventDefault={submit}>
|
||||
{#if queryToSave}
|
||||
<label class="field queryname">
|
||||
@ -79,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}
|
||||
|
||||
@ -88,7 +80,11 @@
|
||||
columns={[ { key: 'n', title: 'Query name' }, { key: 'h', title: 'Host' }, { key: 'ns', title: 'Namespace' } ]}
|
||||
key="n"
|
||||
items={Object.entries($queries).reduce((object, [ name, query ]) => {
|
||||
object[query.name] = { n: name, h: $hosts[query.hostKey]?.name || '?', ns: `${query.dbKey}.${query.collKey}` };
|
||||
object[query.name] = {
|
||||
n: name,
|
||||
h: $hostTree[query.hostKey]?.name || '?',
|
||||
ns: `${query.dbKey}.${query.collKey}`,
|
||||
};
|
||||
return object;
|
||||
}, {})}
|
||||
showHeaders={true}
|
||||
@ -110,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}
|
||||
@ -135,7 +131,7 @@
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.btn + :global(.hint) {
|
||||
.button + :global(.hint) {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
@ -2,18 +2,23 @@
|
||||
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 show = false;
|
||||
export let activeViewKey = 'list';
|
||||
export let firstItem = {};
|
||||
|
||||
$: tabs = Object.entries(views.forCollection(collection.hostKey, collection.dbKey, collection.key))
|
||||
.sort((a, b) => sortTabKeys(a[0], b[0]))
|
||||
.map(([ key, v ]) => ({ key, title: v.name, closable: key !== 'list' }));
|
||||
let tabs = [];
|
||||
$: $views && refresh();
|
||||
|
||||
function refresh() {
|
||||
tabs = Object.entries(views.forCollection(collection.hostKey, collection.dbKey, collection.key))
|
||||
.sort((a, b) => sortTabKeys(a[0], b[0]))
|
||||
.map(([ key, v ]) => {
|
||||
return { key, title: v.name, closable: key !== 'list' };
|
||||
});
|
||||
}
|
||||
|
||||
function sortTabKeys(a, b) {
|
||||
if (a === 'list') {
|
||||
@ -37,29 +42,29 @@
|
||||
type: 'table',
|
||||
columns: [ { key: '_id', showInTable: true, inputType: 'objectid', mandatory: true } ],
|
||||
};
|
||||
activeViewKey = newViewKey;
|
||||
collection.viewKey = newViewKey;
|
||||
}
|
||||
|
||||
function removeView(viewKey) {
|
||||
const keys = Object.keys($views).sort(sortTabKeys);
|
||||
const oldIndex = keys.indexOf(viewKey);
|
||||
const newKey = keys[oldIndex - 1];
|
||||
activeViewKey = newKey;
|
||||
collection.viewKey = newKey;
|
||||
delete $views[viewKey];
|
||||
$views = $views;
|
||||
}
|
||||
|
||||
function addColumn(before) {
|
||||
if (typeof before === 'number') {
|
||||
$views[activeViewKey].columns = [
|
||||
...$views[activeViewKey].columns.slice(0, before),
|
||||
$views[collection.viewKey].columns = [
|
||||
...$views[collection.viewKey].columns.slice(0, before),
|
||||
{ showInTable: true, inputType: 'none' },
|
||||
...$views[activeViewKey].columns.slice(before),
|
||||
...$views[collection.viewKey].columns.slice(before),
|
||||
];
|
||||
}
|
||||
else {
|
||||
$views[activeViewKey].columns = [
|
||||
...$views[activeViewKey].columns,
|
||||
$views[collection.viewKey].columns = [
|
||||
...$views[collection.viewKey].columns,
|
||||
{ showInTable: true, inputType: 'none' },
|
||||
];
|
||||
}
|
||||
@ -70,65 +75,76 @@
|
||||
return;
|
||||
}
|
||||
|
||||
$views[activeViewKey].columns = Object.keys(firstItem).sort().map(key => ({
|
||||
key,
|
||||
showInTable: true,
|
||||
inputType: 'none',
|
||||
}));
|
||||
$views[collection.viewKey].columns = Object.keys(firstItem).sort().map(key => {
|
||||
return {
|
||||
key,
|
||||
showInTable: true,
|
||||
inputType: 'none',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function moveColumn(oldIndex, delta) {
|
||||
const column = $views[activeViewKey].columns[oldIndex];
|
||||
const column = $views[collection.viewKey].columns[oldIndex];
|
||||
const newIndex = oldIndex + delta;
|
||||
|
||||
$views[activeViewKey].columns.splice(oldIndex, 1);
|
||||
$views[activeViewKey].columns.splice(newIndex, 0, column);
|
||||
$views[activeViewKey].columns = $views[activeViewKey].columns;
|
||||
$views[collection.viewKey].columns.splice(oldIndex, 1);
|
||||
$views[collection.viewKey].columns.splice(newIndex, 0, column);
|
||||
$views[collection.viewKey].columns = $views[collection.viewKey].columns;
|
||||
}
|
||||
|
||||
function removeColumn(index) {
|
||||
$views[activeViewKey].columns.splice(index, 1);
|
||||
$views[activeViewKey].columns = $views[activeViewKey].columns;
|
||||
$views[collection.viewKey].columns.splice(index, 1);
|
||||
$views[collection.viewKey].columns = $views[collection.viewKey].columns;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal title="View configuration" bind:show contentPadding={false}>
|
||||
<Modal title="View configuration" contentPadding={false} on:close>
|
||||
<TabBar
|
||||
{tabs}
|
||||
canAddTab={true}
|
||||
on:addTab={createView}
|
||||
on:closeTab={e => removeView(e.detail)}
|
||||
bind:selectedKey={activeViewKey}
|
||||
bind:selectedKey={collection.viewKey}
|
||||
/>
|
||||
|
||||
<div class="options">
|
||||
{#if $views[activeViewKey]}
|
||||
{#if $views[collection.viewKey]}
|
||||
<div class="meta">
|
||||
{#key activeViewKey}
|
||||
{#key collection.viewKey}
|
||||
<label class="field">
|
||||
<span class="label">View name</span>
|
||||
<input type="text" use:input={{ autofocus: true }} bind:value={$views[activeViewKey].name} disabled={activeViewKey === 'list'} />
|
||||
<input
|
||||
type="text"
|
||||
use:input={{ autofocus: true }}
|
||||
bind:value={$views[collection.viewKey].name}
|
||||
disabled={collection.viewKey === 'list'}
|
||||
/>
|
||||
</label>
|
||||
{/key}
|
||||
<label class="field">
|
||||
<span class="label">View type</span>
|
||||
<select bind:value={$views[activeViewKey].type} disabled>
|
||||
<select bind:value={$views[collection.viewKey].type} disabled>
|
||||
<option value="list">List view</option>
|
||||
<option value="table">Table view</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if $views[activeViewKey].type === 'list'}
|
||||
{#if $views[collection.viewKey].type === 'list'}
|
||||
<div class="flex">
|
||||
<input type="checkbox" id="hideObjectIndicators" bind:checked={$views[activeViewKey].hideObjectIndicators} />
|
||||
<input
|
||||
type="checkbox"
|
||||
id="hideObjectIndicators"
|
||||
bind:checked={$views[collection.viewKey].hideObjectIndicators}
|
||||
/>
|
||||
<label for="hideObjectIndicators">
|
||||
Hide object indicators ({'{...}'} and [...]) in list view and show nothing instead
|
||||
</label>
|
||||
</div>
|
||||
{:else if $views[activeViewKey].type === 'table'}
|
||||
{:else if $views[collection.viewKey].type === 'table'}
|
||||
<div class="columns">
|
||||
{#each $views[activeViewKey].columns as column, columnIndex}
|
||||
{#each $views[collection.viewKey].columns as column, columnIndex}
|
||||
<div class="column">
|
||||
<label class="field">
|
||||
<input type="text" use:input bind:value={column.key} placeholder="Column keypath" />
|
||||
@ -178,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[activeViewKey].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>
|
||||
@ -195,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,23 +1,20 @@
|
||||
<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 queries from '$lib/stores/queries';
|
||||
import applicationSettings from '$lib/stores/settings';
|
||||
import views from '$lib/stores/views';
|
||||
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 { createEventDispatcher, onMount } from 'svelte';
|
||||
import ExportInfo from './components/export.svelte';
|
||||
import QueryChooser from './components/querychooser.svelte';
|
||||
|
||||
export let collection;
|
||||
export let visible = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const defaults = {
|
||||
query: '{}',
|
||||
sort: $applicationSettings.defaultSort || '{ "_id": 1 }',
|
||||
@ -28,30 +25,50 @@
|
||||
|
||||
let form = { ...defaults };
|
||||
let result = {};
|
||||
let countResult = {};
|
||||
let submittedForm = {};
|
||||
let queryField;
|
||||
let activePath = [];
|
||||
let objectViewerData;
|
||||
let queryToSave;
|
||||
let showQueryChooser = false;
|
||||
let exportInfo;
|
||||
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})` : ''};`;
|
||||
|
||||
$: viewsForCollection = views.forCollection(collection.hostKey, collection.dbKey, collection.key);
|
||||
$: code = `db.${collection.key}.find(${form.query || '{}'}${form.fields && form.fields !== '{}' ? `, ${form.fields}` : ''}).sort(${form.sort})${form.skip ? `.skip(${form.skip})` : ''}${form.limit ? `.limit(${form.limit})` : ''};`;
|
||||
$: lastPage = (submittedForm.limit && result?.results?.length) ? Math.max(0, Math.ceil((result.total - submittedForm.limit) / submittedForm.limit)) : 0;
|
||||
$: activePage = (submittedForm.limit && submittedForm.skip && result?.results?.length) ? submittedForm.skip / submittedForm.limit : 0;
|
||||
|
||||
$: if ($views) {
|
||||
viewsForCollection = views.forCollection(collection.hostKey, collection.dbKey, collection.key);
|
||||
}
|
||||
|
||||
async function submitQuery() {
|
||||
if (querying) {
|
||||
if (querying || !visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
querying = true;
|
||||
const progress = startProgress('Performing query…');
|
||||
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 }));
|
||||
@ -59,30 +76,39 @@
|
||||
submittedForm = deepClone(form);
|
||||
}
|
||||
|
||||
progress.end();
|
||||
resetFocus();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
function loadQuery() {
|
||||
queryToSave = undefined;
|
||||
showQueryChooser = true;
|
||||
async function loadQuery() {
|
||||
const query = await collection.openQueryChooser();
|
||||
if (query) {
|
||||
form = { ...query };
|
||||
submitQuery();
|
||||
}
|
||||
}
|
||||
|
||||
function saveQuery() {
|
||||
queryToSave = form;
|
||||
showQueryChooser = true;
|
||||
}
|
||||
|
||||
function queryChosen(event) {
|
||||
if ($queries[event?.detail]) {
|
||||
form = { ...$queries[event?.detail] };
|
||||
async function saveQuery() {
|
||||
const query = await collection.openQueryChooser(form);
|
||||
if (query) {
|
||||
form = { ...query };
|
||||
submitQuery();
|
||||
}
|
||||
}
|
||||
@ -114,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();
|
||||
}
|
||||
@ -130,76 +167,116 @@
|
||||
objectViewerData = item;
|
||||
}
|
||||
|
||||
function openViewConfig() {
|
||||
views.openConfig(collection, result.results?.[0] || {});
|
||||
}
|
||||
|
||||
export function performQuery(q) {
|
||||
form = { ...defaults, ...q };
|
||||
submitQuery();
|
||||
}
|
||||
|
||||
async function saveDocument(event) {
|
||||
const progress = startProgress('Performing update…');
|
||||
const success = await UpdateFoundDocument(
|
||||
collection.hostKey,
|
||||
collection.dbKey,
|
||||
collection.key,
|
||||
EJSON.stringify({ _id: event.detail.originalData._id }),
|
||||
event.detail.text
|
||||
convertLooseJson(event.detail.text)
|
||||
);
|
||||
|
||||
if (success) {
|
||||
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" class="code" bind:this={queryField} bind:value={form.query} use:input={{ type: 'json', autofocus: true }} placeholder={defaults.query} />
|
||||
<input
|
||||
type="text"
|
||||
class="code"
|
||||
placeholder={defaults.query}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
use:input
|
||||
bind:this={queryField}
|
||||
bind:value={form.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" min="0" bind:value={form.skip} use:input placeholder={defaults.skip} list="skipstops" />
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
bind:value={form.skip}
|
||||
use:input
|
||||
placeholder={defaults.skip}
|
||||
list="skipstops"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span class="label">Limit</span>
|
||||
<input type="number" min="0" bind:value={form.limit} use:input placeholder={defaults.limit} list="limits" />
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
bind:value={form.limit}
|
||||
use:input
|
||||
placeholder={defaults.limit}
|
||||
list="limits"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<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={() => exportInfo = {}}>
|
||||
<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>
|
||||
@ -215,50 +292,108 @@
|
||||
hideObjectIndicators={$views[collection.viewKey]?.hideObjectIndicators}
|
||||
bind:activePath
|
||||
on:trigger={e => openJson(e.detail?.index)}
|
||||
errorTitle={result.errorTitle}
|
||||
errorDescription={result.errorDescription}
|
||||
busy={querying}
|
||||
/>
|
||||
{:else}
|
||||
<Grid
|
||||
key="_id"
|
||||
columns={$views[collection.viewKey]?.columns?.map(c => ({ key: c.key, title: c.key })) || []}
|
||||
columns={$views[collection.viewKey]?.columns
|
||||
?.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
|
||||
on:trigger={e => openJson(e.detail?.index)}
|
||||
errorTitle={result.errorTitle}
|
||||
errorDescription={result.errorDescription}
|
||||
busy={querying}
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
</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={() => dispatch('openViewConfig', { firstItem: result.results?.[0] })} 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>
|
||||
@ -266,22 +401,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<QueryChooser
|
||||
bind:queryToSave
|
||||
bind:show={showQueryChooser}
|
||||
on:select={queryChosen}
|
||||
{collection}
|
||||
/>
|
||||
|
||||
<ExportInfo on:openViewConfig bind:collection bind:info={exportInfo} />
|
||||
|
||||
{#if objectViewerData}
|
||||
<!-- @todo Implement save -->
|
||||
<ObjectViewer bind:data={objectViewerData} saveable on:save={saveDocument} bind:successMessage={objectViewerSuccessMessage} />
|
||||
<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>
|
||||
@ -301,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;
|
||||
@ -338,4 +476,8 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.count {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,96 +1,72 @@
|
||||
<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';
|
||||
import ViewConfig from './components/viewconfig.svelte';
|
||||
import Find from './find.svelte';
|
||||
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 collectionKey;
|
||||
export let tab = 'find';
|
||||
|
||||
let tab = 'find';
|
||||
let find;
|
||||
let viewConfigModalOpen = false;
|
||||
let firstItem;
|
||||
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 = collectionKey;
|
||||
for (const key of Object.keys(tabs)) {
|
||||
tabs[key].key = key;
|
||||
}
|
||||
|
||||
$: if (hostKey || dbKey || collectionKey) {
|
||||
tab = 'find';
|
||||
}
|
||||
|
||||
EventsOn('OpenCollectionTab', name => (tab = name || tab));
|
||||
|
||||
async function catchQuery(event) {
|
||||
tab = 'find';
|
||||
await tick();
|
||||
find.performQuery(event.detail);
|
||||
}
|
||||
|
||||
function openViewConfig(event) {
|
||||
firstItem = event.detail?.firstItem;
|
||||
viewConfigModalOpen = true;
|
||||
tabs.find.instance.performQuery(event.detail);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="collection" class:empty={!collection}>
|
||||
<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} on:openViewConfig={openViewConfig} />
|
||||
{:else if tab === 'insert'} <Insert {collection} on:performFind={catchQuery} on:openViewConfig={openViewConfig} />
|
||||
{: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}
|
||||
</div>
|
||||
|
||||
{#if collection}
|
||||
<ViewConfig
|
||||
bind:show={viewConfigModalOpen}
|
||||
bind:activeViewKey={collection.viewKey}
|
||||
{firstItem}
|
||||
{collection}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.collection {
|
||||
.view {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template: auto 1fr / 1fr;
|
||||
}
|
||||
.collection.empty {
|
||||
.view.empty {
|
||||
grid-template: 1fr / 1fr;
|
||||
}
|
||||
|
||||
@ -102,6 +78,9 @@
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.container.hidden {
|
||||
display: none;
|
||||
}
|
||||
.container > :global(*) {
|
||||
width: 100%;
|
||||
}
|
||||
|