From a practical point of view, one of the main purpose of config panel is to "expose" settings from the app's configuration files to YunoHost's admins which are therefore able to manipulate them from a nice web ui. This is especially relevant for apps which do not provide any sort of admin UI and expect admins to manually edit the configuration files.
Technically speaking, config panels are used both for apps packaging and also core features (domain configuration, global settings)
Please: Keep in mind the YunoHost spirit, and try to build your panels in such a way as to expose only really useful, "high-level" parameters, and if there are many of them, to relegate those corresponding to rarer use cases to "Advanced" sub-sections. Keep it simple, focus on common needs, don't expect the admins to have 3 PhDs in computer science.
scripts/config
examplescripts/config
of other appsFrom an app packager perspective, config panels are defined in config_panel.toml
at the root of the app repository. It is coupled to the scripts/config
script, which may be used to define custom getters/setters/validations/actions. However, most use cases should be covered automagically by the core, thus it may not be necessary to define such a config
script at all!
The config_panel.toml
describes one or several panels, containing sections, each containing questions generally binded to a params in the app's actual configuration files.
Let's imagine that the upstream app is configured using this simple config.yml
file stored in the app's install directory (typically /var/www/$app/config.yml
):
title: 'My dummy app'
theme: 'white'
max_rate: 10
max_age: 365
A simple configuration panel can be created with this syntax:
version = "1.0"
[main]
[main.main]
[main.main.title]
ask.en = "Title"
type = "string"
bind = ":__INSTALL_DIR__/config.yml"
[main.main.theme]
ask.en = "Theme"
type = "select"
choices = ["white", "dark"]
bind = ":__INSTALL_DIR__/config.yml"
[main.limits]
[main.limits.max_rate]
ask.en = "Maximum display rate"
type = "number"
bind = ":__INSTALL_DIR__/config.yml"
[main.limits.max_age]
ask.en = "Duration of a dummy"
type = "number"
bind = ":__INSTALL_DIR__/config.yml"
Here, a main
panel is created, containing the main
and limits
sections, containing questions corresponding to app options in config.yml
file. Thanks to the bind
property, all those questions are read/written to their values in the actual config.yml
file.
You can learn more about the full list of option types and their properties in their dedicated page.
bind
" statementWithout any bind
statement attached to a config panel property, values are only read/written from/to the app's settings file (/etc/yunohost/$app/settings.yml
). This is usually not very useful in practice.
Using bind = ":/some/config/file"
, one declares that the actual truth used by the app lives in /some/config/file
. The config panel will read/write the value from/to this file. YunoHost will automagically adapt to classic formats such as YAML, TOML, JSON, INI, PHP, .env
-like and .py
. (At least, assuming we're dealing with simple key<->value mappings)
A simple real-life example looks like:
[main.main.theme]
type = "string"
bind = ":__INSTALL_DIR__/config.yml"
In which case, YunoHost will look for something like a key/value, with the key being theme
inside the app's config.yml
.
If the question id in the config panel (here, theme
) differs from the key in the actual conf file (let's say it's not theme
but css_theme
), then the syntax becomes:
[main.main.theme]
type = "string"
bind = "css_theme:__FINALPATH__/config.yml"
You may also encounter situations such as:
{
"foo": {
"max": 123
},
"bar": {
"max": 456
}
}
In which case if we want to interface with foo's max
value, let's write:
bind = "foo>max:__INSTALL_DIR__/conf.json"
Useful when using a question file
or text
for which you want to save the raw content directly as a file on the system.
For example to be able to manipulate an image:
[panel.section.logo]
type = "file"
bind = "__INSTALL_DIR__/assets/logo.png"
Or an entire text file:
[panel.section.config_content]
type = "text"
bind = "__INSTALL_DIR__/config.ini"
default = "This is the default content"
bind=null
)More complex use-case may appear, such as:
You can create specific getter/setters functions inside the config
script of the app to customize how the information is read/written. The basic structure of the script is:
#!/bin/bash
source /usr/share/yunohost/helpers
ynh_abort_if_errors
# Put your getter, setter, validator or action here
# Keep this last line
ynh_app_config_run $1
A question's getter is the function used to read the current value/state. Custom getters are defined using bash functions called getter__QUESTION_SHORT_KEY()
which returns data through stdout.
Stdout can generated using one of those formats:
1) either just the raw value,
2) or a yaml, containing the value and other metadata and properties (for example the style
of an alert
, the list of available choices
of a select
, etc.)
config_panel.toml
[main.main.timezone]
ask = "Timezone"
type = "string"
scripts/config
get__timezone() {
echo "$(cat /etc/timezone)"
}
config_panel.toml
[main.plugins.plugins]
ask = "Plugin to activate"
type = "tags"
choices = []
scripts/config
get__plugins() {
echo "choices: [$(ls $install_dir/plugins/ | tr '\n' ',')]"
}
config_panel.toml
[main.cube.status]
ask = "Custom getter alert"
type = "alert"
style = "info"
bind = "null" # no behaviour on
scripts/config
get__status() {
if [ -f "/sys/class/net/tun0/operstate" ] && [ "$(cat /sys/class/net/tun0/operstate)" == "up" ]
then
cat << EOF
style: success
ask:
en: Your VPN is running :)
EOF
else
cat << EOF
style: danger
ask:
en: Your VPN is down
EOF
fi
}
A question's setter is the function used to set new value/state. Custom setters are defined using bash functions called setter__QUESTION_SHORT_KEY()
. In the context of the setter function, variables named with the various quetion's short keys are avaible ... for example the user-specified date for question [main.main.theme]
is available as $theme
.
When doing non-trivial operations to set a value, you may want to use ynh_print_info
to inform the admin about what's going on.
config_panel.toml
[main.main.timezone]
ask = "Timezone"
type = "string"
scripts/config
set__timezone() {
echo "$timezone" > /etc/timezone
ynh_print_info "The timezone has been changed to $timezone"
}
You will sometimes need to validate data provided by the user before saving it.
Simple validation can be achieved using a regex pattern:
pattern.regexp = '^.+@.+$'
pattern.error = 'An email is required for this field'
You can also restrict the accepted values using a choices list.
choices.foo = "Foo (some explanation)"
choices.bar = "Bar (moar explanation)"
choices.loremipsum = "Lorem Ipsum Dolor Sit Amet"
Some other type specific argument exist like | type | validation arguments |
---|---|---|
number , range |
min , max , step |
|
file |
accept |
|
boolean |
yes no |
See also : custom validators
In addition to the "simple" validation mechanism (see the 'option' doc), custom validators can be defined in a similar fashion as custom getters/setters:
validate__login_user() {
if [[ "${#login_user}" -lt 4 ]]
then
echo 'User login is too short, should be at least 4 chars'
fi
}
visible
& enabled
expression evaluationSometimes we may want to conditionaly display a message or prompt for a value, for this we have the visible
prop.
And we may want to allow a user to trigger an action only if some condition are met, for this we have the enabled
prop.
Expressions are evaluated against a context containing previous values of the current section's options. This quite limited current design exists because on the web-admin or on the CLI we cannot guarantee that a value will be present in the form if the user queried only a single panel/section/option. In the case of an action, the user will be shown or asked for each of the options of the section in which the button is present.
The expression has to be written in javascript (this has been designed for the web-admin first and is converted to python on the fly on the cli).
Available operators are: ==
, !=
, >
, >=
, <
, <=
, !
, &&
, ||
, +
, -
, *
, /
, %
and match()
.
# simple "my_option_id" is thruthy/falsy
visible = "my_option_id"
visible = "!my_option_id"
# misc
visible = "my_value >= 10"
visible = "-(my_value + 1) < 0"
visible = "!!my_value || my_other_value"
For a more complete set of examples, check the tests at the end of the file.
For more complex evaluation we can use regex matching.
[my_string]
default = "Lorem ipsum dolor et si qua met!"
[my_boolean]
type = "boolean"
visible = "my_string && match(my_string, '^Lorem [ia]psumE?')"
Match the content of a file.
[my_file]
type = "file"
accept = ".txt"
bind = "/etc/random/lorem.txt"
[my_boolean]
type = "boolean"
visible = "my_file && match(my_file, '^Lorem [ia]psumE?')"
with a file with content like:
Lorem ipsum dolor et si qua met!
"Actions" correspond to config panel buttons triggering specific pieces of code. For example, one could implement an action to trigger a scan of Nextcloud files, or install a plugin inside an app that does not already provide an interface to do so. In core config panels, buttons are for example used to trigger certificate renewal.
The most basic example looks like this, using type = 'button'
:
[panel.section.my_action]
type = "button"
ask = "Run action"
# (NB: no need to set `bind` to "null")
And then defining the controller, prefixed by run__
inside the app's config
script:
run__my_action() {
ynh_print_info "Running 'my_action'..."
}
You may build more complex actions, where the actions uses other form inputs:
[panel.my_action_section]
name = "Action section"
[panel.my_action_section.my_repo]
type = "url"
bind = "null" # this value won't be saved as a setting, it's ephemeral and only relevant for the action
ask = "gimme a repo link"
[panel.my_action_section.my_repo_name]
type = "string"
bind = "null" # this value won't be saved as a setting, it's ephemeral and only relevant for the action
ask = "gimme a custom folder name"
[panel.my_action_section.my_action]
type = "button"
ask = "Clone the repo"
# the button is clickable only once the previous values are provided
enabled = "my_repo && my_repo_name"
run__my_action() {
ynh_print_info "Cloning '$my_repo'..."
cd /tmp
git clone "$my_repo" "$my_repo_name"
}
All main configuration helpers are overwritable, example:
ynh_app_config_apply() {
# Stop vpn client
touch /tmp/.ynh-vpnclient-stopped
systemctl stop ynh-vpnclient
_ynh_app_config_apply
# Start vpn client
systemctl start ynh-vpnclient
rm -f /tmp/.ynh-vpnclient-stopped
}
List of main configuration helpers:
ynh_app_config_get
ynh_app_config_show
ynh_app_config_validate
ynh_app_config_apply
ynh_app_config_run
More info on this can be found by reading vpnclient_ynh config script
For performance reasons, questions short keys have to be unique in all the config_panel.toml
file, not just inside its panel or its section. Hence it's not possible to have:
[manual.vpn.server_ip]
[advanced.dns.server_ip]
In which two questions have "real variable name" is server_ip
and therefore conflict with each other.
Some short keys are forbidden cause it can interfer with config scripts (old
, file_hash
, types
, binds
, formats
, changed
) and you probably should avoid to use common settings name to avoid to bind your question to this settings (e.g. id
, install_time
, mysql_pwd
, path
, domain
, port
, db_name
, current_revision
, admin
)
bind
versus app settingsIMPORTANT: with the exception of bind = "null"
options, options ids should almost always correspond to an app setting initialized/reused during install/upgrade.
Not doing so may result in inconsistencies between the config panel mechanism and the use of ynh_add_config. See also discussions in https://github.com/YunoHost/issues/issues/1973
Found errors? Think you can improve this documentation? Simply click the Edit link at the top of the page, and then the icon on Github to suggest changes.
Powered by Grav + with by Trilby Media. • Terms of Service