doc: iterate on configpanel/form documentation

This commit is contained in:
Alexandre Aubin 2023-10-30 18:39:31 +01:00
parent 02619e8284
commit f02538cef0
5 changed files with 211 additions and 745 deletions

View file

@ -0,0 +1,171 @@
import sys
import ast
import datetime
import subprocess
version = open("../debian/changelog").readlines()[0].split()[1].strip("()")
today = datetime.datetime.now().strftime("%d/%m/%Y")
def get_current_commit():
p = subprocess.Popen(
"git rev-parse --verify HEAD",
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
stdout, stderr = p.communicate()
current_commit = stdout.strip().decode("utf-8")
return current_commit
current_commit = get_current_commit()
def print_config_panel_docs():
fname = "../src/utils/configpanel.py"
content = open(fname).read()
# NB: This magic is because we want to be able to run this script outside of a YunoHost context,
# in which we cant really 'import' the file because it will trigger a bunch of moulinette/yunohost imports...
tree = ast.parse(content)
ConfigPanelClasses = reversed(
[
c
for c in tree.body
if isinstance(c, ast.ClassDef)
and c.name in {"SectionModel", "PanelModel", "ConfigPanelModel"}
]
)
print("## Configuration panel structure")
for c in ConfigPanelClasses:
doc = ast.get_docstring(c)
print("")
print(f"### {c.name.replace('Model', '')}")
print("")
print(doc)
print("")
print("---")
def print_form_doc():
fname = "../src/utils/form.py"
content = open(fname).read()
# NB: This magic is because we want to be able to run this script outside of a YunoHost context,
# in which we cant really 'import' the file because it will trigger a bunch of moulinette/yunohost imports...
tree = ast.parse(content)
OptionClasses = [
c for c in tree.body if isinstance(c, ast.ClassDef) and c.name.endswith("Option")
]
OptionDocString = {}
print("## List of all option types")
for c in OptionClasses:
if not isinstance(c.body[0], ast.Expr):
continue
option_type = None
if c.name in {"BaseOption", "BaseInputOption"}:
option_type = c.name
elif c.body[1].target.id == "type":
option_type = c.body[1].value.attr
generaltype = c.bases[0].id.replace("Option", "").replace("Base", "").lower() if c.bases else None
docstring = ast.get_docstring(c)
if docstring:
if "#### Properties" not in docstring:
docstring += """
#### Properties
- [common properties](#common-properties)"""
OptionDocString[option_type] = {"doc": docstring, "generaltype": generaltype}
# Dirty hack to have "BaseOption" as first and "BaseInputOption" as 2nd in list
base = OptionDocString.pop("BaseOption")
baseinput = OptionDocString.pop("BaseInputOption")
OptionDocString2 = {
"BaseOption": base,
"BaseInputOption": baseinput,
}
OptionDocString2.update(OptionDocString)
for option_type, infos in OptionDocString2.items():
if option_type == "display_text":
# display_text is kind of legacy x_x
continue
print("")
if option_type == "BaseOption":
print("### Common properties")
elif option_type == "BaseInputOption":
print("### Common inputs properties")
else:
print(f"### `{option_type}`" + (f" ({infos['generaltype']})" if infos["generaltype"] else ""))
print("")
print(infos["doc"])
print("")
print("---")
print(
f"""---
title: Technical details for config panel structure and form option types
template: docs
taxonomy:
category: docs
routes:
default: '/dev/forms'
---
Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{current_commit}/doc/generate_options_doc.py) on {today} (YunoHost version {version})
## Glossary
You may encounter some named types which are used for simplicity.
- `Translation`: a translated property
- used for properties: `ask`, `help` and `Pattern.error`
- a `dict` with locales as keys and translations as values:
```toml
ask.en = "The text in english"
ask.fr = "Le texte en français"
```
It is not currently possible for translators to translate those string in weblate.
- a single `str` for a single english default string
```toml
help = "The text in english"
```
- `JSExpression`: a `str` JS expression to be evaluated to `true` or `false`:
- used for properties: `visible` and `enabled`
- operators availables: `==`, `!=`, `>`, `>=`, `<`, `<=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%` and `match()`
- `Binding`: bind a value to a file/property/variable/getter/setter/validator
- save the value in `settings.yaml` when not defined
- nothing at all with `"null"`
- a custom getter/setter/validator with `"null"` + a function starting with `get__`, `set__`, `validate__` in `scripts/config`
- a variable/property in a file with `:__FINALPATH__/my_file.php`
- a whole file with `__FINALPATH__/my_file.php`
- `Pattern`: a `dict` with a regex to match the value against and an error message
```toml
pattern.regexp = '^[A-F]\d\d$'
pattern.error = "Provide a room number such as F12: one uppercase and 2 numbers"
# or with translated error
pattern.error.en = "Provide a room number such as F12: one uppercase and 2 numbers"
pattern.error.fr = "Entrez un numéro de salle comme F12: une lettre majuscule et deux chiffres."
```
- IMPORTANT: your `pattern.regexp` should be between simple quote, not double.
"""
)
print_config_panel_docs()
print_form_doc()

View file

@ -1,159 +0,0 @@
import ast
import datetime
import subprocess
version = open("../debian/changelog").readlines()[0].split()[1].strip("()")
today = datetime.datetime.now().strftime("%d/%m/%Y")
def get_current_commit():
p = subprocess.Popen(
"git rev-parse --verify HEAD",
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
stdout, stderr = p.communicate()
current_commit = stdout.strip().decode("utf-8")
return current_commit
current_commit = get_current_commit()
print(
f"""---
title: Config Panels
template: docs
taxonomy:
category: docs
routes:
default: '/packaging_config_panels'
---
Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{current_commit}/doc/generate_configpanel_doc.py) on {today} (YunoHost version {version})
## Glossary
You may encounter some named types which are used for simplicity.
- `Translation`: a translated property
- used for properties: `ask`, `help` and `Pattern.error`
- a `dict` with locales as keys and translations as values:
```toml
ask.en = "The text in english"
ask.fr = "Le texte en français"
```
It is not currently possible for translators to translate those string in weblate.
- a single `str` for a single english default string
```toml
help = "The text in english"
```
- `JSExpression`: a `str` JS expression to be evaluated to `true` or `false`:
- used for properties: `visible` and `enabled`
- operators availables: `==`, `!=`, `>`, `>=`, `<`, `<=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%` and `match()`
- [examples available in the advanced section of Options](/packaging_apps_options#advanced-use-cases)
"""
)
fname = "../src/utils/configpanel.py"
content = open(fname).read()
# NB: This magic is because we want to be able to run this script outside of a YunoHost context,
# in which we cant really 'import' the file because it will trigger a bunch of moulinette/yunohost imports...
tree = ast.parse(content)
OptionClasses = reversed(
[
c
for c in tree.body
if isinstance(c, ast.ClassDef)
and c.name in {"SectionModel", "PanelModel", "ConfigPanelModel"}
]
)
for c in OptionClasses:
doc = ast.get_docstring(c)
print("")
print("----------------")
print(f"## {c.name.replace('Model', '')}")
print("")
print(doc)
print("")
print(
"""
## Full example
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`):
```yaml
title: 'My dummy app'
theme: 'white'
max_rate: 10
max_age: 365
```
We could for example create a simple configuration panel for it like this one, by following the syntax `[PANEL.SECTION.QUESTION]`:
```toml
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 we have created one `main` panel, containing the `main` and `limits` sections, containing questions according to params name of our `config.yml` file. Thanks to the `bind` properties, all those questions are bind to their values in the `config.yml` file.
## Overwrite config panel mechanism
All main configuration helpers are overwritable, example:
```bash
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](https://github.com/YunoHost-Apps/vpnclient_ynh/blob/master/scripts/config)
"""
)

View file

@ -1,486 +0,0 @@
import ast
import datetime
import subprocess
version = open("../debian/changelog").readlines()[0].split()[1].strip("()")
today = datetime.datetime.now().strftime("%d/%m/%Y")
def get_current_commit():
p = subprocess.Popen(
"git rev-parse --verify HEAD",
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
stdout, stderr = p.communicate()
current_commit = stdout.strip().decode("utf-8")
return current_commit
current_commit = get_current_commit()
print(
f"""---
title: Options
template: docs
taxonomy:
category: docs
routes:
default: '/dev/forms'
---
Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{current_commit}/doc/generate_options_doc.py) on {today} (YunoHost version {version})
## Glossary
You may encounter some named types which are used for simplicity.
- `Translation`: a translated property
- used for properties: `ask`, `help` and `Pattern.error`
- a `dict` with locales as keys and translations as values:
```toml
ask.en = "The text in english"
ask.fr = "Le texte en français"
```
It is not currently possible for translators to translate those string in weblate.
- a single `str` for a single english default string
```toml
help = "The text in english"
```
- `JSExpression`: a `str` JS expression to be evaluated to `true` or `false`:
- used for properties: `visible` and `enabled`
- operators availables: `==`, `!=`, `>`, `>=`, `<`, `<=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%` and `match()`
- [examples available in the advanced section](#advanced-use-cases)
- `Binding`: bind a value to a file/property/variable/getter/setter/validator
- save the value in `settings.yaml` when not defined
- nothing at all with `"null"`
- a custom getter/setter/validator with `"null"` + a function starting with `get__`, `set__`, `validate__` in `scripts/config`
- a variable/property in a file with `:__FINALPATH__/my_file.php`
- a whole file with `__FINALPATH__/my_file.php`
- [examples available in the advanced section](#bind)
- `Pattern`: a `dict` with a regex to match the value against and an error message
```toml
pattern.regexp = '^[A-F]\d\d$'
pattern.error = "Provide a room like F12: one uppercase and 2 numbers"
# or with translated error
pattern.error.en = "Provide a room like F12: one uppercase and 2 numbers"
pattern.error.fr = "Entrez un numéro de salle comme F12: une lettre majuscule et deux chiffres."
```
- IMPORTANT: your `pattern.regexp` should be between simple quote, not double.
"""
)
fname = "../src/utils/form.py"
content = open(fname).read()
# NB: This magic is because we want to be able to run this script outside of a YunoHost context,
# in which we cant really 'import' the file because it will trigger a bunch of moulinette/yunohost imports...
tree = ast.parse(content)
OptionClasses = [
c for c in tree.body if isinstance(c, ast.ClassDef) and c.name.endswith("Option")
]
OptionDocString = {}
for c in OptionClasses:
if not isinstance(c.body[0], ast.Expr):
continue
option_type = None
if c.name in {"BaseOption", "BaseInputOption"}:
option_type = c.name
elif c.body[1].target.id == "type":
option_type = c.body[1].value.attr
docstring = ast.get_docstring(c)
if docstring:
if "##### Properties" not in docstring:
docstring += """
##### Properties
- [common properties](#common-option-properties)
"""
OptionDocString[option_type] = docstring
for option_type, doc in OptionDocString.items():
print("")
if option_type == "BaseOption":
print("## Common Option properties")
elif option_type == "BaseInputOption":
print("## Common Inputs properties")
elif option_type == "display_text":
print("----------------")
print("## Readonly Options")
print(f"### Option `{option_type}`")
elif option_type == "string":
print("----------------")
print("## Input Options")
print(f"### Option `{option_type}`")
else:
print(f"### Option `{option_type}`")
print("")
print(doc)
print("")
print(
"""
----------------
## Reading and writing values
! Config panels only
You can read and write values with 2 mechanisms: the `bind` property in the `config_panel.toml` and for complex use cases the getter/setter in a `config` script.
If you did not define a specific getter/setter (see below), and no `bind` argument was defined, YunoHost will read/write the value from/to the app's `/etc/yunohost/$app/settings.yml` file.
With `bind`, we can:
- alter the source the value comes from with binds to file or custom getters.
- alter the destination with binds to file or custom setters.
- parse/validate the value before destination with validators
! IMPORTANT: 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
### Read / write into a var of an actual configuration file
Settings usually correspond to key/values in actual app configurations. Hence, a more useful mode is to have `bind = ":FILENAME"` with a colon `:` before. In that case, YunoHost will automagically find a line with `KEY=VALUE` in `FILENAME` (with the adequate separator between `KEY` and `VALUE`).
YunoHost will then use this value for the read/get operation. During write/set operations, YunoHost will overwrite the value in **both** FILENAME and in the app's settings.yml
Configuration file format supported: `YAML`, `TOML`, `JSON`, `INI`, `PHP`, `.env`-like, `.py`.
The feature probably works with others formats, but should be tested carefully.
```toml
[main.main.theme]
# Do not use `file` for this since we only want to insert/save a value
type = "string"
bind = ":__INSTALL_DIR__/config.yml"
```
In which case, YunoHost will look for something like a key/value, with the key being `theme`.
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 you can write:
```toml
[main.main.theme]
type = "string"
bind = "css_theme:__FINALPATH__/config.yml"
```
!!!! Note: This mechanism is quasi language agnostic and will use regexes to find something that looks like a key=value or common variants. However, it does assume that the key and value are stored on the same line. It doesn't support multiline text or file in a variable with this method. If you need to save multiline content in a configuration variable, you should create a custom getter/setter (see below).
Nested syntax is also supported, which may be useful for example to remove ambiguities about stuff looking like:
```json
{
"foo": {
"max": 123
},
"bar": {
"max": 456
}
}
```
which we can `bind` to using:
```toml
bind = "foo>max:__INSTALL_DIR__/conf.json"
```
### Read / write an entire file
Useful when using a question `file` or `text` for which you want to save the raw content directly as a file on the system.
```toml
[panel.section.config_file]
type = "file"
bind = "__FINALPATH__/config.ini"
```
```toml
[panel.section.config_content]
type = "text"
bind = "__FINALPATH__/config.ini"
default = "key: 'value'"
```
## Advanced use cases
Sometimes the `bind` mechanism is not enough:
* the config file format is not supported (e.g. xml, csv)
* the data is not contained in a config file (e.g. database, directory, web resources...)
* the data should be written but not read (e.g. password)
* the data should be read but not written (e.g. fetching status information)
* we want to change other things than the value (e.g. the choices list of a select)
* the question answer contains several values to dispatch in several places
* and so on
You can create specific getter/setters functions inside the `scripts/config` of your app to customize how the information is read/written.
```bash
#!/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
```
### Getters
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 a raw format, in which case the return is binded directly to the value of the question
2) or a yaml format, in this case you dynamically provide properties for your question (for example the `style` of an `alert`, the list of available `choices` of a `select`, etc.)
[details summary="<i>Basic example with raw stdout: get the timezone on the system</i>" class="helper-card-subtitle text-muted"]
`config_panel.toml`
```toml
[main.main.timezone]
ask = "Timezone"
type = "string"
```
`scripts/config`
```bash
get__timezone() {
echo "$(cat /etc/timezone)"
}
```
[/details]
[details summary="<i>Basic example with yaml-formated stdout : Display a list of available plugins</i>" class="helper-card-subtitle text-muted"]
`config_panel.toml`
```toml
[main.plugins.plugins]
ask = "Plugin to activate"
type = "tags"
choices = []
```
`scripts/config`
```bash
get__plugins() {
echo "choices: [$(ls $install_dir/plugins/ | tr '\n' ',')]"
}
```
[/details]
[details summary="<i>Advanced example with yaml-formated stdout : Display the status of a VPN</i>" class="helper-card-subtitle text-muted"]
`config_panel.toml`
```toml
[main.cube.status]
ask = "Custom getter alert"
type = "alert"
style = "info"
bind = "null" # no behaviour on
```
`scripts/config`
```bash
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
}
```
[/details]
### Setters
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.
[details summary="<i>Basic example : Set the system timezone</i>" class="helper-card-subtitle text-muted"]
`config_panel.toml`
```toml
[main.main.timezone]
ask = "Timezone"
type = "string"
```
`scripts/config`
```bash
set__timezone() {
echo "$timezone" > /etc/timezone
ynh_print_info "The timezone has been changed to $timezone"
}
```
[/details]
### Validation
You will often need to validate data answered by the user before to save it somewhere.
Validation can be made with regex through `pattern` argument
```toml
pattern.regexp = '^.+@.+$'
pattern.error = 'An email is required for this field'
```
You can also restrict several types with a choices list.
```toml
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` |
Finally, if you need specific or multi variable validation, you can use custom validators function.
Validators allows us to return custom error messages depending on the value.
```bash
validate__login_user() {
if [[ "${#login_user}" -lt 4 ]]; then echo 'User login is too short, should be at least 4 chars'; fi
}
```
### Actions
Define an option's action in a bash script `script/config`.
It has to be named after a `button`'s id prepended by `run__`.
```toml
[panel.section.my_action]
type = "button"
# no need to set `bind` to "null" it is its hard default
ask = "Run action"
```
```bash
run__my_action() {
ynh_print_info "Running 'my_action'..."
}
```
A more advanced example could look like:
```toml
[panel.my_action_section]
name = "Action section"
[panel.my_action_section.my_repo]
type = "url"
bind = "null" # value will not be saved as a setting
ask = "gimme a repo link"
[panel.my_action_section.my_repo_name]
type = "string"
bind = "null" # value will not be saved as a setting
ask = "gimme a custom folder name"
[panel.my_action_section.my_action]
type = "button"
ask = "Clone the repo"
# enabled the button only if the above values is defined
enabled = "my_repo && my_repo_name"
```
```bash
run__my_action() {
ynh_print_info "Cloning '$my_repo'..."
cd /tmp
git clone "$my_repo" "$my_repo_name"
}
```
### `visible` & `enabled` expression evaluation
Sometimes 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()`.
#### Examples
```toml
# 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](https://github.com/YunoHost/yunohost/blob/dev/src/tests/test_questions.py).
#### match()
For more complex evaluation we can use regex matching.
```toml
[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.
```toml
[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:
```txt
Lorem ipsum dolor et si qua met!
```
"""
)

View file

@ -85,13 +85,13 @@ class ContainerModel(BaseModel):
class SectionModel(ContainerModel, OptionsModel):
"""
Group options. Sections are `dict`s defined inside a Panel and require a unique id (in the below example, the id is `customization` prepended by the panel's id `main`). Keep in mind that this combined id will be used in CLI to refer to the section, so choose something short and meaningfull. Also make sure to not make a typo in the panel id, which would implicitly create an other entire panel.
Sections are, basically, options grouped together. Sections are `dict`s defined inside a Panel and require a unique id (in the below example, the id is `customization` prepended by the panel's id `main`). Keep in mind that this combined id will be used in CLI to refer to the section, so choose something short and meaningfull. Also make sure to not make a typo in the panel id, which would implicitly create an other entire panel.
If at least one `button` is present it then become an action section.
Options in action sections are not considered settings and therefor are not saved, they are more like parameters that exists only during the execution of an action.
FIXME i'm not sure we have this in code.
### Examples
#### Examples
```toml
[main]
@ -106,7 +106,7 @@ class SectionModel(ContainerModel, OptionsModel):
# …refer to Options doc
```
### Properties
#### Properties
- `name` (optional): `Translation` or `str`, displayed as the section's title if any
- `help`: `Translation` or `str`, text to display before the first option
- `services` (optional): `list` of services names to `reload-or-restart` when any option's value contained in the section changes
@ -163,9 +163,9 @@ class SectionModel(ContainerModel, OptionsModel):
class PanelModel(ContainerModel):
"""
Group sections. Panels are `dict`s defined inside a ConfigPanel file and require a unique id (in the below example, the id is `main`). Keep in mind that this id will be used in CLI to refer to the panel, so choose something short and meaningfull.
Panels are, basically, sections grouped together. Panels are `dict`s defined inside a ConfigPanel file and require a unique id (in the below example, the id is `main`). Keep in mind that this id will be used in CLI to refer to the panel, so choose something short and meaningfull.
### Examples
#### Examples
```toml
[main]
name.en = "Main configuration"
@ -176,7 +176,7 @@ class PanelModel(ContainerModel):
[main.customization]
# …refer to Sections doc
```
### Properties
#### Properties
- `name`: `Translation` or `str`, displayed as the panel title
- `help` (optional): `Translation` or `str`, text to display before the first section
- `services` (optional): `list` of services names to `reload-or-restart` when any option's value contained in the panel changes
@ -217,58 +217,23 @@ class PanelModel(ContainerModel):
class ConfigPanelModel(BaseModel):
"""
Configuration panels allows admins to manage parameters or runs actions for which the upstream's app doesn't provide any appropriate UI itself. It's a good way to reduce manual change on config files and avoid conflicts on it.
This is the 'root' level of the config panel toml file
Those panels can also be used to quickly create interfaces that extend the capabilities of YunoHost (e.g. VPN Client, Hotspost, Borg, etc.).
#### Examples
From a packager perspective, this `config_panel.toml` 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 a scripts/config at all!
! 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.
### `config_panel.toml`'s principle and general format
To create configuration panels for apps, you should at least create a `config_panel.toml` at the root of the package. For more complex cases, this TOML file can be paired with a `config` script inside the scripts directory of your package, which will handle specific controller logic.
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.
### Options short keys have to be unique
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:
```toml
[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`)
### Supported questions types and properties
[Learn more about Options](/dev/forms) in their dedicated doc page as those are also used in app install forms and core config panels.
### YunoHost community examples
- [Check the basic example at the end of this doc](#basic-example)
- [Check the example_ynh app toml](https://github.com/YunoHost/example_ynh/blob/master/config_panel.toml.example) and the [basic `scripts/config` example](https://github.com/YunoHost/example_ynh/blob/master/scripts/config)
- [Check config panels of other apps](https://grep.app/search?q=version&filter[repo.pattern][0]=YunoHost-Apps&filter[lang][0]=TOML)
- [Check `scripts/config` of other apps](https://grep.app/search?q=ynh_app_config_apply&filter[repo.pattern][0]=YunoHost-Apps&filter[lang][0]=Shell)
### Examples
```toml
version = 1.0
[config]
# …refer to Panels doc
```
### Properties
#### Properties
- `version`: `float` (default: `1.0`), version that the config panel supports in terms of features.
- `i18n` (optional): `str`, an i18n property that let you internationalize options text.
- However this feature is only available in core configuration panel (like `yunohost domain config`), prefer the use `Translation` in `name`, `help`, etc.
#### Version
Here a small reminder to associate config panel version with YunoHost version.
| Config | YNH | Config panel small change log |
| ------ | --- | ------------------------------------------------------- |
| 0.1 | 3.x | 0.1 config script not compatible with YNH >= 4.3 |
| 1.0 | 4.3.x | The new config panel system with 'bind' property |
"""
version: float = CONFIG_PANEL_VERSION_SUPPORTED

View file

@ -306,7 +306,7 @@ class BaseOption(BaseModel):
! IMPORTANT: as for Panels and Sections you have to choose an id, but this one should be unique in all this document, even if the question is in an other panel.
#### Examples
#### Example
```toml
[section.my_option_id]
@ -325,32 +325,7 @@ class BaseOption(BaseModel):
#### Properties
- `type`:
- readonly types:
- [`display_text`](#option-display_text)
- [`markdown`](#option-markdown)
- [`alert`](#option-alert)
- [`button`](#option-button)
- inputs types:
- [`string`](#option-string)
- [`text`](#option-text)
- [`password`](#option-password)
- [`color`](#option-color)
- [`number`](#option-number)
- [`range`](#option-range)
- [`boolean`](#option-boolean)
- [`date`](#option-date)
- [`time`](#option-time)
- [`email`](#option-email)
- [`path`](#option-path)
- [`url`](#option-url)
- [`file`](#option-file)
- [`select`](#option-select)
- [`tags`](#option-tags)
- [`domain`](#option-domain)
- [`app`](#option-app)
- [`user`](#option-user)
- [`group`](#option-group)
- `type`: the actual type of the option, such as 'markdown', 'password', 'number', 'email', ...
- `ask`: `Translation` (default to the option's `id` if not defined):
- text to display as the option's label for inputs or text to display for readonly options
- in config panels, questions are displayed on the left side and therefore have not much space to be rendered. Therefore, it is better to use a short question, and use the `help` property to provide additional details if necessary.
@ -445,7 +420,7 @@ class DisplayTextOption(BaseReadonlyOption):
"""
Display simple multi-line content.
#### Examples
#### Example
```toml
[section.my_option_id]
@ -462,7 +437,7 @@ class MarkdownOption(BaseReadonlyOption):
Display markdown multi-line content.
Markdown is currently only rendered in the web-admin
#### Examples
#### Example
```toml
[section.my_option_id]
type = "display_text"
@ -485,7 +460,7 @@ class AlertOption(BaseReadonlyOption):
Alerts displays a important message with a level of severity.
You can use markdown in `ask` but will only be rendered in the web-admin.
#### Examples
#### Example
```toml
[section.my_option_id]
@ -496,7 +471,7 @@ class AlertOption(BaseReadonlyOption):
```
#### Properties
- [common properties](#common-option-properties)
- [common properties](#common-properties)
- `style`: any of `"success|info|warning|danger"` (default: `"info"`)
- `icon` (optional): any icon name from [Fork Awesome](https://forkaweso.me/Fork-Awesome/icons/)
- Currently only displayed in the web-admin
@ -526,7 +501,7 @@ class ButtonOption(BaseReadonlyOption):
Every options defined in an action section (a config panel section with at least one `button`) is guaranted to be shown/asked to the user and available in `scripts/config`'s scope.
[check examples in advanced use cases](#actions).
#### Examples
#### Example
```toml
[section.my_option_id]
@ -548,7 +523,7 @@ class ButtonOption(BaseReadonlyOption):
#### Properties
- [common properties](#common-option-properties)
- [common properties](#common-properties)
- `bind`: forced to `"null"`
- `style`: any of `"success|info|warning|danger"` (default: `"success"`)
- `enabled`: `JSExpression` or `bool` (default: `true`)
@ -580,14 +555,14 @@ class BaseInputOption(BaseOption):
"""
Rest of the option types available are considered `inputs`.
#### Examples
#### Example
```toml
[section.my_option_id]
type = "string"
# …any common props… +
optional = false
redact = False
redact = false
default = "some default string"
help = "You can enter almost anything!"
example = "an example string"
@ -596,7 +571,7 @@ class BaseInputOption(BaseOption):
#### Properties
- [common properties](#common-option-properties)
- [common properties](#common-properties)
- `optional`: `bool` (default: `false`, but `true` in config panels)
- `redact`: `bool` (default: `false`), to redact the value in the logs when the value contain private information
- `default`: depends on `type`, the default value to assign to the option
@ -757,7 +732,7 @@ class StringOption(BaseStringOption):
"""
Ask for a simple string.
#### Examples
#### Example
```toml
[section.my_option_id]
type = "string"
@ -780,7 +755,7 @@ class TextOption(BaseStringOption):
Ask for a multiline string.
Renders as a `textarea` in the web-admin and by opening a text editor on the CLI.
#### Examples
#### Example
```toml
[section.my_option_id]
type = "text"
@ -803,7 +778,7 @@ class PasswordOption(BaseInputOption):
Ask for a password.
The password is tested as a regular user password (at least 8 chars)
#### Examples
#### Example
```toml
[section.my_option_id]
type = "password"
@ -855,7 +830,7 @@ class ColorOption(BaseInputOption):
Ask for a color represented as a hex value (with possibly an alpha channel).
Renders as color picker in the web-admin and as a prompt that accept named color like `yellow` in CLI.
#### Examples
#### Example
```toml
[section.my_option_id]
type = "color"
@ -901,7 +876,7 @@ class NumberOption(BaseInputOption):
"""
Ask for a number (an integer).
#### Examples
#### Example
```toml
[section.my_option_id]
type = "number"
@ -976,7 +951,7 @@ class BooleanOption(BaseInputOption):
Ask for a boolean.
Renders as a switch in the web-admin and a yes/no prompt in CLI.
#### Examples
#### Example
```toml
[section.my_option_id]
type = "boolean"
@ -1104,7 +1079,7 @@ class DateOption(BaseInputOption):
Can also take a timestamp as value that will output as an ISO date string.
#### Examples
#### Example
```toml
[section.my_option_id]
type = "date"
@ -1134,7 +1109,7 @@ class TimeOption(BaseInputOption):
Ask for an hour in the form `"22:35"`.
Renders as a date-picker in the web-admin and a regular prompt in CLI.
#### Examples
#### Example
```toml
[section.my_option_id]
type = "time"
@ -1167,7 +1142,7 @@ class EmailOption(BaseInputOption):
"""
Ask for an email. Validation made with [python-email-validator](https://github.com/JoshData/python-email-validator)
#### Examples
#### Example
```toml
[section.my_option_id]
type = "email"
@ -1187,7 +1162,7 @@ class WebPathOption(BaseStringOption):
"""
Ask for an web path (the part of an url after the domain). Used by default in app install to define from where the app will be accessible.
#### Examples
#### Example
```toml
[section.my_option_id]
type = "path"
@ -1235,7 +1210,7 @@ class URLOption(BaseStringOption):
"""
Ask for any url.
#### Examples
#### Example
```toml
[section.my_option_id]
type = "url"
@ -1259,7 +1234,7 @@ class FileOption(BaseInputOption):
Ask for file.
Renders a file prompt in the web-admin and ask for a path in CLI.
#### Examples
#### Example
```toml
[section.my_option_id]
type = "file"
@ -1387,7 +1362,7 @@ class SelectOption(BaseChoicesOption):
Ask for value from a limited set of values.
Renders as a regular `<select/>` in the web-admin and as a regular prompt in CLI with autocompletion of `choices`.
#### Examples
#### Example
```toml
[section.my_option_id]
type = "select"
@ -1415,7 +1390,7 @@ class TagsOption(BaseChoicesOption):
This output as a coma separated list of strings `"one,two,three"`
#### Examples
#### Example
```toml
[section.my_option_id]
type = "tags"
@ -1520,7 +1495,7 @@ class DomainOption(BaseChoicesOption):
Ask for a user domain.
Renders as a select in the web-admin and as a regular prompt in CLI with autocompletion of registered domains.
#### Examples
#### Example
```toml
[section.my_option_id]
type = "domain"
@ -1574,7 +1549,7 @@ class AppOption(BaseChoicesOption):
Ask for a user app.
Renders as a select in the web-admin and as a regular prompt in CLI with autocompletion of installed apps.
#### Examples
#### Example
```toml
[section.my_option_id]
type = "app"
@ -1627,7 +1602,7 @@ class UserOption(BaseChoicesOption):
Ask for a user.
Renders as a select in the web-admin and as a regular prompt in CLI with autocompletion of available usernames.
#### Examples
#### Example
```toml
[section.my_option_id]
type = "user"
@ -1687,7 +1662,7 @@ class GroupOption(BaseChoicesOption):
Ask for a group.
Renders as a select in the web-admin and as a regular prompt in CLI with autocompletion of available groups.
#### Examples
#### Example
```toml
[section.my_option_id]
type = "group"