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 followed the "1 logical change" rule.
|
||||||
- [ ] I have forked the project, and raised this PR on a feature branch.
|
- [ ] 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 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 compile` runs without any issues.
|
||||||
- [ ] `make -wC service codestyle` runs without any issues.
|
- [ ] `make -wC service codestyle` runs without any issues.
|
||||||
- [ ] `make -wC service unittests` 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'
|
if: steps.release.outputs.new_release_published == 'true'
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
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
|
path: dist/OliveTin-windows-amd64.zip
|
||||||
|
archive: false
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload unsigned Windows MSI for SignPath
|
- name: Upload unsigned Windows MSI for SignPath
|
||||||
|
|
@ -165,8 +167,8 @@ jobs:
|
||||||
if: steps.release.outputs.new_release_published == 'true'
|
if: steps.release.outputs.new_release_published == 'true'
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: unsigned-windows-msi
|
|
||||||
path: dist/OliveTin-windows-amd64.msi
|
path: dist/OliveTin-windows-amd64.msi
|
||||||
|
archive: false
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Archive binaries
|
- name: Archive binaries
|
||||||
|
|
@ -205,7 +207,7 @@ jobs:
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if [[ "${missing}" -ne 0 ]]; then
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -219,6 +221,8 @@ jobs:
|
||||||
artifact-configuration-slug: windows-zip
|
artifact-configuration-slug: windows-zip
|
||||||
github-artifact-id: ${{ needs.build.outputs.windows_zip_artifact_id }}
|
github-artifact-id: ${{ needs.build.outputs.windows_zip_artifact_id }}
|
||||||
wait-for-completion: true
|
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
|
output-artifact-directory: signed-windows-zip
|
||||||
|
|
||||||
- name: Sign Windows MSI
|
- name: Sign Windows MSI
|
||||||
|
|
@ -231,9 +235,10 @@ jobs:
|
||||||
artifact-configuration-slug: windows-msi
|
artifact-configuration-slug: windows-msi
|
||||||
github-artifact-id: ${{ needs.build.outputs.windows_msi_artifact_id }}
|
github-artifact-id: ${{ needs.build.outputs.windows_msi_artifact_id }}
|
||||||
wait-for-completion: true
|
wait-for-completion: true
|
||||||
|
skip-decompress: true
|
||||||
output-artifact-directory: signed-windows-msi
|
output-artifact-directory: signed-windows-msi
|
||||||
|
|
||||||
- name: Publish signed Windows assets and undraft release
|
- name: Upload signed Windows assets
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
|
GH_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
|
||||||
GITHUB_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ before:
|
||||||
- make windows-resources VERSION={{ .Version }}
|
- make windows-resources VERSION={{ .Version }}
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
|
# id defaults to project_name (OliveTin) — required for notarize.macos defaults.
|
||||||
- skip: '{{ isEnvSet "GORELEASER_SKIP_BUILD" }}'
|
- skip: '{{ isEnvSet "GORELEASER_SKIP_BUILD" }}'
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
|
|
@ -14,7 +15,6 @@ builds:
|
||||||
dir: service
|
dir: service
|
||||||
goos:
|
goos:
|
||||||
- linux
|
- linux
|
||||||
- windows
|
|
||||||
- darwin
|
- darwin
|
||||||
- freebsd
|
- freebsd
|
||||||
|
|
||||||
|
|
@ -33,19 +33,31 @@ builds:
|
||||||
- goos: darwin
|
- goos: darwin
|
||||||
goarch: arm # Mac does not work on [32bit] arm
|
goarch: arm # Mac does not work on [32bit] arm
|
||||||
|
|
||||||
- goos: windows
|
ldflags:
|
||||||
goarch: arm
|
- -s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{ .CommitDate }}
|
||||||
|
|
||||||
- goos: windows # Does anyone use Windows on arm64?
|
|
||||||
goarch: arm64
|
|
||||||
|
|
||||||
|
# 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:
|
ldflags:
|
||||||
- -s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{ .CommitDate }}
|
- -s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{ .CommitDate }}
|
||||||
|
|
||||||
checksum:
|
checksum:
|
||||||
name_template: 'checksums.txt'
|
name_template: 'checksums.txt'
|
||||||
extra_files:
|
# Exclude the windows archive; signed zip/MSI checksums are added after SignPath.
|
||||||
- glob: ./dist/OliveTin-windows-amd64.msi
|
ids:
|
||||||
|
- default
|
||||||
|
- openrc
|
||||||
|
|
||||||
snapshot:
|
snapshot:
|
||||||
version_template: "{{ .Branch }}-{{ .ShortCommit }}"
|
version_template: "{{ .Branch }}-{{ .ShortCommit }}"
|
||||||
changelog:
|
changelog:
|
||||||
|
|
@ -73,7 +85,26 @@ changelog:
|
||||||
- '^Merge branch'
|
- '^Merge branch'
|
||||||
|
|
||||||
archives:
|
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:
|
files:
|
||||||
- config.yaml
|
- config.yaml
|
||||||
- LICENSE
|
- LICENSE
|
||||||
|
|
@ -85,9 +116,6 @@ archives:
|
||||||
- var
|
- var
|
||||||
name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{ .Arm }}"
|
name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{ .Arm }}"
|
||||||
wrap_in_directory: true
|
wrap_in_directory: true
|
||||||
format_overrides:
|
|
||||||
- goos: windows
|
|
||||||
formats: zip
|
|
||||||
|
|
||||||
# dockers_v2: single multi-platform build with buildx; keeps default provenance + SBOM.
|
# 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).
|
# Replaces legacy dockers + docker_manifests (avoids "is a manifest list" when attestations are on).
|
||||||
|
|
@ -187,8 +215,7 @@ nfpms:
|
||||||
notarize:
|
notarize:
|
||||||
macos:
|
macos:
|
||||||
- enabled: '{{ isEnvSet "MACOS_SIGN_P12" }}'
|
- enabled: '{{ isEnvSet "MACOS_SIGN_P12" }}'
|
||||||
ids:
|
# Defaults to project_name when omitted; must match builds[].id (also project_name).
|
||||||
- default
|
|
||||||
sign:
|
sign:
|
||||||
certificate: "{{.Env.MACOS_SIGN_P12}}"
|
certificate: "{{.Env.MACOS_SIGN_P12}}"
|
||||||
password: "{{.Env.MACOS_SIGN_PASSWORD}}"
|
password: "{{.Env.MACOS_SIGN_PASSWORD}}"
|
||||||
|
|
@ -200,10 +227,11 @@ notarize:
|
||||||
timeout: 30m
|
timeout: 30m
|
||||||
|
|
||||||
release:
|
release:
|
||||||
# Stay draft until the sign-windows job replaces unsigned Windows assets and undrafts.
|
# Publish immediately without Windows assets; sign-windows uploads signed zip/MSI later.
|
||||||
draft: true
|
draft: false
|
||||||
extra_files:
|
ids:
|
||||||
- glob: ./dist/OliveTin-windows-amd64.msi
|
- default
|
||||||
|
- openrc
|
||||||
footer: |
|
footer: |
|
||||||
## Container images (from GitHub)
|
## Container images (from GitHub)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,32 +38,33 @@ repos:
|
||||||
entry: make service-codestyle
|
entry: make service-codestyle
|
||||||
language: system
|
language: system
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
always_run: true
|
files: ^(service/|proto/|lang/|Makefile)
|
||||||
|
|
||||||
- id: frontend-codestyle
|
- id: frontend-codestyle
|
||||||
name: frontend-codestyle
|
name: frontend-codestyle
|
||||||
entry: make frontend-codestyle
|
entry: make frontend-codestyle
|
||||||
language: system
|
language: system
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
always_run: true
|
files: ^(frontend/|Makefile)
|
||||||
|
|
||||||
- id: service-unittests
|
- id: service-unittests
|
||||||
name: service-unittests
|
name: service-unittests
|
||||||
entry: make service-unittests
|
entry: make service-unittests
|
||||||
language: system
|
language: system
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
always_run: true
|
files: ^(service/|proto/|lang/|Makefile)
|
||||||
|
|
||||||
- id: service-build
|
- id: service-build
|
||||||
name: service-build
|
name: service-build
|
||||||
entry: make service
|
entry: make service
|
||||||
language: system
|
language: system
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
always_run: true
|
files: ^(service/|proto/|lang/|Makefile)
|
||||||
|
|
||||||
- id: it
|
- id: it
|
||||||
name: integration-tests
|
name: integration-tests
|
||||||
entry: make it
|
entry: make it
|
||||||
language: system
|
language: system
|
||||||
pass_filenames: false
|
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
|
git clone https://github.com/OliveTin/OliveTin.git
|
||||||
cd OliveTin
|
cd OliveTin
|
||||||
pre-commit install
|
pre-commit install
|
||||||
|
pre-commit install --hook-type pre-push
|
||||||
|
|
||||||
# Step3: compile binary for current dev env (OS, ARCH)
|
# Step3: compile binary for current dev env (OS, ARCH)
|
||||||
# `make proto` will also run `make go-tools`, which installs "buf". This binary
|
# `make proto` will also run `make go-tools`, which installs "buf". This binary
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ defaultPermissions:
|
||||||
logs: true
|
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.
|
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
|
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.
|
== 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.
|
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/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/oauth2.adoc[Configure OAuth2] - Set up OAuth2 for user groups
|
||||||
* xref:security/design_choices.adoc[Security design recommendations] - Learn best practices for ACL design
|
* 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:
|
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).
|
* **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
|
## 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.
|
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:
|
Signed artifacts:
|
||||||
|
|
||||||
* `OliveTin.exe` inside `OliveTin-windows-amd64.zip`
|
* `OliveTin.exe` inside `OliveTin-windows-amd64.zip`
|
||||||
* nested `OliveTin.exe` and the `OliveTin-windows-amd64.msi` installer (deep signing)
|
* 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
|
### Prerequisites
|
||||||
|
|
||||||
|
|
@ -168,9 +168,9 @@ In **Settings → Secrets and variables → Actions**:
|
||||||
|
|
||||||
On a new semantic-release from `main`:
|
On a new semantic-release from `main`:
|
||||||
|
|
||||||
1. GoReleaser publishes container images and creates a **draft** GitHub release (including unsigned Windows zip/MSI).
|
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 those Windows files as GitHub Actions artifacts.
|
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 clobber-upload signed files, refresh `checksums.txt`, and undraft the release.
|
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).
|
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
|
### 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`]
|
- 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`]
|
- 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`]
|
- 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
|
"es2021": true
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
|
"plugin:vue/recommended",
|
||||||
"standard"
|
"standard"
|
||||||
],
|
],
|
||||||
|
"parser": "vue-eslint-parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 12,
|
"ecmaVersion": 12,
|
||||||
"sourceType": "module"
|
"sourceType": "module",
|
||||||
|
"parser": {
|
||||||
|
"js": "espree"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"rules": {
|
"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:
|
codestyle:
|
||||||
npm install
|
npm install
|
||||||
npx eslint --fix main.js js/*
|
npx eslint --fix main.js js/* resources/vue
|
||||||
npx stylelint style.css
|
npx stylelint style.css
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
|
|
@ -18,4 +18,4 @@ build:
|
||||||
|
|
||||||
dist: deps clean build
|
dist: deps clean build
|
||||||
|
|
||||||
.PHONY: codestyle
|
.PHONY: codestyle
|
||||||
|
|
|
||||||
|
|
@ -11,38 +11,39 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@connectrpc/connect": "^2.1.2",
|
"@connectrpc/connect": "^2.1.2",
|
||||||
"@connectrpc/connect-web": "^2.1.2",
|
"@connectrpc/connect-web": "^2.1.2",
|
||||||
"@hugeicons/core-free-icons": "^4.2.1",
|
"@hugeicons/core-free-icons": "^4.2.2",
|
||||||
"@hugeicons/vue": "^1.0.6",
|
"@hugeicons/vue": "^1.0.7",
|
||||||
"@vitejs/plugin-vue": "^6.0.7",
|
"@vitejs/plugin-vue": "^6.0.7",
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/addon-web-links": "^0.12.0",
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"iconify-icon": "^3.0.2",
|
"iconify-icon": "^3.0.2",
|
||||||
"picocrank": "^1.17.0",
|
"picocrank": "^1.18.2",
|
||||||
"standard": "^17.1.2",
|
"standard": "^17.1.2",
|
||||||
"unplugin-vue-components": "^32.1.0",
|
"unplugin-vue-components": "^32.1.0",
|
||||||
"vite": "^8.0.16",
|
"vite": "^8.1.3",
|
||||||
"vue": "^3.5.38",
|
"vue": "^3.5.39",
|
||||||
"vue-i18n": "^11.4.6",
|
"vue-i18n": "^11.4.6",
|
||||||
"vue-router": "^5.1.0"
|
"vue-router": "^5.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"eslint-plugin-vue": "^10.9.2",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"stylelint": "^17.13.0",
|
"stylelint": "^17.14.0",
|
||||||
"stylelint-config-standard": "^40.0.0"
|
"stylelint-config-standard": "^40.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.0.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||||
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.28.5",
|
"@babel/helper-validator-identifier": "^7.29.7",
|
||||||
"js-tokens": "^4.0.0",
|
"js-tokens": "^4.0.0",
|
||||||
"picocolors": "^1.1.1"
|
"picocolors": "^1.1.1"
|
||||||
},
|
},
|
||||||
|
|
@ -409,20 +410,20 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.10.0",
|
"version": "1.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
|
||||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/wasi-threads": "1.2.1",
|
"@emnapi/wasi-threads": "1.2.2",
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.10.0",
|
"version": "1.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
||||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -430,9 +431,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -941,15 +942,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@hugeicons/core-free-icons": {
|
"node_modules/@hugeicons/core-free-icons": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.2.2.tgz",
|
||||||
"integrity": "sha512-75jYZKYyA9VwS35YRmmGUGzFedbY+Fl0Vxx5FzXR2CGDlIhNRumFeVqaaKoClf2MeYEJwPAVMEL9RwCYtOgnSw==",
|
"integrity": "sha512-fjQW3p6cwoJmwK3oCnV8Z3PC5sHihDvr2jkAHax8cxqp62GaV/D7B/Rnao+JwicW8fmLZUQ6QrzWfoyMFOEQlQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@hugeicons/vue": {
|
"node_modules/@hugeicons/vue": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@hugeicons/vue/-/vue-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@hugeicons/vue/-/vue-1.0.7.tgz",
|
||||||
"integrity": "sha512-T1Wbuk2qDdoE2xcY3CDtwMOKewwIYDE7JoMVLeuAV4cbOx5CFtToHCQAmLgVPIFGGZUsV2nN52lBkts9upvePw==",
|
"integrity": "sha512-oUE0ztUNYbsMFpMgbV3bsxQBObNkeouomUThFuFPzYC5eV++VTYonStsltJu2MoLMkxV8mzpoVvIbuMPBZSN0w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": "^2.6.0 || ^3.0.0"
|
"vue": "^2.6.0 || ^3.0.0"
|
||||||
|
|
@ -1110,13 +1111,13 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
|
||||||
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
|
"integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tybys/wasm-util": "^0.10.2"
|
"@tybys/wasm-util": "^0.10.3"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|
@ -1160,18 +1161,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.133.0",
|
"version": "0.139.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
|
||||||
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
"integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/Boshen"
|
"url": "https://github.com/sponsors/Boshen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.0.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
|
||||||
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
|
"integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1185,9 +1186,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.0.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
|
||||||
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
|
"integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1201,9 +1202,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.0.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
|
||||||
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
|
"integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1217,9 +1218,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.0.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
|
||||||
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
|
"integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1233,9 +1234,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.0.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
|
||||||
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
|
"integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
|
@ -1249,9 +1250,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.0.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
|
||||||
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
|
"integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1265,9 +1266,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.0.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
|
||||||
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
|
"integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1281,9 +1282,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.0.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
|
||||||
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
|
"integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
|
@ -1297,9 +1298,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.0.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
|
||||||
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
|
"integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
|
|
@ -1313,9 +1314,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.0.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
|
||||||
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
|
"integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1329,9 +1330,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.0.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
|
||||||
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
|
"integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1345,9 +1346,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.0.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
|
||||||
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
|
"integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1361,27 +1362,27 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
"version": "1.0.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
|
||||||
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
|
"integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/core": "1.10.0",
|
"@emnapi/core": "1.11.1",
|
||||||
"@emnapi/runtime": "1.10.0",
|
"@emnapi/runtime": "1.11.1",
|
||||||
"@napi-rs/wasm-runtime": "^1.1.4"
|
"@napi-rs/wasm-runtime": "^1.1.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.0.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
|
||||||
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
|
"integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1395,9 +1396,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.0.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
|
||||||
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
|
"integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1436,15 +1437,31 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.2",
|
"version": "0.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
|
||||||
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
"integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.4.0"
|
"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": {
|
"node_modules/@types/jsesc": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz",
|
||||||
|
|
@ -1507,39 +1524,39 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-core": {
|
"node_modules/@vue/compiler-core": {
|
||||||
"version": "3.5.38",
|
"version": "3.5.39",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.38.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz",
|
||||||
"integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==",
|
"integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.29.7",
|
"@babel/parser": "^7.29.7",
|
||||||
"@vue/shared": "3.5.38",
|
"@vue/shared": "3.5.39",
|
||||||
"entities": "^7.0.1",
|
"entities": "^7.0.1",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-dom": {
|
"node_modules/@vue/compiler-dom": {
|
||||||
"version": "3.5.38",
|
"version": "3.5.39",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz",
|
||||||
"integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==",
|
"integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-core": "3.5.38",
|
"@vue/compiler-core": "3.5.39",
|
||||||
"@vue/shared": "3.5.38"
|
"@vue/shared": "3.5.39"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-sfc": {
|
"node_modules/@vue/compiler-sfc": {
|
||||||
"version": "3.5.38",
|
"version": "3.5.39",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz",
|
||||||
"integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==",
|
"integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.29.7",
|
"@babel/parser": "^7.29.7",
|
||||||
"@vue/compiler-core": "3.5.38",
|
"@vue/compiler-core": "3.5.39",
|
||||||
"@vue/compiler-dom": "3.5.38",
|
"@vue/compiler-dom": "3.5.39",
|
||||||
"@vue/compiler-ssr": "3.5.38",
|
"@vue/compiler-ssr": "3.5.39",
|
||||||
"@vue/shared": "3.5.38",
|
"@vue/shared": "3.5.39",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
"postcss": "^8.5.15",
|
"postcss": "^8.5.15",
|
||||||
|
|
@ -1547,13 +1564,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-ssr": {
|
"node_modules/@vue/compiler-ssr": {
|
||||||
"version": "3.5.38",
|
"version": "3.5.39",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz",
|
||||||
"integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==",
|
"integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.38",
|
"@vue/compiler-dom": "3.5.39",
|
||||||
"@vue/shared": "3.5.38"
|
"@vue/shared": "3.5.39"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/devtools-api": {
|
"node_modules/@vue/devtools-api": {
|
||||||
|
|
@ -1581,53 +1598,53 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vue/reactivity": {
|
"node_modules/@vue/reactivity": {
|
||||||
"version": "3.5.38",
|
"version": "3.5.39",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.38.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz",
|
||||||
"integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==",
|
"integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/shared": "3.5.38"
|
"@vue/shared": "3.5.39"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/runtime-core": {
|
"node_modules/@vue/runtime-core": {
|
||||||
"version": "3.5.38",
|
"version": "3.5.39",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.38.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz",
|
||||||
"integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==",
|
"integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/reactivity": "3.5.38",
|
"@vue/reactivity": "3.5.39",
|
||||||
"@vue/shared": "3.5.38"
|
"@vue/shared": "3.5.39"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/runtime-dom": {
|
"node_modules/@vue/runtime-dom": {
|
||||||
"version": "3.5.38",
|
"version": "3.5.39",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.38.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz",
|
||||||
"integrity": "sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==",
|
"integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/reactivity": "3.5.38",
|
"@vue/reactivity": "3.5.39",
|
||||||
"@vue/runtime-core": "3.5.38",
|
"@vue/runtime-core": "3.5.39",
|
||||||
"@vue/shared": "3.5.38",
|
"@vue/shared": "3.5.39",
|
||||||
"csstype": "^3.2.3"
|
"csstype": "^3.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/server-renderer": {
|
"node_modules/@vue/server-renderer": {
|
||||||
"version": "3.5.38",
|
"version": "3.5.39",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.38.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz",
|
||||||
"integrity": "sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==",
|
"integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-ssr": "3.5.38",
|
"@vue/compiler-ssr": "3.5.39",
|
||||||
"@vue/shared": "3.5.38"
|
"@vue/shared": "3.5.39"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": "3.5.38"
|
"vue": "3.5.39"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/shared": {
|
"node_modules/@vue/shared": {
|
||||||
"version": "3.5.38",
|
"version": "3.5.39",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz",
|
||||||
"integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==",
|
"integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@xterm/addon-fit": {
|
"node_modules/@xterm/addon-fit": {
|
||||||
|
|
@ -1949,6 +1966,13 @@
|
||||||
"url": "https://github.com/sponsors/antfu"
|
"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": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.14",
|
"version": "1.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||||
|
|
@ -2138,9 +2162,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/cosmiconfig": {
|
"node_modules/cosmiconfig": {
|
||||||
"version": "9.0.1",
|
"version": "9.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz",
|
||||||
"integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==",
|
"integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -2990,6 +3014,51 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/eslint-scope": {
|
||||||
"version": "7.2.2",
|
"version": "7.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
||||||
|
|
@ -4897,6 +4966,19 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
|
@ -5186,19 +5268,19 @@
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picocrank": {
|
"node_modules/picocrank": {
|
||||||
"version": "1.17.0",
|
"version": "1.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.18.2.tgz",
|
||||||
"integrity": "sha512-EIUSI26elt6ulloN74CT2sX5tBxS2wjdV4A+eXHYNtHbH0hM2iKkYzpKKWOEr+yUTfrvjah6VuqieedFw/9R0w==",
|
"integrity": "sha512-YR1Y00KnEPjH6OCrijSxzTHoyHLhKrsPhkA7ubOkbvcUuD7wtVKbRxfPkjAkA838dGymjoG//iFca2oLnNT5tg==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hugeicons/core-free-icons": "^4.1.1",
|
"@hugeicons/core-free-icons": "^4.2.2",
|
||||||
"@hugeicons/vue": "^1.0.5",
|
"@hugeicons/vue": "^1.0.7",
|
||||||
"@vitejs/plugin-vue": "^6.0.6",
|
"@vitejs/plugin-vue": "^6.0.7",
|
||||||
"femtocrank": "^2.5.0",
|
"femtocrank": "^2.5.0",
|
||||||
"unplugin-vue-components": "^32.0.0",
|
"unplugin-vue-components": "^32.1.0",
|
||||||
"vite": "^8.0.10",
|
"vite": "^8.1.2",
|
||||||
"vue": "^3.5.33",
|
"vue": "^3.5.39",
|
||||||
"vue-router": "^5.0.6"
|
"vue-router": "^5.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
|
|
@ -5318,9 +5400,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.15",
|
"version": "8.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|
@ -5373,9 +5455,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss-selector-parser": {
|
"node_modules/postcss-selector-parser": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz",
|
||||||
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
|
"integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -5620,12 +5702,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
|
||||||
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
"integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.133.0",
|
"@oxc-project/types": "=0.139.0",
|
||||||
"@rolldown/pluginutils": "^1.0.0"
|
"@rolldown/pluginutils": "^1.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -5635,21 +5717,21 @@
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm64": "1.0.3",
|
"@rolldown/binding-android-arm64": "1.1.5",
|
||||||
"@rolldown/binding-darwin-arm64": "1.0.3",
|
"@rolldown/binding-darwin-arm64": "1.1.5",
|
||||||
"@rolldown/binding-darwin-x64": "1.0.3",
|
"@rolldown/binding-darwin-x64": "1.1.5",
|
||||||
"@rolldown/binding-freebsd-x64": "1.0.3",
|
"@rolldown/binding-freebsd-x64": "1.1.5",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
|
"@rolldown/binding-linux-arm64-gnu": "1.1.5",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.0.3",
|
"@rolldown/binding-linux-arm64-musl": "1.1.5",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
|
"@rolldown/binding-linux-ppc64-gnu": "1.1.5",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
|
"@rolldown/binding-linux-s390x-gnu": "1.1.5",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.0.3",
|
"@rolldown/binding-linux-x64-gnu": "1.1.5",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.0.3",
|
"@rolldown/binding-linux-x64-musl": "1.1.5",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.0.3",
|
"@rolldown/binding-openharmony-arm64": "1.1.5",
|
||||||
"@rolldown/binding-wasm32-wasi": "1.0.3",
|
"@rolldown/binding-wasm32-wasi": "1.1.5",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
|
"@rolldown/binding-win32-arm64-msvc": "1.1.5",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.0.3"
|
"@rolldown/binding-win32-x64-msvc": "1.1.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
|
|
@ -6151,9 +6233,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stylelint": {
|
"node_modules/stylelint": {
|
||||||
"version": "17.13.0",
|
"version": "17.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.0.tgz",
|
||||||
"integrity": "sha512-G1WYzMerp7ihOaIe9VJCHLt12MoAD2QLf1AFerYP37+BCRBUK5UCpq8e/mN+zCIaJPKQcaxhE4WlPmqdiOx/gw==",
|
"integrity": "sha512-8xkHPpdqYryeIsOgfsYTmr6cIeC4nLYWk5S8BPxpodq8mIuepggkMljsHewWfuAjj/+qpRKou2QerhjMH3iasg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -6169,13 +6251,13 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@csstools/css-calc": "^3.2.1",
|
"@csstools/css-calc": "^3.2.1",
|
||||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
"@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/css-tokenizer": "^4.0.0",
|
||||||
"@csstools/media-query-list-parser": "^5.0.0",
|
"@csstools/media-query-list-parser": "^5.0.0",
|
||||||
"@csstools/selector-resolve-nested": "^4.0.0",
|
"@csstools/selector-resolve-nested": "^4.0.0",
|
||||||
"@csstools/selector-specificity": "^6.0.0",
|
"@csstools/selector-specificity": "^6.0.0",
|
||||||
"colord": "^2.9.3",
|
"colord": "^2.9.3",
|
||||||
"cosmiconfig": "^9.0.1",
|
"cosmiconfig": "^9.0.2",
|
||||||
"css-functions-list": "^3.3.3",
|
"css-functions-list": "^3.3.3",
|
||||||
"css-tree": "^3.2.1",
|
"css-tree": "^3.2.1",
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
|
|
@ -6195,7 +6277,7 @@
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"postcss": "^8.5.15",
|
"postcss": "^8.5.15",
|
||||||
"postcss-safe-parser": "^7.0.1",
|
"postcss-safe-parser": "^7.0.1",
|
||||||
"postcss-selector-parser": "^7.1.1",
|
"postcss-selector-parser": "^7.1.4",
|
||||||
"postcss-value-parser": "^4.2.0",
|
"postcss-value-parser": "^4.2.0",
|
||||||
"string-width": "^8.2.1",
|
"string-width": "^8.2.1",
|
||||||
"supports-hyperlinks": "^4.4.0",
|
"supports-hyperlinks": "^4.4.0",
|
||||||
|
|
@ -6790,15 +6872,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.16",
|
"version": "8.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz",
|
||||||
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
"integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
"postcss": "^8.5.15",
|
"postcss": "^8.5.16",
|
||||||
"rolldown": "1.0.3",
|
"rolldown": "~1.1.3",
|
||||||
"tinyglobby": "^0.2.17"
|
"tinyglobby": "^0.2.17"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -6815,7 +6897,7 @@
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/node": "^20.19.0 || >=22.12.0",
|
"@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",
|
"esbuild": "^0.27.0 || ^0.28.0",
|
||||||
"jiti": ">=1.21.0",
|
"jiti": ">=1.21.0",
|
||||||
"less": "^4.0.0",
|
"less": "^4.0.0",
|
||||||
|
|
@ -6879,16 +6961,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue": {
|
"node_modules/vue": {
|
||||||
"version": "3.5.38",
|
"version": "3.5.39",
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.38.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz",
|
||||||
"integrity": "sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==",
|
"integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.38",
|
"@vue/compiler-dom": "3.5.39",
|
||||||
"@vue/compiler-sfc": "3.5.38",
|
"@vue/compiler-sfc": "3.5.39",
|
||||||
"@vue/runtime-dom": "3.5.38",
|
"@vue/runtime-dom": "3.5.39",
|
||||||
"@vue/server-renderer": "3.5.38",
|
"@vue/server-renderer": "3.5.39",
|
||||||
"@vue/shared": "3.5.38"
|
"@vue/shared": "3.5.39"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "*"
|
"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": {
|
"node_modules/vue-i18n": {
|
||||||
"version": "11.4.6",
|
"version": "11.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.6.tgz",
|
||||||
|
|
@ -7145,6 +7319,16 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/yaml": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@
|
||||||
"repository": "https://github.com/OliveTin/OliveTin",
|
"repository": "https://github.com/OliveTin/OliveTin",
|
||||||
"source": "index.html",
|
"source": "index.html",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"eslint-plugin-vue": "^10.9.2",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"stylelint": "^17.13.0",
|
"stylelint": "^17.14.0",
|
||||||
"stylelint-config-standard": "^40.0.0"
|
"stylelint-config-standard": "^40.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -24,22 +25,22 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@connectrpc/connect": "^2.1.2",
|
"@connectrpc/connect": "^2.1.2",
|
||||||
"@connectrpc/connect-web": "^2.1.2",
|
"@connectrpc/connect-web": "^2.1.2",
|
||||||
"@hugeicons/core-free-icons": "^4.2.1",
|
"@hugeicons/core-free-icons": "^4.2.2",
|
||||||
"@hugeicons/vue": "^1.0.6",
|
"@hugeicons/vue": "^1.0.7",
|
||||||
"@vitejs/plugin-vue": "^6.0.7",
|
"@vitejs/plugin-vue": "^6.0.7",
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/addon-web-links": "^0.12.0",
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"iconify-icon": "^3.0.2",
|
"iconify-icon": "^3.0.2",
|
||||||
"picocrank": "^1.17.0",
|
"picocrank": "^1.18.2",
|
||||||
"standard": "^17.1.2",
|
"standard": "^17.1.2",
|
||||||
"unplugin-vue-components": "^32.1.0",
|
"unplugin-vue-components": "^32.1.0",
|
||||||
"vite": "^8.0.16",
|
"vite": "^8.1.3",
|
||||||
"vue": "^3.5.38",
|
"vue": "^3.5.39",
|
||||||
"vue-i18n": "^11.4.6",
|
"vue-i18n": "^11.4.6",
|
||||||
"vue-router": "^5.1.0"
|
"vue-router": "^5.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.0.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,73 @@
|
||||||
<template>
|
<template>
|
||||||
<div :id="`actionButton-${bindingId}`" role="none" class="action-button" @contextmenu.prevent="openActionDetails">
|
<div
|
||||||
<span
|
:id="`actionButton-${bindingId}`"
|
||||||
v-if="showExecutionIndicator"
|
role="none"
|
||||||
class="execution-indicator"
|
class="action-button"
|
||||||
:class="executionIndicatorClass"
|
@contextmenu.prevent="openActionDetails"
|
||||||
:title="executionIndicatorTitle"
|
>
|
||||||
aria-hidden="true"
|
<span
|
||||||
></span>
|
v-if="showExecutionIndicator"
|
||||||
<button :id="`actionButtonInner-${bindingId}`" :title="title" :disabled="!canExec || isDisabled"
|
class="execution-indicator"
|
||||||
:class="combinedClasses" @click="handleClick">
|
: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">
|
<ActionIconGlyph
|
||||||
<div v-if="navigateOnStart == 'pop'" class="navigate-on-start" title="Opens a popup dialog on start">
|
class="icon"
|
||||||
<HugeiconsIcon :icon="ComputerTerminal01Icon" />
|
:glyph="actionGlyph"
|
||||||
</div>
|
/>
|
||||||
<div v-if="navigateOnStart == 'arg'" class="navigate-on-start" title="Opens an argument form on start">
|
<span
|
||||||
<HugeiconsIcon :icon="TypeCursorIcon" />
|
class="title"
|
||||||
</div>
|
aria-live="polite"
|
||||||
<div v-if="navigateOnStart == 'hist'" class="navigate-on-start" title="Opens action execution history on start">
|
>{{ displayTitle }}
|
||||||
<HugeiconsIcon :icon="WorkHistoryIcon" />
|
</span>
|
||||||
</div>
|
<span
|
||||||
<div v-if="navigateOnStart == ''" class="navigate-on-start" title="Run in the background">
|
v-if="rateLimitMessage"
|
||||||
<HugeiconsIcon :icon="WorkoutRunIcon" />
|
class="rate-limit-message"
|
||||||
</div>
|
>{{ rateLimitMessage }}</span>
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
@ -59,18 +96,18 @@ const navigateOnStart = ref('')
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
actionData: {
|
actionData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
cssClass: {
|
cssClass: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
prefilledArguments: {
|
prefilledArguments: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -84,7 +121,6 @@ const displayTitle = ref('')
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const isDisabled = ref(false)
|
const isDisabled = ref(false)
|
||||||
const showArgumentForm = ref(false)
|
|
||||||
|
|
||||||
// Rate limiting
|
// Rate limiting
|
||||||
const rateLimitExpires = ref(0)
|
const rateLimitExpires = ref(0)
|
||||||
|
|
@ -99,7 +135,7 @@ const flashedTrackingIds = new Set()
|
||||||
|
|
||||||
// Show navigate on start icons - defaults to true if not set
|
// Show navigate on start icons - defaults to true if not set
|
||||||
const showNavigateOnStartIcons = computed(() => {
|
const showNavigateOnStartIcons = computed(() => {
|
||||||
return window.initResponse?.showNavigateOnStartIcons ?? true
|
return window.initResponse?.showNavigateOnStartIcons ?? true
|
||||||
})
|
})
|
||||||
|
|
||||||
const actionGlyph = computed(() => props.actionData?.icon ?? '')
|
const actionGlyph = computed(() => props.actionData?.icon ?? '')
|
||||||
|
|
@ -107,63 +143,63 @@ const glyph = ref('')
|
||||||
|
|
||||||
// Combined classes including custom cssClass
|
// Combined classes including custom cssClass
|
||||||
const combinedClasses = computed(() => {
|
const combinedClasses = computed(() => {
|
||||||
const classes = [...buttonClasses.value]
|
const classes = [...buttonClasses.value]
|
||||||
if (props.cssClass) {
|
if (props.cssClass) {
|
||||||
classes.push(props.cssClass)
|
classes.push(props.cssClass)
|
||||||
}
|
}
|
||||||
return classes
|
return classes
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasRunningInstance = computed(() => {
|
const hasRunningInstance = computed(() => {
|
||||||
const id = bindingId.value
|
const id = bindingId.value
|
||||||
return !!(id && bindingExecutionState[id]?.hasRunning)
|
return !!(id && bindingExecutionState[id]?.hasRunning)
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasQueuedInstance = computed(() => {
|
const hasQueuedInstance = computed(() => {
|
||||||
const id = bindingId.value
|
const id = bindingId.value
|
||||||
return !!(id && bindingExecutionState[id]?.hasQueued)
|
return !!(id && bindingExecutionState[id]?.hasQueued)
|
||||||
})
|
})
|
||||||
|
|
||||||
const showExecutionIndicator = computed(() => {
|
const showExecutionIndicator = computed(() => {
|
||||||
return hasRunningInstance.value || hasQueuedInstance.value
|
return hasRunningInstance.value || hasQueuedInstance.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const executionIndicatorClass = computed(() => {
|
const executionIndicatorClass = computed(() => {
|
||||||
if (hasRunningInstance.value) {
|
if (hasRunningInstance.value) {
|
||||||
return 'execution-indicator-running'
|
return 'execution-indicator-running'
|
||||||
}
|
}
|
||||||
if (hasQueuedInstance.value) {
|
if (hasQueuedInstance.value) {
|
||||||
return 'execution-indicator-queued'
|
return 'execution-indicator-queued'
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const executionIndicatorTitle = computed(() => {
|
const executionIndicatorTitle = computed(() => {
|
||||||
if (hasRunningInstance.value) {
|
if (hasRunningInstance.value) {
|
||||||
return 'Running'
|
return 'Running'
|
||||||
}
|
}
|
||||||
if (hasQueuedInstance.value) {
|
if (hasQueuedInstance.value) {
|
||||||
return 'Queued'
|
return 'Queued'
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
function consumeAndFlashPendingResult () {
|
function consumeAndFlashPendingResult () {
|
||||||
const id = bindingId.value
|
const id = bindingId.value
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const pending = consumePendingBindingFlash(id)
|
const pending = consumePendingBindingFlash(id)
|
||||||
if (pending) {
|
if (pending) {
|
||||||
onExecutionFinished(pending)
|
onExecutionFinished(pending)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timestamps
|
// Timestamps
|
||||||
const updateIterationTimestamp = ref(0)
|
const updateIterationTimestamp = ref(0)
|
||||||
|
|
||||||
function constructFromJson(json) {
|
function constructFromJson (json) {
|
||||||
updateIterationTimestamp.value = 0
|
updateIterationTimestamp.value = 0
|
||||||
|
|
||||||
updateFromJson(json)
|
updateFromJson(json)
|
||||||
|
|
@ -174,11 +210,11 @@ function constructFromJson(json) {
|
||||||
popupOnStart.value = json.popupOnStart
|
popupOnStart.value = json.popupOnStart
|
||||||
|
|
||||||
if (popupOnStart.value.includes('execution-dialog')) {
|
if (popupOnStart.value.includes('execution-dialog')) {
|
||||||
navigateOnStart.value = 'pop'
|
navigateOnStart.value = 'pop'
|
||||||
} else if (popupOnStart.value === 'history') {
|
} else if (popupOnStart.value === 'history') {
|
||||||
navigateOnStart.value = 'hist'
|
navigateOnStart.value = 'hist'
|
||||||
} else if (needsArgumentForm(props.actionData)) {
|
} else if (needsArgumentForm(props.actionData)) {
|
||||||
navigateOnStart.value = 'arg'
|
navigateOnStart.value = 'arg'
|
||||||
}
|
}
|
||||||
|
|
||||||
isDisabled.value = !json.canExec
|
isDisabled.value = !json.canExec
|
||||||
|
|
@ -186,111 +222,117 @@ function constructFromJson(json) {
|
||||||
glyph.value = json.icon ?? ''
|
glyph.value = json.icon ?? ''
|
||||||
// Initialize rate limit from action data (parse datetime string)
|
// Initialize rate limit from action data (parse datetime string)
|
||||||
if (json.datetimeRateLimitExpires) {
|
if (json.datetimeRateLimitExpires) {
|
||||||
const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
|
const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
|
||||||
rateLimitExpires.value = date.getTime() / 1000
|
rateLimitExpires.value = date.getTime() / 1000
|
||||||
} else {
|
} else {
|
||||||
rateLimitExpires.value = 0
|
rateLimitExpires.value = 0
|
||||||
}
|
}
|
||||||
// Also initialize the store so the watch picks it up
|
// Also initialize the store so the watch picks it up
|
||||||
if (bindingId.value) {
|
if (bindingId.value) {
|
||||||
rateLimits[bindingId.value] = rateLimitExpires.value
|
rateLimits[bindingId.value] = rateLimitExpires.value
|
||||||
setBindingExecutionState(
|
setBindingExecutionState(
|
||||||
bindingId.value,
|
bindingId.value,
|
||||||
!!json.hasRunningInstance,
|
!!json.hasRunningInstance,
|
||||||
!!json.hasQueuedInstance
|
!!json.hasQueuedInstance
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
updateRateLimitStatus()
|
updateRateLimitStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFromJson(json) {
|
function updateFromJson (json) {
|
||||||
// Fields that should not be updated
|
// Fields that should not be updated
|
||||||
// title - as the callback URL relies on it
|
// title - as the callback URL relies on it
|
||||||
|
|
||||||
// Update rate limiting if changed (parse datetime string)
|
// Update rate limiting if changed (parse datetime string)
|
||||||
if (json.datetimeRateLimitExpires) {
|
if (json.datetimeRateLimitExpires) {
|
||||||
const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
|
const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
|
||||||
rateLimitExpires.value = date.getTime() / 1000
|
rateLimitExpires.value = date.getTime() / 1000
|
||||||
updateRateLimitStatus()
|
if (bindingId.value) {
|
||||||
|
rateLimits[bindingId.value] = rateLimitExpires.value
|
||||||
|
}
|
||||||
|
updateRateLimitStatus()
|
||||||
} else if (json.datetimeRateLimitExpires === '') {
|
} else if (json.datetimeRateLimitExpires === '') {
|
||||||
// Explicitly clear if empty string
|
// Explicitly clear if empty string
|
||||||
rateLimitExpires.value = 0
|
rateLimitExpires.value = 0
|
||||||
updateRateLimitStatus()
|
if (bindingId.value) {
|
||||||
|
rateLimits[bindingId.value] = 0
|
||||||
|
}
|
||||||
|
updateRateLimitStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRateLimitStatus() {
|
function updateRateLimitStatus () {
|
||||||
if (rateLimitExpires.value === 0) {
|
if (rateLimitExpires.value === 0) {
|
||||||
isRateLimited.value = false
|
isRateLimited.value = false
|
||||||
rateLimitMessage.value = ''
|
rateLimitMessage.value = ''
|
||||||
if (rateLimitInterval.value) {
|
if (rateLimitInterval.value) {
|
||||||
clearInterval(rateLimitInterval.value)
|
clearInterval(rateLimitInterval.value)
|
||||||
rateLimitInterval.value = null
|
rateLimitInterval.value = null
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Math.floor(Date.now() / 1000)
|
const now = Math.floor(Date.now() / 1000)
|
||||||
const expires = rateLimitExpires.value
|
const expires = rateLimitExpires.value
|
||||||
|
|
||||||
if (now >= expires) {
|
if (now >= expires) {
|
||||||
// Rate limit has expired
|
// Rate limit has expired
|
||||||
isRateLimited.value = false
|
isRateLimited.value = false
|
||||||
rateLimitMessage.value = ''
|
rateLimitMessage.value = ''
|
||||||
rateLimitExpires.value = 0
|
rateLimitExpires.value = 0
|
||||||
if (rateLimitInterval.value) {
|
if (rateLimitInterval.value) {
|
||||||
clearInterval(rateLimitInterval.value)
|
clearInterval(rateLimitInterval.value)
|
||||||
rateLimitInterval.value = null
|
rateLimitInterval.value = null
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Still rate limited
|
// Still rate limited
|
||||||
isRateLimited.value = true
|
isRateLimited.value = true
|
||||||
const secondsRemaining = expires - now
|
const secondsRemaining = expires - now
|
||||||
rateLimitMessage.value = `Rate limited, available in ${secondsRemaining} second${secondsRemaining !== 1 ? 's' : ''}`
|
rateLimitMessage.value = `Rate limited, available in ${secondsRemaining} second${secondsRemaining !== 1 ? 's' : ''}`
|
||||||
|
|
||||||
// Set up interval to update every second
|
// Set up interval to update every second
|
||||||
if (!rateLimitInterval.value) {
|
if (!rateLimitInterval.value) {
|
||||||
rateLimitInterval.value = setInterval(() => {
|
rateLimitInterval.value = setInterval(() => {
|
||||||
updateRateLimitStatus()
|
updateRateLimitStatus()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openActionDetails() {
|
function openActionDetails () {
|
||||||
const id = props.actionData?.bindingId
|
const id = props.actionData?.bindingId
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
router.push(`/action/${id}`)
|
router.push(`/action/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleClick() {
|
async function handleClick () {
|
||||||
if (popupOnStart.value === 'history') {
|
if (popupOnStart.value === 'history') {
|
||||||
openActionDetails()
|
openActionDetails()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (needsArgumentForm(props.actionData)) {
|
if (needsArgumentForm(props.actionData)) {
|
||||||
const bindingId = props.actionData.bindingId
|
const bindingId = props.actionData.bindingId
|
||||||
const prefilled = props.prefilledArguments || {}
|
const prefilled = props.prefilledArguments || {}
|
||||||
if (Object.keys(prefilled).length > 0) {
|
if (Object.keys(prefilled).length > 0) {
|
||||||
router.push({
|
router.push({
|
||||||
path: `/actionBinding/${bindingId}/argumentForm`,
|
path: `/actionBinding/${bindingId}/argumentForm`,
|
||||||
state: { prefilledArguments: prefilled }
|
state: { prefilledArguments: prefilled }
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
router.push(`/actionBinding/${bindingId}/argumentForm`)
|
router.push(`/actionBinding/${bindingId}/argumentForm`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await startAction()
|
await startAction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUniqueId() {
|
function getUniqueId () {
|
||||||
if (window.isSecureContext) {
|
if (window.isSecureContext) {
|
||||||
return window.crypto.randomUUID()
|
return window.crypto.randomUUID()
|
||||||
} else {
|
} 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
|
buttonClasses.value = [] // Removes old animation classes
|
||||||
|
|
||||||
if (actionArgs === undefined) {
|
if (actionArgs === undefined) {
|
||||||
actionArgs = []
|
actionArgs = []
|
||||||
}
|
}
|
||||||
|
|
||||||
// UUIDs are create client side, so that we can setup a "execution-button"
|
// 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.
|
// to track the execution before we send the request to the server.
|
||||||
const startActionArgs = {
|
const startActionArgs = {
|
||||||
bindingId: props.actionData.bindingId,
|
bindingId: props.actionData.bindingId,
|
||||||
arguments: actionArgs,
|
arguments: actionArgs,
|
||||||
uniqueTrackingId: getUniqueId()
|
uniqueTrackingId: getUniqueId()
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Watching buttonResults for', startActionArgs.uniqueTrackingId)
|
console.log('Watching buttonResults for', startActionArgs.uniqueTrackingId)
|
||||||
|
|
||||||
watch(
|
stopWatchingButtonResult()
|
||||||
() => buttonResults[startActionArgs.uniqueTrackingId],
|
stopButtonResultWatch = watch(
|
||||||
(newResult, oldResult) => {
|
() => buttonResults[startActionArgs.uniqueTrackingId],
|
||||||
|
(newResult, oldResult) => {
|
||||||
onLogEntryChanged(newResult)
|
onLogEntryChanged(newResult)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
requestReconnectNow()
|
requestReconnectNow()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await window.client.startAction(startActionArgs)
|
const response = await window.client.startAction(startActionArgs)
|
||||||
const trackingId = response.executionTrackingId || startActionArgs.uniqueTrackingId
|
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}`)
|
router.push(`/logs/${trackingId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!connectionState.connected) {
|
if (!connectionState.connected) {
|
||||||
await pollExecutionUntilDone(trackingId)
|
await pollExecutionUntilDone(trackingId)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} 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) {
|
if (logEntry.executionFinished) {
|
||||||
onExecutionFinished(logEntry)
|
stopWatchingButtonResult()
|
||||||
|
onExecutionFinished(logEntry)
|
||||||
} else if (logEntry.queued && !logEntry.executionStarted) {
|
} else if (logEntry.queued && !logEntry.executionStarted) {
|
||||||
onExecutionQueued(logEntry)
|
onExecutionQueued(logEntry)
|
||||||
} else {
|
} else {
|
||||||
onExecutionStarted(logEntry)
|
onExecutionStarted(logEntry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onExecutionQueued(_logEntry) {
|
function onExecutionQueued (_logEntry) {
|
||||||
isDisabled.value = true
|
isDisabled.value = true
|
||||||
updateDom('action-queued', '[Queued]')
|
updateDom('action-queued', '[Queued]')
|
||||||
}
|
}
|
||||||
|
|
||||||
function onExecutionStarted(logEntry) {
|
function onExecutionStarted (logEntry) {
|
||||||
if (
|
if (
|
||||||
popupOnStart.value &&
|
popupOnStart.value &&
|
||||||
popupOnStart.value.includes('execution-dialog') &&
|
popupOnStart.value.includes('execution-dialog') &&
|
||||||
!shouldSuppressPopupOnStartNavigation(router)
|
!shouldSuppressPopupOnStartNavigation(router)
|
||||||
) {
|
) {
|
||||||
router.push(`/logs/${logEntry.executionTrackingId}`)
|
router.push(`/logs/${logEntry.executionTrackingId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
isDisabled.value = true
|
isDisabled.value = true
|
||||||
updateDom(null, title.value)
|
updateDom(null, title.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onExecutionFinished(logEntry) {
|
function onExecutionFinished (logEntry) {
|
||||||
const trackingId = logEntry.executionTrackingId
|
const trackingId = logEntry.executionTrackingId
|
||||||
if (trackingId) {
|
if (trackingId) {
|
||||||
if (flashedTrackingIds.has(trackingId)) {
|
if (flashedTrackingIds.has(trackingId)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
flashedTrackingIds.add(trackingId)
|
flashedTrackingIds.add(trackingId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local no-arg watches and the binding-scoped pending flash can both
|
// Local no-arg watches and the binding-scoped pending flash can both
|
||||||
// observe the same finished execution; consume so only one path flashes.
|
// observe the same finished execution; consume so only one path flashes.
|
||||||
if (bindingId.value) {
|
if (bindingId.value) {
|
||||||
consumePendingBindingFlash(bindingId.value)
|
consumePendingBindingFlash(bindingId.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logEntry.timedOut) {
|
if (logEntry.timedOut) {
|
||||||
renderExecutionResult('action-timeout', 'Timed out')
|
renderExecutionResult('action-timeout', 'Timed out')
|
||||||
} else if (logEntry.blocked) {
|
} else if (logEntry.blocked) {
|
||||||
renderExecutionResult('action-blocked', 'Blocked!')
|
renderExecutionResult('action-blocked', 'Blocked!')
|
||||||
} else if (logEntry.exitCode !== 0) {
|
} else if (logEntry.exitCode !== 0) {
|
||||||
renderExecutionResult('action-nonzero-exit', 'Exit code ' + logEntry.exitCode)
|
renderExecutionResult('action-nonzero-exit', 'Exit code ' + logEntry.exitCode)
|
||||||
} else {
|
} else {
|
||||||
renderExecutionResult('action-success', 'Success!')
|
renderExecutionResult('action-success', 'Success!')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderExecutionResult(resultCssClass, temporaryStatusMessage) {
|
function renderExecutionResult (resultCssClass, temporaryStatusMessage) {
|
||||||
updateDom(resultCssClass, '[' + temporaryStatusMessage + ']')
|
updateDom(resultCssClass, '[' + temporaryStatusMessage + ']')
|
||||||
onExecStatusChanged()
|
onExecStatusChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDom(resultCssClass, newTitle) {
|
function updateDom (resultCssClass, newTitle) {
|
||||||
if (resultCssClass == null) {
|
if (resultCssClass == null) {
|
||||||
buttonClasses.value = []
|
buttonClasses.value = []
|
||||||
} else {
|
} else {
|
||||||
buttonClasses.value = [resultCssClass]
|
buttonClasses.value = [resultCssClass]
|
||||||
}
|
}
|
||||||
|
|
||||||
displayTitle.value = newTitle
|
displayTitle.value = newTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
function onExecStatusChanged() {
|
function onExecStatusChanged () {
|
||||||
isDisabled.value = false
|
isDisabled.value = false
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
updateDom(null, title.value)
|
updateDom(null, title.value)
|
||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -448,47 +506,47 @@ onMounted(() => {
|
||||||
// Watch the central rate limit store for updates to this button's bindingId
|
// 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 the entire rateLimits object to ensure reactivity with dynamic keys
|
||||||
watch(
|
watch(
|
||||||
rateLimits,
|
rateLimits,
|
||||||
() => {
|
() => {
|
||||||
const id = bindingId.value
|
const id = bindingId.value
|
||||||
if (id && rateLimits[id] !== undefined) {
|
if (id && rateLimits[id] !== undefined) {
|
||||||
const newExpires = rateLimits[id]
|
const newExpires = rateLimits[id]
|
||||||
if (newExpires !== rateLimitExpires.value) {
|
if (newExpires !== rateLimitExpires.value) {
|
||||||
rateLimitExpires.value = newExpires
|
rateLimitExpires.value = newExpires
|
||||||
updateRateLimitStatus()
|
updateRateLimitStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// Binding-scoped flash survives argument-form navigation/remount (#920).
|
// Binding-scoped flash survives argument-form navigation/remount (#920).
|
||||||
watch(
|
watch(
|
||||||
() => pendingBindingFlash[bindingId.value],
|
() => pendingBindingFlash[bindingId.value],
|
||||||
(pending) => {
|
(pending) => {
|
||||||
if (pending) {
|
if (pending) {
|
||||||
consumeAndFlashPendingResult()
|
consumeAndFlashPendingResult()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
isComponentMounted.value = false
|
isComponentMounted.value = false
|
||||||
if (rateLimitInterval.value) {
|
if (rateLimitInterval.value) {
|
||||||
clearInterval(rateLimitInterval.value)
|
clearInterval(rateLimitInterval.value)
|
||||||
rateLimitInterval.value = null
|
rateLimitInterval.value = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.actionData,
|
() => props.actionData,
|
||||||
(newData) => {
|
(newData) => {
|
||||||
updateFromJson(newData)
|
updateFromJson(newData)
|
||||||
if (newData?.icon !== undefined) {
|
if (newData?.icon !== undefined) {
|
||||||
glyph.value = newData.icon ?? ''
|
glyph.value = newData.icon ?? ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,126 +1,218 @@
|
||||||
<template>
|
<template>
|
||||||
<Header :title="pageTitle" :logoUrl="logoUrl" @toggleSidebar="toggleSidebar" :sidebarEnabled="sidebarEnabled" :topBarEnabled="topbarEnabled" :navigation="navigation">
|
<Header
|
||||||
<template #toolbar>
|
:title="pageTitle"
|
||||||
<div id="banner" v-if="bannerMessage" :style="bannerCss">
|
:logo-url="logoUrl"
|
||||||
<p>{{ bannerMessage }}</p>
|
:sidebar-enabled="sidebarEnabled"
|
||||||
</div>
|
:top-bar-enabled="topbarEnabled"
|
||||||
</template>
|
:navigation="navigation"
|
||||||
|
@toggle-sidebar="toggleSidebar"
|
||||||
|
>
|
||||||
|
<template #toolbar>
|
||||||
|
<div
|
||||||
|
v-if="bannerMessage"
|
||||||
|
id="banner"
|
||||||
|
:style="bannerCss"
|
||||||
|
>
|
||||||
|
<p>{{ bannerMessage }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #user-info>
|
<template #user-info>
|
||||||
<ConnectionBanner />
|
<ConnectionBanner />
|
||||||
<div class="flex-row user-info" style="gap: .5em;">
|
<div
|
||||||
<span id="link-login" v-if="!isLoggedIn && showLoginLink"><router-link to="/login">{{ t('login-button') }}</router-link></span>
|
class="flex-row user-info"
|
||||||
<router-link v-else to="/user" class="user-link" v-if="isLoggedIn">
|
style="gap: .5em;"
|
||||||
<span id="username-text">{{ username }}</span>
|
>
|
||||||
</router-link>
|
<span
|
||||||
<HugeiconsIcon :icon="UserCircle02Icon" width = "1.5em" height = "1.5em" v-if="isLoggedIn" />
|
v-if="!isLoggedIn && showLoginLink"
|
||||||
</div>
|
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>
|
<div id="layout">
|
||||||
</Header>
|
<Navigation ref="navigation">
|
||||||
|
<Sidebar
|
||||||
|
v-if="sidebarEnabled && showNavigation"
|
||||||
|
id="mainnav"
|
||||||
|
ref="sidebar"
|
||||||
|
/>
|
||||||
|
</Navigation>
|
||||||
|
|
||||||
<div id="layout">
|
<div
|
||||||
<Navigation ref="navigation">
|
id="content"
|
||||||
<Sidebar ref="sidebar" id = "mainnav" v-if="sidebarEnabled && showNavigation"/>
|
:initial-martial-complete="hasLoaded"
|
||||||
</Navigation>
|
>
|
||||||
|
<main title="Main content">
|
||||||
|
<router-view :key="$route.fullPath" />
|
||||||
|
</main>
|
||||||
|
|
||||||
<div id="content" initial-martial-complete="{{ hasLoaded }}">
|
<footer
|
||||||
<main title="Main content">
|
v-if="showFooter"
|
||||||
<router-view :key="$route.fullPath" />
|
title="footer"
|
||||||
</main>
|
>
|
||||||
|
<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">
|
<span>
|
||||||
<p>
|
<a
|
||||||
<img title="application icon" :src="logoUrl" alt="OliveTin logo" style="height: 1em;" class="logo" />
|
href="https://github.com/OliveTin/OliveTin/issues/new/choose"
|
||||||
OliveTin <span v-if="showVersionNumber">{{ currentVersion }}</span>
|
target="_new"
|
||||||
</p>
|
>{{ t('raise-issue') }}</a>
|
||||||
<p>
|
</span>
|
||||||
<span>
|
|
||||||
<a href="https://docs.olivetin.app" target="_new">{{ t('docs') }}</a>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<a href="https://github.com/OliveTin/OliveTin/issues/new/choose" target="_new">{{ t('raise-issue') }}</a>
|
<a
|
||||||
</span>
|
href="#"
|
||||||
|
@click.prevent="openLanguageDialog"
|
||||||
|
>{{ currentLanguageName }}</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span>
|
<span v-if="availableThemes.length > 1">
|
||||||
<a href="#" @click.prevent="openLanguageDialog">{{ currentLanguageName }}</a>
|
<a
|
||||||
</span>
|
href="#"
|
||||||
|
@click.prevent="openThemeDialog"
|
||||||
<span v-if="availableThemes.length > 1">
|
>{{ currentThemeName }}</a>
|
||||||
<a href="#" @click.prevent="openThemeDialog">{{ currentThemeName }}</a>
|
</span>
|
||||||
</span>
|
</p>
|
||||||
</p>
|
<p v-if="showVersionNumber">
|
||||||
<p v-if="showVersionNumber">
|
<a
|
||||||
<a id="available-version" href="http://olivetin.app" target="_blank" hidden>?</a>
|
id="available-version"
|
||||||
</p>
|
href="http://olivetin.app"
|
||||||
</footer>
|
target="_blank"
|
||||||
</div>
|
hidden
|
||||||
|
>?</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<dialog ref="languageDialog" class="language-dialog" @click="handleLanguageDialogClick">
|
<dialog
|
||||||
<div class="dialog-content" @click.stop>
|
ref="languageDialog"
|
||||||
<h2>{{ t('language-dialog.title') }}</h2>
|
class="language-dialog"
|
||||||
<select v-model="selectedLanguage" @change="changeLanguage" class="language-select">
|
@click="handleLanguageDialogClick"
|
||||||
<option v-for="(name, code) in availableLanguages" :key="code" :value="code">
|
>
|
||||||
{{ code === 'auto' ? name : `${name} (${code})` }}
|
<div
|
||||||
</option>
|
class="dialog-content"
|
||||||
</select>
|
@click.stop
|
||||||
<p class="browser-languages">
|
>
|
||||||
{{ t('language-dialog.browser-languages') }}:
|
<h2>{{ t('language-dialog.title') }}</h2>
|
||||||
<span v-if="browserLanguages.length > 0">{{ browserLanguages.join(', ') }}</span>
|
<select
|
||||||
<span v-else>{{ t('language-dialog.not-available') }}</span>
|
v-model="selectedLanguage"
|
||||||
</p>
|
class="language-select"
|
||||||
<div class="dialog-buttons">
|
@change="changeLanguage"
|
||||||
<button @click="closeLanguageDialog">{{ t('language-dialog.close') }}</button>
|
>
|
||||||
</div>
|
<option
|
||||||
</div>
|
v-for="(name, code) in availableLanguages"
|
||||||
</dialog>
|
: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">
|
<dialog
|
||||||
<div class="dialog-content" @click.stop>
|
ref="themeDialog"
|
||||||
<h2>{{ t('theme-dialog.title') }}</h2>
|
class="theme-dialog"
|
||||||
<select v-model="selectedTheme" @change="changeTheme" class="language-select">
|
@click="handleThemeDialogClick"
|
||||||
<option value="">{{ t('theme-dialog.default') }}</option>
|
>
|
||||||
<option v-for="theme in availableThemes" :key="theme" :value="theme">
|
<div
|
||||||
{{ theme }}
|
class="dialog-content"
|
||||||
</option>
|
@click.stop
|
||||||
</select>
|
>
|
||||||
<div class="dialog-buttons">
|
<h2>{{ t('theme-dialog.title') }}</h2>
|
||||||
<button @click="closeThemeDialog">{{ t('theme-dialog.close') }}</button>
|
<select
|
||||||
</div>
|
v-model="selectedTheme"
|
||||||
</div>
|
class="language-select"
|
||||||
</dialog>
|
@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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router'
|
||||||
import Sidebar from 'picocrank/vue/components/Sidebar.vue';
|
import Sidebar from 'picocrank/vue/components/Sidebar.vue'
|
||||||
import Navigation from 'picocrank/vue/components/Navigation.vue';
|
import Navigation from 'picocrank/vue/components/Navigation.vue'
|
||||||
import Header from 'picocrank/vue/components/Header.vue';
|
import Header from 'picocrank/vue/components/Header.vue'
|
||||||
import ConnectionBanner from './components/ConnectionBanner.vue';
|
import ConnectionBanner from './components/ConnectionBanner.vue'
|
||||||
import { connectEventStreamIfNeeded } from '../../js/websocket.js';
|
import { connectEventStreamIfNeeded } from '../../js/websocket.js'
|
||||||
import { HugeiconsIcon } from '@hugeicons/vue'
|
import { HugeiconsIcon } from '@hugeicons/vue'
|
||||||
import { Menu01Icon } from '@hugeicons/core-free-icons'
|
import { UserCircle02Icon, DashboardSquare01Icon } from '@hugeicons/core-free-icons'
|
||||||
import { UserCircle02Icon } from '@hugeicons/core-free-icons'
|
import logoUrl from '../../OliveTinLogo.png'
|
||||||
import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
|
import { useI18n } from 'vue-i18n'
|
||||||
import logoUrl from '../../OliveTinLogo.png';
|
import combinedTranslations from '../../../lang/combined_output.json'
|
||||||
import { useI18n } from 'vue-i18n';
|
const { t } = useI18n()
|
||||||
import combinedTranslations from '../../../lang/combined_output.json';
|
|
||||||
const { t, locale } = useI18n();
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
const sidebar = ref(null);
|
const sidebar = ref(null)
|
||||||
const navigation = ref(null);
|
const navigation = ref(null)
|
||||||
const username = ref('notset');
|
const username = ref('notset')
|
||||||
const isLoggedIn = ref(false);
|
const isLoggedIn = ref(false)
|
||||||
const currentVersion = ref('?');
|
const currentVersion = ref('?')
|
||||||
const pageTitle = ref('OliveTin');
|
const pageTitle = ref('OliveTin')
|
||||||
const bannerMessage = ref('');
|
const bannerMessage = ref('')
|
||||||
const bannerCss = ref('');
|
const bannerCss = ref('')
|
||||||
const hasLoaded = ref(false);
|
const hasLoaded = ref(false)
|
||||||
const showFooter = ref(true)
|
const showFooter = ref(true)
|
||||||
const showNavigation = ref(true)
|
const showNavigation = ref(true)
|
||||||
const showLogs = ref(true)
|
const showLogs = ref(true)
|
||||||
|
|
@ -144,279 +236,279 @@ const selectedTheme = ref(themePreference.value)
|
||||||
|
|
||||||
// Available languages with display names
|
// Available languages with display names
|
||||||
const availableLanguages = {
|
const availableLanguages = {
|
||||||
'auto': 'Browser Language',
|
auto: 'Browser Language',
|
||||||
'en': 'English',
|
en: 'English',
|
||||||
'de-DE': 'Deutsch',
|
'de-DE': 'Deutsch',
|
||||||
'es-ES': 'Español',
|
'es-ES': 'Español',
|
||||||
'it-IT': 'Italiano',
|
'it-IT': 'Italiano',
|
||||||
'zh-Hans-CN': '简体中文'
|
'zh-Hans-CN': '简体中文'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Computed property to get current language display name
|
// Computed property to get current language display name
|
||||||
const currentLanguageName = computed(() => {
|
const currentLanguageName = computed(() => {
|
||||||
if (languagePreference.value === 'auto') {
|
if (languagePreference.value === 'auto') {
|
||||||
return availableLanguages['auto']
|
return availableLanguages.auto
|
||||||
}
|
}
|
||||||
|
|
||||||
return availableLanguages[languagePreference.value] || languagePreference.value
|
return availableLanguages[languagePreference.value] || languagePreference.value
|
||||||
})
|
})
|
||||||
|
|
||||||
// Computed property to get current theme display name
|
// Computed property to get current theme display name
|
||||||
const currentThemeName = computed(() => {
|
const currentThemeName = computed(() => {
|
||||||
if (!themePreference.value || themePreference.value === '') {
|
if (!themePreference.value || themePreference.value === '') {
|
||||||
return t('theme-dialog.default')
|
return t('theme-dialog.default')
|
||||||
}
|
}
|
||||||
return themePreference.value
|
return themePreference.value
|
||||||
})
|
})
|
||||||
|
|
||||||
// Computed properties for navigation style
|
// Computed properties for navigation style
|
||||||
const topbarEnabled = computed(() => {
|
const topbarEnabled = computed(() => {
|
||||||
return sectionNavigationStyle.value === 'topbar'
|
return sectionNavigationStyle.value === 'topbar'
|
||||||
})
|
})
|
||||||
|
|
||||||
const sidebarEnabled = computed(() => {
|
const sidebarEnabled = computed(() => {
|
||||||
return sectionNavigationStyle.value !== 'topbar' && showNavigation.value
|
return sectionNavigationStyle.value !== 'topbar' && showNavigation.value
|
||||||
})
|
})
|
||||||
|
|
||||||
function normalizeBrowserLanguage() {
|
function normalizeBrowserLanguage () {
|
||||||
const available = Object.keys(combinedTranslations.messages || {})
|
const available = Object.keys(combinedTranslations.messages || {})
|
||||||
|
|
||||||
if (navigator.languages && navigator.languages.length > 0) {
|
if (navigator.languages && navigator.languages.length > 0) {
|
||||||
for (const candidate of navigator.languages) {
|
for (const candidate of navigator.languages) {
|
||||||
const lowerCandidate = candidate.toLowerCase()
|
const lowerCandidate = candidate.toLowerCase()
|
||||||
|
|
||||||
// Try exact match (case-insensitive)
|
// Try exact match (case-insensitive)
|
||||||
const exact = available.find(locale => locale.toLowerCase() === lowerCandidate)
|
const exact = available.find(locale => locale.toLowerCase() === lowerCandidate)
|
||||||
if (exact) {
|
if (exact) {
|
||||||
return exact
|
return exact
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try prefix match (e.g., "zh-CN" -> "zh-Hans-CN")
|
// Try prefix match (e.g., "zh-CN" -> "zh-Hans-CN")
|
||||||
const prefix = available.find(locale => locale.toLowerCase().startsWith(lowerCandidate.split('-')[0] + '-'))
|
const prefix = available.find(locale => locale.toLowerCase().startsWith(lowerCandidate.split('-')[0] + '-'))
|
||||||
if (prefix) {
|
if (prefix) {
|
||||||
return prefix
|
return prefix
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return 'en'
|
return 'en'
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSidebar() {
|
function toggleSidebar () {
|
||||||
if (sidebar.value && showNavigation.value) {
|
if (sidebar.value && showNavigation.value) {
|
||||||
sidebar.value.toggle()
|
sidebar.value.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateHeaderFromInit() {
|
function updateHeaderFromInit () {
|
||||||
if (!window.initResponse) {
|
if (!window.initResponse) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
username.value = window.initResponse.authenticatedUser
|
username.value = window.initResponse.authenticatedUser
|
||||||
isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
|
isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
|
||||||
currentVersion.value = window.initResponse.currentVersion
|
currentVersion.value = window.initResponse.currentVersion
|
||||||
pageTitle.value = window.initResponse.pageTitle || 'OliveTin'
|
pageTitle.value = window.initResponse.pageTitle || 'OliveTin'
|
||||||
bannerMessage.value = window.initResponse.bannerMessage || ''
|
bannerMessage.value = window.initResponse.bannerMessage || ''
|
||||||
bannerCss.value = window.initResponse.bannerCss || ''
|
bannerCss.value = window.initResponse.bannerCss || ''
|
||||||
showFooter.value = window.initResponse.showFooter
|
showFooter.value = window.initResponse.showFooter
|
||||||
showNavigation.value = window.initResponse.showNavigation
|
showNavigation.value = window.initResponse.showNavigation
|
||||||
showLogs.value = window.initResponse.showLogList
|
showLogs.value = window.initResponse.showLogList
|
||||||
showDiagnostics.value = window.initResponse.showDiagnostics
|
showDiagnostics.value = window.initResponse.showDiagnostics
|
||||||
showVersionNumber.value = window.initResponse.effectivePolicy?.showVersionNumber ?? true
|
showVersionNumber.value = window.initResponse.effectivePolicy?.showVersionNumber ?? true
|
||||||
sectionNavigationStyle.value = window.initResponse.sectionNavigationStyle || 'sidebar'
|
sectionNavigationStyle.value = window.initResponse.sectionNavigationStyle || 'sidebar'
|
||||||
availableThemes.value = window.initResponse.availableThemes || []
|
availableThemes.value = window.initResponse.availableThemes || []
|
||||||
|
|
||||||
if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
|
if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
|
||||||
showLoginLink.value = false
|
showLoginLink.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
applyStyleMods()
|
applyStyleMods()
|
||||||
loadCustomJsIfEnabled()
|
loadCustomJsIfEnabled()
|
||||||
|
|
||||||
renderNavigation()
|
renderNavigation()
|
||||||
applyTheme()
|
applyTheme()
|
||||||
|
|
||||||
if (window.initResponse.loginRequired) {
|
|
||||||
connectEventStreamIfNeeded()
|
|
||||||
router.push('/login')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (window.initResponse.loginRequired) {
|
||||||
connectEventStreamIfNeeded()
|
connectEventStreamIfNeeded()
|
||||||
|
router.push('/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
connectEventStreamIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNavigation() {
|
function renderNavigation () {
|
||||||
if (!navigation.value) {
|
if (!navigation.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootDashboards = window.initResponse?.rootDashboards || []
|
const rootDashboards = window.initResponse?.rootDashboards || []
|
||||||
|
|
||||||
if (typeof navigation.value.clear === 'function') {
|
if (typeof navigation.value.clear === 'function') {
|
||||||
navigation.value.clear()
|
navigation.value.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const rootDashboard of rootDashboards) {
|
for (const rootDashboard of rootDashboards) {
|
||||||
navigation.value.addNavigationLink({
|
navigation.value.addNavigationLink({
|
||||||
id: rootDashboard,
|
id: rootDashboard,
|
||||||
name: rootDashboard,
|
name: rootDashboard,
|
||||||
title: rootDashboard,
|
title: rootDashboard,
|
||||||
path: rootDashboard === 'Actions' ? '/' : `/dashboards/${rootDashboard}`,
|
path: rootDashboard === 'Actions' ? '/' : `/dashboards/${rootDashboard}`,
|
||||||
icon: DashboardSquare01Icon,
|
icon: DashboardSquare01Icon
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
navigation.value.addSeparator()
|
navigation.value.addSeparator()
|
||||||
navigation.value.addRouterLink('Entities', t('nav.entities'))
|
navigation.value.addRouterLink('Entities', t('nav.entities'))
|
||||||
|
|
||||||
if (showLogs.value) {
|
if (showLogs.value) {
|
||||||
navigation.value.addRouterLink('Logs', t('nav.logs'))
|
navigation.value.addRouterLink('Logs', t('nav.logs'))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDiagnostics.value) {
|
if (showDiagnostics.value) {
|
||||||
navigation.value.addRouterLink('Diagnostics', t('nav.diagnostics'))
|
navigation.value.addRouterLink('Diagnostics', t('nav.diagnostics'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openLanguageDialog() {
|
function openLanguageDialog () {
|
||||||
selectedLanguage.value = languagePreference.value
|
selectedLanguage.value = languagePreference.value
|
||||||
|
|
||||||
if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
|
if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
|
||||||
browserLanguages.value = navigator.languages
|
browserLanguages.value = navigator.languages
|
||||||
} else {
|
} else {
|
||||||
browserLanguages.value = []
|
browserLanguages.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (languageDialog.value) {
|
if (languageDialog.value) {
|
||||||
languageDialog.value.showModal()
|
languageDialog.value.showModal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeLanguageDialog() {
|
function closeLanguageDialog () {
|
||||||
if (languageDialog.value) {
|
if (languageDialog.value) {
|
||||||
languageDialog.value.close()
|
languageDialog.value.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeLanguage() {
|
function changeLanguage () {
|
||||||
if (!window.i18n || !selectedLanguage.value) {
|
if (!window.i18n || !selectedLanguage.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedLanguage.value === 'auto') {
|
if (selectedLanguage.value === 'auto') {
|
||||||
localStorage.removeItem('olivetin-language')
|
localStorage.removeItem('olivetin-language')
|
||||||
languagePreference.value = 'auto'
|
languagePreference.value = 'auto'
|
||||||
window.i18n.locale.value = normalizeBrowserLanguage()
|
window.i18n.locale.value = normalizeBrowserLanguage()
|
||||||
} else {
|
} else {
|
||||||
window.i18n.locale.value = selectedLanguage.value
|
window.i18n.locale.value = selectedLanguage.value
|
||||||
localStorage.setItem('olivetin-language', selectedLanguage.value)
|
localStorage.setItem('olivetin-language', selectedLanguage.value)
|
||||||
languagePreference.value = selectedLanguage.value
|
languagePreference.value = selectedLanguage.value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update navigation with new translations
|
// Update navigation with new translations
|
||||||
if (navigation.value) {
|
if (navigation.value) {
|
||||||
renderNavigation()
|
renderNavigation()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closeLanguageDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLanguageDialogClick (event) {
|
||||||
|
// Close dialog when clicking on the backdrop
|
||||||
|
if (event.target === languageDialog.value) {
|
||||||
closeLanguageDialog()
|
closeLanguageDialog()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLanguageDialogClick(event) {
|
function openThemeDialog () {
|
||||||
// Close dialog when clicking on the backdrop
|
selectedTheme.value = themePreference.value || ''
|
||||||
if (event.target === languageDialog.value) {
|
|
||||||
closeLanguageDialog()
|
if (themeDialog.value) {
|
||||||
}
|
themeDialog.value.showModal()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openThemeDialog() {
|
function closeThemeDialog () {
|
||||||
selectedTheme.value = themePreference.value || ''
|
if (themeDialog.value) {
|
||||||
|
themeDialog.value.close()
|
||||||
if (themeDialog.value) {
|
}
|
||||||
themeDialog.value.showModal()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeThemeDialog() {
|
function changeTheme () {
|
||||||
if (themeDialog.value) {
|
if (!selectedTheme.value || selectedTheme.value === '') {
|
||||||
themeDialog.value.close()
|
localStorage.removeItem('olivetin-theme')
|
||||||
}
|
themePreference.value = ''
|
||||||
|
} else {
|
||||||
|
localStorage.setItem('olivetin-theme', selectedTheme.value)
|
||||||
|
themePreference.value = selectedTheme.value
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTheme()
|
||||||
|
closeThemeDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeTheme() {
|
function applyTheme () {
|
||||||
if (!selectedTheme.value || selectedTheme.value === '') {
|
let themeStyle = document.getElementById('theme-style')
|
||||||
localStorage.removeItem('olivetin-theme')
|
|
||||||
themePreference.value = ''
|
|
||||||
} else {
|
|
||||||
localStorage.setItem('olivetin-theme', selectedTheme.value)
|
|
||||||
themePreference.value = selectedTheme.value
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
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
|
window.updateHeaderFromInit = updateHeaderFromInit
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
updateHeaderFromInit()
|
updateHeaderFromInit()
|
||||||
|
|
||||||
// Initialize selected language from stored preference
|
// Initialize selected language from stored preference
|
||||||
selectedLanguage.value = languagePreference.value
|
selectedLanguage.value = languagePreference.value
|
||||||
|
|
||||||
// Initialize selected theme from stored preference
|
// Initialize selected theme from stored preference
|
||||||
selectedTheme.value = themePreference.value || ''
|
selectedTheme.value = themePreference.value || ''
|
||||||
|
|
||||||
if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
|
if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
|
||||||
browserLanguages.value = navigator.languages
|
browserLanguages.value = navigator.languages
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,125 @@
|
||||||
<template>
|
<template>
|
||||||
<section v-if="!dashboard && !initError" style = "text-align: center; padding: 2em;">
|
<section
|
||||||
<HugeiconsIcon :icon="Loading03Icon" width="3em" height="3em" style="animation: spin 1s linear infinite;" />
|
v-if="!dashboard && !initError && !loadError"
|
||||||
<p>Loading dashboard...</p>
|
style="text-align: center; padding: 2em;"
|
||||||
<p style="color: var(--fg2);">{{ loadingTime }}s</p>
|
>
|
||||||
|
<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>
|
||||||
<section v-if="initError" style="text-align: center; padding: 2em;" class = "bad">
|
|
||||||
<h2 style="color: var(--error);">Initialization Failed</h2>
|
<section
|
||||||
<p>{{ initError }}</p>
|
v-else
|
||||||
<p style="color: var(--fg2);">Please check your configuration and try again.</p>
|
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>
|
</section>
|
||||||
<template v-else-if="dashboard">
|
</template>
|
||||||
<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>
|
<script setup>
|
||||||
|
|
@ -63,189 +130,190 @@ import { HugeiconsIcon } from '@hugeicons/vue'
|
||||||
import { Loading03Icon, ArrowLeftIcon } from '@hugeicons/core-free-icons'
|
import { Loading03Icon, ArrowLeftIcon } from '@hugeicons/core-free-icons'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
entityType: {
|
entityType: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
entityKey: {
|
entityKey: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false
|
required: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const dashboard = ref(null)
|
const dashboard = ref(null)
|
||||||
const loadingTime = ref(0)
|
const loadingTime = ref(0)
|
||||||
const initError = ref(null)
|
const initError = ref(null)
|
||||||
|
const loadError = ref(null)
|
||||||
let loadingTimer = null
|
let loadingTimer = null
|
||||||
let checkInitInterval = null
|
let checkInitInterval = null
|
||||||
let dashboardRequestId = 0
|
let dashboardRequestId = 0
|
||||||
|
|
||||||
const isDirectory = computed(() => {
|
const isDirectory = computed(() => {
|
||||||
if (!dashboard.value || !window.initResponse) {
|
if (!dashboard.value || !window.initResponse) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const rootDashboards = window.initResponse.rootDashboards || []
|
const rootDashboards = window.initResponse.rootDashboards || []
|
||||||
return !rootDashboards.includes(dashboard.value.title) && dashboard.value.title !== 'Actions'
|
return !rootDashboards.includes(dashboard.value.title) && dashboard.value.title !== 'Actions'
|
||||||
})
|
})
|
||||||
|
|
||||||
function goBack() {
|
function goBack () {
|
||||||
if (window.history.length > 1) {
|
if (window.history.length > 1) {
|
||||||
router.back()
|
router.back()
|
||||||
|
} else {
|
||||||
|
const rootDashboards = window.initResponse?.rootDashboards || []
|
||||||
|
if (rootDashboards.length > 0) {
|
||||||
|
router.push({ name: 'Dashboard', params: { title: rootDashboards[0] } })
|
||||||
} else {
|
} else {
|
||||||
const rootDashboards = window.initResponse?.rootDashboards || []
|
router.push({ name: 'Actions' })
|
||||||
if (rootDashboards.length > 0) {
|
|
||||||
router.push({ name: 'Dashboard', params: { title: rootDashboards[0] } })
|
|
||||||
} else {
|
|
||||||
router.push({ name: 'Actions' })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getDashboard() {
|
async function getDashboard () {
|
||||||
const requestId = ++dashboardRequestId
|
const requestId = ++dashboardRequestId
|
||||||
let title = props.title
|
let title = props.title
|
||||||
|
|
||||||
// If no specific title was provided or it's the placeholder 'default',
|
// If no specific title was provided or it's the placeholder 'default',
|
||||||
// prefer the first configured root dashboard (e.g., "Test").
|
// prefer the first configured root dashboard (e.g., "Test").
|
||||||
if ((!title || title === 'default') && window.initResponse.rootDashboards && window.initResponse.rootDashboards.length > 0) {
|
if ((!title || title === 'default') && window.initResponse.rootDashboards && window.initResponse.rootDashboards.length > 0) {
|
||||||
title = window.initResponse.rootDashboards[0]
|
title = window.initResponse.rootDashboards[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = {
|
||||||
|
title
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (props.entityType && props.entityKey) {
|
||||||
const request = {
|
request.entityType = props.entityType
|
||||||
title: title,
|
request.entityKey = props.entityKey
|
||||||
}
|
|
||||||
|
|
||||||
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')
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function waitForInitAndLoadDashboard() {
|
const ret = await window.client.getDashboard(request)
|
||||||
document.body.removeAttribute('loaded-dashboard')
|
|
||||||
|
|
||||||
|
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) {
|
if (loadingTimer) {
|
||||||
clearInterval(loadingTimer)
|
clearInterval(loadingTimer)
|
||||||
loadingTimer = null
|
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)
|
clearInterval(checkInitInterval)
|
||||||
checkInitInterval = null
|
checkInitInterval = null
|
||||||
}
|
|
||||||
|
|
||||||
loadingTime.value = 0
|
|
||||||
loadingTimer = setInterval(() => {
|
|
||||||
loadingTime.value++
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
// Check if init has completed successfully
|
|
||||||
if (window.initResponse) {
|
|
||||||
getDashboard()
|
getDashboard()
|
||||||
} else if (window.initError) {
|
} else if (window.initError) {
|
||||||
// Init failed, show error immediately
|
clearInterval(checkInitInterval)
|
||||||
|
checkInitInterval = null
|
||||||
initError.value = window.initErrorMessage || 'Initialization failed. Please check your configuration and try again.'
|
initError.value = window.initErrorMessage || 'Initialization failed. Please check your configuration and try again.'
|
||||||
// Stop the loading timer since we're showing an error
|
// Stop the loading timer since we're showing an error
|
||||||
if (loadingTimer) {
|
if (loadingTimer) {
|
||||||
clearInterval(loadingTimer)
|
clearInterval(loadingTimer)
|
||||||
loadingTimer = null
|
loadingTimer = null
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
// Init hasn't completed yet, poll for completion
|
}, 100) // Check every 100ms
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
waitForInitAndLoadDashboard()
|
waitForInitAndLoadDashboard()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [props.title, props.entityType, props.entityKey],
|
() => [props.title, props.entityType, props.entityKey],
|
||||||
() => {
|
() => {
|
||||||
dashboard.value = null
|
dashboard.value = null
|
||||||
waitForInitAndLoadDashboard()
|
waitForInitAndLoadDashboard()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.body.removeAttribute('loaded-dashboard')
|
document.body.removeAttribute('loaded-dashboard')
|
||||||
|
|
||||||
// Clean up the timers when component is unmounted
|
// Clean up the timers when component is unmounted
|
||||||
if (loadingTimer) {
|
if (loadingTimer) {
|
||||||
clearInterval(loadingTimer)
|
clearInterval(loadingTimer)
|
||||||
loadingTimer = null
|
loadingTimer = null
|
||||||
}
|
}
|
||||||
if (checkInitInterval) {
|
if (checkInitInterval) {
|
||||||
clearInterval(checkInitInterval)
|
clearInterval(checkInitInterval)
|
||||||
checkInitInterval = null
|
checkInitInterval = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -279,7 +347,6 @@ fieldset {
|
||||||
place-items: stretch;
|
place-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:id="`execution-${executionTrackingId}`"
|
:id="`execution-${trackingId}`"
|
||||||
class="execution-button"
|
class="execution-button"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
|
@ -13,25 +13,27 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//import { ExecutionFeedbackButton } from '../js/ExecutionFeedbackButton.js'
|
// import { ExecutionFeedbackButton } from '../js/ExecutionFeedbackButton.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ExecutionButton',
|
name: 'ExecutionButton',
|
||||||
// mixins: [ExecutionFeedbackButton],
|
// mixins: [ExecutionFeedbackButton],
|
||||||
props: {
|
props: {
|
||||||
executionTrackingId: {
|
executionTrackingId: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
emits: ['show'],
|
||||||
|
data () {
|
||||||
return {
|
return {
|
||||||
|
trackingId: '',
|
||||||
ellapsed: 0,
|
ellapsed: 0,
|
||||||
isWaiting: true
|
isWaiting: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
buttonText() {
|
buttonText () {
|
||||||
if (this.isWaiting) {
|
if (this.isWaiting) {
|
||||||
return 'Executing...'
|
return 'Executing...'
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -39,33 +41,34 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted () {
|
||||||
|
this.trackingId = this.executionTrackingId
|
||||||
this.constructFromJson(this.executionTrackingId)
|
this.constructFromJson(this.executionTrackingId)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
constructFromJson(json) {
|
constructFromJson (json) {
|
||||||
this.executionTrackingId = json
|
this.trackingId = json
|
||||||
this.ellapsed = 0
|
this.ellapsed = 0
|
||||||
this.isWaiting = true
|
this.isWaiting = true
|
||||||
},
|
},
|
||||||
|
|
||||||
show() {
|
async show () {
|
||||||
this.$emit('show')
|
this.$emit('show')
|
||||||
|
|
||||||
if (window.executionDialog) {
|
if (window.executionDialog) {
|
||||||
window.executionDialog.reset()
|
await window.executionDialog.reset()
|
||||||
window.executionDialog.show()
|
window.executionDialog.show()
|
||||||
window.executionDialog.fetchExecutionResult(this.executionTrackingId)
|
window.executionDialog.fetchExecutionResult(this.trackingId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onExecStatusChanged() {
|
onExecStatusChanged () {
|
||||||
this.isWaiting = false
|
this.isWaiting = false
|
||||||
this.domTitle = this.ellapsed + 's'
|
this.domTitle = this.ellapsed + 's'
|
||||||
},
|
},
|
||||||
|
|
||||||
// Override from ExecutionFeedbackButton
|
// Override from ExecutionFeedbackButton
|
||||||
onExecutionFinished(logEntry) {
|
onExecutionFinished (logEntry) {
|
||||||
if (logEntry.timedOut) {
|
if (logEntry.timedOut) {
|
||||||
this.renderExecutionResult('action-timeout', 'Timed out')
|
this.renderExecutionResult('action-timeout', 'Timed out')
|
||||||
} else if (logEntry.blocked) {
|
} else if (logEntry.blocked) {
|
||||||
|
|
@ -77,17 +80,20 @@ export default {
|
||||||
this.renderExecutionResult('action-success', 'Success!')
|
this.renderExecutionResult('action-success', 'Success!')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
renderExecutionResult(resultCssClass, temporaryStatusMessage) {
|
renderExecutionResult (resultCssClass, temporaryStatusMessage) {
|
||||||
this.updateDom(resultCssClass, '[' + temporaryStatusMessage + ']')
|
this.updateDom(resultCssClass, '[' + temporaryStatusMessage + ']')
|
||||||
this.onExecStatusChanged()
|
this.onExecStatusChanged()
|
||||||
},
|
},
|
||||||
|
|
||||||
updateDom(resultCssClass, title) {
|
updateDom (resultCssClass, title) {
|
||||||
// For execution button, we don't need to update classes as much
|
// For execution button, we don't need to update classes as much
|
||||||
// since it's a simpler component
|
// since it's a simpler component
|
||||||
if (resultCssClass) {
|
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;
|
border-color: #c3e6cb;
|
||||||
color: #155724;
|
color: #155724;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
<template>
|
<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 }) }}
|
{{ t('logs.action-group-limits', { concurrent: maxConcurrent, queueSize }) }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<span class="action-icon-glyph">
|
<span class="action-icon-glyph">
|
||||||
<HugeiconsIcon
|
<HugeiconsIcon
|
||||||
v-if="hugeiconsModel"
|
v-if="hugeiconsModel"
|
||||||
:icon="hugeiconsModel"
|
:icon="hugeiconsModel"
|
||||||
width="1em"
|
width="1em"
|
||||||
height="1em"
|
height="1em"
|
||||||
class="action-icon-glyph-svg"
|
class="action-icon-glyph-svg"
|
||||||
/>
|
/>
|
||||||
<span v-else-if="decodedTextGlyphIsHtml" v-html="decodedTextGlyph"></span>
|
<span
|
||||||
<span v-else v-text="decodedTextGlyph"></span>
|
v-else-if="decodedTextGlyphIsHtml"
|
||||||
</span>
|
v-html="decodedTextGlyph"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
v-text="decodedTextGlyph"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
@ -22,38 +28,38 @@ const hugeiconsPrefix = 'hugeicons:'
|
||||||
|
|
||||||
/** Maps config values like hugeicons:CommandLineIcon to Hugeicons icon definitions. */
|
/** Maps config values like hugeicons:CommandLineIcon to Hugeicons icon definitions. */
|
||||||
const hugeiconsRegistry = {
|
const hugeiconsRegistry = {
|
||||||
CommandLineIcon,
|
CommandLineIcon
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
glyph: {
|
glyph: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: '',
|
default: ''
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const hugeiconsModel = computed(() => {
|
const hugeiconsModel = computed(() => {
|
||||||
if (!props.glyph) {
|
if (!props.glyph) {
|
||||||
return CommandLineIcon
|
return CommandLineIcon
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.glyph.startsWith(hugeiconsPrefix)) {
|
if (!props.glyph.startsWith(hugeiconsPrefix)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = props.glyph.slice(hugeiconsPrefix.length)
|
const name = props.glyph.slice(hugeiconsPrefix.length)
|
||||||
const iconModel = hugeiconsRegistry[name]
|
const iconModel = hugeiconsRegistry[name]
|
||||||
|
|
||||||
return iconModel ?? CommandLineIcon
|
return iconModel ?? CommandLineIcon
|
||||||
})
|
})
|
||||||
|
|
||||||
const decodedTextGlyph = computed(() => {
|
const decodedTextGlyph = computed(() => {
|
||||||
if (hugeiconsModel.value) {
|
if (hugeiconsModel.value) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
return decodeHtmlEntities(props.glyph)
|
return decodeHtmlEntities(props.glyph)
|
||||||
})
|
})
|
||||||
|
|
||||||
const decodedTextGlyphIsHtml = computed(() => glyphLooksLikeHtml(decodedTextGlyph.value))
|
const decodedTextGlyphIsHtml = computed(() => glyphLooksLikeHtml(decodedTextGlyph.value))
|
||||||
|
|
|
||||||
|
|
@ -1,86 +1,91 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class = "statusClass + ' annotation'">
|
<div :class="statusClass + ' annotation'">
|
||||||
<router-link v-if="showQueueLink" to="/logs/queue" class="queue-status-link">{{ statusText }}</router-link>
|
<router-link
|
||||||
<span v-else>{{ statusText }}</span><span>{{ exitCodeText }}</span>
|
v-if="showQueueLink"
|
||||||
</div>
|
to="/logs/queue"
|
||||||
|
class="queue-status-link"
|
||||||
|
>
|
||||||
|
{{ statusText }}
|
||||||
|
</router-link>
|
||||||
|
<span v-else>{{ statusText }}</span><span>{{ exitCodeText }}</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
logEntry: {
|
logEntry: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
linkQueuedStatus: {
|
linkQueuedStatus: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function isWaitingInQueue(logEntry) {
|
function isWaitingInQueue (logEntry) {
|
||||||
return logEntry &&
|
return logEntry &&
|
||||||
!logEntry.executionFinished &&
|
!logEntry.executionFinished &&
|
||||||
!logEntry.executionStarted
|
!logEntry.executionStarted
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusText = computed(() => {
|
const statusText = computed(() => {
|
||||||
const logEntry = props.logEntry
|
const logEntry = props.logEntry
|
||||||
if (!logEntry) return 'unknown'
|
if (!logEntry) return 'unknown'
|
||||||
|
|
||||||
if (logEntry.executionFinished) {
|
if (logEntry.executionFinished) {
|
||||||
if (logEntry.blocked) {
|
if (logEntry.blocked) {
|
||||||
return 'Blocked'
|
return 'Blocked'
|
||||||
} else if (logEntry.timedOut) {
|
} else if (logEntry.timedOut) {
|
||||||
return 'Timed out'
|
return 'Timed out'
|
||||||
} else {
|
} else {
|
||||||
return 'Completed'
|
return 'Completed'
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isWaitingInQueue(logEntry)) {
|
if (isWaitingInQueue(logEntry)) {
|
||||||
return 'Queued'
|
return 'Queued'
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Still running...'
|
return 'Still running...'
|
||||||
})
|
})
|
||||||
|
|
||||||
const exitCodeText = computed(() => {
|
const exitCodeText = computed(() => {
|
||||||
const logEntry = props.logEntry
|
const logEntry = props.logEntry
|
||||||
if (!logEntry) return ''
|
if (!logEntry) return ''
|
||||||
if (logEntry.exitCode === 0) {
|
if (logEntry.exitCode === 0) {
|
||||||
return ''
|
|
||||||
}
|
|
||||||
if (logEntry.executionFinished) {
|
|
||||||
if (logEntry.blocked || logEntry.timedOut) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
return ' (Exit code: ' + logEntry.exitCode + ')'
|
|
||||||
}
|
|
||||||
return ''
|
return ''
|
||||||
|
}
|
||||||
|
if (logEntry.executionFinished) {
|
||||||
|
if (logEntry.blocked || logEntry.timedOut) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return ' (Exit code: ' + logEntry.exitCode + ')'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const showQueueLink = computed(() => {
|
const showQueueLink = computed(() => {
|
||||||
return props.linkQueuedStatus && isWaitingInQueue(props.logEntry)
|
return props.linkQueuedStatus && isWaitingInQueue(props.logEntry)
|
||||||
})
|
})
|
||||||
|
|
||||||
const statusClass = computed(() => {
|
const statusClass = computed(() => {
|
||||||
const logEntry = props.logEntry
|
const logEntry = props.logEntry
|
||||||
if (!logEntry) return ''
|
if (!logEntry) return ''
|
||||||
if (logEntry.executionFinished) {
|
if (logEntry.executionFinished) {
|
||||||
if (logEntry.blocked) {
|
if (logEntry.blocked) {
|
||||||
return 'status-blocked'
|
return 'status-blocked'
|
||||||
} else if (logEntry.timedOut) {
|
} else if (logEntry.timedOut) {
|
||||||
return 'status-timeout'
|
return 'status-timeout'
|
||||||
} else if (logEntry.exitCode === 0) {
|
} else if (logEntry.exitCode === 0) {
|
||||||
return 'status-success'
|
return 'status-success'
|
||||||
} else {
|
} else {
|
||||||
return 'status-nonzero-exit'
|
return 'status-nonzero-exit'
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ''
|
}
|
||||||
|
return ''
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -110,5 +115,4 @@ const statusClass = computed(() => {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,50 @@
|
||||||
<template>
|
<template>
|
||||||
<div id = "breadcrumbs">
|
<div id="breadcrumbs">
|
||||||
<template v-for="(link, index) in links" :key="link.name">
|
<template
|
||||||
<router-link :to="link.href">{{ link.name }}</router-link>
|
v-for="(link, index) in links"
|
||||||
<span v-if="index < links.length - 1" class="separator">
|
:key="link.name"
|
||||||
»
|
>
|
||||||
</span>
|
<router-link :to="link.href">
|
||||||
</template>
|
{{ link.name }}
|
||||||
</div>
|
</router-link>
|
||||||
|
<span
|
||||||
|
v-if="index < links.length - 1"
|
||||||
|
class="separator"
|
||||||
|
>
|
||||||
|
»
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</template>
|
</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>
|
<style scoped>
|
||||||
span {
|
span {
|
||||||
color: #bbb;
|
color: #bbb;
|
||||||
|
|
@ -26,34 +62,3 @@ a:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</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>
|
<template>
|
||||||
<div class="choice-checklist" :id="wrapperId">
|
<div
|
||||||
|
:id="wrapperId"
|
||||||
|
class="choice-checklist"
|
||||||
|
>
|
||||||
<div class="choice-checklist-controls">
|
<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
|
Select all
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="choice-checklist-control" @click="selectNone">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="choice-checklist-control"
|
||||||
|
@click="selectNone"
|
||||||
|
>
|
||||||
Select none
|
Select none
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<fieldset class="choice-checklist-fieldset">
|
<fieldset class="choice-checklist-fieldset">
|
||||||
<legend class="visually-hidden">{{ label || name }}</legend>
|
<legend class="visually-hidden">
|
||||||
|
{{ label || name }}
|
||||||
|
</legend>
|
||||||
<label
|
<label
|
||||||
v-for="(choice, index) in choices"
|
v-for="(choice, index) in choices"
|
||||||
:key="choice.value"
|
:key="choice.value"
|
||||||
|
|
@ -21,7 +34,7 @@
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="isSelected(choice.value)"
|
:checked="isSelected(choice.value)"
|
||||||
@change="handleToggle(choice.value)"
|
@change="handleToggle(choice.value)"
|
||||||
/>
|
>
|
||||||
<span>{{ choiceLabel(choice) }}</span>
|
<span>{{ choiceLabel(choice) }}</span>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
@ -34,7 +47,7 @@
|
||||||
:required="required"
|
:required="required"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -86,23 +99,23 @@ function optionId (index) {
|
||||||
return argumentFieldOptionId(props.name, index)
|
return argumentFieldOptionId(props.name, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSelected(value) {
|
function isSelected (value) {
|
||||||
return selectedValues.value.includes(value)
|
return selectedValues.value.includes(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitSelection(selected) {
|
function emitSelection (selected) {
|
||||||
emit('update:modelValue', formatChecklistValue(selected))
|
emit('update:modelValue', formatChecklistValue(selected))
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleToggle(value) {
|
function handleToggle (value) {
|
||||||
emitSelection(toggleChoice(selectedValues.value, value))
|
emitSelection(toggleChoice(selectedValues.value, value))
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectAll() {
|
function selectAll () {
|
||||||
emitSelection(allChoiceValues(props.choices))
|
emitSelection(allChoiceValues(props.choices))
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectNone() {
|
function selectNone () {
|
||||||
emitSelection([])
|
emitSelection([])
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="choice-combobox" ref="rootRef">
|
<div
|
||||||
|
ref="rootRef"
|
||||||
|
class="choice-combobox"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
ref="searchInputRef"
|
|
||||||
:id="id"
|
:id="id"
|
||||||
|
ref="searchInputRef"
|
||||||
type="text"
|
type="text"
|
||||||
class="choice-combobox-input"
|
class="choice-combobox-input"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
|
|
@ -17,12 +20,12 @@
|
||||||
@input="handleSearchInput"
|
@input="handleSearchInput"
|
||||||
@keydown="handleKeydown"
|
@keydown="handleKeydown"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
/>
|
>
|
||||||
<input
|
<input
|
||||||
:name="name"
|
:name="name"
|
||||||
type="hidden"
|
type="hidden"
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
/>
|
>
|
||||||
<ul
|
<ul
|
||||||
v-if="isOpen && filteredChoices.length > 0"
|
v-if="isOpen && filteredChoices.length > 0"
|
||||||
:id="listboxId"
|
:id="listboxId"
|
||||||
|
|
@ -44,7 +47,10 @@
|
||||||
{{ choiceLabel(choice) }}
|
{{ choiceLabel(choice) }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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
|
No matching options
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -140,11 +146,11 @@ watch([() => props.modelValue, () => props.choices], () => {
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
function choiceLabel(choice) {
|
function choiceLabel (choice) {
|
||||||
return choiceDisplayLabel(choice)
|
return choiceDisplayLabel(choice)
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncFromModelValue() {
|
function syncFromModelValue () {
|
||||||
const next = syncStateFromModelValue(props.choices, props.modelValue)
|
const next = syncStateFromModelValue(props.choices, props.modelValue)
|
||||||
query.value = next.query
|
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)
|
const index = choices.findIndex(choice => choice.value === props.modelValue)
|
||||||
return index >= 0 ? index : 0
|
return index >= 0 ? index : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function openList() {
|
function openList () {
|
||||||
document.dispatchEvent(new CustomEvent(closeOthersEvent, { detail: { id: props.id } }))
|
document.dispatchEvent(new CustomEvent(closeOthersEvent, { detail: { id: props.id } }))
|
||||||
const wasClosed = !isOpen.value
|
const wasClosed = !isOpen.value
|
||||||
isOpen.value = true
|
isOpen.value = true
|
||||||
|
|
@ -167,23 +173,23 @@ function openList() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeList() {
|
function closeList () {
|
||||||
isOpen.value = false
|
isOpen.value = false
|
||||||
isUserFiltering.value = false
|
isUserFiltering.value = false
|
||||||
syncFromModelValue()
|
syncFromModelValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitValue(value) {
|
function emitValue (value) {
|
||||||
emit('update:modelValue', value)
|
emit('update:modelValue', value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectChoice(choice) {
|
function selectChoice (choice) {
|
||||||
emitValue(choice.value)
|
emitValue(choice.value)
|
||||||
query.value = choiceLabel(choice)
|
query.value = choiceLabel(choice)
|
||||||
isOpen.value = false
|
isOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFocus() {
|
function handleFocus () {
|
||||||
if (!isOpen.value) {
|
if (!isOpen.value) {
|
||||||
syncFromModelValue()
|
syncFromModelValue()
|
||||||
isUserFiltering.value = false
|
isUserFiltering.value = false
|
||||||
|
|
@ -192,14 +198,14 @@ function handleFocus() {
|
||||||
openList()
|
openList()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearchInput(event) {
|
function handleSearchInput (event) {
|
||||||
isUserFiltering.value = true
|
isUserFiltering.value = true
|
||||||
query.value = event.target.value
|
query.value = event.target.value
|
||||||
openList()
|
openList()
|
||||||
highlightedIndex.value = 0
|
highlightedIndex.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveHighlight(delta) {
|
function moveHighlight (delta) {
|
||||||
if (filteredChoices.value.length === 0) {
|
if (filteredChoices.value.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -218,7 +224,7 @@ function moveHighlight(delta) {
|
||||||
highlightedIndex.value = nextIndex
|
highlightedIndex.value = nextIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeydown(event) {
|
function handleKeydown (event) {
|
||||||
if (event.key === 'ArrowDown') {
|
if (event.key === 'ArrowDown') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const wasOpen = isOpen.value
|
const wasOpen = isOpen.value
|
||||||
|
|
@ -258,17 +264,17 @@ function handleKeydown(event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBlur() {
|
function handleBlur () {
|
||||||
closeList()
|
closeList()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCloseOthers(event) {
|
function handleCloseOthers (event) {
|
||||||
if (event.detail.id !== props.id) {
|
if (event.detail.id !== props.id) {
|
||||||
closeList()
|
closeList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOutsideMouseDown(event) {
|
function handleOutsideMouseDown (event) {
|
||||||
if (!isOpen.value || rootRef.value?.contains(event.target)) {
|
if (!isOpen.value || rootRef.value?.contains(event.target)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<span id="connection-banner" v-if="connectionState.showDisconnectedBanner" class="inline-notification critical user-info-connection">
|
<span
|
||||||
<span class="connection-banner-sr-only" role="status">{{ staticAnnouncement }}</span>
|
v-if="connectionState.showDisconnectedBanner"
|
||||||
<span aria-hidden="true">
|
id="connection-banner"
|
||||||
<a :href="websocketDocsUrl" target="_blank" rel="noopener noreferrer" class="connection-banner-link">{{ linkText }}</a>{{ bannerSuffix }}
|
class="inline-notification critical user-info-connection"
|
||||||
</span>
|
>
|
||||||
|
<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>
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
@ -18,7 +30,7 @@ const websocketDocsUrl = 'https://docs.olivetin.app/troubleshooting/err-websocke
|
||||||
|
|
||||||
const linkText = computed(() => t('disconnected-banner-link-text'))
|
const linkText = computed(() => t('disconnected-banner-link-text'))
|
||||||
|
|
||||||
function formatShortRelative(ms) {
|
function formatShortRelative (ms) {
|
||||||
if (ms < 0) return '0s'
|
if (ms < 0) return '0s'
|
||||||
const secs = Math.floor(ms / 1000)
|
const secs = Math.floor(ms / 1000)
|
||||||
const mins = Math.floor(secs / 60)
|
const mins = Math.floor(secs / 60)
|
||||||
|
|
@ -28,7 +40,7 @@ function formatShortRelative(ms) {
|
||||||
return `${secs}s`
|
return `${secs}s`
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatShortTime(ts) {
|
function formatShortTime (ts) {
|
||||||
if (ts == null) return '--:--'
|
if (ts == null) return '--:--'
|
||||||
return new Date(ts).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
|
return new Date(ts).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,39 @@
|
||||||
<template>
|
<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-else-if="component.type == 'fieldset'">
|
||||||
<template v-for="subcomponent in component.contents" :key="subcomponent.title">
|
<template
|
||||||
<DashboardComponent :component="subcomponent" />
|
v-for="subcomponent in component.contents"
|
||||||
</template>
|
:key="subcomponent.title"
|
||||||
|
>
|
||||||
|
<DashboardComponent :component="subcomponent" />
|
||||||
</template>
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
OTHER: {{ component.type }}
|
OTHER: {{ component.type }}
|
||||||
{{ component }}
|
{{ component }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
@ -26,10 +42,10 @@ import DashboardComponentMostRecentExecution from './DashboardComponentMostRecen
|
||||||
import DashboardComponentDirectory from './DashboardComponentDirectory.vue'
|
import DashboardComponentDirectory from './DashboardComponentDirectory.vue'
|
||||||
import DashboardComponentDisplay from './DashboardComponentDisplay.vue'
|
import DashboardComponentDisplay from './DashboardComponentDisplay.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
defineProps({
|
||||||
component: {
|
component: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<button class="directory-button" :class="component.cssClass" @click="navigateToDirectory">
|
<button
|
||||||
<span class="icon" v-html="unicodeIcon"></span>
|
class="directory-button"
|
||||||
<span class="title">{{ component.title }}</span>
|
:class="component.cssClass"
|
||||||
</button>
|
@click="navigateToDirectory"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
v-html="unicodeIcon"
|
||||||
|
/>
|
||||||
|
<span class="title">{{ component.title }}</span>
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
@ -12,33 +19,33 @@ import { computed } from 'vue'
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
component: {
|
component: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function getUnicodeIcon(icon) {
|
function getUnicodeIcon (icon) {
|
||||||
if (icon === '' || !icon) {
|
if (icon === '' || !icon) {
|
||||||
return '📁' // Default folder icon
|
return '📁' // Default folder icon
|
||||||
} else {
|
} else {
|
||||||
return unescape(icon)
|
return unescape(icon)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const unicodeIcon = computed(() => {
|
const unicodeIcon = computed(() => {
|
||||||
return getUnicodeIcon(props.component.icon)
|
return getUnicodeIcon(props.component.icon)
|
||||||
})
|
})
|
||||||
|
|
||||||
function navigateToDirectory() {
|
function navigateToDirectory () {
|
||||||
const params = { title: props.component.title }
|
const params = { title: props.component.title }
|
||||||
|
|
||||||
if (props.component.entityType && props.component.entityKey) {
|
if (props.component.entityType && props.component.entityKey) {
|
||||||
params.entityType = props.component.entityType
|
params.entityType = props.component.entityType
|
||||||
params.entityKey = props.component.entityKey
|
params.entityKey = props.component.entityKey
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push({ name: 'Dashboard', params })
|
router.push({ name: 'Dashboard', params })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="display" :class="component.cssClass">
|
<div
|
||||||
<div v-html="component.title" />
|
class="display"
|
||||||
</div>
|
:class="component.cssClass"
|
||||||
|
>
|
||||||
|
<div v-html="component.title" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
const props = defineProps({
|
defineProps({
|
||||||
component: {
|
component: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mre-container" :class="component.cssClass">
|
<div
|
||||||
|
class="mre-container"
|
||||||
|
:class="component.cssClass"
|
||||||
|
>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="executionTrackingId"
|
v-if="executionTrackingId"
|
||||||
:to="`/logs/${executionTrackingId}`"
|
:to="`/logs/${executionTrackingId}`"
|
||||||
class="mre-link"
|
class="mre-link"
|
||||||
>
|
>
|
||||||
<pre class="mre-output">{{ output }}</pre>
|
<pre class="mre-output">{{ output }}</pre>
|
||||||
</router-link>
|
</router-link>
|
||||||
<pre v-else class="mre-output fg-important">{{ output }}</pre>
|
<pre
|
||||||
|
v-else
|
||||||
|
class="mre-output fg-important"
|
||||||
|
>{{ output }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -26,7 +32,7 @@ const output = ref('Waiting...')
|
||||||
const executionTrackingId = ref(null)
|
const executionTrackingId = ref(null)
|
||||||
let unwatchButtonResults = null
|
let unwatchButtonResults = null
|
||||||
|
|
||||||
function updateFromLogEntry(logEntry) {
|
function updateFromLogEntry (logEntry) {
|
||||||
if (logEntry) {
|
if (logEntry) {
|
||||||
if (logEntry.output !== undefined) {
|
if (logEntry.output !== undefined) {
|
||||||
output.value = logEntry.output
|
output.value = logEntry.output
|
||||||
|
|
@ -39,7 +45,7 @@ function updateFromLogEntry(logEntry) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchMostRecentExecution() {
|
async function fetchMostRecentExecution () {
|
||||||
if (!props.component.title) {
|
if (!props.component.title) {
|
||||||
output.value = 'Error: No action ID specified'
|
output.value = 'Error: No action ID specified'
|
||||||
executionTrackingId.value = null
|
executionTrackingId.value = null
|
||||||
|
|
|
||||||
|
|
@ -1,167 +1,194 @@
|
||||||
<template>
|
<template>
|
||||||
<Section :padding="!hasTable">
|
<Section :padding="!hasTable">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="section-title-with-icon">
|
<span class="section-title-with-icon">
|
||||||
Entity:
|
Entity:
|
||||||
<ActionIconGlyph v-if="definition.icon" class="entity-title-icon" :glyph="definition.icon" />
|
<ActionIconGlyph
|
||||||
{{ definition.title }}
|
v-if="definition.icon"
|
||||||
</span>
|
class="entity-title-icon"
|
||||||
</template>
|
:glyph="definition.icon"
|
||||||
<template v-if="hasTable" #toolbar>
|
/>
|
||||||
<EntityListFilter v-model="searchText" />
|
{{ definition.title }}
|
||||||
</template>
|
</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">
|
<template v-if="hasTable">
|
||||||
<p v-if="tableError" class="table-error padding" role="alert">{{ tableError }}</p>
|
<p
|
||||||
<EntityInstancesTable
|
v-if="tableError"
|
||||||
v-else
|
class="table-error padding"
|
||||||
:instances="tableInstances"
|
role="alert"
|
||||||
:properties="definition.properties"
|
>
|
||||||
:total-instances="totalInstances"
|
{{ tableError }}
|
||||||
v-model:page="currentPage"
|
</p>
|
||||||
v-model:page-size="pageSize"
|
<EntityInstancesTable
|
||||||
/>
|
v-else
|
||||||
</template>
|
v-model:page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:instances="tableInstances"
|
||||||
|
:properties="definition.properties"
|
||||||
|
:total-instances="totalInstances"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<ul v-else>
|
<ul v-else>
|
||||||
<li v-for="inst in definition.instances" :key="inst.uniqueKey">
|
<li
|
||||||
<router-link :to="entityDetailsRoute(inst)">
|
v-for="inst in definition.instances"
|
||||||
{{ inst.title }}
|
:key="inst.uniqueKey"
|
||||||
</router-link>
|
>
|
||||||
</li>
|
<router-link :to="entityDetailsRoute(inst)">
|
||||||
</ul>
|
{{ inst.title }}
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<div v-if="usedDashboards.length > 0" :class="{ padding: hasTable }">
|
<div
|
||||||
<h3>Used on Dashboards:</h3>
|
v-if="usedDashboards.length > 0"
|
||||||
<ul>
|
:class="{ padding: hasTable }"
|
||||||
<li v-for="dash in usedDashboards" :key="dash">
|
>
|
||||||
<template v-if="isEntityDirectory(dash)">
|
<h3>Used on Dashboards:</h3>
|
||||||
{{ getDashboardTitle(dash) }} <span class="entity-directory-label">[Entity Directory]</span>
|
<ul>
|
||||||
</template>
|
<li
|
||||||
<router-link v-else-if="!dash.includes('entity:')" :to="{ name: 'Dashboard', params: { title: getDashboardTitle(dash) } }">
|
v-for="dash in usedDashboards"
|
||||||
{{ getDashboardTitle(dash) }}
|
:key="dash"
|
||||||
</router-link>
|
>
|
||||||
<span v-else>{{ dash }}</span>
|
<template v-if="isEntityDirectory(dash)">
|
||||||
</li>
|
{{ getDashboardTitle(dash) }} <span class="entity-directory-label">[Entity Directory]</span>
|
||||||
</ul>
|
</template>
|
||||||
</div>
|
<router-link
|
||||||
</Section>
|
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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import Section from 'picocrank/vue/components/Section.vue'
|
import Section from 'picocrank/vue/components/Section.vue'
|
||||||
import ActionIconGlyph from './ActionIconGlyph.vue'
|
import ActionIconGlyph from './ActionIconGlyph.vue'
|
||||||
import EntityInstancesTable from './EntityInstancesTable.vue'
|
import EntityInstancesTable from './EntityInstancesTable.vue'
|
||||||
import EntityListFilter from './EntityListFilter.vue'
|
import EntityListFilter from './EntityListFilter.vue'
|
||||||
import { entityDetailsRoute } from '../utils/entityRoutes.js'
|
import { entityDetailsRoute } from '../utils/entityRoutes.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
definition: {
|
definition: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
const tableInstances = ref([])
|
const tableInstances = ref([])
|
||||||
const totalInstances = ref(0)
|
const totalInstances = ref(0)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
const tableError = ref('')
|
const tableError = ref('')
|
||||||
let fetchTimer = null
|
let fetchTimer = null
|
||||||
let fetchSequence = 0
|
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, () => {
|
watch(searchText, () => {
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
scheduleFetchTableInstances()
|
scheduleFetchTableInstances()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch([currentPage, pageSize], () => {
|
watch([currentPage, pageSize], () => {
|
||||||
scheduleFetchTableInstances()
|
scheduleFetchTableInstances()
|
||||||
})
|
})
|
||||||
|
|
||||||
function filteredDashboards(dashboards) {
|
function filteredDashboards (dashboards) {
|
||||||
return dashboards.filter(d => d && !d.includes('{{'))
|
return dashboards.filter(d => d && !d.includes('{{'))
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEntityDirectory(dashboardTitle) {
|
function isEntityDirectory (dashboardTitle) {
|
||||||
return dashboardTitle.endsWith(' [Entity Directory]')
|
return dashboardTitle.endsWith(' [Entity Directory]')
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDashboardTitle(dashboardTitle) {
|
function getDashboardTitle (dashboardTitle) {
|
||||||
if (isEntityDirectory(dashboardTitle)) {
|
if (isEntityDirectory(dashboardTitle)) {
|
||||||
return dashboardTitle.slice(0, -' [Entity Directory]'.length)
|
return dashboardTitle.slice(0, -' [Entity Directory]'.length)
|
||||||
}
|
}
|
||||||
return dashboardTitle
|
return dashboardTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleFetchTableInstances() {
|
function scheduleFetchTableInstances () {
|
||||||
if (!hasTable.value) {
|
if (!hasTable.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fetchTimer) {
|
if (fetchTimer) {
|
||||||
clearTimeout(fetchTimer)
|
clearTimeout(fetchTimer)
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchTimer = setTimeout(() => {
|
fetchTimer = setTimeout(() => {
|
||||||
fetchTableInstances()
|
fetchTableInstances()
|
||||||
}, 250)
|
}, 250)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchTableInstances() {
|
async function fetchTableInstances () {
|
||||||
if (!hasTable.value) {
|
if (!hasTable.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestId = ++fetchSequence
|
const requestId = ++fetchSequence
|
||||||
tableError.value = ''
|
tableError.value = ''
|
||||||
try {
|
try {
|
||||||
const response = await window.client.getEntities({
|
const response = await window.client.getEntities({
|
||||||
entityType: props.definition.title,
|
entityType: props.definition.title,
|
||||||
filter: searchText.value.trim(),
|
filter: searchText.value.trim(),
|
||||||
page: currentPage.value,
|
page: currentPage.value,
|
||||||
pageSize: pageSize.value
|
pageSize: pageSize.value
|
||||||
})
|
})
|
||||||
|
|
||||||
if (requestId !== fetchSequence) {
|
if (requestId !== fetchSequence) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const definition = response.entityDefinitions?.find(def => def.title === props.definition.title)
|
const definition = response.entityDefinitions?.find(def => def.title === props.definition.title)
|
||||||
tableInstances.value = definition?.instances ?? []
|
tableInstances.value = definition?.instances ?? []
|
||||||
totalInstances.value = definition?.totalInstances ?? 0
|
totalInstances.value = definition?.totalInstances ?? 0
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (requestId !== fetchSequence) {
|
if (requestId !== fetchSequence) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('Failed to fetch entity instances:', err)
|
console.error('Failed to fetch entity instances:', err)
|
||||||
tableError.value = 'Failed to load entity instances.'
|
tableError.value = 'Failed to load entity instances.'
|
||||||
tableInstances.value = []
|
tableInstances.value = []
|
||||||
totalInstances.value = 0
|
totalInstances.value = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (hasTable.value) {
|
if (hasTable.value) {
|
||||||
fetchTableInstances()
|
fetchTableInstances()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (fetchTimer) {
|
if (fetchTimer) {
|
||||||
clearTimeout(fetchTimer)
|
clearTimeout(fetchTimer)
|
||||||
fetchTimer = null
|
fetchTimer = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -1,87 +1,90 @@
|
||||||
<template>
|
<template>
|
||||||
<Table
|
<Table
|
||||||
:data="tableRows"
|
:data="tableRows"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
:show-pagination="false"
|
:show-pagination="false"
|
||||||
>
|
>
|
||||||
<template #cell-title="{ row, value }">
|
<template #cell-title="{ row, value }">
|
||||||
<router-link :to="entityDetailsRoute(row)">
|
<router-link :to="entityDetailsRoute(row)">
|
||||||
{{ value }}
|
{{ value }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<div v-if="totalInstances > 0" class="padding">
|
<div
|
||||||
<Pagination
|
v-if="totalInstances > 0"
|
||||||
:total="totalInstances"
|
class="padding"
|
||||||
v-model:page="currentPageModel"
|
>
|
||||||
v-model:page-size="pageSizeModel"
|
<Pagination
|
||||||
item-title="entities"
|
v-model:page="currentPageModel"
|
||||||
/>
|
v-model:page-size="pageSizeModel"
|
||||||
</div>
|
:total="totalInstances"
|
||||||
|
item-title="entities"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import Table from 'picocrank/vue/components/Table.vue'
|
import Table from 'picocrank/vue/components/Table.vue'
|
||||||
import Pagination from 'picocrank/vue/components/Pagination.vue'
|
import Pagination from 'picocrank/vue/components/Pagination.vue'
|
||||||
import { entityDetailsRoute } from '../utils/entityRoutes.js'
|
import { entityDetailsRoute } from '../utils/entityRoutes.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instances: {
|
instances: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
totalInstances: {
|
totalInstances: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
},
|
},
|
||||||
page: {
|
page: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1
|
default: 1
|
||||||
},
|
},
|
||||||
pageSize: {
|
pageSize: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 10
|
default: 10
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:page', 'update:pageSize'])
|
const emit = defineEmits(['update:page', 'update:pageSize'])
|
||||||
|
|
||||||
const headers = computed(() => {
|
const headers = computed(() => {
|
||||||
const propertyHeaders = props.properties.map(property => ({
|
const propertyHeaders = props.properties.map(property => ({
|
||||||
key: property.name,
|
key: property.name,
|
||||||
label: property.title
|
label: property.title
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ key: 'title', label: 'Name' },
|
{ key: 'title', label: 'Name' },
|
||||||
...propertyHeaders
|
...propertyHeaders
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const tableRows = computed(() =>
|
const tableRows = computed(() =>
|
||||||
props.instances.map(instance => ({
|
props.instances.map(instance => ({
|
||||||
...instance.fields,
|
...instance.fields,
|
||||||
title: instance.title,
|
title: instance.title,
|
||||||
type: instance.type,
|
type: instance.type,
|
||||||
uniqueKey: instance.uniqueKey
|
uniqueKey: instance.uniqueKey
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
const currentPageModel = computed({
|
const currentPageModel = computed({
|
||||||
get: () => props.page,
|
get: () => props.page,
|
||||||
set: value => emit('update:page', value)
|
set: value => emit('update:page', value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const pageSizeModel = computed({
|
const pageSizeModel = computed({
|
||||||
get: () => props.pageSize,
|
get: () => props.pageSize,
|
||||||
set: value => emit('update:pageSize', value)
|
set: value => emit('update:pageSize', value)
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,53 @@
|
||||||
<template>
|
<template>
|
||||||
<label class="input-with-icons">
|
<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">
|
<svg
|
||||||
<path fill="currentColor"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
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" />
|
width="1em"
|
||||||
</svg>
|
height="1em"
|
||||||
<input
|
viewBox="0 0 24 24"
|
||||||
:value="modelValue"
|
aria-hidden="true"
|
||||||
aria-label="Filter entities"
|
>
|
||||||
placeholder="Filter entities..."
|
<path
|
||||||
@input="$emit('update:modelValue', $event.target.value)"
|
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"
|
||||||
<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">
|
</svg>
|
||||||
<path fill="currentColor"
|
<input
|
||||||
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" />
|
:value="modelValue"
|
||||||
</svg>
|
aria-label="Filter entities"
|
||||||
</button>
|
placeholder="Filter entities..."
|
||||||
</label>
|
@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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
defineProps({
|
defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
defineEmits(['update:modelValue'])
|
defineEmits(['update:modelValue'])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<span class="log-action-title">
|
<span class="log-action-title">
|
||||||
<slot>{{ actionTitle }}</slot>
|
<slot>{{ actionTitle }}</slot>
|
||||||
<span v-if="justification" class="log-justification">— {{ justification }}</span>
|
<span
|
||||||
|
v-if="justification"
|
||||||
|
class="log-justification"
|
||||||
|
>— {{ justification }}</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
import Dashboard from './Dashboard.vue'
|
import Dashboard from './Dashboard.vue'
|
||||||
|
|
||||||
import { Wrench01Icon } from '@hugeicons/core-free-icons'
|
import { Wrench01Icon, LeftToRightListDashIcon, CellsIcon, DashboardSquare01Icon } 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'
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
|
|
@ -44,8 +41,8 @@ const routes = [
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Logs Calendar',
|
title: 'Logs Calendar',
|
||||||
breadcrumb: [
|
breadcrumb: [
|
||||||
{ name: "Logs", href: "/logs" },
|
{ name: 'Logs', href: '/logs' },
|
||||||
{ name: "Calendar" },
|
{ name: 'Calendar' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -56,8 +53,8 @@ const routes = [
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Execution Queue',
|
title: 'Execution Queue',
|
||||||
breadcrumb: [
|
breadcrumb: [
|
||||||
{ name: "Logs", href: "/logs" },
|
{ name: 'Logs', href: '/logs' },
|
||||||
{ name: "Queue" },
|
{ name: 'Queue' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -78,8 +75,8 @@ const routes = [
|
||||||
meta: {
|
meta: {
|
||||||
title: 'OliveTin - Entity Details',
|
title: 'OliveTin - Entity Details',
|
||||||
breadcrumb: [
|
breadcrumb: [
|
||||||
{ name: "Entities", href: "/entities" },
|
{ name: 'Entities', href: '/entities' },
|
||||||
{ name: "Entity Details" }
|
{ name: 'Entity Details' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -91,8 +88,8 @@ const routes = [
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Execution',
|
title: 'Execution',
|
||||||
breadcrumb: [
|
breadcrumb: [
|
||||||
{ name: "Logs", href: "/logs" },
|
{ name: 'Logs', href: '/logs' },
|
||||||
{ name: "Execution" },
|
{ name: 'Execution' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -104,8 +101,8 @@ const routes = [
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Action Details',
|
title: 'Action Details',
|
||||||
breadcrumb: [
|
breadcrumb: [
|
||||||
{ name: "Actions", href: "/" },
|
{ name: 'Actions', href: '/' },
|
||||||
{ name: "Action Details" },
|
{ name: 'Action Details' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -117,8 +114,8 @@ const routes = [
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Execution conditions',
|
title: 'Execution conditions',
|
||||||
breadcrumb: [
|
breadcrumb: [
|
||||||
{ name: "Actions", href: "/" },
|
{ name: 'Actions', href: '/' },
|
||||||
{ name: "Execution conditions" },
|
{ name: 'Execution conditions' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -155,7 +152,7 @@ const routes = [
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes,
|
routes,
|
||||||
scrollBehavior(to, from, savedPosition) {
|
scrollBehavior (to, from, savedPosition) {
|
||||||
if (savedPosition) {
|
if (savedPosition) {
|
||||||
return savedPosition
|
return savedPosition
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -168,7 +165,7 @@ const router = createRouter({
|
||||||
router.beforeEach((to) => {
|
router.beforeEach((to) => {
|
||||||
if (to.meta && to.meta.title) {
|
if (to.meta && to.meta.title) {
|
||||||
const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
|
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 !== '')
|
return value.split(',').map((segment) => segment.trim()).filter((segment) => segment !== '')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseChecklistValue(value) {
|
export function parseChecklistValue (value) {
|
||||||
if (!value || value === '') {
|
if (!value || value === '') {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
@ -24,7 +24,7 @@ export function parseChecklistValue(value) {
|
||||||
return parseLegacyChecklistValue(value)
|
return parseLegacyChecklistValue(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatChecklistValue(selected) {
|
export function formatChecklistValue (selected) {
|
||||||
if (!Array.isArray(selected) || selected.length === 0) {
|
if (!Array.isArray(selected) || selected.length === 0) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
@ -32,7 +32,7 @@ export function formatChecklistValue(selected) {
|
||||||
return JSON.stringify(selected)
|
return JSON.stringify(selected)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleChoice(selected, value) {
|
export function toggleChoice (selected, value) {
|
||||||
const current = Array.isArray(selected) ? [...selected] : []
|
const current = Array.isArray(selected) ? [...selected] : []
|
||||||
const index = current.indexOf(value)
|
const index = current.indexOf(value)
|
||||||
|
|
||||||
|
|
@ -45,10 +45,10 @@ export function toggleChoice(selected, value) {
|
||||||
return current
|
return current
|
||||||
}
|
}
|
||||||
|
|
||||||
export function choiceLabel(choice) {
|
export function choiceLabel (choice) {
|
||||||
return choice.title || choice.value || ''
|
return choice.title || choice.value || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export function allChoiceValues(choices) {
|
export function allChoiceValues (choices) {
|
||||||
return choices.map((choice) => choice.value)
|
return choices.map((choice) => choice.value)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export function applyArgumentTemplate(template, args) {
|
export function applyArgumentTemplate (template, args) {
|
||||||
if (!template) {
|
if (!template) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
@ -6,10 +6,10 @@ export function applyArgumentTemplate(template, args) {
|
||||||
return template.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, name) => args[name] ?? '')
|
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
|
return (justification ?? '').length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export function actionJustificationTemplate(justification) {
|
export function actionJustificationTemplate (justification) {
|
||||||
return justification ?? ''
|
return justification ?? ''
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
export function readPrefilledArgumentsFromNavigation (historyState = globalThis.window?.history?.state) {
|
export function readPrefilledArgumentsFromNavigation (historyState = globalThis.window?.history?.state) {
|
||||||
if (historyState?.prefilledArguments && typeof historyState.prefilledArguments === 'object') {
|
if (historyState?.prefilledArguments && typeof historyState.prefilledArguments === 'object') {
|
||||||
return { ...historyState.prefilledArguments }
|
return { ...historyState.prefilledArguments }
|
||||||
}
|
}
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getInitialArgumentValue (paramName, prefilledArguments = {}, search = globalThis.window?.location?.search ?? '') {
|
export function getInitialArgumentValue (paramName, prefilledArguments = {}, search = globalThis.window?.location?.search ?? '') {
|
||||||
const safePrefilledArguments = prefilledArguments && typeof prefilledArguments === 'object'
|
const safePrefilledArguments = prefilledArguments && typeof prefilledArguments === 'object'
|
||||||
? prefilledArguments
|
? prefilledArguments
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(safePrefilledArguments, paramName)) {
|
if (Object.prototype.hasOwnProperty.call(safePrefilledArguments, paramName)) {
|
||||||
return safePrefilledArguments[paramName]
|
return safePrefilledArguments[paramName]
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams(search)
|
const params = new URLSearchParams(search)
|
||||||
return params.get(paramName)
|
return params.get(paramName)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,130 +1,211 @@
|
||||||
<template>
|
<template>
|
||||||
<Section :padding="false">
|
<Section :padding="false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="section-title-with-icon">
|
<span class="section-title-with-icon">
|
||||||
Action Details:
|
Action Details:
|
||||||
<ActionIconGlyph v-if="action" class="action-title-icon" :glyph="action.icon" />
|
<ActionIconGlyph
|
||||||
{{ actionTitle }}
|
v-if="action"
|
||||||
</span>
|
class="action-title-icon"
|
||||||
</template>
|
:glyph="action.icon"
|
||||||
<template #toolbar>
|
/>
|
||||||
<div class="action-details-toolbar">
|
{{ actionTitle }}
|
||||||
<button
|
</span>
|
||||||
v-for="dashboard in backToDashboards"
|
</template>
|
||||||
:key="dashboard.path"
|
<template #toolbar>
|
||||||
@click="goToDashboard(dashboard.path)"
|
<div class="action-details-toolbar">
|
||||||
:title="'Back to ' + dashboard.title"
|
<button
|
||||||
class="button neutral"
|
v-for="dashboard in backToDashboards"
|
||||||
>
|
:key="dashboard.path"
|
||||||
<HugeiconsIcon :icon="DashboardSquare01Icon" />
|
:title="'Back to ' + dashboard.title"
|
||||||
{{ dashboard.title }}
|
class="button neutral"
|
||||||
</button>
|
@click="goToDashboard(dashboard.path)"
|
||||||
<button v-if="action" @click="startAction" title="Run this action" class="button neutral">
|
>
|
||||||
<HugeiconsIcon :icon="WorkoutRunIcon" />
|
<HugeiconsIcon :icon="DashboardSquare01Icon" />
|
||||||
Run
|
{{ dashboard.title }}
|
||||||
</button>
|
</button>
|
||||||
<router-link
|
<button
|
||||||
v-if="action"
|
v-if="action"
|
||||||
:to="{ name: 'ActionExecConditions', params: { actionId: route.params.actionId } }"
|
title="Run this action"
|
||||||
class="button neutral"
|
class="button neutral"
|
||||||
title="View configured automatic triggers and on-demand execution"
|
@click="startAction"
|
||||||
>
|
>
|
||||||
Execution conditions ({{ executionConditionCount }})
|
<HugeiconsIcon :icon="WorkoutRunIcon" />
|
||||||
</router-link>
|
Run
|
||||||
</div>
|
</button>
|
||||||
</template>
|
<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
|
||||||
<div class = "fg1">
|
v-if="action"
|
||||||
<dl>
|
class="flex-row padding"
|
||||||
<dt>Timeout</dt>
|
>
|
||||||
<dd>{{ action.timeout }} seconds</dd>
|
<div class="fg1">
|
||||||
|
<dl>
|
||||||
|
<dt>Timeout</dt>
|
||||||
|
<dd>{{ action.timeout }} seconds</dd>
|
||||||
|
|
||||||
<template v-if="actionGroups.length > 0">
|
<template v-if="actionGroups.length > 0">
|
||||||
<dt>
|
<dt>
|
||||||
<router-link :to="{ name: 'LogsQueue' }" class="action-groups-link">Action groups</router-link>
|
<router-link
|
||||||
</dt>
|
:to="{ name: 'LogsQueue' }"
|
||||||
<dd>
|
class="action-groups-link"
|
||||||
<ul class="action-group-list">
|
>
|
||||||
<li v-for="group in actionGroups" :key="group.name" class="action-group-row">
|
Action groups
|
||||||
<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
|
</router-link>
|
||||||
:max-concurrent="group.maxConcurrent"
|
</dt>
|
||||||
:queue-size="group.queueSize"
|
<dd>
|
||||||
/>
|
<ul class="action-group-list">
|
||||||
</li>
|
<li
|
||||||
</ul>
|
v-for="group in actionGroups"
|
||||||
</dd>
|
:key="group.name"
|
||||||
</template>
|
class="action-group-row"
|
||||||
</dl>
|
>
|
||||||
<p class = "fg1">
|
<router-link
|
||||||
Execution history for this action. You can filter by execution tracking ID.
|
:to="{ name: 'LogsQueue' }"
|
||||||
</p>
|
class="action-groups-link action-group-name"
|
||||||
</div>
|
>
|
||||||
<div style = "align-self: start; text-align: right;">
|
{{ group.name }}
|
||||||
<div class="filter-container">
|
</router-link><template v-if="group.maxConcurrent > 0 && group.queueSize > 0">
|
||||||
<label class="input-with-icons">
|
-
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
</template><ActionGroupLimitsLabel
|
||||||
<path fill="currentColor"
|
:max-concurrent="group.maxConcurrent"
|
||||||
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" />
|
: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>
|
</svg>
|
||||||
<input placeholder="Filter current page" v-model="searchText" />
|
</button>
|
||||||
<button title="Clear search filter" :disabled="!searchText" @click="clearSearch">
|
</label>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-show="filteredLogs.length > 0">
|
<div v-show="filteredLogs.length > 0">
|
||||||
<table class="logs-table row-hover">
|
<table class="logs-table row-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Timestamp</th>
|
<th>Timestamp</th>
|
||||||
<th>Duration</th>
|
<th>Duration</th>
|
||||||
<th>Execution ID</th>
|
<th>Execution ID</th>
|
||||||
<th>Metadata</th>
|
<th>Metadata</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="log in filteredLogs" :key="log.executionTrackingId" class="log-row" :title="log.actionTitle">
|
<tr
|
||||||
<td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td>
|
v-for="log in filteredLogs"
|
||||||
<td class="duration">{{ formatExecutionDuration(log) }}</td>
|
:key="log.executionTrackingId"
|
||||||
<td>
|
class="log-row"
|
||||||
<router-link :to="`/logs/${log.executionTrackingId}`">
|
:title="log.actionTitle"
|
||||||
<LogActionTitle :justification="log.justification">
|
>
|
||||||
{{ log.executionTrackingId }}
|
<td class="timestamp">
|
||||||
</LogActionTitle>
|
{{ formatTimestamp(log.datetimeStarted) }}
|
||||||
</router-link>
|
</td>
|
||||||
</td>
|
<td class="duration">
|
||||||
<td class="tags">
|
{{ formatExecutionDuration(log) }}
|
||||||
<span class="annotation">
|
</td>
|
||||||
<span class="annotation-key">User:</span>
|
<td>
|
||||||
<span class="annotation-val">{{ log.user }}</span>
|
<router-link :to="`/logs/${log.executionTrackingId}`">
|
||||||
</span>
|
<LogActionTitle :justification="log.justification">
|
||||||
<span v-if="log.tags && log.tags.length > 0" class="tag-list">
|
{{ log.executionTrackingId }}
|
||||||
<span v-for="tag in log.tags" :key="tag" class="tag">{{ tag }}</span>
|
</LogActionTitle>
|
||||||
</span>
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td class="exit-code">
|
<td class="tags">
|
||||||
<ActionStatusDisplay :logEntry="log" :link-queued-status="true" />
|
<span class="annotation">
|
||||||
</td>
|
<span class="annotation-key">User:</span>
|
||||||
</tr>
|
<span class="annotation-val">{{ log.user }}</span>
|
||||||
</tbody>
|
</span>
|
||||||
</table>
|
<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"
|
<Pagination
|
||||||
@page-size-change="handlePageSizeChange" itemTitle="execution logs" />
|
:page-size="pageSize"
|
||||||
</div>
|
: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">
|
<div
|
||||||
<p>This action has no execution history.</p>
|
v-show="logs.length === 0 && !loading"
|
||||||
<router-link to="/">Return to index</router-link>
|
class="empty-state"
|
||||||
</div>
|
>
|
||||||
|
<p>This action has no execution history.</p>
|
||||||
|
<router-link to="/">
|
||||||
|
Return to index
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -174,16 +255,16 @@ const executionConditionCount = computed(() => countExecutionConditions(action.v
|
||||||
|
|
||||||
const actionGroups = computed(() => action.value?.groups ?? [])
|
const actionGroups = computed(() => action.value?.groups ?? [])
|
||||||
|
|
||||||
async function fetchActionLogs() {
|
async function fetchActionLogs () {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const actionId = route.params.actionId
|
const actionId = route.params.actionId
|
||||||
const startOffset = (currentPage.value - 1) * pageSize.value
|
const startOffset = (currentPage.value - 1) * pageSize.value
|
||||||
|
|
||||||
const args = {
|
const args = {
|
||||||
"actionId": actionId,
|
actionId,
|
||||||
"startOffset": BigInt(startOffset),
|
startOffset: BigInt(startOffset),
|
||||||
"pageSize": BigInt(Number(pageSize.value)),
|
pageSize: BigInt(Number(pageSize.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await window.client.getActionLogs(args)
|
const response = await window.client.getActionLogs(args)
|
||||||
|
|
@ -203,11 +284,11 @@ async function fetchActionLogs() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAction() {
|
async function fetchAction () {
|
||||||
try {
|
try {
|
||||||
const actionId = route.params.actionId
|
const actionId = route.params.actionId
|
||||||
const args = {
|
const args = {
|
||||||
"bindingId": actionId
|
bindingId: actionId
|
||||||
}
|
}
|
||||||
const response = await window.client.getActionBinding(args)
|
const response = await window.client.getActionBinding(args)
|
||||||
action.value = response.action
|
action.value = response.action
|
||||||
|
|
@ -219,11 +300,11 @@ async function fetchAction() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToDashboard(path) {
|
function goToDashboard (path) {
|
||||||
router.push(path)
|
router.push(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetState() {
|
function resetState () {
|
||||||
action.value = null
|
action.value = null
|
||||||
backToDashboards.value = []
|
backToDashboards.value = []
|
||||||
actionTitle.value = 'Action Details'
|
actionTitle.value = 'Action Details'
|
||||||
|
|
@ -235,11 +316,11 @@ function resetState() {
|
||||||
syncDurationTicker()
|
syncDurationTicker()
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSearch() {
|
function clearSearch () {
|
||||||
searchText.value = ''
|
searchText.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTimestamp(timestamp) {
|
function formatTimestamp (timestamp) {
|
||||||
if (!timestamp) return 'Unknown'
|
if (!timestamp) return 'Unknown'
|
||||||
try {
|
try {
|
||||||
const date = new Date(timestamp)
|
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}`
|
return n === 1 ? `1 ${singular}` : `${n} ${pluralForm}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDurationSimple(ms) {
|
function formatDurationSimple (ms) {
|
||||||
if (!Number.isFinite(ms) || ms < 0) {
|
if (!Number.isFinite(ms) || ms < 0) {
|
||||||
return '—'
|
return '—'
|
||||||
}
|
}
|
||||||
|
|
@ -274,7 +355,7 @@ function formatDurationSimple(ms) {
|
||||||
return parts.join(' ')
|
return parts.join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatExecutionDuration(log) {
|
function formatExecutionDuration (log) {
|
||||||
// Reading durationClock keeps this column reactive while executions are in progress.
|
// Reading durationClock keeps this column reactive while executions are in progress.
|
||||||
const clock = durationClock.value
|
const clock = durationClock.value
|
||||||
|
|
||||||
|
|
@ -300,7 +381,7 @@ function formatExecutionDuration(log) {
|
||||||
return formatDurationSimple(endMs - started.getTime())
|
return formatDurationSimple(endMs - started.getTime())
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncDurationTicker() {
|
function syncDurationTicker () {
|
||||||
if (durationTicker != null) {
|
if (durationTicker != null) {
|
||||||
clearInterval(durationTicker)
|
clearInterval(durationTicker)
|
||||||
durationTicker = null
|
durationTicker = null
|
||||||
|
|
@ -314,18 +395,18 @@ function syncDurationTicker() {
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePageChange(page) {
|
function handlePageChange (page) {
|
||||||
currentPage.value = page
|
currentPage.value = page
|
||||||
fetchActionLogs()
|
fetchActionLogs()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePageSizeChange(newPageSize) {
|
function handlePageSizeChange (newPageSize) {
|
||||||
pageSize.value = newPageSize
|
pageSize.value = newPageSize
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
fetchActionLogs()
|
fetchActionLogs()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startAction() {
|
async function startAction () {
|
||||||
if (!action.value || !action.value.bindingId) {
|
if (!action.value || !action.value.bindingId) {
|
||||||
console.error('Cannot start action: no binding ID')
|
console.error('Cannot start action: no binding ID')
|
||||||
return
|
return
|
||||||
|
|
@ -351,7 +432,7 @@ async function startAction() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onExecutionEvent(evt) {
|
function onExecutionEvent (evt) {
|
||||||
const logEntry = getExecutionLogEntry(evt)
|
const logEntry = getExecutionLogEntry(evt)
|
||||||
if (!logEntry || logEntry.bindingId !== route.params.actionId) {
|
if (!logEntry || logEntry.bindingId !== route.params.actionId) {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<Section :title="'Execution conditions: ' + actionTitle" :padding="false">
|
<Section
|
||||||
|
:title="'Execution conditions: ' + actionTitle"
|
||||||
|
:padding="false"
|
||||||
|
>
|
||||||
<template #toolbar>
|
<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
|
Back to action details
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="action" class="padding content">
|
<div
|
||||||
|
v-if="action"
|
||||||
|
class="padding content"
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
These entries mirror the automatic triggers from your OliveTin configuration for this action.
|
These entries mirror the automatic triggers from your OliveTin configuration for this action.
|
||||||
You can always run the action manually as well.
|
You can always run the action manually as well.
|
||||||
|
|
@ -14,7 +23,12 @@
|
||||||
|
|
||||||
<h3 class="exec-type-heading">
|
<h3 class="exec-type-heading">
|
||||||
On click
|
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>
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
Manual execution from the web UI (dashboard or action details), or via the API (for example StartAction),
|
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">
|
<template v-if="action.execOnStartup">
|
||||||
<h3 class="exec-type-heading">
|
<h3 class="exec-type-heading">
|
||||||
<code>execOnStartup</code>
|
<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>
|
</h3>
|
||||||
<p>Runs once when OliveTin starts.</p>
|
<p>Runs once when OliveTin starts.</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -32,37 +51,72 @@
|
||||||
<template v-if="nonEmptyList(action.execOnCron)">
|
<template v-if="nonEmptyList(action.execOnCron)">
|
||||||
<h3 class="exec-type-heading">
|
<h3 class="exec-type-heading">
|
||||||
<code>execOnCron</code>
|
<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>
|
</h3>
|
||||||
<ul>
|
<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>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="nonEmptyList(action.execOnFileCreatedInDir)">
|
<template v-if="nonEmptyList(action.execOnFileCreatedInDir)">
|
||||||
<h3 class="exec-type-heading">
|
<h3 class="exec-type-heading">
|
||||||
<code>execOnFileCreatedInDir</code>
|
<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>
|
</h3>
|
||||||
<ul>
|
<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>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="nonEmptyList(action.execOnFileChangedInDir)">
|
<template v-if="nonEmptyList(action.execOnFileChangedInDir)">
|
||||||
<h3 class="exec-type-heading">
|
<h3 class="exec-type-heading">
|
||||||
<code>execOnFileChangedInDir</code>
|
<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>
|
</h3>
|
||||||
<ul>
|
<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>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="action.execOnCalendarFile">
|
<template v-if="action.execOnCalendarFile">
|
||||||
<h3 class="exec-type-heading">
|
<h3 class="exec-type-heading">
|
||||||
<code>execOnCalendarFile</code>
|
<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>
|
</h3>
|
||||||
<p><code>{{ action.execOnCalendarFile }}</code></p>
|
<p><code>{{ action.execOnCalendarFile }}</code></p>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -70,10 +124,18 @@
|
||||||
<template v-if="nonEmptyList(action.execOnWebhooks)">
|
<template v-if="nonEmptyList(action.execOnWebhooks)">
|
||||||
<h3 class="exec-type-heading">
|
<h3 class="exec-type-heading">
|
||||||
<code>execOnWebhook</code>
|
<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>
|
</h3>
|
||||||
<ul class="webhook-list">
|
<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.template">template: <code>{{ wh.template }}</code></span>
|
||||||
<span v-if="wh.matchPath"> · matchPath: <code>{{ wh.matchPath }}</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>
|
<span v-if="nonEmptyObject(wh.matchHeaders)"> · matchHeaders: <code>{{ wh.matchHeaders }}</code></span>
|
||||||
|
|
@ -83,14 +145,22 @@
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</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.
|
This action has no automatic triggers in configuration besides on-demand execution.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
<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>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -112,18 +182,18 @@ const execConditionDocs = {
|
||||||
fileCreated: 'https://docs.olivetin.app/action_execution/onfilecreated.html',
|
fileCreated: 'https://docs.olivetin.app/action_execution/onfilecreated.html',
|
||||||
fileChanged: 'https://docs.olivetin.app/action_execution/onfilechanged.html',
|
fileChanged: 'https://docs.olivetin.app/action_execution/onfilechanged.html',
|
||||||
calendar: 'https://docs.olivetin.app/action_execution/oncalendar.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
|
return Array.isArray(list) && list.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function nonEmptyObject(object) {
|
function nonEmptyObject (object) {
|
||||||
return object && Object.keys(object).length > 0
|
return object && Object.keys(object).length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function webhookHasCondition(webhook) {
|
function webhookHasCondition (webhook) {
|
||||||
return webhook.template || webhook.matchPath || nonEmptyObject(webhook.matchHeaders) || nonEmptyObject(webhook.matchQuery)
|
return webhook.template || webhook.matchPath || nonEmptyObject(webhook.matchHeaders) || nonEmptyObject(webhook.matchQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,7 +217,7 @@ const hasConfiguredTriggers = computed(() => {
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
async function fetchAction() {
|
async function fetchAction () {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const actionId = route.params.actionId
|
const actionId = route.params.actionId
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<section id = "argument-popup">
|
<section id="argument-popup">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>
|
<h2>
|
||||||
<span class="section-title-with-icon">
|
<span class="section-title-with-icon">
|
||||||
|
|
@ -8,7 +8,11 @@
|
||||||
:to="`/action/${bindingId}`"
|
:to="`/action/${bindingId}`"
|
||||||
class="action-details-title-link"
|
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 }}
|
{{ title }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -17,48 +21,96 @@
|
||||||
<div class="section-content padding">
|
<div class="section-content padding">
|
||||||
<form @submit="handleSubmit">
|
<form @submit="handleSubmit">
|
||||||
<template v-if="actionArguments.length > 0">
|
<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">
|
<datalist
|
||||||
<label v-if="arg.type !== 'checklist'" :for="argumentFieldId(arg.name)">
|
v-if="(arg.suggestions && Object.keys(arg.suggestions).length > 0) || getBrowserSuggestions(arg).length > 0"
|
||||||
{{ formatLabel(arg.title) }}
|
:id="argumentFieldChoicesId(arg.name)"
|
||||||
</label>
|
>
|
||||||
<div v-else class="argument-label">
|
<option
|
||||||
{{ formatLabel(arg.title) }}
|
v-for="(suggestion, key) in arg.suggestions"
|
||||||
</div>
|
: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)">
|
<ChoiceCombobox
|
||||||
<option v-for="(suggestion, key) in arg.suggestions" :key="key" :value="key">
|
v-if="getInputComponent(arg) === 'select'"
|
||||||
{{ suggestion }}
|
:id="argumentFieldId(arg.name)"
|
||||||
</option>
|
:name="arg.name"
|
||||||
<option v-for="(suggestion, index) in getBrowserSuggestions(arg)" :key="`browser-${index}`" :value="suggestion">
|
:choices="arg.choices"
|
||||||
{{ suggestion }}
|
:model-value="getArgumentValue(arg)"
|
||||||
</option>
|
:required="arg.required"
|
||||||
</datalist>
|
@update:model-value="handleChoiceUpdate(arg, $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
<ChoiceCombobox v-if="getInputComponent(arg) === 'select'" :id="argumentFieldId(arg.name)" :name="arg.name"
|
<ChoiceChecklist
|
||||||
:choices="arg.choices" :model-value="getArgumentValue(arg)" :required="arg.required"
|
v-else-if="arg.type === 'checklist'"
|
||||||
@update:model-value="handleChoiceUpdate(arg, $event)" />
|
: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"
|
<component
|
||||||
:label="arg.title" :choices="arg.choices" :model-value="getArgumentValue(arg)" :required="arg.required"
|
:is="getInputComponent(arg)"
|
||||||
@update:model-value="handleChoiceUpdate(arg, $event)" />
|
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"
|
<span
|
||||||
:value="(arg.type === 'checkbox' || arg.type === 'confirmation') ? undefined : getArgumentValue(arg)"
|
class="argument-description"
|
||||||
:checked="(arg.type === 'checkbox' || arg.type === 'confirmation') ? getArgumentValue(arg) : undefined"
|
v-html="arg.description"
|
||||||
: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>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="justificationRequired">
|
<template v-if="justificationRequired">
|
||||||
<label for="justification">Justification:</label>
|
<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>
|
</template>
|
||||||
|
|
||||||
<div v-if="actionArguments.length === 0 && !justificationRequired">
|
<div v-if="actionArguments.length === 0 && !justificationRequired">
|
||||||
|
|
@ -66,10 +118,18 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button name="start" type="submit" :disabled="!formReady || (hasConfirmation && !confirmationChecked)">
|
<button
|
||||||
|
name="start"
|
||||||
|
type="submit"
|
||||||
|
:disabled="!formReady || (hasConfirmation && !confirmationChecked)"
|
||||||
|
>
|
||||||
Start
|
Start
|
||||||
</button>
|
</button>
|
||||||
<button name="cancel" type="button" @click="handleCancel">
|
<button
|
||||||
|
name="cancel"
|
||||||
|
type="button"
|
||||||
|
@click="handleCancel"
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -103,7 +163,7 @@ const router = useRouter()
|
||||||
const dialog = ref(null)
|
const dialog = ref(null)
|
||||||
const title = ref('')
|
const title = ref('')
|
||||||
const icon = ref('')
|
const icon = ref('')
|
||||||
//const arguments = ref([])
|
// const arguments = ref([])
|
||||||
const argValues = ref({})
|
const argValues = ref({})
|
||||||
const confirmationChecked = ref(false)
|
const confirmationChecked = ref(false)
|
||||||
const hasConfirmation = ref(false)
|
const hasConfirmation = ref(false)
|
||||||
|
|
@ -128,7 +188,7 @@ const props = defineProps({
|
||||||
})
|
})
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
async function setup() {
|
async function setup () {
|
||||||
formReady.value = false
|
formReady.value = false
|
||||||
document.body.removeAttribute('loaded-argument-form')
|
document.body.removeAttribute('loaded-argument-form')
|
||||||
|
|
||||||
|
|
@ -155,32 +215,32 @@ async function setup() {
|
||||||
|
|
||||||
// Initialize values from navigation state, query params, or defaults
|
// Initialize values from navigation state, query params, or defaults
|
||||||
actionArguments.value.forEach(arg => {
|
actionArguments.value.forEach(arg => {
|
||||||
if (arg.type === 'confirmation') {
|
if (arg.type === 'confirmation') {
|
||||||
hasConfirmation.value = true
|
hasConfirmation.value = true
|
||||||
const paramValue = getInitialArgumentValue(arg.name, prefilledArguments)
|
const paramValue = getInitialArgumentValue(arg.name, prefilledArguments)
|
||||||
let checkedValue = false
|
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 (paramValue !== null) {
|
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 !== '') {
|
} else if (arg.defaultValue !== undefined && arg.defaultValue !== '') {
|
||||||
argValues.value[arg.name] = arg.defaultValue === '1' || arg.defaultValue === 'true' || arg.defaultValue === true
|
checkedValue = arg.defaultValue === '1' || arg.defaultValue === 'true' || arg.defaultValue === true
|
||||||
} else {
|
|
||||||
argValues.value[arg.name] = false
|
|
||||||
}
|
}
|
||||||
|
argValues.value[arg.name] = checkedValue
|
||||||
|
confirmationChecked.value = checkedValue
|
||||||
} else {
|
} 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
|
// 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)
|
const lastChar = title.charAt(title.length - 1)
|
||||||
if (lastChar === '?' || lastChar === '.' || lastChar === ':') {
|
if (lastChar === '?' || lastChar === '.' || lastChar === ':') {
|
||||||
return title
|
return title
|
||||||
|
|
@ -210,7 +270,7 @@ function formatLabel(title) {
|
||||||
return title + ':'
|
return title + ':'
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInputComponent(arg) {
|
function getInputComponent (arg) {
|
||||||
if (arg.type === 'html') {
|
if (arg.type === 'html') {
|
||||||
return 'div'
|
return 'div'
|
||||||
} else if (arg.type === 'raw_string_multiline') {
|
} 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') {
|
if (arg.type === 'html' || arg.type === 'raw_string_multiline' || arg.type === 'select') {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
@ -242,33 +302,35 @@ function getInputType(arg) {
|
||||||
return arg.type
|
return arg.type
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPattern(arg) {
|
function getPattern (arg) {
|
||||||
if (arg.type && arg.type.startsWith('regex:')) {
|
if (arg.type && arg.type.startsWith('regex:')) {
|
||||||
return arg.type.replace('regex:', '')
|
return arg.type.replace('regex:', '')
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArgumentValue(arg) {
|
function getArgumentValue (arg) {
|
||||||
if (arg.type === 'checkbox' || arg.type === 'confirmation') {
|
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] === '1' || argValues.value[arg.name] === true || argValues.value[arg.name] === 'true'
|
||||||
}
|
}
|
||||||
return argValues.value[arg.name] || ''
|
return argValues.value[arg.name] || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleJustificationInput(event) {
|
function handleJustificationInput (event) {
|
||||||
justificationValue.value = event.target.value
|
justificationValue.value = event.target.value
|
||||||
justificationEditedManually.value = true
|
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
|
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value
|
||||||
argValues.value[arg.name] = value
|
argValues.value[arg.name] = value
|
||||||
|
event.target.setCustomValidity('')
|
||||||
updateUrlWithArg(arg.name, value)
|
updateUrlWithArg(arg.name, value)
|
||||||
updateJustificationFromTemplate()
|
updateJustificationFromTemplate()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChange(arg, event) {
|
function handleChange (arg, event) {
|
||||||
if (arg.type === 'confirmation') {
|
if (arg.type === 'confirmation') {
|
||||||
confirmationChecked.value = event.target.checked
|
confirmationChecked.value = event.target.checked
|
||||||
return
|
return
|
||||||
|
|
@ -278,18 +340,18 @@ function handleChange(arg, event) {
|
||||||
validateArgument(arg, event.target.value)
|
validateArgument(arg, event.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getValidationElement(arg) {
|
function getValidationElement (arg) {
|
||||||
return document.getElementById(argumentFieldValidationElementId(arg))
|
return document.getElementById(argumentFieldValidationElementId(arg))
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChoiceUpdate(arg, value) {
|
function handleChoiceUpdate (arg, value) {
|
||||||
argValues.value[arg.name] = value
|
argValues.value[arg.name] = value
|
||||||
updateUrlWithArg(arg.name, value)
|
updateUrlWithArg(arg.name, value)
|
||||||
validateArgument(arg, value)
|
validateArgument(arg, value)
|
||||||
updateJustificationFromTemplate()
|
updateJustificationFromTemplate()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function validateArgument(arg, value) {
|
async function validateArgument (arg, value) {
|
||||||
if (!arg.type || arg.type.startsWith('regex:')) {
|
if (!arg.type || arg.type.startsWith('regex:')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -316,7 +378,7 @@ async function validateArgument(arg, value) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const validateArgumentTypeArgs = {
|
const validateArgumentTypeArgs = {
|
||||||
value: value,
|
value,
|
||||||
type: arg.type,
|
type: arg.type,
|
||||||
bindingId: props.bindingId,
|
bindingId: props.bindingId,
|
||||||
argumentName: arg.name
|
argumentName: arg.name
|
||||||
|
|
@ -348,7 +410,7 @@ async function validateArgument(arg, value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUrlWithArg(name, value) {
|
function updateUrlWithArg (name, value) {
|
||||||
if (name && value !== undefined) {
|
if (name && value !== undefined) {
|
||||||
const url = new URL(window.location.href)
|
const url = new URL(window.location.href)
|
||||||
|
|
||||||
|
|
@ -363,7 +425,7 @@ function updateUrlWithArg(name, value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldSendArgument(arg) {
|
function shouldSendArgument (arg) {
|
||||||
if (!arg.name) {
|
if (!arg.name) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -371,7 +433,7 @@ function shouldSendArgument(arg) {
|
||||||
return arg.type !== 'html'
|
return arg.type !== 'html'
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatArgumentValueForApi(arg, rawValue) {
|
function formatArgumentValueForApi (arg, rawValue) {
|
||||||
if (arg.type === 'checkbox' || arg.type === 'confirmation') {
|
if (arg.type === 'checkbox' || arg.type === 'confirmation') {
|
||||||
return rawValue === '1' || rawValue === true || rawValue === 'true' ? '1' : '0'
|
return rawValue === '1' || rawValue === true || rawValue === 'true' ? '1' : '0'
|
||||||
}
|
}
|
||||||
|
|
@ -387,7 +449,7 @@ function formatArgumentValueForApi(arg, rawValue) {
|
||||||
return rawValue ?? ''
|
return rawValue ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectedArgumentEntries() {
|
function getSelectedArgumentEntries () {
|
||||||
const entries = []
|
const entries = []
|
||||||
|
|
||||||
for (const arg of actionArguments.value) {
|
for (const arg of actionArguments.value) {
|
||||||
|
|
@ -404,17 +466,17 @@ function getSelectedArgumentEntries() {
|
||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArgumentValues() {
|
function getArgumentValues () {
|
||||||
return getSelectedArgumentEntries().map(({ name, value }) => ({ name, value }))
|
return getSelectedArgumentEntries().map(({ name, value }) => ({ name, value }))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArgumentMapForTemplate() {
|
function getArgumentMapForTemplate () {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
getSelectedArgumentEntries().map(({ name, value }) => [name, value])
|
getSelectedArgumentEntries().map(({ name, value }) => [name, value])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateJustificationFromTemplate() {
|
function updateJustificationFromTemplate () {
|
||||||
if (!justificationTemplate.value || justificationEditedManually.value) {
|
if (!justificationTemplate.value || justificationEditedManually.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -422,7 +484,7 @@ function updateJustificationFromTemplate() {
|
||||||
justificationValue.value = applyArgumentTemplate(justificationTemplate.value, getArgumentMapForTemplate())
|
justificationValue.value = applyArgumentTemplate(justificationTemplate.value, getArgumentMapForTemplate())
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUniqueId() {
|
function getUniqueId () {
|
||||||
if (window.isSecureContext) {
|
if (window.isSecureContext) {
|
||||||
return window.crypto.randomUUID()
|
return window.crypto.randomUUID()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -430,7 +492,7 @@ function getUniqueId() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBrowserSuggestions(arg) {
|
function getBrowserSuggestions (arg) {
|
||||||
if (!arg.suggestionsBrowserKey) {
|
if (!arg.suggestionsBrowserKey) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
@ -448,7 +510,7 @@ function getBrowserSuggestions(arg) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveBrowserSuggestions() {
|
function saveBrowserSuggestions () {
|
||||||
for (const arg of actionArguments.value) {
|
for (const arg of actionArguments.value) {
|
||||||
if (arg.suggestionsBrowserKey) {
|
if (arg.suggestionsBrowserKey) {
|
||||||
const value = argValues.value[arg.name]
|
const value = argValues.value[arg.name]
|
||||||
|
|
@ -484,7 +546,7 @@ function saveBrowserSuggestions() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startAction(actionArgs) {
|
async function startAction (actionArgs) {
|
||||||
const startActionArgs = {
|
const startActionArgs = {
|
||||||
bindingId: props.bindingId,
|
bindingId: props.bindingId,
|
||||||
arguments: actionArgs,
|
arguments: actionArgs,
|
||||||
|
|
@ -506,7 +568,7 @@ async function startAction(actionArgs) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit(event) {
|
async function handleSubmit (event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
if (!formReady.value) {
|
if (!formReady.value) {
|
||||||
|
|
@ -568,24 +630,24 @@ async function handleSubmit(event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancel() {
|
function handleCancel () {
|
||||||
router.back()
|
router.back()
|
||||||
clearBookmark()
|
clearBookmark()
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearBookmark() {
|
function clearBookmark () {
|
||||||
window.history.replaceState({
|
window.history.replaceState({
|
||||||
path: window.location.pathname
|
path: window.location.pathname
|
||||||
}, '', window.location.pathname)
|
}, '', window.location.pathname)
|
||||||
}
|
}
|
||||||
|
|
||||||
function show() {
|
function show () {
|
||||||
if (dialog.value) {
|
if (dialog.value) {
|
||||||
dialog.value.showModal()
|
dialog.value.showModal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close () {
|
||||||
if (dialog.value) {
|
if (dialog.value) {
|
||||||
dialog.value.close()
|
dialog.value.close()
|
||||||
}
|
}
|
||||||
|
|
@ -638,7 +700,6 @@ form {
|
||||||
grid-template-columns: max-content auto auto;
|
grid-template-columns: max-content auto auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.argument-description {
|
.argument-description {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<Section :title="t('diagnostics.get-support')">
|
<Section :title="t('diagnostics.get-support')">
|
||||||
<p>{{ t('diagnostics.get-support-description') }}
|
<p>
|
||||||
|
{{ t('diagnostics.get-support-description') }}
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
@ -21,26 +25,61 @@
|
||||||
<Section :title="t('diagnostics.server-diagnostics')">
|
<Section :title="t('diagnostics.server-diagnostics')">
|
||||||
<p>{{ t('diagnostics.server-diagnostics-description') }}</p>
|
<p>{{ t('diagnostics.server-diagnostics-description') }}</p>
|
||||||
<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>
|
</p>
|
||||||
|
|
||||||
<div role="toolbar">
|
<div role="toolbar">
|
||||||
<button @click="generateServerDiagnostics" :disabled="loading" class = "good">{{ t('diagnostics.generate-server-diagnostics') }}</button>
|
<button
|
||||||
<button @click="copyServerDiagnostics" :disabled="!serverDiagnostics || loading" :class="serverDiagnosticsCopied ? 'good' : ''">{{ serverDiagnosticsCopied ? t('diagnostics.copied') : t('diagnostics.copy-to-clipboard') }}</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>
|
</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>
|
||||||
|
|
||||||
<Section :title="t('diagnostics.browser-info')">
|
<Section :title="t('diagnostics.browser-info')">
|
||||||
<p>{{ t('diagnostics.browser-info-description') }}</p>
|
<p>{{ t('diagnostics.browser-info-description') }}</p>
|
||||||
|
|
||||||
<div role="toolbar">
|
<div role="toolbar">
|
||||||
<button @click="generateBrowserInfo" :disabled="loading" class = "good">{{ t('diagnostics.generate-browser-info') }}</button>
|
<button
|
||||||
<button @click="copyBrowserInfo" :disabled="!browserInfo || loading" :class="browserInfoCopied ? 'good' : ''">{{ browserInfoCopied ? t('diagnostics.copied') : t('diagnostics.copy-to-clipboard') }}</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>
|
</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>
|
</Section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -58,17 +97,17 @@ const browserInfo = ref('')
|
||||||
const serverDiagnosticsCopied = ref(false)
|
const serverDiagnosticsCopied = ref(false)
|
||||||
const browserInfoCopied = ref(false)
|
const browserInfoCopied = ref(false)
|
||||||
|
|
||||||
async function fetchDiagnostics() {
|
async function fetchDiagnostics () {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await window.client.getDiagnostics();
|
const response = await window.client.getDiagnostics()
|
||||||
diagnostics.value = {
|
diagnostics.value = {
|
||||||
sshFoundKey: response.SshFoundKey,
|
sshFoundKey: response.SshFoundKey,
|
||||||
sshFoundConfig: response.SshFoundConfig
|
sshFoundConfig: response.SshFoundConfig
|
||||||
};
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch diagnostics:', err);
|
console.error('Failed to fetch diagnostics:', err)
|
||||||
diagnostics.value = {
|
diagnostics.value = {
|
||||||
sshFoundKey: t('diagnostics.unknown'),
|
sshFoundKey: t('diagnostics.unknown'),
|
||||||
sshFoundConfig: t('diagnostics.unknown')
|
sshFoundConfig: t('diagnostics.unknown')
|
||||||
|
|
@ -77,20 +116,22 @@ async function fetchDiagnostics() {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatKey(key) {
|
async function generateServerDiagnostics () {
|
||||||
return key
|
loading.value = true
|
||||||
.replace(/([A-Z])/g, ' $1')
|
|
||||||
.replace(/^./, str => str.toUpperCase())
|
try {
|
||||||
.trim()
|
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() {
|
async function copyServerDiagnostics () {
|
||||||
const response = await window.client.serverDiagnostics()
|
|
||||||
console.log("response", response)
|
|
||||||
serverDiagnostics.value = `\`\`\`\n${response.alert}\n\`\`\`\n`
|
|
||||||
}
|
|
||||||
|
|
||||||
async function copyServerDiagnostics() {
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(serverDiagnostics.value)
|
await navigator.clipboard.writeText(serverDiagnostics.value)
|
||||||
serverDiagnosticsCopied.value = true
|
serverDiagnosticsCopied.value = true
|
||||||
|
|
@ -102,7 +143,7 @@ async function copyServerDiagnostics() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateBrowserInfo() {
|
async function generateBrowserInfo () {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
let userAgentData = 'N/A'
|
let userAgentData = 'N/A'
|
||||||
|
|
@ -159,7 +200,7 @@ async function generateBrowserInfo() {
|
||||||
})(),
|
})(),
|
||||||
hardwareConcurrency: navigator.hardwareConcurrency || 'N/A',
|
hardwareConcurrency: navigator.hardwareConcurrency || 'N/A',
|
||||||
maxTouchPoints: navigator.maxTouchPoints || 'N/A',
|
maxTouchPoints: navigator.maxTouchPoints || 'N/A',
|
||||||
userAgentData: userAgentData
|
userAgentData
|
||||||
}
|
}
|
||||||
|
|
||||||
const showVersionNumber = window.initResponse?.effectivePolicy?.showVersionNumber ?? true
|
const showVersionNumber = window.initResponse?.effectivePolicy?.showVersionNumber ?? true
|
||||||
|
|
@ -168,20 +209,20 @@ async function generateBrowserInfo() {
|
||||||
: '[hidden]'
|
: '[hidden]'
|
||||||
const currentLanguage = locale.value || t('diagnostics.unknown')
|
const currentLanguage = locale.value || t('diagnostics.unknown')
|
||||||
|
|
||||||
let output = '';
|
let output = ''
|
||||||
output += `\`\`\`\n`
|
output += '```\n'
|
||||||
output += '### BROWSER INFO START (copy all text to BROWSER INFO END)\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 += `olivetinVersion: ${olivetinVersion}\n`
|
||||||
output += `currentLanguage: ${currentLanguage}\n`
|
output += `currentLanguage: ${currentLanguage}\n`
|
||||||
output += `\n# Browser Information\n`
|
output += '\n# Browser Information\n'
|
||||||
output += `userAgent: ${info.userAgent}\n`
|
output += `userAgent: ${info.userAgent}\n`
|
||||||
output += `platform: ${info.platform}\n`
|
output += `platform: ${info.platform}\n`
|
||||||
output += `language: ${info.language}\n`
|
output += `language: ${info.language}\n`
|
||||||
output += `languages: ${info.languages}\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 += `userAgentData:\n${info.userAgentData}\n`
|
||||||
output += `\n# Display Information\n`
|
output += '\n# Display Information\n'
|
||||||
output += `screenWidth: ${info.screenWidth}\n`
|
output += `screenWidth: ${info.screenWidth}\n`
|
||||||
output += `screenHeight: ${info.screenHeight}\n`
|
output += `screenHeight: ${info.screenHeight}\n`
|
||||||
output += `screenColorDepth: ${info.screenColorDepth}\n`
|
output += `screenColorDepth: ${info.screenColorDepth}\n`
|
||||||
|
|
@ -189,18 +230,18 @@ async function generateBrowserInfo() {
|
||||||
output += `viewportWidth: ${info.viewportWidth}\n`
|
output += `viewportWidth: ${info.viewportWidth}\n`
|
||||||
output += `viewportHeight: ${info.viewportHeight}\n`
|
output += `viewportHeight: ${info.viewportHeight}\n`
|
||||||
output += `devicePixelRatio: ${info.devicePixelRatio}\n`
|
output += `devicePixelRatio: ${info.devicePixelRatio}\n`
|
||||||
output += `\n# Feature Support\n`
|
output += '\n# Feature Support\n'
|
||||||
output += `cookieEnabled: ${info.cookieEnabled}\n`
|
output += `cookieEnabled: ${info.cookieEnabled}\n`
|
||||||
output += `localStorageEnabled: ${info.localStorageEnabled}\n`
|
output += `localStorageEnabled: ${info.localStorageEnabled}\n`
|
||||||
output += `sessionStorageEnabled: ${info.sessionStorageEnabled}\n`
|
output += `sessionStorageEnabled: ${info.sessionStorageEnabled}\n`
|
||||||
output += `onLine: ${info.onLine}\n`
|
output += `onLine: ${info.onLine}\n`
|
||||||
output += `hardwareConcurrency: ${info.hardwareConcurrency}\n`
|
output += `hardwareConcurrency: ${info.hardwareConcurrency}\n`
|
||||||
output += `maxTouchPoints: ${info.maxTouchPoints}\n`
|
output += `maxTouchPoints: ${info.maxTouchPoints}\n`
|
||||||
output += `\n# Location & Time\n`
|
output += '\n# Location & Time\n'
|
||||||
output += `timezone: ${info.timezone}\n`
|
output += `timezone: ${info.timezone}\n`
|
||||||
output += `timezoneOffset: ${info.timezoneOffset}\n`
|
output += `timezoneOffset: ${info.timezoneOffset}\n`
|
||||||
output += `\n### BROWSER INFO END (copy all text from BROWSER INFO START)`
|
output += '\n### BROWSER INFO END (copy all text from BROWSER INFO START)'
|
||||||
output += `\n\`\`\`\n`
|
output += '\n```\n'
|
||||||
|
|
||||||
browserInfo.value = output
|
browserInfo.value = output
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -208,7 +249,7 @@ async function generateBrowserInfo() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyBrowserInfo() {
|
async function copyBrowserInfo () {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(browserInfo.value)
|
await navigator.clipboard.writeText(browserInfo.value)
|
||||||
browserInfoCopied.value = true
|
browserInfoCopied.value = true
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,54 @@
|
||||||
<template>
|
<template>
|
||||||
<Section v-if="!definitionsLoaded" title="Loading entity definitions..." />
|
<Section
|
||||||
<Section
|
v-if="!definitionsLoaded"
|
||||||
v-else-if="totalInstances === 0"
|
title="Loading entity definitions..."
|
||||||
title="There are no entities to show yet."
|
/>
|
||||||
>
|
<Section
|
||||||
<p>
|
v-else-if="totalInstances === 0"
|
||||||
When OliveTin has registered entity instances (for example from entity files or your setup), they will be listed here.
|
title="There are no entities to show yet."
|
||||||
</p>
|
>
|
||||||
</Section>
|
<p>
|
||||||
<template v-else>
|
When OliveTin has registered entity instances (for example from entity files or your setup), they will be listed here.
|
||||||
<EntityDefinitionSection
|
</p>
|
||||||
v-for="def in entityDefinitions"
|
</Section>
|
||||||
:key="def.title"
|
<template v-else>
|
||||||
:definition="def"
|
<EntityDefinitionSection
|
||||||
/>
|
v-for="def in entityDefinitions"
|
||||||
</template>
|
:key="def.title"
|
||||||
|
:definition="def"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import Section from 'picocrank/vue/components/Section.vue'
|
import Section from 'picocrank/vue/components/Section.vue'
|
||||||
import EntityDefinitionSection from '../components/EntityDefinitionSection.vue'
|
import EntityDefinitionSection from '../components/EntityDefinitionSection.vue'
|
||||||
|
|
||||||
const definitionsLoaded = ref(false)
|
const definitionsLoaded = ref(false)
|
||||||
const entityDefinitions = ref([])
|
const entityDefinitions = ref([])
|
||||||
|
|
||||||
const totalInstances = computed(() =>
|
const totalInstances = computed(() =>
|
||||||
entityDefinitions.value.reduce(
|
entityDefinitions.value.reduce(
|
||||||
(sum, def) => sum + (def.instances?.length ?? 0),
|
(sum, def) => sum + (def.instances?.length ?? 0),
|
||||||
0,
|
0
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
async function fetchEntities() {
|
async function fetchEntities () {
|
||||||
try {
|
try {
|
||||||
const ret = await window.client.getEntities()
|
const ret = await window.client.getEntities()
|
||||||
entityDefinitions.value = ret.entityDefinitions ?? []
|
entityDefinitions.value = ret.entityDefinitions ?? []
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch entities:', err)
|
console.error('Failed to fetch entities:', err)
|
||||||
window.showBigError('fetch-entities', 'getting entities', err, false)
|
window.showBigError('fetch-entities', 'getting entities', err, false)
|
||||||
entityDefinitions.value = []
|
entityDefinitions.value = []
|
||||||
} finally {
|
} finally {
|
||||||
definitionsLoaded.value = true
|
definitionsLoaded.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchEntities()
|
fetchEntities()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,130 +1,176 @@
|
||||||
<template>
|
<template>
|
||||||
<Section>
|
<Section>
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="section-title-with-icon">
|
<span class="section-title-with-icon">
|
||||||
Entity Details:
|
Entity Details:
|
||||||
<ActionIconGlyph v-if="entityIcon" class="entity-title-icon" :glyph="entityIcon" />
|
<ActionIconGlyph
|
||||||
<span v-if="entityDetails?.title">{{ entityDetails.title }}</span>
|
v-if="entityIcon"
|
||||||
</span>
|
class="entity-title-icon"
|
||||||
</template>
|
:glyph="entityIcon"
|
||||||
<template #toolbar>
|
/>
|
||||||
<button @click="goBack" class="back-button">
|
<span v-if="entityDetails?.title">{{ entityDetails.title }}</span>
|
||||||
<HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" />
|
</span>
|
||||||
<span>Back</span>
|
</template>
|
||||||
</button>
|
<template #toolbar>
|
||||||
</template>
|
<button
|
||||||
<div v-if="!entityDetails">
|
class="back-button"
|
||||||
<p>Loading entity details...</p>
|
@click="goBack"
|
||||||
</div>
|
>
|
||||||
<template v-else>
|
<HugeiconsIcon
|
||||||
<dl>
|
:icon="ArrowLeftIcon"
|
||||||
<dt>Type</dt>
|
width="1.2em"
|
||||||
<dd>
|
height="1.2em"
|
||||||
<router-link :to="{ name: 'Entities' }" class="entity-type-link">
|
/>
|
||||||
{{ entityType }}
|
<span>Back</span>
|
||||||
</router-link>
|
</button>
|
||||||
</dd>
|
</template>
|
||||||
<dt v-if="entityDetails.title">Title</dt>
|
<div v-if="!entityDetails">
|
||||||
<dd v-if="entityDetails.title">{{ entityDetails.title }}</dd>
|
<p>Loading entity details...</p>
|
||||||
<template v-if="entityDetails.fields">
|
</div>
|
||||||
<template v-for="(value, key) in entityDetails.fields" :key="key">
|
<template v-else>
|
||||||
<dt>{{ key }}</dt>
|
<dl>
|
||||||
<dd>{{ value }}</dd>
|
<dt>Type</dt>
|
||||||
</template>
|
<dd>
|
||||||
</template>
|
<router-link
|
||||||
</dl>
|
:to="{ name: 'Entities' }"
|
||||||
<p v-if="!entityDetails.title && (!entityDetails.fields || Object.keys(entityDetails.fields).length === 0)">No details available for this entity.</p>
|
class="entity-type-link"
|
||||||
</template>
|
>
|
||||||
</Section>
|
{{ 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">
|
<Section
|
||||||
<div v-if="filteredDirectories.length > 0" class="directories-section">
|
v-if="entityDetails"
|
||||||
<ul class="directory-list">
|
title="Dashboard Entity Directories"
|
||||||
<li v-for="(directory, idx) in filteredDirectories" :key="idx">
|
>
|
||||||
<router-link
|
<div
|
||||||
:to="{
|
v-if="filteredDirectories.length > 0"
|
||||||
name: 'Dashboard',
|
class="directories-section"
|
||||||
params: {
|
>
|
||||||
title: directory,
|
<ul class="directory-list">
|
||||||
entityType: entityType,
|
<li
|
||||||
entityKey: entityKey
|
v-for="(directory, idx) in filteredDirectories"
|
||||||
}
|
:key="idx"
|
||||||
}">
|
>
|
||||||
{{ directory }}
|
<router-link
|
||||||
</router-link>
|
:to="{
|
||||||
</li>
|
name: 'Dashboard',
|
||||||
</ul>
|
params: {
|
||||||
</div>
|
title: directory,
|
||||||
<p v-else>No directories found for this entity.
|
entityType: entityType,
|
||||||
<a href="https://docs.olivetin.app/dashboards/entity-directories.html" target="_blank" rel="noopener noreferrer">Learn more</a>
|
entityKey: entityKey
|
||||||
</p>
|
}
|
||||||
</Section>
|
}"
|
||||||
|
>
|
||||||
|
{{ 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">
|
<section
|
||||||
<div class="dashboard-row">
|
v-if="entityDetails && relatedActions.length > 0"
|
||||||
<fieldset>
|
class="transparent"
|
||||||
<legend class="visually-hidden">Related actions</legend>
|
>
|
||||||
<template v-for="(related, idx) in relatedActions" :key="related.action?.bindingId || idx">
|
<div class="dashboard-row">
|
||||||
<ActionButton
|
<fieldset>
|
||||||
v-if="related.action"
|
<legend class="visually-hidden">
|
||||||
:action-data="related.action"
|
Related actions
|
||||||
:prefilled-arguments="related.prefilledArguments"
|
</legend>
|
||||||
/>
|
<template
|
||||||
</template>
|
v-for="(related, idx) in relatedActions"
|
||||||
</fieldset>
|
:key="related.action?.bindingId || idx"
|
||||||
</div>
|
>
|
||||||
</section>
|
<ActionButton
|
||||||
|
v-if="related.action"
|
||||||
|
:action-data="related.action"
|
||||||
|
:prefilled-arguments="related.prefilledArguments"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { HugeiconsIcon } from '@hugeicons/vue'
|
import { HugeiconsIcon } from '@hugeicons/vue'
|
||||||
import { ArrowLeftIcon } from '@hugeicons/core-free-icons'
|
import { ArrowLeftIcon } from '@hugeicons/core-free-icons'
|
||||||
import Section from 'picocrank/vue/components/Section.vue'
|
import Section from 'picocrank/vue/components/Section.vue'
|
||||||
import ActionButton from '../ActionButton.vue'
|
import ActionButton from '../ActionButton.vue'
|
||||||
import ActionIconGlyph from '../components/ActionIconGlyph.vue'
|
import ActionIconGlyph from '../components/ActionIconGlyph.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const entityDetails = ref(null)
|
const entityDetails = ref(null)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
entityType: String,
|
entityType: String,
|
||||||
entityKey: String
|
entityKey: String
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredDirectories = computed(() => {
|
const filteredDirectories = computed(() => {
|
||||||
if (!entityDetails.value?.directories) {
|
if (!entityDetails.value?.directories) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return entityDetails.value.directories.filter(d => d)
|
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() {
|
function goBack () {
|
||||||
router.push({ name: 'Entities' })
|
router.push({ name: 'Entities' })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchEntityDetails() {
|
async function fetchEntityDetails () {
|
||||||
try {
|
try {
|
||||||
const response = await window.client.getEntity({
|
const response = await window.client.getEntity({
|
||||||
type: props.entityType,
|
type: props.entityType,
|
||||||
uniqueKey: props.entityKey
|
uniqueKey: props.entityKey
|
||||||
})
|
})
|
||||||
|
|
||||||
entityDetails.value = response
|
entityDetails.value = response
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch entity details:', err)
|
console.error('Failed to fetch entity details:', err)
|
||||||
window.showBigError('fetch-entity-details', 'getting entity details', err, false)
|
window.showBigError('fetch-entity-details', 'getting entity details', err, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchEntityDetails()
|
fetchEntityDetails()
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,87 +9,141 @@
|
||||||
class="action-details-title-link"
|
class="action-details-title-link"
|
||||||
:title="titleTooltip"
|
:title="titleTooltip"
|
||||||
>
|
>
|
||||||
<ActionIconGlyph class="action-title-icon" :glyph="icon" />
|
<ActionIconGlyph
|
||||||
<LogActionTitle v-if="logEntry" :action-title="title" :justification="logEntry.justification" />
|
class="action-title-icon"
|
||||||
|
:glyph="icon"
|
||||||
|
/>
|
||||||
|
<LogActionTitle
|
||||||
|
v-if="logEntry"
|
||||||
|
:action-title="title"
|
||||||
|
:justification="logEntry.justification"
|
||||||
|
/>
|
||||||
<span v-else>{{ title }}</span>
|
<span v-else>{{ title }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<template v-else>
|
<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>
|
<span v-else>{{ title }}</span>
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<button
|
<button
|
||||||
v-for="dashboard in backToDashboards"
|
v-for="dashboard in backToDashboards"
|
||||||
:key="dashboard.path"
|
:key="dashboard.path"
|
||||||
@click="goToDashboard(dashboard.path)"
|
:title="'Back to ' + dashboard.title"
|
||||||
:title="'Back to ' + dashboard.title"
|
class="button neutral"
|
||||||
class="button neutral"
|
@click="goToDashboard(dashboard.path)"
|
||||||
>
|
>
|
||||||
<HugeiconsIcon :icon="DashboardSquare01Icon" />
|
<HugeiconsIcon :icon="DashboardSquare01Icon" />
|
||||||
{{ dashboard.title }}
|
{{ dashboard.title }}
|
||||||
</button>
|
</button>
|
||||||
<button v-if="backToDashboards.length === 0" @click="goBack" title="Go back" class="button neutral">
|
<button
|
||||||
<HugeiconsIcon :icon="ArrowLeftIcon" />
|
v-if="backToDashboards.length === 0"
|
||||||
Back
|
title="Go back"
|
||||||
</button>
|
class="button neutral"
|
||||||
|
@click="goBack"
|
||||||
|
>
|
||||||
|
<HugeiconsIcon :icon="ArrowLeftIcon" />
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="logEntry" class = "flex-row">
|
<div
|
||||||
<dl class = "fg1">
|
v-if="logEntry"
|
||||||
<dt>Duration</dt>
|
class="flex-row"
|
||||||
<dd><span v-html="duration"></span></dd>
|
>
|
||||||
|
<dl class="fg1">
|
||||||
|
<dt>Duration</dt>
|
||||||
|
<dd><span v-html="duration" /></dd>
|
||||||
|
|
||||||
<dt>Status</dt>
|
<dt>Status</dt>
|
||||||
<dd class="execution-dialog-status">
|
<dd class="execution-dialog-status">
|
||||||
<ActionStatusDisplay :log-entry="logEntry" :link-queued-status="true" />
|
<ActionStatusDisplay
|
||||||
</dd>
|
:log-entry="logEntry"
|
||||||
</dl>
|
:link-queued-status="true"
|
||||||
|
/>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="notFound" class="error-message padded-content">
|
<div
|
||||||
<h3>Execution Not Found</h3>
|
v-if="notFound"
|
||||||
<p>{{ errorMessage }}</p>
|
class="error-message padded-content"
|
||||||
<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>.
|
<h3>Execution Not Found</h3>
|
||||||
</div>
|
<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-output-container">
|
||||||
<div class="xterm-overlay-toolbar">
|
<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" />
|
<HugeiconsIcon :icon="Copy01Icon" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="xterm-overlay-button" @click="toggleSize" title="Toggle fullscreen">
|
<button
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
type="button"
|
||||||
<path fill="currentColor"
|
class="xterm-overlay-button"
|
||||||
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" />
|
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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div ref="xtermOutput"></div>
|
<div ref="xtermOutput" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br>
|
||||||
|
|
||||||
<div class="flex-row g1 buttons padded-content">
|
<div class="flex-row g1 buttons padded-content">
|
||||||
<div class = "fg1" />
|
<div class="fg1" />
|
||||||
|
|
||||||
<button :disabled="!canRerun" @click="rerunAction" title="Rerun">
|
<button
|
||||||
<HugeiconsIcon :icon="WorkoutRunIcon" />
|
:disabled="!canRerun"
|
||||||
Rerun
|
title="Rerun"
|
||||||
</button>
|
@click="rerunAction"
|
||||||
<button :disabled="!canKill" @click="killAction" title="Kill" id = "execution-dialog-kill-action">
|
>
|
||||||
<HugeiconsIcon :icon="Cancel02Icon" />
|
<HugeiconsIcon :icon="WorkoutRunIcon" />
|
||||||
Kill
|
Rerun
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<button
|
||||||
|
id="execution-dialog-kill-action"
|
||||||
</Section>
|
:disabled="!canKill"
|
||||||
|
title="Kill"
|
||||||
|
@click="killAction"
|
||||||
|
>
|
||||||
|
<HugeiconsIcon :icon="Cancel02Icon" />
|
||||||
|
Kill
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||||
import ActionIconGlyph from '../components/ActionIconGlyph.vue'
|
import ActionIconGlyph from '../components/ActionIconGlyph.vue'
|
||||||
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
|
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
|
||||||
import LogActionTitle from '../components/LogActionTitle.vue'
|
import LogActionTitle from '../components/LogActionTitle.vue'
|
||||||
|
|
@ -113,8 +167,8 @@ const xtermOutput = ref(null)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
executionTrackingId: {
|
executionTrackingId: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -138,7 +192,7 @@ const errorMessage = ref('')
|
||||||
let executionTicker = null
|
let executionTicker = null
|
||||||
let terminal = null
|
let terminal = null
|
||||||
|
|
||||||
function initializeTerminal() {
|
function initializeTerminal () {
|
||||||
terminal = new OutputTerminal(executionTrackingId.value)
|
terminal = new OutputTerminal(executionTrackingId.value)
|
||||||
terminal.open(xtermOutput.value)
|
terminal.open(xtermOutput.value)
|
||||||
terminal.resize(80, 40)
|
terminal.resize(80, 40)
|
||||||
|
|
@ -146,7 +200,7 @@ function initializeTerminal() {
|
||||||
window.terminal = terminal
|
window.terminal = terminal
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSize() {
|
function toggleSize () {
|
||||||
if (!xtermOutput.value) {
|
if (!xtermOutput.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -162,7 +216,7 @@ function toggleSize() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyOutput() {
|
async function copyOutput () {
|
||||||
const text = terminal?.getBufferAsString?.() || logEntry.value?.output || ''
|
const text = terminal?.getBufferAsString?.() || logEntry.value?.output || ''
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return
|
return
|
||||||
|
|
@ -175,7 +229,7 @@ async function copyOutput() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reset() {
|
async function reset () {
|
||||||
executionSeconds.value = 0
|
executionSeconds.value = 0
|
||||||
executionTrackingId.value = 'notset'
|
executionTrackingId.value = 'notset'
|
||||||
hideBasics.value = false
|
hideBasics.value = false
|
||||||
|
|
@ -195,31 +249,31 @@ async function reset() {
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
|
||||||
if (terminal) {
|
if (terminal) {
|
||||||
await terminal.reset()
|
await terminal.reset()
|
||||||
terminal.fit()
|
terminal.fit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function show(actionButton) {
|
function show (actionButton) {
|
||||||
if (actionButton) {
|
if (actionButton) {
|
||||||
icon.value = actionButton.glyph ?? ''
|
icon.value = actionButton.glyph ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
canKill.value = true
|
canKill.value = true
|
||||||
|
|
||||||
// Clear existing ticker
|
// Clear existing ticker
|
||||||
if (executionTicker) {
|
if (executionTicker) {
|
||||||
clearInterval(executionTicker)
|
clearInterval(executionTicker)
|
||||||
}
|
}
|
||||||
|
|
||||||
executionSeconds.value = 0
|
executionSeconds.value = 0
|
||||||
executionTick()
|
executionTick()
|
||||||
executionTicker = setInterval(() => {
|
executionTicker = setInterval(() => {
|
||||||
executionTick()
|
executionTick()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rerunAction() {
|
async function rerunAction () {
|
||||||
const bindingId = logEntry.value?.bindingId
|
const bindingId = logEntry.value?.bindingId
|
||||||
if (!logEntry.value || !bindingId) {
|
if (!logEntry.value || !bindingId) {
|
||||||
console.error('Cannot rerun: no action ID available')
|
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') {
|
if (!executionTrackingId.value || executionTrackingId.value === 'notset') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const killActionArgs = {
|
const killActionArgs = {
|
||||||
executionTrackingId: executionTrackingId.value
|
executionTrackingId: executionTrackingId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await window.client.killAction(killActionArgs)
|
await window.client.killAction(killActionArgs)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to kill action:', err)
|
console.error('Failed to kill action:', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function executionTick() {
|
function executionTick () {
|
||||||
executionSeconds.value++
|
executionSeconds.value++
|
||||||
updateDuration(null)
|
updateDuration(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideEverythingApartFromOutput() {
|
function hideEverythingApartFromOutput () {
|
||||||
hideDetailsOnResult.value = true
|
hideDetailsOnResult.value = true
|
||||||
hideBasics.value = true
|
hideBasics.value = true
|
||||||
hideDetailsOnResult.value = true
|
hideDetailsOnResult.value = true
|
||||||
hideBasics.value = true
|
hideBasics.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchExecutionResult(executionTrackingIdParam) {
|
async function fetchExecutionResult (executionTrackingIdParam) {
|
||||||
console.log("fetchExecutionResult", executionTrackingIdParam)
|
console.log('fetchExecutionResult', executionTrackingIdParam)
|
||||||
|
|
||||||
executionTrackingId.value = executionTrackingIdParam
|
executionTrackingId.value = executionTrackingIdParam
|
||||||
notFound.value = false
|
notFound.value = false
|
||||||
|
|
@ -284,50 +338,49 @@ async function fetchExecutionResult(executionTrackingIdParam) {
|
||||||
backToDashboards.value = []
|
backToDashboards.value = []
|
||||||
|
|
||||||
const executionStatusArgs = {
|
const executionStatusArgs = {
|
||||||
executionTrackingId: executionTrackingId.value
|
executionTrackingId: executionTrackingId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const executionStatusResult = await window.client.executionStatus(executionStatusArgs)
|
const executionStatusResult = await window.client.executionStatus(executionStatusArgs)
|
||||||
|
|
||||||
await renderExecutionResult(executionStatusResult)
|
await renderExecutionResult(executionStatusResult)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Check if it's a "not found" error (404 or similar)
|
// Check if it's a "not found" error (404 or similar)
|
||||||
if (err.status === 404 || err.code === 'NotFound' || err.message?.includes('not found')) {
|
if (err.status === 404 || err.code === 'NotFound' || err.message?.includes('not found')) {
|
||||||
notFound.value = true
|
notFound.value = true
|
||||||
errorMessage.value = err.message || 'The execution could not be found in the system.'
|
errorMessage.value = err.message || 'The execution could not be found in the system.'
|
||||||
} else {
|
} else {
|
||||||
renderError(err)
|
renderError(err)
|
||||||
}
|
}
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDuration(logEntryParam) {
|
function updateDuration (logEntryParam) {
|
||||||
logEntry.value = logEntryParam
|
logEntry.value = logEntryParam
|
||||||
if (logEntry.value == null) {
|
if (logEntry.value == null) {
|
||||||
duration.value = executionSeconds.value + ' seconds'
|
duration.value = executionSeconds.value + ' seconds'
|
||||||
duration.value = duration.value
|
|
||||||
} else if (!logEntry.value.executionStarted) {
|
} 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) {
|
} else if (logEntry.value.executionStarted && !logEntry.value.executionFinished) {
|
||||||
duration.value = logEntry.value.datetimeStarted
|
duration.value = logEntry.value.datetimeStarted
|
||||||
} else {
|
} else {
|
||||||
let delta = ''
|
let delta = ''
|
||||||
try {
|
try {
|
||||||
delta = (new Date(logEntry.value.datetimeFinished) - new Date(logEntry.value.datetimeStarted)) / 1000
|
delta = (new Date(logEntry.value.datetimeFinished) - new Date(logEntry.value.datetimeStarted)) / 1000
|
||||||
delta = new Intl.RelativeTimeFormat().format(delta, 'seconds').replace('in ', '').replace('ago', '')
|
delta = new Intl.RelativeTimeFormat().format(delta, 'seconds').replace('in ', '').replace('ago', '')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to calculate delta', e)
|
console.warn('Failed to calculate delta', e)
|
||||||
}
|
}
|
||||||
duration.value = logEntry.value.datetimeStarted + ' → ' + logEntry.value.datetimeFinished
|
duration.value = logEntry.value.datetimeStarted + ' → ' + logEntry.value.datetimeFinished
|
||||||
if (delta !== '') {
|
if (delta !== '') {
|
||||||
duration.value += ' (' + delta + ')'
|
duration.value += ' (' + delta + ')'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderExecutionResult(res) {
|
async function renderExecutionResult (res) {
|
||||||
logEntry.value = res.logEntry
|
logEntry.value = res.logEntry
|
||||||
if (res.backToDashboards) {
|
if (res.backToDashboards) {
|
||||||
backToDashboards.value = res.backToDashboards.slice(0, 3)
|
backToDashboards.value = res.backToDashboards.slice(0, 3)
|
||||||
|
|
@ -335,12 +388,12 @@ async function renderExecutionResult(res) {
|
||||||
|
|
||||||
// Clear ticker
|
// Clear ticker
|
||||||
if (executionTicker) {
|
if (executionTicker) {
|
||||||
clearInterval(executionTicker)
|
clearInterval(executionTicker)
|
||||||
}
|
}
|
||||||
executionTicker = null
|
executionTicker = null
|
||||||
|
|
||||||
if (hideDetailsOnResult.value) {
|
if (hideDetailsOnResult.value) {
|
||||||
hideDetails.value = true
|
hideDetails.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
executionTrackingId.value = res.logEntry.executionTrackingId
|
executionTrackingId.value = res.logEntry.executionTrackingId
|
||||||
|
|
@ -355,41 +408,41 @@ async function renderExecutionResult(res) {
|
||||||
updateDuration(res.logEntry)
|
updateDuration(res.logEntry)
|
||||||
|
|
||||||
if (terminal) {
|
if (terminal) {
|
||||||
await terminal.reset()
|
await terminal.reset()
|
||||||
await terminal.write(res.logEntry.output, () => {
|
await terminal.write(res.logEntry.output, () => {
|
||||||
terminal.fit()
|
terminal.fit()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderError(err) {
|
function renderError (err) {
|
||||||
window.showBigError('execution-dlg-err', 'in the execution dialog', 'Failed to fetch execution result. ' + err, false)
|
window.showBigError('execution-dlg-err', 'in the execution dialog', 'Failed to fetch execution result. ' + err, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose () {
|
||||||
if (executionTicker) {
|
if (executionTicker) {
|
||||||
clearInterval(executionTicker)
|
clearInterval(executionTicker)
|
||||||
}
|
}
|
||||||
|
|
||||||
executionTicker = null
|
executionTicker = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanup() {
|
function cleanup () {
|
||||||
if (executionTicker) {
|
if (executionTicker) {
|
||||||
clearInterval(executionTicker)
|
clearInterval(executionTicker)
|
||||||
}
|
}
|
||||||
executionTicker = null
|
executionTicker = null
|
||||||
if (terminal != null) {
|
if (terminal != null) {
|
||||||
terminal.close()
|
terminal.close()
|
||||||
}
|
}
|
||||||
terminal = null
|
terminal = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function goBack() {
|
function goBack () {
|
||||||
router.back()
|
router.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToDashboard(path) {
|
function goToDashboard (path) {
|
||||||
router.push(path)
|
router.push(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -409,16 +462,15 @@ onMounted(() => {
|
||||||
fetchExecutionResult(props.executionTrackingId)
|
fetchExecutionResult(props.executionTrackingId)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => buttonResults[props.executionTrackingId],
|
() => buttonResults[props.executionTrackingId],
|
||||||
(newResult, oldResult) => {
|
(newResult, oldResult) => {
|
||||||
if (newResult) {
|
if (newResult) {
|
||||||
renderExecutionResult({
|
renderExecutionResult({
|
||||||
logEntry: newResult
|
logEntry: newResult
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,83 @@
|
||||||
<template>
|
<template>
|
||||||
<Section title="Login to OliveTin" class="small">
|
<Section
|
||||||
|
title="Login to OliveTin"
|
||||||
|
class="small"
|
||||||
|
>
|
||||||
<div class="login-form">
|
<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>
|
<span>This server is not configured with either OAuth, or local users, so you cannot login.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="hasOAuth" class="login-oauth2">
|
<div
|
||||||
|
v-if="hasOAuth"
|
||||||
|
class="login-oauth2"
|
||||||
|
>
|
||||||
<h3>OAuth Login</h3>
|
<h3>OAuth Login</h3>
|
||||||
<div class="oauth-providers">
|
<div class="oauth-providers">
|
||||||
<button v-for="provider in oauthProviders" :key="provider.key" class="oauth-button"
|
<button
|
||||||
@click="loginWithOAuth(provider)">
|
v-for="provider in oauthProviders"
|
||||||
<span v-if="provider.icon" class="provider-icon" v-html="provider.icon"></span>
|
: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>
|
<span class="provider-name">Login with {{ provider.title }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="hasLocalLogin" class="login-local">
|
<div
|
||||||
|
v-if="hasLocalLogin"
|
||||||
|
class="login-local"
|
||||||
|
>
|
||||||
<h3>Local Login</h3>
|
<h3>Local Login</h3>
|
||||||
<form @submit.prevent="handleLocalLogin" class="local-login-form">
|
<form
|
||||||
<div v-if="loginError" class="bad">
|
class="local-login-form"
|
||||||
|
@submit.prevent="handleLocalLogin"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="loginError"
|
||||||
|
class="bad"
|
||||||
|
>
|
||||||
{{ loginError }}
|
{{ loginError }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input id="username" v-model="username" type="text" name="username" autocomplete="username" required placeholder="Username" />
|
<input
|
||||||
<input id="password" v-model="password" type="password" name="password" autocomplete="current-password" placeholder="Password"
|
id="username"
|
||||||
required />
|
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' }}
|
{{ loading ? 'Logging in...' : 'Login' }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -51,7 +101,39 @@ const hasOAuth = ref(false)
|
||||||
const hasLocalLogin = ref(false)
|
const hasLocalLogin = ref(false)
|
||||||
const oauthProviders = ref([])
|
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
|
// Use the init response data that was loaded in App.vue
|
||||||
if (window.initResponse) {
|
if (window.initResponse) {
|
||||||
hasOAuth.value = window.initResponse.oAuth2Providers && window.initResponse.oAuth2Providers.length > 0
|
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
|
loading.value = true
|
||||||
loginError.value = ''
|
loginError.value = ''
|
||||||
|
|
||||||
|
|
@ -83,7 +165,7 @@ async function handleLocalLogin() {
|
||||||
window.initError = false
|
window.initError = false
|
||||||
window.initErrorMessage = ''
|
window.initErrorMessage = ''
|
||||||
window.initCompleted = true
|
window.initCompleted = true
|
||||||
|
|
||||||
// Update the header with new user info
|
// Update the header with new user info
|
||||||
if (window.updateHeaderFromInit) {
|
if (window.updateHeaderFromInit) {
|
||||||
window.updateHeaderFromInit()
|
window.updateHeaderFromInit()
|
||||||
|
|
@ -91,7 +173,7 @@ async function handleLocalLogin() {
|
||||||
} catch (initErr) {
|
} catch (initErr) {
|
||||||
console.error('Failed to reinitialize after login:', initErr)
|
console.error('Failed to reinitialize after login:', initErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to home page on successful login
|
// Redirect to home page on successful login
|
||||||
router.push('/')
|
router.push('/')
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -105,21 +187,21 @@ async function handleLocalLogin() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loginWithOAuth(provider) {
|
function loginWithOAuth (provider) {
|
||||||
if (!provider.key) {
|
if (!provider.key) {
|
||||||
console.error('OAuth provider missing key:', provider)
|
console.error('OAuth provider missing key:', provider)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerKey = encodeURIComponent(provider.key)
|
const providerKey = encodeURIComponent(provider.key)
|
||||||
window.location.href = `/oauth/login?provider=${providerKey}`
|
window.location.href = `/oauth/login?provider=${providerKey}`
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadLoginOptions()
|
loadLoginOptions()
|
||||||
|
|
||||||
// Also watch for when init response becomes available
|
// Also watch for when init response becomes available
|
||||||
const stopWatcher = watch(() => window.initResponse, () => {
|
watch(() => window.initResponse, () => {
|
||||||
loadLoginOptions()
|
loadLoginOptions()
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<Section :title="t('logs.calendar-title')" :padding="false">
|
<Section
|
||||||
|
:title="t('logs.calendar-title')"
|
||||||
|
:padding="false"
|
||||||
|
>
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<router-link to="/logs" class="button neutral">
|
<router-link
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
to="/logs"
|
||||||
<path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8l8 8l1.41-1.41L7.83 13H20z"/>
|
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>
|
</svg>
|
||||||
{{ t('logs.back-to-list') }}
|
{{ t('logs.back-to-list') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
@ -52,7 +66,7 @@ const calendarEvents = computed(() => {
|
||||||
.map(log => {
|
.map(log => {
|
||||||
const startDate = new Date(log.datetimeStarted)
|
const startDate = new Date(log.datetimeStarted)
|
||||||
let endDate = log.datetimeFinished ? new Date(log.datetimeFinished) : null
|
let endDate = log.datetimeFinished ? new Date(log.datetimeFinished) : null
|
||||||
|
|
||||||
// Validate end date
|
// Validate end date
|
||||||
if (endDate && isNaN(endDate.getTime())) {
|
if (endDate && isNaN(endDate.getTime())) {
|
||||||
endDate = null
|
endDate = null
|
||||||
|
|
@ -62,8 +76,8 @@ const calendarEvents = computed(() => {
|
||||||
id: log.executionTrackingId,
|
id: log.executionTrackingId,
|
||||||
title: log.actionTitle || 'Untitled Action',
|
title: log.actionTitle || 'Untitled Action',
|
||||||
date: startDate,
|
date: startDate,
|
||||||
startDate: startDate,
|
startDate,
|
||||||
endDate: endDate,
|
endDate,
|
||||||
actionIcon: log.actionIcon,
|
actionIcon: log.actionIcon,
|
||||||
user: log.user,
|
user: log.user,
|
||||||
tags: log.tags,
|
tags: log.tags,
|
||||||
|
|
@ -72,44 +86,43 @@ const calendarEvents = computed(() => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
async function fetchLogs() {
|
async function fetchLogs () {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Currently fetches only the default page (startOffset: 0)
|
// Currently fetches only the default page (startOffset: 0)
|
||||||
// Multi-page fetching: loop through pages until no more logs or limit reached
|
// Multi-page fetching: loop through pages until no more logs or limit reached
|
||||||
const allLogs = []
|
const allLogs = []
|
||||||
let startOffset = BigInt(0)
|
let startOffset = BigInt(0)
|
||||||
const maxLogs = 10000 // Reasonable limit to prevent excessive API calls
|
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) {
|
while (allLogs.length < maxLogs) {
|
||||||
const args = {
|
const args = {
|
||||||
"startOffset": startOffset,
|
startOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await window.client.getLogs(args)
|
const response = await window.client.getLogs(args)
|
||||||
const pageLogs = response.logs || []
|
const pageLogs = response.logs || []
|
||||||
|
|
||||||
// If no logs returned, we've reached the end
|
// If no logs returned, we've reached the end
|
||||||
if (pageLogs.length === 0) {
|
if (pageLogs.length === 0) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append logs from this page
|
// Append logs from this page
|
||||||
allLogs.push(...pageLogs)
|
allLogs.push(...pageLogs)
|
||||||
|
|
||||||
// Update offset for next page
|
// Update offset for next page
|
||||||
const currentPageSize = Number(response.pageSize) || pageLogs.length
|
const currentPageSize = Number(response.pageSize) || pageLogs.length
|
||||||
startOffset += BigInt(currentPageSize)
|
startOffset += BigInt(currentPageSize)
|
||||||
|
|
||||||
// If we got fewer logs than the page size, we've reached the end
|
// If we got fewer logs than the page size, we've reached the end
|
||||||
if (pageLogs.length < currentPageSize) {
|
if (pageLogs.length < currentPageSize) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logs.value = allLogs
|
logs.value = allLogs
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch logs:', 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
|
// Navigate to the execution view when clicking on a calendar event
|
||||||
if (event.id) {
|
if (event.id) {
|
||||||
router.push(`/logs/${event.id}`)
|
router.push(`/logs/${event.id}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDayClick(date) {
|
function handleDayClick (date) {
|
||||||
// Navigate to logs list filtered by the selected date
|
// Navigate to logs list filtered by the selected date
|
||||||
// Format date as YYYY-MM-DD using local date components to avoid timezone issues
|
// Format date as YYYY-MM-DD using local date components to avoid timezone issues
|
||||||
const year = date.getFullYear()
|
const year = date.getFullYear()
|
||||||
|
|
@ -137,7 +150,7 @@ function handleDayClick(date) {
|
||||||
router.push({ path: '/logs', query: { date: dateString } })
|
router.push({ path: '/logs', query: { date: dateString } })
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMonthChange(month, year) {
|
function handleMonthChange (month, year) {
|
||||||
currentMonthIndex.value = month
|
currentMonthIndex.value = month
|
||||||
currentYear.value = year
|
currentYear.value = year
|
||||||
// Optionally fetch logs for the new month if needed
|
// Optionally fetch logs for the new month if needed
|
||||||
|
|
|
||||||
|
|
@ -1,116 +1,212 @@
|
||||||
<template>
|
<template>
|
||||||
<Section :title="t('logs.title')" :padding="false">
|
<Section
|
||||||
<template #toolbar>
|
:title="t('logs.title')"
|
||||||
<router-link to="/logs/queue" class="button neutral">
|
:padding="false"
|
||||||
{{ t('logs.queue') }}
|
>
|
||||||
</router-link>
|
<template #toolbar>
|
||||||
<router-link to="/logs/calendar" class="button neutral">
|
<router-link
|
||||||
{{ t('logs.calendar') }}
|
to="/logs/queue"
|
||||||
</router-link>
|
class="button neutral"
|
||||||
<label class="input-with-icons">
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
{{ t('logs.queue') }}
|
||||||
<path fill="currentColor"
|
</router-link>
|
||||||
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" />
|
<router-link
|
||||||
</svg>
|
to="/logs/calendar"
|
||||||
<input
|
class="button neutral"
|
||||||
:placeholder="t('logs.filter-placeholder')"
|
>
|
||||||
v-model="searchText"
|
{{ t('logs.calendar') }}
|
||||||
list="logs-filter-suggestions"
|
</router-link>
|
||||||
:aria-invalid="filterError ? 'true' : 'false'"
|
<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">
|
</svg>
|
||||||
<option v-for="suggestion in filterSuggestions" :key="suggestion" :value="suggestion" />
|
<input
|
||||||
</datalist>
|
v-model="searchText"
|
||||||
<button :title="t('logs.clear-filter')" :disabled="!searchText" @click="clearSearch">
|
:placeholder="t('logs.filter-placeholder')"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
list="logs-filter-suggestions"
|
||||||
<path fill="currentColor"
|
:aria-invalid="filterError ? 'true' : 'false'"
|
||||||
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>
|
<datalist id="logs-filter-suggestions">
|
||||||
</button>
|
<option
|
||||||
</label>
|
v-for="suggestion in filterSuggestions"
|
||||||
</template>
|
:key="suggestion"
|
||||||
|
:value="suggestion"
|
||||||
<div class="padding logs-intro">
|
/>
|
||||||
<p>{{ t('logs.page-description') }}</p>
|
</datalist>
|
||||||
<details class="filter-help">
|
<button
|
||||||
<summary>{{ t('logs.filter-help-title') }}</summary>
|
:title="t('logs.clear-filter')"
|
||||||
<p>{{ t('logs.filter-help-intro') }}</p>
|
:disabled="!searchText"
|
||||||
<p>{{ t('logs.filter-help-fields') }}</p>
|
@click="clearSearch"
|
||||||
<p><code>{{ t('logs.filter-help-examples') }}</code></p>
|
>
|
||||||
</details>
|
<svg
|
||||||
<p v-if="filterError" class="filter-error" role="alert">{{ filterError }}</p>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</div>
|
width="1em"
|
||||||
|
height="1em"
|
||||||
<div v-show="logs.length > 0">
|
viewBox="0 0 24 24"
|
||||||
<table class="logs-table row-hover">
|
>
|
||||||
<thead>
|
<path
|
||||||
<tr>
|
fill="currentColor"
|
||||||
<th>
|
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"
|
||||||
<div class="timestamp-header">
|
/>
|
||||||
<span>{{ t('logs.timestamp') }}</span>
|
</svg>
|
||||||
<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') }}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div v-show="selectedDate && logs.length === 0 && !loading && !searchText" class="empty-state padding">
|
<div class="padding logs-intro">
|
||||||
<p>{{ t('logs.no-logs-to-display') }} {{ formatDateFilter(selectedDate) }}.</p>
|
<p>{{ t('logs.page-description') }}</p>
|
||||||
<button @click="clearDateFilter" class="button neutral">
|
<details class="filter-help">
|
||||||
{{ t('logs.clear-date-filter') }}
|
<summary>{{ t('logs.filter-help-title') }}</summary>
|
||||||
</button>
|
<p>{{ t('logs.filter-help-intro') }}</p>
|
||||||
</div>
|
<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">
|
<div v-show="logs.length > 0">
|
||||||
<p>{{ t('logs.no-logs-to-display') }}</p>
|
<table class="logs-table row-hover">
|
||||||
<router-link to="/">{{ t('return-to-index') }}</router-link>
|
<thead>
|
||||||
</div>
|
<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>
|
</Section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -150,7 +246,7 @@ const filterSuggestions = [
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
function updateDateFromRoute() {
|
function updateDateFromRoute () {
|
||||||
const dateParam = route.query.date
|
const dateParam = route.query.date
|
||||||
if (dateParam) {
|
if (dateParam) {
|
||||||
selectedDate.value = dateParam
|
selectedDate.value = dateParam
|
||||||
|
|
@ -169,7 +265,7 @@ watch(searchText, () => {
|
||||||
scheduleFetchLogs()
|
scheduleFetchLogs()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function fetchLogs() {
|
async function fetchLogs () {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
filterError.value = ''
|
filterError.value = ''
|
||||||
try {
|
try {
|
||||||
|
|
@ -206,7 +302,7 @@ async function fetchLogs() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleFetchLogs() {
|
function scheduleFetchLogs () {
|
||||||
if (fetchTimer) {
|
if (fetchTimer) {
|
||||||
clearTimeout(fetchTimer)
|
clearTimeout(fetchTimer)
|
||||||
}
|
}
|
||||||
|
|
@ -215,18 +311,18 @@ function scheduleFetchLogs() {
|
||||||
}, 400)
|
}, 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSearch() {
|
function clearSearch () {
|
||||||
searchText.value = ''
|
searchText.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearDateFilter() {
|
function clearDateFilter () {
|
||||||
selectedDate.value = null
|
selectedDate.value = null
|
||||||
const query = { ...route.query }
|
const query = { ...route.query }
|
||||||
delete query.date
|
delete query.date
|
||||||
router.push({ path: route.path, query })
|
router.push({ path: route.path, query })
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateFilter(dateString) {
|
function formatDateFilter (dateString) {
|
||||||
try {
|
try {
|
||||||
const date = new Date(dateString + 'T00:00:00')
|
const date = new Date(dateString + 'T00:00:00')
|
||||||
return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
|
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'
|
if (!timestamp) return 'Unknown'
|
||||||
try {
|
try {
|
||||||
const date = new Date(timestamp)
|
const date = new Date(timestamp)
|
||||||
|
|
@ -245,18 +341,18 @@ function formatTimestamp(timestamp) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePageChange(page) {
|
function handlePageChange (page) {
|
||||||
currentPage.value = page
|
currentPage.value = page
|
||||||
fetchLogs()
|
fetchLogs()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePageSizeChange(newPageSize) {
|
function handlePageSizeChange (newPageSize) {
|
||||||
pageSize.value = newPageSize
|
pageSize.value = newPageSize
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
fetchLogs()
|
fetchLogs()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onExecutionEvent(evt) {
|
function onExecutionEvent (evt) {
|
||||||
const logEntry = getExecutionLogEntry(evt)
|
const logEntry = getExecutionLogEntry(evt)
|
||||||
if (!logEntry) {
|
if (!logEntry) {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,40 @@
|
||||||
<template>
|
<template>
|
||||||
<Section :title="t('logs.queue-title')" :padding="false">
|
<Section
|
||||||
|
:title="t('logs.queue-title')"
|
||||||
|
:padding="false"
|
||||||
|
>
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<router-link to="/logs" class="button neutral">
|
<router-link
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
to="/logs"
|
||||||
<path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8l8 8l1.41-1.41L7.83 13H20z"/>
|
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>
|
</svg>
|
||||||
{{ t('logs.back-to-list') }}
|
{{ t('logs.back-to-list') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</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>
|
<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>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
|
@ -24,7 +45,10 @@
|
||||||
>
|
>
|
||||||
<div class="section-header flex-row">
|
<div class="section-header flex-row">
|
||||||
<div class="fg1 queue-action-group-heading">
|
<div class="fg1 queue-action-group-heading">
|
||||||
<ActionIconGlyph class="icon" :glyph="actionGroup.icon" />
|
<ActionIconGlyph
|
||||||
|
class="icon"
|
||||||
|
:glyph="actionGroup.icon"
|
||||||
|
/>
|
||||||
<h2>{{ displayActionGroupName(actionGroup.name) }}</h2>
|
<h2>{{ displayActionGroupName(actionGroup.name) }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<ActionGroupLimitsLabel
|
<ActionGroupLimitsLabel
|
||||||
|
|
@ -44,23 +68,34 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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
|
<tr
|
||||||
v-for="(entry, index) in action.entries"
|
v-for="(entry, index) in action.entries"
|
||||||
:key="entry.executionTrackingId"
|
:key="entry.executionTrackingId"
|
||||||
class="log-row"
|
class="log-row"
|
||||||
:title="action.actionTitle"
|
:title="action.actionTitle"
|
||||||
>
|
>
|
||||||
<td class="timestamp">{{ formatTimestamp(entry.datetimeStarted) }}</td>
|
<td class="timestamp">
|
||||||
|
{{ formatTimestamp(entry.datetimeStarted) }}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<ActionIconGlyph class="icon" :glyph="action.actionIcon" />
|
<ActionIconGlyph
|
||||||
|
class="icon"
|
||||||
|
:glyph="action.actionIcon"
|
||||||
|
/>
|
||||||
<router-link :to="`/logs/${entry.executionTrackingId}`">
|
<router-link :to="`/logs/${entry.executionTrackingId}`">
|
||||||
<LogActionTitle
|
<LogActionTitle
|
||||||
:action-title="action.actionTitle"
|
:action-title="action.actionTitle"
|
||||||
:justification="entry.justification"
|
:justification="entry.justification"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<span v-if="action.entityPrefix" class="queue-entity annotation">
|
<span
|
||||||
|
v-if="action.entityPrefix"
|
||||||
|
class="queue-entity annotation"
|
||||||
|
>
|
||||||
{{ action.entityPrefix }}
|
{{ action.entityPrefix }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -69,15 +104,28 @@
|
||||||
<span class="annotation-key">User:</span>
|
<span class="annotation-key">User:</span>
|
||||||
<span class="annotation-val">{{ entry.user }}</span>
|
<span class="annotation-val">{{ entry.user }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="entry.tags && entry.tags.length > 0" class="tag-list">
|
<span
|
||||||
<span v-for="tag in entry.tags" :key="tag" class="tag">{{ tag }}</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>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="exit-code">
|
<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 }) }}
|
{{ t('logs.queue-position', { position: index + 1 }) }}
|
||||||
</span>
|
</span>
|
||||||
<ActionStatusDisplay :logEntry="entry" link-queued-status />
|
<ActionStatusDisplay
|
||||||
|
:log-entry="entry"
|
||||||
|
link-queued-status
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,18 @@
|
||||||
<div class="not-found-content">
|
<div class="not-found-content">
|
||||||
<h1>404</h1>
|
<h1>404</h1>
|
||||||
<h2>Page Not Found</h2>
|
<h2>Page Not Found</h2>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class = "button good" @click="goToHome">
|
<button
|
||||||
|
class="button good"
|
||||||
|
@click="goToHome"
|
||||||
|
>
|
||||||
Go to Home
|
Go to Home
|
||||||
</button>
|
</button>
|
||||||
<button class="button neutral" @click="goBack">
|
<button
|
||||||
|
class="button neutral"
|
||||||
|
@click="goBack"
|
||||||
|
>
|
||||||
Go Back
|
Go Back
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -22,10 +28,10 @@
|
||||||
export default {
|
export default {
|
||||||
name: 'NotFoundView',
|
name: 'NotFoundView',
|
||||||
methods: {
|
methods: {
|
||||||
goBack() {
|
goBack () {
|
||||||
this.$router.go(-1)
|
this.$router.go(-1)
|
||||||
},
|
},
|
||||||
goToHome() {
|
goToHome () {
|
||||||
this.$router.push('/')
|
this.$router.push('/')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -57,4 +63,4 @@ export default {
|
||||||
color: #666;
|
color: #666;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,58 @@
|
||||||
<template>
|
<template>
|
||||||
<Section title="User Information" class="small">
|
<Section
|
||||||
<div v-if="!isLoggedIn" class="user-not-logged-in">
|
title="User Information"
|
||||||
|
class="small"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!isLoggedIn"
|
||||||
|
class="user-not-logged-in"
|
||||||
|
>
|
||||||
<p>You are not currently logged in.</p>
|
<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>
|
||||||
|
|
||||||
<div v-else class="user-control-panel">
|
<div
|
||||||
|
v-else
|
||||||
|
class="user-control-panel"
|
||||||
|
>
|
||||||
<dl class="user-info">
|
<dl class="user-info">
|
||||||
<dt>Username</dt>
|
<dt>Username</dt>
|
||||||
<dd>{{ username }}</dd>
|
<dd>{{ username }}</dd>
|
||||||
<dt v-if="userProvider !== 'system'">Provider</dt>
|
<dt v-if="userProvider !== 'system'">
|
||||||
<dd v-if="userProvider !== 'system'">{{ userProvider }}</dd>
|
Provider
|
||||||
<dt v-if="usergroup">Group</dt>
|
</dt>
|
||||||
<dd v-if="usergroup">{{ usergroup }}</dd>
|
<dd v-if="userProvider !== 'system'">
|
||||||
<dt v-if="acls && acls.length > 0">Matched ACLs</dt>
|
{{ 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">
|
<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>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<div class="user-actions">
|
<div class="user-actions">
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button @click="handleLogout" class="button bad" :disabled="loggingOut">
|
<button
|
||||||
|
class="button bad"
|
||||||
|
:disabled="loggingOut"
|
||||||
|
@click="handleLogout"
|
||||||
|
>
|
||||||
{{ loggingOut ? 'Logging out...' : 'Logout' }}
|
{{ loggingOut ? 'Logging out...' : 'Logout' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -31,7 +62,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import Section from 'picocrank/vue/components/Section.vue'
|
import Section from 'picocrank/vue/components/Section.vue'
|
||||||
|
|
||||||
|
|
@ -44,7 +75,7 @@ const usergroup = ref('')
|
||||||
const loggingOut = ref(false)
|
const loggingOut = ref(false)
|
||||||
const acls = ref([])
|
const acls = ref([])
|
||||||
|
|
||||||
function updateUserInfo() {
|
function updateUserInfo () {
|
||||||
if (window.initResponse) {
|
if (window.initResponse) {
|
||||||
isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
|
isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
|
||||||
username.value = window.initResponse.authenticatedUser
|
username.value = window.initResponse.authenticatedUser
|
||||||
|
|
@ -53,7 +84,7 @@ function updateUserInfo() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchWhoAmI() {
|
async function fetchWhoAmI () {
|
||||||
try {
|
try {
|
||||||
const res = await window.client.whoAmI({})
|
const res = await window.client.whoAmI({})
|
||||||
acls.value = res.acls || []
|
acls.value = res.acls || []
|
||||||
|
|
@ -67,12 +98,12 @@ async function fetchWhoAmI() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLogout() {
|
async function handleLogout () {
|
||||||
loggingOut.value = true
|
loggingOut.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await window.client.logout({})
|
await window.client.logout({})
|
||||||
|
|
||||||
// Re-initialize to get updated user context (should be guest)
|
// Re-initialize to get updated user context (should be guest)
|
||||||
try {
|
try {
|
||||||
const initResponse = await window.client.init({})
|
const initResponse = await window.client.init({})
|
||||||
|
|
@ -80,7 +111,7 @@ async function handleLogout() {
|
||||||
window.initError = false
|
window.initError = false
|
||||||
window.initErrorMessage = ''
|
window.initErrorMessage = ''
|
||||||
window.initCompleted = true
|
window.initCompleted = true
|
||||||
|
|
||||||
// Update the header with new user info
|
// Update the header with new user info
|
||||||
if (window.updateHeaderFromInit) {
|
if (window.updateHeaderFromInit) {
|
||||||
window.updateHeaderFromInit()
|
window.updateHeaderFromInit()
|
||||||
|
|
@ -88,7 +119,7 @@ async function handleLogout() {
|
||||||
} catch (initErr) {
|
} catch (initErr) {
|
||||||
console.error('Failed to reinitialize after logout:', initErr)
|
console.error('Failed to reinitialize after logout:', initErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect based on init response: if login is required, go to login page
|
// Redirect based on init response: if login is required, go to login page
|
||||||
if (window.initResponse && window.initResponse.loginRequired) {
|
if (window.initResponse && window.initResponse.loginRequired) {
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
|
|
@ -102,13 +133,12 @@ async function handleLogout() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let watchInterval = null
|
const watchInterval = null
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
updateUserInfo()
|
updateUserInfo()
|
||||||
fetchWhoAmI()
|
fetchWhoAmI()
|
||||||
|
})
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (watchInterval) {
|
if (watchInterval) {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^6.2.2",
|
"chai": "^6.2.2",
|
||||||
"eslint": "^10.5.0",
|
"eslint": "^10.6.0",
|
||||||
"mocha": "^11.7.6",
|
"mocha": "^11.7.6",
|
||||||
"selenium-webdriver": "^4.45.0"
|
"selenium-webdriver": "^4.45.0"
|
||||||
}
|
}
|
||||||
|
|
@ -783,9 +783,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "10.5.0",
|
"version": "10.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz",
|
||||||
"integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==",
|
"integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^6.2.2",
|
"chai": "^6.2.2",
|
||||||
"eslint": "^10.5.0",
|
"eslint": "^10.6.0",
|
||||||
"mocha": "^11.7.6",
|
"mocha": "^11.7.6",
|
||||||
"selenium-webdriver": "^4.45.0"
|
"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
|
module github.com/OliveTin/OliveTin/langtool
|
||||||
|
|
||||||
go 1.24.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
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
|
github.com/sirupsen/logrus v1.9.4
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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-20260510214136-6ef80e0ce8da h1:hYsJqujd3A4Xtp9swe2d6Y6ij2ecd16E+i2oHAH/xaA=
|
||||||
github.com/jamesread/golure v0.0.0-20250919212919-976d085a100c/go.mod h1:BZ/CMtZJJ4LNEBDSjGfafTJMjlDPIA9FS16+reN9NUE=
|
github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/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 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
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.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
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=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
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/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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)",
|
"Go codestyle (install via: make go-tools)",
|
||||||
frozenset({"gocyclo", "gocritic"}),
|
frozenset({"golangci-lint"}),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"Protocol buffers (install via: make -C service go-tools-all)",
|
"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
|
codestyle: go-tools
|
||||||
go fmt ./...
|
go fmt ./...
|
||||||
go vet ./...
|
go vet ./...
|
||||||
gocyclo -over 4 internal
|
golangci-lint run ./...
|
||||||
gocritic check ./...
|
|
||||||
|
|
||||||
test: unittests
|
test: unittests
|
||||||
|
|
||||||
|
|
@ -54,13 +53,11 @@ find-flakey-tests-inf:
|
||||||
go run ./scripts/find-flakey-tests-inf
|
go run ./scripts/find-flakey-tests-inf
|
||||||
|
|
||||||
go-tools:
|
go-tools:
|
||||||
go install "github.com/fzipp/gocyclo/cmd/gocyclo"
|
go install "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2"
|
||||||
go install "github.com/go-critic/go-critic/cmd/gocritic"
|
|
||||||
|
|
||||||
.PHONY: unittests unittests-fast find-flakey-tests find-flakey-tests-inf
|
.PHONY: unittests unittests-fast find-flakey-tests find-flakey-tests-inf
|
||||||
|
|
||||||
go-tools-all:
|
go-tools-all:
|
||||||
go install "github.com/bufbuild/buf/cmd/buf"
|
go install "github.com/bufbuild/buf/cmd/buf"
|
||||||
go install "github.com/fzipp/gocyclo/cmd/gocyclo"
|
go install "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2"
|
||||||
go install "github.com/go-critic/go-critic/cmd/gocritic"
|
|
||||||
go install "google.golang.org/protobuf/cmd/protoc-gen-go"
|
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
|
module github.com/OliveTin/OliveTin
|
||||||
|
|
||||||
go 1.25.10
|
go 1.26
|
||||||
|
|
||||||
exclude google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884
|
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/MicahParks/keyfunc/v3 v3.8.0
|
||||||
github.com/PaesslerAG/jsonpath v0.1.1
|
github.com/PaesslerAG/jsonpath v0.1.1
|
||||||
github.com/alexedwards/argon2id v1.0.0
|
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/fsnotify/fsnotify v1.10.1
|
||||||
github.com/fzipp/gocyclo v0.6.0
|
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||||
github.com/go-critic/go-critic v0.14.3
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
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/google/uuid v1.6.0
|
||||||
github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da
|
github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da
|
||||||
github.com/knadh/koanf/parsers/yaml v1.1.0
|
github.com/knadh/koanf/parsers/yaml v1.1.0
|
||||||
github.com/knadh/koanf/providers/env 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/file v1.2.1
|
||||||
github.com/knadh/koanf/providers/rawbytes v1.0.0
|
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/prometheus/client_golang v1.23.2
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/sirupsen/logrus v1.9.4
|
github.com/sirupsen/logrus v1.9.4
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
go.akshayshah.org/connectproto v0.6.0
|
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/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
|
google.golang.org/protobuf v1.36.11
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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/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/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/connectrpc/go v1.20.0-20260629211719-be09125d9193.1 // indirect
|
||||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260507063250-43b0c5a6cd08.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/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/app v0.2.1-0.20260407195847-833f8f978cda // indirect
|
||||||
buf.build/go/bufplugin v0.10.0 // 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/spdx v0.2.0 // indirect
|
||||||
buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 // indirect
|
buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 // indirect
|
||||||
cel.dev/expr v0.25.2 // 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
|
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/MicahParks/jwkset v0.11.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // 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/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/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/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/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/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/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 v1.0.0 // indirect
|
||||||
github.com/containerd/errdefs/pkg v0.3.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/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/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/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/cli v29.5.2+incompatible // indirect
|
github.com/dlclark/regexp2 v1.12.0 // indirect
|
||||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
github.com/docker/cli v29.6.1+incompatible // indirect
|
||||||
github.com/docker/docker v28.5.2+incompatible // indirect
|
github.com/docker/docker-credential-helpers v0.9.8 // indirect
|
||||||
github.com/docker/docker-credential-helpers v0.9.7 // indirect
|
|
||||||
github.com/docker/go-connections v0.7.0 // indirect
|
github.com/docker/go-connections v0.7.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/expr-lang/expr v1.17.8 // indirect
|
github.com/ettle/strcase v0.2.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/fatih/color v1.19.0 // indirect
|
||||||
github.com/go-chi/chi/v5 v5.2.5 // 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/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-toolsmith/astcast v1.1.0 // 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/astequal v1.2.0 // indirect
|
||||||
github.com/go-toolsmith/astfmt v1.1.0 // indirect
|
github.com/go-toolsmith/astfmt v1.1.0 // indirect
|
||||||
github.com/go-toolsmith/astp 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/strparse v1.1.0 // indirect
|
||||||
github.com/go-toolsmith/typep 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/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-cmp v0.7.0 // indirect
|
||||||
github.com/google/go-containerregistry v0.21.6 // indirect
|
github.com/google/go-containerregistry v0.21.7 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // 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/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jdx/go-netrc v1.0.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/klauspost/pgzip v1.2.6 // indirect
|
||||||
github.com/knadh/koanf/maps v0.1.2 // 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-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/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.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/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/moby/api v1.54.2 // indirect
|
github.com/moby/moby/api v1.55.0 // indirect
|
||||||
github.com/moby/moby/client v0.4.1 // indirect
|
github.com/moby/moby/client v0.5.0 // indirect
|
||||||
github.com/moby/term v0.5.2 // indirect
|
github.com/moricho/tparallel v0.3.2 // indirect
|
||||||
github.com/morikuni/aec v1.1.0 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // 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/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.5 // indirect
|
github.com/prometheus/common v0.69.0 // indirect
|
||||||
github.com/prometheus/procfs v0.20.1 // indirect
|
github.com/prometheus/procfs v0.21.1 // indirect
|
||||||
github.com/quasilyte/go-ruleguard v0.4.5 // 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/gogrep v0.5.0 // indirect
|
||||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
|
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
|
||||||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
|
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
|
||||||
github.com/quic-go/qpack v0.6.0 // 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/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/rs/cors v1.11.1 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/segmentio/asm v1.2.1 // indirect
|
github.com/ryancurrah/gomodguard v1.4.1 // indirect
|
||||||
github.com/segmentio/encoding v0.5.4 // 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/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/cobra v1.10.2 // indirect
|
||||||
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/stoewer/go-strcase v1.3.1 // indirect
|
github.com/spf13/viper v1.12.0 // indirect
|
||||||
github.com/tetratelabs/wazero v1.11.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/tidwall/btree v1.8.1 // indirect
|
||||||
github.com/vbatts/tar-split v0.12.3 // indirect
|
github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 // indirect
|
||||||
go.lsp.dev/jsonrpc2 v0.10.0 // indirect
|
github.com/timonwong/loggercheck v0.11.0 // indirect
|
||||||
go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 // indirect
|
github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect
|
||||||
go.lsp.dev/protocol v0.12.0 // indirect
|
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
|
||||||
go.lsp.dev/uri v0.3.0 // 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/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||||
go.uber.org/mock v0.6.0 // indirect
|
go.uber.org/mock v0.6.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.28.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
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/crypto v0.52.0 // indirect
|
golang.org/x/crypto v0.53.0 // indirect
|
||||||
golang.org/x/exp/typeparams v0.0.0-20260508232706-74f9aab9d74a // indirect
|
golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976 // indirect
|
||||||
golang.org/x/mod v0.36.0 // indirect
|
golang.org/x/mod v0.37.0 // indirect
|
||||||
golang.org/x/net v0.55.0 // indirect
|
golang.org/x/net v0.56.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.22.0 // indirect
|
||||||
golang.org/x/term v0.43.0 // indirect
|
golang.org/x/term v0.44.0 // indirect
|
||||||
golang.org/x/text v0.37.0 // indirect
|
golang.org/x/text v0.39.0 // indirect
|
||||||
golang.org/x/time v0.15.0 // indirect
|
golang.org/x/time v0.15.0 // indirect
|
||||||
golang.org/x/tools v0.45.0 // indirect
|
golang.org/x/tools v0.47.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
|
||||||
google.golang.org/grpc v1.79.3 // 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
|
mvdan.cc/xurls/v2 v2.6.0 // indirect
|
||||||
pluginrpc.com/pluginrpc v0.5.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
|
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 {
|
if cfg.LogDebugOptions.AclNotMatched {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"User": user.Username,
|
"User": user.Username,
|
||||||
"Action": action.Title,
|
"Resource": resourceTitle,
|
||||||
"ACL": acl.Name,
|
"ACL": acl.Name,
|
||||||
}).Debugf("%v - ACL Not Matched", aclFunction)
|
}).Debugf("%v - ACL Not Matched", aclFunction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func logAclMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
|
func logAclMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, resourceTitle string, acl *config.AccessControlList) {
|
||||||
actionTitle := "N/A"
|
if resourceTitle == "" {
|
||||||
|
resourceTitle = "N/A"
|
||||||
if action != nil {
|
|
||||||
actionTitle = action.Title
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.LogDebugOptions.AclMatched {
|
if cfg.LogDebugOptions.AclMatched {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"User": user.Username,
|
"User": user.Username,
|
||||||
"Action": actionTitle,
|
"Resource": resourceTitle,
|
||||||
"ACL": acl.Name,
|
"ACL": acl.Name,
|
||||||
}).Debugf("%v - Matched ACL", aclFunction)
|
}).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 {
|
if cfg.LogDebugOptions.AclNoneMatched {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"User": user.Username,
|
"User": user.Username,
|
||||||
"Action": action.Title,
|
"Resource": resourceTitle,
|
||||||
"Default": defaultPermission,
|
"Default": defaultPermission,
|
||||||
}).Debugf("%v - No ACLs Matched, returning default permission", aclFunction)
|
}).Debugf("%v - No ACLs Matched, returning default permission", aclFunction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -81,12 +79,12 @@ func permissionsConfigToBits(permissions config.PermissionsList) PermissionBits
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action) bool {
|
func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, resourceTitle string, resourceAcls []string, includeAddToEvery bool) bool {
|
||||||
relevantAcls := getRelevantAcls(cfg, action.Acls, user)
|
relevantAcls := getRelevantAcls(cfg, resourceAcls, user, includeAddToEvery)
|
||||||
|
|
||||||
if cfg.LogDebugOptions.AclCheckStarted {
|
if cfg.LogDebugOptions.AclCheckStarted {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"actionTitle": action.Title,
|
"resourceTitle": resourceTitle,
|
||||||
"username": user.Username,
|
"username": user.Username,
|
||||||
"usergroupLine": user.UsergroupLine,
|
"usergroupLine": user.UsergroupLine,
|
||||||
"relevantAcls": len(relevantAcls),
|
"relevantAcls": len(relevantAcls),
|
||||||
|
|
@ -98,27 +96,27 @@ func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.
|
||||||
permissionBits := permissionsConfigToBits(acl.Permissions)
|
permissionBits := permissionsConfigToBits(acl.Permissions)
|
||||||
|
|
||||||
if permissionBits.Has(requiredPermission) {
|
if permissionBits.Has(requiredPermission) {
|
||||||
logAclMatched(cfg, aclFunction, user, action, acl)
|
logAclMatched(cfg, aclFunction, user, resourceTitle, acl)
|
||||||
|
|
||||||
return true
|
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
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAllowedLogs checks if a AuthenticatedUser is allowed to view an action's logs
|
// 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 {
|
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
|
// IsAllowedExec checks if a AuthenticatedUser is allowed to execute an Action
|
||||||
func IsAllowedExec(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
|
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
|
// 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 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 {
|
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 {
|
// IsAllowedViewDashboard checks if a user may see a root dashboard.
|
||||||
if !slices.Contains(user.Acls, acl.Name) {
|
// Dashboards with no acls are unrestricted. AddToEveryAction does not apply.
|
||||||
// If the user does not have this ACL, then it is not relevant
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if acl.AddToEveryAction {
|
if includeAddToEvery && acl.AddToEveryAction {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if slices.Contains(actionAcls, acl.Name) {
|
return slices.Contains(resourceAcls, acl.Name)
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
var ret []*config.AccessControlList
|
||||||
|
|
||||||
for _, acl := range cfg.AccessControlLists {
|
for _, acl := range cfg.AccessControlLists {
|
||||||
if isACLRelevantToAction(cfg, actionAcls, acl, user) {
|
if isACLRelevant(resourceAcls, acl, user, includeAddToEvery) {
|
||||||
ret = append(ret, acl)
|
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 {
|
func renderDashboardIfValid(dashboard *config.DashboardComponent, rr *DashboardRenderRequest) *apiv1.Dashboard {
|
||||||
|
if !acl.IsAllowedViewDashboard(rr.cfg, rr.AuthenticatedUser, dashboard) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if len(dashboard.Contents) == 0 {
|
if len(dashboard.Contents) == 0 {
|
||||||
logEmptyDashboard(dashboard.Title, rr.AuthenticatedUser.Username)
|
logEmptyDashboard(dashboard.Title, rr.AuthenticatedUser.Username)
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -71,13 +75,21 @@ func renderDirectoryDashboard(rr *DashboardRenderRequest, dashboardTitle string)
|
||||||
|
|
||||||
func findDirectoryComponent(rr *DashboardRenderRequest, title string) *config.DashboardComponent {
|
func findDirectoryComponent(rr *DashboardRenderRequest, title string) *config.DashboardComponent {
|
||||||
for _, dashboard := range rr.cfg.Dashboards {
|
for _, dashboard := range rr.cfg.Dashboards {
|
||||||
if component := searchDirectoryInComponent(dashboard, title); component != nil {
|
if component := findDirectoryInRootIfAllowed(rr, dashboard, title); component != nil {
|
||||||
return component
|
return component
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
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 {
|
func searchDirectoryInComponent(component *config.DashboardComponent, title string) *config.DashboardComponent {
|
||||||
if isMatchingDirectory(component, title) {
|
if isMatchingDirectory(component, title) {
|
||||||
return component
|
return component
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ func createKeys(t *testing.T) (*rsa.PrivateKey, string) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create temp file: %v", err)
|
t.Fatalf("failed to create temp file: %v", err)
|
||||||
}
|
}
|
||||||
defer tmpFile.Close()
|
defer func() { _ = tmpFile.Close() }()
|
||||||
|
|
||||||
t.Logf("Created File: %s", tmpFile.Name())
|
t.Logf("Created File: %s", tmpFile.Name())
|
||||||
|
|
||||||
|
|
@ -65,10 +65,6 @@ func newMux() *http.ServeMux {
|
||||||
return mux
|
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 {
|
func createJWTTokenWithExpirationAndAudience(t *testing.T, privateKey *rsa.PrivateKey, expire int64, audience string) string {
|
||||||
token := jwt.New(jwt.SigningMethodRS256)
|
token := jwt.New(jwt.SigningMethodRS256)
|
||||||
claims := token.Claims.(jwt.MapClaims)
|
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) {
|
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)
|
assert.Equal(t, expectCode, res.StatusCode)
|
||||||
body, _ := io.ReadAll(res.Body)
|
body, _ := io.ReadAll(res.Body)
|
||||||
t.Logf("Response body: %s", string(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) {
|
func testJwkValidationWithAudience(t *testing.T, expire int64, expectCode int, configAudience, tokenAudience string) {
|
||||||
privateKey, publicKeyPath := createKeys(t)
|
privateKey, publicKeyPath := createKeys(t)
|
||||||
defer os.Remove(publicKeyPath)
|
defer func() { _ = os.Remove(publicKeyPath) }()
|
||||||
|
|
||||||
cfg := config.DefaultConfig()
|
cfg := config.DefaultConfig()
|
||||||
cfg.AuthJwtPubKeyPath = publicKeyPath
|
cfg.AuthJwtPubKeyPath = publicKeyPath
|
||||||
|
|
@ -186,7 +182,7 @@ func makeJWTRequest(t *testing.T, srv *httptest.Server, tokenStr string) *http.R
|
||||||
|
|
||||||
func TestJWTHeader(t *testing.T) {
|
func TestJWTHeader(t *testing.T) {
|
||||||
privateKey, publicKeyPath := createKeys(t)
|
privateKey, publicKeyPath := createKeys(t)
|
||||||
defer os.Remove(publicKeyPath)
|
defer func() { _ = os.Remove(publicKeyPath) }()
|
||||||
|
|
||||||
cfg := config.DefaultConfig()
|
cfg := config.DefaultConfig()
|
||||||
cfg.AuthJwtPubKeyPath = publicKeyPath
|
cfg.AuthJwtPubKeyPath = publicKeyPath
|
||||||
|
|
@ -217,7 +213,7 @@ func TestJWTHeader(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
res := makeJWTRequest(t, srv, tokenStr)
|
res := makeJWTRequest(t, srv, tokenStr)
|
||||||
defer res.Body.Close()
|
defer func() { _ = res.Body.Close() }()
|
||||||
|
|
||||||
assert.Equal(t, 200, res.StatusCode)
|
assert.Equal(t, 200, res.StatusCode)
|
||||||
body, _ := io.ReadAll(res.Body)
|
body, _ := io.ReadAll(res.Body)
|
||||||
|
|
|
||||||
|
|
@ -349,13 +349,13 @@ func getUserInfo(cfg *config.Config, client *http.Client, provider *config.OAuth
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() { _ = res.Body.Close() }()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
log.Errorf("Failed to get user data: %v", res.StatusCode)
|
log.Errorf("Failed to get user data: %v", res.StatusCode)
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
contents, err := io.ReadAll(res.Body)
|
contents, err := io.ReadAll(res.Body)
|
||||||
|
|
||||||
if err != nil {
|
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))
|
log.Debugf("OAuth2 User Data: %v+", string(contents))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal([]byte(contents), &userData)
|
err = json.Unmarshal(contents, &userData)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to unmarshal user data: %v", err)
|
log.Errorf("Failed to unmarshal user data: %v", err)
|
||||||
|
|
|
||||||
|
|
@ -289,6 +289,7 @@ type DashboardComponent struct {
|
||||||
Entity string `koanf:"entity"`
|
Entity string `koanf:"entity"`
|
||||||
Icon string `koanf:"icon"`
|
Icon string `koanf:"icon"`
|
||||||
CssClass string `koanf:"cssClass"`
|
CssClass string `koanf:"cssClass"`
|
||||||
|
Acls []string `koanf:"acls"`
|
||||||
InlineAction *Action `koanf:"inlineAction"`
|
InlineAction *Action `koanf:"inlineAction"`
|
||||||
Contents []*DashboardComponent `koanf:"contents"`
|
Contents []*DashboardComponent `koanf:"contents"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/knadh/koanf/providers/file"
|
"github.com/knadh/koanf/providers/file"
|
||||||
"github.com/knadh/koanf/v2"
|
"github.com/knadh/koanf/v2"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUserLoadingFromConfig(t *testing.T) {
|
func TestUserLoadingFromConfig(t *testing.T) {
|
||||||
|
|
@ -30,13 +31,13 @@ actions:
|
||||||
|
|
||||||
// Create temporary file
|
// Create temporary file
|
||||||
tmpFile, err := os.CreateTemp("", "test_config_*.yaml")
|
tmpFile, err := os.CreateTemp("", "test_config_*.yaml")
|
||||||
assert.NoError(t, err, "Should create temporary file")
|
require.NoError(t, err, "Should create temporary file")
|
||||||
defer os.Remove(tmpFile.Name())
|
defer func() { _ = os.Remove(tmpFile.Name()) }()
|
||||||
|
|
||||||
// Write test config to file
|
// Write test config to file
|
||||||
_, err = tmpFile.WriteString(testConfig)
|
_, err = tmpFile.WriteString(testConfig)
|
||||||
assert.NoError(t, err, "Should write test config to file")
|
require.NoError(t, err, "Should write test config to file")
|
||||||
tmpFile.Close()
|
require.NoError(t, tmpFile.Close())
|
||||||
|
|
||||||
// Load config using koanf
|
// Load config using koanf
|
||||||
k := koanf.New(".")
|
k := koanf.New(".")
|
||||||
|
|
@ -81,12 +82,12 @@ actions:
|
||||||
`
|
`
|
||||||
|
|
||||||
tmpFile, err := os.CreateTemp("", "test_config_empty_*.yaml")
|
tmpFile, err := os.CreateTemp("", "test_config_empty_*.yaml")
|
||||||
assert.NoError(t, err, "Should create temporary file")
|
require.NoError(t, err, "Should create temporary file")
|
||||||
defer os.Remove(tmpFile.Name())
|
defer func() { _ = os.Remove(tmpFile.Name()) }()
|
||||||
|
|
||||||
_, err = tmpFile.WriteString(testConfig)
|
_, err = tmpFile.WriteString(testConfig)
|
||||||
assert.NoError(t, err, "Should write test config to file")
|
require.NoError(t, err, "Should write test config to file")
|
||||||
tmpFile.Close()
|
require.NoError(t, tmpFile.Close())
|
||||||
|
|
||||||
k := koanf.New(".")
|
k := koanf.New(".")
|
||||||
err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser())
|
err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser())
|
||||||
|
|
@ -116,12 +117,12 @@ actions:
|
||||||
`
|
`
|
||||||
|
|
||||||
tmpFile, err := os.CreateTemp("", "test_config_disabled_*.yaml")
|
tmpFile, err := os.CreateTemp("", "test_config_disabled_*.yaml")
|
||||||
assert.NoError(t, err, "Should create temporary file")
|
require.NoError(t, err, "Should create temporary file")
|
||||||
defer os.Remove(tmpFile.Name())
|
defer func() { _ = os.Remove(tmpFile.Name()) }()
|
||||||
|
|
||||||
_, err = tmpFile.WriteString(testConfig)
|
_, err = tmpFile.WriteString(testConfig)
|
||||||
assert.NoError(t, err, "Should write test config to file")
|
require.NoError(t, err, "Should write test config to file")
|
||||||
tmpFile.Close()
|
require.NoError(t, tmpFile.Close())
|
||||||
|
|
||||||
k := koanf.New(".")
|
k := koanf.New(".")
|
||||||
err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser())
|
err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser())
|
||||||
|
|
@ -147,12 +148,12 @@ actions:
|
||||||
`
|
`
|
||||||
|
|
||||||
tmpFile, err := os.CreateTemp("", "test_config_no_auth_*.yaml")
|
tmpFile, err := os.CreateTemp("", "test_config_no_auth_*.yaml")
|
||||||
assert.NoError(t, err, "Should create temporary file")
|
require.NoError(t, err, "Should create temporary file")
|
||||||
defer os.Remove(tmpFile.Name())
|
defer func() { _ = os.Remove(tmpFile.Name()) }()
|
||||||
|
|
||||||
_, err = tmpFile.WriteString(testConfig)
|
_, err = tmpFile.WriteString(testConfig)
|
||||||
assert.NoError(t, err, "Should write test config to file")
|
require.NoError(t, err, "Should write test config to file")
|
||||||
tmpFile.Close()
|
require.NoError(t, tmpFile.Close())
|
||||||
|
|
||||||
k := koanf.New(".")
|
k := koanf.New(".")
|
||||||
err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser())
|
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 {
|
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 {
|
func hasExec(req *ExecutionRequest) bool {
|
||||||
|
|
|
||||||
|
|
@ -186,16 +186,6 @@ func (e *Executor) groupsHaveCapacityForQueued(req *ExecutionRequest) bool {
|
||||||
return true
|
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 {
|
func firstFullGroupNameLocked(e *Executor, req *ExecutionRequest) string {
|
||||||
for _, limit := range actionGroupLimits(req) {
|
for _, limit := range actionGroupLimits(req) {
|
||||||
if e.countActiveInGroupLocked(limit.name) >= (limit.maxConcurrent + 1) {
|
if e.countActiveInGroupLocked(limit.name) >= (limit.maxConcurrent + 1) {
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ func (tc *timeoutContext) setProcess(process *os.Process) {
|
||||||
tc.processMu.Unlock()
|
tc.processMu.Unlock()
|
||||||
|
|
||||||
// If deadline already expired before process was set, kill now
|
// 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}
|
logEntry := &InternalLogEntry{Process: process}
|
||||||
if err := tc.executor.Kill(logEntry); err != nil {
|
if err := tc.executor.Kill(logEntry); err != nil {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,11 @@ func watchPath(ctx *watchContext) {
|
||||||
return
|
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)
|
done := make(chan bool)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,6 @@ func WriteFile(filename string, out []byte) {
|
||||||
|
|
||||||
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||||
handle, err := os.Create(filename)
|
handle, err := os.Create(filename)
|
||||||
handle.Close()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"error": err,
|
"error": err,
|
||||||
|
|
@ -25,6 +23,12 @@ func WriteFile(filename string, out []byte) {
|
||||||
return
|
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)
|
err := os.WriteFile(filename, out, 0600)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RuntimeInfo struct {
|
type RuntimeInfo struct {
|
||||||
|
|
@ -85,6 +87,14 @@ func getOsReleasePrettyName() string {
|
||||||
return ""
|
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 := bufio.NewScanner(handle)
|
||||||
scanner.Split(bufio.ScanLines)
|
scanner.Split(bufio.ScanLines)
|
||||||
|
|
||||||
|
|
@ -96,7 +106,5 @@ func getOsReleasePrettyName() string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handle.Close()
|
|
||||||
|
|
||||||
return "notfound"
|
return "notfound"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package servicehost
|
package servicehost
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -20,25 +19,3 @@ func resolveLogDirectory(dir string, baseDir string) string {
|
||||||
|
|
||||||
return filepath.Join(baseDir, dir)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
_, err = file.WriteString(content)
|
_, writeErr := file.WriteString(content)
|
||||||
return err
|
closeErr := file.Close()
|
||||||
|
if writeErr != nil {
|
||||||
|
return writeErr
|
||||||
|
}
|
||||||
|
return closeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestRunState() *testRunState {
|
func newTestRunState() *testRunState {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ package tools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "github.com/bufbuild/buf/cmd/buf"
|
_ "github.com/bufbuild/buf/cmd/buf"
|
||||||
_ "github.com/fzipp/gocyclo/cmd/gocyclo"
|
_ "github.com/golangci/golangci-lint/v2/cmd/golangci-lint"
|
||||||
_ "github.com/go-critic/go-critic/cmd/gocritic"
|
|
||||||
_ "google.golang.org/protobuf/cmd/protoc-gen-go"
|
_ "google.golang.org/protobuf/cmd/protoc-gen-go"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,8 @@ update_checksum() {
|
||||||
update_checksum "${ZIP_NAME}"
|
update_checksum "${ZIP_NAME}"
|
||||||
update_checksum "${MSI_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}" \
|
gh release upload "${TAG}" \
|
||||||
"${DIST_DIR}/${ZIP_NAME}" \
|
"${DIST_DIR}/${ZIP_NAME}" \
|
||||||
"${DIST_DIR}/${MSI_NAME}" \
|
"${DIST_DIR}/${MSI_NAME}" \
|
||||||
|
|
@ -87,6 +88,4 @@ if ! gh release upload "${TAG}" "${checksums_path}" --clobber; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
gh release edit "${TAG}" --draft=false
|
echo "Uploaded signed ${ZIP_NAME} and ${MSI_NAME} to release ${TAG}"
|
||||||
|
|
||||||
echo "Published signed ${ZIP_NAME} and ${MSI_NAME} on release ${TAG}"
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue