Next (#1039)
This commit is contained in:
commit
b2096d4a8d
|
|
@ -1,132 +0,0 @@
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
# npm updates for frontend - targeting "next" branch
|
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: "/frontend"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
target-branch: "next"
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
labels:
|
|
||||||
- "3k"
|
|
||||||
- "dependencies"
|
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
|
|
||||||
# npm updates for frontend - targeting "release/2k" branch (security updates only)
|
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: "/frontend"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
target-branch: "release/2k"
|
|
||||||
open-pull-requests-limit: 0
|
|
||||||
labels:
|
|
||||||
- "2k"
|
|
||||||
- "dependencies"
|
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
|
|
||||||
# npm updates for integration-tests - targeting "next" branch
|
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: "/integration-tests"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
target-branch: "next"
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
labels:
|
|
||||||
- "3k"
|
|
||||||
- "dependencies"
|
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
|
|
||||||
# npm updates for integration-tests - targeting "release/2k" branch (security updates only)
|
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: "/integration-tests"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
target-branch: "release/2k"
|
|
||||||
open-pull-requests-limit: 0
|
|
||||||
labels:
|
|
||||||
- "2k"
|
|
||||||
- "dependencies"
|
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
|
|
||||||
# Go modules updates for service - targeting "next" branch
|
|
||||||
- package-ecosystem: "gomod"
|
|
||||||
directory: "/service"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
target-branch: "next"
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
labels:
|
|
||||||
- "3k"
|
|
||||||
- "dependencies"
|
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
|
|
||||||
# Go modules updates for service - targeting "release/2k" branch (security updates only)
|
|
||||||
- package-ecosystem: "gomod"
|
|
||||||
directory: "/service"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
target-branch: "release/2k"
|
|
||||||
open-pull-requests-limit: 0
|
|
||||||
labels:
|
|
||||||
- "2k"
|
|
||||||
- "dependencies"
|
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
|
|
||||||
# Go modules updates for lang - targeting "next" branch
|
|
||||||
- package-ecosystem: "gomod"
|
|
||||||
directory: "/lang"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
target-branch: "next"
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
labels:
|
|
||||||
- "3k"
|
|
||||||
- "dependencies"
|
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
|
|
||||||
# Go modules updates for lang - targeting "release/2k" branch (security updates only)
|
|
||||||
- package-ecosystem: "gomod"
|
|
||||||
directory: "/lang"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
target-branch: "release/2k"
|
|
||||||
open-pull-requests-limit: 0
|
|
||||||
labels:
|
|
||||||
- "2k"
|
|
||||||
- "dependencies"
|
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
|
|
||||||
# Docker updates - targeting "next" branch
|
|
||||||
- package-ecosystem: "docker"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
target-branch: "next"
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
labels:
|
|
||||||
- "3k"
|
|
||||||
- "dependencies"
|
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
|
|
||||||
# Docker updates - targeting "release/2k" branch (security updates only)
|
|
||||||
- package-ecosystem: "docker"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
target-branch: "release/2k"
|
|
||||||
open-pull-requests-limit: 0
|
|
||||||
labels:
|
|
||||||
- "2k"
|
|
||||||
- "dependencies"
|
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
|
|
||||||
31
config.yaml
31
config.yaml
|
|
@ -14,6 +14,10 @@ logLevel: "INFO"
|
||||||
#
|
#
|
||||||
# Docs: https://docs.olivetin.app/action_execution/create_your_first.html
|
# Docs: https://docs.olivetin.app/action_execution/create_your_first.html
|
||||||
actions:
|
actions:
|
||||||
|
# Every action can still be run on demand from the web UI or API. The keys
|
||||||
|
# below are optional *additional* triggers (see each action and
|
||||||
|
# https://docs.olivetin.app/action_execution/ ).
|
||||||
|
#
|
||||||
# This is the most simple action, it just runs the command and flashes the
|
# This is the most simple action, it just runs the command and flashes the
|
||||||
# button to indicate status.
|
# button to indicate status.
|
||||||
#
|
#
|
||||||
|
|
@ -23,6 +27,8 @@ actions:
|
||||||
shell: ping -c 3 1.1.1.1
|
shell: ping -c 3 1.1.1.1
|
||||||
icon: ping
|
icon: ping
|
||||||
popupOnStart: execution-dialog-stdout-only
|
popupOnStart: execution-dialog-stdout-only
|
||||||
|
# https://docs.olivetin.app/action_execution/onstartup.html
|
||||||
|
execOnStartup: true
|
||||||
|
|
||||||
# This uses `popupOnStart: execution-dialog-stdout-only` to simply show just
|
# This uses `popupOnStart: execution-dialog-stdout-only` to simply show just
|
||||||
# the command output.
|
# the command output.
|
||||||
|
|
@ -30,6 +36,10 @@ actions:
|
||||||
icon: disk
|
icon: disk
|
||||||
shell: df -h /media
|
shell: df -h /media
|
||||||
popupOnStart: execution-dialog-stdout-only
|
popupOnStart: execution-dialog-stdout-only
|
||||||
|
# https://docs.olivetin.app/action_execution/onfilechanged.html
|
||||||
|
# Create the directory first, e.g. mkdir -p /tmp/olivetin-demo-file-changed
|
||||||
|
execOnFileChangedInDir:
|
||||||
|
- /tmp/olivetin-demo-file-changed
|
||||||
|
|
||||||
# This uses `popupOnStart: execution-dialog` to show a dialog with more
|
# This uses `popupOnStart: execution-dialog` to show a dialog with more
|
||||||
# information about the command that was run.
|
# information about the command that was run.
|
||||||
|
|
@ -37,6 +47,10 @@ actions:
|
||||||
shell: dmesg | tail
|
shell: dmesg | tail
|
||||||
icon: logs
|
icon: logs
|
||||||
popupOnStart: execution-dialog
|
popupOnStart: execution-dialog
|
||||||
|
# https://docs.olivetin.app/action_execution/oncron.html — second example;
|
||||||
|
# the "date" action uses @hourly elsewhere in this file.
|
||||||
|
execOnCron:
|
||||||
|
- "0 3 * * 0"
|
||||||
|
|
||||||
# This uses `popupOnStart: execution-button` to display a mini button that
|
# This uses `popupOnStart: execution-button` to display a mini button that
|
||||||
# links to the logs.
|
# links to the logs.
|
||||||
|
|
@ -51,6 +65,8 @@ actions:
|
||||||
maxRate:
|
maxRate:
|
||||||
- limit: 3
|
- limit: 3
|
||||||
duration: 1m
|
duration: 1m
|
||||||
|
execOnCron:
|
||||||
|
- "@hourly"
|
||||||
|
|
||||||
# You are not limited to operating system commands, and of course you can run
|
# You are not limited to operating system commands, and of course you can run
|
||||||
# your own scripts. Here `maxConcurrent` stops the script running multiple
|
# your own scripts. Here `maxConcurrent` stops the script running multiple
|
||||||
|
|
@ -63,6 +79,8 @@ actions:
|
||||||
timeout: 10
|
timeout: 10
|
||||||
icon: backup
|
icon: backup
|
||||||
popupOnStart: execution-dialog
|
popupOnStart: execution-dialog
|
||||||
|
# https://docs.olivetin.app/action_execution/oncalendar.html
|
||||||
|
execOnCalendarFile: examples/demo-olivetin-calendar.yaml
|
||||||
|
|
||||||
# When you want to prompt users for input, that is when you should use
|
# When you want to prompt users for input, that is when you should use
|
||||||
# `arguments` - this presents a popup dialog and asks for argument values.
|
# `arguments` - this presents a popup dialog and asks for argument values.
|
||||||
|
|
@ -74,6 +92,11 @@ actions:
|
||||||
icon: ping
|
icon: ping
|
||||||
timeout: 100
|
timeout: 100
|
||||||
popupOnStart: execution-dialog-stdout-only
|
popupOnStart: execution-dialog-stdout-only
|
||||||
|
# https://docs.olivetin.app/action_execution/onwebhook.html — POST to /webhooks
|
||||||
|
# with header X-OliveTin-Demo: ping-host (path and payload rules are documented).
|
||||||
|
execOnWebhook:
|
||||||
|
- matchHeaders:
|
||||||
|
X-OliveTin-Demo: ping-host
|
||||||
arguments:
|
arguments:
|
||||||
- name: host
|
- name: host
|
||||||
title: Host
|
title: Host
|
||||||
|
|
@ -149,6 +172,10 @@ actions:
|
||||||
icon: ssh
|
icon: ssh
|
||||||
shell: olivetin-setup-easy-ssh
|
shell: olivetin-setup-easy-ssh
|
||||||
popupOnStart: execution-dialog
|
popupOnStart: execution-dialog
|
||||||
|
# Second webhook example: POST /webhooks?demo=setup-ssh
|
||||||
|
execOnWebhook:
|
||||||
|
- matchQuery:
|
||||||
|
demo: setup-ssh
|
||||||
|
|
||||||
# Here's how to use SSH with the "easy" config, to restart a service on
|
# Here's how to use SSH with the "easy" config, to restart a service on
|
||||||
# another server.
|
# another server.
|
||||||
|
|
@ -215,6 +242,10 @@ actions:
|
||||||
- title: Ping All Servers
|
- title: Ping All Servers
|
||||||
shell: "echo 'Ping all servers'"
|
shell: "echo 'Ping all servers'"
|
||||||
icon: ping
|
icon: ping
|
||||||
|
# https://docs.olivetin.app/action_execution/onfilecreated.html
|
||||||
|
# mkdir -p /tmp/olivetin-demo-file-created
|
||||||
|
execOnFileCreatedInDir:
|
||||||
|
- /tmp/olivetin-demo-file-created
|
||||||
|
|
||||||
- title: Start {{ .CurrentEntity.Names }}
|
- title: Start {{ .CurrentEntity.Names }}
|
||||||
icon: box
|
icon: box
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
= Icons
|
= Icons
|
||||||
|
|
||||||
You can specify any HTML for an icon. It's a popular choice to use Unicode
|
You can specify any HTML for an icon. It's a popular choice to use Unicode
|
||||||
icons because they are extremely fast to load and there are a lot of them,
|
icons because they are extremely fast to load and there are a lot of them,
|
||||||
but OliveTin also support Iconify, and simple PNG, JPG, WEBP and similar images.
|
but OliveTin also support Iconify, and simple PNG, JPG, WEBP and similar images.
|
||||||
|
|
||||||
.Examples of icons in OliveTin
|
.Examples of icons in OliveTin
|
||||||
|
|
@ -39,14 +39,30 @@ And you should get something that looks like this;
|
||||||
|
|
||||||
image::../action-button-iconify.png[]
|
image::../action-button-iconify.png[]
|
||||||
|
|
||||||
|
== Default Icon (bundled HugeIcon)
|
||||||
|
|
||||||
|
OliveTin used to use a default emoji smiley face as the default icon for actions, but that was a bit too "emoji" and not everyone liked it. Now, OliveTin uses a simple "command line" icon from the HugeIcons set as the default icon for actions. This is a simple and neutral icon that should work well for most actions.
|
||||||
|
|
||||||
|
If you need to reset a default icon for some reason, this is how you can do it;
|
||||||
|
|
||||||
|
.`config.yaml`
|
||||||
|
----
|
||||||
|
actions:
|
||||||
|
- title: Action with the bundled CLI HugeIcon
|
||||||
|
icon: hugeicons:CommandLineIcon
|
||||||
|
shell: echo hello
|
||||||
|
----
|
||||||
|
|
||||||
|
If you want to use other icons from the HugeIcons set, you need to use the Iconify method described above, not with the "hugeicons:" prefix - that only works for the default icon.
|
||||||
|
|
||||||
== Unicode icons ("emoji")
|
== Unicode icons ("emoji")
|
||||||
|
|
||||||
Using simple emoji (unicode) icons from your browser's font is extremely fast, and can look good on some platforms. However, the icons are platform specific, which mean's they'll look different between browsers and between operating systems.
|
Using simple emoji (unicode) icons from your browser's font is extremely fast, and can look good on some platforms. However, the icons are platform specific, which mean's they'll look different between browsers and between operating systems.
|
||||||
|
|
||||||
There are great sites like link:https://symbl.cc/en/emoji/[symbl.cc - a list of
|
There are great sites like link:https://symbl.cc/en/emoji/[symbl.cc - a list of
|
||||||
"Emoji" in unicode].
|
"Emoji" in unicode].
|
||||||
|
|
||||||
For example, if you find "link:https://symbl.cc/en/1F60E/[Smiling face with sunglasses]" you can click
|
For example, if you find "link:https://symbl.cc/en/1F60E/[Smiling face with sunglasses]" you can click
|
||||||
on it to see it's "HTML-code". In OliveTin, you'd setup the icon like this;
|
on it to see it's "HTML-code". In OliveTin, you'd setup the icon like this;
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
@ -56,16 +72,16 @@ actions:
|
||||||
shell: echo "You are awesome"
|
shell: echo "You are awesome"
|
||||||
----
|
----
|
||||||
|
|
||||||
=== Unicode alises
|
=== Unicode aliases
|
||||||
|
|
||||||
OliveTin has hard-coded aliases for a few commonly used icons, so you don't have to type out the full unicode codes. A list of those hard coded icons is;
|
OliveTin has hard-coded aliases for a few commonly used icons, so you don't have to type out the full unicode codes. A list of those hard coded icons is;
|
||||||
|
|
||||||
.Alias'd unicode reference table
|
.Alias'd unicode reference table
|
||||||
[%header]
|
[%header]
|
||||||
|===
|
|===
|
||||||
| Alias | Rendered as
|
| Alias | Rendered as
|
||||||
|
|
||||||
| `poop` | 💩
|
| `poop` | 💩
|
||||||
| `smile` | 😀
|
| `smile` | 😀
|
||||||
| `ping` | 📡
|
| `ping` | 📡
|
||||||
| `backup` | 💾
|
| `backup` | 💾
|
||||||
|
|
@ -140,5 +156,3 @@ examples;
|
||||||
shell: echo "I like purple"
|
shell: echo "I like purple"
|
||||||
----
|
----
|
||||||
////
|
////
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ A full list of argument types are below;
|
||||||
| (default) | xref:args/input.adoc[Textbox] | If a `type:` is not set, and `choices:` is empty, then ascii will be used, and a warning will be logged. It is recommended that you set the type explicitly, rather than relying on defaults.
|
| (default) | xref:args/input.adoc[Textbox] | If a `type:` is not set, and `choices:` is empty, then ascii will be used, and a warning will be logged. It is recommended that you set the type explicitly, rather than relying on defaults.
|
||||||
| ascii | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, but no spaces or punctuation
|
| ascii | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, but no spaces or punctuation
|
||||||
| ascii_identifier | xref:args/input.adoc[Textbox] | Like a DNS name, a-Z (case insensitive), 0-9, `-`, `.`, and `_`.
|
| ascii_identifier | xref:args/input.adoc[Textbox] | Like a DNS name, a-Z (case insensitive), 0-9, `-`, `.`, and `_`.
|
||||||
|
| shell_safe_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but also allows `@` and `+`. Useful for shell-safe usernames and email-style identifiers.
|
||||||
| ascii_sentence | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, with spaces, `.` and `,`.
|
| ascii_sentence | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, with spaces, `.` and `,`.
|
||||||
| unicode_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but allows unicode characters. This is useful for languages that use non-ascii characters, such as Chinese, Japanese, etc.
|
| unicode_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but allows unicode characters. This is useful for languages that use non-ascii characters, such as Chinese, Japanese, etc.
|
||||||
| email | xref:args/input.adoc[Textbox] | An email address.
|
| email | xref:args/input.adoc[Textbox] | An email address.
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
= Configuration
|
= Configuration
|
||||||
|
|
||||||
OliveTin is controlled by a `config.yaml` file. On startup, it looks for this
|
OliveTin is controlled by a `config.yaml` file. On startup, it looks for this
|
||||||
file in the following locations;
|
file in the following locations;
|
||||||
|
|
||||||
1. The value specified by the `--configdir` argument, which defaults to the current working directory (`./`)
|
1. The value specified by the `--configdir` argument, which defaults to the current working directory (`./`)
|
||||||
2. `/config/` - Mostly used for containers
|
2. `/config/` - Mostly used for containers
|
||||||
3. `/etc/OliveTin/` - this is the recommended directory on Linux for your `config.yaml`.
|
3. `/etc/OliveTin/` - this is the recommended directory on Linux for your `config.yaml`.
|
||||||
|
|
||||||
The most simple `config.yaml` would be something like this;
|
The most simple `config.yaml` would be something like this;
|
||||||
|
|
||||||
|
|
@ -18,9 +18,9 @@ actions:
|
||||||
shell: echo 'Hello World!'
|
shell: echo 'Hello World!'
|
||||||
----
|
----
|
||||||
|
|
||||||
The configuration does not really get more complicated than that. You can of course add more actions, and customize more, but the syntax otherwise extremely simple.
|
The configuration does not really get more complicated than that. You can of course add more actions, and customize more, but the syntax is otherwise extremely simple.
|
||||||
|
|
||||||
For building up from here, look at the following resources;
|
For building up from here, look at the following resources;
|
||||||
|
|
||||||
* See the xref:action_examples/intro.adoc[action examples] section for extra examples of what OliveTin could be configured to do.
|
* See the xref:action_examples/intro.adoc[action examples] section for extra examples of what OliveTin could be configured to do.
|
||||||
|
|
||||||
|
|
@ -54,7 +54,7 @@ All configuration options are covered in the solution sections
|
||||||
| `showNavigateOnStartIcons` | Show (or hide) the small icons on action buttons that indicate popup/argument/background behavior on start. | `true` | Live reloadable | xref:advanced_configuration/webui.adoc[Customize the web UI].
|
| `showNavigateOnStartIcons` | Show (or hide) the small icons on action buttons that indicate popup/argument/background behavior on start. | `true` | Live reloadable | xref:advanced_configuration/webui.adoc[Customize the web UI].
|
||||||
| `sectionNavigationStyle` | The style of the section navigation. `sidebar`, `topbar` | `sidebar` | Live reloadable | xref:advanced_configuration/webui.adoc[Customize the web UI].
|
| `sectionNavigationStyle` | The style of the section navigation. `sidebar`, `topbar` | `sidebar` | Live reloadable | xref:advanced_configuration/webui.adoc[Customize the web UI].
|
||||||
| `defaultPopupOnStart` | The default popup to show on start. | `none` | Live reloadable | xref:action_customization/popuponstart.adoc[Popup On Start].
|
| `defaultPopupOnStart` | The default popup to show on start. | `none` | Live reloadable | xref:action_customization/popuponstart.adoc[Popup On Start].
|
||||||
| `defaultIconForActions` | The default icon to use for actions. | `smile` | Requires Restart | -
|
| `defaultIconForActions` | The default icon string for actions (Unicode aliases such as `smile`, `hugeicons:NeutralIcon`, HTML, Iconify snippets, images, etc.). See xref:action_customization/icons.adoc[Icons]. | `hugeicons:CommandLineIcon` | Requires Restart | -
|
||||||
| `defaultIconForDirectories` | The default icon to use for directories. | `directory` | Requires Restart | -
|
| `defaultIconForDirectories` | The default icon to use for directories. | `directory` | Requires Restart | -
|
||||||
| `defaultIconForBack` | The default icon to use for back (from directories). | `«` | Requires Restart | -
|
| `defaultIconForBack` | The default icon to use for back (from directories). | `«` | Requires Restart | -
|
||||||
| `enableCustomJs` | Enable custom JavaScript. | `false` | Live Reloadable, but refreshing the web browser is required. | xref:advanced_configuration/webui.adoc[Custom JS].
|
| `enableCustomJs` | Enable custom JavaScript. | `false` | Live Reloadable, but refreshing the web browser is required. | xref:advanced_configuration/webui.adoc[Custom JS].
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ Note that OliveTin will load `/theme.css` depending on `themeName:` in your conf
|
||||||
|
|
||||||
== Understanding theme URLs
|
== Understanding theme URLs
|
||||||
|
|
||||||
When you create a theme, OliveTin will serve your theme's CSS at `/theme.css` and any other assets at `/custom-webui/themes/mytheme/`. This might be a little strange at first, as your theme.css file wil be in the `/custom-webui/themes/mytheme/` directory, but OliveTin will still serve it at `/theme.css`. Let's explain why this happens;
|
When you create a theme, OliveTin will serve your theme's CSS at `/theme.css` and any other assets at `/custom-webui/themes/mytheme/`. This might be a little strange at first, as your theme.css file will be in the `/custom-webui/themes/mytheme/` directory, but OliveTin will still serve it at `/theme.css`. Let's explain why this happens;
|
||||||
|
|
||||||
OliveTin wants to make it easy for your reverse proxy, cache server, or browser, to cache as much content as possible. This means that if OliveTin had to inject a new CSS file into the HTML every time you changed your theme, then your reverse proxy, cache server, or browser would have to re-download the HTML every time you changed your theme. This is not ideal.
|
OliveTin wants to make it easy for your reverse proxy, cache server, or browser, to cache as much content as possible. This means that if OliveTin had to inject a new CSS file into the HTML every time you changed your theme, then your reverse proxy, cache server, or browser would have to re-download the HTML every time you changed your theme. This is not ideal.
|
||||||
|
|
||||||
|
|
@ -26,7 +26,7 @@ It is possible that OliveTin's initial webUiSettings.json (that is loaded to set
|
||||||
|
|
||||||
To make things fast, OliveTin will copy the content of your `/custom-webui/themes/mytheme/theme.css` file into memory when it starts, and then requests for `/theme.css` will load this file.
|
To make things fast, OliveTin will copy the content of your `/custom-webui/themes/mytheme/theme.css` file into memory when it starts, and then requests for `/theme.css` will load this file.
|
||||||
|
|
||||||
What this means for you, is that to get to files like `backgrond.png` from your CSS, you must write your CSS to point to the file in the `/custom-webui/themes/mytheme/` directory;
|
What this means for you, is that to get to files like `background.png` from your CSS, you must write your CSS to point to the file in the `/custom-webui/themes/mytheme/` directory;
|
||||||
|
|
||||||
.Correct example
|
.Correct example
|
||||||
```
|
```
|
||||||
|
|
@ -48,6 +48,4 @@ The OliveTin themes page is here; https://olivetin.app/themes
|
||||||
|
|
||||||
When you are done with your theme, fork https://github.com/OliveTin/themes on GitHub and create a new page under the "content" directory for your new theme. Commit that to GitHub and then raise a pull request.
|
When you are done with your theme, fork https://github.com/OliveTin/themes on GitHub and create a new page under the "content" directory for your new theme. Commit that to GitHub and then raise a pull request.
|
||||||
|
|
||||||
If you meed more help, please jump on our discord server!
|
If you need more help, please jump on our Discord server!
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,15 @@
|
||||||
"iconify-icon": "^3.0.2",
|
"iconify-icon": "^3.0.2",
|
||||||
"picocrank": "^1.15.0",
|
"picocrank": "^1.15.0",
|
||||||
"standard": "^17.1.2",
|
"standard": "^17.1.2",
|
||||||
"unplugin-vue-components": "^32.0.0",
|
"unplugin-vue-components": "^32.1.0",
|
||||||
"vite": "^8.0.13",
|
"vite": "^8.0.14",
|
||||||
"vue": "^3.5.34",
|
"vue": "^3.5.34",
|
||||||
"vue-i18n": "^11.4.4",
|
"vue-i18n": "^11.4.4",
|
||||||
"vue-router": "^5.0.7"
|
"vue-router": "^5.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"stylelint": "^17.11.1",
|
"stylelint": "^17.12.0",
|
||||||
"stylelint-config-standard": "^40.0.0"
|
"stylelint-config-standard": "^40.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -164,16 +164,16 @@
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@cacheable/memory": {
|
"node_modules/@cacheable/memory": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.9.tgz",
|
||||||
"integrity": "sha512-RbxnxAMf89Tp1dLhXMS7ceft/PGsDl1Ip7T20z5nZ+pwIAsQ1p2izPjVG69oCLv/jfQ7HDPHTWK0c9rcAWXN3A==",
|
"integrity": "sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cacheable/utils": "^2.3.3",
|
"@cacheable/utils": "^2.4.1",
|
||||||
"@keyv/bigmap": "^1.3.0",
|
"@keyv/bigmap": "^1.3.1",
|
||||||
"hookified": "^1.14.0",
|
"hookified": "^1.15.1",
|
||||||
"keyv": "^5.5.5"
|
"keyv": "^5.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cacheable/memory/node_modules/@keyv/bigmap": {
|
"node_modules/@cacheable/memory/node_modules/@keyv/bigmap": {
|
||||||
|
|
@ -204,13 +204,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cacheable/utils": {
|
"node_modules/@cacheable/utils": {
|
||||||
"version": "2.3.4",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz",
|
||||||
"integrity": "sha512-knwKUJEYgIfwShABS1BX6JyJJTglAFcEU7EXqzTdiGCXur4voqkiJkdgZIQtWNFhynzDWERcTYv/sETMu3uJWA==",
|
"integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hashery": "^1.3.0",
|
"hashery": "^1.5.1",
|
||||||
"keyv": "^5.6.0"
|
"keyv": "^5.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1157,18 +1157,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.130.0",
|
"version": "0.132.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz",
|
||||||
"integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
|
"integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==",
|
||||||
"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.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
|
||||||
"integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
|
"integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1182,9 +1182,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz",
|
||||||
"integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
|
"integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1198,9 +1198,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz",
|
||||||
"integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
|
"integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1214,9 +1214,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz",
|
||||||
"integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
|
"integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1230,9 +1230,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz",
|
||||||
"integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
|
"integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
|
@ -1246,9 +1246,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz",
|
||||||
"integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
|
"integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1262,9 +1262,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz",
|
||||||
"integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
|
"integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1278,9 +1278,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz",
|
||||||
"integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
|
"integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
|
@ -1294,9 +1294,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz",
|
||||||
"integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
|
"integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
|
|
@ -1310,9 +1310,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz",
|
||||||
"integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
|
"integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1326,9 +1326,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz",
|
||||||
"integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
|
"integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1342,9 +1342,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz",
|
||||||
"integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
|
"integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1358,9 +1358,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz",
|
||||||
"integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
|
"integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
|
|
@ -1376,9 +1376,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz",
|
||||||
"integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
|
"integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1392,9 +1392,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz",
|
||||||
"integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
|
"integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1990,17 +1990,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cacheable": {
|
"node_modules/cacheable": {
|
||||||
"version": "2.3.2",
|
"version": "2.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz",
|
||||||
"integrity": "sha512-w+ZuRNmex9c1TR9RcsxbfTKCjSL0rh1WA5SABbrWprIHeNBdmyQLSYonlDy9gpD+63XT8DgZ/wNh1Smvc9WnJA==",
|
"integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cacheable/memory": "^2.0.7",
|
"@cacheable/memory": "^2.0.8",
|
||||||
"@cacheable/utils": "^2.3.3",
|
"@cacheable/utils": "^2.4.1",
|
||||||
"hookified": "^1.15.0",
|
"hookified": "^1.15.0",
|
||||||
"keyv": "^5.5.5",
|
"keyv": "^5.6.0",
|
||||||
"qified": "^0.6.0"
|
"qified": "^0.10.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cacheable/node_modules/keyv": {
|
"node_modules/cacheable/node_modules/keyv": {
|
||||||
|
|
@ -3672,13 +3672,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hashery": {
|
"node_modules/hashery": {
|
||||||
"version": "1.4.0",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/hashery/-/hashery-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz",
|
||||||
"integrity": "sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==",
|
"integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hookified": "^1.14.0"
|
"hookified": "^1.15.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
|
|
@ -4645,9 +4645,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/local-pkg": {
|
"node_modules/local-pkg": {
|
||||||
"version": "1.1.2",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz",
|
||||||
"integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
|
"integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mlly": "^1.7.4",
|
"mlly": "^1.7.4",
|
||||||
|
|
@ -4851,9 +4851,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|
@ -5304,9 +5304,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.14",
|
"version": "8.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||||
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|
@ -5323,7 +5323,7 @@
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.12",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
|
|
@ -5416,18 +5416,25 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/qified": {
|
"node_modules/qified": {
|
||||||
"version": "0.6.0",
|
"version": "0.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/qified/-/qified-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz",
|
||||||
"integrity": "sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==",
|
"integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hookified": "^1.14.0"
|
"hookified": "^2.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qified/node_modules/hookified": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/quansync": {
|
"node_modules/quansync": {
|
||||||
"version": "0.2.11",
|
"version": "0.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||||
|
|
@ -5599,12 +5606,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz",
|
||||||
"integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
|
"integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.130.0",
|
"@oxc-project/types": "=0.132.0",
|
||||||
"@rolldown/pluginutils": "^1.0.0"
|
"@rolldown/pluginutils": "^1.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -5614,21 +5621,21 @@
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm64": "1.0.1",
|
"@rolldown/binding-android-arm64": "1.0.2",
|
||||||
"@rolldown/binding-darwin-arm64": "1.0.1",
|
"@rolldown/binding-darwin-arm64": "1.0.2",
|
||||||
"@rolldown/binding-darwin-x64": "1.0.1",
|
"@rolldown/binding-darwin-x64": "1.0.2",
|
||||||
"@rolldown/binding-freebsd-x64": "1.0.1",
|
"@rolldown/binding-freebsd-x64": "1.0.2",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.0.2",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.0.1",
|
"@rolldown/binding-linux-arm64-gnu": "1.0.2",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.0.1",
|
"@rolldown/binding-linux-arm64-musl": "1.0.2",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.1",
|
"@rolldown/binding-linux-ppc64-gnu": "1.0.2",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.0.1",
|
"@rolldown/binding-linux-s390x-gnu": "1.0.2",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.0.1",
|
"@rolldown/binding-linux-x64-gnu": "1.0.2",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.0.1",
|
"@rolldown/binding-linux-x64-musl": "1.0.2",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.0.1",
|
"@rolldown/binding-openharmony-arm64": "1.0.2",
|
||||||
"@rolldown/binding-wasm32-wasi": "1.0.1",
|
"@rolldown/binding-wasm32-wasi": "1.0.2",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.0.1",
|
"@rolldown/binding-win32-arm64-msvc": "1.0.2",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.0.1"
|
"@rolldown/binding-win32-x64-msvc": "1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
|
|
@ -6130,9 +6137,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stylelint": {
|
"node_modules/stylelint": {
|
||||||
"version": "17.11.1",
|
"version": "17.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.12.0.tgz",
|
||||||
"integrity": "sha512-+smN/HqVTggUx3iuAzOi9fPh8SrH+cJWlZrYVldXoJ06orWBhZ4Ue/QEp64oei6pVrAh4w3tG+Y12Vw7MbCFRQ==",
|
"integrity": "sha512-KIlzWXMHUvgfPUR0R7TK3H80yCIi0uoivUwf+6Az4yrHJD1Q3c1qIkh/H5Z0i/K3QXgtq/UMEkWyBUSUwnpnOg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -6160,7 +6167,7 @@
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"fast-glob": "^3.3.3",
|
"fast-glob": "^3.3.3",
|
||||||
"fastest-levenshtein": "^1.0.16",
|
"fastest-levenshtein": "^1.0.16",
|
||||||
"file-entry-cache": "^11.1.2",
|
"file-entry-cache": "^11.1.3",
|
||||||
"global-modules": "^2.0.0",
|
"global-modules": "^2.0.0",
|
||||||
"globby": "^16.2.0",
|
"globby": "^16.2.0",
|
||||||
"globjoin": "^0.1.4",
|
"globjoin": "^0.1.4",
|
||||||
|
|
@ -6252,24 +6259,24 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stylelint/node_modules/file-entry-cache": {
|
"node_modules/stylelint/node_modules/file-entry-cache": {
|
||||||
"version": "11.1.2",
|
"version": "11.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.3.tgz",
|
||||||
"integrity": "sha512-N2WFfK12gmrK1c1GXOqiAJ1tc5YE+R53zvQ+t5P8S5XhnmKYVB5eZEiLNZKDSmoG8wqqbF9EXYBBW/nef19log==",
|
"integrity": "sha512-oMbq0PD6VIiIwMF6LIa7MEwd/l9huKwmqRKXqmrkqIZv8CvRbfowL+L0ryAl8h//HfAS0zS+4SbYoRyAoA6BJA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"flat-cache": "^6.1.20"
|
"flat-cache": "^6.1.22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stylelint/node_modules/flat-cache": {
|
"node_modules/stylelint/node_modules/flat-cache": {
|
||||||
"version": "6.1.20",
|
"version": "6.1.22",
|
||||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.20.tgz",
|
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.22.tgz",
|
||||||
"integrity": "sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==",
|
"integrity": "sha512-N2dnzVJIphnNsjHcrxGW7DePckJ6haPrSFqpsBUhHYgwtKGVq4JrBGielEGD2fCVnsGm1zlBVZ8wGhkyuetgug==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cacheable": "^2.3.2",
|
"cacheable": "^2.3.4",
|
||||||
"flatted": "^3.3.3",
|
"flatted": "^3.4.2",
|
||||||
"hookified": "^1.15.0"
|
"hookified": "^1.15.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -6689,18 +6696,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unplugin-vue-components": {
|
"node_modules/unplugin-vue-components": {
|
||||||
"version": "32.0.0",
|
"version": "32.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-32.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-32.1.0.tgz",
|
||||||
"integrity": "sha512-uLdccgS7mf3pv1bCCP20y/hm+u1eOjAmygVkh+Oa70MPkzgl1eQv1L0CwdHNM3gscO8/GDMGIET98Ja47CBbZg==",
|
"integrity": "sha512-YiUkSxuRjab18XFOrX5VsIxXzccrfmHVGsGeJgSgklb829DQmCy9E4vvDUE4tuvZZdxyFJZX0Oc4TPnnxiiMyg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^5.0.0",
|
"chokidar": "^5.0.0",
|
||||||
"local-pkg": "^1.1.2",
|
"local-pkg": "^1.2.0",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
"mlly": "^1.8.2",
|
"mlly": "^1.8.2",
|
||||||
"obug": "^2.1.1",
|
"obug": "^2.1.1",
|
||||||
"picomatch": "^4.0.3",
|
"picomatch": "^4.0.4",
|
||||||
"tinyglobby": "^0.2.15",
|
"tinyglobby": "^0.2.16",
|
||||||
"unplugin": "^3.0.0",
|
"unplugin": "^3.0.0",
|
||||||
"unplugin-utils": "^0.3.1"
|
"unplugin-utils": "^0.3.1"
|
||||||
},
|
},
|
||||||
|
|
@ -6769,15 +6776,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.13",
|
"version": "8.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz",
|
||||||
"integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
|
"integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==",
|
||||||
"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.14",
|
"postcss": "^8.5.15",
|
||||||
"rolldown": "1.0.1",
|
"rolldown": "1.0.2",
|
||||||
"tinyglobby": "^0.2.16"
|
"tinyglobby": "^0.2.16"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@
|
||||||
"source": "index.html",
|
"source": "index.html",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"stylelint": "^17.11.1",
|
"stylelint": "^17.12.0",
|
||||||
"stylelint-config-standard": "^40.0.0"
|
"stylelint-config-standard": "^40.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "node --test resources/vue/components/*.test.mjs"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"parcelIgnore": [
|
"parcelIgnore": [
|
||||||
|
|
@ -33,8 +33,8 @@
|
||||||
"iconify-icon": "^3.0.2",
|
"iconify-icon": "^3.0.2",
|
||||||
"picocrank": "^1.15.0",
|
"picocrank": "^1.15.0",
|
||||||
"standard": "^17.1.2",
|
"standard": "^17.1.2",
|
||||||
"unplugin-vue-components": "^32.0.0",
|
"unplugin-vue-components": "^32.1.0",
|
||||||
"vite": "^8.0.13",
|
"vite": "^8.0.14",
|
||||||
"vue": "^3.5.34",
|
"vue": "^3.5.34",
|
||||||
"vue-i18n": "^11.4.4",
|
"vue-i18n": "^11.4.4",
|
||||||
"vue-router": "^5.0.7"
|
"vue-router": "^5.0.7"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// @generated by protoc-gen-es v2.11.0
|
// @generated by protoc-gen-es v2.12.0
|
||||||
// @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3)
|
// @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3)
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
@ -60,6 +60,36 @@ export declare type Action = Message<"olivetin.api.v1.Action"> & {
|
||||||
* @generated from field: string datetime_rate_limit_expires = 9;
|
* @generated from field: string datetime_rate_limit_expires = 9;
|
||||||
*/
|
*/
|
||||||
datetimeRateLimitExpires: string;
|
datetimeRateLimitExpires: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: bool exec_on_startup = 10;
|
||||||
|
*/
|
||||||
|
execOnStartup: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: repeated string exec_on_cron = 11;
|
||||||
|
*/
|
||||||
|
execOnCron: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: repeated string exec_on_file_created_in_dir = 12;
|
||||||
|
*/
|
||||||
|
execOnFileCreatedInDir: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: repeated string exec_on_file_changed_in_dir = 13;
|
||||||
|
*/
|
||||||
|
execOnFileChangedInDir: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: string exec_on_calendar_file = 14;
|
||||||
|
*/
|
||||||
|
execOnCalendarFile: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: repeated olivetin.api.v1.ActionWebhookExecHint exec_on_webhooks = 15;
|
||||||
|
*/
|
||||||
|
execOnWebhooks: ActionWebhookExecHint[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -68,6 +98,37 @@ export declare type Action = Message<"olivetin.api.v1.Action"> & {
|
||||||
*/
|
*/
|
||||||
export declare const ActionSchema: GenMessage<Action>;
|
export declare const ActionSchema: GenMessage<Action>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from message olivetin.api.v1.ActionWebhookExecHint
|
||||||
|
*/
|
||||||
|
export declare type ActionWebhookExecHint = Message<"olivetin.api.v1.ActionWebhookExecHint"> & {
|
||||||
|
/**
|
||||||
|
* @generated from field: string template = 1;
|
||||||
|
*/
|
||||||
|
template: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: string match_path = 2;
|
||||||
|
*/
|
||||||
|
matchPath: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: map<string, string> match_headers = 3;
|
||||||
|
*/
|
||||||
|
matchHeaders: { [key: string]: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: map<string, string> match_query = 4;
|
||||||
|
*/
|
||||||
|
matchQuery: { [key: string]: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the message olivetin.api.v1.ActionWebhookExecHint.
|
||||||
|
* Use `create(ActionWebhookExecHintSchema)` to create a new message.
|
||||||
|
*/
|
||||||
|
export declare const ActionWebhookExecHintSchema: GenMessage<ActionWebhookExecHint>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from message olivetin.api.v1.ActionArgument
|
* @generated from message olivetin.api.v1.ActionArgument
|
||||||
*/
|
*/
|
||||||
|
|
@ -188,7 +249,7 @@ export declare type GetDashboardResponse = Message<"olivetin.api.v1.GetDashboard
|
||||||
/**
|
/**
|
||||||
* @generated from field: olivetin.api.v1.Dashboard dashboard = 4;
|
* @generated from field: olivetin.api.v1.Dashboard dashboard = 4;
|
||||||
*/
|
*/
|
||||||
dashboard?: Dashboard;
|
dashboard?: Dashboard | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -302,7 +363,7 @@ export declare type DashboardComponent = Message<"olivetin.api.v1.DashboardCompo
|
||||||
/**
|
/**
|
||||||
* @generated from field: olivetin.api.v1.Action action = 6;
|
* @generated from field: olivetin.api.v1.Action action = 6;
|
||||||
*/
|
*/
|
||||||
action?: Action;
|
action?: Action | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from field: string entity_type = 7;
|
* @generated from field: string entity_type = 7;
|
||||||
|
|
@ -412,7 +473,7 @@ export declare type StartActionAndWaitResponse = Message<"olivetin.api.v1.StartA
|
||||||
/**
|
/**
|
||||||
* @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
|
* @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
|
||||||
*/
|
*/
|
||||||
logEntry?: LogEntry;
|
logEntry?: LogEntry | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -476,7 +537,7 @@ export declare type StartActionByGetAndWaitResponse = Message<"olivetin.api.v1.S
|
||||||
/**
|
/**
|
||||||
* @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
|
* @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
|
||||||
*/
|
*/
|
||||||
logEntry?: LogEntry;
|
logEntry?: LogEntry | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -825,7 +886,7 @@ export declare type ExecutionStatusResponse = Message<"olivetin.api.v1.Execution
|
||||||
/**
|
/**
|
||||||
* @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
|
* @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
|
||||||
*/
|
*/
|
||||||
logEntry?: LogEntry;
|
logEntry?: LogEntry | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1135,7 +1196,7 @@ export declare type EventExecutionFinished = Message<"olivetin.api.v1.EventExecu
|
||||||
/**
|
/**
|
||||||
* @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
|
* @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
|
||||||
*/
|
*/
|
||||||
logEntry?: LogEntry;
|
logEntry?: LogEntry | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1151,7 +1212,7 @@ export declare type EventExecutionStarted = Message<"olivetin.api.v1.EventExecut
|
||||||
/**
|
/**
|
||||||
* @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
|
* @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
|
||||||
*/
|
*/
|
||||||
logEntry?: LogEntry;
|
logEntry?: LogEntry | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1437,7 +1498,7 @@ export declare type InitResponse = Message<"olivetin.api.v1.InitResponse"> & {
|
||||||
/**
|
/**
|
||||||
* @generated from field: olivetin.api.v1.EffectivePolicy effective_policy = 18;
|
* @generated from field: olivetin.api.v1.EffectivePolicy effective_policy = 18;
|
||||||
*/
|
*/
|
||||||
effectivePolicy?: EffectivePolicy;
|
effectivePolicy?: EffectivePolicy | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from field: string banner_message = 19;
|
* @generated from field: string banner_message = 19;
|
||||||
|
|
@ -1553,7 +1614,7 @@ export declare type GetActionBindingResponse = Message<"olivetin.api.v1.GetActio
|
||||||
/**
|
/**
|
||||||
* @generated from field: olivetin.api.v1.Action action = 1;
|
* @generated from field: olivetin.api.v1.Action action = 1;
|
||||||
*/
|
*/
|
||||||
action?: Action;
|
action?: Action | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1858,3 +1919,4 @@ export declare const OliveTinApiService: GenService<{
|
||||||
output: typeof EntitySchema;
|
output: typeof EntitySchema;
|
||||||
},
|
},
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div :id="`actionButton-${bindingId}`" role="none" class="action-button">
|
<div :id="`actionButton-${bindingId}`" role="none" class="action-button" @contextmenu.prevent="openActionDetails">
|
||||||
<button :id="`actionButtonInner-${bindingId}`" :title="title" :disabled="!canExec || isDisabled"
|
<button :id="`actionButtonInner-${bindingId}`" :title="title" :disabled="!canExec || isDisabled"
|
||||||
:class="combinedClasses" @click="handleClick">
|
:class="combinedClasses" @click="handleClick">
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="icon" v-html="unicodeIcon"></span>
|
<ActionIconGlyph class="icon" :glyph="actionGlyph" />
|
||||||
<span class="title" aria-live="polite">{{ displayTitle }}
|
<span class="title" aria-live="polite">{{ displayTitle }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="rateLimitMessage" class="rate-limit-message">{{ rateLimitMessage }}</span>
|
<span v-if="rateLimitMessage" class="rate-limit-message">{{ rateLimitMessage }}</span>
|
||||||
|
|
@ -33,7 +33,9 @@ import { useRouter } from 'vue-router'
|
||||||
import { HugeiconsIcon } from '@hugeicons/vue'
|
import { HugeiconsIcon } from '@hugeicons/vue'
|
||||||
import { WorkoutRunIcon, TypeCursorIcon, ComputerTerminal01Icon, WorkHistoryIcon } from '@hugeicons/core-free-icons'
|
import { WorkoutRunIcon, TypeCursorIcon, ComputerTerminal01Icon, WorkHistoryIcon } from '@hugeicons/core-free-icons'
|
||||||
|
|
||||||
import { ref, watch, onMounted, onUnmounted, inject, computed } from 'vue'
|
import ActionIconGlyph from './components/ActionIconGlyph.vue'
|
||||||
|
|
||||||
|
import { ref, watch, onMounted, onUnmounted, computed } from 'vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const navigateOnStart = ref('')
|
const navigateOnStart = ref('')
|
||||||
|
|
@ -56,7 +58,6 @@ const canExec = ref(true)
|
||||||
const popupOnStart = ref('')
|
const popupOnStart = ref('')
|
||||||
|
|
||||||
// Display properties
|
// Display properties
|
||||||
const unicodeIcon = ref('💩')
|
|
||||||
const displayTitle = ref('')
|
const displayTitle = ref('')
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
@ -77,6 +78,9 @@ const showNavigateOnStartIcons = computed(() => {
|
||||||
return window.initResponse?.showNavigateOnStartIcons ?? true
|
return window.initResponse?.showNavigateOnStartIcons ?? true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const actionGlyph = computed(() => props.actionData?.icon ?? '')
|
||||||
|
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]
|
||||||
|
|
@ -89,16 +93,6 @@ const combinedClasses = computed(() => {
|
||||||
// Timestamps
|
// Timestamps
|
||||||
const updateIterationTimestamp = ref(0)
|
const updateIterationTimestamp = ref(0)
|
||||||
|
|
||||||
function getUnicodeIcon(icon) {
|
|
||||||
if (icon === '') {
|
|
||||||
console.log('icon not found ', icon)
|
|
||||||
|
|
||||||
return '💩'
|
|
||||||
} else {
|
|
||||||
return unescape(icon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function constructFromJson(json) {
|
function constructFromJson(json) {
|
||||||
updateIterationTimestamp.value = 0
|
updateIterationTimestamp.value = 0
|
||||||
|
|
||||||
|
|
@ -119,8 +113,7 @@ function constructFromJson(json) {
|
||||||
|
|
||||||
isDisabled.value = !json.canExec
|
isDisabled.value = !json.canExec
|
||||||
displayTitle.value = title.value
|
displayTitle.value = title.value
|
||||||
unicodeIcon.value = getUnicodeIcon(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'))
|
||||||
|
|
@ -139,8 +132,6 @@ 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
|
||||||
|
|
||||||
unicodeIcon.value = getUnicodeIcon(json.icon)
|
|
||||||
|
|
||||||
// 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'))
|
||||||
|
|
@ -191,7 +182,19 @@ function updateRateLimitStatus() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openActionDetails() {
|
||||||
|
const id = props.actionData?.bindingId
|
||||||
|
if (!id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router.push(`/action/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
async function handleClick() {
|
async function handleClick() {
|
||||||
|
if (popupOnStart.value === 'history') {
|
||||||
|
openActionDetails()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (props.actionData.arguments && props.actionData.arguments.length > 0) {
|
if (props.actionData.arguments && props.actionData.arguments.length > 0) {
|
||||||
router.push(`/actionBinding/${props.actionData.bindingId}/argumentForm`)
|
router.push(`/actionBinding/${props.actionData.bindingId}/argumentForm`)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -249,8 +252,6 @@ function onLogEntryChanged(logEntry) {
|
||||||
function onExecutionStarted(logEntry) {
|
function onExecutionStarted(logEntry) {
|
||||||
if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
|
if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
|
||||||
router.push(`/logs/${logEntry.executionTrackingId}`)
|
router.push(`/logs/${logEntry.executionTrackingId}`)
|
||||||
} else if (popupOnStart.value === 'history') {
|
|
||||||
router.push(`/action/${bindingId.value}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isDisabled.value = true
|
isDisabled.value = true
|
||||||
|
|
@ -324,10 +325,17 @@ watch(
|
||||||
() => props.actionData,
|
() => props.actionData,
|
||||||
(newData) => {
|
(newData) => {
|
||||||
updateFromJson(newData)
|
updateFromJson(newData)
|
||||||
|
if (newData?.icon !== undefined) {
|
||||||
|
glyph.value = newData.icon ?? ''
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
glyph
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<span class="action-icon-glyph">
|
||||||
|
<HugeiconsIcon
|
||||||
|
v-if="hugeiconsModel"
|
||||||
|
:icon="hugeiconsModel"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
class="action-icon-glyph-svg"
|
||||||
|
/>
|
||||||
|
<span v-else-if="decodedTextGlyphIsHtml" v-html="decodedTextGlyph"></span>
|
||||||
|
<span v-else v-text="decodedTextGlyph"></span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { HugeiconsIcon } from '@hugeicons/vue'
|
||||||
|
import { CommandLineIcon } from '@hugeicons/core-free-icons'
|
||||||
|
import { decodeHtmlEntities, glyphLooksLikeHtml } from './actionIconGlyphHelpers.mjs'
|
||||||
|
|
||||||
|
const hugeiconsPrefix = 'hugeicons:'
|
||||||
|
|
||||||
|
/** Maps config values like hugeicons:CommandLineIcon to Hugeicons icon definitions. */
|
||||||
|
const hugeiconsRegistry = {
|
||||||
|
CommandLineIcon,
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
glyph: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const hugeiconsModel = computed(() => {
|
||||||
|
if (!props.glyph) {
|
||||||
|
return CommandLineIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.glyph.startsWith(hugeiconsPrefix)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = props.glyph.slice(hugeiconsPrefix.length)
|
||||||
|
const iconModel = hugeiconsRegistry[name]
|
||||||
|
|
||||||
|
return iconModel ?? CommandLineIcon
|
||||||
|
})
|
||||||
|
|
||||||
|
const decodedTextGlyph = computed(() => {
|
||||||
|
if (hugeiconsModel.value) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodeHtmlEntities(props.glyph)
|
||||||
|
})
|
||||||
|
|
||||||
|
const decodedTextGlyphIsHtml = computed(() => glyphLooksLikeHtml(decodedTextGlyph.value))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.action-icon-glyph {
|
||||||
|
display: inline-flex;
|
||||||
|
vertical-align: middle;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon-glyph-svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
const fallbackNamedHtmlEntities = {
|
||||||
|
amp: '&',
|
||||||
|
apos: "'",
|
||||||
|
darr: '\u2193',
|
||||||
|
gt: '>',
|
||||||
|
laquo: '\u00ab',
|
||||||
|
larr: '\u2190',
|
||||||
|
nbsp: '\u00a0',
|
||||||
|
quot: '"',
|
||||||
|
raquo: '\u00bb',
|
||||||
|
rarr: '\u2192',
|
||||||
|
uarr: '\u2191',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeHtmlEntities(text) {
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
textarea.innerHTML = text
|
||||||
|
|
||||||
|
return textarea.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.replace(/&#x([0-9a-fA-F]+);?/g, (_, hex) => {
|
||||||
|
const codePoint = Number.parseInt(hex, 16)
|
||||||
|
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : ''
|
||||||
|
}).replace(/&#(\d+);?/g, (_, decimal) => {
|
||||||
|
const codePoint = Number.parseInt(decimal, 10)
|
||||||
|
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : ''
|
||||||
|
}).replace(/&([a-zA-Z][a-zA-Z0-9]+);?/g, (entity, name) => {
|
||||||
|
return fallbackNamedHtmlEntities[name] ?? entity
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function glyphLooksLikeHtml(text) {
|
||||||
|
const trimmedText = text.trim()
|
||||||
|
|
||||||
|
return trimmedText.startsWith('<') || /<img\b/i.test(text) || /\/custom-webui\//i.test(text)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { decodeHtmlEntities, glyphLooksLikeHtml } from './actionIconGlyphHelpers.mjs'
|
||||||
|
|
||||||
|
test('decodeHtmlEntities decodes named entity icons as plain glyph text', () => {
|
||||||
|
assert.equal(decodeHtmlEntities('«'), '\u00ab')
|
||||||
|
assert.equal(decodeHtmlEntities('→'), '\u2192')
|
||||||
|
assert.equal(decodeHtmlEntities('« next →'), '\u00ab next \u2192')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('decoded named entity icons are not treated as HTML markup', () => {
|
||||||
|
const decodedGlyph = decodeHtmlEntities('→')
|
||||||
|
|
||||||
|
assert.equal(glyphLooksLikeHtml(decodedGlyph), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('decodeHtmlEntities keeps existing numeric entity icon support', () => {
|
||||||
|
assert.equal(decodeHtmlEntities('💩'), '\ud83d\udca9')
|
||||||
|
assert.equal(decodeHtmlEntities('💾'), '\ud83d\udcbe')
|
||||||
|
})
|
||||||
|
|
@ -95,6 +95,19 @@ const routes = [
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/action/:actionId/actionexecconditions',
|
||||||
|
name: 'ActionExecConditions',
|
||||||
|
component: () => import('./views/ActionExecConditionsView.vue'),
|
||||||
|
props: true,
|
||||||
|
meta: {
|
||||||
|
title: 'Execution conditions',
|
||||||
|
breadcrumb: [
|
||||||
|
{ name: "Actions", href: "/" },
|
||||||
|
{ name: "Execution conditions" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/diagnostics',
|
path: '/diagnostics',
|
||||||
name: 'Diagnostics',
|
name: 'Diagnostics',
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<Section :title="'Action Details: ' + actionTitle" :padding="false">
|
<Section :title="'Action Details: ' + actionTitle" :padding="false">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<button v-if="action" @click="startAction" title="Start this action" class="button neutral">
|
<div class="action-details-toolbar">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
<button v-if="action" @click="startAction" title="Start this action" class="button neutral">
|
||||||
<path fill="currentColor" d="M8 6v12l8-6z" />
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||||
</svg>
|
<path fill="currentColor" d="M8 6v12l8-6z" />
|
||||||
Start
|
</svg>
|
||||||
</button>
|
Start
|
||||||
|
</button>
|
||||||
|
<router-link
|
||||||
|
v-if="action"
|
||||||
|
:to="{ name: 'ActionExecConditions', params: { actionId: route.params.actionId } }"
|
||||||
|
class="button neutral"
|
||||||
|
title="View configured automatic triggers and on-demand execution"
|
||||||
|
>
|
||||||
|
Execution conditions
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class = "flex-row padding" v-if="action">
|
<div class = "flex-row padding" v-if="action">
|
||||||
|
|
@ -22,7 +32,7 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style = "align-self: start; text-align: right;">
|
<div style = "align-self: start; text-align: right;">
|
||||||
<span class="icon" v-html="action.icon"></span>
|
<ActionIconGlyph class="icon" :glyph="action.icon" />
|
||||||
|
|
||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
<label class="input-with-icons">
|
<label class="input-with-icons">
|
||||||
|
|
@ -94,6 +104,7 @@ import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import Pagination from 'picocrank/vue/components/Pagination.vue'
|
import Pagination from 'picocrank/vue/components/Pagination.vue'
|
||||||
import Section from 'picocrank/vue/components/Section.vue'
|
import Section from 'picocrank/vue/components/Section.vue'
|
||||||
|
import ActionIconGlyph from '../components/ActionIconGlyph.vue'
|
||||||
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
|
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
@ -428,5 +439,11 @@ watch(
|
||||||
.padding {
|
.padding {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
.action-details-toolbar {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,219 @@
|
||||||
|
<template>
|
||||||
|
<Section :title="'Execution conditions: ' + actionTitle" :padding="false">
|
||||||
|
<template #toolbar>
|
||||||
|
<router-link :to="{ name: 'ActionDetails', params: { actionId: route.params.actionId } }" class="button neutral">
|
||||||
|
Back to action details
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="action" class="padding content">
|
||||||
|
<p>
|
||||||
|
These entries mirror the automatic triggers from your OliveTin configuration for this action.
|
||||||
|
You can always run the action manually as well.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="exec-type-heading">
|
||||||
|
On demand
|
||||||
|
<a class="doc-link" :href="execConditionDocs.onDemand" target="_blank" rel="noopener noreferrer">Documentation</a>
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
Manual execution from the web UI (dashboard or action details), or via the API (for example StartAction),
|
||||||
|
is always available when your user is allowed to execute the action.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<template v-if="action.execOnStartup">
|
||||||
|
<h3 class="exec-type-heading">
|
||||||
|
<code>execOnStartup</code>
|
||||||
|
<a class="doc-link" :href="execConditionDocs.startup" target="_blank" rel="noopener noreferrer">Documentation</a>
|
||||||
|
</h3>
|
||||||
|
<p>Runs once when OliveTin starts.</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="nonEmptyList(action.execOnCron)">
|
||||||
|
<h3 class="exec-type-heading">
|
||||||
|
<code>execOnCron</code>
|
||||||
|
<a class="doc-link" :href="execConditionDocs.cron" target="_blank" rel="noopener noreferrer">Documentation</a>
|
||||||
|
</h3>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(line, idx) in action.execOnCron" :key="'cron-' + idx"><code>{{ line }}</code></li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="nonEmptyList(action.execOnFileCreatedInDir)">
|
||||||
|
<h3 class="exec-type-heading">
|
||||||
|
<code>execOnFileCreatedInDir</code>
|
||||||
|
<a class="doc-link" :href="execConditionDocs.fileCreated" target="_blank" rel="noopener noreferrer">Documentation</a>
|
||||||
|
</h3>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(dir, idx) in action.execOnFileCreatedInDir" :key="'created-' + idx"><code>{{ dir }}</code></li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="nonEmptyList(action.execOnFileChangedInDir)">
|
||||||
|
<h3 class="exec-type-heading">
|
||||||
|
<code>execOnFileChangedInDir</code>
|
||||||
|
<a class="doc-link" :href="execConditionDocs.fileChanged" target="_blank" rel="noopener noreferrer">Documentation</a>
|
||||||
|
</h3>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(dir, idx) in action.execOnFileChangedInDir" :key="'changed-' + idx"><code>{{ dir }}</code></li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="action.execOnCalendarFile">
|
||||||
|
<h3 class="exec-type-heading">
|
||||||
|
<code>execOnCalendarFile</code>
|
||||||
|
<a class="doc-link" :href="execConditionDocs.calendar" target="_blank" rel="noopener noreferrer">Documentation</a>
|
||||||
|
</h3>
|
||||||
|
<p><code>{{ action.execOnCalendarFile }}</code></p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="nonEmptyList(action.execOnWebhooks)">
|
||||||
|
<h3 class="exec-type-heading">
|
||||||
|
<code>execOnWebhook</code>
|
||||||
|
<a class="doc-link" :href="execConditionDocs.webhook" target="_blank" rel="noopener noreferrer">Documentation</a>
|
||||||
|
</h3>
|
||||||
|
<ul class="webhook-list">
|
||||||
|
<li v-for="(wh, idx) in action.execOnWebhooks" :key="'wh-' + idx">
|
||||||
|
<span v-if="wh.template">template: <code>{{ wh.template }}</code></span>
|
||||||
|
<span v-if="wh.matchPath"> · matchPath: <code>{{ wh.matchPath }}</code></span>
|
||||||
|
<span v-if="nonEmptyObject(wh.matchHeaders)"> · matchHeaders: <code>{{ wh.matchHeaders }}</code></span>
|
||||||
|
<span v-if="nonEmptyObject(wh.matchQuery)"> · matchQuery: <code>{{ wh.matchQuery }}</code></span>
|
||||||
|
<span v-if="!webhookHasCondition(wh)">Webhook trigger (no conditions in response)</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<p v-if="!hasConfiguredTriggers" class="muted">
|
||||||
|
This action has no automatic triggers in configuration besides on-demand execution.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="!loading" class="padding empty-state">
|
||||||
|
<p>Could not load this action.</p>
|
||||||
|
<router-link :to="{ name: 'Actions' }">Return to index</router-link>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import Section from 'picocrank/vue/components/Section.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const action = ref(null)
|
||||||
|
const actionTitle = ref('Action')
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
const execConditionDocs = {
|
||||||
|
onDemand: 'https://docs.olivetin.app/action_execution/ondemand.html',
|
||||||
|
startup: 'https://docs.olivetin.app/action_execution/onstartup.html',
|
||||||
|
cron: 'https://docs.olivetin.app/action_execution/oncron.html',
|
||||||
|
fileCreated: 'https://docs.olivetin.app/action_execution/onfilecreated.html',
|
||||||
|
fileChanged: 'https://docs.olivetin.app/action_execution/onfilechanged.html',
|
||||||
|
calendar: 'https://docs.olivetin.app/action_execution/oncalendar.html',
|
||||||
|
webhook: 'https://docs.olivetin.app/action_execution/onwebhook.html',
|
||||||
|
}
|
||||||
|
|
||||||
|
function nonEmptyList(list) {
|
||||||
|
return Array.isArray(list) && list.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function nonEmptyObject(object) {
|
||||||
|
return object && Object.keys(object).length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function webhookHasCondition(webhook) {
|
||||||
|
return webhook.template || webhook.matchPath || nonEmptyObject(webhook.matchHeaders) || nonEmptyObject(webhook.matchQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasConfiguredTriggers = computed(() => {
|
||||||
|
const a = action.value
|
||||||
|
if (!a) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (a.execOnStartup) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (nonEmptyList(a.execOnCron) || nonEmptyList(a.execOnFileCreatedInDir) || nonEmptyList(a.execOnFileChangedInDir)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (a.execOnCalendarFile) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (nonEmptyList(a.execOnWebhooks)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetchAction() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const actionId = route.params.actionId
|
||||||
|
const response = await window.client.getActionBinding({ bindingId: actionId })
|
||||||
|
action.value = response.action
|
||||||
|
actionTitle.value = response.action?.title || 'Action'
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch action:', err)
|
||||||
|
window.showBigError('fetch-action-exec-conditions', 'getting action', err, false)
|
||||||
|
action.value = null
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(fetchAction)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.params.actionId,
|
||||||
|
() => {
|
||||||
|
action.value = null
|
||||||
|
actionTitle.value = 'Action'
|
||||||
|
fetchAction()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.content h3 {
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exec-type-heading {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.35rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exec-type-heading .doc-link {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content p,
|
||||||
|
.content ul {
|
||||||
|
margin: 0.35rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.webhook-list li {
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -174,7 +174,7 @@ function getInputType(arg) {
|
||||||
return 'checkbox'
|
return 'checkbox'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arg.type === 'ascii_identifier' || arg.type === 'ascii' || arg.type === 'ascii_sentence') {
|
if (arg.type === 'ascii_identifier' || arg.type === 'shell_safe_identifier' || arg.type === 'ascii' || arg.type === 'ascii_sentence') {
|
||||||
return 'text'
|
return 'text'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -392,6 +392,11 @@ async function startAction(actionArgs) {
|
||||||
async function handleSubmit(event) {
|
async function handleSubmit(event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (popupOnStart.value === 'history') {
|
||||||
|
router.push(`/action/${props.bindingId}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Set custom validity for required fields
|
// Set custom validity for required fields
|
||||||
for (const arg of actionArguments.value) {
|
for (const arg of actionArguments.value) {
|
||||||
const value = argValues.value[arg.name]
|
const value = argValues.value[arg.name]
|
||||||
|
|
@ -422,8 +427,6 @@ async function handleSubmit(event) {
|
||||||
const response = await startAction(argvs)
|
const response = await startAction(argvs)
|
||||||
if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
|
if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
|
||||||
router.push(`/logs/${response.executionTrackingId}`)
|
router.push(`/logs/${response.executionTrackingId}`)
|
||||||
} else if (popupOnStart.value === 'history') {
|
|
||||||
router.push(`/action/${props.bindingId}`)
|
|
||||||
} else {
|
} else {
|
||||||
router.back()
|
router.back()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
<ActionStatusDisplay :log-entry="logEntry" id = "execution-dialog-status" />
|
<ActionStatusDisplay :log-entry="logEntry" id = "execution-dialog-status" />
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<span class="icon" role="img" v-html="icon" style = "align-self: start"></span>
|
<ActionIconGlyph class="icon" role="img" :glyph="icon" style="align-self: start" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="notFound" class="error-message padded-content">
|
<div v-if="notFound" class="error-message padded-content">
|
||||||
|
|
@ -62,6 +62,7 @@
|
||||||
|
|
||||||
<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 ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
|
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
|
||||||
import Section from 'picocrank/vue/components/Section.vue'
|
import Section from 'picocrank/vue/components/Section.vue'
|
||||||
import { OutputTerminal } from '../../../js/OutputTerminal.js'
|
import { OutputTerminal } from '../../../js/OutputTerminal.js'
|
||||||
|
|
@ -151,7 +152,7 @@ async function reset() {
|
||||||
|
|
||||||
function show(actionButton) {
|
function show(actionButton) {
|
||||||
if (actionButton) {
|
if (actionButton) {
|
||||||
icon.value = actionButton.domIcon.innerText
|
icon.value = actionButton.glyph ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
canKill.value = true
|
canKill.value = true
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
<tr v-for="log in filteredLogs" :key="log.executionTrackingId" class="log-row" :title="log.actionTitle">
|
<tr v-for="log in filteredLogs" :key="log.executionTrackingId" class="log-row" :title="log.actionTitle">
|
||||||
<td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td>
|
<td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="icon" v-html="log.actionIcon"></span>
|
<ActionIconGlyph class="icon" :glyph="log.actionIcon" />
|
||||||
<router-link :to="`/logs/${log.executionTrackingId}`">
|
<router-link :to="`/logs/${log.executionTrackingId}`">
|
||||||
{{ log.actionTitle }}
|
{{ log.actionTitle }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
@ -93,6 +93,7 @@ import Pagination from 'picocrank/vue/components/Pagination.vue'
|
||||||
import Section from 'picocrank/vue/components/Section.vue'
|
import Section from 'picocrank/vue/components/Section.vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
|
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
|
||||||
|
import ActionIconGlyph from '../components/ActionIconGlyph.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -126,7 +127,7 @@ watch(() => route.query.date, () => {
|
||||||
|
|
||||||
const filteredLogs = computed(() => {
|
const filteredLogs = computed(() => {
|
||||||
let result = logs.value
|
let result = logs.value
|
||||||
|
|
||||||
// Date filtering is now done server-side, so we only need to filter by search text
|
// Date filtering is now done server-side, so we only need to filter by search text
|
||||||
if (searchText.value) {
|
if (searchText.value) {
|
||||||
const searchLower = searchText.value.toLowerCase()
|
const searchLower = searchText.value.toLowerCase()
|
||||||
|
|
@ -134,7 +135,7 @@ const filteredLogs = computed(() => {
|
||||||
log.actionTitle.toLowerCase().includes(searchLower)
|
log.actionTitle.toLowerCase().includes(searchLower)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by timestamp with most recent first
|
// Sort by timestamp with most recent first
|
||||||
return [...result].sort((a, b) => {
|
return [...result].sort((a, b) => {
|
||||||
const dateA = a.datetimeStarted ? new Date(a.datetimeStarted).getTime() : 0
|
const dateA = a.datetimeStarted ? new Date(a.datetimeStarted).getTime() : 0
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^6.2.2",
|
"chai": "^6.2.2",
|
||||||
"eslint": "^10.4.0",
|
"eslint": "^10.4.0",
|
||||||
"mocha": "^11.7.5",
|
"mocha": "^11.7.6",
|
||||||
"selenium-webdriver": "^4.44.0"
|
"selenium-webdriver": "^4.44.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1575,9 +1575,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mocha": {
|
"node_modules/mocha": {
|
||||||
"version": "11.7.5",
|
"version": "11.7.6",
|
||||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.6.tgz",
|
||||||
"integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==",
|
"integrity": "sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^6.2.2",
|
"chai": "^6.2.2",
|
||||||
"eslint": "^10.4.0",
|
"eslint": "^10.4.0",
|
||||||
"mocha": "^11.7.5",
|
"mocha": "^11.7.6",
|
||||||
"selenium-webdriver": "^4.44.0"
|
"selenium-webdriver": "^4.44.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,19 @@ message Action {
|
||||||
int32 order = 7;
|
int32 order = 7;
|
||||||
int32 timeout = 8;
|
int32 timeout = 8;
|
||||||
string datetime_rate_limit_expires = 9; // Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05"
|
string datetime_rate_limit_expires = 9; // Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05"
|
||||||
|
bool exec_on_startup = 10;
|
||||||
|
repeated string exec_on_cron = 11;
|
||||||
|
repeated string exec_on_file_created_in_dir = 12;
|
||||||
|
repeated string exec_on_file_changed_in_dir = 13;
|
||||||
|
string exec_on_calendar_file = 14;
|
||||||
|
repeated ActionWebhookExecHint exec_on_webhooks = 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ActionWebhookExecHint {
|
||||||
|
string template = 1;
|
||||||
|
string match_path = 2;
|
||||||
|
map<string, string> match_headers = 3;
|
||||||
|
map<string, string> match_query = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ActionArgument {
|
message ActionArgument {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,16 +1,16 @@
|
||||||
module github.com/OliveTin/OliveTin
|
module github.com/OliveTin/OliveTin
|
||||||
|
|
||||||
go 1.25.7
|
go 1.25.10
|
||||||
|
|
||||||
exclude google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884
|
exclude google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884
|
||||||
|
|
||||||
require (
|
require (
|
||||||
connectrpc.com/connect v1.19.2
|
connectrpc.com/connect v1.20.0
|
||||||
github.com/Masterminds/semver v1.5.0
|
github.com/Masterminds/semver v1.5.0
|
||||||
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.69.0
|
github.com/bufbuild/buf v1.70.0
|
||||||
github.com/fsnotify/fsnotify v1.10.1
|
github.com/fsnotify/fsnotify v1.10.1
|
||||||
github.com/fzipp/gocyclo v0.6.0
|
github.com/fzipp/gocyclo v0.6.0
|
||||||
github.com/go-critic/go-critic v0.14.3
|
github.com/go-critic/go-critic v0.14.3
|
||||||
|
|
@ -29,7 +29,7 @@ require (
|
||||||
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-20260508232706-74f9aab9d74a
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
golang.org/x/sys v0.44.0
|
golang.org/x/sys v0.45.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
|
||||||
)
|
)
|
||||||
|
|
@ -38,7 +38,7 @@ require (
|
||||||
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1 // indirect
|
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.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.19.2-20260507063250-43b0c5a6cd08.1 // indirect
|
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260507063250-43b0c5a6cd08.1 // indirect
|
||||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260507063250-43b0c5a6cd08.1 // indirect
|
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260507063250-43b0c5a6cd08.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
|
||||||
|
|
@ -57,7 +57,7 @@ require (
|
||||||
github.com/PaesslerAG/gval v1.2.4 // indirect
|
github.com/PaesslerAG/gval v1.2.4 // indirect
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
github.com/antlr4-go/antlr/v4 v4.13.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.20260429155904-12ef1ef2ce91 // indirect
|
github.com/bufbuild/protocompile v0.14.2-0.20260522222248-64e6ad034132 // indirect
|
||||||
github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b // indirect
|
github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cli/browser v1.3.0 // indirect
|
github.com/cli/browser v1.3.0 // indirect
|
||||||
|
|
@ -68,7 +68,7 @@ require (
|
||||||
github.com/cristalhq/acmd v0.12.0 // indirect
|
github.com/cristalhq/acmd v0.12.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/cli v29.5.1+incompatible // indirect
|
github.com/docker/cli v29.5.2+incompatible // indirect
|
||||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||||
github.com/docker/docker v28.5.2+incompatible // indirect
|
github.com/docker/docker v28.5.2+incompatible // indirect
|
||||||
github.com/docker/docker-credential-helpers v0.9.7 // indirect
|
github.com/docker/docker-credential-helpers v0.9.7 // indirect
|
||||||
|
|
@ -148,17 +148,17 @@ require (
|
||||||
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/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.51.0 // indirect
|
golang.org/x/crypto v0.52.0 // indirect
|
||||||
golang.org/x/exp/typeparams v0.0.0-20260508232706-74f9aab9d74a // indirect
|
golang.org/x/exp/typeparams v0.0.0-20260508232706-74f9aab9d74a // indirect
|
||||||
golang.org/x/mod v0.36.0 // indirect
|
golang.org/x/mod v0.36.0 // indirect
|
||||||
golang.org/x/net v0.54.0 // indirect
|
golang.org/x/net v0.55.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/term v0.43.0 // indirect
|
golang.org/x/term v0.43.0 // indirect
|
||||||
golang.org/x/text v0.37.0 // indirect
|
golang.org/x/text v0.37.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.45.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 // indirect
|
||||||
google.golang.org/grpc v1.79.3 // indirect
|
google.golang.org/grpc v1.79.3 // 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
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-81958296
|
||||||
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-819582968857.2/go.mod h1:mpsjeEaxOYPIJV2cz4IagLghZufRvx+NPVtInjEeoQ8=
|
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-819582968857.2/go.mod h1:mpsjeEaxOYPIJV2cz4IagLghZufRvx+NPVtInjEeoQ8=
|
||||||
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.2-20260507063250-43b0c5a6cd08.1 h1:DcwtSdaY9CwXwPSOneDxJ/B0OCAgNPQQaQxAr/pTHvc=
|
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.2-20260507063250-43b0c5a6cd08.1 h1:DcwtSdaY9CwXwPSOneDxJ/B0OCAgNPQQaQxAr/pTHvc=
|
||||||
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.2-20260507063250-43b0c5a6cd08.1/go.mod h1:WjOwVG7wzFSwEkjCjHVRWEOdGYyON/TQYPabl7N2VGI=
|
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.2-20260507063250-43b0c5a6cd08.1/go.mod h1:WjOwVG7wzFSwEkjCjHVRWEOdGYyON/TQYPabl7N2VGI=
|
||||||
|
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260507063250-43b0c5a6cd08.1 h1:f8pa4iy1Bs+hQ16f3jg22rV/StDKIRj1rNNWX5rLwZ8=
|
||||||
|
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260507063250-43b0c5a6cd08.1/go.mod h1:7MNigA51XJjPKrLnbcE61BmgW+pAp3mLW61gUqLBBRY=
|
||||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20251202164234-62b14f0b533c.1 h1:PdfIJUbUVKdajMVYuMdvr2Wvo+wmzGnlPEYA4bhFaWI=
|
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20251202164234-62b14f0b533c.1 h1:PdfIJUbUVKdajMVYuMdvr2Wvo+wmzGnlPEYA4bhFaWI=
|
||||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20251202164234-62b14f0b533c.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40=
|
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20251202164234-62b14f0b533c.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40=
|
||||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260122161138-ab4e39a3c3bc.1 h1:yWmrELGX6l1GphG9kPVcrMQLjWfXGI5bLDxwE+SfbDw=
|
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260122161138-ab4e39a3c3bc.1 h1:yWmrELGX6l1GphG9kPVcrMQLjWfXGI5bLDxwE+SfbDw=
|
||||||
|
|
@ -64,6 +66,8 @@ connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
|
||||||
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
||||||
connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo=
|
connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo=
|
||||||
connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
||||||
|
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
|
||||||
|
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
|
||||||
connectrpc.com/otelconnect v0.8.0 h1:a4qrN4H8aEE2jAoCxheZYYfEjXMgVPyL9OzPQLBEFXU=
|
connectrpc.com/otelconnect v0.8.0 h1:a4qrN4H8aEE2jAoCxheZYYfEjXMgVPyL9OzPQLBEFXU=
|
||||||
connectrpc.com/otelconnect v0.8.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc=
|
connectrpc.com/otelconnect v0.8.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc=
|
||||||
connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA=
|
connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA=
|
||||||
|
|
@ -112,6 +116,8 @@ github.com/bufbuild/buf v1.66.1 h1:wqmmU+6uoxB/eYDOmXq2To4qEXvOJN7gR6L9AxrPL1E=
|
||||||
github.com/bufbuild/buf v1.66.1/go.mod h1:Vd3ELm8IePWaDJaS9FLy94FFOnLrjLi4mDxmXtw9Xio=
|
github.com/bufbuild/buf v1.66.1/go.mod h1:Vd3ELm8IePWaDJaS9FLy94FFOnLrjLi4mDxmXtw9Xio=
|
||||||
github.com/bufbuild/buf v1.69.0 h1:q1YTnHJISHuoeUdmsuC9u+nb9rV8glM/TOsPNEteEzg=
|
github.com/bufbuild/buf v1.69.0 h1:q1YTnHJISHuoeUdmsuC9u+nb9rV8glM/TOsPNEteEzg=
|
||||||
github.com/bufbuild/buf v1.69.0/go.mod h1:Q3KRCXSanDCMFs2zL/MqUwUQV0OUqs23P2sy58CW0nc=
|
github.com/bufbuild/buf v1.69.0/go.mod h1:Q3KRCXSanDCMFs2zL/MqUwUQV0OUqs23P2sy58CW0nc=
|
||||||
|
github.com/bufbuild/buf v1.70.0 h1:rGL4TGoy8F1DbQa4BSlMOVBBR7lWblfnKxUdOxmeFns=
|
||||||
|
github.com/bufbuild/buf v1.70.0/go.mod h1:5gCCIpDmBzhiSJwqmxmbdN5aRZQYGmFSGnOBE7seP8c=
|
||||||
github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e h1:LQA+1MyiPkolGHJGC2GMDC5Xu+0RDVH6jGMKech7Exs=
|
github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e h1:LQA+1MyiPkolGHJGC2GMDC5Xu+0RDVH6jGMKech7Exs=
|
||||||
github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
|
github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
|
||||||
github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8 h1:cQYwUyAzyMmYr7AyJU1C6pVCpUrJJBkmx7UunZosxxs=
|
github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8 h1:cQYwUyAzyMmYr7AyJU1C6pVCpUrJJBkmx7UunZosxxs=
|
||||||
|
|
@ -126,6 +132,8 @@ github.com/bufbuild/protocompile v0.14.2-0.20260306221011-519528254156 h1:XOfIIn
|
||||||
github.com/bufbuild/protocompile v0.14.2-0.20260306221011-519528254156/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE=
|
github.com/bufbuild/protocompile v0.14.2-0.20260306221011-519528254156/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE=
|
||||||
github.com/bufbuild/protocompile v0.14.2-0.20260429155904-12ef1ef2ce91 h1:RPIMBLTMx/CRy0NVyb6yJDlGx2Vo84FsU+kAh46zqIA=
|
github.com/bufbuild/protocompile v0.14.2-0.20260429155904-12ef1ef2ce91 h1:RPIMBLTMx/CRy0NVyb6yJDlGx2Vo84FsU+kAh46zqIA=
|
||||||
github.com/bufbuild/protocompile v0.14.2-0.20260429155904-12ef1ef2ce91/go.mod h1:DhgqsRznX/F0sGkUYtTQJRP+q8xMReQRQ3qr+n1opWU=
|
github.com/bufbuild/protocompile v0.14.2-0.20260429155904-12ef1ef2ce91/go.mod h1:DhgqsRznX/F0sGkUYtTQJRP+q8xMReQRQ3qr+n1opWU=
|
||||||
|
github.com/bufbuild/protocompile v0.14.2-0.20260522222248-64e6ad034132 h1:f4T4k/41jHHhp2Otl6ZShDedr4wF9b+NdqIfLezx4R4=
|
||||||
|
github.com/bufbuild/protocompile v0.14.2-0.20260522222248-64e6ad034132/go.mod h1:jPUiZUFWc8E3Kc2Y4SRlGAdjde4amGkHY0BUACNS43E=
|
||||||
github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU=
|
github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU=
|
||||||
github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
|
github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
|
||||||
github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b h1:b7wvo9ZhjLzCp7tGbOUMvgtYTnd33zGSAmMxcdxMnhQ=
|
github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b h1:b7wvo9ZhjLzCp7tGbOUMvgtYTnd33zGSAmMxcdxMnhQ=
|
||||||
|
|
@ -170,6 +178,8 @@ github.com/docker/cli v29.4.3+incompatible h1:u+UliYm2J/rYrIh2FqHQg32neRG8GjbvNu
|
||||||
github.com/docker/cli v29.4.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v29.4.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/cli v29.5.1+incompatible h1:NiufLAJoRcPauFoBNYthfuM4REFwM8H2h9xnLABNHGs=
|
github.com/docker/cli v29.5.1+incompatible h1:NiufLAJoRcPauFoBNYthfuM4REFwM8H2h9xnLABNHGs=
|
||||||
github.com/docker/cli v29.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v29.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
|
github.com/docker/cli v29.5.2+incompatible h1:ubykJ1Y8LmNRGJ2BuMQ0kHOt/RO1YzGNswqWMJgivuQ=
|
||||||
|
github.com/docker/cli v29.5.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||||
|
|
@ -531,6 +541,8 @@ golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||||
|
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||||
|
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||||
|
|
@ -582,6 +594,8 @@ golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||||
|
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||||
|
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||||
|
|
@ -616,6 +630,8 @@ golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
|
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
|
@ -683,6 +699,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 h1:
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348/go.mod h1:Yzdzr5OOZFgSsEV2D/Xi9NL3bszpXFAg0hFJiRohcD8=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348/go.mod h1:Yzdzr5OOZFgSsEV2D/Xi9NL3bszpXFAg0hFJiRohcD8=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94 h1:DddG61lE5LkX6144z22i0gma9BMBs5aZ9B8lZLobxyw=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94 h1:DddG61lE5LkX6144z22i0gma9BMBs5aZ9B8lZLobxyw=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:1dCETSCY2YKZNXQE3h4fun3TYwF5p8jejRKZgfWAgAY=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:1dCETSCY2YKZNXQE3h4fun3TYwF5p8jejRKZgfWAgAY=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68 h1:WVVw1Nl19li0fMX++FJ3ye1z9+S1N35QODDy5qpnaXw=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:1dCETSCY2YKZNXQE3h4fun3TYwF5p8jejRKZgfWAgAY=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||||
|
|
@ -699,6 +717,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 h1:
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 h1:eZCjr/aAF8c5ccm5pb6T4EXgIei5MlAAPWPJk+5ArfY=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 h1:eZCjr/aAF8c5ccm5pb6T4EXgIei5MlAAPWPJk+5ArfY=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 h1:PvEgGJf9C/1u5CHkInMg7UFYYUoiaQmW2LbtH0pjB78=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||||
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func applyActionExecTriggers(pb *apiv1.Action, cfg *config.Action) {
|
||||||
|
if cfg == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pb.ExecOnStartup = cfg.ExecOnStartup
|
||||||
|
pb.ExecOnCron = append([]string(nil), cfg.ExecOnCron...)
|
||||||
|
pb.ExecOnFileCreatedInDir = append([]string(nil), cfg.ExecOnFileCreatedInDir...)
|
||||||
|
pb.ExecOnFileChangedInDir = append([]string(nil), cfg.ExecOnFileChangedInDir...)
|
||||||
|
pb.ExecOnCalendarFile = cfg.ExecOnCalendarFile
|
||||||
|
|
||||||
|
for _, wh := range cfg.ExecOnWebhook {
|
||||||
|
pb.ExecOnWebhooks = append(pb.ExecOnWebhooks, &apiv1.ActionWebhookExecHint{
|
||||||
|
Template: wh.Template,
|
||||||
|
MatchPath: wh.MatchPath,
|
||||||
|
MatchHeaders: wh.MatchHeaders,
|
||||||
|
MatchQuery: wh.MatchQuery,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -156,6 +156,8 @@ func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderReque
|
||||||
DatetimeRateLimitExpires: datetimeRateLimitExpires,
|
DatetimeRateLimitExpires: datetimeRateLimitExpires,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyActionExecTriggers(&btn, action)
|
||||||
|
|
||||||
for _, cfgArg := range action.Arguments {
|
for _, cfgArg := range action.Arguments {
|
||||||
pbArg := apiv1.ActionArgument{
|
pbArg := apiv1.ActionArgument{
|
||||||
Name: cfgArg.Name,
|
Name: cfgArg.Name,
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,24 @@ func getNewTestServerAndClient(injectedConfig *config.Config) (*httptest.Server,
|
||||||
return ts, client
|
return ts, client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyActionExecTriggersIncludesWebhookHeaderAndQueryMatches(t *testing.T) {
|
||||||
|
cfg := &config.Action{
|
||||||
|
ExecOnWebhook: []config.WebhookConfig{
|
||||||
|
{
|
||||||
|
MatchHeaders: map[string]string{"X-GitHub-Event": "push"},
|
||||||
|
MatchQuery: map[string]string{"source": "github"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
pb := &apiv1.Action{}
|
||||||
|
|
||||||
|
applyActionExecTriggers(pb, cfg)
|
||||||
|
|
||||||
|
require.Len(t, pb.ExecOnWebhooks, 1)
|
||||||
|
assert.Equal(t, cfg.ExecOnWebhook[0].MatchHeaders, pb.ExecOnWebhooks[0].MatchHeaders)
|
||||||
|
assert.Equal(t, cfg.ExecOnWebhook[0].MatchQuery, pb.ExecOnWebhooks[0].MatchQuery)
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetActionsAndStart(t *testing.T) {
|
func TestGetActionsAndStart(t *testing.T) {
|
||||||
cfg := config.DefaultConfig()
|
cfg := config.DefaultConfig()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ReservedArgumentNamePrefix is reserved for OliveTin-injected system arguments.
|
||||||
|
const ReservedArgumentNamePrefix = "ot_"
|
||||||
|
|
||||||
// Action represents the core functionality of OliveTin - commands that show up
|
// Action represents the core functionality of OliveTin - commands that show up
|
||||||
// as buttons in the UI.
|
// as buttons in the UI.
|
||||||
type Action struct {
|
type Action struct {
|
||||||
|
|
@ -286,7 +289,7 @@ func DefaultConfigWithBasePort(basePort int) *Config {
|
||||||
config.Security.HeaderXContentTypeOptions = true
|
config.Security.HeaderXContentTypeOptions = true
|
||||||
config.Security.HeaderXFrameOptions = true
|
config.Security.HeaderXFrameOptions = true
|
||||||
config.Security.XFrameOptions = "DENY"
|
config.Security.XFrameOptions = "DENY"
|
||||||
config.DefaultIconForActions = "😀"
|
config.DefaultIconForActions = "hugeicons:CommandLineIcon"
|
||||||
config.DefaultIconForDirectories = "📁"
|
config.DefaultIconForDirectories = "📁"
|
||||||
config.DefaultIconForBack = "«"
|
config.DefaultIconForBack = "«"
|
||||||
config.ThemeCacheDisabled = false
|
config.ThemeCacheDisabled = false
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,34 @@ func (cfg *Config) Sanitize() {
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.sanitizeDashboardsForInlineActions()
|
cfg.sanitizeDashboardsForInlineActions()
|
||||||
|
|
||||||
|
if err := cfg.validateReservedActionArgumentNames(); err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) validateReservedActionArgumentNames() error {
|
||||||
|
for _, action := range cfg.Actions {
|
||||||
|
if err := action.validateReservedArgumentNames(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (action *Action) validateReservedArgumentNames() error {
|
||||||
|
if action == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arg := range action.Arguments {
|
||||||
|
if strings.HasPrefix(arg.Name, ReservedArgumentNamePrefix) {
|
||||||
|
return fmt.Errorf("action %q argument %q uses reserved prefix %q", action.Title, arg.Name, ReservedArgumentNamePrefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) sanitizeDashboardsForInlineActions() {
|
func (cfg *Config) sanitizeDashboardsForInlineActions() {
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ func TestSanitizeConfig(t *testing.T) {
|
||||||
|
|
||||||
assert.NotNil(t, a2, "Found action after adding it")
|
assert.NotNil(t, a2, "Found action after adding it")
|
||||||
assert.Equal(t, 3, a2.Timeout, "Default timeout is set")
|
assert.Equal(t, 3, a2.Timeout, "Default timeout is set")
|
||||||
assert.Equal(t, "😀", a2.Icon, "Default icon is a smiley")
|
assert.Equal(t, "hugeicons:CommandLineIcon", a2.Icon, "Default icon is the neutral CLI glyph")
|
||||||
assert.Equal(t, "Carrots", a2.Arguments[0].Title, "Arg title is set to name")
|
assert.Equal(t, "Carrots", a2.Arguments[0].Title, "Arg title is set to name")
|
||||||
assert.Equal(t, "Waffle", a2.Arguments[0].Choices[0].Title, "Choice title is set to name")
|
assert.Equal(t, "Waffle", a2.Arguments[0].Choices[0].Title, "Choice title is set to name")
|
||||||
}
|
}
|
||||||
|
|
@ -92,6 +92,59 @@ func TestSanitizeConfigInlineDashboardActions(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateReservedActionArgumentNames(t *testing.T) {
|
||||||
|
c := DefaultConfig()
|
||||||
|
c.Actions = append(c.Actions, &Action{
|
||||||
|
Title: "Reserved arg",
|
||||||
|
Arguments: []ActionArgument{
|
||||||
|
{Name: "ot_custom", Type: "ascii"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
err := c.validateReservedActionArgumentNames()
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), `action "Reserved arg" argument "ot_custom" uses reserved prefix "ot_"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateReservedActionArgumentNamesAllowsNonReserved(t *testing.T) {
|
||||||
|
c := DefaultConfig()
|
||||||
|
c.Actions = append(c.Actions, &Action{
|
||||||
|
Title: "Allowed arg",
|
||||||
|
Arguments: []ActionArgument{
|
||||||
|
{Name: "target", Type: "ascii"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, c.validateReservedActionArgumentNames())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateReservedActionArgumentNamesChecksInlineActions(t *testing.T) {
|
||||||
|
c := DefaultConfig()
|
||||||
|
c.Dashboards = []*DashboardComponent{
|
||||||
|
{
|
||||||
|
Title: "Dashboard",
|
||||||
|
Contents: []*DashboardComponent{
|
||||||
|
{
|
||||||
|
Title: "Inline reserved arg",
|
||||||
|
InlineAction: &Action{
|
||||||
|
Shell: "echo test",
|
||||||
|
Arguments: []ActionArgument{
|
||||||
|
{Name: "ot_custom", Type: "ascii"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
c.sanitizeDashboardsForInlineActions()
|
||||||
|
err := c.validateReservedActionArgumentNames()
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), `action "Inline reserved arg" argument "ot_custom" uses reserved prefix "ot_"`)
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateUniqueLocalUserAPIKeys(t *testing.T) {
|
func TestValidateUniqueLocalUserAPIKeys(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ var (
|
||||||
"unicode_identifier": `^[\w\-\.\_\d]+$`,
|
"unicode_identifier": `^[\w\-\.\_\d]+$`,
|
||||||
"ascii": `^[a-zA-Z0-9]+$`,
|
"ascii": `^[a-zA-Z0-9]+$`,
|
||||||
"ascii_identifier": `^[a-zA-Z0-9\-\._]+$`,
|
"ascii_identifier": `^[a-zA-Z0-9\-\._]+$`,
|
||||||
|
"shell_safe_identifier": `^[a-zA-Z0-9@\.\_\+\-]+$`,
|
||||||
"ascii_sentence": `^[a-zA-Z0-9\-\._, ]+$`,
|
"ascii_sentence": `^[a-zA-Z0-9\-\._, ]+$`,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -576,6 +576,38 @@ func TestTypeSafetyCheckAsciiIdentifier(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTypeSafetyCheckShellSafeIdentifier(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
hasError bool
|
||||||
|
}{
|
||||||
|
{"Simple username", "alice123", false},
|
||||||
|
{"Email username", "alice@example.com", false},
|
||||||
|
{"Plus addressing", "alice+test@example.com", false},
|
||||||
|
{"Hyphen underscore dot", "alice-test_user.example", false},
|
||||||
|
{"Invalid space", "alice example", true},
|
||||||
|
{"Invalid shell substitution", "$(whoami)", true},
|
||||||
|
{"Invalid backtick", "`whoami`", true},
|
||||||
|
{"Invalid semicolon", "alice;id", true},
|
||||||
|
{"Invalid ampersand", "alice&id", true},
|
||||||
|
{"Invalid pipe", "alice|id", true},
|
||||||
|
{"Invalid quote", "alice'example", true},
|
||||||
|
{"Invalid slash", "alice/example", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := TypeSafetyCheck("username", tt.value, "shell_safe_identifier")
|
||||||
|
if tt.hasError {
|
||||||
|
assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
|
||||||
|
} else {
|
||||||
|
assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTypeSafetyCheckAsciiSentence(t *testing.T) {
|
func TestTypeSafetyCheckAsciiSentence(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
||||||
|
|
@ -666,13 +666,15 @@ func stepACLCheck(req *ExecutionRequest) bool {
|
||||||
|
|
||||||
func stepParseArgs(req *ExecutionRequest) bool {
|
func stepParseArgs(req *ExecutionRequest) bool {
|
||||||
ensureArgumentMap(req)
|
ensureArgumentMap(req)
|
||||||
injectSystemArgs(req)
|
|
||||||
|
|
||||||
if !hasBindingAndAction(req) {
|
if !hasBindingAndAction(req) {
|
||||||
return fail(req, fmt.Errorf("cannot parse arguments: Binding or Action is nil"))
|
return fail(req, fmt.Errorf("cannot parse arguments: Binding or Action is nil"))
|
||||||
}
|
}
|
||||||
|
|
||||||
filterToDefinedArgumentsOnly(req)
|
filterToDefinedArgumentsOnly(req)
|
||||||
|
if err := injectSystemArgs(req); err != nil {
|
||||||
|
return fail(req, err)
|
||||||
|
}
|
||||||
mangleInvalidArgumentValues(req)
|
mangleInvalidArgumentValues(req)
|
||||||
|
|
||||||
if hasExec(req) {
|
if hasExec(req) {
|
||||||
|
|
@ -735,7 +737,7 @@ func filterToDefinedArgumentsOnly(req *ExecutionRequest) {
|
||||||
|
|
||||||
func keepArgument(name string, definedNames map[string]struct{}) bool {
|
func keepArgument(name string, definedNames map[string]struct{}) bool {
|
||||||
_, ok := definedNames[name]
|
_, ok := definedNames[name]
|
||||||
return ok || strings.HasPrefix(name, "ot_")
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasWebhookTag(req *ExecutionRequest) bool {
|
func hasWebhookTag(req *ExecutionRequest) bool {
|
||||||
|
|
@ -747,9 +749,38 @@ func hasWebhookTag(req *ExecutionRequest) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func injectSystemArgs(req *ExecutionRequest) {
|
var systemArgumentDefinitions = []config.ActionArgument{
|
||||||
req.Arguments["ot_executionTrackingId"] = req.TrackingID
|
{Name: "ot_executionTrackingId", Type: "ascii_identifier", RejectNull: true},
|
||||||
req.Arguments["ot_username"] = req.AuthenticatedUser.Username
|
{Name: "ot_username", Type: "shell_safe_identifier", RejectNull: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
func injectSystemArgs(req *ExecutionRequest) error {
|
||||||
|
args, err := validatedSystemArgs(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, value := range args {
|
||||||
|
req.Arguments[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatedSystemArgs(req *ExecutionRequest) (map[string]string, error) {
|
||||||
|
values := map[string]string{
|
||||||
|
"ot_executionTrackingId": req.TrackingID,
|
||||||
|
"ot_username": req.AuthenticatedUser.Username,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range systemArgumentDefinitions {
|
||||||
|
arg := &systemArgumentDefinitions[i]
|
||||||
|
if err := ValidateArgument(arg, values[arg.Name], req.Binding.Action); err != nil {
|
||||||
|
return nil, fmt.Errorf("system argument %q failed validation: %w", arg.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasBindingAndAction(req *ExecutionRequest) bool {
|
func hasBindingAndAction(req *ExecutionRequest) bool {
|
||||||
|
|
@ -939,36 +970,20 @@ func prepareCommand(cmd *exec.Cmd, streamer *OutputStreamer, req *ExecutionReque
|
||||||
}
|
}
|
||||||
|
|
||||||
func stepExecAfter(req *ExecutionRequest) bool {
|
func stepExecAfter(req *ExecutionRequest) bool {
|
||||||
if req.Binding.Action.ShellAfterCompleted == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := newTimeoutContext(context.Background(), time.Duration(req.Binding.Action.Timeout)*time.Second, req.executor)
|
ctx, cancel := newTimeoutContext(context.Background(), time.Duration(req.Binding.Action.Timeout)*time.Second, req.executor)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
args := map[string]string{
|
cmd, args, err := buildShellAfterCommand(ctx, req, &stdout, &stderr)
|
||||||
"output": req.logEntry.Output,
|
|
||||||
"exitCode": fmt.Sprintf("%v", req.logEntry.ExitCode),
|
|
||||||
"ot_executionTrackingId": req.TrackingID,
|
|
||||||
"ot_username": req.AuthenticatedUser.Username,
|
|
||||||
}
|
|
||||||
|
|
||||||
finalParsedCommand, err := tpl.ParseTemplateWithActionContext(req.Binding.Action.ShellAfterCompleted, req.Binding.Entity, args)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := "Could not prepare shellAfterCompleted command: " + err.Error() + "\n"
|
return fail(req, err)
|
||||||
req.logEntry.Output += msg
|
}
|
||||||
log.Warn(msg)
|
if cmd == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := wrapCommandInShell(ctx, finalParsedCommand)
|
|
||||||
cmd.Stdout = &stdout
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
|
|
||||||
cmd.Env = buildEnv(args)
|
cmd.Env = buildEnv(args)
|
||||||
|
|
||||||
runerr := cmd.Start()
|
runerr := cmd.Start()
|
||||||
|
|
@ -998,6 +1013,43 @@ func stepExecAfter(req *ExecutionRequest) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildShellAfterCommand(ctx context.Context, req *ExecutionRequest, stdout, stderr *bytes.Buffer) (*exec.Cmd, map[string]string, error) {
|
||||||
|
if req.Binding.Action.ShellAfterCompleted == "" {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
args, err := buildShellAfterArgs(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
finalParsedCommand, err := tpl.ParseTemplateWithActionContext(req.Binding.Action.ShellAfterCompleted, req.Binding.Entity, args)
|
||||||
|
if err != nil {
|
||||||
|
msg := "Could not prepare shellAfterCompleted command: " + err.Error() + "\n"
|
||||||
|
req.logEntry.Output += msg
|
||||||
|
log.Warn(msg)
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := wrapCommandInShell(ctx, finalParsedCommand)
|
||||||
|
cmd.Stdout = stdout
|
||||||
|
cmd.Stderr = stderr
|
||||||
|
|
||||||
|
return cmd, args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildShellAfterArgs(req *ExecutionRequest) (map[string]string, error) {
|
||||||
|
args, err := validatedSystemArgs(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
args["output"] = req.logEntry.Output
|
||||||
|
args["exitCode"] = fmt.Sprintf("%v", req.logEntry.ExitCode)
|
||||||
|
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
//gocyclo:ignore
|
//gocyclo:ignore
|
||||||
func stepTrigger(req *ExecutionRequest) bool {
|
func stepTrigger(req *ExecutionRequest) bool {
|
||||||
if req.Binding.Action.Triggers == nil {
|
if req.Binding.Action.Triggers == nil {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -37,7 +38,7 @@ func TestCreateExecutorAndExec(t *testing.T) {
|
||||||
e, cfg := testingExecutor()
|
e, cfg := testingExecutor()
|
||||||
|
|
||||||
req := ExecutionRequest{
|
req := ExecutionRequest{
|
||||||
AuthenticatedUser: &authpublic.AuthenticatedUser{Username: "Mr Tickle"},
|
AuthenticatedUser: &authpublic.AuthenticatedUser{Username: "MrTickle"},
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
Arguments: map[string]string{
|
Arguments: map[string]string{
|
||||||
"person": "yourself",
|
"person": "yourself",
|
||||||
|
|
@ -379,7 +380,7 @@ func TestFilterToDefinedArgumentsOnly(t *testing.T) {
|
||||||
assert.Empty(t, req.Arguments["extra_undefined"])
|
assert.Empty(t, req.Arguments["extra_undefined"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilterToDefinedArgumentsPreservesSystemArgs(t *testing.T) {
|
func TestFilterToDefinedArgumentsDropsReservedPrefixArgs(t *testing.T) {
|
||||||
req := newExecRequest()
|
req := newExecRequest()
|
||||||
req.Binding.Action = &config.Action{
|
req.Binding.Action = &config.Action{
|
||||||
Title: "Filter test",
|
Title: "Filter test",
|
||||||
|
|
@ -393,8 +394,209 @@ func TestFilterToDefinedArgumentsPreservesSystemArgs(t *testing.T) {
|
||||||
|
|
||||||
filterToDefinedArgumentsOnly(req)
|
filterToDefinedArgumentsOnly(req)
|
||||||
|
|
||||||
assert.Equal(t, "track-123", req.Arguments["ot_executionTrackingId"])
|
assert.Empty(t, req.Arguments["ot_executionTrackingId"])
|
||||||
assert.Equal(t, "webhook", req.Arguments["ot_username"])
|
assert.Empty(t, req.Arguments["ot_username"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepParseArgsInjectsSystemArgsAfterFiltering(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.TrackingID = "server-track-456"
|
||||||
|
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice"}
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
|
Title: "Filter then inject",
|
||||||
|
Shell: "echo test",
|
||||||
|
Arguments: []config.ActionArgument{
|
||||||
|
{Name: "name", Type: "ascii"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req.Arguments = map[string]string{
|
||||||
|
"name": "Alice",
|
||||||
|
"ot_executionTrackingId": "attacker-track",
|
||||||
|
"ot_username": "mallory",
|
||||||
|
"ot_custom": "polluted",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, stepParseArgs(req))
|
||||||
|
assert.Equal(t, "Alice", req.Arguments["name"])
|
||||||
|
assert.Equal(t, "server-track-456", req.Arguments["ot_executionTrackingId"])
|
||||||
|
assert.Equal(t, "alice", req.Arguments["ot_username"])
|
||||||
|
assert.Empty(t, req.Arguments["ot_custom"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepParseArgsDropsReservedPrefixArgsFromEnvironment(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.TrackingID = "server-track-456"
|
||||||
|
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"}
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
|
Title: "No reserved prefix pollution",
|
||||||
|
Shell: "echo test",
|
||||||
|
Arguments: []config.ActionArgument{},
|
||||||
|
}
|
||||||
|
req.Arguments = map[string]string{
|
||||||
|
"ot_custom": "polluted",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, stepParseArgs(req))
|
||||||
|
env := buildEnv(req.Arguments)
|
||||||
|
|
||||||
|
assert.False(t, containsEnvPrefix(env, "OT_CUSTOM="))
|
||||||
|
assert.True(t, containsEnvPrefix(env, "OT_USERNAME=alice@example.com"))
|
||||||
|
assert.True(t, containsEnvPrefix(env, "OT_EXECUTIONTRACKINGID=server-track-456"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSystemArgumentDefinitionsAreReservedAndShellSafe(t *testing.T) {
|
||||||
|
unsafeTypes := map[string]struct{}{
|
||||||
|
"email": {},
|
||||||
|
"password": {},
|
||||||
|
"raw_string_multiline": {},
|
||||||
|
"url": {},
|
||||||
|
"very_dangerous_raw_string": {},
|
||||||
|
}
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
|
||||||
|
for _, arg := range systemArgumentDefinitions {
|
||||||
|
assert.True(t, strings.HasPrefix(arg.Name, config.ReservedArgumentNamePrefix))
|
||||||
|
assert.NotEmpty(t, arg.Type)
|
||||||
|
assert.True(t, arg.RejectNull)
|
||||||
|
|
||||||
|
_, duplicate := seen[arg.Name]
|
||||||
|
assert.False(t, duplicate, "duplicate system argument definition %q", arg.Name)
|
||||||
|
seen[arg.Name] = struct{}{}
|
||||||
|
|
||||||
|
_, unsafe := unsafeTypes[arg.Type]
|
||||||
|
assert.False(t, unsafe, "system argument %q uses unsafe type %q", arg.Name, arg.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatedSystemArgsMatchesSystemArgumentDefinitions(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.TrackingID = "server-track-456"
|
||||||
|
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"}
|
||||||
|
|
||||||
|
args, err := validatedSystemArgs(req)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Len(t, args, len(systemArgumentDefinitions))
|
||||||
|
for _, arg := range systemArgumentDefinitions {
|
||||||
|
assert.Contains(t, args, arg.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildShellAfterArgsOnlyAddsExpectedNonSystemArgs(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.logEntry = &InternalLogEntry{
|
||||||
|
Output: "hello",
|
||||||
|
ExitCode: 7,
|
||||||
|
}
|
||||||
|
req.TrackingID = "server-track-456"
|
||||||
|
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"}
|
||||||
|
req.Binding.Action = &config.Action{ShellAfterCompleted: "echo test"}
|
||||||
|
|
||||||
|
args, err := buildShellAfterArgs(req)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Len(t, args, len(systemArgumentDefinitions)+2)
|
||||||
|
assert.Contains(t, args, "output")
|
||||||
|
assert.Contains(t, args, "exitCode")
|
||||||
|
for _, arg := range systemArgumentDefinitions {
|
||||||
|
assert.Contains(t, args, arg.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepParseArgsAllowsEmailUsernameSystemArg(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.logEntry = &InternalLogEntry{}
|
||||||
|
req.TrackingID = "server-track-456"
|
||||||
|
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"}
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
|
Title: "Email username",
|
||||||
|
Shell: "echo test",
|
||||||
|
Arguments: []config.ActionArgument{},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, stepParseArgs(req))
|
||||||
|
assert.Equal(t, "alice@example.com", req.Arguments["ot_username"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepParseArgsFailsWhenUsernameSystemArgIsInvalid(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.logEntry = &InternalLogEntry{}
|
||||||
|
req.TrackingID = "server-track-456"
|
||||||
|
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice;id"}
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
|
Title: "Invalid system arg",
|
||||||
|
Shell: "echo test",
|
||||||
|
Arguments: []config.ActionArgument{},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.False(t, stepParseArgs(req))
|
||||||
|
assert.Contains(t, req.logEntry.Output, `system argument "ot_username" failed validation`)
|
||||||
|
assert.Empty(t, req.Arguments["ot_username"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepParseArgsFailsWhenTrackingIDSystemArgIsInvalid(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.logEntry = &InternalLogEntry{}
|
||||||
|
req.TrackingID = "track/../../bad"
|
||||||
|
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice"}
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
|
Title: "Invalid tracking ID",
|
||||||
|
Shell: "echo test",
|
||||||
|
Arguments: []config.ActionArgument{},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.False(t, stepParseArgs(req))
|
||||||
|
assert.Contains(t, req.logEntry.Output, `system argument "ot_executionTrackingId" failed validation`)
|
||||||
|
assert.Empty(t, req.Arguments["ot_executionTrackingId"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildShellAfterArgsUsesValidatedSystemArgs(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.logEntry = &InternalLogEntry{
|
||||||
|
Output: "hello",
|
||||||
|
ExitCode: 7,
|
||||||
|
}
|
||||||
|
req.TrackingID = "server-track-456"
|
||||||
|
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"}
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
|
Title: "Shell after",
|
||||||
|
ShellAfterCompleted: "echo test",
|
||||||
|
}
|
||||||
|
|
||||||
|
args, err := buildShellAfterArgs(req)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "alice@example.com", args["ot_username"])
|
||||||
|
assert.Equal(t, "server-track-456", args["ot_executionTrackingId"])
|
||||||
|
assert.Equal(t, "hello", args["output"])
|
||||||
|
assert.Equal(t, "7", args["exitCode"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildShellAfterArgsFailsWhenSystemArgIsInvalid(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.logEntry = &InternalLogEntry{}
|
||||||
|
req.TrackingID = "server-track-456"
|
||||||
|
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice;id"}
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
|
Title: "Shell after invalid username",
|
||||||
|
ShellAfterCompleted: "echo test",
|
||||||
|
}
|
||||||
|
|
||||||
|
args, err := buildShellAfterArgs(req)
|
||||||
|
|
||||||
|
assert.Nil(t, args)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Contains(t, err.Error(), `system argument "ot_username" failed validation`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsEnvPrefix(env []string, prefix string) bool {
|
||||||
|
for _, item := range env {
|
||||||
|
if strings.HasPrefix(item, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTriggerExecutesTriggeredAction(t *testing.T) {
|
func TestTriggerExecutesTriggeredAction(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,13 @@ import (
|
||||||
|
|
||||||
func WatchFilesInDirectory(cfg *config.Config, ex *executor.Executor) {
|
func WatchFilesInDirectory(cfg *config.Config, ex *executor.Executor) {
|
||||||
for _, action := range cfg.Actions {
|
for _, action := range cfg.Actions {
|
||||||
|
for _, dirname := range action.ExecOnFileCreatedInDir {
|
||||||
|
go func(act *config.Action, dir string) {
|
||||||
|
filehelper.WatchDirectoryCreate(dir, func(filename string) {
|
||||||
|
scheduleExec(act, cfg, ex, filename)
|
||||||
|
})
|
||||||
|
}(action, dirname)
|
||||||
|
}
|
||||||
for _, dirname := range action.ExecOnFileChangedInDir {
|
for _, dirname := range action.ExecOnFileChangedInDir {
|
||||||
// Pass values into anonymous function because of this issue
|
// Pass values into anonymous function because of this issue
|
||||||
// https://github.com/OliveTin/OliveTin/issues/503
|
// https://github.com/OliveTin/OliveTin/issues/503
|
||||||
|
|
@ -22,12 +29,6 @@ func WatchFilesInDirectory(cfg *config.Config, ex *executor.Executor) {
|
||||||
scheduleExec(act, cfg, ex, filename)
|
scheduleExec(act, cfg, ex, filename)
|
||||||
})
|
})
|
||||||
}(action, dirname)
|
}(action, dirname)
|
||||||
|
|
||||||
go func(act *config.Action, dir string) {
|
|
||||||
filehelper.WatchDirectoryCreate(dir, func(filename string) {
|
|
||||||
scheduleExec(act, cfg, ex, filename)
|
|
||||||
})
|
|
||||||
}(action, dirname)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue