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

27 Commits

Author SHA1 Message Date
0209e2d9a1 Corrected some imports 2023-08-07 18:30:30 +02:00
a3afad6d6b Lint code 2023-08-07 18:23:36 +02:00
59d7895f62 Use yeslint! as configuration for ESLint 2023-08-07 18:23:36 +02:00
82796d5a8c Converted some JS files to TypeScript 2023-08-07 17:08:22 +02:00
5b15782d7d Moved editors and grid components to their own directories 2023-08-07 16:59:22 +02:00
f33cac7f5c DB/collection dropping bugfix
After dropping a collection or database, the hosttree was not reloaded
correctly. Now, dropped databases and collections are correctly removed
from the host tree.
2023-08-07 14:57:31 +02:00
b49faea073 Added newline in changelog 2023-08-07 14:49:12 +02:00
239af3590d Added deadline for counting documents
Set a deadline for counting documents, and added a button to count
documents if the deadline has been exceeded.
2023-07-24 20:38:30 +02:00
0e376866a7 Open file tab by default 2023-07-24 20:07:29 +02:00
d677b825e1 Disable spellcheck and autocomplete on all inputs 2023-07-21 21:49:29 +02:00
3ca561b4b4 Added collection copy feature (fixes #63) 2023-07-21 21:40:08 +02:00
7e5e2127ff Docs: note that shell is currently under development 2023-07-21 19:35:21 +02:00
24b3180eb5 build.js recreates build output dir after it has been removed (fixes #62) 2023-07-21 19:28:44 +02:00
3827ae3482 Bump word-wrap from 1.2.3 to 1.2.4 in /frontend (#61)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3
to 1.2.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/jonschlinkert/word-wrap/releases">word-wrap's
releases</a>.</em></p>
<blockquote>
<h2>1.2.4</h2>
<h2>What's Changed</h2>
<ul>
<li>Remove default indent by <a
href="https://github.com/mohd-akram"><code>@​mohd-akram</code></a> in <a
href="https://redirect.github.com/jonschlinkert/word-wrap/pull/24">jonschlinkert/word-wrap#24</a></li>
<li>🔒fix: CVE 2023 26115 (2) by <a
href="https://github.com/OlafConijn"><code>@​OlafConijn</code></a> in <a
href="https://redirect.github.com/jonschlinkert/word-wrap/pull/41">jonschlinkert/word-wrap#41</a></li>
<li>🔒 fix: CVE-2023-26115 by <a
href="https://github.com/aashutoshrathi"><code>@​aashutoshrathi</code></a>
in <a
href="https://redirect.github.com/jonschlinkert/word-wrap/pull/33">jonschlinkert/word-wrap#33</a></li>
<li>chore: publish workflow by <a
href="https://github.com/OlafConijn"><code>@​OlafConijn</code></a> in <a
href="https://redirect.github.com/jonschlinkert/word-wrap/pull/42">jonschlinkert/word-wrap#42</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/mohd-akram"><code>@​mohd-akram</code></a> made
their first contribution in <a
href="https://redirect.github.com/jonschlinkert/word-wrap/pull/24">jonschlinkert/word-wrap#24</a></li>
<li><a
href="https://github.com/OlafConijn"><code>@​OlafConijn</code></a> made
their first contribution in <a
href="https://redirect.github.com/jonschlinkert/word-wrap/pull/41">jonschlinkert/word-wrap#41</a></li>
<li><a
href="https://github.com/aashutoshrathi"><code>@​aashutoshrathi</code></a>
made their first contribution in <a
href="https://redirect.github.com/jonschlinkert/word-wrap/pull/33">jonschlinkert/word-wrap#33</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4">https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="f64b188c72"><code>f64b188</code></a>
run verb to generate README</li>
<li><a
href="03ea08256b"><code>03ea082</code></a>
Merge pull request <a
href="https://redirect.github.com/jonschlinkert/word-wrap/issues/42">#42</a>
from jonschlinkert/chore/publish-workflow</li>
<li><a
href="420dce9a24"><code>420dce9</code></a>
Merge pull request <a
href="https://redirect.github.com/jonschlinkert/word-wrap/issues/41">#41</a>
from jonschlinkert/fix/CVE-2023-26115-2</li>
<li><a
href="bfa694edf5"><code>bfa694e</code></a>
Update .github/workflows/publish.yml</li>
<li><a
href="ace0b3c78f"><code>ace0b3c</code></a>
chore: bump version to 1.2.4</li>
<li><a
href="6fd7275946"><code>6fd7275</code></a>
chore: add publish workflow</li>
<li><a
href="30d6daf60f"><code>30d6daf</code></a>
chore: fix test</li>
<li><a
href="655929cabe"><code>655929c</code></a>
chore: remove package-lock</li>
<li><a
href="49e08bbc32"><code>49e08bb</code></a>
chore: added an additional testcase</li>
<li><a
href="9f626935f3"><code>9f62693</code></a>
fix: cve 2023-26115</li>
<li>Additional commits viewable in <a
href="https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=word-wrap&package-manager=npm_and_yarn&previous-version=1.2.3&new-version=1.2.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/garraflavatra/rolens/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-20 16:57:15 +02:00
3556098bc5 Updated the changelog 2023-07-19 21:31:39 +02:00
5deab93162 Find view: confirm deletion (fixes #58) 2023-07-19 21:07:50 +02:00
3d13dd974e Docs: styled stub warning 2023-07-19 20:53:15 +02:00
43fce1f27e Added shell documentation 2023-07-19 20:45:05 +02:00
77747c10c2 Added horizontal/vertical view toggle to the shell (#37) 2023-07-19 20:18:40 +02:00
9d577ac429 Added functionality to save and import shell scripts (#37) 2023-07-19 20:01:15 +02:00
ae5002d356 Updated the changelog 2023-07-09 12:56:44 +02:00
6027ab155a Find view: paste ID and press Enter (fixes #55) 2023-07-08 13:17:30 +02:00
5476df5fe9 Preserve state between tabs (cc #37) (fixes #56) 2023-07-08 13:07:55 +02:00
64fb9ed173 Added shell to changelog 2023-07-02 11:13:58 +02:00
770a204be2 Moved icon SVGs out of icons.svelte 2023-07-02 11:00:20 +02:00
7a5354c5f4 Report stderr in shell (#37) 2023-07-02 10:10:45 +02:00
61142844fa Shell feature (#44)
Resolves #37: adds a shell tab where the user can write mongo shell
scripts and execute them.
2023-07-01 21:34:32 +02:00
81 changed files with 1779 additions and 533 deletions

6
.gitattributes vendored
View File

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

View File

@ -1,6 +1,22 @@
## [Unreleased]
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.
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]

View File

@ -1,7 +1,7 @@
#!/usr/bin/env node
const { execSync, spawn } = require('child_process');
const { readFileSync, statSync, rmdirSync } = require('fs');
const { readFileSync, statSync, rmdirSync, mkdirSync } = require('fs');
// Check that the script is run from the root.
@ -145,6 +145,12 @@ if (missingDependencies.length > 0) {
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.
@ -154,8 +160,25 @@ console.log();
const proc = spawn('wails', [ 'build', '-clean', isWindows ? '-nsis' : '' ]);
if (!quiet) {
proc.stdout.on('data', data => process.stdout.write(data));
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 => process.exit(code));
proc.on('exit', code => {
console.log();
process.exit(code);
});

BIN
docs/images/shell.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

17
docs/user-guide/shell.md Normal file
View 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)._
![The shell tab](/images/shell.png)
## Requirements
To use the script editor, you need to install the official [`mongosh` tool](https://www.mongodb.com/docs/mongodb-shell/) from MongoDB. You can [download it](https://www.mongodb.com/try/download/shell) from the MongoDB web site, or run `brew install mongosh` if you use a Mac.

View File

@ -6,6 +6,11 @@ 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] %}

View File

@ -22,6 +22,7 @@
"include": [
"src/**/*.d.ts",
"src/**/*.js",
"src/**/*.ts",
"src/**/*.svelte"
]
}

View File

@ -16,6 +16,7 @@
"date-fns": "^2.30.0"
},
"devDependencies": {
"@garraflavatra/yeslint": "^1.0.0",
"@sveltejs/vite-plugin-svelte": "^1.0.1",
"eslint": "^8.43.0",
"eslint-config-svelte3": "github:johbog/eslint-config-svelte3",
@ -212,6 +213,24 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@garraflavatra/yeslint": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@garraflavatra/yeslint/-/yeslint-1.0.0.tgz",
"integrity": "sha512-LvpQq+buhjjGMtzdzgj5iFCOG9ZGvbgOsFMvtRCYiqpzzNjhXsfO4IGUrJuVL+AwWUzNPJNTzKI5OyQZbh07Jg==",
"dev": true,
"dependencies": {
"eslint-plugin-import": "^2.27.5"
},
"peerDependencies": {
"eslint": "^7.9.0 || ^8.0.0",
"eslint-plugin-svelte": "^2.32.4"
},
"peerDependenciesMeta": {
"eslint-plugin-svelte": {
"optional": true
}
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
@ -654,6 +673,18 @@
"node": ">= 8"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"bin": {
"cssesc": "bin/cssesc"
},
"engines": {
"node": ">=4"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@ -1349,20 +1380,22 @@
}
},
"node_modules/eslint-plugin-svelte": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.30.0.tgz",
"integrity": "sha512-2/qj0BJsfM0U2j4EjGb7iC/0nbUvXx1Gn78CdtyuXpi/rSomLPCPwnsZsloXMzlt6Xwe8LBlpRvZObSKEHLP5A==",
"version": "2.32.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.32.4.tgz",
"integrity": "sha512-VJ12i2Iogug1jvhwxSlognnfGj76P5gks/V4pUD4SCSVQOp14u47MNP0zAG8AQR3LT0Fi1iUvIFnY4l9z5Rwbg==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@jridgewell/sourcemap-codec": "^1.4.14",
"debug": "^4.3.1",
"esutils": "^2.0.3",
"known-css-properties": "^0.27.0",
"known-css-properties": "^0.28.0",
"postcss": "^8.4.5",
"postcss-load-config": "^3.1.4",
"postcss-safe-parser": "^6.0.0",
"svelte-eslint-parser": "^0.30.0"
"postcss-selector-parser": "^6.0.11",
"semver": "^7.5.3",
"svelte-eslint-parser": "^0.32.2"
},
"engines": {
"node": "^14.17.0 || >=16.0.0"
@ -1372,7 +1405,7 @@
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0-0",
"svelte": "^3.37.0 || ^4.0.0-0"
"svelte": "^3.37.0 || ^4.0.0"
},
"peerDependenciesMeta": {
"svelte": {
@ -1380,6 +1413,21 @@
}
}
},
"node_modules/eslint-plugin-svelte/node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/eslint-scope": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
@ -2157,9 +2205,9 @@
}
},
"node_modules/known-css-properties": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.27.0.tgz",
"integrity": "sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.28.0.tgz",
"integrity": "sha512-9pSL5XB4J+ifHP0e0jmmC98OGC1nL8/JjS+fi6mnTlIf//yt/MfVLtKg7S6nCtj/8KTcWX7nRlY0XywoYY1ISQ==",
"dev": true
},
"node_modules/levn": {
@ -2205,6 +2253,18 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/magic-string": {
"version": "0.26.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz",
@ -2245,10 +2305,16 @@
"dev": true
},
"node_modules/nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@ -2423,9 +2489,9 @@
"dev": true
},
"node_modules/postcss": {
"version": "8.4.20",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz",
"integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==",
"version": "8.4.27",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz",
"integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==",
"dev": true,
"funding": [
{
@ -2435,10 +2501,14 @@
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"nanoid": "^3.3.4",
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
@ -2491,6 +2561,41 @@
"postcss": "^8.3.3"
}
},
"node_modules/postcss-scss": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz",
"integrity": "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss-scss"
}
],
"engines": {
"node": ">=12.0"
},
"peerDependencies": {
"postcss": "^8.4.19"
}
},
"node_modules/postcss-selector-parser": {
"version": "6.0.13",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz",
"integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==",
"dev": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -2831,14 +2936,16 @@
}
},
"node_modules/svelte-eslint-parser": {
"version": "0.30.0",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.30.0.tgz",
"integrity": "sha512-H0Cn2TKr70DU9p/Gb04CfwtS7eK28MYumrHYPaDNkIFbfwGDLADpbERBn7u8G1Rcm2RMr2/mL6mq0J2e8iKFlA==",
"version": "0.32.2",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.32.2.tgz",
"integrity": "sha512-Ok9D3A4b23iLQsONrjqtXtYDu5ZZ/826Blaw2LeFZVTg1pwofKDG4mz3/GYTax8fQ0plRGHI6j+d9VQYy5Lo/A==",
"dev": true,
"dependencies": {
"eslint-scope": "^7.0.0",
"eslint-visitor-keys": "^3.0.0",
"espree": "^9.0.0"
"espree": "^9.0.0",
"postcss": "^8.4.25",
"postcss-scss": "^4.0.6"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@ -2847,7 +2954,7 @@
"url": "https://github.com/sponsors/ota-meshi"
},
"peerDependencies": {
"svelte": "^3.37.0 || ^4.0.0-0"
"svelte": "^3.37.0 || ^4.0.0"
},
"peerDependenciesMeta": {
"svelte": {
@ -2947,6 +3054,12 @@
"punycode": "^2.1.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
},
"node_modules/vite": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz",
@ -3067,9 +3180,9 @@
}
},
"node_modules/word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@ -3081,6 +3194,12 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
@ -3248,6 +3367,15 @@
"integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==",
"dev": true
},
"@garraflavatra/yeslint": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@garraflavatra/yeslint/-/yeslint-1.0.0.tgz",
"integrity": "sha512-LvpQq+buhjjGMtzdzgj5iFCOG9ZGvbgOsFMvtRCYiqpzzNjhXsfO4IGUrJuVL+AwWUzNPJNTzKI5OyQZbh07Jg==",
"dev": true,
"requires": {
"eslint-plugin-import": "^2.27.5"
}
},
"@humanwhocodes/config-array": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
@ -3567,6 +3695,12 @@
"which": "^2.0.1"
}
},
"cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true
},
"date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@ -4007,20 +4141,33 @@
}
},
"eslint-plugin-svelte": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.30.0.tgz",
"integrity": "sha512-2/qj0BJsfM0U2j4EjGb7iC/0nbUvXx1Gn78CdtyuXpi/rSomLPCPwnsZsloXMzlt6Xwe8LBlpRvZObSKEHLP5A==",
"version": "2.32.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.32.4.tgz",
"integrity": "sha512-VJ12i2Iogug1jvhwxSlognnfGj76P5gks/V4pUD4SCSVQOp14u47MNP0zAG8AQR3LT0Fi1iUvIFnY4l9z5Rwbg==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.2.0",
"@jridgewell/sourcemap-codec": "^1.4.14",
"debug": "^4.3.1",
"esutils": "^2.0.3",
"known-css-properties": "^0.27.0",
"known-css-properties": "^0.28.0",
"postcss": "^8.4.5",
"postcss-load-config": "^3.1.4",
"postcss-safe-parser": "^6.0.0",
"svelte-eslint-parser": "^0.30.0"
"postcss-selector-parser": "^6.0.11",
"semver": "^7.5.3",
"svelte-eslint-parser": "^0.32.2"
},
"dependencies": {
"semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
}
}
}
},
"eslint-scope": {
@ -4563,9 +4710,9 @@
"dev": true
},
"known-css-properties": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.27.0.tgz",
"integrity": "sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.28.0.tgz",
"integrity": "sha512-9pSL5XB4J+ifHP0e0jmmC98OGC1nL8/JjS+fi6mnTlIf//yt/MfVLtKg7S6nCtj/8KTcWX7nRlY0XywoYY1ISQ==",
"dev": true
},
"levn": {
@ -4599,6 +4746,15 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"requires": {
"yallist": "^4.0.0"
}
},
"magic-string": {
"version": "0.26.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz",
@ -4630,9 +4786,9 @@
"dev": true
},
"nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"dev": true
},
"natural-compare": {
@ -4757,12 +4913,12 @@
"dev": true
},
"postcss": {
"version": "8.4.20",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz",
"integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==",
"version": "8.4.27",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz",
"integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==",
"dev": true,
"requires": {
"nanoid": "^3.3.4",
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
}
@ -4784,6 +4940,23 @@
"dev": true,
"requires": {}
},
"postcss-scss": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz",
"integrity": "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==",
"dev": true,
"requires": {}
},
"postcss-selector-parser": {
"version": "6.0.13",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz",
"integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==",
"dev": true,
"requires": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
}
},
"prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -5004,14 +5177,16 @@
"dev": true
},
"svelte-eslint-parser": {
"version": "0.30.0",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.30.0.tgz",
"integrity": "sha512-H0Cn2TKr70DU9p/Gb04CfwtS7eK28MYumrHYPaDNkIFbfwGDLADpbERBn7u8G1Rcm2RMr2/mL6mq0J2e8iKFlA==",
"version": "0.32.2",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.32.2.tgz",
"integrity": "sha512-Ok9D3A4b23iLQsONrjqtXtYDu5ZZ/826Blaw2LeFZVTg1pwofKDG4mz3/GYTax8fQ0plRGHI6j+d9VQYy5Lo/A==",
"dev": true,
"requires": {
"eslint-scope": "^7.0.0",
"eslint-visitor-keys": "^3.0.0",
"espree": "^9.0.0"
"espree": "^9.0.0",
"postcss": "^8.4.25",
"postcss-scss": "^4.0.6"
}
},
"svelte-hmr": {
@ -5086,6 +5261,12 @@
"punycode": "^2.1.0"
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
},
"vite": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz",
@ -5148,9 +5329,9 @@
}
},
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true
},
"wrappy": {
@ -5159,6 +5340,12 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",

View File

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

View File

@ -1 +1 @@
7fc6d7b66151030191ded7136d96970e
015746ba33749cd2864cd336088387ef

View File

@ -1,14 +1,14 @@
<script>
import BlankState from '$components/blankstate.svelte';
import ContextMenu from '$components/contextmenu.svelte';
import dialogs from '$lib/dialogs';
import contextMenu from '$lib/stores/contextmenu';
import environment from '$lib/stores/environment';
import hostTree from '$lib/stores/hosttree';
import applicationInited from '$lib/stores/inited';
import windowTitle from '$lib/stores/windowtitle';
import dialogs from '$lib/dialogs.js';
import contextMenu from '$lib/stores/contextmenu.js';
import environment from '$lib/stores/environment.js';
import hostTree from '$lib/stores/hosttree.js';
import applicationInited from '$lib/stores/inited.js';
import windowTitle from '$lib/stores/windowtitle.js';
import Connection from '$organisms/connection/index.svelte';
import { EventsOn } from '$wails/runtime';
import { EventsOn } from '$wails/runtime/runtime.js';
import { tick } from 'svelte';
import AboutDialog from './dialogs/about.svelte';
import SettingsDialog from './dialogs/settings/index.svelte';

View File

@ -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}

View File

@ -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,7 +8,7 @@
export let text = '';
export let editor = undefined;
export let readonly = false;
export let extensions = [];
const dispatch = createEventDispatcher();
let editorParent;
@ -19,9 +18,7 @@
extensions: [
basicSetup,
keymap.of([ indentWithTab, indentOnInput ]),
javascript(),
EditorState.tabSize.of(4),
EditorState.readOnly.of(readonly),
EditorView.updateListener.of(e => {
if (!e.docChanged) {
return;
@ -29,6 +26,7 @@
text = e.state.doc.toString();
dispatch('updated', { text });
}),
...extensions,
],
});
@ -38,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 {

View File

@ -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}

View File

@ -1,5 +1,5 @@
<script>
import { OpenDirectory } from '$wails/go/ui/UI';
import { OpenDirectory } from '$wails/go/ui/UI.js';
export let value = '';
export let id = '';
@ -10,7 +10,13 @@
}
</script>
<input type="text" on:pointerdown={selectDir} readonly {id} {value} />
<input
type="text"
on:pointerdown={selectDir}
readonly
{id}
{value}
/>
<style>
input {

View File

@ -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 }} autocomplete="off" spellcheck="false" />
<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>

View 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}
/>

View File

@ -1,9 +1,9 @@
<script>
import { pathsAreEqual, resolveKeypath, setValue } from '$lib/objects';
import contextMenu from '$lib/stores/contextmenu';
import { pathsAreEqual, resolveKeypath, setValue } from '$lib/objects.js';
import contextMenu from '$lib/stores/contextmenu.js';
import { createEventDispatcher } from 'svelte';
import FormInput from './forminput.svelte';
import Icon from './icon.svelte';
import FormInput from '$components/editors/forminput.svelte';
import Icon from '$components/icon.svelte';
export let items = [];
export let columns = [];
@ -131,7 +131,7 @@
on:dblclick={() => doubleClick(item[key], index)}
on:contextmenu|preventDefault={evt => showContextMenu(evt, item)}
class:selectable={canSelect}
class:selected={canSelect && pathsAreEqual(activePath, [ ...path, item[key] ])}
class:selected={canSelect && pathsAreEqual(activePath, ...path, item[key])}
class:striped
>
{#if !hideChildrenToggles}
@ -157,7 +157,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])}
@ -168,7 +172,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>

View File

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

View File

@ -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';
@ -22,7 +22,6 @@
].filter(c => !!c);
$: if (data) {
// items = dissectObject(data).map(item => ({ ...item, menu: getRootMenu(item.key, item) }));
items = [];
if (Array.isArray(data)) {

View File

@ -1,6 +1,9 @@
<script>
import icons from '$lib/icons.json';
export let name = '';
export let spin = false;
export let rotation = 0;
if (name === 'loading') {
spin = true;
@ -8,26 +11,34 @@
</script>
<style>
:global(.field) svg {
width: 13px;
height: 13px;
margin-right: 2px;
}
:global(.button) svg {
height: 13px;
width: auto;
vertical-align: bottom;
}
@keyframes spinning {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
svg {
transition: transform 0.25s;
will-change: transform;
}
svg.spinning {
animation: spinning 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
}
:global(.field) svg {
width: 13px;
height: 13px;
margin-right: 2px;
}
:global(.button) svg {
height: 13px;
width: auto;
vertical-align: bottom;
}
:global(.blankstate .button) svg {
height: 17px;
vertical-align: -3px;
margin-right: 4px;
}
</style>
<svg
@ -41,113 +52,7 @@
stroke-linecap="round"
stroke-linejoin="round"
class:spinning={spin}
style:transform="rotate({rotation}deg)"
>
{#if name === 'radio'}
<circle cx="12" cy="12" r="2"></circle><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path>
{:else if name === 'chev-l'}
<polyline points="15 18 9 12 15 6"></polyline>
{:else if name === 'chev-r'}
<polyline points="9 18 15 12 9 6"></polyline>
{:else if name === 'chev-d'}
<polyline points="6 9 12 15 18 9"></polyline>
{:else if name === 'chev-u'}
<path d="m18 15-6-6-6 6" />
{:else if name === 'chevs-l'}
<path d="m11 17-5-5 5-5M18 17l-5-5 5-5" />
{:else if name === 'chevs-r'}
<path d="m13 17 5-5-5-5M6 17l5-5-5-5" />
{:else if name === 'arr-d'}
<path d="M12 5v14M19 12l-7 7-7-7" />
{:else if name === 'db'}
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
{:else if name === 'x'}
<line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line>
{:else if name === '+'}
<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>
{:else if name === '-'}
<line x1="5" y1="12" x2="19" y2="12"></line>
{:else if name === 'reload'}
<polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
{:else if name === 'clipboard'}
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8"
y="2"
width="8"
height="4"
rx="1"
ry="1"></rect>
{:else if name === 'edit'}
<path d="M12 20h9"></path><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
{:else if name === 'check'}
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline>
{:else if name === 'list'}
<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" />
{:else if name === 'table'}
<path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18" />
{:else if name === 'form'}
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" />
{:else if name === 'cog'}
<circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
{:else if name === 'zap'}
<path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z" />
{:else if name === 'server'}
<rect x="2"
y="2"
width="20"
height="8"
rx="2"
ry="2" /><rect x="2"
y="14"
width="20"
height="8"
rx="2"
ry="2" /><path d="M6 6h.01M6 18h.01" />
{:else if name === 'text'}
<path d="M4 7V4h16v3M9 20h6M12 4v16" />
{:else if name === 'hash'}
<path d="M4 9h16M4 15h16M10 3 8 21M16 3l-2 18" />
{:else if name === 'toggle-l'}
<rect x="1"
y="5"
width="22"
height="14"
rx="7"
ry="7" /><circle cx="8" cy="12" r="3" />
{:else if name === 'cal'}
<rect x="3"
y="4"
width="18"
height="18"
rx="2"
ry="2" /><path d="M16 2v4M8 2v4M3 10h18" />
{:else if name === 'code'}
<path d="m16 18 6-6-6-6M8 6l-6 6 6 6" />
{:else if name === 'target'}
<circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" />
{:else if name === 'trash'}
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M10 11v6M14 11v6" />
{:else if name === 'anchor'}
<circle cx="12" cy="5" r="3" /><path d="M12 22V8M5 12H2a10 10 0 0 0 20 0h-3" />
{:else if name === 'o'}
<circle cx="12" cy="12" r="10" />
{:else if name === 'info'}
<circle cx="12" cy="12" r="10" /><path d="M12 16v-4M12 8h.01" />
{:else if name === 'play'}
<path d="m5 3 14 9-14 9V3z" />
{:else if name === 'upload'}
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" />
{:else if name === 'save'}
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" /><path d="M17 21v-8H7v8M7 3v5h8" />
{:else if name === 're'}
<path d="m17 1 4 4-4 4" /><path d="M3 11V9a4 4 0 0 1 4-4h14M7 23l-4-4 4-4" /><path d="M21 13v2a4 4 0 0 1-4 4H3" />
{:else if name === 'chart'}
<path d="M18 20V10M12 20V4M6 20v-6" />
{:else if name === '?'}
<circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line>
{:else if name === '!'}
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM12 9v4M12 17h.01" />
{:else if name === 'loading'}
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
{:else if name === 'doc'}
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" />
{/if}
{@html icons[name] || ''}
</svg>

View File

@ -3,7 +3,7 @@
</script>
<script>
import { Beep } from '$wails/go/ui/UI';
import { Beep } from '$wails/go/ui/UI.js';
import { createEventDispatcher } from 'svelte';
import Icon from './icon.svelte';

View File

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

View File

@ -3,7 +3,7 @@
import Icon from './icon.svelte';
export let tabs = [];
export let selectedKey = {};
export let selectedKey = '';
export let canAddTab = false;
export let multiline = false;

View File

@ -1,7 +1,7 @@
<script>
import Modal from '$components/modal.svelte';
import alink from '$lib/actions/alink';
import environment from '$lib/stores/environment';
import alink from '$lib/actions/alink.js';
import environment from '$lib/stores/environment.js';
</script>
<Modal width="400px" title=" " on:close>

View File

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

View File

@ -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 => {

View File

@ -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) {

View File

@ -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,8 +10,11 @@ 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 checkInteger = () => (Number.isInteger(node.value) ? false : 'Value must be an integer');
const checkNumberBoundaries = boundaries => {
if (node.value < boundaries[0]) {
return `Input is too low for type ${type}`;

View File

@ -1,7 +0,0 @@
// Months
export const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ];
export const monthsAbbr = months.map(m => m.slice(0, 3));
// Days
export const days = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' ];
export const daysAbbr = days.map(d => d.slice(0, 3));

View File

@ -0,0 +1,27 @@
export const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
export const days = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday'
];
export const daysAbbr = days.map(d => d.slice(0, 3));
export const monthsAbbr = months.map(m => m.slice(0, 3));

View File

@ -1,3 +1,4 @@
import { AskConfirmation } from '$wails/go/app/App.js';
import InputDialog from '../dialogs/input.svelte';
function newDialog(dialogComponent, data = {}) {
@ -29,6 +30,10 @@ function enterText(title = '', description = '', value = '') {
});
}
const dialogs = { new: newDialog, enterText };
function confirm(message = '') {
return AskConfirmation(message);
}
const dialogs = { new: newDialog, enterText, confirm };
export default dialogs;

View 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\"/>"
}

View File

@ -1,13 +1,4 @@
// https://stackoverflow.com/a/14794066
export function isInt(value) {
if (isNaN(value)) {
return false;
}
const x = parseFloat(value);
return (x | 0) === x;
}
export function randInt(min, max) {
export function randInt(min: number, max: number) {
return Math.round(Math.random() * (max - min) + min);
}

View File

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

View File

@ -1,4 +1,4 @@
import { StartProgressBar, StopProgressBar } from '$wails/go/ui/UI';
import { StartProgressBar, StopProgressBar } from '$wails/go/ui/UI.js';
let taskCounter = 0;

View File

@ -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({});

View File

@ -1,21 +1,24 @@
import dialogs from '$lib/dialogs';
import { startProgress } from '$lib/progress';
import dialogs from '$lib/dialogs.js';
import { startProgress } from '$lib/progress.js';
import { get, writable } from 'svelte/store';
import applicationInited from './inited';
import queries from './queries';
import windowTitle from './windowtitle';
import applicationInited from './inited.js';
import queries from './queries.js';
import windowTitle from './windowtitle.js';
import ExportDialog from '$organisms/connection/collection/dialogs/export.svelte';
import IndexDetailDialog from '$organisms/connection/collection/dialogs/indexdetail.svelte';
import QueryChooserDialog from '$organisms/connection/collection/dialogs/querychooser.svelte';
import DumpDialog from '$organisms/connection/database/dialogs/dump.svelte';
import HostDetailDialog from '$organisms/connection/host/dialogs/hostdetail.svelte';
import DuplicateDialog from '$organisms/connection/collection/dialogs/duplicate.svelte';
import {
CreateIndex,
DropCollection,
DropDatabase,
DropIndex,
DuplicateCollection,
ExecuteShellScript,
GetIndexes,
HostLogs,
Hosts,
@ -27,7 +30,7 @@ import {
RemoveHost,
RenameCollection,
TruncateCollection
} from '$wails/go/app/App';
} from '$wails/go/app/App.js';
const { set, subscribe } = writable({});
const getValue = () => get({ subscribe });
@ -45,7 +48,14 @@ async function refresh() {
host.uri = hostDetails.uri;
host.open = async function() {
const { databases: dbNames, status, statusError, systemInfo, systemInfoError } = await OpenConnection(hostKey);
const {
databases: dbNames,
status,
statusError,
systemInfo,
systemInfoError,
} = await OpenConnection(hostKey);
host.status = status;
host.statusError = statusError;
host.systemInfo = systemInfo;
@ -94,6 +104,7 @@ async function refresh() {
collection.key = collKey;
collection.dbKey = dbKey;
collection.hostKey = hostKey;
collection.viewKey = 'list';
collection.indexes = collection.indexes || [];
delete collection.new;
@ -110,7 +121,10 @@ async function refresh() {
collection.rename = async function() {
const newCollKey = await dialogs.enterText('Rename collection', `Enter a new name for collection ${collKey}.`, collKey);
if (newCollKey && (newCollKey !== collKey)) {
const progress = startProgress(`Renaming collection "${collKey}" to "${newCollKey}"…`);
const progress = startProgress(
`Renaming collection "${collKey}" to "${newCollKey}"…`
);
const ok = await RenameCollection(hostKey, dbKey, collKey, newCollKey);
await database.open();
progress.end();
@ -118,12 +132,39 @@ async function refresh() {
}
};
collection.duplicate = async function() {
const dialog = dialogs.new(DuplicateDialog, { host, dbKey, collKey });
return new Promise(resolve => {
dialog.$on('duplicate', async event => {
const success = await DuplicateCollection(
hostKey,
dbKey,
collKey,
event.detail.newHost,
event.detail.newDb,
event.detail.newColl
);
if (success) {
await refresh();
dialog.$close();
resolve();
}
});
});
};
collection.export = function(query) {
const dialog = dialogs.new(ExportDialog, { collection, query });
return new Promise(resolve => {
dialog.$on('export', async event => {
const success = await PerformFindExport(hostKey, dbKey, collKey, JSON.stringify(event.detail.exportInfo));
const success = await PerformFindExport(
hostKey,
dbKey,
collKey,
JSON.stringify(event.detail.exportInfo)
);
if (success) {
dialog.$close();
resolve();
@ -158,6 +199,7 @@ async function refresh() {
collection.drop = async function() {
const success = await DropCollection(hostKey, dbKey, collKey);
if (success) {
delete database.collections[collKey];
await refresh();
}
};
@ -197,10 +239,17 @@ async function refresh() {
return new Promise(resolve => {
dialog.$on('create', async event => {
const newIndexName = await CreateIndex(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(event.detail.index));
const newIndexName = await CreateIndex(
collection.hostKey,
collection.dbKey,
collection.key,
JSON.stringify(event.detail.index)
);
if (newIndexName) {
dialog.$close();
}
resolve(newIndexName);
});
});
@ -224,6 +273,11 @@ async function refresh() {
});
});
};
collection.executeShellScript = async function(script) {
const result = await ExecuteShellScript(hostKey, dbKey, collKey, script);
return result;
};
}
await refresh();
@ -245,14 +299,11 @@ async function refresh() {
};
database.drop = async function() {
const progress = startProgress(`Dropping database "${dbKey}"…`);
const success = await DropDatabase(hostKey, dbKey);
if (success) {
delete host.databases[dbKey];
await refresh();
}
progress.end();
};
database.newCollection = async function() {
@ -262,11 +313,21 @@ async function refresh() {
await database.open();
}
};
database.executeShellScript = async function(script) {
const result = await ExecuteShellScript(hostKey, dbKey, '', script);
return result;
};
}
await refresh();
};
host.executeShellScript = async function(script) {
const result = await ExecuteShellScript(hostKey, '', '', script);
return result;
};
host.newDatabase = async function() {
const name = await dialogs.enterText('Create a database', 'Enter the database name. Note: databases in MongoDB do not exist until they have a collection and an item. Your new database will not persist on the server; fill it to have it created.', '');
if (name) {

View File

@ -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;

View File

@ -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({});

View File

@ -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({});

View File

@ -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) {

View File

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

View File

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

View File

@ -1,24 +0,0 @@
export function capitalise(string = '') {
const capitalised = string.charAt(0).toUpperCase() + string.slice(1);
return capitalised;
}
export function jsonLooseParse(json) {
const obj = new Function(`return (${json})`)();
return obj;
}
export function convertLooseJson(json) {
const j = JSON.stringify(jsonLooseParse(json));
return j;
}
export function looseJsonIsValid(json) {
try {
jsonLooseParse(json);
return true;
}
catch {
return false;
}
}

View File

@ -0,0 +1,28 @@
export function capitalise(string = ''): string {
const capitalised = string.charAt(0).toUpperCase() + string.slice(1);
return capitalised;
}
export function jsonLooseParse<T>(json: string): T {
const obj: T = new Function(`return (${json})`)();
return obj;
}
export function convertLooseJson(json: any) {
const j = JSON.stringify(jsonLooseParse(json));
return j;
}
export function looseJsonIsValid(json: string): boolean {
try {
jsonLooseParse(json);
return true;
}
catch {
return false;
}
}
export function stringCouldBeID(string: string) {
return /^[a-zA-Z0-9_-]{1,}$/.test(string);
}

View File

@ -2,7 +2,7 @@ import './styles/loading.css';
import './styles/reset.css';
import './styles/style.css';
import { LogError } from '$wails/runtime';
import { LogError } from '$wails/runtime/runtime.js';
import App from './app.svelte';
window.addEventListener('unhandledrejection', event => {

View File

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

View File

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

View File

@ -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>

View File

@ -1,7 +1,7 @@
<script>
import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte';
import views from '$lib/stores/views';
import views from '$lib/stores/views.js';
import { createEventDispatcher } from 'svelte';
export let collection;

View File

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

View File

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

View File

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

View File

@ -1,19 +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 applicationSettings from '$lib/stores/settings';
import views from '$lib/stores/views';
import { convertLooseJson } from '$lib/strings';
import { FindItems, RemoveItemById, UpdateFoundDocument } from '$wails/go/app/App';
import input from '$lib/actions/input.js';
import dialogs from '$lib/dialogs.js';
import { deepClone } from '$lib/objects.js';
import { startProgress } from '$lib/progress.js';
import applicationSettings from '$lib/stores/settings.js';
import views from '$lib/stores/views.js';
import { convertLooseJson, stringCouldBeID } from '$lib/strings.js';
import { CountItems, FindItems, RemoveItemById, UpdateFoundDocument } from '$wails/go/app/App.js';
import { EJSON } from 'bson';
import { onMount } from 'svelte';
export let collection;
export let visible = false;
const defaults = {
query: '{}',
@ -25,15 +26,21 @@
let form = { ...defaults };
let result = {};
let countResult = {};
let submittedForm = {};
let queryField;
let activePath = [];
let objectViewerData;
let querying = false;
let counting = false;
let objectViewerSuccessMessage = '';
let viewsForCollection = {};
// $: code = `db.${collection.key}.find(${form.query || '{}'}${form.fields && form.fields !== '{}' ? `, ${form.fields}` : ''}).sort(${form.sort})${form.skip ? `.skip(${form.skip})` : ''}${form.limit ? `.limit(${form.limit})` : ''};`;
// $: code = `db.${collection.key}.find(${form.query || '{}'}${form.fields &&
// form.fields !== '{}' ? `, ${form.fields}` : ''}).sort(${form.sort})
// ${form.skip ? `.skip(${form.skip})` : ''}${form.limit ? `
// .limit(${form.limit})` : ''};`;
$: lastPage = (submittedForm.limit && result?.results?.length) ? Math.max(0, Math.ceil((result.total - submittedForm.limit) / submittedForm.limit)) : 0;
$: activePage = (submittedForm.limit && submittedForm.skip && result?.results?.length) ? submittedForm.skip / submittedForm.limit : 0;
@ -42,19 +49,27 @@
}
async function submitQuery() {
if (querying) {
if (querying || !visible) {
return;
}
if (stringCouldBeID(form.query)) {
form.query = `{ "_id": "${form.query}" }`;
}
querying = `Querying ${collection.key}…`;
activePath = [];
const newResult = await FindItems(collection.hostKey, collection.dbKey, collection.key, JSON.stringify({
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,
}));
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 }));
@ -66,6 +81,17 @@
querying = false;
}
async function countItems() {
counting = true;
countResult = await CountItems(
collection.hostKey,
collection.dbKey,
collection.key,
convertLooseJson(form.query) || defaults.query
);
counting = false;
}
async function refresh() {
if ($applicationSettings.autosubmitQuery) {
await submitQuery();
@ -115,7 +141,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();
}
@ -159,7 +196,7 @@
}
$: collection && refresh();
onMount(refresh);
$: visible && refresh();
</script>
<div class="find">
@ -167,12 +204,13 @@
<div class="formrow one">
<label class="field">
<span class="label">Query or id</span>
<input type="text"
<input
type="text"
class="code"
placeholder={defaults.query}
autocomplete="off"
spellcheck="false"
use:input={{ type: 'json', autofocus: true }}
use:input
bind:this={queryField}
bind:value={form.query}
/>
@ -208,7 +246,8 @@
<label class="field">
<span class="label">Skip</span>
<input type="number"
<input
type="number"
min="0"
bind:value={form.skip}
use:input
@ -219,7 +258,8 @@
<label class="field">
<span class="label">Limit</span>
<input type="number"
<input
type="number"
min="0"
bind:value={form.limit}
use:input
@ -281,11 +321,26 @@
</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}>
@ -297,19 +352,49 @@
<Icon name="cog" />
</button>
</label>
<button class="button danger" on:click={removeActive} disabled={!activePath?.length} title="Drop selected item">
<button
class="button danger"
on:click={removeActive}
disabled={!activePath?.length}
title="Drop selected item"
>
<Icon name="-" />
</button>
<button class="button" on:click={first} disabled={!submittedForm.limit || (submittedForm.skip <= 0) || !result?.results || (activePage === 0)} title="First page">
<button
class="button"
on:click={first}
disabled={!submittedForm.limit || (submittedForm.skip <= 0) || !result?.results || (activePage === 0)}
title="First page"
>
<Icon name="chevs-l" />
</button>
<button class="button" on:click={prev} disabled={!submittedForm.limit || (submittedForm.skip <= 0) || !result?.results || (activePage === 0)} title="Previous {submittedForm.limit} items">
<button
class="button"
on:click={prev}
disabled={!submittedForm.limit || (submittedForm.skip <= 0) || !result?.results || (activePage === 0)}
title="Previous {submittedForm.limit} items"
>
<Icon name="chev-l" />
</button>
<button class="button" on:click={next} disabled={!submittedForm.limit || ((result?.results?.length || 0) < submittedForm.limit) || !result?.results || !lastPage || (activePage >= lastPage)} title="Next {submittedForm.limit} items">
<button
class="button"
on:click={next}
disabled={!submittedForm.limit || ((result?.results?.length || 0) < submittedForm.limit) || !result?.results || !lastPage || (activePage >= lastPage)}
title="Next {submittedForm.limit} items"
>
<Icon name="chev-r" />
</button>
<button class="button" on:click={last} disabled={!submittedForm.limit || ((result?.results?.length || 0) < submittedForm.limit) || !result?.results || !lastPage || (activePage >= lastPage)} title="Last page">
<button
class="button"
on:click={last}
disabled={!submittedForm.limit || ((result?.results?.length || 0) < submittedForm.limit) || !result?.results || !lastPage || (activePage >= lastPage)}
title="Last page"
>
<Icon name="chevs-r" />
</button>
</div>
@ -318,7 +403,12 @@
</div>
{#if objectViewerData}
<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">
@ -379,4 +469,8 @@
justify-content: space-between;
align-items: center;
}
.count {
text-overflow: ellipsis;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
<script>
import DirectoryChooser from '$components/directorychooser.svelte';
import Grid from '$components/grid.svelte';
import DirectoryChooser from '$components/editors/directorychooser.svelte';
import Grid from '$components/grid/grid.svelte';
import Modal from '$components/modal.svelte';
import { startProgress } from '$lib/progress';
import hostTree from '$lib/stores/hosttree';
import applicationSettings from '$lib/stores/settings';
import { OpenConnection, OpenDatabase } from '$wails/go/app/App';
import { startProgress } from '$lib/progress.js';
import hostTree from '$lib/stores/hosttree.js';
import applicationSettings from '$lib/stores/settings.js';
import { OpenConnection, OpenDatabase } from '$wails/go/app/App.js';
import { createEventDispatcher } from 'svelte';
export let info = {};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<script>
import Grid from '$components/grid.svelte';
import hostTree from '$lib/stores/hosttree';
import Grid from '$components/grid/grid.svelte';
import hostTree from '$lib/stores/hosttree.js';
export let path = [];
</script>
@ -33,6 +33,8 @@
{ label: 'Dump collection (BSON via mongodump)…', fn: collection.dump },
{ separator: true },
{ label: 'Rename collection…', fn: collection.rename },
{ label: 'Duplicate collection…', fn: collection.duplicate },
{ separator: true },
{ label: 'Truncate collection…', fn: collection.truncate },
{ label: 'Drop collection…', fn: collection.drop },
{ separator: true },

View File

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

View File

@ -0,0 +1,230 @@
<script>
import BlankState from '$components/blankstate.svelte';
import CodeEditor from '$components/editors/codeeditor.svelte';
import Icon from '$components/icon.svelte';
import environment from '$lib/stores/environment.js';
import { OpenShellScript, SaveShellScript } from '$wails/go/app/App.js';
import { BrowserOpenURL } from '$wails/runtime/runtime.js';
import { javascript } from '@codemirror/lang-javascript';
import { onDestroy, onMount } from 'svelte';
export let host = undefined;
export let database = undefined;
export let collection = undefined;
export let visible = false;
const placeholder = '// Write your script here...';
const extensions = [ javascript() ];
let script = '';
let result = {};
let horizontal = false;
let copySucceeded = false;
let timeout;
let busy = false;
let editor;
async function runScript() {
if (!$environment.hasMongoDump) {
return;
}
busy = true;
if (collection) {
result = await collection.executeShellScript(script);
}
else if (database) {
result = await database.executeShellScript(script);
}
else if (host) {
result = await host.executeShellScript(script);
}
busy = false;
}
async function loadScript() {
const _script = await OpenShellScript();
if (_script) {
script = _script;
editor.dispatch({
changes: {
from: 0,
to: editor.state.doc.length,
insert: script,
},
selection: {
from: 0,
anchor: 0,
to: 0,
head: 0,
},
});
}
}
async function saveScript() {
await SaveShellScript(
host?.key || '',
database?.key || '',
collection?.key || '',
script,
false // not temporary
);
}
async function copyErrorDescription() {
await navigator.clipboard.writeText(result.errorDescription);
copySucceeded = true;
timeout = setTimeout(() => copySucceeded = false, 1500);
}
function toggleView() {
horizontal = !horizontal;
}
function openMongoshInstallDocs() {
BrowserOpenURL('https://garraflavatra.github.io/rolens/user-guide/shell/');
}
$: visible && editor.focus();
onMount(() => {
editor.dispatch({
changes: {
from: 0,
to: editor.state.doc.length,
insert: placeholder,
},
selection: {
from: 0,
anchor: 0,
to: placeholder.length,
head: placeholder.length,
},
});
editor.focus();
});
onDestroy(() => clearTimeout(timeout));
</script>
<div class="shell" class:horizontal>
<div class="overflow">
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="field">
<CodeEditor bind:editor bind:text={script} {extensions} />
</label>
</div>
<div class="output">
{#if !$environment.hasMongoShell}
<BlankState
title="mongosh is required to run shell scripts"
label="Please refer to the documentation for more information."
icon="!"
>
<button class="button" on:click={openMongoshInstallDocs}>
<Icon name="info" /> Read the documentation
</button>
</BlankState>
{:else if busy}
<BlankState icon="loading" label="Executing…" />
{:else if result.errorTitle || result.errorDescription}
<BlankState title={result.errorTitle} label={result.errorDescription} icon="!">
<button class="button-small" on:click={copyErrorDescription}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} /> Copy error message
</button>
</BlankState>
{:else}
<pre>{result.output || ''}{#if result.stderr}<div class="error">{result.stderr}</div>{/if}</pre>
{/if}
</div>
<div class="controls">
<button class="button" on:click={runScript}>
<Icon name="play" /> Run
</button>
<div class="field inline">
<button class="button secondary" on:click={loadScript}>
<Icon name="upload" /> Load script…
</button>
<button class="button secondary" on:click={saveScript}>
<Icon name="save" /> Save as…
</button>
</div>
<button class="button viewtoggle" title="Toggle horizontal/vertical view" on:click={toggleView}>
<Icon name="columns" rotation={horizontal ? 90 : 0} />
</button>
{#key result}
<div class="status flash-green">
{#if result?.status}
Exit code: {result.status}
{/if}
</div>
{/key}
</div>
</div>
<style>
.shell {
display: grid;
grid-template: 1fr auto / 1fr 1fr;
}
.shell.horizontal {
grid-template: 1fr 1fr auto / 1fr;
}
.overflow {
overflow: auto;
}
.field {
height: 100%;
}
.field :global(.editor) {
border-radius: 0;
}
.output {
background-color: #2e3027;
color: #fff;
overflow: auto;
display: flex;
}
.output :global(*) {
color: #fff;
}
.output pre {
font-family: monospace;
padding: 0.5rem;
user-select: text;
-webkit-user-select: text;
cursor: text;
}
.output pre .error {
color: #ff8989;
margin-top: 2px;
}
.output :global(.blankstate) {
margin: auto;
padding: 0.5rem;
}
.controls {
margin-top: 0.5rem;
display: flex;
gap: 0.2rem;
align-items: center;
grid-column: 1 / 3;
}
.controls .viewtoggle {
margin-left: auto;
}
.shell.horizontal .controls {
grid-column: 1;
}
</style>

View File

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

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

@ -10,6 +10,10 @@ export function AddHost(arg1:string):Promise<string>;
export function Aggregate(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string):Promise<void>;
export function AskConfirmation(arg1:string):Promise<boolean>;
export function CountItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise<app.CountItemsResult>;
export function CreateIndex(arg1:string,arg2:string,arg3:string,arg4:string):Promise<string>;
export function DropCollection(arg1:string,arg2:string,arg3:string):Promise<boolean>;
@ -18,8 +22,12 @@ export function DropDatabase(arg1:string,arg2:string):Promise<boolean>;
export function DropIndex(arg1:string,arg2:string,arg3:string,arg4:string):Promise<boolean>;
export function DuplicateCollection(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string,arg6:string):Promise<boolean>;
export function Environment():Promise<app.EnvironmentInfo>;
export function ExecuteShellScript(arg1:string,arg2:string,arg3:string,arg4:string):Promise<app.ExecuteShellScriptResult>;
export function FindItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise<app.FindItemsResult>;
export function GetIndexes(arg1:string,arg2:string,arg3:string):Promise<app.GetIndexesResult>;
@ -38,6 +46,8 @@ export function OpenConnection(arg1:string):Promise<app.OpenConnectionResult>;
export function OpenDatabase(arg1:string,arg2:string):Promise<app.OpenDatabaseResult>;
export function OpenShellScript():Promise<string>;
export function PerformDump(arg1:string):Promise<boolean>;
export function PerformFindExport(arg1:string,arg2:string,arg3:string,arg4:string):Promise<boolean>;
@ -60,6 +70,8 @@ export function ReportSharedStateVariable(arg1:string,arg2:string):Promise<void>
export function SaveQuery(arg1:string):Promise<string>;
export function SaveShellScript(arg1:string,arg2:string,arg3:string,arg4:string,arg5:boolean):Promise<app.SaveShellScriptResult>;
export function SavedQueries():Promise<map[string]app.SavedQuery>;
export function Settings():Promise<app.Settings>;

View File

@ -10,6 +10,14 @@ export function Aggregate(arg1, arg2, arg3, arg4, arg5) {
return window['go']['app']['App']['Aggregate'](arg1, arg2, arg3, arg4, arg5);
}
export function AskConfirmation(arg1) {
return window['go']['app']['App']['AskConfirmation'](arg1);
}
export function CountItems(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['CountItems'](arg1, arg2, arg3, arg4);
}
export function CreateIndex(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['CreateIndex'](arg1, arg2, arg3, arg4);
}
@ -26,10 +34,18 @@ export function DropIndex(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['DropIndex'](arg1, arg2, arg3, arg4);
}
export function DuplicateCollection(arg1, arg2, arg3, arg4, arg5, arg6) {
return window['go']['app']['App']['DuplicateCollection'](arg1, arg2, arg3, arg4, arg5, arg6);
}
export function Environment() {
return window['go']['app']['App']['Environment']();
}
export function ExecuteShellScript(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['ExecuteShellScript'](arg1, arg2, arg3, arg4);
}
export function FindItems(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['FindItems'](arg1, arg2, arg3, arg4);
}
@ -66,6 +82,10 @@ export function OpenDatabase(arg1, arg2) {
return window['go']['app']['App']['OpenDatabase'](arg1, arg2);
}
export function OpenShellScript() {
return window['go']['app']['App']['OpenShellScript']();
}
export function PerformDump(arg1) {
return window['go']['app']['App']['PerformDump'](arg1);
}
@ -110,6 +130,10 @@ export function SaveQuery(arg1) {
return window['go']['app']['App']['SaveQuery'](arg1);
}
export function SaveShellScript(arg1, arg2, arg3, arg4, arg5) {
return window['go']['app']['App']['SaveShellScript'](arg1, arg2, arg3, arg4, arg5);
}
export function SavedQueries() {
return window['go']['app']['App']['SavedQueries']();
}

View File

@ -25,6 +25,7 @@ type EnvironmentInfo struct {
HasMongoExport bool `json:"hasMongoExport"`
HasMongoDump bool `json:"hasMongoDump"`
HasMongoShell bool `json:"hasMongoShell"`
HomeDirectory string `json:"homeDirectory"`
DataDirectory string `json:"dataDirectory"`
@ -49,6 +50,9 @@ func NewApp(version string) *App {
_, err = exec.LookPath("mongoexport")
a.Env.HasMongoExport = err == nil
_, err = exec.LookPath("mongosh")
a.Env.HasMongoShell = err == nil
a.Env.HomeDirectory, err = os.UserHomeDir()
if err != nil {
panic(errors.New("encountered an error while getting home directory"))
@ -137,3 +141,28 @@ func (a *App) ReportSharedStateVariable(key, value string) {
a.State.Store(key, value)
wailsRuntime.LogDebug(a.ctx, fmt.Sprintf("State: %s=\"%s\"", key, value))
}
func (a *App) AskConfirmation(message string) bool {
var title string = ""
if runtime.GOOS == "darwin" {
title = message
message = ""
} else {
title = "Confirm"
}
button, err := wailsRuntime.MessageDialog(a.ctx, wailsRuntime.MessageDialogOptions{
Type: wailsRuntime.QuestionDialog,
Title: title,
Message: message,
Buttons: []string{"Yes", "No"},
DefaultButton: "Yes",
CancelButton: "No",
})
if err == nil {
return button == "Yes"
} else {
return false
}
}

View File

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

View File

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

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

@ -0,0 +1,184 @@
package app
import (
"fmt"
"net/url"
"os"
"os/exec"
"path"
"strings"
"github.com/google/uuid"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
type ExecuteShellScriptResult struct {
Output string `json:"output"`
Stderr string `json:"stderr"`
Status int `json:"status"`
ErrorTitle string `json:"errorTitle"`
ErrorDescription string `json:"errorDescription"`
}
type SaveShellScriptResult struct {
Host Host `json:"host"`
Fname string `json:"filename"`
ErrorTitle string `json:"errorTitle"`
ErrorDescription string `json:"errorDescription"`
}
func (a *App) ExecuteShellScript(hostKey, dbKey, collKey, script string) (result ExecuteShellScriptResult) {
if !a.Env.HasMongoShell {
result.ErrorTitle = "mongosh not found"
result.ErrorDescription = "The mongosh executable is required to run a shell script. Please see https://www.mongodb.com/docs/mongodb-shell/install/"
return
}
saveRes := a.SaveShellScript(hostKey, dbKey, collKey, script, true)
if (saveRes.ErrorTitle != "") || (saveRes.ErrorDescription != "") {
result.ErrorTitle = saveRes.ErrorTitle
result.ErrorDescription = saveRes.ErrorDescription
return
}
var outbuf, errbuf strings.Builder
cmd := exec.Command("mongosh", "--file", saveRes.Fname, saveRes.Host.URI)
cmd.Stdout = &outbuf
cmd.Stderr = &errbuf
err := cmd.Run()
if exiterr, ok := err.(*exec.ExitError); ok {
result.Status = exiterr.ExitCode()
} else if err != nil {
runtime.LogWarningf(a.ctx, "Shell: failed to execute: mongosh --file %s: %s", saveRes.Fname, err.Error())
result.ErrorTitle = "mongosh failure"
result.ErrorDescription = err.Error()
return
} else {
result.Status = 0
}
os.Remove(saveRes.Fname)
result.Output = outbuf.String()
result.Stderr = errbuf.String()
return
}
func (a *App) SaveShellScript(hostKey, dbKey, collKey, script string, temp bool) (result SaveShellScriptResult) {
hosts, err := a.Hosts()
if err != nil {
runtime.LogWarningf(a.ctx, "Shell: could not get hosts: %s", err.Error())
result.ErrorTitle = "Could not get hosts"
result.ErrorDescription = err.Error()
return
}
host, hostFound := hosts[hostKey]
if !hostFound {
runtime.LogWarningf(a.ctx, "Shell: host %s does not exist", host)
result.ErrorTitle = "The specified host does not seem to exist"
return
}
result.Host = host
id, err := uuid.NewRandom()
if err != nil {
runtime.LogErrorf(a.ctx, "Shell: failed to generate a UUID: %s", err.Error())
result.ErrorTitle = "Could not generate UUID"
result.ErrorDescription = err.Error()
return
}
if temp {
dirname, err := os.MkdirTemp(os.TempDir(), "rolens-script")
if err != nil {
runtime.LogErrorf(a.ctx, "Shell: failed to create temporary directory: %s", err.Error())
result.ErrorTitle = "Could not generate temporary directory for script"
result.ErrorDescription = err.Error()
return
}
result.Fname = path.Join(dirname, fmt.Sprintf("%s.mongosh.js", id.String()))
} else {
result.Fname, err = runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
DefaultFilename: "New Script.js",
DefaultDirectory: path.Join(a.Env.DataDirectory, "Shell Scripts"),
Title: "Save MongoDB Shell Script",
CanCreateDirectories: true,
Filters: []runtime.FileFilter{
{
DisplayName: "MongoDB Shell Script (*.js)",
Pattern: "*.js",
},
},
})
if err != nil {
runtime.LogErrorf(a.ctx, "Shell: failed to save script: %s", err.Error())
result.ErrorTitle = "Could not save shell script"
result.ErrorDescription = err.Error()
return
}
}
scriptHeader := fmt.Sprintf("// Namespace: %s.%s\n", dbKey, collKey)
if dbKey != "" {
url, err := url.Parse(host.URI)
if err != nil {
runtime.LogWarningf(a.ctx, "Shell: failed to parse host URI %s: %s", host.URI, err.Error())
result.ErrorTitle = "Could parse host URI"
result.ErrorDescription = err.Error()
return
}
url.Path = "/" + dbKey
scriptHeader = scriptHeader + fmt.Sprintf("db = connect('%s');\n", url.String())
}
if collKey != "" {
scriptHeader = scriptHeader + fmt.Sprintf("coll = db.getCollection('%s');\n", collKey)
}
scriptHeader = scriptHeader + "\n"
script = scriptHeader + strings.TrimLeft(strings.TrimRight(script, " \t\n"), "\n")
if err := os.WriteFile(result.Fname, []byte(script), os.ModePerm); err != nil {
runtime.LogWarningf(a.ctx, "Shell: failed to write script to %s: %s", result.Fname, err.Error())
result.ErrorTitle = "Could not create temporary script file"
result.ErrorDescription = err.Error()
return
}
return
}
func (a *App) OpenShellScript() string {
dir := path.Join(a.Env.DataDirectory, "Shell Scripts")
os.MkdirAll(dir, os.ModePerm)
fname, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
DefaultDirectory: path.Join(a.Env.DataDirectory, "Shell Scripts"),
Title: "Load a MongoDB Shell Script",
CanCreateDirectories: true,
Filters: []runtime.FileFilter{
{
DisplayName: "MongoDB Shell Script (*.js)",
Pattern: "*.js",
},
},
})
if err != nil {
runtime.LogWarningf(a.ctx, "Shell: error opening script: %s", err.Error())
return ""
}
script, err := os.ReadFile(fname)
if err != nil {
runtime.LogWarningf(a.ctx, "Shell: error reading script %s: %s", fname, err.Error())
return ""
}
return string(script)
}

View File

@ -39,10 +39,11 @@ navigationOptions:
{{ content }}
{% if stub %}
<div class="wipdoc">
<div class="stubmessage">
<p>
Oops… this Rolens documentation page is a work-in-progress! You could
<a href="{{ github.pageSourceUrl }}" target="_blank">help writing it</a> — thanks!
This Rolens documentation page is a work-in-progress! You could
<a href="{{ github.pageSourceUrl }}" target="_blank">help writing it</a>
by sending a PR — thanks!
</p>
</div>
{% endif %}

View File

@ -262,6 +262,16 @@ article {
}
}
.stubmessage {
padding: 1rem;
border: 1px solid #aeae27;
border-radius: 0.75rem;
background-color: rgba(255, 255, 0, 0.235);
}
.stubmessage > :last-child {
margin-bottom: 0;
}
footer {
margin-top: 3rem;
padding-top: 1rem;