next (#1077)
This commit is contained in:
commit
c56b4f3855
|
|
@ -18,6 +18,7 @@ Please put a X in the boxes as evidence of reading through the checklist.
|
|||
- [ ] I followed the "1 logical change" rule.
|
||||
- [ ] I have forked the project, and raised this PR on a feature branch.
|
||||
- [ ] I ran the `pre-commit` hooks, and my commit message was validated.
|
||||
- [ ] I installed the pre-push hook (`pre-commit install --hook-type pre-push`) and integration tests pass on push when relevant files changed.
|
||||
- [ ] `make -wC service compile` runs without any issues.
|
||||
- [ ] `make -wC service codestyle` runs without any issues.
|
||||
- [ ] `make -wC service unittests` runs without any issues.
|
||||
|
|
|
|||
|
|
@ -156,8 +156,10 @@ jobs:
|
|||
if: steps.release.outputs.new_release_published == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: unsigned-windows-zip
|
||||
# Upload as-is so SignPath receives OliveTin-windows-amd64.zip, not a wrapper zip.
|
||||
# With archive: false, the artifact name is the filename (`name` is ignored).
|
||||
path: dist/OliveTin-windows-amd64.zip
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload unsigned Windows MSI for SignPath
|
||||
|
|
@ -165,8 +167,8 @@ jobs:
|
|||
if: steps.release.outputs.new_release_published == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: unsigned-windows-msi
|
||||
path: dist/OliveTin-windows-amd64.msi
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Archive binaries
|
||||
|
|
@ -205,7 +207,7 @@ jobs:
|
|||
fi
|
||||
done
|
||||
if [[ "${missing}" -ne 0 ]]; then
|
||||
echo "Windows signing is required before a draft release can be published. Configure SignPath secrets/vars (see docs/modules/dev/pages/signing.adoc)." >&2
|
||||
echo "SignPath secrets/vars are required to upload signed Windows assets. Configure them (see docs/modules/dev/pages/signing.adoc)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -219,6 +221,8 @@ jobs:
|
|||
artifact-configuration-slug: windows-zip
|
||||
github-artifact-id: ${{ needs.build.outputs.windows_zip_artifact_id }}
|
||||
wait-for-completion: true
|
||||
# Preserve the signed .zip/.msi files; default decompress would unpack the zip.
|
||||
skip-decompress: true
|
||||
output-artifact-directory: signed-windows-zip
|
||||
|
||||
- name: Sign Windows MSI
|
||||
|
|
@ -231,9 +235,10 @@ jobs:
|
|||
artifact-configuration-slug: windows-msi
|
||||
github-artifact-id: ${{ needs.build.outputs.windows_msi_artifact_id }}
|
||||
wait-for-completion: true
|
||||
skip-decompress: true
|
||||
output-artifact-directory: signed-windows-msi
|
||||
|
||||
- name: Publish signed Windows assets and undraft release
|
||||
- name: Upload signed Windows assets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ before:
|
|||
- make windows-resources VERSION={{ .Version }}
|
||||
|
||||
builds:
|
||||
# id defaults to project_name (OliveTin) — required for notarize.macos defaults.
|
||||
- skip: '{{ isEnvSet "GORELEASER_SKIP_BUILD" }}'
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
|
|
@ -14,7 +15,6 @@ builds:
|
|||
dir: service
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
- freebsd
|
||||
|
||||
|
|
@ -33,19 +33,31 @@ builds:
|
|||
- goos: darwin
|
||||
goarch: arm # Mac does not work on [32bit] arm
|
||||
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
|
||||
- goos: windows # Does anyone use Windows on arm64?
|
||||
goarch: arm64
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{ .CommitDate }}
|
||||
|
||||
# Built and archived for SignPath, but excluded from the GitHub release upload.
|
||||
- id: windows
|
||||
skip: '{{ isEnvSet "GORELEASER_SKIP_BUILD" }}'
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
binary: OliveTin
|
||||
main: .
|
||||
dir: service
|
||||
goos:
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{ .CommitDate }}
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
extra_files:
|
||||
- glob: ./dist/OliveTin-windows-amd64.msi
|
||||
# Exclude the windows archive; signed zip/MSI checksums are added after SignPath.
|
||||
ids:
|
||||
- default
|
||||
- openrc
|
||||
|
||||
snapshot:
|
||||
version_template: "{{ .Branch }}-{{ .ShortCommit }}"
|
||||
changelog:
|
||||
|
|
@ -73,7 +85,26 @@ changelog:
|
|||
- '^Merge branch'
|
||||
|
||||
archives:
|
||||
- formats: tar.gz
|
||||
- id: default
|
||||
ids:
|
||||
- OliveTin
|
||||
formats: tar.gz
|
||||
files:
|
||||
- config.yaml
|
||||
- LICENSE
|
||||
- README.md
|
||||
- src: Dockerfile.singlearch
|
||||
dst: Dockerfile
|
||||
- examples/backupScript.sh
|
||||
- webui
|
||||
- var
|
||||
name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{ .Arm }}"
|
||||
wrap_in_directory: true
|
||||
|
||||
- id: windows
|
||||
ids:
|
||||
- windows
|
||||
formats: zip
|
||||
files:
|
||||
- config.yaml
|
||||
- LICENSE
|
||||
|
|
@ -85,9 +116,6 @@ archives:
|
|||
- var
|
||||
name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{ .Arm }}"
|
||||
wrap_in_directory: true
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats: zip
|
||||
|
||||
# dockers_v2: single multi-platform build with buildx; keeps default provenance + SBOM.
|
||||
# Replaces legacy dockers + docker_manifests (avoids "is a manifest list" when attestations are on).
|
||||
|
|
@ -187,8 +215,7 @@ nfpms:
|
|||
notarize:
|
||||
macos:
|
||||
- enabled: '{{ isEnvSet "MACOS_SIGN_P12" }}'
|
||||
ids:
|
||||
- default
|
||||
# Defaults to project_name when omitted; must match builds[].id (also project_name).
|
||||
sign:
|
||||
certificate: "{{.Env.MACOS_SIGN_P12}}"
|
||||
password: "{{.Env.MACOS_SIGN_PASSWORD}}"
|
||||
|
|
@ -200,10 +227,11 @@ notarize:
|
|||
timeout: 30m
|
||||
|
||||
release:
|
||||
# Stay draft until the sign-windows job replaces unsigned Windows assets and undrafts.
|
||||
draft: true
|
||||
extra_files:
|
||||
- glob: ./dist/OliveTin-windows-amd64.msi
|
||||
# Publish immediately without Windows assets; sign-windows uploads signed zip/MSI later.
|
||||
draft: false
|
||||
ids:
|
||||
- default
|
||||
- openrc
|
||||
footer: |
|
||||
## Container images (from GitHub)
|
||||
|
||||
|
|
|
|||
|
|
@ -38,32 +38,33 @@ repos:
|
|||
entry: make service-codestyle
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
files: ^(service/|proto/|lang/|Makefile)
|
||||
|
||||
- id: frontend-codestyle
|
||||
name: frontend-codestyle
|
||||
entry: make frontend-codestyle
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
files: ^(frontend/|Makefile)
|
||||
|
||||
- id: service-unittests
|
||||
name: service-unittests
|
||||
entry: make service-unittests
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
files: ^(service/|proto/|lang/|Makefile)
|
||||
|
||||
- id: service-build
|
||||
name: service-build
|
||||
entry: make service
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
files: ^(service/|proto/|lang/|Makefile)
|
||||
|
||||
- id: it
|
||||
name: integration-tests
|
||||
entry: make it
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
stages: [pre-push]
|
||||
files: ^(service/|frontend/|integration-tests/|proto/|Makefile)
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ choco install git go protoc make python nodejs-lts -y && pip install pre-commit
|
|||
git clone https://github.com/OliveTin/OliveTin.git
|
||||
cd OliveTin
|
||||
pre-commit install
|
||||
pre-commit install --hook-type pre-push
|
||||
|
||||
# Step3: compile binary for current dev env (OS, ARCH)
|
||||
# `make proto` will also run `make go-tools`, which installs "buf". This binary
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ defaultPermissions:
|
|||
logs: true
|
||||
----
|
||||
|
||||
In the example above, all users will start off with the permissions to only see action logs - but will not be able to view or execute actions.
|
||||
In the example above, all users will start off with the permissions to only see action logs - but will not be able to view or execute actions.
|
||||
|
||||
It is then possible to add an "admins" ACL on top of every action. In the example below, we define one extra ACL called "admins", which matches any users with the usergroup also called "admins". This ACL will then be applied to all actions, and will allow users in the "admins" usergroup to view and execute the action.
|
||||
|
||||
|
|
@ -116,6 +116,48 @@ accessControlLists:
|
|||
addToEveryAction: true
|
||||
```
|
||||
|
||||
== ACLs and Dashboards
|
||||
|
||||
Root dashboards can also list `acls`. This controls whether the **whole dashboard page** is visible (including `display` widgets and entity fieldsets), not just action buttons.
|
||||
|
||||
* If a dashboard has **no** `acls` (or an empty list), it is unrestricted — anyone can see it in the side menu (subject to the usual “empty dashboard” hiding).
|
||||
* If a dashboard lists one or more `acls`, access uses the same allow-list rules as actions: a matching ACL that grants `view`, otherwise `defaultPermissions.view`.
|
||||
* `addToEveryAction` does **not** apply to dashboards. List the ACL on the dashboard explicitly when you want to restrict it.
|
||||
* Nested fieldsets and directories do not have their own `acls`; the root dashboard decision covers the whole page.
|
||||
|
||||
Action `acls` still control individual buttons. Use dashboard `acls` when you need to hide a page that contains status or other non-action content from some users.
|
||||
|
||||
[source,yaml]
|
||||
.`config.yaml`
|
||||
----
|
||||
defaultPermissions:
|
||||
view: false
|
||||
exec: false
|
||||
|
||||
accessControlLists:
|
||||
- name: admins
|
||||
matchUsergroups:
|
||||
- admins
|
||||
permissions:
|
||||
view: true
|
||||
exec: true
|
||||
|
||||
dashboards:
|
||||
- title: Public tools
|
||||
contents:
|
||||
- title: Welcome
|
||||
type: display
|
||||
|
||||
- title: Services
|
||||
acls:
|
||||
- admins
|
||||
contents:
|
||||
- title: 'Status: running'
|
||||
type: display
|
||||
----
|
||||
|
||||
In the example above, guests can open **Public tools**, but **Services** is hidden from the side menu and cannot be loaded by deep link.
|
||||
|
||||
== ACL Matching - usernames and usergroups.
|
||||
|
||||
You can match users based on their usergroup which is the most common, but it is also possible to match based on the user's username.
|
||||
|
|
@ -149,4 +191,3 @@ Now that you understand ACLs, here's how to implement them:
|
|||
* xref:security/local.adoc[Set up local users] - Create users for ACL matching
|
||||
* xref:security/oauth2.adoc[Configure OAuth2] - Set up OAuth2 for user groups
|
||||
* xref:security/design_choices.adoc[Security design recommendations] - Learn best practices for ACL design
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
OliveTin signs release binaries on two platforms:
|
||||
|
||||
* **macOS** — Developer ID + notarization via [quill](https://github.com/anchore/quill) inside GoReleaser (optional only when `MACOS_SIGN_P12` is unset).
|
||||
* **Windows** — Authenticode via [SignPath Foundation](https://signpath.org/) in a separate GitHub Actions job (required before a draft release is published).
|
||||
* **Windows** — Authenticode via [SignPath Foundation](https://signpath.org/) in a separate GitHub Actions job (signed zip/MSI are uploaded after the release is published).
|
||||
|
||||
## macOS release signing
|
||||
|
||||
|
|
@ -117,14 +117,14 @@ A signed and notarized binary should report `accepted` with `source=Notarized De
|
|||
|
||||
Windows Authenticode signing uses [SignPath Foundation](https://signpath.org/) (free for qualifying open-source projects). It does **not** use GoReleaser Pro.
|
||||
|
||||
GoReleaser creates a **draft** GitHub release with unsigned Windows assets. A separate `sign-windows` job submits those assets to SignPath, replaces them on the draft (including updated `checksums.txt`), then publishes the release.
|
||||
GoReleaser publishes a GitHub release **without** Windows zip/MSI assets. A separate `sign-windows` job submits the unsigned Windows files (as workflow artifacts) to SignPath, then uploads the signed zip/MSI and updates `checksums.txt` on the already-published release.
|
||||
|
||||
Signed artifacts:
|
||||
|
||||
* `OliveTin.exe` inside `OliveTin-windows-amd64.zip`
|
||||
* nested `OliveTin.exe` and the `OliveTin-windows-amd64.msi` installer (deep signing)
|
||||
|
||||
Signing is **required** to publish. If SignPath secrets/vars are missing, `sign-windows` fails and the draft stays unpublished.
|
||||
If SignPath secrets/vars are missing or signing fails, the release still publishes; Windows assets are simply missing until a successful `sign-windows` run. Install URLs for the Windows zip may 404 until signing finishes.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
|
|
@ -168,9 +168,9 @@ In **Settings → Secrets and variables → Actions**:
|
|||
|
||||
On a new semantic-release from `main`:
|
||||
|
||||
1. GoReleaser publishes container images and creates a **draft** GitHub release (including unsigned Windows zip/MSI).
|
||||
2. The build job uploads those Windows files as GitHub Actions artifacts.
|
||||
3. The `sign-windows` job submits each artifact to SignPath, waits for completion, then runs `var/windows/signpath-publish-signed.sh` to clobber-upload signed files, refresh `checksums.txt`, and undraft the release.
|
||||
1. GoReleaser publishes container images and a GitHub release **without** Windows zip/MSI assets (those are built locally for SignPath only).
|
||||
2. The build job uploads the unsigned Windows files as GitHub Actions artifacts.
|
||||
3. The `sign-windows` job submits each artifact to SignPath, waits for completion, then runs `var/windows/signpath-publish-signed.sh` to upload the signed files and refresh `checksums.txt`.
|
||||
|
||||
All jobs in this chain use GitHub-hosted runners (required by SignPath for OSS projects).
|
||||
|
||||
|
|
@ -188,7 +188,7 @@ signtool verify /pa OliveTin-windows-amd64.msi
|
|||
|
||||
### Configuration reference
|
||||
|
||||
- Draft release: `release.draft: true` in link:https://github.com/OliveTin/OliveTin/blob/main/.goreleaser.yml[`.goreleaser.yml`]
|
||||
- Release publish (no Windows assets initially): `release.draft: false` and `release.ids` in link:https://github.com/OliveTin/OliveTin/blob/main/.goreleaser.yml[`.goreleaser.yml`]
|
||||
- CI job: `sign-windows` in link:https://github.com/OliveTin/OliveTin/blob/main/.github/workflows/build-and-release.yml[`.github/workflows/build-and-release.yml`]
|
||||
- Publish helper: link:https://github.com/OliveTin/OliveTin/blob/main/var/windows/signpath-publish-signed.sh[`var/windows/signpath-publish-signed.sh`]
|
||||
- SignPath artifact configs (reference only): link:https://github.com/OliveTin/OliveTin/blob/main/docs/modules/dev/signpath/windows-zip.xml[`signpath/windows-zip.xml`], link:https://github.com/OliveTin/OliveTin/blob/main/docs/modules/dev/signpath/windows-msi.xml[`signpath/windows-msi.xml`]
|
||||
|
|
|
|||
|
|
@ -4,12 +4,22 @@
|
|||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/recommended",
|
||||
"standard"
|
||||
],
|
||||
"parser": "vue-eslint-parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
"sourceType": "module",
|
||||
"parser": {
|
||||
"js": "espree"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/no-v-html": "warn",
|
||||
"no-tabs": "off",
|
||||
"no-mixed-spaces-and-tabs": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ endef
|
|||
|
||||
codestyle:
|
||||
npm install
|
||||
npx eslint --fix main.js js/*
|
||||
npx eslint --fix main.js js/* resources/vue
|
||||
npx stylelint style.css
|
||||
|
||||
clean:
|
||||
|
|
@ -18,4 +18,4 @@ build:
|
|||
|
||||
dist: deps clean build
|
||||
|
||||
.PHONY: codestyle
|
||||
.PHONY: codestyle
|
||||
|
|
|
|||
|
|
@ -11,38 +11,39 @@
|
|||
"dependencies": {
|
||||
"@connectrpc/connect": "^2.1.2",
|
||||
"@connectrpc/connect-web": "^2.1.2",
|
||||
"@hugeicons/core-free-icons": "^4.2.1",
|
||||
"@hugeicons/vue": "^1.0.6",
|
||||
"@hugeicons/core-free-icons": "^4.2.2",
|
||||
"@hugeicons/vue": "^1.0.7",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"iconify-icon": "^3.0.2",
|
||||
"picocrank": "^1.17.0",
|
||||
"picocrank": "^1.18.2",
|
||||
"standard": "^17.1.2",
|
||||
"unplugin-vue-components": "^32.1.0",
|
||||
"vite": "^8.0.16",
|
||||
"vue": "^3.5.38",
|
||||
"vite": "^8.1.3",
|
||||
"vue": "^3.5.39",
|
||||
"vue-i18n": "^11.4.6",
|
||||
"vue-router": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-plugin-vue": "^10.9.2",
|
||||
"process": "^0.11.10",
|
||||
"stylelint": "^17.13.0",
|
||||
"stylelint": "^17.14.0",
|
||||
"stylelint-config-standard": "^40.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.28.5",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
|
|
@ -409,20 +410,20 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
|
||||
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"@emnapi/wasi-threads": "1.2.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
||||
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
|
|
@ -430,9 +431,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
|
|
@ -941,15 +942,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@hugeicons/core-free-icons": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.2.1.tgz",
|
||||
"integrity": "sha512-75jYZKYyA9VwS35YRmmGUGzFedbY+Fl0Vxx5FzXR2CGDlIhNRumFeVqaaKoClf2MeYEJwPAVMEL9RwCYtOgnSw==",
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.2.2.tgz",
|
||||
"integrity": "sha512-fjQW3p6cwoJmwK3oCnV8Z3PC5sHihDvr2jkAHax8cxqp62GaV/D7B/Rnao+JwicW8fmLZUQ6QrzWfoyMFOEQlQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@hugeicons/vue": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@hugeicons/vue/-/vue-1.0.6.tgz",
|
||||
"integrity": "sha512-T1Wbuk2qDdoE2xcY3CDtwMOKewwIYDE7JoMVLeuAV4cbOx5CFtToHCQAmLgVPIFGGZUsV2nN52lBkts9upvePw==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@hugeicons/vue/-/vue-1.0.7.tgz",
|
||||
"integrity": "sha512-oUE0ztUNYbsMFpMgbV3bsxQBObNkeouomUThFuFPzYC5eV++VTYonStsltJu2MoLMkxV8mzpoVvIbuMPBZSN0w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^2.6.0 || ^3.0.0"
|
||||
|
|
@ -1110,13 +1111,13 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
|
||||
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
|
||||
"integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@tybys/wasm-util": "^0.10.2"
|
||||
"@tybys/wasm-util": "^0.10.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
|
|
@ -1160,18 +1161,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.133.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
||||
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
||||
"version": "0.139.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
|
||||
"integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
|
||||
"integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1185,9 +1186,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
|
||||
"integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1201,9 +1202,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
|
||||
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
|
||||
"integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1217,9 +1218,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
|
||||
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
|
||||
"integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1233,9 +1234,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
|
||||
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
|
||||
"integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -1249,9 +1250,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
|
||||
"integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1265,9 +1266,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
|
||||
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
|
||||
"integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1281,9 +1282,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
|
||||
"integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
|
|
@ -1297,9 +1298,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
|
||||
"integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
|
|
@ -1313,9 +1314,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
|
||||
"integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1329,9 +1330,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
|
||||
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
|
||||
"integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1345,9 +1346,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
|
||||
"integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1361,27 +1362,27 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
|
||||
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
|
||||
"integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "1.10.0",
|
||||
"@emnapi/runtime": "1.10.0",
|
||||
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||
"@emnapi/core": "1.11.1",
|
||||
"@emnapi/runtime": "1.11.1",
|
||||
"@napi-rs/wasm-runtime": "^1.1.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
|
||||
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
|
||||
"integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1395,9 +1396,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
|
||||
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
|
||||
"integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1436,15 +1437,31 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
|
||||
"integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/esrecurse": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
||||
"integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/jsesc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz",
|
||||
|
|
@ -1507,39 +1524,39 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.38.tgz",
|
||||
"integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==",
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz",
|
||||
"integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@vue/shared": "3.5.38",
|
||||
"@vue/shared": "3.5.39",
|
||||
"entities": "^7.0.1",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz",
|
||||
"integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==",
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz",
|
||||
"integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.38",
|
||||
"@vue/shared": "3.5.38"
|
||||
"@vue/compiler-core": "3.5.39",
|
||||
"@vue/shared": "3.5.39"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz",
|
||||
"integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==",
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz",
|
||||
"integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@vue/compiler-core": "3.5.38",
|
||||
"@vue/compiler-dom": "3.5.38",
|
||||
"@vue/compiler-ssr": "3.5.38",
|
||||
"@vue/shared": "3.5.38",
|
||||
"@vue/compiler-core": "3.5.39",
|
||||
"@vue/compiler-dom": "3.5.39",
|
||||
"@vue/compiler-ssr": "3.5.39",
|
||||
"@vue/shared": "3.5.39",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"postcss": "^8.5.15",
|
||||
|
|
@ -1547,13 +1564,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz",
|
||||
"integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==",
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz",
|
||||
"integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.38",
|
||||
"@vue/shared": "3.5.38"
|
||||
"@vue/compiler-dom": "3.5.39",
|
||||
"@vue/shared": "3.5.39"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
|
|
@ -1581,53 +1598,53 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.38.tgz",
|
||||
"integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==",
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz",
|
||||
"integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.38"
|
||||
"@vue/shared": "3.5.39"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.38.tgz",
|
||||
"integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==",
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz",
|
||||
"integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.38",
|
||||
"@vue/shared": "3.5.38"
|
||||
"@vue/reactivity": "3.5.39",
|
||||
"@vue/shared": "3.5.39"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.38.tgz",
|
||||
"integrity": "sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==",
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz",
|
||||
"integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.38",
|
||||
"@vue/runtime-core": "3.5.38",
|
||||
"@vue/shared": "3.5.38",
|
||||
"@vue/reactivity": "3.5.39",
|
||||
"@vue/runtime-core": "3.5.39",
|
||||
"@vue/shared": "3.5.39",
|
||||
"csstype": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.38.tgz",
|
||||
"integrity": "sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==",
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz",
|
||||
"integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.38",
|
||||
"@vue/shared": "3.5.38"
|
||||
"@vue/compiler-ssr": "3.5.39",
|
||||
"@vue/shared": "3.5.39"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.38"
|
||||
"vue": "3.5.39"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz",
|
||||
"integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==",
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz",
|
||||
"integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xterm/addon-fit": {
|
||||
|
|
@ -1949,6 +1966,13 @@
|
|||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
|
|
@ -2138,9 +2162,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cosmiconfig": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz",
|
||||
"integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==",
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz",
|
||||
"integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -2990,6 +3014,51 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-vue": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.9.2.tgz",
|
||||
"integrity": "sha512-4g7ZP3pYcuqd7Zp0pzUKcos0W+RkjBz4EGdhJ92FcYk6v03Ti/GK5NwjgsjxHK+98eXDbHeK7VtX1az7/8doZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
"nth-check": "^2.1.1",
|
||||
"postcss-selector-parser": "^7.1.0",
|
||||
"semver": "^7.6.3",
|
||||
"xml-name-validator": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0 || ^8.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"vue-eslint-parser": "^10.3.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@stylistic/eslint-plugin": {
|
||||
"optional": true
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-vue/node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
||||
|
|
@ -4897,6 +4966,19 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
|
@ -5186,19 +5268,19 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picocrank": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.17.0.tgz",
|
||||
"integrity": "sha512-EIUSI26elt6ulloN74CT2sX5tBxS2wjdV4A+eXHYNtHbH0hM2iKkYzpKKWOEr+yUTfrvjah6VuqieedFw/9R0w==",
|
||||
"version": "1.18.2",
|
||||
"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.18.2.tgz",
|
||||
"integrity": "sha512-YR1Y00KnEPjH6OCrijSxzTHoyHLhKrsPhkA7ubOkbvcUuD7wtVKbRxfPkjAkA838dGymjoG//iFca2oLnNT5tg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@hugeicons/core-free-icons": "^4.1.1",
|
||||
"@hugeicons/vue": "^1.0.5",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"@hugeicons/core-free-icons": "^4.2.2",
|
||||
"@hugeicons/vue": "^1.0.7",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"femtocrank": "^2.5.0",
|
||||
"unplugin-vue-components": "^32.0.0",
|
||||
"vite": "^8.0.10",
|
||||
"vue": "^3.5.33",
|
||||
"vue-router": "^5.0.6"
|
||||
"unplugin-vue-components": "^32.1.0",
|
||||
"vite": "^8.1.2",
|
||||
"vue": "^3.5.39",
|
||||
"vue-router": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
|
|
@ -5318,9 +5400,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||
"version": "8.5.16",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||||
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
|
@ -5373,9 +5455,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
|
||||
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz",
|
||||
"integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -5620,12 +5702,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
||||
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
|
||||
"integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.133.0",
|
||||
"@oxc-project/types": "=0.139.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -5635,21 +5717,21 @@
|
|||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.3",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.3",
|
||||
"@rolldown/binding-darwin-x64": "1.0.3",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.3",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.3",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.3",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.3",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.3",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.3"
|
||||
"@rolldown/binding-android-arm64": "1.1.5",
|
||||
"@rolldown/binding-darwin-arm64": "1.1.5",
|
||||
"@rolldown/binding-darwin-x64": "1.1.5",
|
||||
"@rolldown/binding-freebsd-x64": "1.1.5",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.1.5",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.1.5",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.1.5",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.1.5",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.1.5",
|
||||
"@rolldown/binding-linux-x64-musl": "1.1.5",
|
||||
"@rolldown/binding-openharmony-arm64": "1.1.5",
|
||||
"@rolldown/binding-wasm32-wasi": "1.1.5",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.1.5",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
|
|
@ -6151,9 +6233,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/stylelint": {
|
||||
"version": "17.13.0",
|
||||
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.13.0.tgz",
|
||||
"integrity": "sha512-G1WYzMerp7ihOaIe9VJCHLt12MoAD2QLf1AFerYP37+BCRBUK5UCpq8e/mN+zCIaJPKQcaxhE4WlPmqdiOx/gw==",
|
||||
"version": "17.14.0",
|
||||
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.0.tgz",
|
||||
"integrity": "sha512-8xkHPpdqYryeIsOgfsYTmr6cIeC4nLYWk5S8BPxpodq8mIuepggkMljsHewWfuAjj/+qpRKou2QerhjMH3iasg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -6169,13 +6251,13 @@
|
|||
"dependencies": {
|
||||
"@csstools/css-calc": "^3.2.1",
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.1.4",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.1.5",
|
||||
"@csstools/css-tokenizer": "^4.0.0",
|
||||
"@csstools/media-query-list-parser": "^5.0.0",
|
||||
"@csstools/selector-resolve-nested": "^4.0.0",
|
||||
"@csstools/selector-specificity": "^6.0.0",
|
||||
"colord": "^2.9.3",
|
||||
"cosmiconfig": "^9.0.1",
|
||||
"cosmiconfig": "^9.0.2",
|
||||
"css-functions-list": "^3.3.3",
|
||||
"css-tree": "^3.2.1",
|
||||
"debug": "^4.4.3",
|
||||
|
|
@ -6195,7 +6277,7 @@
|
|||
"picocolors": "^1.1.1",
|
||||
"postcss": "^8.5.15",
|
||||
"postcss-safe-parser": "^7.0.1",
|
||||
"postcss-selector-parser": "^7.1.1",
|
||||
"postcss-selector-parser": "^7.1.4",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
"string-width": "^8.2.1",
|
||||
"supports-hyperlinks": "^4.4.0",
|
||||
|
|
@ -6790,15 +6872,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.16",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
||||
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
||||
"version": "8.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz",
|
||||
"integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
"postcss": "^8.5.15",
|
||||
"rolldown": "1.0.3",
|
||||
"postcss": "^8.5.16",
|
||||
"rolldown": "~1.1.3",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -6815,7 +6897,7 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.1.18",
|
||||
"@vitejs/devtools": "^0.3.0",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
|
|
@ -6879,16 +6961,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.38.tgz",
|
||||
"integrity": "sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==",
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz",
|
||||
"integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.38",
|
||||
"@vue/compiler-sfc": "3.5.38",
|
||||
"@vue/runtime-dom": "3.5.38",
|
||||
"@vue/server-renderer": "3.5.38",
|
||||
"@vue/shared": "3.5.38"
|
||||
"@vue/compiler-dom": "3.5.39",
|
||||
"@vue/compiler-sfc": "3.5.39",
|
||||
"@vue/runtime-dom": "3.5.39",
|
||||
"@vue/server-renderer": "3.5.39",
|
||||
"@vue/shared": "3.5.39"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
|
|
@ -6899,6 +6981,98 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-eslint-parser": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.1.tgz",
|
||||
"integrity": "sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"eslint-scope": "^8.2.0 || ^9.0.0",
|
||||
"eslint-visitor-keys": "^4.2.0 || ^5.0.0",
|
||||
"espree": "^10.3.0 || ^11.0.0",
|
||||
"esquery": "^1.6.0",
|
||||
"semver": "^7.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mysticatea"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-eslint-parser/node_modules/eslint-scope": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
|
||||
"integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/esrecurse": "^4.3.1",
|
||||
"@types/estree": "^1.0.8",
|
||||
"esrecurse": "^4.3.0",
|
||||
"estraverse": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-eslint-parser/node_modules/espree": {
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
|
||||
"integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.16.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
"eslint-visitor-keys": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-eslint-parser/node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "11.4.6",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.6.tgz",
|
||||
|
|
@ -7145,6 +7319,16 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
|
||||
"integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@
|
|||
"repository": "https://github.com/OliveTin/OliveTin",
|
||||
"source": "index.html",
|
||||
"devDependencies": {
|
||||
"eslint-plugin-vue": "^10.9.2",
|
||||
"process": "^0.11.10",
|
||||
"stylelint": "^17.13.0",
|
||||
"stylelint": "^17.14.0",
|
||||
"stylelint-config-standard": "^40.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
@ -24,22 +25,22 @@
|
|||
"dependencies": {
|
||||
"@connectrpc/connect": "^2.1.2",
|
||||
"@connectrpc/connect-web": "^2.1.2",
|
||||
"@hugeicons/core-free-icons": "^4.2.1",
|
||||
"@hugeicons/vue": "^1.0.6",
|
||||
"@hugeicons/core-free-icons": "^4.2.2",
|
||||
"@hugeicons/vue": "^1.0.7",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"iconify-icon": "^3.0.2",
|
||||
"picocrank": "^1.17.0",
|
||||
"picocrank": "^1.18.2",
|
||||
"standard": "^17.1.2",
|
||||
"unplugin-vue-components": "^32.1.0",
|
||||
"vite": "^8.0.16",
|
||||
"vue": "^3.5.38",
|
||||
"vite": "^8.1.3",
|
||||
"vue": "^3.5.39",
|
||||
"vue-i18n": "^11.4.6",
|
||||
"vue-router": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,73 @@
|
|||
<template>
|
||||
<div :id="`actionButton-${bindingId}`" role="none" class="action-button" @contextmenu.prevent="openActionDetails">
|
||||
<span
|
||||
v-if="showExecutionIndicator"
|
||||
class="execution-indicator"
|
||||
:class="executionIndicatorClass"
|
||||
:title="executionIndicatorTitle"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<button :id="`actionButtonInner-${bindingId}`" :title="title" :disabled="!canExec || isDisabled"
|
||||
:class="combinedClasses" @click="handleClick">
|
||||
<div
|
||||
:id="`actionButton-${bindingId}`"
|
||||
role="none"
|
||||
class="action-button"
|
||||
@contextmenu.prevent="openActionDetails"
|
||||
>
|
||||
<span
|
||||
v-if="showExecutionIndicator"
|
||||
class="execution-indicator"
|
||||
:class="executionIndicatorClass"
|
||||
:title="executionIndicatorTitle"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<button
|
||||
:id="`actionButtonInner-${bindingId}`"
|
||||
:title="title"
|
||||
:disabled="!canExec || isDisabled"
|
||||
:class="combinedClasses"
|
||||
@click="handleClick"
|
||||
>
|
||||
<div
|
||||
v-if="showNavigateOnStartIcons"
|
||||
class="navigate-on-start-container"
|
||||
>
|
||||
<div
|
||||
v-if="navigateOnStart == 'pop'"
|
||||
class="navigate-on-start"
|
||||
title="Opens a popup dialog on start"
|
||||
>
|
||||
<HugeiconsIcon :icon="ComputerTerminal01Icon" />
|
||||
</div>
|
||||
<div
|
||||
v-if="navigateOnStart == 'arg'"
|
||||
class="navigate-on-start"
|
||||
title="Opens an argument form on start"
|
||||
>
|
||||
<HugeiconsIcon :icon="TypeCursorIcon" />
|
||||
</div>
|
||||
<div
|
||||
v-if="navigateOnStart == 'hist'"
|
||||
class="navigate-on-start"
|
||||
title="Opens action execution history on start"
|
||||
>
|
||||
<HugeiconsIcon :icon="WorkHistoryIcon" />
|
||||
</div>
|
||||
<div
|
||||
v-if="navigateOnStart == ''"
|
||||
class="navigate-on-start"
|
||||
title="Run in the background"
|
||||
>
|
||||
<HugeiconsIcon :icon="WorkoutRunIcon" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showNavigateOnStartIcons" class="navigate-on-start-container">
|
||||
<div v-if="navigateOnStart == 'pop'" class="navigate-on-start" title="Opens a popup dialog on start">
|
||||
<HugeiconsIcon :icon="ComputerTerminal01Icon" />
|
||||
</div>
|
||||
<div v-if="navigateOnStart == 'arg'" class="navigate-on-start" title="Opens an argument form on start">
|
||||
<HugeiconsIcon :icon="TypeCursorIcon" />
|
||||
</div>
|
||||
<div v-if="navigateOnStart == 'hist'" class="navigate-on-start" title="Opens action execution history on start">
|
||||
<HugeiconsIcon :icon="WorkHistoryIcon" />
|
||||
</div>
|
||||
<div v-if="navigateOnStart == ''" class="navigate-on-start" title="Run in the background">
|
||||
<HugeiconsIcon :icon="WorkoutRunIcon" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ActionIconGlyph class="icon" :glyph="actionGlyph" />
|
||||
<span class="title" aria-live="polite">{{ displayTitle }}
|
||||
</span>
|
||||
<span v-if="rateLimitMessage" class="rate-limit-message">{{ rateLimitMessage }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<ActionIconGlyph
|
||||
class="icon"
|
||||
:glyph="actionGlyph"
|
||||
/>
|
||||
<span
|
||||
class="title"
|
||||
aria-live="polite"
|
||||
>{{ displayTitle }}
|
||||
</span>
|
||||
<span
|
||||
v-if="rateLimitMessage"
|
||||
class="rate-limit-message"
|
||||
>{{ rateLimitMessage }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
|
@ -59,18 +96,18 @@ const navigateOnStart = ref('')
|
|||
|
||||
const props = defineProps({
|
||||
actionData: {
|
||||
type: Object,
|
||||
required: true
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
cssClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
prefilledArguments: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -84,7 +121,6 @@ const displayTitle = ref('')
|
|||
|
||||
// State
|
||||
const isDisabled = ref(false)
|
||||
const showArgumentForm = ref(false)
|
||||
|
||||
// Rate limiting
|
||||
const rateLimitExpires = ref(0)
|
||||
|
|
@ -99,7 +135,7 @@ const flashedTrackingIds = new Set()
|
|||
|
||||
// Show navigate on start icons - defaults to true if not set
|
||||
const showNavigateOnStartIcons = computed(() => {
|
||||
return window.initResponse?.showNavigateOnStartIcons ?? true
|
||||
return window.initResponse?.showNavigateOnStartIcons ?? true
|
||||
})
|
||||
|
||||
const actionGlyph = computed(() => props.actionData?.icon ?? '')
|
||||
|
|
@ -107,63 +143,63 @@ const glyph = ref('')
|
|||
|
||||
// Combined classes including custom cssClass
|
||||
const combinedClasses = computed(() => {
|
||||
const classes = [...buttonClasses.value]
|
||||
if (props.cssClass) {
|
||||
classes.push(props.cssClass)
|
||||
}
|
||||
return classes
|
||||
const classes = [...buttonClasses.value]
|
||||
if (props.cssClass) {
|
||||
classes.push(props.cssClass)
|
||||
}
|
||||
return classes
|
||||
})
|
||||
|
||||
const hasRunningInstance = computed(() => {
|
||||
const id = bindingId.value
|
||||
return !!(id && bindingExecutionState[id]?.hasRunning)
|
||||
const id = bindingId.value
|
||||
return !!(id && bindingExecutionState[id]?.hasRunning)
|
||||
})
|
||||
|
||||
const hasQueuedInstance = computed(() => {
|
||||
const id = bindingId.value
|
||||
return !!(id && bindingExecutionState[id]?.hasQueued)
|
||||
const id = bindingId.value
|
||||
return !!(id && bindingExecutionState[id]?.hasQueued)
|
||||
})
|
||||
|
||||
const showExecutionIndicator = computed(() => {
|
||||
return hasRunningInstance.value || hasQueuedInstance.value
|
||||
return hasRunningInstance.value || hasQueuedInstance.value
|
||||
})
|
||||
|
||||
const executionIndicatorClass = computed(() => {
|
||||
if (hasRunningInstance.value) {
|
||||
return 'execution-indicator-running'
|
||||
}
|
||||
if (hasQueuedInstance.value) {
|
||||
return 'execution-indicator-queued'
|
||||
}
|
||||
return ''
|
||||
if (hasRunningInstance.value) {
|
||||
return 'execution-indicator-running'
|
||||
}
|
||||
if (hasQueuedInstance.value) {
|
||||
return 'execution-indicator-queued'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const executionIndicatorTitle = computed(() => {
|
||||
if (hasRunningInstance.value) {
|
||||
return 'Running'
|
||||
}
|
||||
if (hasQueuedInstance.value) {
|
||||
return 'Queued'
|
||||
}
|
||||
return ''
|
||||
if (hasRunningInstance.value) {
|
||||
return 'Running'
|
||||
}
|
||||
if (hasQueuedInstance.value) {
|
||||
return 'Queued'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
function consumeAndFlashPendingResult () {
|
||||
const id = bindingId.value
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
const id = bindingId.value
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
|
||||
const pending = consumePendingBindingFlash(id)
|
||||
if (pending) {
|
||||
onExecutionFinished(pending)
|
||||
}
|
||||
const pending = consumePendingBindingFlash(id)
|
||||
if (pending) {
|
||||
onExecutionFinished(pending)
|
||||
}
|
||||
}
|
||||
|
||||
// Timestamps
|
||||
const updateIterationTimestamp = ref(0)
|
||||
|
||||
function constructFromJson(json) {
|
||||
function constructFromJson (json) {
|
||||
updateIterationTimestamp.value = 0
|
||||
|
||||
updateFromJson(json)
|
||||
|
|
@ -174,11 +210,11 @@ function constructFromJson(json) {
|
|||
popupOnStart.value = json.popupOnStart
|
||||
|
||||
if (popupOnStart.value.includes('execution-dialog')) {
|
||||
navigateOnStart.value = 'pop'
|
||||
navigateOnStart.value = 'pop'
|
||||
} else if (popupOnStart.value === 'history') {
|
||||
navigateOnStart.value = 'hist'
|
||||
navigateOnStart.value = 'hist'
|
||||
} else if (needsArgumentForm(props.actionData)) {
|
||||
navigateOnStart.value = 'arg'
|
||||
navigateOnStart.value = 'arg'
|
||||
}
|
||||
|
||||
isDisabled.value = !json.canExec
|
||||
|
|
@ -186,111 +222,117 @@ function constructFromJson(json) {
|
|||
glyph.value = json.icon ?? ''
|
||||
// Initialize rate limit from action data (parse datetime string)
|
||||
if (json.datetimeRateLimitExpires) {
|
||||
const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
|
||||
rateLimitExpires.value = date.getTime() / 1000
|
||||
const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
|
||||
rateLimitExpires.value = date.getTime() / 1000
|
||||
} else {
|
||||
rateLimitExpires.value = 0
|
||||
rateLimitExpires.value = 0
|
||||
}
|
||||
// Also initialize the store so the watch picks it up
|
||||
if (bindingId.value) {
|
||||
rateLimits[bindingId.value] = rateLimitExpires.value
|
||||
setBindingExecutionState(
|
||||
rateLimits[bindingId.value] = rateLimitExpires.value
|
||||
setBindingExecutionState(
|
||||
bindingId.value,
|
||||
!!json.hasRunningInstance,
|
||||
!!json.hasQueuedInstance
|
||||
)
|
||||
)
|
||||
}
|
||||
updateRateLimitStatus()
|
||||
}
|
||||
|
||||
function updateFromJson(json) {
|
||||
function updateFromJson (json) {
|
||||
// Fields that should not be updated
|
||||
// title - as the callback URL relies on it
|
||||
|
||||
// Update rate limiting if changed (parse datetime string)
|
||||
if (json.datetimeRateLimitExpires) {
|
||||
const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
|
||||
rateLimitExpires.value = date.getTime() / 1000
|
||||
updateRateLimitStatus()
|
||||
const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
|
||||
rateLimitExpires.value = date.getTime() / 1000
|
||||
if (bindingId.value) {
|
||||
rateLimits[bindingId.value] = rateLimitExpires.value
|
||||
}
|
||||
updateRateLimitStatus()
|
||||
} else if (json.datetimeRateLimitExpires === '') {
|
||||
// Explicitly clear if empty string
|
||||
rateLimitExpires.value = 0
|
||||
updateRateLimitStatus()
|
||||
// Explicitly clear if empty string
|
||||
rateLimitExpires.value = 0
|
||||
if (bindingId.value) {
|
||||
rateLimits[bindingId.value] = 0
|
||||
}
|
||||
updateRateLimitStatus()
|
||||
}
|
||||
}
|
||||
|
||||
function updateRateLimitStatus() {
|
||||
function updateRateLimitStatus () {
|
||||
if (rateLimitExpires.value === 0) {
|
||||
isRateLimited.value = false
|
||||
rateLimitMessage.value = ''
|
||||
if (rateLimitInterval.value) {
|
||||
isRateLimited.value = false
|
||||
rateLimitMessage.value = ''
|
||||
if (rateLimitInterval.value) {
|
||||
clearInterval(rateLimitInterval.value)
|
||||
rateLimitInterval.value = null
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const expires = rateLimitExpires.value
|
||||
|
||||
if (now >= expires) {
|
||||
// Rate limit has expired
|
||||
isRateLimited.value = false
|
||||
rateLimitMessage.value = ''
|
||||
rateLimitExpires.value = 0
|
||||
if (rateLimitInterval.value) {
|
||||
// Rate limit has expired
|
||||
isRateLimited.value = false
|
||||
rateLimitMessage.value = ''
|
||||
rateLimitExpires.value = 0
|
||||
if (rateLimitInterval.value) {
|
||||
clearInterval(rateLimitInterval.value)
|
||||
rateLimitInterval.value = null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Still rate limited
|
||||
isRateLimited.value = true
|
||||
const secondsRemaining = expires - now
|
||||
rateLimitMessage.value = `Rate limited, available in ${secondsRemaining} second${secondsRemaining !== 1 ? 's' : ''}`
|
||||
// Still rate limited
|
||||
isRateLimited.value = true
|
||||
const secondsRemaining = expires - now
|
||||
rateLimitMessage.value = `Rate limited, available in ${secondsRemaining} second${secondsRemaining !== 1 ? 's' : ''}`
|
||||
|
||||
// Set up interval to update every second
|
||||
if (!rateLimitInterval.value) {
|
||||
// Set up interval to update every second
|
||||
if (!rateLimitInterval.value) {
|
||||
rateLimitInterval.value = setInterval(() => {
|
||||
updateRateLimitStatus()
|
||||
updateRateLimitStatus()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openActionDetails() {
|
||||
function openActionDetails () {
|
||||
const id = props.actionData?.bindingId
|
||||
if (!id) {
|
||||
return
|
||||
return
|
||||
}
|
||||
router.push(`/action/${id}`)
|
||||
}
|
||||
|
||||
async function handleClick() {
|
||||
async function handleClick () {
|
||||
if (popupOnStart.value === 'history') {
|
||||
openActionDetails()
|
||||
return
|
||||
openActionDetails()
|
||||
return
|
||||
}
|
||||
if (needsArgumentForm(props.actionData)) {
|
||||
const bindingId = props.actionData.bindingId
|
||||
const prefilled = props.prefilledArguments || {}
|
||||
if (Object.keys(prefilled).length > 0) {
|
||||
const bindingId = props.actionData.bindingId
|
||||
const prefilled = props.prefilledArguments || {}
|
||||
if (Object.keys(prefilled).length > 0) {
|
||||
router.push({
|
||||
path: `/actionBinding/${bindingId}/argumentForm`,
|
||||
state: { prefilledArguments: prefilled }
|
||||
path: `/actionBinding/${bindingId}/argumentForm`,
|
||||
state: { prefilledArguments: prefilled }
|
||||
})
|
||||
} else {
|
||||
} else {
|
||||
router.push(`/actionBinding/${bindingId}/argumentForm`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await startAction()
|
||||
await startAction()
|
||||
}
|
||||
}
|
||||
|
||||
function getUniqueId() {
|
||||
function getUniqueId () {
|
||||
if (window.isSecureContext) {
|
||||
return window.crypto.randomUUID()
|
||||
return window.crypto.randomUUID()
|
||||
} else {
|
||||
return Date.now().toString()
|
||||
return Date.now().toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -323,122 +365,138 @@ async function pollExecutionUntilDone (trackingId) {
|
|||
}
|
||||
}
|
||||
|
||||
async function startAction(actionArgs) {
|
||||
let stopButtonResultWatch = null
|
||||
|
||||
function stopWatchingButtonResult () {
|
||||
if (stopButtonResultWatch) {
|
||||
stopButtonResultWatch()
|
||||
stopButtonResultWatch = null
|
||||
}
|
||||
}
|
||||
|
||||
async function startAction (actionArgs) {
|
||||
buttonClasses.value = [] // Removes old animation classes
|
||||
|
||||
if (actionArgs === undefined) {
|
||||
actionArgs = []
|
||||
actionArgs = []
|
||||
}
|
||||
|
||||
// UUIDs are create client side, so that we can setup a "execution-button"
|
||||
// to track the execution before we send the request to the server.
|
||||
const startActionArgs = {
|
||||
bindingId: props.actionData.bindingId,
|
||||
arguments: actionArgs,
|
||||
uniqueTrackingId: getUniqueId()
|
||||
bindingId: props.actionData.bindingId,
|
||||
arguments: actionArgs,
|
||||
uniqueTrackingId: getUniqueId()
|
||||
}
|
||||
|
||||
console.log('Watching buttonResults for', startActionArgs.uniqueTrackingId)
|
||||
|
||||
watch(
|
||||
() => buttonResults[startActionArgs.uniqueTrackingId],
|
||||
(newResult, oldResult) => {
|
||||
stopWatchingButtonResult()
|
||||
stopButtonResultWatch = watch(
|
||||
() => buttonResults[startActionArgs.uniqueTrackingId],
|
||||
(newResult, oldResult) => {
|
||||
onLogEntryChanged(newResult)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
requestReconnectNow()
|
||||
|
||||
try {
|
||||
const response = await window.client.startAction(startActionArgs)
|
||||
const trackingId = response.executionTrackingId || startActionArgs.uniqueTrackingId
|
||||
const response = await window.client.startAction(startActionArgs)
|
||||
const trackingId = response.executionTrackingId || startActionArgs.uniqueTrackingId
|
||||
|
||||
if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
|
||||
if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
|
||||
router.push(`/logs/${trackingId}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!connectionState.connected) {
|
||||
if (!connectionState.connected) {
|
||||
await pollExecutionUntilDone(trackingId)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to start action:', err)
|
||||
stopWatchingButtonResult()
|
||||
console.error('Failed to start action:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function onLogEntryChanged(logEntry) {
|
||||
function onLogEntryChanged (logEntry) {
|
||||
if (!logEntry) {
|
||||
return
|
||||
}
|
||||
|
||||
if (logEntry.executionFinished) {
|
||||
onExecutionFinished(logEntry)
|
||||
stopWatchingButtonResult()
|
||||
onExecutionFinished(logEntry)
|
||||
} else if (logEntry.queued && !logEntry.executionStarted) {
|
||||
onExecutionQueued(logEntry)
|
||||
onExecutionQueued(logEntry)
|
||||
} else {
|
||||
onExecutionStarted(logEntry)
|
||||
onExecutionStarted(logEntry)
|
||||
}
|
||||
}
|
||||
|
||||
function onExecutionQueued(_logEntry) {
|
||||
function onExecutionQueued (_logEntry) {
|
||||
isDisabled.value = true
|
||||
updateDom('action-queued', '[Queued]')
|
||||
}
|
||||
|
||||
function onExecutionStarted(logEntry) {
|
||||
function onExecutionStarted (logEntry) {
|
||||
if (
|
||||
popupOnStart.value &&
|
||||
popupOnStart.value &&
|
||||
popupOnStart.value.includes('execution-dialog') &&
|
||||
!shouldSuppressPopupOnStartNavigation(router)
|
||||
) {
|
||||
router.push(`/logs/${logEntry.executionTrackingId}`)
|
||||
router.push(`/logs/${logEntry.executionTrackingId}`)
|
||||
}
|
||||
|
||||
isDisabled.value = true
|
||||
updateDom(null, title.value)
|
||||
}
|
||||
|
||||
function onExecutionFinished(logEntry) {
|
||||
function onExecutionFinished (logEntry) {
|
||||
const trackingId = logEntry.executionTrackingId
|
||||
if (trackingId) {
|
||||
if (flashedTrackingIds.has(trackingId)) {
|
||||
return
|
||||
}
|
||||
flashedTrackingIds.add(trackingId)
|
||||
if (flashedTrackingIds.has(trackingId)) {
|
||||
return
|
||||
}
|
||||
flashedTrackingIds.add(trackingId)
|
||||
}
|
||||
|
||||
// Local no-arg watches and the binding-scoped pending flash can both
|
||||
// observe the same finished execution; consume so only one path flashes.
|
||||
if (bindingId.value) {
|
||||
consumePendingBindingFlash(bindingId.value)
|
||||
consumePendingBindingFlash(bindingId.value)
|
||||
}
|
||||
|
||||
if (logEntry.timedOut) {
|
||||
renderExecutionResult('action-timeout', 'Timed out')
|
||||
renderExecutionResult('action-timeout', 'Timed out')
|
||||
} else if (logEntry.blocked) {
|
||||
renderExecutionResult('action-blocked', 'Blocked!')
|
||||
renderExecutionResult('action-blocked', 'Blocked!')
|
||||
} else if (logEntry.exitCode !== 0) {
|
||||
renderExecutionResult('action-nonzero-exit', 'Exit code ' + logEntry.exitCode)
|
||||
renderExecutionResult('action-nonzero-exit', 'Exit code ' + logEntry.exitCode)
|
||||
} else {
|
||||
renderExecutionResult('action-success', 'Success!')
|
||||
renderExecutionResult('action-success', 'Success!')
|
||||
}
|
||||
}
|
||||
|
||||
function renderExecutionResult(resultCssClass, temporaryStatusMessage) {
|
||||
function renderExecutionResult (resultCssClass, temporaryStatusMessage) {
|
||||
updateDom(resultCssClass, '[' + temporaryStatusMessage + ']')
|
||||
onExecStatusChanged()
|
||||
}
|
||||
|
||||
function updateDom(resultCssClass, newTitle) {
|
||||
function updateDom (resultCssClass, newTitle) {
|
||||
if (resultCssClass == null) {
|
||||
buttonClasses.value = []
|
||||
buttonClasses.value = []
|
||||
} else {
|
||||
buttonClasses.value = [resultCssClass]
|
||||
buttonClasses.value = [resultCssClass]
|
||||
}
|
||||
|
||||
displayTitle.value = newTitle
|
||||
}
|
||||
|
||||
function onExecStatusChanged() {
|
||||
function onExecStatusChanged () {
|
||||
isDisabled.value = false
|
||||
|
||||
setTimeout(() => {
|
||||
updateDom(null, title.value)
|
||||
updateDom(null, title.value)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
|
|
@ -448,47 +506,47 @@ onMounted(() => {
|
|||
// Watch the central rate limit store for updates to this button's bindingId
|
||||
// Watch the entire rateLimits object to ensure reactivity with dynamic keys
|
||||
watch(
|
||||
rateLimits,
|
||||
() => {
|
||||
rateLimits,
|
||||
() => {
|
||||
const id = bindingId.value
|
||||
if (id && rateLimits[id] !== undefined) {
|
||||
const newExpires = rateLimits[id]
|
||||
if (newExpires !== rateLimitExpires.value) {
|
||||
const newExpires = rateLimits[id]
|
||||
if (newExpires !== rateLimitExpires.value) {
|
||||
rateLimitExpires.value = newExpires
|
||||
updateRateLimitStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// Binding-scoped flash survives argument-form navigation/remount (#920).
|
||||
watch(
|
||||
() => pendingBindingFlash[bindingId.value],
|
||||
(pending) => {
|
||||
() => pendingBindingFlash[bindingId.value],
|
||||
(pending) => {
|
||||
if (pending) {
|
||||
consumeAndFlashPendingResult()
|
||||
consumeAndFlashPendingResult()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
isComponentMounted.value = false
|
||||
if (rateLimitInterval.value) {
|
||||
clearInterval(rateLimitInterval.value)
|
||||
rateLimitInterval.value = null
|
||||
clearInterval(rateLimitInterval.value)
|
||||
rateLimitInterval.value = null
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.actionData,
|
||||
(newData) => {
|
||||
updateFromJson(newData)
|
||||
if (newData?.icon !== undefined) {
|
||||
updateFromJson(newData)
|
||||
if (newData?.icon !== undefined) {
|
||||
glyph.value = newData.icon ?? ''
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,126 +1,218 @@
|
|||
<template>
|
||||
<Header :title="pageTitle" :logoUrl="logoUrl" @toggleSidebar="toggleSidebar" :sidebarEnabled="sidebarEnabled" :topBarEnabled="topbarEnabled" :navigation="navigation">
|
||||
<template #toolbar>
|
||||
<div id="banner" v-if="bannerMessage" :style="bannerCss">
|
||||
<p>{{ bannerMessage }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<Header
|
||||
:title="pageTitle"
|
||||
:logo-url="logoUrl"
|
||||
:sidebar-enabled="sidebarEnabled"
|
||||
:top-bar-enabled="topbarEnabled"
|
||||
:navigation="navigation"
|
||||
@toggle-sidebar="toggleSidebar"
|
||||
>
|
||||
<template #toolbar>
|
||||
<div
|
||||
v-if="bannerMessage"
|
||||
id="banner"
|
||||
:style="bannerCss"
|
||||
>
|
||||
<p>{{ bannerMessage }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #user-info>
|
||||
<ConnectionBanner />
|
||||
<div class="flex-row user-info" style="gap: .5em;">
|
||||
<span id="link-login" v-if="!isLoggedIn && showLoginLink"><router-link to="/login">{{ t('login-button') }}</router-link></span>
|
||||
<router-link v-else to="/user" class="user-link" v-if="isLoggedIn">
|
||||
<span id="username-text">{{ username }}</span>
|
||||
</router-link>
|
||||
<HugeiconsIcon :icon="UserCircle02Icon" width = "1.5em" height = "1.5em" v-if="isLoggedIn" />
|
||||
</div>
|
||||
<template #user-info>
|
||||
<ConnectionBanner />
|
||||
<div
|
||||
class="flex-row user-info"
|
||||
style="gap: .5em;"
|
||||
>
|
||||
<span
|
||||
v-if="!isLoggedIn && showLoginLink"
|
||||
id="link-login"
|
||||
><router-link to="/login">{{ t('login-button') }}</router-link></span>
|
||||
<router-link
|
||||
v-else-if="isLoggedIn"
|
||||
to="/user"
|
||||
class="user-link"
|
||||
>
|
||||
<span id="username-text">{{ username }}</span>
|
||||
</router-link>
|
||||
<HugeiconsIcon
|
||||
v-if="isLoggedIn"
|
||||
:icon="UserCircle02Icon"
|
||||
width="1.5em"
|
||||
height="1.5em"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Header>
|
||||
|
||||
</template>
|
||||
</Header>
|
||||
<div id="layout">
|
||||
<Navigation ref="navigation">
|
||||
<Sidebar
|
||||
v-if="sidebarEnabled && showNavigation"
|
||||
id="mainnav"
|
||||
ref="sidebar"
|
||||
/>
|
||||
</Navigation>
|
||||
|
||||
<div id="layout">
|
||||
<Navigation ref="navigation">
|
||||
<Sidebar ref="sidebar" id = "mainnav" v-if="sidebarEnabled && showNavigation"/>
|
||||
</Navigation>
|
||||
<div
|
||||
id="content"
|
||||
:initial-martial-complete="hasLoaded"
|
||||
>
|
||||
<main title="Main content">
|
||||
<router-view :key="$route.fullPath" />
|
||||
</main>
|
||||
|
||||
<div id="content" initial-martial-complete="{{ hasLoaded }}">
|
||||
<main title="Main content">
|
||||
<router-view :key="$route.fullPath" />
|
||||
</main>
|
||||
<footer
|
||||
v-if="showFooter"
|
||||
title="footer"
|
||||
>
|
||||
<p>
|
||||
<img
|
||||
title="application icon"
|
||||
:src="logoUrl"
|
||||
alt="OliveTin logo"
|
||||
style="height: 1em;"
|
||||
class="logo"
|
||||
>
|
||||
OliveTin <span v-if="showVersionNumber">{{ currentVersion }}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
<a
|
||||
href="https://docs.olivetin.app"
|
||||
target="_new"
|
||||
>{{ t('docs') }}</a>
|
||||
</span>
|
||||
|
||||
<footer title="footer" v-if="showFooter">
|
||||
<p>
|
||||
<img title="application icon" :src="logoUrl" alt="OliveTin logo" style="height: 1em;" class="logo" />
|
||||
OliveTin <span v-if="showVersionNumber">{{ currentVersion }}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
<a href="https://docs.olivetin.app" target="_new">{{ t('docs') }}</a>
|
||||
</span>
|
||||
<span>
|
||||
<a
|
||||
href="https://github.com/OliveTin/OliveTin/issues/new/choose"
|
||||
target="_new"
|
||||
>{{ t('raise-issue') }}</a>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<a href="https://github.com/OliveTin/OliveTin/issues/new/choose" target="_new">{{ t('raise-issue') }}</a>
|
||||
</span>
|
||||
<span>
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="openLanguageDialog"
|
||||
>{{ currentLanguageName }}</a>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<a href="#" @click.prevent="openLanguageDialog">{{ currentLanguageName }}</a>
|
||||
</span>
|
||||
|
||||
<span v-if="availableThemes.length > 1">
|
||||
<a href="#" @click.prevent="openThemeDialog">{{ currentThemeName }}</a>
|
||||
</span>
|
||||
</p>
|
||||
<p v-if="showVersionNumber">
|
||||
<a id="available-version" href="http://olivetin.app" target="_blank" hidden>?</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
<span v-if="availableThemes.length > 1">
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="openThemeDialog"
|
||||
>{{ currentThemeName }}</a>
|
||||
</span>
|
||||
</p>
|
||||
<p v-if="showVersionNumber">
|
||||
<a
|
||||
id="available-version"
|
||||
href="http://olivetin.app"
|
||||
target="_blank"
|
||||
hidden
|
||||
>?</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog ref="languageDialog" class="language-dialog" @click="handleLanguageDialogClick">
|
||||
<div class="dialog-content" @click.stop>
|
||||
<h2>{{ t('language-dialog.title') }}</h2>
|
||||
<select v-model="selectedLanguage" @change="changeLanguage" class="language-select">
|
||||
<option v-for="(name, code) in availableLanguages" :key="code" :value="code">
|
||||
{{ code === 'auto' ? name : `${name} (${code})` }}
|
||||
</option>
|
||||
</select>
|
||||
<p class="browser-languages">
|
||||
{{ t('language-dialog.browser-languages') }}:
|
||||
<span v-if="browserLanguages.length > 0">{{ browserLanguages.join(', ') }}</span>
|
||||
<span v-else>{{ t('language-dialog.not-available') }}</span>
|
||||
</p>
|
||||
<div class="dialog-buttons">
|
||||
<button @click="closeLanguageDialog">{{ t('language-dialog.close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog
|
||||
ref="languageDialog"
|
||||
class="language-dialog"
|
||||
@click="handleLanguageDialogClick"
|
||||
>
|
||||
<div
|
||||
class="dialog-content"
|
||||
@click.stop
|
||||
>
|
||||
<h2>{{ t('language-dialog.title') }}</h2>
|
||||
<select
|
||||
v-model="selectedLanguage"
|
||||
class="language-select"
|
||||
@change="changeLanguage"
|
||||
>
|
||||
<option
|
||||
v-for="(name, code) in availableLanguages"
|
||||
:key="code"
|
||||
:value="code"
|
||||
>
|
||||
{{ code === 'auto' ? name : `${name} (${code})` }}
|
||||
</option>
|
||||
</select>
|
||||
<p class="browser-languages">
|
||||
{{ t('language-dialog.browser-languages') }}:
|
||||
<span v-if="browserLanguages.length > 0">{{ browserLanguages.join(', ') }}</span>
|
||||
<span v-else>{{ t('language-dialog.not-available') }}</span>
|
||||
</p>
|
||||
<div class="dialog-buttons">
|
||||
<button @click="closeLanguageDialog">
|
||||
{{ t('language-dialog.close') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog ref="themeDialog" class="theme-dialog" @click="handleThemeDialogClick">
|
||||
<div class="dialog-content" @click.stop>
|
||||
<h2>{{ t('theme-dialog.title') }}</h2>
|
||||
<select v-model="selectedTheme" @change="changeTheme" class="language-select">
|
||||
<option value="">{{ t('theme-dialog.default') }}</option>
|
||||
<option v-for="theme in availableThemes" :key="theme" :value="theme">
|
||||
{{ theme }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="dialog-buttons">
|
||||
<button @click="closeThemeDialog">{{ t('theme-dialog.close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog
|
||||
ref="themeDialog"
|
||||
class="theme-dialog"
|
||||
@click="handleThemeDialogClick"
|
||||
>
|
||||
<div
|
||||
class="dialog-content"
|
||||
@click.stop
|
||||
>
|
||||
<h2>{{ t('theme-dialog.title') }}</h2>
|
||||
<select
|
||||
v-model="selectedTheme"
|
||||
class="language-select"
|
||||
@change="changeTheme"
|
||||
>
|
||||
<option value="">
|
||||
{{ t('theme-dialog.default') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="theme in availableThemes"
|
||||
:key="theme"
|
||||
:value="theme"
|
||||
>
|
||||
{{ theme }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="dialog-buttons">
|
||||
<button @click="closeThemeDialog">
|
||||
{{ t('theme-dialog.close') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import Sidebar from 'picocrank/vue/components/Sidebar.vue';
|
||||
import Navigation from 'picocrank/vue/components/Navigation.vue';
|
||||
import Header from 'picocrank/vue/components/Header.vue';
|
||||
import ConnectionBanner from './components/ConnectionBanner.vue';
|
||||
import { connectEventStreamIfNeeded } from '../../js/websocket.js';
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Sidebar from 'picocrank/vue/components/Sidebar.vue'
|
||||
import Navigation from 'picocrank/vue/components/Navigation.vue'
|
||||
import Header from 'picocrank/vue/components/Header.vue'
|
||||
import ConnectionBanner from './components/ConnectionBanner.vue'
|
||||
import { connectEventStreamIfNeeded } from '../../js/websocket.js'
|
||||
import { HugeiconsIcon } from '@hugeicons/vue'
|
||||
import { Menu01Icon } from '@hugeicons/core-free-icons'
|
||||
import { UserCircle02Icon } from '@hugeicons/core-free-icons'
|
||||
import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
|
||||
import logoUrl from '../../OliveTinLogo.png';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import combinedTranslations from '../../../lang/combined_output.json';
|
||||
const { t, locale } = useI18n();
|
||||
import { UserCircle02Icon, DashboardSquare01Icon } from '@hugeicons/core-free-icons'
|
||||
import logoUrl from '../../OliveTinLogo.png'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import combinedTranslations from '../../../lang/combined_output.json'
|
||||
const { t } = useI18n()
|
||||
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
|
||||
const sidebar = ref(null);
|
||||
const navigation = ref(null);
|
||||
const username = ref('notset');
|
||||
const isLoggedIn = ref(false);
|
||||
const currentVersion = ref('?');
|
||||
const pageTitle = ref('OliveTin');
|
||||
const bannerMessage = ref('');
|
||||
const bannerCss = ref('');
|
||||
const hasLoaded = ref(false);
|
||||
const sidebar = ref(null)
|
||||
const navigation = ref(null)
|
||||
const username = ref('notset')
|
||||
const isLoggedIn = ref(false)
|
||||
const currentVersion = ref('?')
|
||||
const pageTitle = ref('OliveTin')
|
||||
const bannerMessage = ref('')
|
||||
const bannerCss = ref('')
|
||||
const hasLoaded = ref(false)
|
||||
const showFooter = ref(true)
|
||||
const showNavigation = ref(true)
|
||||
const showLogs = ref(true)
|
||||
|
|
@ -144,279 +236,279 @@ const selectedTheme = ref(themePreference.value)
|
|||
|
||||
// Available languages with display names
|
||||
const availableLanguages = {
|
||||
'auto': 'Browser Language',
|
||||
'en': 'English',
|
||||
'de-DE': 'Deutsch',
|
||||
'es-ES': 'Español',
|
||||
'it-IT': 'Italiano',
|
||||
'zh-Hans-CN': '简体中文'
|
||||
auto: 'Browser Language',
|
||||
en: 'English',
|
||||
'de-DE': 'Deutsch',
|
||||
'es-ES': 'Español',
|
||||
'it-IT': 'Italiano',
|
||||
'zh-Hans-CN': '简体中文'
|
||||
}
|
||||
|
||||
// Computed property to get current language display name
|
||||
const currentLanguageName = computed(() => {
|
||||
if (languagePreference.value === 'auto') {
|
||||
return availableLanguages['auto']
|
||||
}
|
||||
if (languagePreference.value === 'auto') {
|
||||
return availableLanguages.auto
|
||||
}
|
||||
|
||||
return availableLanguages[languagePreference.value] || languagePreference.value
|
||||
return availableLanguages[languagePreference.value] || languagePreference.value
|
||||
})
|
||||
|
||||
// Computed property to get current theme display name
|
||||
const currentThemeName = computed(() => {
|
||||
if (!themePreference.value || themePreference.value === '') {
|
||||
return t('theme-dialog.default')
|
||||
}
|
||||
return themePreference.value
|
||||
if (!themePreference.value || themePreference.value === '') {
|
||||
return t('theme-dialog.default')
|
||||
}
|
||||
return themePreference.value
|
||||
})
|
||||
|
||||
// Computed properties for navigation style
|
||||
const topbarEnabled = computed(() => {
|
||||
return sectionNavigationStyle.value === 'topbar'
|
||||
return sectionNavigationStyle.value === 'topbar'
|
||||
})
|
||||
|
||||
const sidebarEnabled = computed(() => {
|
||||
return sectionNavigationStyle.value !== 'topbar' && showNavigation.value
|
||||
return sectionNavigationStyle.value !== 'topbar' && showNavigation.value
|
||||
})
|
||||
|
||||
function normalizeBrowserLanguage() {
|
||||
const available = Object.keys(combinedTranslations.messages || {})
|
||||
function normalizeBrowserLanguage () {
|
||||
const available = Object.keys(combinedTranslations.messages || {})
|
||||
|
||||
if (navigator.languages && navigator.languages.length > 0) {
|
||||
for (const candidate of navigator.languages) {
|
||||
const lowerCandidate = candidate.toLowerCase()
|
||||
if (navigator.languages && navigator.languages.length > 0) {
|
||||
for (const candidate of navigator.languages) {
|
||||
const lowerCandidate = candidate.toLowerCase()
|
||||
|
||||
// Try exact match (case-insensitive)
|
||||
const exact = available.find(locale => locale.toLowerCase() === lowerCandidate)
|
||||
if (exact) {
|
||||
return exact
|
||||
}
|
||||
// Try exact match (case-insensitive)
|
||||
const exact = available.find(locale => locale.toLowerCase() === lowerCandidate)
|
||||
if (exact) {
|
||||
return exact
|
||||
}
|
||||
|
||||
// Try prefix match (e.g., "zh-CN" -> "zh-Hans-CN")
|
||||
const prefix = available.find(locale => locale.toLowerCase().startsWith(lowerCandidate.split('-')[0] + '-'))
|
||||
if (prefix) {
|
||||
return prefix
|
||||
}
|
||||
}
|
||||
// Try prefix match (e.g., "zh-CN" -> "zh-Hans-CN")
|
||||
const prefix = available.find(locale => locale.toLowerCase().startsWith(lowerCandidate.split('-')[0] + '-'))
|
||||
if (prefix) {
|
||||
return prefix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'en'
|
||||
return 'en'
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
if (sidebar.value && showNavigation.value) {
|
||||
sidebar.value.toggle()
|
||||
}
|
||||
function toggleSidebar () {
|
||||
if (sidebar.value && showNavigation.value) {
|
||||
sidebar.value.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
function updateHeaderFromInit() {
|
||||
if (!window.initResponse) {
|
||||
return
|
||||
}
|
||||
function updateHeaderFromInit () {
|
||||
if (!window.initResponse) {
|
||||
return
|
||||
}
|
||||
|
||||
username.value = window.initResponse.authenticatedUser
|
||||
isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
|
||||
currentVersion.value = window.initResponse.currentVersion
|
||||
pageTitle.value = window.initResponse.pageTitle || 'OliveTin'
|
||||
bannerMessage.value = window.initResponse.bannerMessage || ''
|
||||
bannerCss.value = window.initResponse.bannerCss || ''
|
||||
showFooter.value = window.initResponse.showFooter
|
||||
showNavigation.value = window.initResponse.showNavigation
|
||||
showLogs.value = window.initResponse.showLogList
|
||||
showDiagnostics.value = window.initResponse.showDiagnostics
|
||||
showVersionNumber.value = window.initResponse.effectivePolicy?.showVersionNumber ?? true
|
||||
sectionNavigationStyle.value = window.initResponse.sectionNavigationStyle || 'sidebar'
|
||||
availableThemes.value = window.initResponse.availableThemes || []
|
||||
username.value = window.initResponse.authenticatedUser
|
||||
isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
|
||||
currentVersion.value = window.initResponse.currentVersion
|
||||
pageTitle.value = window.initResponse.pageTitle || 'OliveTin'
|
||||
bannerMessage.value = window.initResponse.bannerMessage || ''
|
||||
bannerCss.value = window.initResponse.bannerCss || ''
|
||||
showFooter.value = window.initResponse.showFooter
|
||||
showNavigation.value = window.initResponse.showNavigation
|
||||
showLogs.value = window.initResponse.showLogList
|
||||
showDiagnostics.value = window.initResponse.showDiagnostics
|
||||
showVersionNumber.value = window.initResponse.effectivePolicy?.showVersionNumber ?? true
|
||||
sectionNavigationStyle.value = window.initResponse.sectionNavigationStyle || 'sidebar'
|
||||
availableThemes.value = window.initResponse.availableThemes || []
|
||||
|
||||
if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
|
||||
showLoginLink.value = false
|
||||
}
|
||||
if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
|
||||
showLoginLink.value = false
|
||||
}
|
||||
|
||||
applyStyleMods()
|
||||
loadCustomJsIfEnabled()
|
||||
applyStyleMods()
|
||||
loadCustomJsIfEnabled()
|
||||
|
||||
renderNavigation()
|
||||
applyTheme()
|
||||
|
||||
if (window.initResponse.loginRequired) {
|
||||
connectEventStreamIfNeeded()
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
renderNavigation()
|
||||
applyTheme()
|
||||
|
||||
if (window.initResponse.loginRequired) {
|
||||
connectEventStreamIfNeeded()
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
|
||||
connectEventStreamIfNeeded()
|
||||
}
|
||||
|
||||
function renderNavigation() {
|
||||
if (!navigation.value) {
|
||||
return
|
||||
}
|
||||
function renderNavigation () {
|
||||
if (!navigation.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const rootDashboards = window.initResponse?.rootDashboards || []
|
||||
const rootDashboards = window.initResponse?.rootDashboards || []
|
||||
|
||||
if (typeof navigation.value.clear === 'function') {
|
||||
navigation.value.clear()
|
||||
}
|
||||
if (typeof navigation.value.clear === 'function') {
|
||||
navigation.value.clear()
|
||||
}
|
||||
|
||||
for (const rootDashboard of rootDashboards) {
|
||||
navigation.value.addNavigationLink({
|
||||
id: rootDashboard,
|
||||
name: rootDashboard,
|
||||
title: rootDashboard,
|
||||
path: rootDashboard === 'Actions' ? '/' : `/dashboards/${rootDashboard}`,
|
||||
icon: DashboardSquare01Icon,
|
||||
})
|
||||
}
|
||||
for (const rootDashboard of rootDashboards) {
|
||||
navigation.value.addNavigationLink({
|
||||
id: rootDashboard,
|
||||
name: rootDashboard,
|
||||
title: rootDashboard,
|
||||
path: rootDashboard === 'Actions' ? '/' : `/dashboards/${rootDashboard}`,
|
||||
icon: DashboardSquare01Icon
|
||||
})
|
||||
}
|
||||
|
||||
navigation.value.addSeparator()
|
||||
navigation.value.addRouterLink('Entities', t('nav.entities'))
|
||||
navigation.value.addSeparator()
|
||||
navigation.value.addRouterLink('Entities', t('nav.entities'))
|
||||
|
||||
if (showLogs.value) {
|
||||
navigation.value.addRouterLink('Logs', t('nav.logs'))
|
||||
}
|
||||
if (showLogs.value) {
|
||||
navigation.value.addRouterLink('Logs', t('nav.logs'))
|
||||
}
|
||||
|
||||
if (showDiagnostics.value) {
|
||||
navigation.value.addRouterLink('Diagnostics', t('nav.diagnostics'))
|
||||
}
|
||||
if (showDiagnostics.value) {
|
||||
navigation.value.addRouterLink('Diagnostics', t('nav.diagnostics'))
|
||||
}
|
||||
}
|
||||
|
||||
function openLanguageDialog() {
|
||||
selectedLanguage.value = languagePreference.value
|
||||
function openLanguageDialog () {
|
||||
selectedLanguage.value = languagePreference.value
|
||||
|
||||
if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
|
||||
browserLanguages.value = navigator.languages
|
||||
} else {
|
||||
browserLanguages.value = []
|
||||
}
|
||||
if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
|
||||
browserLanguages.value = navigator.languages
|
||||
} else {
|
||||
browserLanguages.value = []
|
||||
}
|
||||
|
||||
if (languageDialog.value) {
|
||||
languageDialog.value.showModal()
|
||||
}
|
||||
if (languageDialog.value) {
|
||||
languageDialog.value.showModal()
|
||||
}
|
||||
}
|
||||
|
||||
function closeLanguageDialog() {
|
||||
if (languageDialog.value) {
|
||||
languageDialog.value.close()
|
||||
}
|
||||
function closeLanguageDialog () {
|
||||
if (languageDialog.value) {
|
||||
languageDialog.value.close()
|
||||
}
|
||||
}
|
||||
|
||||
function changeLanguage() {
|
||||
if (!window.i18n || !selectedLanguage.value) {
|
||||
return
|
||||
}
|
||||
function changeLanguage () {
|
||||
if (!window.i18n || !selectedLanguage.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedLanguage.value === 'auto') {
|
||||
localStorage.removeItem('olivetin-language')
|
||||
languagePreference.value = 'auto'
|
||||
window.i18n.locale.value = normalizeBrowserLanguage()
|
||||
} else {
|
||||
window.i18n.locale.value = selectedLanguage.value
|
||||
localStorage.setItem('olivetin-language', selectedLanguage.value)
|
||||
languagePreference.value = selectedLanguage.value
|
||||
}
|
||||
if (selectedLanguage.value === 'auto') {
|
||||
localStorage.removeItem('olivetin-language')
|
||||
languagePreference.value = 'auto'
|
||||
window.i18n.locale.value = normalizeBrowserLanguage()
|
||||
} else {
|
||||
window.i18n.locale.value = selectedLanguage.value
|
||||
localStorage.setItem('olivetin-language', selectedLanguage.value)
|
||||
languagePreference.value = selectedLanguage.value
|
||||
}
|
||||
|
||||
// Update navigation with new translations
|
||||
if (navigation.value) {
|
||||
renderNavigation()
|
||||
}
|
||||
// Update navigation with new translations
|
||||
if (navigation.value) {
|
||||
renderNavigation()
|
||||
}
|
||||
|
||||
closeLanguageDialog()
|
||||
}
|
||||
|
||||
function handleLanguageDialogClick (event) {
|
||||
// Close dialog when clicking on the backdrop
|
||||
if (event.target === languageDialog.value) {
|
||||
closeLanguageDialog()
|
||||
}
|
||||
}
|
||||
|
||||
function handleLanguageDialogClick(event) {
|
||||
// Close dialog when clicking on the backdrop
|
||||
if (event.target === languageDialog.value) {
|
||||
closeLanguageDialog()
|
||||
}
|
||||
function openThemeDialog () {
|
||||
selectedTheme.value = themePreference.value || ''
|
||||
|
||||
if (themeDialog.value) {
|
||||
themeDialog.value.showModal()
|
||||
}
|
||||
}
|
||||
|
||||
function openThemeDialog() {
|
||||
selectedTheme.value = themePreference.value || ''
|
||||
|
||||
if (themeDialog.value) {
|
||||
themeDialog.value.showModal()
|
||||
}
|
||||
function closeThemeDialog () {
|
||||
if (themeDialog.value) {
|
||||
themeDialog.value.close()
|
||||
}
|
||||
}
|
||||
|
||||
function closeThemeDialog() {
|
||||
if (themeDialog.value) {
|
||||
themeDialog.value.close()
|
||||
}
|
||||
function changeTheme () {
|
||||
if (!selectedTheme.value || selectedTheme.value === '') {
|
||||
localStorage.removeItem('olivetin-theme')
|
||||
themePreference.value = ''
|
||||
} else {
|
||||
localStorage.setItem('olivetin-theme', selectedTheme.value)
|
||||
themePreference.value = selectedTheme.value
|
||||
}
|
||||
|
||||
applyTheme()
|
||||
closeThemeDialog()
|
||||
}
|
||||
|
||||
function changeTheme() {
|
||||
if (!selectedTheme.value || selectedTheme.value === '') {
|
||||
localStorage.removeItem('olivetin-theme')
|
||||
themePreference.value = ''
|
||||
} else {
|
||||
localStorage.setItem('olivetin-theme', selectedTheme.value)
|
||||
themePreference.value = selectedTheme.value
|
||||
}
|
||||
function applyTheme () {
|
||||
let themeStyle = document.getElementById('theme-style')
|
||||
|
||||
applyTheme()
|
||||
if (!themeStyle) {
|
||||
themeStyle = document.createElement('style')
|
||||
themeStyle.id = 'theme-style'
|
||||
themeStyle.type = 'text/css'
|
||||
document.head.appendChild(themeStyle)
|
||||
}
|
||||
|
||||
// Load theme into @layer theme so it takes precedence over @layer components
|
||||
if (themePreference.value && themePreference.value !== '') {
|
||||
themeStyle.textContent = `@import url('/custom-webui/themes/${themePreference.value}/theme.css') layer(theme);`
|
||||
} else {
|
||||
themeStyle.textContent = '@import url(\'/theme.css\') layer(theme);'
|
||||
}
|
||||
}
|
||||
|
||||
function loadCustomJsIfEnabled () {
|
||||
if (!window.initResponse?.enableCustomJs || document.getElementById('olivetin-custom-js')) {
|
||||
return
|
||||
}
|
||||
const script = document.createElement('script')
|
||||
script.src = '/custom-webui/custom.js'
|
||||
script.async = true
|
||||
script.id = 'olivetin-custom-js'
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
|
||||
function applyStyleMods () {
|
||||
if (!window.initResponse || !window.initResponse.styleMods) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const styleMod of window.initResponse.styleMods) {
|
||||
if (styleMod) {
|
||||
document.body.classList.add(styleMod)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleThemeDialogClick (event) {
|
||||
if (event.target === themeDialog.value) {
|
||||
closeThemeDialog()
|
||||
}
|
||||
|
||||
function applyTheme() {
|
||||
let themeStyle = document.getElementById('theme-style')
|
||||
|
||||
if (!themeStyle) {
|
||||
themeStyle = document.createElement('style')
|
||||
themeStyle.id = 'theme-style'
|
||||
themeStyle.type = 'text/css'
|
||||
document.head.appendChild(themeStyle)
|
||||
}
|
||||
|
||||
// Load theme into @layer theme so it takes precedence over @layer components
|
||||
if (themePreference.value && themePreference.value !== '') {
|
||||
themeStyle.textContent = `@import url('/custom-webui/themes/${themePreference.value}/theme.css') layer(theme);`
|
||||
} else {
|
||||
themeStyle.textContent = `@import url('/theme.css') layer(theme);`
|
||||
}
|
||||
}
|
||||
|
||||
function loadCustomJsIfEnabled() {
|
||||
if (!window.initResponse?.enableCustomJs || document.getElementById('olivetin-custom-js')) {
|
||||
return
|
||||
}
|
||||
const script = document.createElement('script')
|
||||
script.src = '/custom-webui/custom.js'
|
||||
script.async = true
|
||||
script.id = 'olivetin-custom-js'
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
|
||||
function applyStyleMods() {
|
||||
if (!window.initResponse || !window.initResponse.styleMods) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const styleMod of window.initResponse.styleMods) {
|
||||
if (styleMod) {
|
||||
document.body.classList.add(styleMod)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleThemeDialogClick(event) {
|
||||
if (event.target === themeDialog.value) {
|
||||
closeThemeDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.updateHeaderFromInit = updateHeaderFromInit
|
||||
|
||||
onMounted(() => {
|
||||
updateHeaderFromInit()
|
||||
updateHeaderFromInit()
|
||||
|
||||
// Initialize selected language from stored preference
|
||||
selectedLanguage.value = languagePreference.value
|
||||
// Initialize selected language from stored preference
|
||||
selectedLanguage.value = languagePreference.value
|
||||
|
||||
// Initialize selected theme from stored preference
|
||||
selectedTheme.value = themePreference.value || ''
|
||||
// Initialize selected theme from stored preference
|
||||
selectedTheme.value = themePreference.value || ''
|
||||
|
||||
if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
|
||||
browserLanguages.value = navigator.languages
|
||||
}
|
||||
if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
|
||||
browserLanguages.value = navigator.languages
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,58 +1,125 @@
|
|||
<template>
|
||||
<section v-if="!dashboard && !initError" style = "text-align: center; padding: 2em;">
|
||||
<HugeiconsIcon :icon="Loading03Icon" width="3em" height="3em" style="animation: spin 1s linear infinite;" />
|
||||
<p>Loading dashboard...</p>
|
||||
<p style="color: var(--fg2);">{{ loadingTime }}s</p>
|
||||
<section
|
||||
v-if="!dashboard && !initError && !loadError"
|
||||
style="text-align: center; padding: 2em;"
|
||||
>
|
||||
<HugeiconsIcon
|
||||
:icon="Loading03Icon"
|
||||
width="3em"
|
||||
height="3em"
|
||||
style="animation: spin 1s linear infinite;"
|
||||
/>
|
||||
<p>Loading dashboard...</p>
|
||||
<p style="color: var(--fg2);">
|
||||
{{ loadingTime }}s
|
||||
</p>
|
||||
</section>
|
||||
<section
|
||||
v-if="initError"
|
||||
style="text-align: center; padding: 2em;"
|
||||
class="bad"
|
||||
>
|
||||
<h2 style="color: var(--error);">
|
||||
Initialization Failed
|
||||
</h2>
|
||||
<p>{{ initError }}</p>
|
||||
<p style="color: var(--fg2);">
|
||||
Please check your configuration and try again.
|
||||
</p>
|
||||
</section>
|
||||
<section
|
||||
v-else-if="loadError"
|
||||
style="text-align: center; padding: 2em;"
|
||||
class="bad"
|
||||
>
|
||||
<h2 style="color: var(--error);">
|
||||
Failed to Load Dashboard
|
||||
</h2>
|
||||
<p>{{ loadError }}</p>
|
||||
<p style="color: var(--fg2);">
|
||||
Please check your configuration and try again.
|
||||
</p>
|
||||
</section>
|
||||
<template v-else-if="dashboard">
|
||||
<section v-if="dashboard.contents.length == 0">
|
||||
<div
|
||||
v-if="isDirectory"
|
||||
class="back-button-container"
|
||||
>
|
||||
<button
|
||||
class="back-button"
|
||||
@click="goBack"
|
||||
>
|
||||
<HugeiconsIcon
|
||||
:icon="ArrowLeftIcon"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
/>
|
||||
<span>Back</span>
|
||||
</button>
|
||||
</div>
|
||||
<h2>{{ dashboard.title }}</h2>
|
||||
<p
|
||||
style="text-align: center"
|
||||
class="padding"
|
||||
>
|
||||
This dashboard is empty.
|
||||
</p>
|
||||
</section>
|
||||
<section v-if="initError" style="text-align: center; padding: 2em;" class = "bad">
|
||||
<h2 style="color: var(--error);">Initialization Failed</h2>
|
||||
<p>{{ initError }}</p>
|
||||
<p style="color: var(--fg2);">Please check your configuration and try again.</p>
|
||||
|
||||
<section
|
||||
v-else
|
||||
class="transparent"
|
||||
>
|
||||
<div
|
||||
v-if="isDirectory"
|
||||
class="back-button-container"
|
||||
>
|
||||
<button
|
||||
class="back-button"
|
||||
@click="goBack"
|
||||
>
|
||||
<HugeiconsIcon
|
||||
:icon="ArrowLeftIcon"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
/>
|
||||
<span>Back</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-for="component in dashboard.contents"
|
||||
:key="component.title"
|
||||
class="dashboard-row"
|
||||
>
|
||||
<h2 v-if="dashboard.title != 'Default'">
|
||||
<router-link
|
||||
v-if="component.entityType && component.entityKey"
|
||||
:to="{
|
||||
name: 'EntityDetails',
|
||||
params: {
|
||||
entityType: component.entityType,
|
||||
entityKey: component.entityKey
|
||||
}
|
||||
}"
|
||||
class="entity-link"
|
||||
>
|
||||
{{ component.title }}
|
||||
</router-link>
|
||||
<span v-else>{{ component.title }}</span>
|
||||
</h2>
|
||||
|
||||
<fieldset :class="component.cssClass">
|
||||
<template
|
||||
v-for="subcomponent in component.contents"
|
||||
:key="subcomponent.title || subcomponent.type"
|
||||
>
|
||||
<DashboardComponent :component="subcomponent" />
|
||||
</template>
|
||||
</fieldset>
|
||||
</div>
|
||||
</section>
|
||||
<template v-else-if="dashboard">
|
||||
<section v-if="dashboard.contents.length == 0">
|
||||
<div class="back-button-container" v-if="isDirectory">
|
||||
<button @click="goBack" class="back-button">
|
||||
<HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
</div>
|
||||
<h2>{{ dashboard.title }}</h2>
|
||||
<p style = "text-align: center" class = "padding">This dashboard is empty.</p>
|
||||
</section>
|
||||
|
||||
<section class="transparent" v-else>
|
||||
<div class="back-button-container" v-if="isDirectory">
|
||||
<button @click="goBack" class="back-button">
|
||||
<HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class = "dashboard-row" v-for="component in dashboard.contents" :key="component.title">
|
||||
<h2 v-if = "dashboard.title != 'Default'">
|
||||
<router-link
|
||||
v-if="component.entityType && component.entityKey"
|
||||
:to="{
|
||||
name: 'EntityDetails',
|
||||
params: {
|
||||
entityType: component.entityType,
|
||||
entityKey: component.entityKey
|
||||
}
|
||||
}"
|
||||
class="entity-link">
|
||||
{{ component.title }}
|
||||
</router-link>
|
||||
<span v-else>{{ component.title }}</span>
|
||||
</h2>
|
||||
|
||||
<fieldset :class="component.cssClass">
|
||||
<template v-for="subcomponent in component.contents">
|
||||
<DashboardComponent :component="subcomponent" />
|
||||
</template>
|
||||
</fieldset>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
|
@ -63,189 +130,190 @@ import { HugeiconsIcon } from '@hugeicons/vue'
|
|||
import { Loading03Icon, ArrowLeftIcon } from '@hugeicons/core-free-icons'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
entityType: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
entityKey: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
title: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
entityType: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
entityKey: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const dashboard = ref(null)
|
||||
const loadingTime = ref(0)
|
||||
const initError = ref(null)
|
||||
const loadError = ref(null)
|
||||
let loadingTimer = null
|
||||
let checkInitInterval = null
|
||||
let dashboardRequestId = 0
|
||||
|
||||
const isDirectory = computed(() => {
|
||||
if (!dashboard.value || !window.initResponse) {
|
||||
return false
|
||||
}
|
||||
const rootDashboards = window.initResponse.rootDashboards || []
|
||||
return !rootDashboards.includes(dashboard.value.title) && dashboard.value.title !== 'Actions'
|
||||
if (!dashboard.value || !window.initResponse) {
|
||||
return false
|
||||
}
|
||||
const rootDashboards = window.initResponse.rootDashboards || []
|
||||
return !rootDashboards.includes(dashboard.value.title) && dashboard.value.title !== 'Actions'
|
||||
})
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
router.back()
|
||||
function goBack () {
|
||||
if (window.history.length > 1) {
|
||||
router.back()
|
||||
} else {
|
||||
const rootDashboards = window.initResponse?.rootDashboards || []
|
||||
if (rootDashboards.length > 0) {
|
||||
router.push({ name: 'Dashboard', params: { title: rootDashboards[0] } })
|
||||
} else {
|
||||
const rootDashboards = window.initResponse?.rootDashboards || []
|
||||
if (rootDashboards.length > 0) {
|
||||
router.push({ name: 'Dashboard', params: { title: rootDashboards[0] } })
|
||||
} else {
|
||||
router.push({ name: 'Actions' })
|
||||
}
|
||||
router.push({ name: 'Actions' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getDashboard() {
|
||||
const requestId = ++dashboardRequestId
|
||||
let title = props.title
|
||||
async function getDashboard () {
|
||||
const requestId = ++dashboardRequestId
|
||||
let title = props.title
|
||||
|
||||
// If no specific title was provided or it's the placeholder 'default',
|
||||
// prefer the first configured root dashboard (e.g., "Test").
|
||||
if ((!title || title === 'default') && window.initResponse.rootDashboards && window.initResponse.rootDashboards.length > 0) {
|
||||
title = window.initResponse.rootDashboards[0]
|
||||
// If no specific title was provided or it's the placeholder 'default',
|
||||
// prefer the first configured root dashboard (e.g., "Test").
|
||||
if ((!title || title === 'default') && window.initResponse.rootDashboards && window.initResponse.rootDashboards.length > 0) {
|
||||
title = window.initResponse.rootDashboards[0]
|
||||
}
|
||||
|
||||
try {
|
||||
const request = {
|
||||
title
|
||||
}
|
||||
|
||||
try {
|
||||
const request = {
|
||||
title: title,
|
||||
}
|
||||
|
||||
if (props.entityType && props.entityKey) {
|
||||
request.entityType = props.entityType
|
||||
request.entityKey = props.entityKey
|
||||
}
|
||||
|
||||
const ret = await window.client.getDashboard(request)
|
||||
|
||||
if (requestId !== dashboardRequestId) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!ret || !ret.dashboard) {
|
||||
throw new Error('No dashboard found')
|
||||
}
|
||||
|
||||
dashboard.value = ret.dashboard
|
||||
const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
|
||||
document.title = ret.dashboard.title + ' - ' + pageTitle
|
||||
|
||||
// Clear any previous init error since we successfully loaded
|
||||
initError.value = null
|
||||
|
||||
// Stop the loading timer once dashboard is loaded
|
||||
if (loadingTimer) {
|
||||
clearInterval(loadingTimer)
|
||||
loadingTimer = null
|
||||
}
|
||||
|
||||
// Set attribute to indicate dashboard is loaded successfully
|
||||
document.body.setAttribute('loaded-dashboard', title || 'default')
|
||||
} catch (e) {
|
||||
if (requestId !== dashboardRequestId) {
|
||||
return
|
||||
}
|
||||
|
||||
// On error, provide a safe fallback state
|
||||
console.error('Failed to load dashboard', e)
|
||||
dashboard.value = { title: title || 'Default', contents: [] }
|
||||
const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
|
||||
document.title = 'Error - ' + pageTitle
|
||||
|
||||
// Stop the loading timer on error
|
||||
if (loadingTimer) {
|
||||
clearInterval(loadingTimer)
|
||||
loadingTimer = null
|
||||
}
|
||||
|
||||
// Set attribute even on error so tests can proceed
|
||||
document.body.setAttribute('loaded-dashboard', title || 'error')
|
||||
if (props.entityType && props.entityKey) {
|
||||
request.entityType = props.entityType
|
||||
request.entityKey = props.entityKey
|
||||
}
|
||||
}
|
||||
|
||||
function waitForInitAndLoadDashboard() {
|
||||
document.body.removeAttribute('loaded-dashboard')
|
||||
const ret = await window.client.getDashboard(request)
|
||||
|
||||
if (requestId !== dashboardRequestId) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!ret || !ret.dashboard) {
|
||||
throw new Error('No dashboard found')
|
||||
}
|
||||
|
||||
dashboard.value = ret.dashboard
|
||||
const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
|
||||
document.title = ret.dashboard.title + ' - ' + pageTitle
|
||||
|
||||
// Clear any previous errors since we successfully loaded
|
||||
initError.value = null
|
||||
loadError.value = null
|
||||
|
||||
// Stop the loading timer once dashboard is loaded
|
||||
if (loadingTimer) {
|
||||
clearInterval(loadingTimer)
|
||||
loadingTimer = null
|
||||
clearInterval(loadingTimer)
|
||||
loadingTimer = null
|
||||
}
|
||||
if (checkInitInterval) {
|
||||
|
||||
// Set attribute to indicate dashboard is loaded successfully
|
||||
document.body.setAttribute('loaded-dashboard', title || 'default')
|
||||
} catch (e) {
|
||||
if (requestId !== dashboardRequestId) {
|
||||
return
|
||||
}
|
||||
|
||||
console.error('Failed to load dashboard', e)
|
||||
dashboard.value = null
|
||||
loadError.value = e.message || 'Failed to load dashboard'
|
||||
const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
|
||||
document.title = 'Error - ' + pageTitle
|
||||
|
||||
// Stop the loading timer on error
|
||||
if (loadingTimer) {
|
||||
clearInterval(loadingTimer)
|
||||
loadingTimer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function waitForInitAndLoadDashboard () {
|
||||
document.body.removeAttribute('loaded-dashboard')
|
||||
loadError.value = null
|
||||
dashboard.value = null
|
||||
|
||||
if (loadingTimer) {
|
||||
clearInterval(loadingTimer)
|
||||
loadingTimer = null
|
||||
}
|
||||
if (checkInitInterval) {
|
||||
clearInterval(checkInitInterval)
|
||||
checkInitInterval = null
|
||||
}
|
||||
|
||||
loadingTime.value = 0
|
||||
loadingTimer = setInterval(() => {
|
||||
loadingTime.value++
|
||||
}, 1000)
|
||||
|
||||
// Check if init has completed successfully
|
||||
if (window.initResponse) {
|
||||
getDashboard()
|
||||
} else if (window.initError) {
|
||||
// Init failed, show error immediately
|
||||
initError.value = window.initErrorMessage || 'Initialization failed. Please check your configuration and try again.'
|
||||
// Stop the loading timer since we're showing an error
|
||||
if (loadingTimer) {
|
||||
clearInterval(loadingTimer)
|
||||
loadingTimer = null
|
||||
}
|
||||
} else {
|
||||
// Init hasn't completed yet, poll for completion
|
||||
checkInitInterval = setInterval(() => {
|
||||
if (window.initResponse) {
|
||||
clearInterval(checkInitInterval)
|
||||
checkInitInterval = null
|
||||
}
|
||||
|
||||
loadingTime.value = 0
|
||||
loadingTimer = setInterval(() => {
|
||||
loadingTime.value++
|
||||
}, 1000)
|
||||
|
||||
// Check if init has completed successfully
|
||||
if (window.initResponse) {
|
||||
getDashboard()
|
||||
} else if (window.initError) {
|
||||
// Init failed, show error immediately
|
||||
} else if (window.initError) {
|
||||
clearInterval(checkInitInterval)
|
||||
checkInitInterval = null
|
||||
initError.value = window.initErrorMessage || 'Initialization failed. Please check your configuration and try again.'
|
||||
// Stop the loading timer since we're showing an error
|
||||
if (loadingTimer) {
|
||||
clearInterval(loadingTimer)
|
||||
loadingTimer = null
|
||||
clearInterval(loadingTimer)
|
||||
loadingTimer = null
|
||||
}
|
||||
} else {
|
||||
// Init hasn't completed yet, poll for completion
|
||||
checkInitInterval = setInterval(() => {
|
||||
if (window.initResponse) {
|
||||
clearInterval(checkInitInterval)
|
||||
checkInitInterval = null
|
||||
getDashboard()
|
||||
} else if (window.initError) {
|
||||
clearInterval(checkInitInterval)
|
||||
checkInitInterval = null
|
||||
initError.value = window.initErrorMessage || 'Initialization failed. Please check your configuration and try again.'
|
||||
// Stop the loading timer since we're showing an error
|
||||
if (loadingTimer) {
|
||||
clearInterval(loadingTimer)
|
||||
loadingTimer = null
|
||||
}
|
||||
}
|
||||
}, 100) // Check every 100ms
|
||||
}
|
||||
}
|
||||
}, 100) // Check every 100ms
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
waitForInitAndLoadDashboard()
|
||||
waitForInitAndLoadDashboard()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [props.title, props.entityType, props.entityKey],
|
||||
() => {
|
||||
dashboard.value = null
|
||||
waitForInitAndLoadDashboard()
|
||||
}
|
||||
() => [props.title, props.entityType, props.entityKey],
|
||||
() => {
|
||||
dashboard.value = null
|
||||
waitForInitAndLoadDashboard()
|
||||
}
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
document.body.removeAttribute('loaded-dashboard')
|
||||
document.body.removeAttribute('loaded-dashboard')
|
||||
|
||||
// Clean up the timers when component is unmounted
|
||||
if (loadingTimer) {
|
||||
clearInterval(loadingTimer)
|
||||
loadingTimer = null
|
||||
}
|
||||
if (checkInitInterval) {
|
||||
clearInterval(checkInitInterval)
|
||||
checkInitInterval = null
|
||||
}
|
||||
// Clean up the timers when component is unmounted
|
||||
if (loadingTimer) {
|
||||
clearInterval(loadingTimer)
|
||||
loadingTimer = null
|
||||
}
|
||||
if (checkInitInterval) {
|
||||
clearInterval(checkInitInterval)
|
||||
checkInitInterval = null
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
|
@ -279,7 +347,6 @@ fieldset {
|
|||
place-items: stretch;
|
||||
}
|
||||
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
:id="`execution-${executionTrackingId}`"
|
||||
<div
|
||||
:id="`execution-${trackingId}`"
|
||||
class="execution-button"
|
||||
>
|
||||
<button
|
||||
|
|
@ -13,25 +13,27 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
//import { ExecutionFeedbackButton } from '../js/ExecutionFeedbackButton.js'
|
||||
// import { ExecutionFeedbackButton } from '../js/ExecutionFeedbackButton.js'
|
||||
|
||||
export default {
|
||||
name: 'ExecutionButton',
|
||||
// mixins: [ExecutionFeedbackButton],
|
||||
// mixins: [ExecutionFeedbackButton],
|
||||
props: {
|
||||
executionTrackingId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
emits: ['show'],
|
||||
data () {
|
||||
return {
|
||||
trackingId: '',
|
||||
ellapsed: 0,
|
||||
isWaiting: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
buttonText() {
|
||||
buttonText () {
|
||||
if (this.isWaiting) {
|
||||
return 'Executing...'
|
||||
} else {
|
||||
|
|
@ -39,33 +41,34 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
mounted () {
|
||||
this.trackingId = this.executionTrackingId
|
||||
this.constructFromJson(this.executionTrackingId)
|
||||
},
|
||||
methods: {
|
||||
constructFromJson(json) {
|
||||
this.executionTrackingId = json
|
||||
constructFromJson (json) {
|
||||
this.trackingId = json
|
||||
this.ellapsed = 0
|
||||
this.isWaiting = true
|
||||
},
|
||||
|
||||
show() {
|
||||
|
||||
async show () {
|
||||
this.$emit('show')
|
||||
|
||||
|
||||
if (window.executionDialog) {
|
||||
window.executionDialog.reset()
|
||||
await window.executionDialog.reset()
|
||||
window.executionDialog.show()
|
||||
window.executionDialog.fetchExecutionResult(this.executionTrackingId)
|
||||
window.executionDialog.fetchExecutionResult(this.trackingId)
|
||||
}
|
||||
},
|
||||
|
||||
onExecStatusChanged() {
|
||||
|
||||
onExecStatusChanged () {
|
||||
this.isWaiting = false
|
||||
this.domTitle = this.ellapsed + 's'
|
||||
},
|
||||
|
||||
|
||||
// Override from ExecutionFeedbackButton
|
||||
onExecutionFinished(logEntry) {
|
||||
onExecutionFinished (logEntry) {
|
||||
if (logEntry.timedOut) {
|
||||
this.renderExecutionResult('action-timeout', 'Timed out')
|
||||
} else if (logEntry.blocked) {
|
||||
|
|
@ -77,17 +80,20 @@ export default {
|
|||
this.renderExecutionResult('action-success', 'Success!')
|
||||
}
|
||||
},
|
||||
|
||||
renderExecutionResult(resultCssClass, temporaryStatusMessage) {
|
||||
|
||||
renderExecutionResult (resultCssClass, temporaryStatusMessage) {
|
||||
this.updateDom(resultCssClass, '[' + temporaryStatusMessage + ']')
|
||||
this.onExecStatusChanged()
|
||||
},
|
||||
|
||||
updateDom(resultCssClass, title) {
|
||||
|
||||
updateDom (resultCssClass, title) {
|
||||
// For execution button, we don't need to update classes as much
|
||||
// since it's a simpler component
|
||||
if (resultCssClass) {
|
||||
this.$el.classList.add(resultCssClass)
|
||||
const button = this.$el.querySelector('button')
|
||||
if (button) {
|
||||
button.classList.add(resultCssClass)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -138,4 +144,4 @@ export default {
|
|||
border-color: #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<span v-if="showLimits" class="action-group-limit">
|
||||
<span
|
||||
v-if="showLimits"
|
||||
class="action-group-limit"
|
||||
>
|
||||
{{ t('logs.action-group-limits', { concurrent: maxConcurrent, queueSize }) }}
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,21 @@
|
|||
<template>
|
||||
<span class="action-icon-glyph">
|
||||
<HugeiconsIcon
|
||||
v-if="hugeiconsModel"
|
||||
:icon="hugeiconsModel"
|
||||
width="1em"
|
||||
height="1em"
|
||||
class="action-icon-glyph-svg"
|
||||
/>
|
||||
<span v-else-if="decodedTextGlyphIsHtml" v-html="decodedTextGlyph"></span>
|
||||
<span v-else v-text="decodedTextGlyph"></span>
|
||||
</span>
|
||||
<span class="action-icon-glyph">
|
||||
<HugeiconsIcon
|
||||
v-if="hugeiconsModel"
|
||||
:icon="hugeiconsModel"
|
||||
width="1em"
|
||||
height="1em"
|
||||
class="action-icon-glyph-svg"
|
||||
/>
|
||||
<span
|
||||
v-else-if="decodedTextGlyphIsHtml"
|
||||
v-html="decodedTextGlyph"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
v-text="decodedTextGlyph"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
|
@ -22,38 +28,38 @@ const hugeiconsPrefix = 'hugeicons:'
|
|||
|
||||
/** Maps config values like hugeicons:CommandLineIcon to Hugeicons icon definitions. */
|
||||
const hugeiconsRegistry = {
|
||||
CommandLineIcon,
|
||||
CommandLineIcon
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
glyph: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
glyph: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const hugeiconsModel = computed(() => {
|
||||
if (!props.glyph) {
|
||||
return CommandLineIcon
|
||||
}
|
||||
if (!props.glyph) {
|
||||
return CommandLineIcon
|
||||
}
|
||||
|
||||
if (!props.glyph.startsWith(hugeiconsPrefix)) {
|
||||
return null
|
||||
}
|
||||
if (!props.glyph.startsWith(hugeiconsPrefix)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const name = props.glyph.slice(hugeiconsPrefix.length)
|
||||
const iconModel = hugeiconsRegistry[name]
|
||||
const name = props.glyph.slice(hugeiconsPrefix.length)
|
||||
const iconModel = hugeiconsRegistry[name]
|
||||
|
||||
return iconModel ?? CommandLineIcon
|
||||
return iconModel ?? CommandLineIcon
|
||||
})
|
||||
|
||||
const decodedTextGlyph = computed(() => {
|
||||
if (hugeiconsModel.value) {
|
||||
return ''
|
||||
}
|
||||
if (hugeiconsModel.value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return decodeHtmlEntities(props.glyph)
|
||||
return decodeHtmlEntities(props.glyph)
|
||||
})
|
||||
|
||||
const decodedTextGlyphIsHtml = computed(() => glyphLooksLikeHtml(decodedTextGlyph.value))
|
||||
|
|
|
|||
|
|
@ -1,86 +1,91 @@
|
|||
<template>
|
||||
<div :class = "statusClass + ' annotation'">
|
||||
<router-link v-if="showQueueLink" to="/logs/queue" class="queue-status-link">{{ statusText }}</router-link>
|
||||
<span v-else>{{ statusText }}</span><span>{{ exitCodeText }}</span>
|
||||
</div>
|
||||
|
||||
<div :class="statusClass + ' annotation'">
|
||||
<router-link
|
||||
v-if="showQueueLink"
|
||||
to="/logs/queue"
|
||||
class="queue-status-link"
|
||||
>
|
||||
{{ statusText }}
|
||||
</router-link>
|
||||
<span v-else>{{ statusText }}</span><span>{{ exitCodeText }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
logEntry: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
linkQueuedStatus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
logEntry: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
linkQueuedStatus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
function isWaitingInQueue(logEntry) {
|
||||
return logEntry &&
|
||||
function isWaitingInQueue (logEntry) {
|
||||
return logEntry &&
|
||||
!logEntry.executionFinished &&
|
||||
!logEntry.executionStarted
|
||||
}
|
||||
|
||||
const statusText = computed(() => {
|
||||
const logEntry = props.logEntry
|
||||
if (!logEntry) return 'unknown'
|
||||
const logEntry = props.logEntry
|
||||
if (!logEntry) return 'unknown'
|
||||
|
||||
if (logEntry.executionFinished) {
|
||||
if (logEntry.blocked) {
|
||||
return 'Blocked'
|
||||
} else if (logEntry.timedOut) {
|
||||
return 'Timed out'
|
||||
} else {
|
||||
return 'Completed'
|
||||
}
|
||||
if (logEntry.executionFinished) {
|
||||
if (logEntry.blocked) {
|
||||
return 'Blocked'
|
||||
} else if (logEntry.timedOut) {
|
||||
return 'Timed out'
|
||||
} else {
|
||||
return 'Completed'
|
||||
}
|
||||
}
|
||||
|
||||
if (isWaitingInQueue(logEntry)) {
|
||||
return 'Queued'
|
||||
}
|
||||
if (isWaitingInQueue(logEntry)) {
|
||||
return 'Queued'
|
||||
}
|
||||
|
||||
return 'Still running...'
|
||||
return 'Still running...'
|
||||
})
|
||||
|
||||
const exitCodeText = computed(() => {
|
||||
const logEntry = props.logEntry
|
||||
if (!logEntry) return ''
|
||||
if (logEntry.exitCode === 0) {
|
||||
return ''
|
||||
}
|
||||
if (logEntry.executionFinished) {
|
||||
if (logEntry.blocked || logEntry.timedOut) {
|
||||
return ''
|
||||
}
|
||||
return ' (Exit code: ' + logEntry.exitCode + ')'
|
||||
}
|
||||
const logEntry = props.logEntry
|
||||
if (!logEntry) return ''
|
||||
if (logEntry.exitCode === 0) {
|
||||
return ''
|
||||
}
|
||||
if (logEntry.executionFinished) {
|
||||
if (logEntry.blocked || logEntry.timedOut) {
|
||||
return ''
|
||||
}
|
||||
return ' (Exit code: ' + logEntry.exitCode + ')'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const showQueueLink = computed(() => {
|
||||
return props.linkQueuedStatus && isWaitingInQueue(props.logEntry)
|
||||
return props.linkQueuedStatus && isWaitingInQueue(props.logEntry)
|
||||
})
|
||||
|
||||
const statusClass = computed(() => {
|
||||
const logEntry = props.logEntry
|
||||
if (!logEntry) return ''
|
||||
if (logEntry.executionFinished) {
|
||||
if (logEntry.blocked) {
|
||||
return 'status-blocked'
|
||||
} else if (logEntry.timedOut) {
|
||||
return 'status-timeout'
|
||||
} else if (logEntry.exitCode === 0) {
|
||||
return 'status-success'
|
||||
} else {
|
||||
return 'status-nonzero-exit'
|
||||
}
|
||||
const logEntry = props.logEntry
|
||||
if (!logEntry) return ''
|
||||
if (logEntry.executionFinished) {
|
||||
if (logEntry.blocked) {
|
||||
return 'status-blocked'
|
||||
} else if (logEntry.timedOut) {
|
||||
return 'status-timeout'
|
||||
} else if (logEntry.exitCode === 0) {
|
||||
return 'status-success'
|
||||
} else {
|
||||
return 'status-nonzero-exit'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
return ''
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -110,5 +115,4 @@ const statusClass = computed(() => {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,50 @@
|
|||
<template>
|
||||
<div id = "breadcrumbs">
|
||||
<template v-for="(link, index) in links" :key="link.name">
|
||||
<router-link :to="link.href">{{ link.name }}</router-link>
|
||||
<span v-if="index < links.length - 1" class="separator">
|
||||
»
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<div id="breadcrumbs">
|
||||
<template
|
||||
v-for="(link, index) in links"
|
||||
:key="link.name"
|
||||
>
|
||||
<router-link :to="link.href">
|
||||
{{ link.name }}
|
||||
</router-link>
|
||||
<span
|
||||
v-if="index < links.length - 1"
|
||||
class="separator"
|
||||
>
|
||||
»
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const links = ref([])
|
||||
|
||||
watch(() => route.matched, (matched) => {
|
||||
links.value = []
|
||||
matched.forEach((record) => {
|
||||
if (!record) return
|
||||
if (record.meta && record.meta.breadcrumb) {
|
||||
record.meta.breadcrumb.forEach((item) => {
|
||||
links.value.push({
|
||||
name: item.name,
|
||||
href: item.href || record.path || '/'
|
||||
})
|
||||
})
|
||||
} else if (record.name) {
|
||||
links.value.push({
|
||||
name: record.name,
|
||||
href: record.path || '/'
|
||||
})
|
||||
}
|
||||
})
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
span {
|
||||
color: #bbb;
|
||||
|
|
@ -26,34 +62,3 @@ a:hover {
|
|||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const links = ref([]);
|
||||
|
||||
watch(() => route.matched, (matched) => {
|
||||
|
||||
links.value = [];
|
||||
matched.forEach((record) => {
|
||||
if (!record) return;
|
||||
if (record.meta && record.meta.breadcrumb) {
|
||||
record.meta.breadcrumb.forEach((item) => {
|
||||
links.value.push({
|
||||
name: item.name,
|
||||
href: item.href || record.path || '/'
|
||||
});
|
||||
});
|
||||
} else if (record.name) {
|
||||
links.value.push({
|
||||
name: record.name,
|
||||
href: record.path || '/'
|
||||
});
|
||||
}
|
||||
});
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,28 @@
|
|||
<template>
|
||||
<div class="choice-checklist" :id="wrapperId">
|
||||
<div
|
||||
:id="wrapperId"
|
||||
class="choice-checklist"
|
||||
>
|
||||
<div class="choice-checklist-controls">
|
||||
<button type="button" class="choice-checklist-control" @click="selectAll">
|
||||
<button
|
||||
type="button"
|
||||
class="choice-checklist-control"
|
||||
@click="selectAll"
|
||||
>
|
||||
Select all
|
||||
</button>
|
||||
<button type="button" class="choice-checklist-control" @click="selectNone">
|
||||
<button
|
||||
type="button"
|
||||
class="choice-checklist-control"
|
||||
@click="selectNone"
|
||||
>
|
||||
Select none
|
||||
</button>
|
||||
</div>
|
||||
<fieldset class="choice-checklist-fieldset">
|
||||
<legend class="visually-hidden">{{ label || name }}</legend>
|
||||
<legend class="visually-hidden">
|
||||
{{ label || name }}
|
||||
</legend>
|
||||
<label
|
||||
v-for="(choice, index) in choices"
|
||||
:key="choice.value"
|
||||
|
|
@ -21,7 +34,7 @@
|
|||
type="checkbox"
|
||||
:checked="isSelected(choice.value)"
|
||||
@change="handleToggle(choice.value)"
|
||||
/>
|
||||
>
|
||||
<span>{{ choiceLabel(choice) }}</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
|
@ -34,7 +47,7 @@
|
|||
:required="required"
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -86,23 +99,23 @@ function optionId (index) {
|
|||
return argumentFieldOptionId(props.name, index)
|
||||
}
|
||||
|
||||
function isSelected(value) {
|
||||
function isSelected (value) {
|
||||
return selectedValues.value.includes(value)
|
||||
}
|
||||
|
||||
function emitSelection(selected) {
|
||||
function emitSelection (selected) {
|
||||
emit('update:modelValue', formatChecklistValue(selected))
|
||||
}
|
||||
|
||||
function handleToggle(value) {
|
||||
function handleToggle (value) {
|
||||
emitSelection(toggleChoice(selectedValues.value, value))
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
function selectAll () {
|
||||
emitSelection(allChoiceValues(props.choices))
|
||||
}
|
||||
|
||||
function selectNone() {
|
||||
function selectNone () {
|
||||
emitSelection([])
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
<template>
|
||||
<div class="choice-combobox" ref="rootRef">
|
||||
<div
|
||||
ref="rootRef"
|
||||
class="choice-combobox"
|
||||
>
|
||||
<input
|
||||
ref="searchInputRef"
|
||||
:id="id"
|
||||
ref="searchInputRef"
|
||||
type="text"
|
||||
class="choice-combobox-input"
|
||||
role="combobox"
|
||||
|
|
@ -17,12 +20,12 @@
|
|||
@input="handleSearchInput"
|
||||
@keydown="handleKeydown"
|
||||
@blur="handleBlur"
|
||||
/>
|
||||
>
|
||||
<input
|
||||
:name="name"
|
||||
type="hidden"
|
||||
:value="modelValue"
|
||||
/>
|
||||
>
|
||||
<ul
|
||||
v-if="isOpen && filteredChoices.length > 0"
|
||||
:id="listboxId"
|
||||
|
|
@ -44,7 +47,10 @@
|
|||
{{ choiceLabel(choice) }}
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else-if="isOpen && query" class="choice-combobox-list choice-combobox-empty">
|
||||
<div
|
||||
v-else-if="isOpen && query"
|
||||
class="choice-combobox-list choice-combobox-empty"
|
||||
>
|
||||
No matching options
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -140,11 +146,11 @@ watch([() => props.modelValue, () => props.choices], () => {
|
|||
}
|
||||
}, { immediate: true })
|
||||
|
||||
function choiceLabel(choice) {
|
||||
function choiceLabel (choice) {
|
||||
return choiceDisplayLabel(choice)
|
||||
}
|
||||
|
||||
function syncFromModelValue() {
|
||||
function syncFromModelValue () {
|
||||
const next = syncStateFromModelValue(props.choices, props.modelValue)
|
||||
query.value = next.query
|
||||
|
||||
|
|
@ -153,12 +159,12 @@ function syncFromModelValue() {
|
|||
}
|
||||
}
|
||||
|
||||
function selectedChoiceIndex(choices) {
|
||||
function selectedChoiceIndex (choices) {
|
||||
const index = choices.findIndex(choice => choice.value === props.modelValue)
|
||||
return index >= 0 ? index : 0
|
||||
}
|
||||
|
||||
function openList() {
|
||||
function openList () {
|
||||
document.dispatchEvent(new CustomEvent(closeOthersEvent, { detail: { id: props.id } }))
|
||||
const wasClosed = !isOpen.value
|
||||
isOpen.value = true
|
||||
|
|
@ -167,23 +173,23 @@ function openList() {
|
|||
}
|
||||
}
|
||||
|
||||
function closeList() {
|
||||
function closeList () {
|
||||
isOpen.value = false
|
||||
isUserFiltering.value = false
|
||||
syncFromModelValue()
|
||||
}
|
||||
|
||||
function emitValue(value) {
|
||||
function emitValue (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
function selectChoice(choice) {
|
||||
function selectChoice (choice) {
|
||||
emitValue(choice.value)
|
||||
query.value = choiceLabel(choice)
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
function handleFocus() {
|
||||
function handleFocus () {
|
||||
if (!isOpen.value) {
|
||||
syncFromModelValue()
|
||||
isUserFiltering.value = false
|
||||
|
|
@ -192,14 +198,14 @@ function handleFocus() {
|
|||
openList()
|
||||
}
|
||||
|
||||
function handleSearchInput(event) {
|
||||
function handleSearchInput (event) {
|
||||
isUserFiltering.value = true
|
||||
query.value = event.target.value
|
||||
openList()
|
||||
highlightedIndex.value = 0
|
||||
}
|
||||
|
||||
function moveHighlight(delta) {
|
||||
function moveHighlight (delta) {
|
||||
if (filteredChoices.value.length === 0) {
|
||||
return
|
||||
}
|
||||
|
|
@ -218,7 +224,7 @@ function moveHighlight(delta) {
|
|||
highlightedIndex.value = nextIndex
|
||||
}
|
||||
|
||||
function handleKeydown(event) {
|
||||
function handleKeydown (event) {
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault()
|
||||
const wasOpen = isOpen.value
|
||||
|
|
@ -258,17 +264,17 @@ function handleKeydown(event) {
|
|||
}
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
function handleBlur () {
|
||||
closeList()
|
||||
}
|
||||
|
||||
function handleCloseOthers(event) {
|
||||
function handleCloseOthers (event) {
|
||||
if (event.detail.id !== props.id) {
|
||||
closeList()
|
||||
}
|
||||
}
|
||||
|
||||
function handleOutsideMouseDown(event) {
|
||||
function handleOutsideMouseDown (event) {
|
||||
if (!isOpen.value || rootRef.value?.contains(event.target)) {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,22 @@
|
|||
<template>
|
||||
<span id="connection-banner" v-if="connectionState.showDisconnectedBanner" class="inline-notification critical user-info-connection">
|
||||
<span class="connection-banner-sr-only" role="status">{{ staticAnnouncement }}</span>
|
||||
<span aria-hidden="true">
|
||||
<a :href="websocketDocsUrl" target="_blank" rel="noopener noreferrer" class="connection-banner-link">{{ linkText }}</a>{{ bannerSuffix }}
|
||||
</span>
|
||||
<span
|
||||
v-if="connectionState.showDisconnectedBanner"
|
||||
id="connection-banner"
|
||||
class="inline-notification critical user-info-connection"
|
||||
>
|
||||
<span
|
||||
class="connection-banner-sr-only"
|
||||
role="status"
|
||||
>{{ staticAnnouncement }}</span>
|
||||
<span aria-hidden="true">
|
||||
<a
|
||||
:href="websocketDocsUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="connection-banner-link"
|
||||
>{{ linkText }}</a>{{ bannerSuffix }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
|
@ -18,7 +30,7 @@ const websocketDocsUrl = 'https://docs.olivetin.app/troubleshooting/err-websocke
|
|||
|
||||
const linkText = computed(() => t('disconnected-banner-link-text'))
|
||||
|
||||
function formatShortRelative(ms) {
|
||||
function formatShortRelative (ms) {
|
||||
if (ms < 0) return '0s'
|
||||
const secs = Math.floor(ms / 1000)
|
||||
const mins = Math.floor(secs / 60)
|
||||
|
|
@ -28,7 +40,7 @@ function formatShortRelative(ms) {
|
|||
return `${secs}s`
|
||||
}
|
||||
|
||||
function formatShortTime(ts) {
|
||||
function formatShortTime (ts) {
|
||||
if (ts == null) return '--:--'
|
||||
return new Date(ts).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,39 @@
|
|||
<template>
|
||||
<ActionButton v-if="component.type == 'link'" :actionData="component.action" :cssClass="component.cssClass" :key="component.title" />
|
||||
<ActionButton
|
||||
v-if="component.type == 'link'"
|
||||
:key="component.title"
|
||||
:action-data="component.action"
|
||||
:css-class="component.cssClass"
|
||||
/>
|
||||
|
||||
<DashboardComponentDirectory v-else-if="component.type == 'directory'" :component="component" />
|
||||
<DashboardComponentDirectory
|
||||
v-else-if="component.type == 'directory'"
|
||||
:component="component"
|
||||
/>
|
||||
|
||||
<DashboardComponentDisplay v-else-if="component.type == 'display'" :component="component" />
|
||||
<DashboardComponentDisplay
|
||||
v-else-if="component.type == 'display'"
|
||||
:component="component"
|
||||
/>
|
||||
|
||||
<DashboardComponentMostRecentExecution v-else-if="component.type == 'stdout-most-recent-execution'" :component="component" />
|
||||
<DashboardComponentMostRecentExecution
|
||||
v-else-if="component.type == 'stdout-most-recent-execution'"
|
||||
:component="component"
|
||||
/>
|
||||
|
||||
<template v-else-if="component.type == 'fieldset'">
|
||||
<template v-for="subcomponent in component.contents" :key="subcomponent.title">
|
||||
<DashboardComponent :component="subcomponent" />
|
||||
</template>
|
||||
<template v-else-if="component.type == 'fieldset'">
|
||||
<template
|
||||
v-for="subcomponent in component.contents"
|
||||
:key="subcomponent.title"
|
||||
>
|
||||
<DashboardComponent :component="subcomponent" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<div v-else>
|
||||
OTHER: {{ component.type }}
|
||||
{{ component }}
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
OTHER: {{ component.type }}
|
||||
{{ component }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
|
@ -26,10 +42,10 @@ import DashboardComponentMostRecentExecution from './DashboardComponentMostRecen
|
|||
import DashboardComponentDirectory from './DashboardComponentDirectory.vue'
|
||||
import DashboardComponentDisplay from './DashboardComponentDisplay.vue'
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
<template>
|
||||
<button class="directory-button" :class="component.cssClass" @click="navigateToDirectory">
|
||||
<span class="icon" v-html="unicodeIcon"></span>
|
||||
<span class="title">{{ component.title }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="directory-button"
|
||||
:class="component.cssClass"
|
||||
@click="navigateToDirectory"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
v-html="unicodeIcon"
|
||||
/>
|
||||
<span class="title">{{ component.title }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
|
@ -12,33 +19,33 @@ import { computed } from 'vue'
|
|||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
function getUnicodeIcon(icon) {
|
||||
if (icon === '' || !icon) {
|
||||
return '📁' // Default folder icon
|
||||
} else {
|
||||
return unescape(icon)
|
||||
}
|
||||
function getUnicodeIcon (icon) {
|
||||
if (icon === '' || !icon) {
|
||||
return '📁' // Default folder icon
|
||||
} else {
|
||||
return unescape(icon)
|
||||
}
|
||||
}
|
||||
|
||||
const unicodeIcon = computed(() => {
|
||||
return getUnicodeIcon(props.component.icon)
|
||||
return getUnicodeIcon(props.component.icon)
|
||||
})
|
||||
|
||||
function navigateToDirectory() {
|
||||
const params = { title: props.component.title }
|
||||
function navigateToDirectory () {
|
||||
const params = { title: props.component.title }
|
||||
|
||||
if (props.component.entityType && props.component.entityKey) {
|
||||
params.entityType = props.component.entityType
|
||||
params.entityKey = props.component.entityKey
|
||||
}
|
||||
if (props.component.entityType && props.component.entityKey) {
|
||||
params.entityType = props.component.entityType
|
||||
params.entityKey = props.component.entityKey
|
||||
}
|
||||
|
||||
router.push({ name: 'Dashboard', params })
|
||||
router.push({ name: 'Dashboard', params })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
<template>
|
||||
<div class="display" :class="component.cssClass">
|
||||
<div v-html="component.title" />
|
||||
</div>
|
||||
<div
|
||||
class="display"
|
||||
:class="component.cssClass"
|
||||
>
|
||||
<div v-html="component.title" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
<template>
|
||||
<div class="mre-container" :class="component.cssClass">
|
||||
<div
|
||||
class="mre-container"
|
||||
:class="component.cssClass"
|
||||
>
|
||||
<router-link
|
||||
v-if="executionTrackingId"
|
||||
:to="`/logs/${executionTrackingId}`"
|
||||
class="mre-link"
|
||||
v-if="executionTrackingId"
|
||||
:to="`/logs/${executionTrackingId}`"
|
||||
class="mre-link"
|
||||
>
|
||||
<pre class="mre-output">{{ output }}</pre>
|
||||
<pre class="mre-output">{{ output }}</pre>
|
||||
</router-link>
|
||||
<pre v-else class="mre-output fg-important">{{ output }}</pre>
|
||||
<pre
|
||||
v-else
|
||||
class="mre-output fg-important"
|
||||
>{{ output }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -26,7 +32,7 @@ const output = ref('Waiting...')
|
|||
const executionTrackingId = ref(null)
|
||||
let unwatchButtonResults = null
|
||||
|
||||
function updateFromLogEntry(logEntry) {
|
||||
function updateFromLogEntry (logEntry) {
|
||||
if (logEntry) {
|
||||
if (logEntry.output !== undefined) {
|
||||
output.value = logEntry.output
|
||||
|
|
@ -39,7 +45,7 @@ function updateFromLogEntry(logEntry) {
|
|||
}
|
||||
}
|
||||
|
||||
async function fetchMostRecentExecution() {
|
||||
async function fetchMostRecentExecution () {
|
||||
if (!props.component.title) {
|
||||
output.value = 'Error: No action ID specified'
|
||||
executionTrackingId.value = null
|
||||
|
|
|
|||
|
|
@ -1,167 +1,194 @@
|
|||
<template>
|
||||
<Section :padding="!hasTable">
|
||||
<template #title>
|
||||
<span class="section-title-with-icon">
|
||||
Entity:
|
||||
<ActionIconGlyph v-if="definition.icon" class="entity-title-icon" :glyph="definition.icon" />
|
||||
{{ definition.title }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="hasTable" #toolbar>
|
||||
<EntityListFilter v-model="searchText" />
|
||||
</template>
|
||||
<Section :padding="!hasTable">
|
||||
<template #title>
|
||||
<span class="section-title-with-icon">
|
||||
Entity:
|
||||
<ActionIconGlyph
|
||||
v-if="definition.icon"
|
||||
class="entity-title-icon"
|
||||
:glyph="definition.icon"
|
||||
/>
|
||||
{{ definition.title }}
|
||||
</span>
|
||||
</template>
|
||||
<template
|
||||
v-if="hasTable"
|
||||
#toolbar
|
||||
>
|
||||
<EntityListFilter v-model="searchText" />
|
||||
</template>
|
||||
|
||||
<p v-if="!hasTable">{{ definition.instances.length }} instances.</p>
|
||||
<p v-if="!hasTable">
|
||||
{{ definition.instances.length }} instances.
|
||||
</p>
|
||||
|
||||
<template v-if="hasTable">
|
||||
<p v-if="tableError" class="table-error padding" role="alert">{{ tableError }}</p>
|
||||
<EntityInstancesTable
|
||||
v-else
|
||||
:instances="tableInstances"
|
||||
:properties="definition.properties"
|
||||
:total-instances="totalInstances"
|
||||
v-model:page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="hasTable">
|
||||
<p
|
||||
v-if="tableError"
|
||||
class="table-error padding"
|
||||
role="alert"
|
||||
>
|
||||
{{ tableError }}
|
||||
</p>
|
||||
<EntityInstancesTable
|
||||
v-else
|
||||
v-model:page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:instances="tableInstances"
|
||||
:properties="definition.properties"
|
||||
:total-instances="totalInstances"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<ul v-else>
|
||||
<li v-for="inst in definition.instances" :key="inst.uniqueKey">
|
||||
<router-link :to="entityDetailsRoute(inst)">
|
||||
{{ inst.title }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-else>
|
||||
<li
|
||||
v-for="inst in definition.instances"
|
||||
:key="inst.uniqueKey"
|
||||
>
|
||||
<router-link :to="entityDetailsRoute(inst)">
|
||||
{{ inst.title }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="usedDashboards.length > 0" :class="{ padding: hasTable }">
|
||||
<h3>Used on Dashboards:</h3>
|
||||
<ul>
|
||||
<li v-for="dash in usedDashboards" :key="dash">
|
||||
<template v-if="isEntityDirectory(dash)">
|
||||
{{ getDashboardTitle(dash) }} <span class="entity-directory-label">[Entity Directory]</span>
|
||||
</template>
|
||||
<router-link v-else-if="!dash.includes('entity:')" :to="{ name: 'Dashboard', params: { title: getDashboardTitle(dash) } }">
|
||||
{{ getDashboardTitle(dash) }}
|
||||
</router-link>
|
||||
<span v-else>{{ dash }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Section>
|
||||
<div
|
||||
v-if="usedDashboards.length > 0"
|
||||
:class="{ padding: hasTable }"
|
||||
>
|
||||
<h3>Used on Dashboards:</h3>
|
||||
<ul>
|
||||
<li
|
||||
v-for="dash in usedDashboards"
|
||||
:key="dash"
|
||||
>
|
||||
<template v-if="isEntityDirectory(dash)">
|
||||
{{ getDashboardTitle(dash) }} <span class="entity-directory-label">[Entity Directory]</span>
|
||||
</template>
|
||||
<router-link
|
||||
v-else-if="!dash.includes('entity:')"
|
||||
:to="{ name: 'Dashboard', params: { title: getDashboardTitle(dash) } }"
|
||||
>
|
||||
{{ getDashboardTitle(dash) }}
|
||||
</router-link>
|
||||
<span v-else>{{ dash }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import Section from 'picocrank/vue/components/Section.vue'
|
||||
import ActionIconGlyph from './ActionIconGlyph.vue'
|
||||
import EntityInstancesTable from './EntityInstancesTable.vue'
|
||||
import EntityListFilter from './EntityListFilter.vue'
|
||||
import { entityDetailsRoute } from '../utils/entityRoutes.js'
|
||||
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import Section from 'picocrank/vue/components/Section.vue'
|
||||
import ActionIconGlyph from './ActionIconGlyph.vue'
|
||||
import EntityInstancesTable from './EntityInstancesTable.vue'
|
||||
import EntityListFilter from './EntityListFilter.vue'
|
||||
import { entityDetailsRoute } from '../utils/entityRoutes.js'
|
||||
|
||||
const props = defineProps({
|
||||
definition: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const props = defineProps({
|
||||
definition: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const searchText = ref('')
|
||||
const tableInstances = ref([])
|
||||
const totalInstances = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const tableError = ref('')
|
||||
let fetchTimer = null
|
||||
let fetchSequence = 0
|
||||
const searchText = ref('')
|
||||
const tableInstances = ref([])
|
||||
const totalInstances = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const tableError = ref('')
|
||||
let fetchTimer = null
|
||||
let fetchSequence = 0
|
||||
|
||||
const hasTable = computed(() => (props.definition.properties?.length ?? 0) > 0)
|
||||
const hasTable = computed(() => (props.definition.properties?.length ?? 0) > 0)
|
||||
|
||||
const usedDashboards = computed(() => filteredDashboards(props.definition.usedOnDashboards ?? []))
|
||||
const usedDashboards = computed(() => filteredDashboards(props.definition.usedOnDashboards ?? []))
|
||||
|
||||
watch(searchText, () => {
|
||||
currentPage.value = 1
|
||||
scheduleFetchTableInstances()
|
||||
})
|
||||
watch(searchText, () => {
|
||||
currentPage.value = 1
|
||||
scheduleFetchTableInstances()
|
||||
})
|
||||
|
||||
watch([currentPage, pageSize], () => {
|
||||
scheduleFetchTableInstances()
|
||||
})
|
||||
watch([currentPage, pageSize], () => {
|
||||
scheduleFetchTableInstances()
|
||||
})
|
||||
|
||||
function filteredDashboards(dashboards) {
|
||||
return dashboards.filter(d => d && !d.includes('{{'))
|
||||
}
|
||||
function filteredDashboards (dashboards) {
|
||||
return dashboards.filter(d => d && !d.includes('{{'))
|
||||
}
|
||||
|
||||
function isEntityDirectory(dashboardTitle) {
|
||||
return dashboardTitle.endsWith(' [Entity Directory]')
|
||||
}
|
||||
function isEntityDirectory (dashboardTitle) {
|
||||
return dashboardTitle.endsWith(' [Entity Directory]')
|
||||
}
|
||||
|
||||
function getDashboardTitle(dashboardTitle) {
|
||||
if (isEntityDirectory(dashboardTitle)) {
|
||||
return dashboardTitle.slice(0, -' [Entity Directory]'.length)
|
||||
}
|
||||
return dashboardTitle
|
||||
}
|
||||
function getDashboardTitle (dashboardTitle) {
|
||||
if (isEntityDirectory(dashboardTitle)) {
|
||||
return dashboardTitle.slice(0, -' [Entity Directory]'.length)
|
||||
}
|
||||
return dashboardTitle
|
||||
}
|
||||
|
||||
function scheduleFetchTableInstances() {
|
||||
if (!hasTable.value) {
|
||||
return
|
||||
}
|
||||
function scheduleFetchTableInstances () {
|
||||
if (!hasTable.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (fetchTimer) {
|
||||
clearTimeout(fetchTimer)
|
||||
}
|
||||
if (fetchTimer) {
|
||||
clearTimeout(fetchTimer)
|
||||
}
|
||||
|
||||
fetchTimer = setTimeout(() => {
|
||||
fetchTableInstances()
|
||||
}, 250)
|
||||
}
|
||||
fetchTimer = setTimeout(() => {
|
||||
fetchTableInstances()
|
||||
}, 250)
|
||||
}
|
||||
|
||||
async function fetchTableInstances() {
|
||||
if (!hasTable.value) {
|
||||
return
|
||||
}
|
||||
async function fetchTableInstances () {
|
||||
if (!hasTable.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = ++fetchSequence
|
||||
tableError.value = ''
|
||||
try {
|
||||
const response = await window.client.getEntities({
|
||||
entityType: props.definition.title,
|
||||
filter: searchText.value.trim(),
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value
|
||||
})
|
||||
const requestId = ++fetchSequence
|
||||
tableError.value = ''
|
||||
try {
|
||||
const response = await window.client.getEntities({
|
||||
entityType: props.definition.title,
|
||||
filter: searchText.value.trim(),
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value
|
||||
})
|
||||
|
||||
if (requestId !== fetchSequence) {
|
||||
return
|
||||
}
|
||||
if (requestId !== fetchSequence) {
|
||||
return
|
||||
}
|
||||
|
||||
const definition = response.entityDefinitions?.find(def => def.title === props.definition.title)
|
||||
tableInstances.value = definition?.instances ?? []
|
||||
totalInstances.value = definition?.totalInstances ?? 0
|
||||
} catch (err) {
|
||||
if (requestId !== fetchSequence) {
|
||||
return
|
||||
}
|
||||
const definition = response.entityDefinitions?.find(def => def.title === props.definition.title)
|
||||
tableInstances.value = definition?.instances ?? []
|
||||
totalInstances.value = definition?.totalInstances ?? 0
|
||||
} catch (err) {
|
||||
if (requestId !== fetchSequence) {
|
||||
return
|
||||
}
|
||||
|
||||
console.error('Failed to fetch entity instances:', err)
|
||||
tableError.value = 'Failed to load entity instances.'
|
||||
tableInstances.value = []
|
||||
totalInstances.value = 0
|
||||
}
|
||||
}
|
||||
console.error('Failed to fetch entity instances:', err)
|
||||
tableError.value = 'Failed to load entity instances.'
|
||||
tableInstances.value = []
|
||||
totalInstances.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (hasTable.value) {
|
||||
fetchTableInstances()
|
||||
}
|
||||
})
|
||||
onMounted(() => {
|
||||
if (hasTable.value) {
|
||||
fetchTableInstances()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (fetchTimer) {
|
||||
clearTimeout(fetchTimer)
|
||||
fetchTimer = null
|
||||
}
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
if (fetchTimer) {
|
||||
clearTimeout(fetchTimer)
|
||||
fetchTimer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -1,87 +1,90 @@
|
|||
<template>
|
||||
<Table
|
||||
:data="tableRows"
|
||||
:headers="headers"
|
||||
:show-pagination="false"
|
||||
>
|
||||
<template #cell-title="{ row, value }">
|
||||
<router-link :to="entityDetailsRoute(row)">
|
||||
{{ value }}
|
||||
</router-link>
|
||||
</template>
|
||||
</Table>
|
||||
<Table
|
||||
:data="tableRows"
|
||||
:headers="headers"
|
||||
:show-pagination="false"
|
||||
>
|
||||
<template #cell-title="{ row, value }">
|
||||
<router-link :to="entityDetailsRoute(row)">
|
||||
{{ value }}
|
||||
</router-link>
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<div v-if="totalInstances > 0" class="padding">
|
||||
<Pagination
|
||||
:total="totalInstances"
|
||||
v-model:page="currentPageModel"
|
||||
v-model:page-size="pageSizeModel"
|
||||
item-title="entities"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="totalInstances > 0"
|
||||
class="padding"
|
||||
>
|
||||
<Pagination
|
||||
v-model:page="currentPageModel"
|
||||
v-model:page-size="pageSizeModel"
|
||||
:total="totalInstances"
|
||||
item-title="entities"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import Table from 'picocrank/vue/components/Table.vue'
|
||||
import Pagination from 'picocrank/vue/components/Pagination.vue'
|
||||
import { entityDetailsRoute } from '../utils/entityRoutes.js'
|
||||
import { computed } from 'vue'
|
||||
import Table from 'picocrank/vue/components/Table.vue'
|
||||
import Pagination from 'picocrank/vue/components/Pagination.vue'
|
||||
import { entityDetailsRoute } from '../utils/entityRoutes.js'
|
||||
|
||||
const props = defineProps({
|
||||
instances: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
properties: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
totalInstances: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 10
|
||||
}
|
||||
})
|
||||
const props = defineProps({
|
||||
instances: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
properties: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
totalInstances: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 10
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:page', 'update:pageSize'])
|
||||
const emit = defineEmits(['update:page', 'update:pageSize'])
|
||||
|
||||
const headers = computed(() => {
|
||||
const propertyHeaders = props.properties.map(property => ({
|
||||
key: property.name,
|
||||
label: property.title
|
||||
}))
|
||||
const headers = computed(() => {
|
||||
const propertyHeaders = props.properties.map(property => ({
|
||||
key: property.name,
|
||||
label: property.title
|
||||
}))
|
||||
|
||||
return [
|
||||
{ key: 'title', label: 'Name' },
|
||||
...propertyHeaders
|
||||
]
|
||||
})
|
||||
return [
|
||||
{ key: 'title', label: 'Name' },
|
||||
...propertyHeaders
|
||||
]
|
||||
})
|
||||
|
||||
const tableRows = computed(() =>
|
||||
props.instances.map(instance => ({
|
||||
...instance.fields,
|
||||
title: instance.title,
|
||||
type: instance.type,
|
||||
uniqueKey: instance.uniqueKey
|
||||
}))
|
||||
)
|
||||
const tableRows = computed(() =>
|
||||
props.instances.map(instance => ({
|
||||
...instance.fields,
|
||||
title: instance.title,
|
||||
type: instance.type,
|
||||
uniqueKey: instance.uniqueKey
|
||||
}))
|
||||
)
|
||||
|
||||
const currentPageModel = computed({
|
||||
get: () => props.page,
|
||||
set: value => emit('update:page', value)
|
||||
})
|
||||
const currentPageModel = computed({
|
||||
get: () => props.page,
|
||||
set: value => emit('update:page', value)
|
||||
})
|
||||
|
||||
const pageSizeModel = computed({
|
||||
get: () => props.pageSize,
|
||||
set: value => emit('update:pageSize', value)
|
||||
})
|
||||
const pageSizeModel = computed({
|
||||
get: () => props.pageSize,
|
||||
set: value => emit('update:pageSize', value)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,33 +1,53 @@
|
|||
<template>
|
||||
<label class="input-with-icons">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill="currentColor"
|
||||
d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14" />
|
||||
</svg>
|
||||
<input
|
||||
:value="modelValue"
|
||||
aria-label="Filter entities"
|
||||
placeholder="Filter entities..."
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
<button title="Clear search filter" :disabled="!modelValue" @click="$emit('update:modelValue', '')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill="currentColor"
|
||||
d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z" />
|
||||
</svg>
|
||||
</button>
|
||||
</label>
|
||||
<label class="input-with-icons">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
:value="modelValue"
|
||||
aria-label="Filter entities"
|
||||
placeholder="Filter entities..."
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
>
|
||||
<button
|
||||
title="Clear search filter"
|
||||
:disabled="!modelValue"
|
||||
@click="$emit('update:modelValue', '')"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
defineEmits(['update:modelValue'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
<template>
|
||||
<span class="log-action-title">
|
||||
<slot>{{ actionTitle }}</slot>
|
||||
<span v-if="justification" class="log-justification">— {{ justification }}</span>
|
||||
<span
|
||||
v-if="justification"
|
||||
class="log-justification"
|
||||
>— {{ justification }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||
|
||||
import Dashboard from './Dashboard.vue'
|
||||
|
||||
import { Wrench01Icon } from '@hugeicons/core-free-icons'
|
||||
import { LeftToRightListDashIcon } from '@hugeicons/core-free-icons'
|
||||
import { CellsIcon } from '@hugeicons/core-free-icons'
|
||||
import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
|
||||
import { Wrench01Icon, LeftToRightListDashIcon, CellsIcon, DashboardSquare01Icon } from '@hugeicons/core-free-icons'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
|
|
@ -44,8 +41,8 @@ const routes = [
|
|||
meta: {
|
||||
title: 'Logs Calendar',
|
||||
breadcrumb: [
|
||||
{ name: "Logs", href: "/logs" },
|
||||
{ name: "Calendar" },
|
||||
{ name: 'Logs', href: '/logs' },
|
||||
{ name: 'Calendar' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -56,8 +53,8 @@ const routes = [
|
|||
meta: {
|
||||
title: 'Execution Queue',
|
||||
breadcrumb: [
|
||||
{ name: "Logs", href: "/logs" },
|
||||
{ name: "Queue" },
|
||||
{ name: 'Logs', href: '/logs' },
|
||||
{ name: 'Queue' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -78,8 +75,8 @@ const routes = [
|
|||
meta: {
|
||||
title: 'OliveTin - Entity Details',
|
||||
breadcrumb: [
|
||||
{ name: "Entities", href: "/entities" },
|
||||
{ name: "Entity Details" }
|
||||
{ name: 'Entities', href: '/entities' },
|
||||
{ name: 'Entity Details' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -91,8 +88,8 @@ const routes = [
|
|||
meta: {
|
||||
title: 'Execution',
|
||||
breadcrumb: [
|
||||
{ name: "Logs", href: "/logs" },
|
||||
{ name: "Execution" },
|
||||
{ name: 'Logs', href: '/logs' },
|
||||
{ name: 'Execution' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -104,8 +101,8 @@ const routes = [
|
|||
meta: {
|
||||
title: 'Action Details',
|
||||
breadcrumb: [
|
||||
{ name: "Actions", href: "/" },
|
||||
{ name: "Action Details" },
|
||||
{ name: 'Actions', href: '/' },
|
||||
{ name: 'Action Details' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -117,8 +114,8 @@ const routes = [
|
|||
meta: {
|
||||
title: 'Execution conditions',
|
||||
breadcrumb: [
|
||||
{ name: "Actions", href: "/" },
|
||||
{ name: "Execution conditions" },
|
||||
{ name: 'Actions', href: '/' },
|
||||
{ name: 'Execution conditions' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -155,7 +152,7 @@ const routes = [
|
|||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
scrollBehavior (to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition
|
||||
} else {
|
||||
|
|
@ -168,7 +165,7 @@ const router = createRouter({
|
|||
router.beforeEach((to) => {
|
||||
if (to.meta && to.meta.title) {
|
||||
const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
|
||||
document.title = to.meta.title + " - " + pageTitle
|
||||
document.title = to.meta.title + ' - ' + pageTitle
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
function parseLegacyChecklistValue(value) {
|
||||
function parseLegacyChecklistValue (value) {
|
||||
return value.split(',').map((segment) => segment.trim()).filter((segment) => segment !== '')
|
||||
}
|
||||
|
||||
export function parseChecklistValue(value) {
|
||||
export function parseChecklistValue (value) {
|
||||
if (!value || value === '') {
|
||||
return []
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ export function parseChecklistValue(value) {
|
|||
return parseLegacyChecklistValue(value)
|
||||
}
|
||||
|
||||
export function formatChecklistValue(selected) {
|
||||
export function formatChecklistValue (selected) {
|
||||
if (!Array.isArray(selected) || selected.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ export function formatChecklistValue(selected) {
|
|||
return JSON.stringify(selected)
|
||||
}
|
||||
|
||||
export function toggleChoice(selected, value) {
|
||||
export function toggleChoice (selected, value) {
|
||||
const current = Array.isArray(selected) ? [...selected] : []
|
||||
const index = current.indexOf(value)
|
||||
|
||||
|
|
@ -45,10 +45,10 @@ export function toggleChoice(selected, value) {
|
|||
return current
|
||||
}
|
||||
|
||||
export function choiceLabel(choice) {
|
||||
export function choiceLabel (choice) {
|
||||
return choice.title || choice.value || ''
|
||||
}
|
||||
|
||||
export function allChoiceValues(choices) {
|
||||
export function allChoiceValues (choices) {
|
||||
return choices.map((choice) => choice.value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export function applyArgumentTemplate(template, args) {
|
||||
export function applyArgumentTemplate (template, args) {
|
||||
if (!template) {
|
||||
return ''
|
||||
}
|
||||
|
|
@ -6,10 +6,10 @@ export function applyArgumentTemplate(template, args) {
|
|||
return template.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, name) => args[name] ?? '')
|
||||
}
|
||||
|
||||
export function actionRequiresJustification(justification) {
|
||||
export function actionRequiresJustification (justification) {
|
||||
return (justification ?? '').length > 0
|
||||
}
|
||||
|
||||
export function actionJustificationTemplate(justification) {
|
||||
export function actionJustificationTemplate (justification) {
|
||||
return justification ?? ''
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
export function readPrefilledArgumentsFromNavigation (historyState = globalThis.window?.history?.state) {
|
||||
if (historyState?.prefilledArguments && typeof historyState.prefilledArguments === 'object') {
|
||||
return { ...historyState.prefilledArguments }
|
||||
}
|
||||
if (historyState?.prefilledArguments && typeof historyState.prefilledArguments === 'object') {
|
||||
return { ...historyState.prefilledArguments }
|
||||
}
|
||||
|
||||
return {}
|
||||
return {}
|
||||
}
|
||||
|
||||
export function getInitialArgumentValue (paramName, prefilledArguments = {}, search = globalThis.window?.location?.search ?? '') {
|
||||
const safePrefilledArguments = prefilledArguments && typeof prefilledArguments === 'object'
|
||||
? prefilledArguments
|
||||
: {}
|
||||
const safePrefilledArguments = prefilledArguments && typeof prefilledArguments === 'object'
|
||||
? prefilledArguments
|
||||
: {}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(safePrefilledArguments, paramName)) {
|
||||
return safePrefilledArguments[paramName]
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(safePrefilledArguments, paramName)) {
|
||||
return safePrefilledArguments[paramName]
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(search)
|
||||
return params.get(paramName)
|
||||
const params = new URLSearchParams(search)
|
||||
return params.get(paramName)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,130 +1,211 @@
|
|||
<template>
|
||||
<Section :padding="false">
|
||||
<template #title>
|
||||
<span class="section-title-with-icon">
|
||||
Action Details:
|
||||
<ActionIconGlyph v-if="action" class="action-title-icon" :glyph="action.icon" />
|
||||
{{ actionTitle }}
|
||||
</span>
|
||||
</template>
|
||||
<template #toolbar>
|
||||
<div class="action-details-toolbar">
|
||||
<button
|
||||
v-for="dashboard in backToDashboards"
|
||||
:key="dashboard.path"
|
||||
@click="goToDashboard(dashboard.path)"
|
||||
:title="'Back to ' + dashboard.title"
|
||||
class="button neutral"
|
||||
>
|
||||
<HugeiconsIcon :icon="DashboardSquare01Icon" />
|
||||
{{ dashboard.title }}
|
||||
</button>
|
||||
<button v-if="action" @click="startAction" title="Run this action" class="button neutral">
|
||||
<HugeiconsIcon :icon="WorkoutRunIcon" />
|
||||
Run
|
||||
</button>
|
||||
<router-link
|
||||
v-if="action"
|
||||
:to="{ name: 'ActionExecConditions', params: { actionId: route.params.actionId } }"
|
||||
class="button neutral"
|
||||
title="View configured automatic triggers and on-demand execution"
|
||||
>
|
||||
Execution conditions ({{ executionConditionCount }})
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
<template #title>
|
||||
<span class="section-title-with-icon">
|
||||
Action Details:
|
||||
<ActionIconGlyph
|
||||
v-if="action"
|
||||
class="action-title-icon"
|
||||
:glyph="action.icon"
|
||||
/>
|
||||
{{ actionTitle }}
|
||||
</span>
|
||||
</template>
|
||||
<template #toolbar>
|
||||
<div class="action-details-toolbar">
|
||||
<button
|
||||
v-for="dashboard in backToDashboards"
|
||||
:key="dashboard.path"
|
||||
:title="'Back to ' + dashboard.title"
|
||||
class="button neutral"
|
||||
@click="goToDashboard(dashboard.path)"
|
||||
>
|
||||
<HugeiconsIcon :icon="DashboardSquare01Icon" />
|
||||
{{ dashboard.title }}
|
||||
</button>
|
||||
<button
|
||||
v-if="action"
|
||||
title="Run this action"
|
||||
class="button neutral"
|
||||
@click="startAction"
|
||||
>
|
||||
<HugeiconsIcon :icon="WorkoutRunIcon" />
|
||||
Run
|
||||
</button>
|
||||
<router-link
|
||||
v-if="action"
|
||||
:to="{ name: 'ActionExecConditions', params: { actionId: route.params.actionId } }"
|
||||
class="button neutral"
|
||||
title="View configured automatic triggers and on-demand execution"
|
||||
>
|
||||
Execution conditions ({{ executionConditionCount }})
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class = "flex-row padding" v-if="action">
|
||||
<div class = "fg1">
|
||||
<dl>
|
||||
<dt>Timeout</dt>
|
||||
<dd>{{ action.timeout }} seconds</dd>
|
||||
<div
|
||||
v-if="action"
|
||||
class="flex-row padding"
|
||||
>
|
||||
<div class="fg1">
|
||||
<dl>
|
||||
<dt>Timeout</dt>
|
||||
<dd>{{ action.timeout }} seconds</dd>
|
||||
|
||||
<template v-if="actionGroups.length > 0">
|
||||
<dt>
|
||||
<router-link :to="{ name: 'LogsQueue' }" class="action-groups-link">Action groups</router-link>
|
||||
</dt>
|
||||
<dd>
|
||||
<ul class="action-group-list">
|
||||
<li v-for="group in actionGroups" :key="group.name" class="action-group-row">
|
||||
<router-link :to="{ name: 'LogsQueue' }" class="action-groups-link action-group-name">{{ group.name }}</router-link><template v-if="group.maxConcurrent > 0 && group.queueSize > 0"> - </template><ActionGroupLimitsLabel
|
||||
:max-concurrent="group.maxConcurrent"
|
||||
:queue-size="group.queueSize"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</template>
|
||||
</dl>
|
||||
<p class = "fg1">
|
||||
Execution history for this action. You can filter by execution tracking ID.
|
||||
</p>
|
||||
</div>
|
||||
<div style = "align-self: start; text-align: right;">
|
||||
<div class="filter-container">
|
||||
<label class="input-with-icons">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14" />
|
||||
<template v-if="actionGroups.length > 0">
|
||||
<dt>
|
||||
<router-link
|
||||
:to="{ name: 'LogsQueue' }"
|
||||
class="action-groups-link"
|
||||
>
|
||||
Action groups
|
||||
</router-link>
|
||||
</dt>
|
||||
<dd>
|
||||
<ul class="action-group-list">
|
||||
<li
|
||||
v-for="group in actionGroups"
|
||||
:key="group.name"
|
||||
class="action-group-row"
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'LogsQueue' }"
|
||||
class="action-groups-link action-group-name"
|
||||
>
|
||||
{{ group.name }}
|
||||
</router-link><template v-if="group.maxConcurrent > 0 && group.queueSize > 0">
|
||||
-
|
||||
</template><ActionGroupLimitsLabel
|
||||
:max-concurrent="group.maxConcurrent"
|
||||
:queue-size="group.queueSize"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</template>
|
||||
</dl>
|
||||
<p class="fg1">
|
||||
Execution history for this action. You can filter by execution tracking ID.
|
||||
</p>
|
||||
</div>
|
||||
<div style="align-self: start; text-align: right;">
|
||||
<div class="filter-container">
|
||||
<label class="input-with-icons">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
v-model="searchText"
|
||||
placeholder="Filter current page"
|
||||
>
|
||||
<button
|
||||
title="Clear search filter"
|
||||
:disabled="!searchText"
|
||||
@click="clearSearch"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"
|
||||
/>
|
||||
</svg>
|
||||
<input placeholder="Filter current page" v-model="searchText" />
|
||||
<button title="Clear search filter" :disabled="!searchText" @click="clearSearch">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z" />
|
||||
</svg>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="filteredLogs.length > 0">
|
||||
<table class="logs-table row-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Duration</th>
|
||||
<th>Execution ID</th>
|
||||
<th>Metadata</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="log in filteredLogs" :key="log.executionTrackingId" class="log-row" :title="log.actionTitle">
|
||||
<td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td>
|
||||
<td class="duration">{{ formatExecutionDuration(log) }}</td>
|
||||
<td>
|
||||
<router-link :to="`/logs/${log.executionTrackingId}`">
|
||||
<LogActionTitle :justification="log.justification">
|
||||
{{ log.executionTrackingId }}
|
||||
</LogActionTitle>
|
||||
</router-link>
|
||||
</td>
|
||||
<td class="tags">
|
||||
<span class="annotation">
|
||||
<span class="annotation-key">User:</span>
|
||||
<span class="annotation-val">{{ log.user }}</span>
|
||||
</span>
|
||||
<span v-if="log.tags && log.tags.length > 0" class="tag-list">
|
||||
<span v-for="tag in log.tags" :key="tag" class="tag">{{ tag }}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="exit-code">
|
||||
<ActionStatusDisplay :logEntry="log" :link-queued-status="true" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-show="filteredLogs.length > 0">
|
||||
<table class="logs-table row-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Duration</th>
|
||||
<th>Execution ID</th>
|
||||
<th>Metadata</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="log in filteredLogs"
|
||||
:key="log.executionTrackingId"
|
||||
class="log-row"
|
||||
:title="log.actionTitle"
|
||||
>
|
||||
<td class="timestamp">
|
||||
{{ formatTimestamp(log.datetimeStarted) }}
|
||||
</td>
|
||||
<td class="duration">
|
||||
{{ formatExecutionDuration(log) }}
|
||||
</td>
|
||||
<td>
|
||||
<router-link :to="`/logs/${log.executionTrackingId}`">
|
||||
<LogActionTitle :justification="log.justification">
|
||||
{{ log.executionTrackingId }}
|
||||
</LogActionTitle>
|
||||
</router-link>
|
||||
</td>
|
||||
<td class="tags">
|
||||
<span class="annotation">
|
||||
<span class="annotation-key">User:</span>
|
||||
<span class="annotation-val">{{ log.user }}</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="log.tags && log.tags.length > 0"
|
||||
class="tag-list"
|
||||
>
|
||||
<span
|
||||
v-for="tag in log.tags"
|
||||
:key="tag"
|
||||
class="tag"
|
||||
>{{ tag }}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="exit-code">
|
||||
<ActionStatusDisplay
|
||||
:log-entry="log"
|
||||
:link-queued-status="true"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Pagination :pageSize="pageSize" :total="totalCount" :currentPage="currentPage" :page="currentPage" @page-change="handlePageChange" class="padding"
|
||||
@page-size-change="handlePageSizeChange" itemTitle="execution logs" />
|
||||
</div>
|
||||
<Pagination
|
||||
:page-size="pageSize"
|
||||
:total="totalCount"
|
||||
:current-page="currentPage"
|
||||
:page="currentPage"
|
||||
class="padding"
|
||||
item-title="execution logs"
|
||||
@page-change="handlePageChange"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-show="logs.length === 0 && !loading" class="empty-state">
|
||||
<p>This action has no execution history.</p>
|
||||
<router-link to="/">Return to index</router-link>
|
||||
</div>
|
||||
<div
|
||||
v-show="logs.length === 0 && !loading"
|
||||
class="empty-state"
|
||||
>
|
||||
<p>This action has no execution history.</p>
|
||||
<router-link to="/">
|
||||
Return to index
|
||||
</router-link>
|
||||
</div>
|
||||
</Section>
|
||||
</template>
|
||||
|
||||
|
|
@ -174,16 +255,16 @@ const executionConditionCount = computed(() => countExecutionConditions(action.v
|
|||
|
||||
const actionGroups = computed(() => action.value?.groups ?? [])
|
||||
|
||||
async function fetchActionLogs() {
|
||||
async function fetchActionLogs () {
|
||||
loading.value = true
|
||||
try {
|
||||
const actionId = route.params.actionId
|
||||
const startOffset = (currentPage.value - 1) * pageSize.value
|
||||
|
||||
const args = {
|
||||
"actionId": actionId,
|
||||
"startOffset": BigInt(startOffset),
|
||||
"pageSize": BigInt(Number(pageSize.value)),
|
||||
actionId,
|
||||
startOffset: BigInt(startOffset),
|
||||
pageSize: BigInt(Number(pageSize.value))
|
||||
}
|
||||
|
||||
const response = await window.client.getActionLogs(args)
|
||||
|
|
@ -203,11 +284,11 @@ async function fetchActionLogs() {
|
|||
}
|
||||
}
|
||||
|
||||
async function fetchAction() {
|
||||
async function fetchAction () {
|
||||
try {
|
||||
const actionId = route.params.actionId
|
||||
const args = {
|
||||
"bindingId": actionId
|
||||
bindingId: actionId
|
||||
}
|
||||
const response = await window.client.getActionBinding(args)
|
||||
action.value = response.action
|
||||
|
|
@ -219,11 +300,11 @@ async function fetchAction() {
|
|||
}
|
||||
}
|
||||
|
||||
function goToDashboard(path) {
|
||||
function goToDashboard (path) {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
function resetState () {
|
||||
action.value = null
|
||||
backToDashboards.value = []
|
||||
actionTitle.value = 'Action Details'
|
||||
|
|
@ -235,11 +316,11 @@ function resetState() {
|
|||
syncDurationTicker()
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
function clearSearch () {
|
||||
searchText.value = ''
|
||||
}
|
||||
|
||||
function formatTimestamp(timestamp) {
|
||||
function formatTimestamp (timestamp) {
|
||||
if (!timestamp) return 'Unknown'
|
||||
try {
|
||||
const date = new Date(timestamp)
|
||||
|
|
@ -249,11 +330,11 @@ function formatTimestamp(timestamp) {
|
|||
}
|
||||
}
|
||||
|
||||
function plural(n, singular, pluralForm) {
|
||||
function plural (n, singular, pluralForm) {
|
||||
return n === 1 ? `1 ${singular}` : `${n} ${pluralForm}`
|
||||
}
|
||||
|
||||
function formatDurationSimple(ms) {
|
||||
function formatDurationSimple (ms) {
|
||||
if (!Number.isFinite(ms) || ms < 0) {
|
||||
return '—'
|
||||
}
|
||||
|
|
@ -274,7 +355,7 @@ function formatDurationSimple(ms) {
|
|||
return parts.join(' ')
|
||||
}
|
||||
|
||||
function formatExecutionDuration(log) {
|
||||
function formatExecutionDuration (log) {
|
||||
// Reading durationClock keeps this column reactive while executions are in progress.
|
||||
const clock = durationClock.value
|
||||
|
||||
|
|
@ -300,7 +381,7 @@ function formatExecutionDuration(log) {
|
|||
return formatDurationSimple(endMs - started.getTime())
|
||||
}
|
||||
|
||||
function syncDurationTicker() {
|
||||
function syncDurationTicker () {
|
||||
if (durationTicker != null) {
|
||||
clearInterval(durationTicker)
|
||||
durationTicker = null
|
||||
|
|
@ -314,18 +395,18 @@ function syncDurationTicker() {
|
|||
}, 1000)
|
||||
}
|
||||
|
||||
function handlePageChange(page) {
|
||||
function handlePageChange (page) {
|
||||
currentPage.value = page
|
||||
fetchActionLogs()
|
||||
}
|
||||
|
||||
function handlePageSizeChange(newPageSize) {
|
||||
function handlePageSizeChange (newPageSize) {
|
||||
pageSize.value = newPageSize
|
||||
currentPage.value = 1
|
||||
fetchActionLogs()
|
||||
}
|
||||
|
||||
async function startAction() {
|
||||
async function startAction () {
|
||||
if (!action.value || !action.value.bindingId) {
|
||||
console.error('Cannot start action: no binding ID')
|
||||
return
|
||||
|
|
@ -351,7 +432,7 @@ async function startAction() {
|
|||
}
|
||||
}
|
||||
|
||||
function onExecutionEvent(evt) {
|
||||
function onExecutionEvent (evt) {
|
||||
const logEntry = getExecutionLogEntry(evt)
|
||||
if (!logEntry || logEntry.bindingId !== route.params.actionId) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,12 +1,21 @@
|
|||
<template>
|
||||
<Section :title="'Execution conditions: ' + actionTitle" :padding="false">
|
||||
<Section
|
||||
:title="'Execution conditions: ' + actionTitle"
|
||||
:padding="false"
|
||||
>
|
||||
<template #toolbar>
|
||||
<router-link :to="{ name: 'ActionDetails', params: { actionId: route.params.actionId } }" class="button neutral">
|
||||
<router-link
|
||||
:to="{ name: 'ActionDetails', params: { actionId: route.params.actionId } }"
|
||||
class="button neutral"
|
||||
>
|
||||
Back to action details
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<div v-if="action" class="padding content">
|
||||
<div
|
||||
v-if="action"
|
||||
class="padding content"
|
||||
>
|
||||
<p>
|
||||
These entries mirror the automatic triggers from your OliveTin configuration for this action.
|
||||
You can always run the action manually as well.
|
||||
|
|
@ -14,7 +23,12 @@
|
|||
|
||||
<h3 class="exec-type-heading">
|
||||
On click
|
||||
<a class="doc-link" :href="execConditionDocs.onDemand" target="_blank" rel="noopener noreferrer">Documentation</a>
|
||||
<a
|
||||
class="doc-link"
|
||||
:href="execConditionDocs.onDemand"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Documentation</a>
|
||||
</h3>
|
||||
<p>
|
||||
Manual execution from the web UI (dashboard or action details), or via the API (for example StartAction),
|
||||
|
|
@ -24,7 +38,12 @@
|
|||
<template v-if="action.execOnStartup">
|
||||
<h3 class="exec-type-heading">
|
||||
<code>execOnStartup</code>
|
||||
<a class="doc-link" :href="execConditionDocs.startup" target="_blank" rel="noopener noreferrer">Documentation</a>
|
||||
<a
|
||||
class="doc-link"
|
||||
:href="execConditionDocs.startup"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Documentation</a>
|
||||
</h3>
|
||||
<p>Runs once when OliveTin starts.</p>
|
||||
</template>
|
||||
|
|
@ -32,37 +51,72 @@
|
|||
<template v-if="nonEmptyList(action.execOnCron)">
|
||||
<h3 class="exec-type-heading">
|
||||
<code>execOnCron</code>
|
||||
<a class="doc-link" :href="execConditionDocs.cron" target="_blank" rel="noopener noreferrer">Documentation</a>
|
||||
<a
|
||||
class="doc-link"
|
||||
:href="execConditionDocs.cron"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Documentation</a>
|
||||
</h3>
|
||||
<ul>
|
||||
<li v-for="(line, idx) in action.execOnCron" :key="'cron-' + idx"><code>{{ line }}</code></li>
|
||||
<li
|
||||
v-for="(line, idx) in action.execOnCron"
|
||||
:key="'cron-' + idx"
|
||||
>
|
||||
<code>{{ line }}</code>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<template v-if="nonEmptyList(action.execOnFileCreatedInDir)">
|
||||
<h3 class="exec-type-heading">
|
||||
<code>execOnFileCreatedInDir</code>
|
||||
<a class="doc-link" :href="execConditionDocs.fileCreated" target="_blank" rel="noopener noreferrer">Documentation</a>
|
||||
<a
|
||||
class="doc-link"
|
||||
:href="execConditionDocs.fileCreated"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Documentation</a>
|
||||
</h3>
|
||||
<ul>
|
||||
<li v-for="(dir, idx) in action.execOnFileCreatedInDir" :key="'created-' + idx"><code>{{ dir }}</code></li>
|
||||
<li
|
||||
v-for="(dir, idx) in action.execOnFileCreatedInDir"
|
||||
:key="'created-' + idx"
|
||||
>
|
||||
<code>{{ dir }}</code>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<template v-if="nonEmptyList(action.execOnFileChangedInDir)">
|
||||
<h3 class="exec-type-heading">
|
||||
<code>execOnFileChangedInDir</code>
|
||||
<a class="doc-link" :href="execConditionDocs.fileChanged" target="_blank" rel="noopener noreferrer">Documentation</a>
|
||||
<a
|
||||
class="doc-link"
|
||||
:href="execConditionDocs.fileChanged"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Documentation</a>
|
||||
</h3>
|
||||
<ul>
|
||||
<li v-for="(dir, idx) in action.execOnFileChangedInDir" :key="'changed-' + idx"><code>{{ dir }}</code></li>
|
||||
<li
|
||||
v-for="(dir, idx) in action.execOnFileChangedInDir"
|
||||
:key="'changed-' + idx"
|
||||
>
|
||||
<code>{{ dir }}</code>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<template v-if="action.execOnCalendarFile">
|
||||
<h3 class="exec-type-heading">
|
||||
<code>execOnCalendarFile</code>
|
||||
<a class="doc-link" :href="execConditionDocs.calendar" target="_blank" rel="noopener noreferrer">Documentation</a>
|
||||
<a
|
||||
class="doc-link"
|
||||
:href="execConditionDocs.calendar"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Documentation</a>
|
||||
</h3>
|
||||
<p><code>{{ action.execOnCalendarFile }}</code></p>
|
||||
</template>
|
||||
|
|
@ -70,10 +124,18 @@
|
|||
<template v-if="nonEmptyList(action.execOnWebhooks)">
|
||||
<h3 class="exec-type-heading">
|
||||
<code>execOnWebhook</code>
|
||||
<a class="doc-link" :href="execConditionDocs.webhook" target="_blank" rel="noopener noreferrer">Documentation</a>
|
||||
<a
|
||||
class="doc-link"
|
||||
:href="execConditionDocs.webhook"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Documentation</a>
|
||||
</h3>
|
||||
<ul class="webhook-list">
|
||||
<li v-for="(wh, idx) in action.execOnWebhooks" :key="'wh-' + idx">
|
||||
<li
|
||||
v-for="(wh, idx) in action.execOnWebhooks"
|
||||
:key="'wh-' + idx"
|
||||
>
|
||||
<span v-if="wh.template">template: <code>{{ wh.template }}</code></span>
|
||||
<span v-if="wh.matchPath"> · matchPath: <code>{{ wh.matchPath }}</code></span>
|
||||
<span v-if="nonEmptyObject(wh.matchHeaders)"> · matchHeaders: <code>{{ wh.matchHeaders }}</code></span>
|
||||
|
|
@ -83,14 +145,22 @@
|
|||
</ul>
|
||||
</template>
|
||||
|
||||
<p v-if="!hasConfiguredTriggers" class="muted">
|
||||
<p
|
||||
v-if="!hasConfiguredTriggers"
|
||||
class="muted"
|
||||
>
|
||||
This action has no automatic triggers in configuration besides on-demand execution.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!loading" class="padding empty-state">
|
||||
<div
|
||||
v-else-if="!loading"
|
||||
class="padding empty-state"
|
||||
>
|
||||
<p>Could not load this action.</p>
|
||||
<router-link :to="{ name: 'Actions' }">Return to index</router-link>
|
||||
<router-link :to="{ name: 'Actions' }">
|
||||
Return to index
|
||||
</router-link>
|
||||
</div>
|
||||
</Section>
|
||||
</template>
|
||||
|
|
@ -112,18 +182,18 @@ const execConditionDocs = {
|
|||
fileCreated: 'https://docs.olivetin.app/action_execution/onfilecreated.html',
|
||||
fileChanged: 'https://docs.olivetin.app/action_execution/onfilechanged.html',
|
||||
calendar: 'https://docs.olivetin.app/action_execution/oncalendar.html',
|
||||
webhook: 'https://docs.olivetin.app/action_execution/onwebhook.html',
|
||||
webhook: 'https://docs.olivetin.app/action_execution/onwebhook.html'
|
||||
}
|
||||
|
||||
function nonEmptyList(list) {
|
||||
function nonEmptyList (list) {
|
||||
return Array.isArray(list) && list.length > 0
|
||||
}
|
||||
|
||||
function nonEmptyObject(object) {
|
||||
function nonEmptyObject (object) {
|
||||
return object && Object.keys(object).length > 0
|
||||
}
|
||||
|
||||
function webhookHasCondition(webhook) {
|
||||
function webhookHasCondition (webhook) {
|
||||
return webhook.template || webhook.matchPath || nonEmptyObject(webhook.matchHeaders) || nonEmptyObject(webhook.matchQuery)
|
||||
}
|
||||
|
||||
|
|
@ -147,7 +217,7 @@ const hasConfiguredTriggers = computed(() => {
|
|||
return false
|
||||
})
|
||||
|
||||
async function fetchAction() {
|
||||
async function fetchAction () {
|
||||
loading.value = true
|
||||
try {
|
||||
const actionId = route.params.actionId
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<section id = "argument-popup">
|
||||
<section id="argument-popup">
|
||||
<div class="section-header">
|
||||
<h2>
|
||||
<span class="section-title-with-icon">
|
||||
|
|
@ -8,7 +8,11 @@
|
|||
:to="`/action/${bindingId}`"
|
||||
class="action-details-title-link"
|
||||
>
|
||||
<ActionIconGlyph v-if="icon" class="action-title-icon" :glyph="icon" />
|
||||
<ActionIconGlyph
|
||||
v-if="icon"
|
||||
class="action-title-icon"
|
||||
:glyph="icon"
|
||||
/>
|
||||
{{ title }}
|
||||
</router-link>
|
||||
</span>
|
||||
|
|
@ -17,48 +21,96 @@
|
|||
<div class="section-content padding">
|
||||
<form @submit="handleSubmit">
|
||||
<template v-if="actionArguments.length > 0">
|
||||
<template
|
||||
v-for="arg in actionArguments"
|
||||
:key="arg.name"
|
||||
>
|
||||
<label
|
||||
v-if="arg.type !== 'checklist'"
|
||||
:for="argumentFieldId(arg.name)"
|
||||
>
|
||||
{{ formatLabel(arg.title) }}
|
||||
</label>
|
||||
<div
|
||||
v-else
|
||||
class="argument-label"
|
||||
>
|
||||
{{ formatLabel(arg.title) }}
|
||||
</div>
|
||||
|
||||
<template v-for="arg in actionArguments" :key="arg.name">
|
||||
<label v-if="arg.type !== 'checklist'" :for="argumentFieldId(arg.name)">
|
||||
{{ formatLabel(arg.title) }}
|
||||
</label>
|
||||
<div v-else class="argument-label">
|
||||
{{ formatLabel(arg.title) }}
|
||||
</div>
|
||||
<datalist
|
||||
v-if="(arg.suggestions && Object.keys(arg.suggestions).length > 0) || getBrowserSuggestions(arg).length > 0"
|
||||
:id="argumentFieldChoicesId(arg.name)"
|
||||
>
|
||||
<option
|
||||
v-for="(suggestion, key) in arg.suggestions"
|
||||
:key="key"
|
||||
:value="key"
|
||||
>
|
||||
{{ suggestion }}
|
||||
</option>
|
||||
<option
|
||||
v-for="(suggestion, index) in getBrowserSuggestions(arg)"
|
||||
:key="`browser-${index}`"
|
||||
:value="suggestion"
|
||||
>
|
||||
{{ suggestion }}
|
||||
</option>
|
||||
</datalist>
|
||||
|
||||
<datalist v-if="(arg.suggestions && Object.keys(arg.suggestions).length > 0) || getBrowserSuggestions(arg).length > 0" :id="argumentFieldChoicesId(arg.name)">
|
||||
<option v-for="(suggestion, key) in arg.suggestions" :key="key" :value="key">
|
||||
{{ suggestion }}
|
||||
</option>
|
||||
<option v-for="(suggestion, index) in getBrowserSuggestions(arg)" :key="`browser-${index}`" :value="suggestion">
|
||||
{{ suggestion }}
|
||||
</option>
|
||||
</datalist>
|
||||
<ChoiceCombobox
|
||||
v-if="getInputComponent(arg) === 'select'"
|
||||
:id="argumentFieldId(arg.name)"
|
||||
:name="arg.name"
|
||||
:choices="arg.choices"
|
||||
:model-value="getArgumentValue(arg)"
|
||||
:required="arg.required"
|
||||
@update:model-value="handleChoiceUpdate(arg, $event)"
|
||||
/>
|
||||
|
||||
<ChoiceCombobox v-if="getInputComponent(arg) === 'select'" :id="argumentFieldId(arg.name)" :name="arg.name"
|
||||
:choices="arg.choices" :model-value="getArgumentValue(arg)" :required="arg.required"
|
||||
@update:model-value="handleChoiceUpdate(arg, $event)" />
|
||||
<ChoiceChecklist
|
||||
v-else-if="arg.type === 'checklist'"
|
||||
:name="arg.name"
|
||||
:label="arg.title"
|
||||
:choices="arg.choices"
|
||||
:model-value="getArgumentValue(arg)"
|
||||
:required="arg.required"
|
||||
@update:model-value="handleChoiceUpdate(arg, $event)"
|
||||
/>
|
||||
|
||||
<ChoiceChecklist v-else-if="arg.type === 'checklist'" :name="arg.name"
|
||||
:label="arg.title" :choices="arg.choices" :model-value="getArgumentValue(arg)" :required="arg.required"
|
||||
@update:model-value="handleChoiceUpdate(arg, $event)" />
|
||||
<component
|
||||
:is="getInputComponent(arg)"
|
||||
v-else
|
||||
:id="argumentFieldId(arg.name)"
|
||||
:name="arg.name"
|
||||
:value="(arg.type === 'checkbox' || arg.type === 'confirmation') ? undefined : getArgumentValue(arg)"
|
||||
:checked="(arg.type === 'checkbox' || arg.type === 'confirmation') ? getArgumentValue(arg) : undefined"
|
||||
:list="(arg.suggestions || getBrowserSuggestions(arg).length > 0) ? argumentFieldChoicesId(arg.name) : undefined"
|
||||
:type="getInputComponent(arg) !== 'select' ? getInputType(arg) : undefined"
|
||||
:rows="arg.type === 'raw_string_multiline' ? 5 : undefined"
|
||||
:step="arg.type === 'datetime' ? 1 : undefined"
|
||||
:pattern="getPattern(arg)"
|
||||
@input="handleInput(arg, $event)"
|
||||
@change="handleChange(arg, $event)"
|
||||
/>
|
||||
|
||||
<component v-else :is="getInputComponent(arg)" :id="argumentFieldId(arg.name)" :name="arg.name"
|
||||
:value="(arg.type === 'checkbox' || arg.type === 'confirmation') ? undefined : getArgumentValue(arg)"
|
||||
:checked="(arg.type === 'checkbox' || arg.type === 'confirmation') ? getArgumentValue(arg) : undefined"
|
||||
:list="(arg.suggestions || getBrowserSuggestions(arg).length > 0) ? argumentFieldChoicesId(arg.name) : undefined"
|
||||
:type="getInputComponent(arg) !== 'select' ? getInputType(arg) : undefined"
|
||||
:rows="arg.type === 'raw_string_multiline' ? 5 : undefined"
|
||||
:step="arg.type === 'datetime' ? 1 : undefined" :pattern="getPattern(arg)"
|
||||
@input="handleInput(arg, $event)" @change="handleChange(arg, $event)" />
|
||||
|
||||
<span class="argument-description" v-html="arg.description"></span>
|
||||
<span
|
||||
class="argument-description"
|
||||
v-html="arg.description"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-if="justificationRequired">
|
||||
<label for="justification">Justification:</label>
|
||||
<input id="justification" name="justification" type="text" :value="justificationValue" required @input="handleJustificationInput" />
|
||||
<input
|
||||
id="justification"
|
||||
name="justification"
|
||||
type="text"
|
||||
:value="justificationValue"
|
||||
required
|
||||
@input="handleJustificationInput"
|
||||
>
|
||||
</template>
|
||||
|
||||
<div v-if="actionArguments.length === 0 && !justificationRequired">
|
||||
|
|
@ -66,10 +118,18 @@
|
|||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<button name="start" type="submit" :disabled="!formReady || (hasConfirmation && !confirmationChecked)">
|
||||
<button
|
||||
name="start"
|
||||
type="submit"
|
||||
:disabled="!formReady || (hasConfirmation && !confirmationChecked)"
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
<button name="cancel" type="button" @click="handleCancel">
|
||||
<button
|
||||
name="cancel"
|
||||
type="button"
|
||||
@click="handleCancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -103,7 +163,7 @@ const router = useRouter()
|
|||
const dialog = ref(null)
|
||||
const title = ref('')
|
||||
const icon = ref('')
|
||||
//const arguments = ref([])
|
||||
// const arguments = ref([])
|
||||
const argValues = ref({})
|
||||
const confirmationChecked = ref(false)
|
||||
const hasConfirmation = ref(false)
|
||||
|
|
@ -128,7 +188,7 @@ const props = defineProps({
|
|||
})
|
||||
|
||||
// Methods
|
||||
async function setup() {
|
||||
async function setup () {
|
||||
formReady.value = false
|
||||
document.body.removeAttribute('loaded-argument-form')
|
||||
|
||||
|
|
@ -155,32 +215,32 @@ async function setup() {
|
|||
|
||||
// Initialize values from navigation state, query params, or defaults
|
||||
actionArguments.value.forEach(arg => {
|
||||
if (arg.type === 'confirmation') {
|
||||
hasConfirmation.value = true
|
||||
const paramValue = getInitialArgumentValue(arg.name, prefilledArguments)
|
||||
let checkedValue = false
|
||||
if (paramValue !== null) {
|
||||
checkedValue = paramValue === '1' || paramValue === 'true' || paramValue === true
|
||||
} else if (arg.defaultValue !== undefined && arg.defaultValue !== '') {
|
||||
checkedValue = arg.defaultValue === '1' || arg.defaultValue === 'true' || arg.defaultValue === true
|
||||
}
|
||||
argValues.value[arg.name] = checkedValue
|
||||
confirmationChecked.value = checkedValue
|
||||
} else {
|
||||
const paramValue = getInitialArgumentValue(arg.name, prefilledArguments)
|
||||
if (arg.type === 'checkbox') {
|
||||
// For checkboxes, handle boolean default values properly
|
||||
if (arg.type === 'confirmation') {
|
||||
hasConfirmation.value = true
|
||||
const paramValue = getInitialArgumentValue(arg.name, prefilledArguments)
|
||||
let checkedValue = false
|
||||
if (paramValue !== null) {
|
||||
argValues.value[arg.name] = paramValue === '1' || paramValue === 'true' || paramValue === true
|
||||
checkedValue = paramValue === '1' || paramValue === 'true' || paramValue === true
|
||||
} else if (arg.defaultValue !== undefined && arg.defaultValue !== '') {
|
||||
argValues.value[arg.name] = arg.defaultValue === '1' || arg.defaultValue === 'true' || arg.defaultValue === true
|
||||
} else {
|
||||
argValues.value[arg.name] = false
|
||||
checkedValue = arg.defaultValue === '1' || arg.defaultValue === 'true' || arg.defaultValue === true
|
||||
}
|
||||
argValues.value[arg.name] = checkedValue
|
||||
confirmationChecked.value = checkedValue
|
||||
} else {
|
||||
argValues.value[arg.name] = paramValue !== null ? paramValue : arg.defaultValue || ''
|
||||
const paramValue = getInitialArgumentValue(arg.name, prefilledArguments)
|
||||
if (arg.type === 'checkbox') {
|
||||
// For checkboxes, handle boolean default values properly
|
||||
if (paramValue !== null) {
|
||||
argValues.value[arg.name] = paramValue === '1' || paramValue === 'true' || paramValue === true
|
||||
} else if (arg.defaultValue !== undefined && arg.defaultValue !== '') {
|
||||
argValues.value[arg.name] = arg.defaultValue === '1' || arg.defaultValue === 'true' || arg.defaultValue === true
|
||||
} else {
|
||||
argValues.value[arg.name] = false
|
||||
}
|
||||
} else {
|
||||
argValues.value[arg.name] = paramValue !== null ? paramValue : arg.defaultValue || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Run initial validation on all fields after DOM is updated
|
||||
|
|
@ -202,7 +262,7 @@ async function setup() {
|
|||
}
|
||||
}
|
||||
|
||||
function formatLabel(title) {
|
||||
function formatLabel (title) {
|
||||
const lastChar = title.charAt(title.length - 1)
|
||||
if (lastChar === '?' || lastChar === '.' || lastChar === ':') {
|
||||
return title
|
||||
|
|
@ -210,7 +270,7 @@ function formatLabel(title) {
|
|||
return title + ':'
|
||||
}
|
||||
|
||||
function getInputComponent(arg) {
|
||||
function getInputComponent (arg) {
|
||||
if (arg.type === 'html') {
|
||||
return 'div'
|
||||
} else if (arg.type === 'raw_string_multiline') {
|
||||
|
|
@ -222,7 +282,7 @@ function getInputComponent(arg) {
|
|||
}
|
||||
}
|
||||
|
||||
function getInputType(arg) {
|
||||
function getInputType (arg) {
|
||||
if (arg.type === 'html' || arg.type === 'raw_string_multiline' || arg.type === 'select') {
|
||||
return undefined
|
||||
}
|
||||
|
|
@ -242,33 +302,35 @@ function getInputType(arg) {
|
|||
return arg.type
|
||||
}
|
||||
|
||||
function getPattern(arg) {
|
||||
function getPattern (arg) {
|
||||
if (arg.type && arg.type.startsWith('regex:')) {
|
||||
return arg.type.replace('regex:', '')
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getArgumentValue(arg) {
|
||||
function getArgumentValue (arg) {
|
||||
if (arg.type === 'checkbox' || arg.type === 'confirmation') {
|
||||
return argValues.value[arg.name] === '1' || argValues.value[arg.name] === true || argValues.value[arg.name] === 'true'
|
||||
}
|
||||
return argValues.value[arg.name] || ''
|
||||
}
|
||||
|
||||
function handleJustificationInput(event) {
|
||||
function handleJustificationInput (event) {
|
||||
justificationValue.value = event.target.value
|
||||
justificationEditedManually.value = true
|
||||
event.target.setCustomValidity('')
|
||||
}
|
||||
|
||||
function handleInput(arg, event) {
|
||||
function handleInput (arg, event) {
|
||||
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value
|
||||
argValues.value[arg.name] = value
|
||||
event.target.setCustomValidity('')
|
||||
updateUrlWithArg(arg.name, value)
|
||||
updateJustificationFromTemplate()
|
||||
}
|
||||
|
||||
function handleChange(arg, event) {
|
||||
function handleChange (arg, event) {
|
||||
if (arg.type === 'confirmation') {
|
||||
confirmationChecked.value = event.target.checked
|
||||
return
|
||||
|
|
@ -278,18 +340,18 @@ function handleChange(arg, event) {
|
|||
validateArgument(arg, event.target.value)
|
||||
}
|
||||
|
||||
function getValidationElement(arg) {
|
||||
function getValidationElement (arg) {
|
||||
return document.getElementById(argumentFieldValidationElementId(arg))
|
||||
}
|
||||
|
||||
function handleChoiceUpdate(arg, value) {
|
||||
function handleChoiceUpdate (arg, value) {
|
||||
argValues.value[arg.name] = value
|
||||
updateUrlWithArg(arg.name, value)
|
||||
validateArgument(arg, value)
|
||||
updateJustificationFromTemplate()
|
||||
}
|
||||
|
||||
async function validateArgument(arg, value) {
|
||||
async function validateArgument (arg, value) {
|
||||
if (!arg.type || arg.type.startsWith('regex:')) {
|
||||
return
|
||||
}
|
||||
|
|
@ -316,7 +378,7 @@ async function validateArgument(arg, value) {
|
|||
|
||||
try {
|
||||
const validateArgumentTypeArgs = {
|
||||
value: value,
|
||||
value,
|
||||
type: arg.type,
|
||||
bindingId: props.bindingId,
|
||||
argumentName: arg.name
|
||||
|
|
@ -348,7 +410,7 @@ async function validateArgument(arg, value) {
|
|||
}
|
||||
}
|
||||
|
||||
function updateUrlWithArg(name, value) {
|
||||
function updateUrlWithArg (name, value) {
|
||||
if (name && value !== undefined) {
|
||||
const url = new URL(window.location.href)
|
||||
|
||||
|
|
@ -363,7 +425,7 @@ function updateUrlWithArg(name, value) {
|
|||
}
|
||||
}
|
||||
|
||||
function shouldSendArgument(arg) {
|
||||
function shouldSendArgument (arg) {
|
||||
if (!arg.name) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -371,7 +433,7 @@ function shouldSendArgument(arg) {
|
|||
return arg.type !== 'html'
|
||||
}
|
||||
|
||||
function formatArgumentValueForApi(arg, rawValue) {
|
||||
function formatArgumentValueForApi (arg, rawValue) {
|
||||
if (arg.type === 'checkbox' || arg.type === 'confirmation') {
|
||||
return rawValue === '1' || rawValue === true || rawValue === 'true' ? '1' : '0'
|
||||
}
|
||||
|
|
@ -387,7 +449,7 @@ function formatArgumentValueForApi(arg, rawValue) {
|
|||
return rawValue ?? ''
|
||||
}
|
||||
|
||||
function getSelectedArgumentEntries() {
|
||||
function getSelectedArgumentEntries () {
|
||||
const entries = []
|
||||
|
||||
for (const arg of actionArguments.value) {
|
||||
|
|
@ -404,17 +466,17 @@ function getSelectedArgumentEntries() {
|
|||
return entries
|
||||
}
|
||||
|
||||
function getArgumentValues() {
|
||||
function getArgumentValues () {
|
||||
return getSelectedArgumentEntries().map(({ name, value }) => ({ name, value }))
|
||||
}
|
||||
|
||||
function getArgumentMapForTemplate() {
|
||||
function getArgumentMapForTemplate () {
|
||||
return Object.fromEntries(
|
||||
getSelectedArgumentEntries().map(({ name, value }) => [name, value])
|
||||
)
|
||||
}
|
||||
|
||||
function updateJustificationFromTemplate() {
|
||||
function updateJustificationFromTemplate () {
|
||||
if (!justificationTemplate.value || justificationEditedManually.value) {
|
||||
return
|
||||
}
|
||||
|
|
@ -422,7 +484,7 @@ function updateJustificationFromTemplate() {
|
|||
justificationValue.value = applyArgumentTemplate(justificationTemplate.value, getArgumentMapForTemplate())
|
||||
}
|
||||
|
||||
function getUniqueId() {
|
||||
function getUniqueId () {
|
||||
if (window.isSecureContext) {
|
||||
return window.crypto.randomUUID()
|
||||
} else {
|
||||
|
|
@ -430,7 +492,7 @@ function getUniqueId() {
|
|||
}
|
||||
}
|
||||
|
||||
function getBrowserSuggestions(arg) {
|
||||
function getBrowserSuggestions (arg) {
|
||||
if (!arg.suggestionsBrowserKey) {
|
||||
return []
|
||||
}
|
||||
|
|
@ -448,7 +510,7 @@ function getBrowserSuggestions(arg) {
|
|||
return []
|
||||
}
|
||||
|
||||
function saveBrowserSuggestions() {
|
||||
function saveBrowserSuggestions () {
|
||||
for (const arg of actionArguments.value) {
|
||||
if (arg.suggestionsBrowserKey) {
|
||||
const value = argValues.value[arg.name]
|
||||
|
|
@ -484,7 +546,7 @@ function saveBrowserSuggestions() {
|
|||
}
|
||||
}
|
||||
|
||||
async function startAction(actionArgs) {
|
||||
async function startAction (actionArgs) {
|
||||
const startActionArgs = {
|
||||
bindingId: props.bindingId,
|
||||
arguments: actionArgs,
|
||||
|
|
@ -506,7 +568,7 @@ async function startAction(actionArgs) {
|
|||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(event) {
|
||||
async function handleSubmit (event) {
|
||||
event.preventDefault()
|
||||
|
||||
if (!formReady.value) {
|
||||
|
|
@ -568,24 +630,24 @@ async function handleSubmit(event) {
|
|||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
function handleCancel () {
|
||||
router.back()
|
||||
clearBookmark()
|
||||
}
|
||||
|
||||
function clearBookmark() {
|
||||
function clearBookmark () {
|
||||
window.history.replaceState({
|
||||
path: window.location.pathname
|
||||
}, '', window.location.pathname)
|
||||
}
|
||||
|
||||
function show() {
|
||||
function show () {
|
||||
if (dialog.value) {
|
||||
dialog.value.showModal()
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
function close () {
|
||||
if (dialog.value) {
|
||||
dialog.value.close()
|
||||
}
|
||||
|
|
@ -638,7 +700,6 @@ form {
|
|||
grid-template-columns: max-content auto auto;
|
||||
}
|
||||
|
||||
|
||||
.argument-description {
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
<template>
|
||||
<Section :title="t('diagnostics.get-support')">
|
||||
<p>{{ t('diagnostics.get-support-description') }}
|
||||
<p>
|
||||
{{ t('diagnostics.get-support-description') }}
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href = "https://docs.olivetin.app/troubleshooting/wheretofindhelp.html" target="_blank">{{ t('diagnostics.where-to-find-help') }}</a>
|
||||
<a
|
||||
href="https://docs.olivetin.app/troubleshooting/wheretofindhelp.html"
|
||||
target="_blank"
|
||||
>{{ t('diagnostics.where-to-find-help') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</Section>
|
||||
|
|
@ -21,26 +25,61 @@
|
|||
<Section :title="t('diagnostics.server-diagnostics')">
|
||||
<p>{{ t('diagnostics.server-diagnostics-description') }}</p>
|
||||
<p>
|
||||
<a href="https://docs.olivetin.app/troubleshooting/server-diagnostics.html" target="_blank">{{ t('diagnostics.server-diagnostics-docs') }}</a>
|
||||
<a
|
||||
href="https://docs.olivetin.app/troubleshooting/server-diagnostics.html"
|
||||
target="_blank"
|
||||
>{{ t('diagnostics.server-diagnostics-docs') }}</a>
|
||||
</p>
|
||||
|
||||
<div role="toolbar">
|
||||
<button @click="generateServerDiagnostics" :disabled="loading" class = "good">{{ t('diagnostics.generate-server-diagnostics') }}</button>
|
||||
<button @click="copyServerDiagnostics" :disabled="!serverDiagnostics || loading" :class="serverDiagnosticsCopied ? 'good' : ''">{{ serverDiagnosticsCopied ? t('diagnostics.copied') : t('diagnostics.copy-to-clipboard') }}</button>
|
||||
<button
|
||||
:disabled="loading"
|
||||
class="good"
|
||||
@click="generateServerDiagnostics"
|
||||
>
|
||||
{{ t('diagnostics.generate-server-diagnostics') }}
|
||||
</button>
|
||||
<button
|
||||
:disabled="!serverDiagnostics || loading"
|
||||
:class="serverDiagnosticsCopied ? 'good' : ''"
|
||||
@click="copyServerDiagnostics"
|
||||
>
|
||||
{{ serverDiagnosticsCopied ? t('diagnostics.copied') : t('diagnostics.copy-to-clipboard') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<textarea v-model="serverDiagnostics" readonly style="flex: 1; min-height: 200px; resize: vertical; width: 100%; box-sizing: border-box;"></textarea>
|
||||
<textarea
|
||||
v-model="serverDiagnostics"
|
||||
readonly
|
||||
style="flex: 1; min-height: 200px; resize: vertical; width: 100%; box-sizing: border-box;"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section :title="t('diagnostics.browser-info')">
|
||||
<p>{{ t('diagnostics.browser-info-description') }}</p>
|
||||
|
||||
<div role="toolbar">
|
||||
<button @click="generateBrowserInfo" :disabled="loading" class = "good">{{ t('diagnostics.generate-browser-info') }}</button>
|
||||
<button @click="copyBrowserInfo" :disabled="!browserInfo || loading" :class="browserInfoCopied ? 'good' : ''">{{ browserInfoCopied ? t('diagnostics.copied') : t('diagnostics.copy-to-clipboard') }}</button>
|
||||
<button
|
||||
:disabled="loading"
|
||||
class="good"
|
||||
@click="generateBrowserInfo"
|
||||
>
|
||||
{{ t('diagnostics.generate-browser-info') }}
|
||||
</button>
|
||||
<button
|
||||
:disabled="!browserInfo || loading"
|
||||
:class="browserInfoCopied ? 'good' : ''"
|
||||
@click="copyBrowserInfo"
|
||||
>
|
||||
{{ browserInfoCopied ? t('diagnostics.copied') : t('diagnostics.copy-to-clipboard') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<textarea v-model="browserInfo" readonly style="flex: 1; min-height: 200px; resize: vertical; width: 100%; box-sizing: border-box;"></textarea>
|
||||
<textarea
|
||||
v-model="browserInfo"
|
||||
readonly
|
||||
style="flex: 1; min-height: 200px; resize: vertical; width: 100%; box-sizing: border-box;"
|
||||
/>
|
||||
</Section>
|
||||
</template>
|
||||
|
||||
|
|
@ -58,17 +97,17 @@ const browserInfo = ref('')
|
|||
const serverDiagnosticsCopied = ref(false)
|
||||
const browserInfoCopied = ref(false)
|
||||
|
||||
async function fetchDiagnostics() {
|
||||
async function fetchDiagnostics () {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const response = await window.client.getDiagnostics();
|
||||
const response = await window.client.getDiagnostics()
|
||||
diagnostics.value = {
|
||||
sshFoundKey: response.SshFoundKey,
|
||||
sshFoundConfig: response.SshFoundConfig
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch diagnostics:', err);
|
||||
console.error('Failed to fetch diagnostics:', err)
|
||||
diagnostics.value = {
|
||||
sshFoundKey: t('diagnostics.unknown'),
|
||||
sshFoundConfig: t('diagnostics.unknown')
|
||||
|
|
@ -77,20 +116,22 @@ async function fetchDiagnostics() {
|
|||
loading.value = false
|
||||
}
|
||||
|
||||
function formatKey(key) {
|
||||
return key
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, str => str.toUpperCase())
|
||||
.trim()
|
||||
async function generateServerDiagnostics () {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const response = await window.client.serverDiagnostics()
|
||||
console.log('response', response)
|
||||
serverDiagnostics.value = `\`\`\`\n${response.alert}\n\`\`\`\n`
|
||||
} catch (err) {
|
||||
console.error('Failed to generate server diagnostics:', err)
|
||||
serverDiagnostics.value = ''
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function generateServerDiagnostics() {
|
||||
const response = await window.client.serverDiagnostics()
|
||||
console.log("response", response)
|
||||
serverDiagnostics.value = `\`\`\`\n${response.alert}\n\`\`\`\n`
|
||||
}
|
||||
|
||||
async function copyServerDiagnostics() {
|
||||
async function copyServerDiagnostics () {
|
||||
try {
|
||||
await navigator.clipboard.writeText(serverDiagnostics.value)
|
||||
serverDiagnosticsCopied.value = true
|
||||
|
|
@ -102,7 +143,7 @@ async function copyServerDiagnostics() {
|
|||
}
|
||||
}
|
||||
|
||||
async function generateBrowserInfo() {
|
||||
async function generateBrowserInfo () {
|
||||
loading.value = true
|
||||
try {
|
||||
let userAgentData = 'N/A'
|
||||
|
|
@ -159,7 +200,7 @@ async function generateBrowserInfo() {
|
|||
})(),
|
||||
hardwareConcurrency: navigator.hardwareConcurrency || 'N/A',
|
||||
maxTouchPoints: navigator.maxTouchPoints || 'N/A',
|
||||
userAgentData: userAgentData
|
||||
userAgentData
|
||||
}
|
||||
|
||||
const showVersionNumber = window.initResponse?.effectivePolicy?.showVersionNumber ?? true
|
||||
|
|
@ -168,20 +209,20 @@ async function generateBrowserInfo() {
|
|||
: '[hidden]'
|
||||
const currentLanguage = locale.value || t('diagnostics.unknown')
|
||||
|
||||
let output = '';
|
||||
output += `\`\`\`\n`
|
||||
let output = ''
|
||||
output += '```\n'
|
||||
output += '### BROWSER INFO START (copy all text to BROWSER INFO END)\n'
|
||||
output += `# OliveTin Information\n`
|
||||
output += '# OliveTin Information\n'
|
||||
output += `olivetinVersion: ${olivetinVersion}\n`
|
||||
output += `currentLanguage: ${currentLanguage}\n`
|
||||
output += `\n# Browser Information\n`
|
||||
output += '\n# Browser Information\n'
|
||||
output += `userAgent: ${info.userAgent}\n`
|
||||
output += `platform: ${info.platform}\n`
|
||||
output += `language: ${info.language}\n`
|
||||
output += `languages: ${info.languages}\n`
|
||||
output += `\n# User Agent Data\n`
|
||||
output += '\n# User Agent Data\n'
|
||||
output += `userAgentData:\n${info.userAgentData}\n`
|
||||
output += `\n# Display Information\n`
|
||||
output += '\n# Display Information\n'
|
||||
output += `screenWidth: ${info.screenWidth}\n`
|
||||
output += `screenHeight: ${info.screenHeight}\n`
|
||||
output += `screenColorDepth: ${info.screenColorDepth}\n`
|
||||
|
|
@ -189,18 +230,18 @@ async function generateBrowserInfo() {
|
|||
output += `viewportWidth: ${info.viewportWidth}\n`
|
||||
output += `viewportHeight: ${info.viewportHeight}\n`
|
||||
output += `devicePixelRatio: ${info.devicePixelRatio}\n`
|
||||
output += `\n# Feature Support\n`
|
||||
output += '\n# Feature Support\n'
|
||||
output += `cookieEnabled: ${info.cookieEnabled}\n`
|
||||
output += `localStorageEnabled: ${info.localStorageEnabled}\n`
|
||||
output += `sessionStorageEnabled: ${info.sessionStorageEnabled}\n`
|
||||
output += `onLine: ${info.onLine}\n`
|
||||
output += `hardwareConcurrency: ${info.hardwareConcurrency}\n`
|
||||
output += `maxTouchPoints: ${info.maxTouchPoints}\n`
|
||||
output += `\n# Location & Time\n`
|
||||
output += '\n# Location & Time\n'
|
||||
output += `timezone: ${info.timezone}\n`
|
||||
output += `timezoneOffset: ${info.timezoneOffset}\n`
|
||||
output += `\n### BROWSER INFO END (copy all text from BROWSER INFO START)`
|
||||
output += `\n\`\`\`\n`
|
||||
output += '\n### BROWSER INFO END (copy all text from BROWSER INFO START)'
|
||||
output += '\n```\n'
|
||||
|
||||
browserInfo.value = output
|
||||
} finally {
|
||||
|
|
@ -208,7 +249,7 @@ async function generateBrowserInfo() {
|
|||
}
|
||||
}
|
||||
|
||||
async function copyBrowserInfo() {
|
||||
async function copyBrowserInfo () {
|
||||
try {
|
||||
await navigator.clipboard.writeText(browserInfo.value)
|
||||
browserInfoCopied.value = true
|
||||
|
|
|
|||
|
|
@ -1,51 +1,54 @@
|
|||
<template>
|
||||
<Section v-if="!definitionsLoaded" title="Loading entity definitions..." />
|
||||
<Section
|
||||
v-else-if="totalInstances === 0"
|
||||
title="There are no entities to show yet."
|
||||
>
|
||||
<p>
|
||||
When OliveTin has registered entity instances (for example from entity files or your setup), they will be listed here.
|
||||
</p>
|
||||
</Section>
|
||||
<template v-else>
|
||||
<EntityDefinitionSection
|
||||
v-for="def in entityDefinitions"
|
||||
:key="def.title"
|
||||
:definition="def"
|
||||
/>
|
||||
</template>
|
||||
<Section
|
||||
v-if="!definitionsLoaded"
|
||||
title="Loading entity definitions..."
|
||||
/>
|
||||
<Section
|
||||
v-else-if="totalInstances === 0"
|
||||
title="There are no entities to show yet."
|
||||
>
|
||||
<p>
|
||||
When OliveTin has registered entity instances (for example from entity files or your setup), they will be listed here.
|
||||
</p>
|
||||
</Section>
|
||||
<template v-else>
|
||||
<EntityDefinitionSection
|
||||
v-for="def in entityDefinitions"
|
||||
:key="def.title"
|
||||
:definition="def"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import Section from 'picocrank/vue/components/Section.vue'
|
||||
import EntityDefinitionSection from '../components/EntityDefinitionSection.vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import Section from 'picocrank/vue/components/Section.vue'
|
||||
import EntityDefinitionSection from '../components/EntityDefinitionSection.vue'
|
||||
|
||||
const definitionsLoaded = ref(false)
|
||||
const entityDefinitions = ref([])
|
||||
const definitionsLoaded = ref(false)
|
||||
const entityDefinitions = ref([])
|
||||
|
||||
const totalInstances = computed(() =>
|
||||
entityDefinitions.value.reduce(
|
||||
(sum, def) => sum + (def.instances?.length ?? 0),
|
||||
0,
|
||||
),
|
||||
)
|
||||
const totalInstances = computed(() =>
|
||||
entityDefinitions.value.reduce(
|
||||
(sum, def) => sum + (def.instances?.length ?? 0),
|
||||
0
|
||||
)
|
||||
)
|
||||
|
||||
async function fetchEntities() {
|
||||
try {
|
||||
const ret = await window.client.getEntities()
|
||||
entityDefinitions.value = ret.entityDefinitions ?? []
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch entities:', err)
|
||||
window.showBigError('fetch-entities', 'getting entities', err, false)
|
||||
entityDefinitions.value = []
|
||||
} finally {
|
||||
definitionsLoaded.value = true
|
||||
}
|
||||
}
|
||||
async function fetchEntities () {
|
||||
try {
|
||||
const ret = await window.client.getEntities()
|
||||
entityDefinitions.value = ret.entityDefinitions ?? []
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch entities:', err)
|
||||
window.showBigError('fetch-entities', 'getting entities', err, false)
|
||||
entityDefinitions.value = []
|
||||
} finally {
|
||||
definitionsLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(() => {
|
||||
fetchEntities()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,130 +1,176 @@
|
|||
<template>
|
||||
<Section>
|
||||
<template #title>
|
||||
<span class="section-title-with-icon">
|
||||
Entity Details:
|
||||
<ActionIconGlyph v-if="entityIcon" class="entity-title-icon" :glyph="entityIcon" />
|
||||
<span v-if="entityDetails?.title">{{ entityDetails.title }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<template #toolbar>
|
||||
<button @click="goBack" class="back-button">
|
||||
<HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
</template>
|
||||
<div v-if="!entityDetails">
|
||||
<p>Loading entity details...</p>
|
||||
</div>
|
||||
<template v-else>
|
||||
<dl>
|
||||
<dt>Type</dt>
|
||||
<dd>
|
||||
<router-link :to="{ name: 'Entities' }" class="entity-type-link">
|
||||
{{ entityType }}
|
||||
</router-link>
|
||||
</dd>
|
||||
<dt v-if="entityDetails.title">Title</dt>
|
||||
<dd v-if="entityDetails.title">{{ entityDetails.title }}</dd>
|
||||
<template v-if="entityDetails.fields">
|
||||
<template v-for="(value, key) in entityDetails.fields" :key="key">
|
||||
<dt>{{ key }}</dt>
|
||||
<dd>{{ value }}</dd>
|
||||
</template>
|
||||
</template>
|
||||
</dl>
|
||||
<p v-if="!entityDetails.title && (!entityDetails.fields || Object.keys(entityDetails.fields).length === 0)">No details available for this entity.</p>
|
||||
</template>
|
||||
</Section>
|
||||
<Section>
|
||||
<template #title>
|
||||
<span class="section-title-with-icon">
|
||||
Entity Details:
|
||||
<ActionIconGlyph
|
||||
v-if="entityIcon"
|
||||
class="entity-title-icon"
|
||||
:glyph="entityIcon"
|
||||
/>
|
||||
<span v-if="entityDetails?.title">{{ entityDetails.title }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<template #toolbar>
|
||||
<button
|
||||
class="back-button"
|
||||
@click="goBack"
|
||||
>
|
||||
<HugeiconsIcon
|
||||
:icon="ArrowLeftIcon"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
/>
|
||||
<span>Back</span>
|
||||
</button>
|
||||
</template>
|
||||
<div v-if="!entityDetails">
|
||||
<p>Loading entity details...</p>
|
||||
</div>
|
||||
<template v-else>
|
||||
<dl>
|
||||
<dt>Type</dt>
|
||||
<dd>
|
||||
<router-link
|
||||
:to="{ name: 'Entities' }"
|
||||
class="entity-type-link"
|
||||
>
|
||||
{{ entityType }}
|
||||
</router-link>
|
||||
</dd>
|
||||
<dt v-if="entityDetails.title">
|
||||
Title
|
||||
</dt>
|
||||
<dd v-if="entityDetails.title">
|
||||
{{ entityDetails.title }}
|
||||
</dd>
|
||||
<template v-if="entityDetails.fields">
|
||||
<template
|
||||
v-for="(value, key) in entityDetails.fields"
|
||||
:key="key"
|
||||
>
|
||||
<dt>{{ key }}</dt>
|
||||
<dd>{{ value }}</dd>
|
||||
</template>
|
||||
</template>
|
||||
</dl>
|
||||
<p v-if="!entityDetails.title && (!entityDetails.fields || Object.keys(entityDetails.fields).length === 0)">
|
||||
No details available for this entity.
|
||||
</p>
|
||||
</template>
|
||||
</Section>
|
||||
|
||||
<Section v-if="entityDetails" title="Dashboard Entity Directories">
|
||||
<div v-if="filteredDirectories.length > 0" class="directories-section">
|
||||
<ul class="directory-list">
|
||||
<li v-for="(directory, idx) in filteredDirectories" :key="idx">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'Dashboard',
|
||||
params: {
|
||||
title: directory,
|
||||
entityType: entityType,
|
||||
entityKey: entityKey
|
||||
}
|
||||
}">
|
||||
{{ directory }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p v-else>No directories found for this entity.
|
||||
<a href="https://docs.olivetin.app/dashboards/entity-directories.html" target="_blank" rel="noopener noreferrer">Learn more</a>
|
||||
</p>
|
||||
</Section>
|
||||
<Section
|
||||
v-if="entityDetails"
|
||||
title="Dashboard Entity Directories"
|
||||
>
|
||||
<div
|
||||
v-if="filteredDirectories.length > 0"
|
||||
class="directories-section"
|
||||
>
|
||||
<ul class="directory-list">
|
||||
<li
|
||||
v-for="(directory, idx) in filteredDirectories"
|
||||
:key="idx"
|
||||
>
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'Dashboard',
|
||||
params: {
|
||||
title: directory,
|
||||
entityType: entityType,
|
||||
entityKey: entityKey
|
||||
}
|
||||
}"
|
||||
>
|
||||
{{ directory }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p v-else>
|
||||
No directories found for this entity.
|
||||
<a
|
||||
href="https://docs.olivetin.app/dashboards/entity-directories.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Learn more</a>
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<section v-if="entityDetails && relatedActions.length > 0" class="transparent">
|
||||
<div class="dashboard-row">
|
||||
<fieldset>
|
||||
<legend class="visually-hidden">Related actions</legend>
|
||||
<template v-for="(related, idx) in relatedActions" :key="related.action?.bindingId || idx">
|
||||
<ActionButton
|
||||
v-if="related.action"
|
||||
:action-data="related.action"
|
||||
:prefilled-arguments="related.prefilledArguments"
|
||||
/>
|
||||
</template>
|
||||
</fieldset>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
v-if="entityDetails && relatedActions.length > 0"
|
||||
class="transparent"
|
||||
>
|
||||
<div class="dashboard-row">
|
||||
<fieldset>
|
||||
<legend class="visually-hidden">
|
||||
Related actions
|
||||
</legend>
|
||||
<template
|
||||
v-for="(related, idx) in relatedActions"
|
||||
:key="related.action?.bindingId || idx"
|
||||
>
|
||||
<ActionButton
|
||||
v-if="related.action"
|
||||
:action-data="related.action"
|
||||
:prefilled-arguments="related.prefilledArguments"
|
||||
/>
|
||||
</template>
|
||||
</fieldset>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { HugeiconsIcon } from '@hugeicons/vue'
|
||||
import { ArrowLeftIcon } from '@hugeicons/core-free-icons'
|
||||
import Section from 'picocrank/vue/components/Section.vue'
|
||||
import ActionButton from '../ActionButton.vue'
|
||||
import ActionIconGlyph from '../components/ActionIconGlyph.vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { HugeiconsIcon } from '@hugeicons/vue'
|
||||
import { ArrowLeftIcon } from '@hugeicons/core-free-icons'
|
||||
import Section from 'picocrank/vue/components/Section.vue'
|
||||
import ActionButton from '../ActionButton.vue'
|
||||
import ActionIconGlyph from '../components/ActionIconGlyph.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const entityDetails = ref(null)
|
||||
const router = useRouter()
|
||||
const entityDetails = ref(null)
|
||||
|
||||
const props = defineProps({
|
||||
entityType: String,
|
||||
entityKey: String
|
||||
})
|
||||
const props = defineProps({
|
||||
entityType: String,
|
||||
entityKey: String
|
||||
})
|
||||
|
||||
const filteredDirectories = computed(() => {
|
||||
if (!entityDetails.value?.directories) {
|
||||
return []
|
||||
}
|
||||
return entityDetails.value.directories.filter(d => d)
|
||||
})
|
||||
const filteredDirectories = computed(() => {
|
||||
if (!entityDetails.value?.directories) {
|
||||
return []
|
||||
}
|
||||
return entityDetails.value.directories.filter(d => d)
|
||||
})
|
||||
|
||||
const relatedActions = computed(() => entityDetails.value?.relatedActions ?? [])
|
||||
const relatedActions = computed(() => entityDetails.value?.relatedActions ?? [])
|
||||
|
||||
const entityIcon = computed(() => entityDetails.value?.icon ?? '')
|
||||
const entityIcon = computed(() => entityDetails.value?.icon ?? '')
|
||||
|
||||
function goBack() {
|
||||
router.push({ name: 'Entities' })
|
||||
}
|
||||
function goBack () {
|
||||
router.push({ name: 'Entities' })
|
||||
}
|
||||
|
||||
async function fetchEntityDetails() {
|
||||
try {
|
||||
const response = await window.client.getEntity({
|
||||
type: props.entityType,
|
||||
uniqueKey: props.entityKey
|
||||
})
|
||||
async function fetchEntityDetails () {
|
||||
try {
|
||||
const response = await window.client.getEntity({
|
||||
type: props.entityType,
|
||||
uniqueKey: props.entityKey
|
||||
})
|
||||
|
||||
entityDetails.value = response
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch entity details:', err)
|
||||
window.showBigError('fetch-entity-details', 'getting entity details', err, false)
|
||||
}
|
||||
}
|
||||
entityDetails.value = response
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch entity details:', err)
|
||||
window.showBigError('fetch-entity-details', 'getting entity details', err, false)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(() => {
|
||||
fetchEntityDetails()
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,87 +9,141 @@
|
|||
class="action-details-title-link"
|
||||
:title="titleTooltip"
|
||||
>
|
||||
<ActionIconGlyph class="action-title-icon" :glyph="icon" />
|
||||
<LogActionTitle v-if="logEntry" :action-title="title" :justification="logEntry.justification" />
|
||||
<ActionIconGlyph
|
||||
class="action-title-icon"
|
||||
:glyph="icon"
|
||||
/>
|
||||
<LogActionTitle
|
||||
v-if="logEntry"
|
||||
:action-title="title"
|
||||
:justification="logEntry.justification"
|
||||
/>
|
||||
<span v-else>{{ title }}</span>
|
||||
</router-link>
|
||||
<template v-else>
|
||||
<LogActionTitle v-if="logEntry" :action-title="title" :justification="logEntry.justification" />
|
||||
<LogActionTitle
|
||||
v-if="logEntry"
|
||||
:action-title="title"
|
||||
:justification="logEntry.justification"
|
||||
/>
|
||||
<span v-else>{{ title }}</span>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<template #toolbar>
|
||||
<button
|
||||
v-for="dashboard in backToDashboards"
|
||||
:key="dashboard.path"
|
||||
@click="goToDashboard(dashboard.path)"
|
||||
:title="'Back to ' + dashboard.title"
|
||||
class="button neutral"
|
||||
>
|
||||
<HugeiconsIcon :icon="DashboardSquare01Icon" />
|
||||
{{ dashboard.title }}
|
||||
</button>
|
||||
<button v-if="backToDashboards.length === 0" @click="goBack" title="Go back" class="button neutral">
|
||||
<HugeiconsIcon :icon="ArrowLeftIcon" />
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
v-for="dashboard in backToDashboards"
|
||||
:key="dashboard.path"
|
||||
:title="'Back to ' + dashboard.title"
|
||||
class="button neutral"
|
||||
@click="goToDashboard(dashboard.path)"
|
||||
>
|
||||
<HugeiconsIcon :icon="DashboardSquare01Icon" />
|
||||
{{ dashboard.title }}
|
||||
</button>
|
||||
<button
|
||||
v-if="backToDashboards.length === 0"
|
||||
title="Go back"
|
||||
class="button neutral"
|
||||
@click="goBack"
|
||||
>
|
||||
<HugeiconsIcon :icon="ArrowLeftIcon" />
|
||||
Back
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<div v-if="logEntry" class = "flex-row">
|
||||
<dl class = "fg1">
|
||||
<dt>Duration</dt>
|
||||
<dd><span v-html="duration"></span></dd>
|
||||
<div
|
||||
v-if="logEntry"
|
||||
class="flex-row"
|
||||
>
|
||||
<dl class="fg1">
|
||||
<dt>Duration</dt>
|
||||
<dd><span v-html="duration" /></dd>
|
||||
|
||||
<dt>Status</dt>
|
||||
<dd class="execution-dialog-status">
|
||||
<ActionStatusDisplay :log-entry="logEntry" :link-queued-status="true" />
|
||||
</dd>
|
||||
</dl>
|
||||
<dt>Status</dt>
|
||||
<dd class="execution-dialog-status">
|
||||
<ActionStatusDisplay
|
||||
:log-entry="logEntry"
|
||||
:link-queued-status="true"
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div v-if="notFound" class="error-message padded-content">
|
||||
<h3>Execution Not Found</h3>
|
||||
<p>{{ errorMessage }}</p>
|
||||
<p>The execution with ID <code>{{ executionTrackingId }}</code> could not be found.</p>
|
||||
<router-link to="/logs">View all logs</router-link> or <router-link to="/">return to home</router-link>.
|
||||
</div>
|
||||
<div
|
||||
v-if="notFound"
|
||||
class="error-message padded-content"
|
||||
>
|
||||
<h3>Execution Not Found</h3>
|
||||
<p>{{ errorMessage }}</p>
|
||||
<p>The execution with ID <code>{{ executionTrackingId }}</code> could not be found.</p>
|
||||
<router-link to="/logs">
|
||||
View all logs
|
||||
</router-link> or <router-link to="/">
|
||||
return to home
|
||||
</router-link>.
|
||||
</div>
|
||||
|
||||
<div class="xterm-output-container">
|
||||
<div class="xterm-overlay-toolbar">
|
||||
<button type="button" class="xterm-overlay-button" @click="copyOutput" title="Copy to clipboard">
|
||||
<button
|
||||
type="button"
|
||||
class="xterm-overlay-button"
|
||||
title="Copy to clipboard"
|
||||
@click="copyOutput"
|
||||
>
|
||||
<HugeiconsIcon :icon="Copy01Icon" />
|
||||
</button>
|
||||
<button type="button" class="xterm-overlay-button" @click="toggleSize" title="Toggle fullscreen">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
d="M3 3h6v2H6.462l4.843 4.843l-1.415 1.414L5 6.367V9H3zm0 18h6v-2H6.376l4.929-4.928l-1.415-1.414L5 17.548V15H3zm12 0h6v-6h-2v2.524l-4.867-4.866l-1.414 1.414L17.647 19H15zm6-18h-6v2h2.562l-4.843 4.843l1.414 1.414L19 6.39V9h2z" />
|
||||
<button
|
||||
type="button"
|
||||
class="xterm-overlay-button"
|
||||
title="Toggle fullscreen"
|
||||
@click="toggleSize"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M3 3h6v2H6.462l4.843 4.843l-1.415 1.414L5 6.367V9H3zm0 18h6v-2H6.376l4.929-4.928l-1.415-1.414L5 17.548V15H3zm12 0h6v-6h-2v2.524l-4.867-4.866l-1.414 1.414L17.647 19H15zm6-18h-6v2h2.562l-4.843 4.843l1.414 1.414L19 6.39V9h2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div ref="xtermOutput"></div>
|
||||
<div ref="xtermOutput" />
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br>
|
||||
|
||||
<div class="flex-row g1 buttons padded-content">
|
||||
<div class = "fg1" />
|
||||
<div class="flex-row g1 buttons padded-content">
|
||||
<div class="fg1" />
|
||||
|
||||
<button :disabled="!canRerun" @click="rerunAction" title="Rerun">
|
||||
<HugeiconsIcon :icon="WorkoutRunIcon" />
|
||||
Rerun
|
||||
</button>
|
||||
<button :disabled="!canKill" @click="killAction" title="Kill" id = "execution-dialog-kill-action">
|
||||
<HugeiconsIcon :icon="Cancel02Icon" />
|
||||
Kill
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</Section>
|
||||
<button
|
||||
:disabled="!canRerun"
|
||||
title="Rerun"
|
||||
@click="rerunAction"
|
||||
>
|
||||
<HugeiconsIcon :icon="WorkoutRunIcon" />
|
||||
Rerun
|
||||
</button>
|
||||
<button
|
||||
id="execution-dialog-kill-action"
|
||||
:disabled="!canKill"
|
||||
title="Kill"
|
||||
@click="killAction"
|
||||
>
|
||||
<HugeiconsIcon :icon="Cancel02Icon" />
|
||||
Kill
|
||||
</button>
|
||||
</div>
|
||||
</Section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import ActionIconGlyph from '../components/ActionIconGlyph.vue'
|
||||
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
|
||||
import LogActionTitle from '../components/LogActionTitle.vue'
|
||||
|
|
@ -113,8 +167,8 @@ const xtermOutput = ref(null)
|
|||
|
||||
const props = defineProps({
|
||||
executionTrackingId: {
|
||||
type: String,
|
||||
required: true
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -138,7 +192,7 @@ const errorMessage = ref('')
|
|||
let executionTicker = null
|
||||
let terminal = null
|
||||
|
||||
function initializeTerminal() {
|
||||
function initializeTerminal () {
|
||||
terminal = new OutputTerminal(executionTrackingId.value)
|
||||
terminal.open(xtermOutput.value)
|
||||
terminal.resize(80, 40)
|
||||
|
|
@ -146,7 +200,7 @@ function initializeTerminal() {
|
|||
window.terminal = terminal
|
||||
}
|
||||
|
||||
function toggleSize() {
|
||||
function toggleSize () {
|
||||
if (!xtermOutput.value) {
|
||||
return
|
||||
}
|
||||
|
|
@ -162,7 +216,7 @@ function toggleSize() {
|
|||
}
|
||||
}
|
||||
|
||||
async function copyOutput() {
|
||||
async function copyOutput () {
|
||||
const text = terminal?.getBufferAsString?.() || logEntry.value?.output || ''
|
||||
if (!text) {
|
||||
return
|
||||
|
|
@ -175,7 +229,7 @@ async function copyOutput() {
|
|||
}
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
async function reset () {
|
||||
executionSeconds.value = 0
|
||||
executionTrackingId.value = 'notset'
|
||||
hideBasics.value = false
|
||||
|
|
@ -195,31 +249,31 @@ async function reset() {
|
|||
errorMessage.value = ''
|
||||
|
||||
if (terminal) {
|
||||
await terminal.reset()
|
||||
terminal.fit()
|
||||
await terminal.reset()
|
||||
terminal.fit()
|
||||
}
|
||||
}
|
||||
|
||||
function show(actionButton) {
|
||||
function show (actionButton) {
|
||||
if (actionButton) {
|
||||
icon.value = actionButton.glyph ?? ''
|
||||
icon.value = actionButton.glyph ?? ''
|
||||
}
|
||||
|
||||
canKill.value = true
|
||||
|
||||
// Clear existing ticker
|
||||
if (executionTicker) {
|
||||
clearInterval(executionTicker)
|
||||
clearInterval(executionTicker)
|
||||
}
|
||||
|
||||
executionSeconds.value = 0
|
||||
executionTick()
|
||||
executionTicker = setInterval(() => {
|
||||
executionTick()
|
||||
executionTick()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function rerunAction() {
|
||||
async function rerunAction () {
|
||||
const bindingId = logEntry.value?.bindingId
|
||||
if (!logEntry.value || !bindingId) {
|
||||
console.error('Cannot rerun: no action ID available')
|
||||
|
|
@ -247,36 +301,36 @@ async function rerunAction() {
|
|||
}
|
||||
}
|
||||
|
||||
async function killAction() {
|
||||
async function killAction () {
|
||||
if (!executionTrackingId.value || executionTrackingId.value === 'notset') {
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
const killActionArgs = {
|
||||
executionTrackingId: executionTrackingId.value
|
||||
executionTrackingId: executionTrackingId.value
|
||||
}
|
||||
|
||||
try {
|
||||
await window.client.killAction(killActionArgs)
|
||||
await window.client.killAction(killActionArgs)
|
||||
} catch (err) {
|
||||
console.error('Failed to kill action:', err)
|
||||
console.error('Failed to kill action:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function executionTick() {
|
||||
function executionTick () {
|
||||
executionSeconds.value++
|
||||
updateDuration(null)
|
||||
}
|
||||
|
||||
function hideEverythingApartFromOutput() {
|
||||
function hideEverythingApartFromOutput () {
|
||||
hideDetailsOnResult.value = true
|
||||
hideBasics.value = true
|
||||
hideDetailsOnResult.value = true
|
||||
hideBasics.value = true
|
||||
}
|
||||
|
||||
async function fetchExecutionResult(executionTrackingIdParam) {
|
||||
console.log("fetchExecutionResult", executionTrackingIdParam)
|
||||
async function fetchExecutionResult (executionTrackingIdParam) {
|
||||
console.log('fetchExecutionResult', executionTrackingIdParam)
|
||||
|
||||
executionTrackingId.value = executionTrackingIdParam
|
||||
notFound.value = false
|
||||
|
|
@ -284,50 +338,49 @@ async function fetchExecutionResult(executionTrackingIdParam) {
|
|||
backToDashboards.value = []
|
||||
|
||||
const executionStatusArgs = {
|
||||
executionTrackingId: executionTrackingId.value
|
||||
executionTrackingId: executionTrackingId.value
|
||||
}
|
||||
|
||||
try {
|
||||
const executionStatusResult = await window.client.executionStatus(executionStatusArgs)
|
||||
const executionStatusResult = await window.client.executionStatus(executionStatusArgs)
|
||||
|
||||
await renderExecutionResult(executionStatusResult)
|
||||
await renderExecutionResult(executionStatusResult)
|
||||
} catch (err) {
|
||||
// Check if it's a "not found" error (404 or similar)
|
||||
if (err.status === 404 || err.code === 'NotFound' || err.message?.includes('not found')) {
|
||||
// Check if it's a "not found" error (404 or similar)
|
||||
if (err.status === 404 || err.code === 'NotFound' || err.message?.includes('not found')) {
|
||||
notFound.value = true
|
||||
errorMessage.value = err.message || 'The execution could not be found in the system.'
|
||||
} else {
|
||||
} else {
|
||||
renderError(err)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
function updateDuration(logEntryParam) {
|
||||
function updateDuration (logEntryParam) {
|
||||
logEntry.value = logEntryParam
|
||||
if (logEntry.value == null) {
|
||||
duration.value = executionSeconds.value + ' seconds'
|
||||
duration.value = duration.value
|
||||
duration.value = executionSeconds.value + ' seconds'
|
||||
} else if (!logEntry.value.executionStarted) {
|
||||
duration.value = logEntry.value.datetimeStarted + ' (request time). Not executed.'
|
||||
duration.value = logEntry.value.datetimeStarted + ' (request time). Not executed.'
|
||||
} else if (logEntry.value.executionStarted && !logEntry.value.executionFinished) {
|
||||
duration.value = logEntry.value.datetimeStarted
|
||||
duration.value = logEntry.value.datetimeStarted
|
||||
} else {
|
||||
let delta = ''
|
||||
try {
|
||||
let delta = ''
|
||||
try {
|
||||
delta = (new Date(logEntry.value.datetimeFinished) - new Date(logEntry.value.datetimeStarted)) / 1000
|
||||
delta = new Intl.RelativeTimeFormat().format(delta, 'seconds').replace('in ', '').replace('ago', '')
|
||||
} catch (e) {
|
||||
} catch (e) {
|
||||
console.warn('Failed to calculate delta', e)
|
||||
}
|
||||
duration.value = logEntry.value.datetimeStarted + ' → ' + logEntry.value.datetimeFinished
|
||||
if (delta !== '') {
|
||||
}
|
||||
duration.value = logEntry.value.datetimeStarted + ' → ' + logEntry.value.datetimeFinished
|
||||
if (delta !== '') {
|
||||
duration.value += ' (' + delta + ')'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function renderExecutionResult(res) {
|
||||
async function renderExecutionResult (res) {
|
||||
logEntry.value = res.logEntry
|
||||
if (res.backToDashboards) {
|
||||
backToDashboards.value = res.backToDashboards.slice(0, 3)
|
||||
|
|
@ -335,12 +388,12 @@ async function renderExecutionResult(res) {
|
|||
|
||||
// Clear ticker
|
||||
if (executionTicker) {
|
||||
clearInterval(executionTicker)
|
||||
clearInterval(executionTicker)
|
||||
}
|
||||
executionTicker = null
|
||||
|
||||
if (hideDetailsOnResult.value) {
|
||||
hideDetails.value = true
|
||||
hideDetails.value = true
|
||||
}
|
||||
|
||||
executionTrackingId.value = res.logEntry.executionTrackingId
|
||||
|
|
@ -355,41 +408,41 @@ async function renderExecutionResult(res) {
|
|||
updateDuration(res.logEntry)
|
||||
|
||||
if (terminal) {
|
||||
await terminal.reset()
|
||||
await terminal.write(res.logEntry.output, () => {
|
||||
await terminal.reset()
|
||||
await terminal.write(res.logEntry.output, () => {
|
||||
terminal.fit()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function renderError(err) {
|
||||
function renderError (err) {
|
||||
window.showBigError('execution-dlg-err', 'in the execution dialog', 'Failed to fetch execution result. ' + err, false)
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
function handleClose () {
|
||||
if (executionTicker) {
|
||||
clearInterval(executionTicker)
|
||||
clearInterval(executionTicker)
|
||||
}
|
||||
|
||||
executionTicker = null
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
function cleanup () {
|
||||
if (executionTicker) {
|
||||
clearInterval(executionTicker)
|
||||
clearInterval(executionTicker)
|
||||
}
|
||||
executionTicker = null
|
||||
if (terminal != null) {
|
||||
terminal.close()
|
||||
terminal.close()
|
||||
}
|
||||
terminal = null
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
function goBack () {
|
||||
router.back()
|
||||
}
|
||||
|
||||
function goToDashboard(path) {
|
||||
function goToDashboard (path) {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
|
|
@ -409,16 +462,15 @@ onMounted(() => {
|
|||
fetchExecutionResult(props.executionTrackingId)
|
||||
|
||||
watch(
|
||||
() => buttonResults[props.executionTrackingId],
|
||||
(newResult, oldResult) => {
|
||||
() => buttonResults[props.executionTrackingId],
|
||||
(newResult, oldResult) => {
|
||||
if (newResult) {
|
||||
renderExecutionResult({
|
||||
renderExecutionResult({
|
||||
logEntry: newResult
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
|
|
|||
|
|
@ -1,33 +1,83 @@
|
|||
<template>
|
||||
<Section title="Login to OliveTin" class="small">
|
||||
<Section
|
||||
title="Login to OliveTin"
|
||||
class="small"
|
||||
>
|
||||
<div class="login-form">
|
||||
<div v-if="!hasOAuth && !hasLocalLogin" class="login-disabled">
|
||||
<div
|
||||
v-if="!hasOAuth && !hasLocalLogin"
|
||||
class="login-disabled"
|
||||
>
|
||||
<span>This server is not configured with either OAuth, or local users, so you cannot login.</span>
|
||||
</div>
|
||||
|
||||
<div v-if="hasOAuth" class="login-oauth2">
|
||||
<div
|
||||
v-if="hasOAuth"
|
||||
class="login-oauth2"
|
||||
>
|
||||
<h3>OAuth Login</h3>
|
||||
<div class="oauth-providers">
|
||||
<button v-for="provider in oauthProviders" :key="provider.key" class="oauth-button"
|
||||
@click="loginWithOAuth(provider)">
|
||||
<span v-if="provider.icon" class="provider-icon" v-html="provider.icon"></span>
|
||||
<button
|
||||
v-for="provider in oauthProviders"
|
||||
:key="provider.key"
|
||||
class="oauth-button"
|
||||
@click="loginWithOAuth(provider)"
|
||||
>
|
||||
<span
|
||||
v-if="providerIcon(provider)"
|
||||
class="provider-icon"
|
||||
>
|
||||
<iconify-icon
|
||||
v-if="providerIcon(provider).kind === 'iconify'"
|
||||
:icon="providerIcon(provider).id"
|
||||
/>
|
||||
<span v-else>{{ providerIcon(provider).text }}</span>
|
||||
</span>
|
||||
<span class="provider-name">Login with {{ provider.title }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasLocalLogin" class="login-local">
|
||||
<div
|
||||
v-if="hasLocalLogin"
|
||||
class="login-local"
|
||||
>
|
||||
<h3>Local Login</h3>
|
||||
<form @submit.prevent="handleLocalLogin" class="local-login-form">
|
||||
<div v-if="loginError" class="bad">
|
||||
<form
|
||||
class="local-login-form"
|
||||
@submit.prevent="handleLocalLogin"
|
||||
>
|
||||
<div
|
||||
v-if="loginError"
|
||||
class="bad"
|
||||
>
|
||||
{{ loginError }}
|
||||
</div>
|
||||
|
||||
<input id="username" v-model="username" type="text" name="username" autocomplete="username" required placeholder="Username" />
|
||||
<input id="password" v-model="password" type="password" name="password" autocomplete="current-password" placeholder="Password"
|
||||
required />
|
||||
<input
|
||||
id="username"
|
||||
v-model="username"
|
||||
type="text"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
required
|
||||
placeholder="Username"
|
||||
>
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
placeholder="Password"
|
||||
required
|
||||
>
|
||||
|
||||
<button type="submit" :disabled="loading" class="login-button">
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="login-button"
|
||||
>
|
||||
{{ loading ? 'Logging in...' : 'Login' }}
|
||||
</button>
|
||||
</form>
|
||||
|
|
@ -51,7 +101,39 @@ const hasOAuth = ref(false)
|
|||
const hasLocalLogin = ref(false)
|
||||
const oauthProviders = ref([])
|
||||
|
||||
function loadLoginOptions() {
|
||||
const trustedProviderIconifyIds = {
|
||||
github: 'simple-icons:github',
|
||||
google: 'simple-icons:google'
|
||||
}
|
||||
|
||||
function providerIcon (provider) {
|
||||
const raw = (provider?.icon || '').trim()
|
||||
if (!raw) {
|
||||
return null
|
||||
}
|
||||
|
||||
const iconifyTagMatch = raw.match(/<iconify-icon\b[^>]*\bicon=["']([^"']+)["'][^>]*>/i)
|
||||
if (iconifyTagMatch) {
|
||||
return { kind: 'iconify', id: iconifyTagMatch[1] }
|
||||
}
|
||||
|
||||
if (/^[a-z0-9-]+:[a-z0-9-]+$/i.test(raw)) {
|
||||
return { kind: 'iconify', id: raw }
|
||||
}
|
||||
|
||||
const trustedId = trustedProviderIconifyIds[provider.key]
|
||||
if (trustedId && (raw.includes('<') || raw === provider.key)) {
|
||||
return { kind: 'iconify', id: trustedId }
|
||||
}
|
||||
|
||||
if (!raw.includes('<')) {
|
||||
return { kind: 'text', text: raw }
|
||||
}
|
||||
|
||||
return trustedId ? { kind: 'iconify', id: trustedId } : null
|
||||
}
|
||||
|
||||
function loadLoginOptions () {
|
||||
// Use the init response data that was loaded in App.vue
|
||||
if (window.initResponse) {
|
||||
hasOAuth.value = window.initResponse.oAuth2Providers && window.initResponse.oAuth2Providers.length > 0
|
||||
|
|
@ -65,7 +147,7 @@ function loadLoginOptions() {
|
|||
}
|
||||
}
|
||||
|
||||
async function handleLocalLogin() {
|
||||
async function handleLocalLogin () {
|
||||
loading.value = true
|
||||
loginError.value = ''
|
||||
|
||||
|
|
@ -83,7 +165,7 @@ async function handleLocalLogin() {
|
|||
window.initError = false
|
||||
window.initErrorMessage = ''
|
||||
window.initCompleted = true
|
||||
|
||||
|
||||
// Update the header with new user info
|
||||
if (window.updateHeaderFromInit) {
|
||||
window.updateHeaderFromInit()
|
||||
|
|
@ -91,7 +173,7 @@ async function handleLocalLogin() {
|
|||
} catch (initErr) {
|
||||
console.error('Failed to reinitialize after login:', initErr)
|
||||
}
|
||||
|
||||
|
||||
// Redirect to home page on successful login
|
||||
router.push('/')
|
||||
} else {
|
||||
|
|
@ -105,21 +187,21 @@ async function handleLocalLogin() {
|
|||
}
|
||||
}
|
||||
|
||||
function loginWithOAuth(provider) {
|
||||
function loginWithOAuth (provider) {
|
||||
if (!provider.key) {
|
||||
console.error('OAuth provider missing key:', provider)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const providerKey = encodeURIComponent(provider.key)
|
||||
window.location.href = `/oauth/login?provider=${providerKey}`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadLoginOptions()
|
||||
|
||||
|
||||
// Also watch for when init response becomes available
|
||||
const stopWatcher = watch(() => window.initResponse, () => {
|
||||
watch(() => window.initResponse, () => {
|
||||
loadLoginOptions()
|
||||
}, { immediate: true })
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,9 +1,23 @@
|
|||
<template>
|
||||
<Section :title="t('logs.calendar-title')" :padding="false">
|
||||
<Section
|
||||
:title="t('logs.calendar-title')"
|
||||
:padding="false"
|
||||
>
|
||||
<template #toolbar>
|
||||
<router-link to="/logs" class="button neutral">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8l8 8l1.41-1.41L7.83 13H20z"/>
|
||||
<router-link
|
||||
to="/logs"
|
||||
class="button neutral"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M20 11H7.83l5.59-5.59L12 4l-8 8l8 8l1.41-1.41L7.83 13H20z"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('logs.back-to-list') }}
|
||||
</router-link>
|
||||
|
|
@ -52,7 +66,7 @@ const calendarEvents = computed(() => {
|
|||
.map(log => {
|
||||
const startDate = new Date(log.datetimeStarted)
|
||||
let endDate = log.datetimeFinished ? new Date(log.datetimeFinished) : null
|
||||
|
||||
|
||||
// Validate end date
|
||||
if (endDate && isNaN(endDate.getTime())) {
|
||||
endDate = null
|
||||
|
|
@ -62,8 +76,8 @@ const calendarEvents = computed(() => {
|
|||
id: log.executionTrackingId,
|
||||
title: log.actionTitle || 'Untitled Action',
|
||||
date: startDate,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
startDate,
|
||||
endDate,
|
||||
actionIcon: log.actionIcon,
|
||||
user: log.user,
|
||||
tags: log.tags,
|
||||
|
|
@ -72,44 +86,43 @@ const calendarEvents = computed(() => {
|
|||
})
|
||||
})
|
||||
|
||||
async function fetchLogs() {
|
||||
async function fetchLogs () {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
|
||||
try {
|
||||
// Currently fetches only the default page (startOffset: 0)
|
||||
// Multi-page fetching: loop through pages until no more logs or limit reached
|
||||
const allLogs = []
|
||||
let startOffset = BigInt(0)
|
||||
const maxLogs = 10000 // Reasonable limit to prevent excessive API calls
|
||||
const pageSize = 100 // Typical page size, will be updated from API response
|
||||
|
||||
|
||||
while (allLogs.length < maxLogs) {
|
||||
const args = {
|
||||
"startOffset": startOffset,
|
||||
startOffset
|
||||
}
|
||||
|
||||
const response = await window.client.getLogs(args)
|
||||
const pageLogs = response.logs || []
|
||||
|
||||
|
||||
// If no logs returned, we've reached the end
|
||||
if (pageLogs.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
// Append logs from this page
|
||||
allLogs.push(...pageLogs)
|
||||
|
||||
|
||||
// Update offset for next page
|
||||
const currentPageSize = Number(response.pageSize) || pageLogs.length
|
||||
startOffset += BigInt(currentPageSize)
|
||||
|
||||
|
||||
// If we got fewer logs than the page size, we've reached the end
|
||||
if (pageLogs.length < currentPageSize) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
logs.value = allLogs
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch logs:', err)
|
||||
|
|
@ -120,14 +133,14 @@ async function fetchLogs() {
|
|||
}
|
||||
}
|
||||
|
||||
function handleEventClick(event) {
|
||||
function handleEventClick (event) {
|
||||
// Navigate to the execution view when clicking on a calendar event
|
||||
if (event.id) {
|
||||
router.push(`/logs/${event.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDayClick(date) {
|
||||
function handleDayClick (date) {
|
||||
// Navigate to logs list filtered by the selected date
|
||||
// Format date as YYYY-MM-DD using local date components to avoid timezone issues
|
||||
const year = date.getFullYear()
|
||||
|
|
@ -137,7 +150,7 @@ function handleDayClick(date) {
|
|||
router.push({ path: '/logs', query: { date: dateString } })
|
||||
}
|
||||
|
||||
function handleMonthChange(month, year) {
|
||||
function handleMonthChange (month, year) {
|
||||
currentMonthIndex.value = month
|
||||
currentYear.value = year
|
||||
// Optionally fetch logs for the new month if needed
|
||||
|
|
|
|||
|
|
@ -1,116 +1,212 @@
|
|||
<template>
|
||||
<Section :title="t('logs.title')" :padding="false">
|
||||
<template #toolbar>
|
||||
<router-link to="/logs/queue" class="button neutral">
|
||||
{{ t('logs.queue') }}
|
||||
</router-link>
|
||||
<router-link to="/logs/calendar" class="button neutral">
|
||||
{{ t('logs.calendar') }}
|
||||
</router-link>
|
||||
<label class="input-with-icons">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14" />
|
||||
</svg>
|
||||
<input
|
||||
:placeholder="t('logs.filter-placeholder')"
|
||||
v-model="searchText"
|
||||
list="logs-filter-suggestions"
|
||||
:aria-invalid="filterError ? 'true' : 'false'"
|
||||
<Section
|
||||
:title="t('logs.title')"
|
||||
:padding="false"
|
||||
>
|
||||
<template #toolbar>
|
||||
<router-link
|
||||
to="/logs/queue"
|
||||
class="button neutral"
|
||||
>
|
||||
{{ t('logs.queue') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/logs/calendar"
|
||||
class="button neutral"
|
||||
>
|
||||
{{ t('logs.calendar') }}
|
||||
</router-link>
|
||||
<label class="input-with-icons">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14"
|
||||
/>
|
||||
<datalist id="logs-filter-suggestions">
|
||||
<option v-for="suggestion in filterSuggestions" :key="suggestion" :value="suggestion" />
|
||||
</datalist>
|
||||
<button :title="t('logs.clear-filter')" :disabled="!searchText" @click="clearSearch">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z" />
|
||||
</svg>
|
||||
</button>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<div class="padding logs-intro">
|
||||
<p>{{ t('logs.page-description') }}</p>
|
||||
<details class="filter-help">
|
||||
<summary>{{ t('logs.filter-help-title') }}</summary>
|
||||
<p>{{ t('logs.filter-help-intro') }}</p>
|
||||
<p>{{ t('logs.filter-help-fields') }}</p>
|
||||
<p><code>{{ t('logs.filter-help-examples') }}</code></p>
|
||||
</details>
|
||||
<p v-if="filterError" class="filter-error" role="alert">{{ filterError }}</p>
|
||||
</div>
|
||||
|
||||
<div v-show="logs.length > 0">
|
||||
<table class="logs-table row-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="timestamp-header">
|
||||
<span>{{ t('logs.timestamp') }}</span>
|
||||
<span v-if="selectedDate" class="date-filter-indicator">
|
||||
<span class="date-filter-text">{{ formatDateFilter(selectedDate) }}</span>
|
||||
<button :title="t('logs.clear-date-filter')" @click="clearDateFilter" class="clear-date-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th>{{ t('logs.action') }}</th>
|
||||
<th>{{ t('logs.metadata') }}</th>
|
||||
<th>{{ t('logs.status') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="log in logs" :key="log.executionTrackingId" class="log-row" :title="log.actionTitle">
|
||||
<td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td>
|
||||
<td>
|
||||
<ActionIconGlyph class="icon" :glyph="log.actionIcon" />
|
||||
<router-link :to="`/logs/${log.executionTrackingId}`">
|
||||
<LogActionTitle :action-title="log.actionTitle" :justification="log.justification" />
|
||||
</router-link>
|
||||
</td>
|
||||
<td class="tags">
|
||||
<span class="annotation">
|
||||
<span class="annotation-key">User:</span>
|
||||
<span class="annotation-val">{{ log.user }}</span>
|
||||
</span>
|
||||
<span v-if="log.tags && log.tags.length > 0" class="tag-list">
|
||||
<span v-for="tag in log.tags" :key="tag" class="tag">{{ tag }}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="exit-code">
|
||||
<ActionStatusDisplay :logEntry="log" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Pagination :pageSize="pageSize" :total="totalCount" :currentPage="currentPage" :page="currentPage" @page-change="handlePageChange" class = "padding"
|
||||
@page-size-change="handlePageSizeChange" itemTitle="execution logs" />
|
||||
</div>
|
||||
|
||||
<div v-show="logs.length === 0 && !loading && searchText && !filterError" class="empty-state padding">
|
||||
<p>{{ t('logs.no-logs-for-filter') }}</p>
|
||||
<button @click="clearSearch" class="button neutral">
|
||||
{{ t('logs.clear-filter') }}
|
||||
</svg>
|
||||
<input
|
||||
v-model="searchText"
|
||||
:placeholder="t('logs.filter-placeholder')"
|
||||
list="logs-filter-suggestions"
|
||||
:aria-invalid="filterError ? 'true' : 'false'"
|
||||
>
|
||||
<datalist id="logs-filter-suggestions">
|
||||
<option
|
||||
v-for="suggestion in filterSuggestions"
|
||||
:key="suggestion"
|
||||
:value="suggestion"
|
||||
/>
|
||||
</datalist>
|
||||
<button
|
||||
:title="t('logs.clear-filter')"
|
||||
:disabled="!searchText"
|
||||
@click="clearSearch"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<div v-show="selectedDate && logs.length === 0 && !loading && !searchText" class="empty-state padding">
|
||||
<p>{{ t('logs.no-logs-to-display') }} {{ formatDateFilter(selectedDate) }}.</p>
|
||||
<button @click="clearDateFilter" class="button neutral">
|
||||
{{ t('logs.clear-date-filter') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="padding logs-intro">
|
||||
<p>{{ t('logs.page-description') }}</p>
|
||||
<details class="filter-help">
|
||||
<summary>{{ t('logs.filter-help-title') }}</summary>
|
||||
<p>{{ t('logs.filter-help-intro') }}</p>
|
||||
<p>{{ t('logs.filter-help-fields') }}</p>
|
||||
<p><code>{{ t('logs.filter-help-examples') }}</code></p>
|
||||
</details>
|
||||
<p
|
||||
v-if="filterError"
|
||||
class="filter-error"
|
||||
role="alert"
|
||||
>
|
||||
{{ filterError }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-show="logs.length === 0 && !loading && !selectedDate && !searchText" class="empty-state padding">
|
||||
<p>{{ t('logs.no-logs-to-display') }}</p>
|
||||
<router-link to="/">{{ t('return-to-index') }}</router-link>
|
||||
</div>
|
||||
<div v-show="logs.length > 0">
|
||||
<table class="logs-table row-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="timestamp-header">
|
||||
<span>{{ t('logs.timestamp') }}</span>
|
||||
<span
|
||||
v-if="selectedDate"
|
||||
class="date-filter-indicator"
|
||||
>
|
||||
<span class="date-filter-text">{{ formatDateFilter(selectedDate) }}</span>
|
||||
<button
|
||||
:title="t('logs.clear-date-filter')"
|
||||
class="clear-date-button"
|
||||
@click="clearDateFilter"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th>{{ t('logs.action') }}</th>
|
||||
<th>{{ t('logs.metadata') }}</th>
|
||||
<th>{{ t('logs.status') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="log in logs"
|
||||
:key="log.executionTrackingId"
|
||||
class="log-row"
|
||||
:title="log.actionTitle"
|
||||
>
|
||||
<td class="timestamp">
|
||||
{{ formatTimestamp(log.datetimeStarted) }}
|
||||
</td>
|
||||
<td>
|
||||
<ActionIconGlyph
|
||||
class="icon"
|
||||
:glyph="log.actionIcon"
|
||||
/>
|
||||
<router-link :to="`/logs/${log.executionTrackingId}`">
|
||||
<LogActionTitle
|
||||
:action-title="log.actionTitle"
|
||||
:justification="log.justification"
|
||||
/>
|
||||
</router-link>
|
||||
</td>
|
||||
<td class="tags">
|
||||
<span class="annotation">
|
||||
<span class="annotation-key">User:</span>
|
||||
<span class="annotation-val">{{ log.user }}</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="log.tags && log.tags.length > 0"
|
||||
class="tag-list"
|
||||
>
|
||||
<span
|
||||
v-for="tag in log.tags"
|
||||
:key="tag"
|
||||
class="tag"
|
||||
>{{ tag }}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="exit-code">
|
||||
<ActionStatusDisplay :log-entry="log" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Pagination
|
||||
:page-size="pageSize"
|
||||
:total="totalCount"
|
||||
:current-page="currentPage"
|
||||
:page="currentPage"
|
||||
class="padding"
|
||||
item-title="execution logs"
|
||||
@page-change="handlePageChange"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="logs.length === 0 && !loading && searchText && !filterError"
|
||||
class="empty-state padding"
|
||||
>
|
||||
<p>{{ t('logs.no-logs-for-filter') }}</p>
|
||||
<button
|
||||
class="button neutral"
|
||||
@click="clearSearch"
|
||||
>
|
||||
{{ t('logs.clear-filter') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="selectedDate && logs.length === 0 && !loading && !searchText"
|
||||
class="empty-state padding"
|
||||
>
|
||||
<p>{{ t('logs.no-logs-to-display') }} {{ formatDateFilter(selectedDate) }}.</p>
|
||||
<button
|
||||
class="button neutral"
|
||||
@click="clearDateFilter"
|
||||
>
|
||||
{{ t('logs.clear-date-filter') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="logs.length === 0 && !loading && !selectedDate && !searchText"
|
||||
class="empty-state padding"
|
||||
>
|
||||
<p>{{ t('logs.no-logs-to-display') }}</p>
|
||||
<router-link to="/">
|
||||
{{ t('return-to-index') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</Section>
|
||||
</template>
|
||||
|
||||
|
|
@ -150,7 +246,7 @@ const filterSuggestions = [
|
|||
|
||||
const { t } = useI18n()
|
||||
|
||||
function updateDateFromRoute() {
|
||||
function updateDateFromRoute () {
|
||||
const dateParam = route.query.date
|
||||
if (dateParam) {
|
||||
selectedDate.value = dateParam
|
||||
|
|
@ -169,7 +265,7 @@ watch(searchText, () => {
|
|||
scheduleFetchLogs()
|
||||
})
|
||||
|
||||
async function fetchLogs() {
|
||||
async function fetchLogs () {
|
||||
loading.value = true
|
||||
filterError.value = ''
|
||||
try {
|
||||
|
|
@ -206,7 +302,7 @@ async function fetchLogs() {
|
|||
}
|
||||
}
|
||||
|
||||
function scheduleFetchLogs() {
|
||||
function scheduleFetchLogs () {
|
||||
if (fetchTimer) {
|
||||
clearTimeout(fetchTimer)
|
||||
}
|
||||
|
|
@ -215,18 +311,18 @@ function scheduleFetchLogs() {
|
|||
}, 400)
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
function clearSearch () {
|
||||
searchText.value = ''
|
||||
}
|
||||
|
||||
function clearDateFilter() {
|
||||
function clearDateFilter () {
|
||||
selectedDate.value = null
|
||||
const query = { ...route.query }
|
||||
delete query.date
|
||||
router.push({ path: route.path, query })
|
||||
}
|
||||
|
||||
function formatDateFilter(dateString) {
|
||||
function formatDateFilter (dateString) {
|
||||
try {
|
||||
const date = new Date(dateString + 'T00:00:00')
|
||||
return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
|
||||
|
|
@ -235,7 +331,7 @@ function formatDateFilter(dateString) {
|
|||
}
|
||||
}
|
||||
|
||||
function formatTimestamp(timestamp) {
|
||||
function formatTimestamp (timestamp) {
|
||||
if (!timestamp) return 'Unknown'
|
||||
try {
|
||||
const date = new Date(timestamp)
|
||||
|
|
@ -245,18 +341,18 @@ function formatTimestamp(timestamp) {
|
|||
}
|
||||
}
|
||||
|
||||
function handlePageChange(page) {
|
||||
function handlePageChange (page) {
|
||||
currentPage.value = page
|
||||
fetchLogs()
|
||||
}
|
||||
|
||||
function handlePageSizeChange(newPageSize) {
|
||||
function handlePageSizeChange (newPageSize) {
|
||||
pageSize.value = newPageSize
|
||||
currentPage.value = 1
|
||||
fetchLogs()
|
||||
}
|
||||
|
||||
function onExecutionEvent(evt) {
|
||||
function onExecutionEvent (evt) {
|
||||
const logEntry = getExecutionLogEntry(evt)
|
||||
if (!logEntry) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,19 +1,40 @@
|
|||
<template>
|
||||
<Section :title="t('logs.queue-title')" :padding="false">
|
||||
<Section
|
||||
:title="t('logs.queue-title')"
|
||||
:padding="false"
|
||||
>
|
||||
<template #toolbar>
|
||||
<router-link to="/logs" class="button neutral">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8l8 8l1.41-1.41L7.83 13H20z"/>
|
||||
<router-link
|
||||
to="/logs"
|
||||
class="button neutral"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M20 11H7.83l5.59-5.59L12 4l-8 8l8 8l1.41-1.41L7.83 13H20z"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('logs.back-to-list') }}
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<p class="padding">{{ t('logs.queue-page-description') }}</p>
|
||||
<p class="padding">
|
||||
{{ t('logs.queue-page-description') }}
|
||||
</p>
|
||||
|
||||
<div v-if="groups.length === 0 && !loading" class="empty-state padding">
|
||||
<div
|
||||
v-if="groups.length === 0 && !loading"
|
||||
class="empty-state padding"
|
||||
>
|
||||
<p>{{ t('logs.queue-empty') }}</p>
|
||||
<router-link to="/logs">{{ t('logs.back-to-list') }}</router-link>
|
||||
<router-link to="/logs">
|
||||
{{ t('logs.back-to-list') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
|
|
@ -24,7 +45,10 @@
|
|||
>
|
||||
<div class="section-header flex-row">
|
||||
<div class="fg1 queue-action-group-heading">
|
||||
<ActionIconGlyph class="icon" :glyph="actionGroup.icon" />
|
||||
<ActionIconGlyph
|
||||
class="icon"
|
||||
:glyph="actionGroup.icon"
|
||||
/>
|
||||
<h2>{{ displayActionGroupName(actionGroup.name) }}</h2>
|
||||
</div>
|
||||
<ActionGroupLimitsLabel
|
||||
|
|
@ -44,23 +68,34 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="action in actionGroup.actions" :key="`${actionGroup.name}:${action.bindingId}`">
|
||||
<template
|
||||
v-for="action in actionGroup.actions"
|
||||
:key="`${actionGroup.name}:${action.bindingId}`"
|
||||
>
|
||||
<tr
|
||||
v-for="(entry, index) in action.entries"
|
||||
:key="entry.executionTrackingId"
|
||||
class="log-row"
|
||||
:title="action.actionTitle"
|
||||
>
|
||||
<td class="timestamp">{{ formatTimestamp(entry.datetimeStarted) }}</td>
|
||||
<td class="timestamp">
|
||||
{{ formatTimestamp(entry.datetimeStarted) }}
|
||||
</td>
|
||||
<td>
|
||||
<ActionIconGlyph class="icon" :glyph="action.actionIcon" />
|
||||
<ActionIconGlyph
|
||||
class="icon"
|
||||
:glyph="action.actionIcon"
|
||||
/>
|
||||
<router-link :to="`/logs/${entry.executionTrackingId}`">
|
||||
<LogActionTitle
|
||||
:action-title="action.actionTitle"
|
||||
:justification="entry.justification"
|
||||
/>
|
||||
</router-link>
|
||||
<span v-if="action.entityPrefix" class="queue-entity annotation">
|
||||
<span
|
||||
v-if="action.entityPrefix"
|
||||
class="queue-entity annotation"
|
||||
>
|
||||
{{ action.entityPrefix }}
|
||||
</span>
|
||||
</td>
|
||||
|
|
@ -69,15 +104,28 @@
|
|||
<span class="annotation-key">User:</span>
|
||||
<span class="annotation-val">{{ entry.user }}</span>
|
||||
</span>
|
||||
<span v-if="entry.tags && entry.tags.length > 0" class="tag-list">
|
||||
<span v-for="tag in entry.tags" :key="tag" class="tag">{{ tag }}</span>
|
||||
<span
|
||||
v-if="entry.tags && entry.tags.length > 0"
|
||||
class="tag-list"
|
||||
>
|
||||
<span
|
||||
v-for="tag in entry.tags"
|
||||
:key="tag"
|
||||
class="tag"
|
||||
>{{ tag }}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="exit-code">
|
||||
<span v-if="!entry.executionFinished" class="queue-position">
|
||||
<span
|
||||
v-if="!entry.executionFinished"
|
||||
class="queue-position"
|
||||
>
|
||||
{{ t('logs.queue-position', { position: index + 1 }) }}
|
||||
</span>
|
||||
<ActionStatusDisplay :logEntry="entry" link-queued-status />
|
||||
<ActionStatusDisplay
|
||||
:log-entry="entry"
|
||||
link-queued-status
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -4,12 +4,18 @@
|
|||
<div class="not-found-content">
|
||||
<h1>404</h1>
|
||||
<h2>Page Not Found</h2>
|
||||
|
||||
|
||||
<div class="actions">
|
||||
<button class = "button good" @click="goToHome">
|
||||
<button
|
||||
class="button good"
|
||||
@click="goToHome"
|
||||
>
|
||||
Go to Home
|
||||
</button>
|
||||
<button class="button neutral" @click="goBack">
|
||||
<button
|
||||
class="button neutral"
|
||||
@click="goBack"
|
||||
>
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -22,10 +28,10 @@
|
|||
export default {
|
||||
name: 'NotFoundView',
|
||||
methods: {
|
||||
goBack() {
|
||||
goBack () {
|
||||
this.$router.go(-1)
|
||||
},
|
||||
goToHome() {
|
||||
goToHome () {
|
||||
this.$router.push('/')
|
||||
}
|
||||
}
|
||||
|
|
@ -57,4 +63,4 @@ export default {
|
|||
color: #666;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,27 +1,58 @@
|
|||
<template>
|
||||
<Section title="User Information" class="small">
|
||||
<div v-if="!isLoggedIn" class="user-not-logged-in">
|
||||
<Section
|
||||
title="User Information"
|
||||
class="small"
|
||||
>
|
||||
<div
|
||||
v-if="!isLoggedIn"
|
||||
class="user-not-logged-in"
|
||||
>
|
||||
<p>You are not currently logged in.</p>
|
||||
<p>To access user settings and logout, please <router-link to="/login">log in</router-link>.</p>
|
||||
<p>
|
||||
To access user settings and logout, please <router-link to="/login">
|
||||
log in
|
||||
</router-link>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="user-control-panel">
|
||||
<div
|
||||
v-else
|
||||
class="user-control-panel"
|
||||
>
|
||||
<dl class="user-info">
|
||||
<dt>Username</dt>
|
||||
<dd>{{ username }}</dd>
|
||||
<dt v-if="userProvider !== 'system'">Provider</dt>
|
||||
<dd v-if="userProvider !== 'system'">{{ userProvider }}</dd>
|
||||
<dt v-if="usergroup">Group</dt>
|
||||
<dd v-if="usergroup">{{ usergroup }}</dd>
|
||||
<dt v-if="acls && acls.length > 0">Matched ACLs</dt>
|
||||
<dt v-if="userProvider !== 'system'">
|
||||
Provider
|
||||
</dt>
|
||||
<dd v-if="userProvider !== 'system'">
|
||||
{{ userProvider }}
|
||||
</dd>
|
||||
<dt v-if="usergroup">
|
||||
Group
|
||||
</dt>
|
||||
<dd v-if="usergroup">
|
||||
{{ usergroup }}
|
||||
</dd>
|
||||
<dt v-if="acls && acls.length > 0">
|
||||
Matched ACLs
|
||||
</dt>
|
||||
<dd v-if="acls && acls.length > 0">
|
||||
<span class="acl-tag" v-for="(acl, idx) in acls" :key="`acl-${idx}`">{{ acl }}</span>
|
||||
<span
|
||||
v-for="(acl, idx) in acls"
|
||||
:key="`acl-${idx}`"
|
||||
class="acl-tag"
|
||||
>{{ acl }}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<div class="user-actions">
|
||||
<div class="action-buttons">
|
||||
<button @click="handleLogout" class="button bad" :disabled="loggingOut">
|
||||
<button
|
||||
class="button bad"
|
||||
:disabled="loggingOut"
|
||||
@click="handleLogout"
|
||||
>
|
||||
{{ loggingOut ? 'Logging out...' : 'Logout' }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -31,7 +62,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, onUnmounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Section from 'picocrank/vue/components/Section.vue'
|
||||
|
||||
|
|
@ -44,7 +75,7 @@ const usergroup = ref('')
|
|||
const loggingOut = ref(false)
|
||||
const acls = ref([])
|
||||
|
||||
function updateUserInfo() {
|
||||
function updateUserInfo () {
|
||||
if (window.initResponse) {
|
||||
isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
|
||||
username.value = window.initResponse.authenticatedUser
|
||||
|
|
@ -53,7 +84,7 @@ function updateUserInfo() {
|
|||
}
|
||||
}
|
||||
|
||||
async function fetchWhoAmI() {
|
||||
async function fetchWhoAmI () {
|
||||
try {
|
||||
const res = await window.client.whoAmI({})
|
||||
acls.value = res.acls || []
|
||||
|
|
@ -67,12 +98,12 @@ async function fetchWhoAmI() {
|
|||
}
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
async function handleLogout () {
|
||||
loggingOut.value = true
|
||||
|
||||
|
||||
try {
|
||||
await window.client.logout({})
|
||||
|
||||
|
||||
// Re-initialize to get updated user context (should be guest)
|
||||
try {
|
||||
const initResponse = await window.client.init({})
|
||||
|
|
@ -80,7 +111,7 @@ async function handleLogout() {
|
|||
window.initError = false
|
||||
window.initErrorMessage = ''
|
||||
window.initCompleted = true
|
||||
|
||||
|
||||
// Update the header with new user info
|
||||
if (window.updateHeaderFromInit) {
|
||||
window.updateHeaderFromInit()
|
||||
|
|
@ -88,7 +119,7 @@ async function handleLogout() {
|
|||
} catch (initErr) {
|
||||
console.error('Failed to reinitialize after logout:', initErr)
|
||||
}
|
||||
|
||||
|
||||
// Redirect based on init response: if login is required, go to login page
|
||||
if (window.initResponse && window.initResponse.loginRequired) {
|
||||
router.push('/login')
|
||||
|
|
@ -102,13 +133,12 @@ async function handleLogout() {
|
|||
}
|
||||
}
|
||||
|
||||
let watchInterval = null
|
||||
const watchInterval = null
|
||||
|
||||
onMounted(() => {
|
||||
updateUserInfo()
|
||||
fetchWhoAmI()
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (watchInterval) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"chai": "^6.2.2",
|
||||
"eslint": "^10.5.0",
|
||||
"eslint": "^10.6.0",
|
||||
"mocha": "^11.7.6",
|
||||
"selenium-webdriver": "^4.45.0"
|
||||
}
|
||||
|
|
@ -783,9 +783,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz",
|
||||
"integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==",
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz",
|
||||
"integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
"license": "AGPL-3.0-only",
|
||||
"devDependencies": {
|
||||
"chai": "^6.2.2",
|
||||
"eslint": "^10.5.0",
|
||||
"eslint": "^10.6.0",
|
||||
"mocha": "^11.7.6",
|
||||
"selenium-webdriver": "^4.45.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
#
|
||||
# Integration Test Config: dashboardAcls
|
||||
#
|
||||
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
logLevel: "DEBUG"
|
||||
checkForUpdates: false
|
||||
|
||||
defaultPermissions:
|
||||
view: false
|
||||
exec: false
|
||||
|
||||
accessControlLists:
|
||||
- name: admins
|
||||
matchUsergroups:
|
||||
- admins
|
||||
permissions:
|
||||
view: true
|
||||
exec: true
|
||||
|
||||
dashboards:
|
||||
- title: Public tools
|
||||
contents:
|
||||
- title: Welcome guest
|
||||
type: display
|
||||
|
||||
- title: Services
|
||||
acls:
|
||||
- admins
|
||||
contents:
|
||||
- title: 'Status: running'
|
||||
type: display
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import {
|
||||
getRootAndWait,
|
||||
openSidebar,
|
||||
getNavigationLinks,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
describe('config: dashboardAcls', function () {
|
||||
before(async function () {
|
||||
await runner.start('dashboardAcls')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver)
|
||||
})
|
||||
|
||||
it('hides ACL-restricted dashboards from guests in the side menu', async function () {
|
||||
await getRootAndWait()
|
||||
await openSidebar()
|
||||
|
||||
const navigationLinks = await getNavigationLinks()
|
||||
expect(navigationLinks).to.not.be.empty
|
||||
|
||||
const linkTexts = []
|
||||
for (const link of navigationLinks) {
|
||||
linkTexts.push(await link.getText())
|
||||
}
|
||||
|
||||
expect(linkTexts).to.include('Public tools')
|
||||
expect(linkTexts).to.not.include('Services')
|
||||
})
|
||||
})
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
module github.com/OliveTin/OliveTin/langtool
|
||||
|
||||
go 1.24.0
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/jamesread/golure v0.0.0-20260104005024-ad0d6ec8c0ac
|
||||
github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.40.0 // indirect
|
||||
require golang.org/x/sys v0.47.0 // indirect
|
||||
|
|
|
|||
20
lang/go.sum
20
lang/go.sum
|
|
@ -1,28 +1,16 @@
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/jamesread/golure v0.0.0-20250919212919-976d085a100c h1:v8gN2xXFQjkF0PsoGSqDviRNmPHcBsvl6rMSbvXz1sM=
|
||||
github.com/jamesread/golure v0.0.0-20250919212919-976d085a100c/go.mod h1:BZ/CMtZJJ4LNEBDSjGfafTJMjlDPIA9FS16+reN9NUE=
|
||||
github.com/jamesread/golure v0.0.0-20260104005024-ad0d6ec8c0ac h1:JQ6AC9tf2xhwTxMY9nuIeOPM7Cj0BDeCNgKPrNgQvtQ=
|
||||
github.com/jamesread/golure v0.0.0-20260104005024-ad0d6ec8c0ac/go.mod h1:BZ/CMtZJJ4LNEBDSjGfafTJMjlDPIA9FS16+reN9NUE=
|
||||
github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da h1:hYsJqujd3A4Xtp9swe2d6Y6ij2ecd16E+i2oHAH/xaA=
|
||||
github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da/go.mod h1:BZ/CMtZJJ4LNEBDSjGfafTJMjlDPIA9FS16+reN9NUE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ TOOL_GROUPS: list[tuple[str, frozenset[str]]] = [
|
|||
),
|
||||
(
|
||||
"Go codestyle (install via: make go-tools)",
|
||||
frozenset({"gocyclo", "gocritic"}),
|
||||
frozenset({"golangci-lint"}),
|
||||
),
|
||||
(
|
||||
"Protocol buffers (install via: make -C service go-tools-all)",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
tests: true
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- errcheck
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- ineffassign
|
||||
- misspell
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- unused
|
||||
settings:
|
||||
gocyclo:
|
||||
min-complexity: 5
|
||||
exclusions:
|
||||
paths:
|
||||
- gen
|
||||
|
|
@ -30,8 +30,7 @@ compile: compile-armhf compile-x64-lin compile-x64-win
|
|||
codestyle: go-tools
|
||||
go fmt ./...
|
||||
go vet ./...
|
||||
gocyclo -over 4 internal
|
||||
gocritic check ./...
|
||||
golangci-lint run ./...
|
||||
|
||||
test: unittests
|
||||
|
||||
|
|
@ -54,13 +53,11 @@ find-flakey-tests-inf:
|
|||
go run ./scripts/find-flakey-tests-inf
|
||||
|
||||
go-tools:
|
||||
go install "github.com/fzipp/gocyclo/cmd/gocyclo"
|
||||
go install "github.com/go-critic/go-critic/cmd/gocritic"
|
||||
go install "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2"
|
||||
|
||||
.PHONY: unittests unittests-fast find-flakey-tests find-flakey-tests-inf
|
||||
|
||||
go-tools-all:
|
||||
go install "github.com/bufbuild/buf/cmd/buf"
|
||||
go install "github.com/fzipp/gocyclo/cmd/gocyclo"
|
||||
go install "github.com/go-critic/go-critic/cmd/gocritic"
|
||||
go install "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2"
|
||||
go install "google.golang.org/protobuf/cmd/protoc-gen-go"
|
||||
|
|
|
|||
272
service/go.mod
272
service/go.mod
|
|
@ -1,6 +1,6 @@
|
|||
module github.com/OliveTin/OliveTin
|
||||
|
||||
go 1.25.10
|
||||
go 1.26
|
||||
|
||||
exclude google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884
|
||||
|
||||
|
|
@ -10,36 +10,39 @@ require (
|
|||
github.com/MicahParks/keyfunc/v3 v3.8.0
|
||||
github.com/PaesslerAG/jsonpath v0.1.1
|
||||
github.com/alexedwards/argon2id v1.0.0
|
||||
github.com/bufbuild/buf v1.70.0
|
||||
github.com/bufbuild/buf v1.71.0
|
||||
github.com/expr-lang/expr v1.17.8
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
github.com/fzipp/gocyclo v0.6.0
|
||||
github.com/go-critic/go-critic v0.14.3
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/golangci/golangci-lint/v2 v2.12.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da
|
||||
github.com/knadh/koanf/parsers/yaml v1.1.0
|
||||
github.com/knadh/koanf/providers/env v1.1.0
|
||||
github.com/knadh/koanf/providers/file v1.2.1
|
||||
github.com/knadh/koanf/providers/rawbytes v1.0.0
|
||||
github.com/knadh/koanf/v2 v2.3.4
|
||||
github.com/knadh/koanf/v2 v2.3.5
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/stretchr/testify v1.11.1
|
||||
go.akshayshah.org/connectproto v0.6.0
|
||||
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a
|
||||
golang.org/x/exp v0.0.0-20260611194520-c48552f49976
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sys v0.45.0
|
||||
golang.org/x/sys v0.47.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1 // indirect
|
||||
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
|
||||
4d63.com/gochecknoglobals v0.2.2 // indirect
|
||||
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20260626152828-968bf0468096.1 // indirect
|
||||
buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 // indirect
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 // indirect
|
||||
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260507063250-43b0c5a6cd08.1 // indirect
|
||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260507063250-43b0c5a6cd08.1 // indirect
|
||||
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260629211719-be09125d9193.1 // indirect
|
||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260629211719-be09125d9193.1 // indirect
|
||||
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 // indirect
|
||||
buf.build/go/app v0.2.1-0.20260407195847-833f8f978cda // indirect
|
||||
buf.build/go/bufplugin v0.10.0 // indirect
|
||||
|
|
@ -50,33 +53,86 @@ require (
|
|||
buf.build/go/spdx v0.2.0 // indirect
|
||||
buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 // indirect
|
||||
cel.dev/expr v0.25.2 // indirect
|
||||
charm.land/lipgloss/v2 v2.0.3 // indirect
|
||||
codeberg.org/chavacava/garif v0.2.0 // indirect
|
||||
codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect
|
||||
connectrpc.com/otelconnect v0.9.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect
|
||||
dev.gaijin.team/go/golib v0.6.0 // indirect
|
||||
github.com/4meepo/tagalign v1.4.3 // indirect
|
||||
github.com/Abirdcfly/dupword v0.1.7 // indirect
|
||||
github.com/AdminBenni/iota-mixing v1.0.0 // indirect
|
||||
github.com/AlwxSin/noinlineerr v1.0.5 // indirect
|
||||
github.com/Antonboom/errname v1.1.1 // indirect
|
||||
github.com/Antonboom/nilnil v1.1.1 // indirect
|
||||
github.com/Antonboom/testifylint v1.6.4 // indirect
|
||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||
github.com/ClickHouse/clickhouse-go-linter v1.2.0 // indirect
|
||||
github.com/Djarvur/go-err113 v0.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.5.0 // indirect
|
||||
github.com/MicahParks/jwkset v0.11.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/MirrexOne/unqueryvet v1.5.4 // indirect
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
|
||||
github.com/PaesslerAG/gval v1.2.4 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.24.1 // indirect
|
||||
github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
|
||||
github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
|
||||
github.com/alexkohler/prealloc v1.1.0 // indirect
|
||||
github.com/alfatraining/structtag v1.0.0 // indirect
|
||||
github.com/alingse/asasalint v0.0.11 // indirect
|
||||
github.com/alingse/nilnesserr v0.2.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/ashanbrown/forbidigo/v2 v2.3.1 // indirect
|
||||
github.com/ashanbrown/makezero/v2 v2.2.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bufbuild/protocompile v0.14.2-0.20260522222248-64e6ad034132 // indirect
|
||||
github.com/bkielbasa/cyclop v1.2.3 // indirect
|
||||
github.com/blizzy78/varnamelen v0.8.0 // indirect
|
||||
github.com/bombsimon/wsl/v4 v4.7.0 // indirect
|
||||
github.com/bombsimon/wsl/v5 v5.8.0 // indirect
|
||||
github.com/breml/bidichk v0.3.3 // indirect
|
||||
github.com/breml/errchkjson v0.4.1 // indirect
|
||||
github.com/bufbuild/protocompile v0.14.2-0.20260605203730-cd7c3c124e10 // indirect
|
||||
github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b // indirect
|
||||
github.com/butuzov/ireturn v0.4.1 // indirect
|
||||
github.com/butuzov/mirror v1.3.0 // indirect
|
||||
github.com/catenacyber/perfsprint v0.10.1 // indirect
|
||||
github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charithe/durationcheck v0.0.11 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.7 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||
github.com/ckaznocha/intrange v0.3.1 // indirect
|
||||
github.com/cli/browser v1.3.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/cristalhq/acmd v0.12.0 // indirect
|
||||
github.com/curioswitch/go-reassign v0.3.0 // indirect
|
||||
github.com/daixiang0/gci v0.13.7 // indirect
|
||||
github.com/dave/dst v0.27.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/denis-tingaikin/go-header v0.5.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/cli v29.5.2+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v28.5.2+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.7 // indirect
|
||||
github.com/dlclark/regexp2 v1.12.0 // indirect
|
||||
github.com/docker/cli v29.6.1+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.8 // indirect
|
||||
github.com/docker/go-connections v0.7.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/expr-lang/expr v1.17.8 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||
github.com/ettle/strcase v0.2.0 // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/fatih/structtag v1.2.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.1.0 // indirect
|
||||
github.com/firefart/nonamedreturns v1.0.6 // indirect
|
||||
github.com/fzipp/gocyclo v0.6.0 // indirect
|
||||
github.com/ghostiam/protogetter v0.3.20 // indirect
|
||||
github.com/go-critic/go-critic v0.14.4 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-toolsmith/astcast v1.1.0 // indirect
|
||||
|
|
@ -84,83 +140,175 @@ require (
|
|||
github.com/go-toolsmith/astequal v1.2.0 // indirect
|
||||
github.com/go-toolsmith/astfmt v1.1.0 // indirect
|
||||
github.com/go-toolsmith/astp v1.1.0 // indirect
|
||||
github.com/go-toolsmith/pkgload v1.2.2 // indirect
|
||||
github.com/go-toolsmith/strparse v1.1.0 // indirect
|
||||
github.com/go-toolsmith/typep v1.1.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/godoc-lint/godoc-lint v0.11.2 // indirect
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/google/cel-go v0.28.1 // indirect
|
||||
github.com/golangci/asciicheck v0.5.0 // indirect
|
||||
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 // indirect
|
||||
github.com/golangci/go-printf-func-name v0.1.1 // indirect
|
||||
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect
|
||||
github.com/golangci/golines v0.15.0 // indirect
|
||||
github.com/golangci/misspell v0.8.0 // indirect
|
||||
github.com/golangci/plugin-module-register v0.1.2 // indirect
|
||||
github.com/golangci/revgrep v0.8.0 // indirect
|
||||
github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba // indirect
|
||||
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect
|
||||
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect
|
||||
github.com/google/cel-go v0.29.2 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-containerregistry v0.21.6 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/google/go-containerregistry v0.21.7 // indirect
|
||||
github.com/gordonklaus/ineffassign v0.2.0 // indirect
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
|
||||
github.com/gostaticanalysis/comment v1.5.0 // indirect
|
||||
github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
|
||||
github.com/gostaticanalysis/nilerr v0.1.2 // indirect
|
||||
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
|
||||
github.com/hashicorp/go-version v1.9.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hexops/gotextdiff v1.0.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jdx/go-netrc v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/jgautheron/goconst v1.10.0 // indirect
|
||||
github.com/jjti/go-spancheck v0.6.5 // indirect
|
||||
github.com/julz/importas v0.2.0 // indirect
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect
|
||||
github.com/kisielk/errcheck v1.10.0 // indirect
|
||||
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
|
||||
github.com/klauspost/compress v1.19.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/knadh/koanf/maps v0.1.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/kulti/thelper v0.7.1 // indirect
|
||||
github.com/kunwardeep/paralleltest v1.0.15 // indirect
|
||||
github.com/lasiar/canonicalheader v1.1.2 // indirect
|
||||
github.com/ldez/exptostd v0.4.5 // indirect
|
||||
github.com/ldez/gomoddirectives v0.8.0 // indirect
|
||||
github.com/ldez/grignotin v0.10.1 // indirect
|
||||
github.com/ldez/structtags v0.6.1 // indirect
|
||||
github.com/ldez/tagliatelle v0.7.2 // indirect
|
||||
github.com/ldez/usetesting v0.5.0 // indirect
|
||||
github.com/leonklingele/grouper v1.1.2 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||
github.com/macabu/inamedparam v0.2.0 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect
|
||||
github.com/manuelarte/funcorder v0.6.0 // indirect
|
||||
github.com/maratori/testableexamples v1.0.1 // indirect
|
||||
github.com/maratori/testpackage v1.1.2 // indirect
|
||||
github.com/matoous/godox v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||
github.com/mgechev/revive v1.15.0 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/moby/api v1.54.2 // indirect
|
||||
github.com/moby/moby/client v0.4.1 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/morikuni/aec v1.1.0 // indirect
|
||||
github.com/moby/moby/api v1.55.0 // indirect
|
||||
github.com/moby/moby/client v0.5.0 // indirect
|
||||
github.com/moricho/tparallel v0.3.2 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nakabonne/nestif v0.3.1 // indirect
|
||||
github.com/nishanths/exhaustive v0.12.0 // indirect
|
||||
github.com/nishanths/predeclared v0.2.2 // indirect
|
||||
github.com/nunnatsa/ginkgolinter v0.23.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260701081913-4f67fd55d3b4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/prometheus/common v0.69.0 // indirect
|
||||
github.com/prometheus/procfs v0.21.1 // indirect
|
||||
github.com/quasilyte/go-ruleguard v0.4.5 // indirect
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect
|
||||
github.com/quasilyte/gogrep v0.5.0 // indirect
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
|
||||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.1 // indirect
|
||||
github.com/quic-go/quic-go v0.60.0 // indirect
|
||||
github.com/raeperd/recvcheck v0.2.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/rs/cors v1.11.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
github.com/segmentio/encoding v0.5.4 // indirect
|
||||
github.com/ryancurrah/gomodguard v1.4.1 // indirect
|
||||
github.com/ryancurrah/gomodguard/v2 v2.1.3 // indirect
|
||||
github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect
|
||||
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
|
||||
github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
|
||||
github.com/securego/gosec/v2 v2.26.1 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sivchari/containedctx v1.0.3 // indirect
|
||||
github.com/sonatard/noctx v0.5.1 // indirect
|
||||
github.com/sourcegraph/go-diff v0.8.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/cobra v1.10.2 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.1 // indirect
|
||||
github.com/tetratelabs/wazero v1.11.0 // indirect
|
||||
github.com/spf13/viper v1.12.0 // indirect
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
|
||||
github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.4.1 // indirect
|
||||
github.com/tetafro/godot v1.5.6 // indirect
|
||||
github.com/tetratelabs/wazero v1.12.0 // indirect
|
||||
github.com/tidwall/btree v1.8.1 // indirect
|
||||
github.com/vbatts/tar-split v0.12.3 // indirect
|
||||
go.lsp.dev/jsonrpc2 v0.10.0 // indirect
|
||||
go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 // indirect
|
||||
go.lsp.dev/protocol v0.12.0 // indirect
|
||||
go.lsp.dev/uri v0.3.0 // indirect
|
||||
github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 // indirect
|
||||
github.com/timonwong/loggercheck v0.11.0 // indirect
|
||||
github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
|
||||
github.com/ultraware/funlen v0.2.0 // indirect
|
||||
github.com/ultraware/whitespace v0.2.0 // indirect
|
||||
github.com/uudashr/gocognit v1.2.1 // indirect
|
||||
github.com/uudashr/iface v1.4.2 // indirect
|
||||
github.com/xen0n/gosmopolitan v1.3.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yagipy/maintidx v1.0.0 // indirect
|
||||
github.com/yeya24/promlinter v0.3.0 // indirect
|
||||
github.com/ykadowak/zerologlint v0.1.5 // indirect
|
||||
gitlab.com/bosi/decorder v0.4.2 // indirect
|
||||
go-simpler.org/musttag v0.14.0 // indirect
|
||||
go-simpler.org/sloglint v0.12.0 // indirect
|
||||
go.augendre.info/arangolint v0.4.0 // indirect
|
||||
go.augendre.info/fatcontext v0.9.0 // indirect
|
||||
go.lsp.dev/jsonrpc2 v1.0.1 // indirect
|
||||
go.lsp.dev/protocol v1.0.1 // indirect
|
||||
go.lsp.dev/uri v1.0.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
|
||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.28.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.52.0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20260508232706-74f9aab9d74a // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/term v0.43.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/crypto v0.53.0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976 // indirect
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
golang.org/x/net v0.56.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/term v0.44.0 // indirect
|
||||
golang.org/x/text v0.39.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/tools v0.45.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
golang.org/x/tools v0.47.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
honnef.co/go/tools v0.7.0 // indirect
|
||||
mvdan.cc/gofumpt v0.10.0 // indirect
|
||||
mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect
|
||||
mvdan.cc/xurls/v2 v2.6.0 // indirect
|
||||
pluginrpc.com/pluginrpc v0.5.0 // indirect
|
||||
)
|
||||
|
|
|
|||
979
service/go.sum
979
service/go.sum
File diff suppressed because it is too large
Load Diff
|
|
@ -21,38 +21,36 @@ func (p PermissionBits) Has(permission PermissionBits) bool {
|
|||
return p&permission != 0
|
||||
}
|
||||
|
||||
func logAclNotMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
|
||||
func logAclNotMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, resourceTitle string, acl *config.AccessControlList) {
|
||||
if cfg.LogDebugOptions.AclNotMatched {
|
||||
log.WithFields(log.Fields{
|
||||
"User": user.Username,
|
||||
"Action": action.Title,
|
||||
"ACL": acl.Name,
|
||||
"User": user.Username,
|
||||
"Resource": resourceTitle,
|
||||
"ACL": acl.Name,
|
||||
}).Debugf("%v - ACL Not Matched", aclFunction)
|
||||
}
|
||||
}
|
||||
|
||||
func logAclMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
|
||||
actionTitle := "N/A"
|
||||
|
||||
if action != nil {
|
||||
actionTitle = action.Title
|
||||
func logAclMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, resourceTitle string, acl *config.AccessControlList) {
|
||||
if resourceTitle == "" {
|
||||
resourceTitle = "N/A"
|
||||
}
|
||||
|
||||
if cfg.LogDebugOptions.AclMatched {
|
||||
log.WithFields(log.Fields{
|
||||
"User": user.Username,
|
||||
"Action": actionTitle,
|
||||
"ACL": acl.Name,
|
||||
"User": user.Username,
|
||||
"Resource": resourceTitle,
|
||||
"ACL": acl.Name,
|
||||
}).Debugf("%v - Matched ACL", aclFunction)
|
||||
}
|
||||
}
|
||||
|
||||
func logAclNoneMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action, defaultPermission bool) {
|
||||
func logAclNoneMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, resourceTitle string, defaultPermission bool) {
|
||||
if cfg.LogDebugOptions.AclNoneMatched {
|
||||
log.WithFields(log.Fields{
|
||||
"User": user.Username,
|
||||
"Action": action.Title,
|
||||
"Default": defaultPermission,
|
||||
"User": user.Username,
|
||||
"Resource": resourceTitle,
|
||||
"Default": defaultPermission,
|
||||
}).Debugf("%v - No ACLs Matched, returning default permission", aclFunction)
|
||||
}
|
||||
}
|
||||
|
|
@ -81,12 +79,12 @@ func permissionsConfigToBits(permissions config.PermissionsList) PermissionBits
|
|||
return ret
|
||||
}
|
||||
|
||||
func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action) bool {
|
||||
relevantAcls := getRelevantAcls(cfg, action.Acls, user)
|
||||
func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, resourceTitle string, resourceAcls []string, includeAddToEvery bool) bool {
|
||||
relevantAcls := getRelevantAcls(cfg, resourceAcls, user, includeAddToEvery)
|
||||
|
||||
if cfg.LogDebugOptions.AclCheckStarted {
|
||||
log.WithFields(log.Fields{
|
||||
"actionTitle": action.Title,
|
||||
"resourceTitle": resourceTitle,
|
||||
"username": user.Username,
|
||||
"usergroupLine": user.UsergroupLine,
|
||||
"relevantAcls": len(relevantAcls),
|
||||
|
|
@ -98,27 +96,27 @@ func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.
|
|||
permissionBits := permissionsConfigToBits(acl.Permissions)
|
||||
|
||||
if permissionBits.Has(requiredPermission) {
|
||||
logAclMatched(cfg, aclFunction, user, action, acl)
|
||||
logAclMatched(cfg, aclFunction, user, resourceTitle, acl)
|
||||
|
||||
return true
|
||||
} else {
|
||||
logAclNotMatched(cfg, aclFunction, user, action, acl)
|
||||
}
|
||||
|
||||
logAclNotMatched(cfg, aclFunction, user, resourceTitle, acl)
|
||||
}
|
||||
|
||||
logAclNoneMatched(cfg, aclFunction, user, action, cfg.DefaultPermissions.Logs)
|
||||
logAclNoneMatched(cfg, aclFunction, user, resourceTitle, defaultValue)
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// IsAllowedLogs checks if a AuthenticatedUser is allowed to view an action's logs
|
||||
func IsAllowedLogs(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
|
||||
return aclCheck(Logs, cfg.DefaultPermissions.Logs, cfg, "isAllowedLogs", user, action)
|
||||
return aclCheck(Logs, cfg.DefaultPermissions.Logs, cfg, "isAllowedLogs", user, action.Title, action.Acls, true)
|
||||
}
|
||||
|
||||
// IsAllowedExec checks if a AuthenticatedUser is allowed to execute an Action
|
||||
func IsAllowedExec(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
|
||||
return aclCheck(Exec, cfg.DefaultPermissions.Exec, cfg, "isAllowedExec", user, action)
|
||||
return aclCheck(Exec, cfg.DefaultPermissions.Exec, cfg, "isAllowedExec", user, action.Title, action.Acls, true)
|
||||
}
|
||||
|
||||
// IsAllowedView checks if a User is allowed to view an Action
|
||||
|
|
@ -127,36 +125,40 @@ func IsAllowedView(cfg *config.Config, user *authpublic.AuthenticatedUser, actio
|
|||
return false
|
||||
}
|
||||
|
||||
return aclCheck(View, cfg.DefaultPermissions.View, cfg, "isAllowedView", user, action)
|
||||
return aclCheck(View, cfg.DefaultPermissions.View, cfg, "isAllowedView", user, action.Title, action.Acls, true)
|
||||
}
|
||||
|
||||
func IsAllowedKill(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
|
||||
return aclCheck(Kill, cfg.DefaultPermissions.Kill, cfg, "isAllowedKill", user, action)
|
||||
return aclCheck(Kill, cfg.DefaultPermissions.Kill, cfg, "isAllowedKill", user, action.Title, action.Acls, true)
|
||||
}
|
||||
|
||||
func isACLRelevantToAction(cfg *config.Config, actionAcls []string, acl *config.AccessControlList, user *authpublic.AuthenticatedUser) bool {
|
||||
if !slices.Contains(user.Acls, acl.Name) {
|
||||
// If the user does not have this ACL, then it is not relevant
|
||||
// IsAllowedViewDashboard checks if a user may see a root dashboard.
|
||||
// Dashboards with no acls are unrestricted. AddToEveryAction does not apply.
|
||||
func IsAllowedViewDashboard(cfg *config.Config, user *authpublic.AuthenticatedUser, dashboard *config.DashboardComponent) bool {
|
||||
if dashboard == nil || len(dashboard.Acls) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return aclCheck(View, cfg.DefaultPermissions.View, cfg, "isAllowedViewDashboard", user, dashboard.Title, dashboard.Acls, false)
|
||||
}
|
||||
|
||||
func isACLRelevant(resourceAcls []string, acl *config.AccessControlList, user *authpublic.AuthenticatedUser, includeAddToEvery bool) bool {
|
||||
if !slices.Contains(user.Acls, acl.Name) {
|
||||
return false
|
||||
}
|
||||
|
||||
if acl.AddToEveryAction {
|
||||
if includeAddToEvery && acl.AddToEveryAction {
|
||||
return true
|
||||
}
|
||||
|
||||
if slices.Contains(actionAcls, acl.Name) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
return slices.Contains(resourceAcls, acl.Name)
|
||||
}
|
||||
|
||||
func getRelevantAcls(cfg *config.Config, actionAcls []string, user *authpublic.AuthenticatedUser) []*config.AccessControlList {
|
||||
func getRelevantAcls(cfg *config.Config, resourceAcls []string, user *authpublic.AuthenticatedUser, includeAddToEvery bool) []*config.AccessControlList {
|
||||
var ret []*config.AccessControlList
|
||||
|
||||
for _, acl := range cfg.AccessControlLists {
|
||||
if isACLRelevantToAction(cfg, actionAcls, acl, user) {
|
||||
if isACLRelevant(resourceAcls, acl, user, includeAddToEvery) {
|
||||
ret = append(ret, acl)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
package acl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsAllowedViewDashboardAbsentAclsUnrestricted(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.DefaultPermissions.View = false
|
||||
|
||||
dashboard := &config.DashboardComponent{
|
||||
Title: "Public",
|
||||
Contents: []*config.DashboardComponent{
|
||||
{Title: "Status", Type: "display"},
|
||||
},
|
||||
}
|
||||
|
||||
guest := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
|
||||
guest.BuildUserAcls(cfg)
|
||||
|
||||
assert.True(t, IsAllowedViewDashboard(cfg, guest, dashboard))
|
||||
assert.True(t, IsAllowedViewDashboard(cfg, guest, nil))
|
||||
}
|
||||
|
||||
func TestIsAllowedViewDashboardAllowDenyAndDefaultFallback(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.DefaultPermissions.View = false
|
||||
cfg.AccessControlLists = []*config.AccessControlList{
|
||||
{
|
||||
Name: "admins",
|
||||
MatchUsernames: []string{"admin"},
|
||||
Permissions: config.PermissionsList{View: true, Exec: true},
|
||||
},
|
||||
}
|
||||
|
||||
dashboard := &config.DashboardComponent{
|
||||
Title: "Services",
|
||||
Acls: []string{"admins"},
|
||||
Contents: []*config.DashboardComponent{
|
||||
{Title: "Status: running", Type: "display"},
|
||||
},
|
||||
}
|
||||
|
||||
guest := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
|
||||
guest.BuildUserAcls(cfg)
|
||||
admin := &authpublic.AuthenticatedUser{Username: "admin"}
|
||||
admin.BuildUserAcls(cfg)
|
||||
|
||||
assert.False(t, IsAllowedViewDashboard(cfg, guest, dashboard))
|
||||
assert.True(t, IsAllowedViewDashboard(cfg, admin, dashboard))
|
||||
|
||||
cfg.DefaultPermissions.View = true
|
||||
assert.True(t, IsAllowedViewDashboard(cfg, guest, dashboard),
|
||||
"when no relevant ACL matches, fall back to defaultPermissions.view")
|
||||
}
|
||||
|
||||
func TestIsAllowedViewDashboardIgnoresAddToEveryAction(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.DefaultPermissions.View = false
|
||||
cfg.AccessControlLists = []*config.AccessControlList{
|
||||
{
|
||||
Name: "admins",
|
||||
MatchUsernames: []string{"admin"},
|
||||
AddToEveryAction: true,
|
||||
Permissions: config.PermissionsList{View: true, Exec: true},
|
||||
},
|
||||
}
|
||||
|
||||
dashboard := &config.DashboardComponent{
|
||||
Title: "Secret",
|
||||
Acls: []string{"other"},
|
||||
Contents: []*config.DashboardComponent{
|
||||
{Title: "Hidden status", Type: "display"},
|
||||
},
|
||||
}
|
||||
|
||||
admin := &authpublic.AuthenticatedUser{Username: "admin"}
|
||||
admin.BuildUserAcls(cfg)
|
||||
|
||||
assert.False(t, IsAllowedViewDashboard(cfg, admin, dashboard),
|
||||
"AddToEveryAction must not grant dashboard view without listing the ACL on the dashboard")
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/OliveTin/OliveTin/internal/executor"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func buildDashboardAclTestConfig() *config.Config {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.DefaultPermissions.View = false
|
||||
cfg.DefaultPermissions.Exec = false
|
||||
cfg.AccessControlLists = []*config.AccessControlList{
|
||||
{
|
||||
Name: "admins",
|
||||
MatchUsernames: []string{"admin"},
|
||||
Permissions: config.PermissionsList{View: true, Exec: true},
|
||||
},
|
||||
}
|
||||
cfg.Dashboards = []*config.DashboardComponent{
|
||||
{
|
||||
Title: "Public tools",
|
||||
Contents: []*config.DashboardComponent{
|
||||
{Title: "Welcome", Type: "display"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "Services",
|
||||
Acls: []string{"admins"},
|
||||
Contents: []*config.DashboardComponent{
|
||||
{Title: "Status: running", Type: "display"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func TestDashboardAclsRootNavAndGetDashboard(t *testing.T) {
|
||||
cfg := buildDashboardAclTestConfig()
|
||||
ex := executor.DefaultExecutor(cfg)
|
||||
api := newServer(ex)
|
||||
|
||||
guest := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
|
||||
guest.BuildUserAcls(cfg)
|
||||
admin := &authpublic.AuthenticatedUser{Username: "admin"}
|
||||
admin.BuildUserAcls(cfg)
|
||||
|
||||
guestRoots := api.buildRootDashboards(guest, cfg.Dashboards)
|
||||
assert.Contains(t, guestRoots, "Public tools")
|
||||
assert.NotContains(t, guestRoots, "Services")
|
||||
|
||||
adminRoots := api.buildRootDashboards(admin, cfg.Dashboards)
|
||||
assert.Contains(t, adminRoots, "Public tools")
|
||||
assert.Contains(t, adminRoots, "Services")
|
||||
|
||||
guestRR := api.createDashboardRenderRequest(guest, "", "")
|
||||
assert.Nil(t, renderDashboard(guestRR, "Services"),
|
||||
"GetDashboard must not leak ACL-restricted dashboard content via deep link")
|
||||
|
||||
adminRR := api.createDashboardRenderRequest(admin, "", "")
|
||||
db := renderDashboard(adminRR, "Services")
|
||||
require.NotNil(t, db)
|
||||
assert.Equal(t, "Services", db.Title)
|
||||
}
|
||||
|
||||
func TestDashboardAclsNestedDirectoryDeepLink(t *testing.T) {
|
||||
cfg := buildDashboardAclTestConfig()
|
||||
cfg.Dashboards = []*config.DashboardComponent{
|
||||
{
|
||||
Title: "Public tools",
|
||||
Contents: []*config.DashboardComponent{
|
||||
{Title: "Welcome", Type: "display"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "Services",
|
||||
Acls: []string{"admins"},
|
||||
Contents: []*config.DashboardComponent{
|
||||
{
|
||||
Title: "Infrastructure",
|
||||
Contents: []*config.DashboardComponent{
|
||||
{Title: "Status: running", Type: "display"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ex := executor.DefaultExecutor(cfg)
|
||||
api := newServer(ex)
|
||||
|
||||
guest := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
|
||||
guest.BuildUserAcls(cfg)
|
||||
admin := &authpublic.AuthenticatedUser{Username: "admin"}
|
||||
admin.BuildUserAcls(cfg)
|
||||
|
||||
guestRR := api.createDashboardRenderRequest(guest, "", "")
|
||||
assert.Nil(t, renderDashboard(guestRR, "Infrastructure"),
|
||||
"nested directory under ACL-restricted root must not leak via deep link")
|
||||
|
||||
adminRR := api.createDashboardRenderRequest(admin, "", "")
|
||||
db := renderDashboard(adminRR, "Infrastructure")
|
||||
require.NotNil(t, db)
|
||||
assert.Equal(t, "Infrastructure", db.Title)
|
||||
}
|
||||
|
|
@ -52,6 +52,10 @@ func findDashboardByTitle(rr *DashboardRenderRequest, dashboardTitle string) *co
|
|||
}
|
||||
|
||||
func renderDashboardIfValid(dashboard *config.DashboardComponent, rr *DashboardRenderRequest) *apiv1.Dashboard {
|
||||
if !acl.IsAllowedViewDashboard(rr.cfg, rr.AuthenticatedUser, dashboard) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(dashboard.Contents) == 0 {
|
||||
logEmptyDashboard(dashboard.Title, rr.AuthenticatedUser.Username)
|
||||
return nil
|
||||
|
|
@ -71,13 +75,21 @@ func renderDirectoryDashboard(rr *DashboardRenderRequest, dashboardTitle string)
|
|||
|
||||
func findDirectoryComponent(rr *DashboardRenderRequest, title string) *config.DashboardComponent {
|
||||
for _, dashboard := range rr.cfg.Dashboards {
|
||||
if component := searchDirectoryInComponent(dashboard, title); component != nil {
|
||||
if component := findDirectoryInRootIfAllowed(rr, dashboard, title); component != nil {
|
||||
return component
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findDirectoryInRootIfAllowed(rr *DashboardRenderRequest, root *config.DashboardComponent, title string) *config.DashboardComponent {
|
||||
if !acl.IsAllowedViewDashboard(rr.cfg, rr.AuthenticatedUser, root) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return searchDirectoryInComponent(root, title)
|
||||
}
|
||||
|
||||
func searchDirectoryInComponent(component *config.DashboardComponent, title string) *config.DashboardComponent {
|
||||
if isMatchingDirectory(component, title) {
|
||||
return component
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ func createKeys(t *testing.T) (*rsa.PrivateKey, string) {
|
|||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
defer func() { _ = tmpFile.Close() }()
|
||||
|
||||
t.Logf("Created File: %s", tmpFile.Name())
|
||||
|
||||
|
|
@ -65,10 +65,6 @@ func newMux() *http.ServeMux {
|
|||
return mux
|
||||
}
|
||||
|
||||
func createJWTTokenWithExpiration(t *testing.T, privateKey *rsa.PrivateKey, expire int64) string {
|
||||
return createJWTTokenWithExpirationAndAudience(t, privateKey, expire, "")
|
||||
}
|
||||
|
||||
func createJWTTokenWithExpirationAndAudience(t *testing.T, privateKey *rsa.PrivateKey, expire int64, audience string) string {
|
||||
token := jwt.New(jwt.SigningMethodRS256)
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
|
|
@ -108,7 +104,7 @@ func setupJWTTestHandler(t *testing.T, cfg *config.Config) http.Handler {
|
|||
}
|
||||
|
||||
func verifyJWTResponse(t *testing.T, res *http.Response, expectCode int) {
|
||||
defer res.Body.Close()
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
assert.Equal(t, expectCode, res.StatusCode)
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
t.Logf("Response body: %s", string(body))
|
||||
|
|
@ -120,7 +116,7 @@ func testJwkValidation(t *testing.T, expire int64, expectCode int) {
|
|||
|
||||
func testJwkValidationWithAudience(t *testing.T, expire int64, expectCode int, configAudience, tokenAudience string) {
|
||||
privateKey, publicKeyPath := createKeys(t)
|
||||
defer os.Remove(publicKeyPath)
|
||||
defer func() { _ = os.Remove(publicKeyPath) }()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.AuthJwtPubKeyPath = publicKeyPath
|
||||
|
|
@ -186,7 +182,7 @@ func makeJWTRequest(t *testing.T, srv *httptest.Server, tokenStr string) *http.R
|
|||
|
||||
func TestJWTHeader(t *testing.T) {
|
||||
privateKey, publicKeyPath := createKeys(t)
|
||||
defer os.Remove(publicKeyPath)
|
||||
defer func() { _ = os.Remove(publicKeyPath) }()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.AuthJwtPubKeyPath = publicKeyPath
|
||||
|
|
@ -217,7 +213,7 @@ func TestJWTHeader(t *testing.T) {
|
|||
defer srv.Close()
|
||||
|
||||
res := makeJWTRequest(t, srv, tokenStr)
|
||||
defer res.Body.Close()
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
assert.Equal(t, 200, res.StatusCode)
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
|
|
|
|||
|
|
@ -349,13 +349,13 @@ func getUserInfo(cfg *config.Config, client *http.Client, provider *config.OAuth
|
|||
return ret
|
||||
}
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
log.Errorf("Failed to get user data: %v", res.StatusCode)
|
||||
return ret
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
contents, err := io.ReadAll(res.Body)
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -369,7 +369,7 @@ func getUserInfo(cfg *config.Config, client *http.Client, provider *config.OAuth
|
|||
log.Debugf("OAuth2 User Data: %v+", string(contents))
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(contents), &userData)
|
||||
err = json.Unmarshal(contents, &userData)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Failed to unmarshal user data: %v", err)
|
||||
|
|
|
|||
|
|
@ -289,6 +289,7 @@ type DashboardComponent struct {
|
|||
Entity string `koanf:"entity"`
|
||||
Icon string `koanf:"icon"`
|
||||
CssClass string `koanf:"cssClass"`
|
||||
Acls []string `koanf:"acls"`
|
||||
InlineAction *Action `koanf:"inlineAction"`
|
||||
Contents []*DashboardComponent `koanf:"contents"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/knadh/koanf/providers/file"
|
||||
"github.com/knadh/koanf/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUserLoadingFromConfig(t *testing.T) {
|
||||
|
|
@ -30,13 +31,13 @@ actions:
|
|||
|
||||
// Create temporary file
|
||||
tmpFile, err := os.CreateTemp("", "test_config_*.yaml")
|
||||
assert.NoError(t, err, "Should create temporary file")
|
||||
defer os.Remove(tmpFile.Name())
|
||||
require.NoError(t, err, "Should create temporary file")
|
||||
defer func() { _ = os.Remove(tmpFile.Name()) }()
|
||||
|
||||
// Write test config to file
|
||||
_, err = tmpFile.WriteString(testConfig)
|
||||
assert.NoError(t, err, "Should write test config to file")
|
||||
tmpFile.Close()
|
||||
require.NoError(t, err, "Should write test config to file")
|
||||
require.NoError(t, tmpFile.Close())
|
||||
|
||||
// Load config using koanf
|
||||
k := koanf.New(".")
|
||||
|
|
@ -81,12 +82,12 @@ actions:
|
|||
`
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "test_config_empty_*.yaml")
|
||||
assert.NoError(t, err, "Should create temporary file")
|
||||
defer os.Remove(tmpFile.Name())
|
||||
require.NoError(t, err, "Should create temporary file")
|
||||
defer func() { _ = os.Remove(tmpFile.Name()) }()
|
||||
|
||||
_, err = tmpFile.WriteString(testConfig)
|
||||
assert.NoError(t, err, "Should write test config to file")
|
||||
tmpFile.Close()
|
||||
require.NoError(t, err, "Should write test config to file")
|
||||
require.NoError(t, tmpFile.Close())
|
||||
|
||||
k := koanf.New(".")
|
||||
err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser())
|
||||
|
|
@ -116,12 +117,12 @@ actions:
|
|||
`
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "test_config_disabled_*.yaml")
|
||||
assert.NoError(t, err, "Should create temporary file")
|
||||
defer os.Remove(tmpFile.Name())
|
||||
require.NoError(t, err, "Should create temporary file")
|
||||
defer func() { _ = os.Remove(tmpFile.Name()) }()
|
||||
|
||||
_, err = tmpFile.WriteString(testConfig)
|
||||
assert.NoError(t, err, "Should write test config to file")
|
||||
tmpFile.Close()
|
||||
require.NoError(t, err, "Should write test config to file")
|
||||
require.NoError(t, tmpFile.Close())
|
||||
|
||||
k := koanf.New(".")
|
||||
err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser())
|
||||
|
|
@ -147,12 +148,12 @@ actions:
|
|||
`
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "test_config_no_auth_*.yaml")
|
||||
assert.NoError(t, err, "Should create temporary file")
|
||||
defer os.Remove(tmpFile.Name())
|
||||
require.NoError(t, err, "Should create temporary file")
|
||||
defer func() { _ = os.Remove(tmpFile.Name()) }()
|
||||
|
||||
_, err = tmpFile.WriteString(testConfig)
|
||||
assert.NoError(t, err, "Should write test config to file")
|
||||
tmpFile.Close()
|
||||
require.NoError(t, err, "Should write test config to file")
|
||||
require.NoError(t, tmpFile.Close())
|
||||
|
||||
k := koanf.New(".")
|
||||
err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser())
|
||||
|
|
|
|||
|
|
@ -982,7 +982,7 @@ func validatedSystemArgs(req *ExecutionRequest) (map[string]string, error) {
|
|||
}
|
||||
|
||||
func hasBindingAndAction(req *ExecutionRequest) bool {
|
||||
return !(req.Binding == nil || req.Binding.Action == nil)
|
||||
return req.Binding != nil && req.Binding.Action != nil
|
||||
}
|
||||
|
||||
func hasExec(req *ExecutionRequest) bool {
|
||||
|
|
|
|||
|
|
@ -186,16 +186,6 @@ func (e *Executor) groupsHaveCapacityForQueued(req *ExecutionRequest) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func firstFullGroupName(e *Executor, req *ExecutionRequest) string {
|
||||
for _, limit := range actionGroupLimits(req) {
|
||||
if e.countActiveInGroup(limit.name) >= (limit.maxConcurrent + 1) {
|
||||
return limit.name
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstFullGroupNameLocked(e *Executor, req *ExecutionRequest) string {
|
||||
for _, limit := range actionGroupLimits(req) {
|
||||
if e.countActiveInGroupLocked(limit.name) >= (limit.maxConcurrent + 1) {
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ func (tc *timeoutContext) setProcess(process *os.Process) {
|
|||
tc.processMu.Unlock()
|
||||
|
||||
// If deadline already expired before process was set, kill now
|
||||
if tc.Context.Err() == context.DeadlineExceeded && process != nil {
|
||||
if tc.Err() == context.DeadlineExceeded && process != nil {
|
||||
logEntry := &InternalLogEntry{Process: process}
|
||||
if err := tc.executor.Kill(logEntry); err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
|
|
|
|||
|
|
@ -74,7 +74,11 @@ func watchPath(ctx *watchContext) {
|
|||
return
|
||||
}
|
||||
|
||||
defer watcher.Close()
|
||||
defer func() {
|
||||
if err := watcher.Close(); err != nil {
|
||||
log.Errorf("Failed to close file watcher: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
done := make(chan bool)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ func WriteFile(filename string, out []byte) {
|
|||
|
||||
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||
handle, err := os.Create(filename)
|
||||
handle.Close()
|
||||
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
|
|
@ -25,6 +23,12 @@ func WriteFile(filename string, out []byte) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := handle.Close(); err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"filename": filename,
|
||||
}).Errorf("Failed to close %v", filename)
|
||||
}
|
||||
}
|
||||
|
||||
err := os.WriteFile(filename, out, 0600)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import (
|
|||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type RuntimeInfo struct {
|
||||
|
|
@ -85,6 +87,14 @@ func getOsReleasePrettyName() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if closeErr := handle.Close(); closeErr != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": closeErr,
|
||||
}).Warn("Failed to close /etc/os-release")
|
||||
}
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(handle)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
|
||||
|
|
@ -96,7 +106,5 @@ func getOsReleasePrettyName() string {
|
|||
}
|
||||
}
|
||||
|
||||
handle.Close()
|
||||
|
||||
return "notfound"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package servicehost
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
|
|
@ -20,25 +19,3 @@ func resolveLogDirectory(dir string, baseDir string) string {
|
|||
|
||||
return filepath.Join(baseDir, dir)
|
||||
}
|
||||
|
||||
func executableDirectory() (string, error) {
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Dir(ex), nil
|
||||
}
|
||||
|
||||
func configuredServiceLogDirectory(dir string) (string, error) {
|
||||
if dir == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
exeDir, err := executableDirectory()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return resolveLogDirectory(dir, exeDir), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
//go:build windows
|
||||
|
||||
package servicehost
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func executableDirectory() (string, error) {
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Dir(ex), nil
|
||||
}
|
||||
|
||||
func configuredServiceLogDirectory(dir string) (string, error) {
|
||||
if dir == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
exeDir, err := executableDirectory()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return resolveLogDirectory(dir, exeDir), nil
|
||||
}
|
||||
|
|
@ -202,10 +202,13 @@ func appendFile(path, content string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.WriteString(content)
|
||||
return err
|
||||
_, writeErr := file.WriteString(content)
|
||||
closeErr := file.Close()
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
return closeErr
|
||||
}
|
||||
|
||||
func newTestRunState() *testRunState {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ package tools
|
|||
|
||||
import (
|
||||
_ "github.com/bufbuild/buf/cmd/buf"
|
||||
_ "github.com/fzipp/gocyclo/cmd/gocyclo"
|
||||
_ "github.com/go-critic/go-critic/cmd/gocritic"
|
||||
_ "github.com/golangci/golangci-lint/v2/cmd/golangci-lint"
|
||||
_ "google.golang.org/protobuf/cmd/protoc-gen-go"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -72,7 +72,8 @@ update_checksum() {
|
|||
update_checksum "${ZIP_NAME}"
|
||||
update_checksum "${MSI_NAME}"
|
||||
|
||||
# Replace binaries first so a failed checksums upload leaves the draft recoverable.
|
||||
# Upload binaries first so a failed checksums upload can restore the previous file.
|
||||
# --clobber overwrites same-named assets (needed if the sign job is re-run).
|
||||
gh release upload "${TAG}" \
|
||||
"${DIST_DIR}/${ZIP_NAME}" \
|
||||
"${DIST_DIR}/${MSI_NAME}" \
|
||||
|
|
@ -87,6 +88,4 @@ if ! gh release upload "${TAG}" "${checksums_path}" --clobber; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
gh release edit "${TAG}" --draft=false
|
||||
|
||||
echo "Published signed ${ZIP_NAME} and ${MSI_NAME} on release ${TAG}"
|
||||
echo "Uploaded signed ${ZIP_NAME} and ${MSI_NAME} to release ${TAG}"
|
||||
|
|
|
|||
Loading…
Reference in New Issue