mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
doc: iterate on configpanel/form documentation
This commit is contained in:
parent
02619e8284
commit
f02538cef0
5 changed files with 211 additions and 745 deletions
171
doc/generate_configpanel_and_formoptions_doc.py
Normal file
171
doc/generate_configpanel_and_formoptions_doc.py
Normal 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()
|
|
@ -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)
|
||||
"""
|
||||
)
|
|
@ -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!
|
||||
```
|
||||
"""
|
||||
)
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue