From 1221fd1458a2cd8a2d9249f2ee7c38c3466cb923 Mon Sep 17 00:00:00 2001 From: axolotle Date: Fri, 21 Apr 2023 22:15:34 +0200 Subject: [PATCH 1/9] doc:options: add documentation and generator for configpanel/manifest options --- doc/generate_options_doc.py | 365 +++++++++++++++++++++++++++ src/utils/form.py | 486 ++++++++++++++++++++++++++++++++++++ 2 files changed, 851 insertions(+) create mode 100644 doc/generate_options_doc.py diff --git a/doc/generate_options_doc.py b/doc/generate_options_doc.py new file mode 100644 index 000000000..fc6078370 --- /dev/null +++ b/doc/generate_options_doc.py @@ -0,0 +1,365 @@ +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: '/packaging_apps_options' +--- + +# Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{current_commit}/doc/generate_options_doc.py) on {today} (YunoHost version {version}) + +# Options + +Options are fields declaration that renders as form items in the web-admin and prompts in cli. +They are used in app manifests to declare the before installation form and in config panels. + +## 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: `==`, `!=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%`, `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 (`None`) + - 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." + ``` + +""" +) + + +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( + """ +---------------- + +## Advanced use cases + +### `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! +``` + + +### `bind` + +Config panels only + +`bind` allows us to alter the generic behavior of option's values which is: get from and set in `settings.yml`. + +We can: +- alter the source the value comes from with getters. +- alter the destination with setters +- parse/validate the value before destination with validators + +##### Getters + +Define an option's custom getter in a bash script `script/config`. +It has to be named after an option's id prepended by `get__`. + +To display a custom alert message for example. We setup a base option in `config_panel.toml`. + +```toml +[panel.section.alert] +type = "alert" +# bind to "null" to inform there's something in `scripts/config` +bind = "null" +# `ask` & `style` will be injected by a custom getter +``` + +Then add a custom getter that output yaml, every properties defined here will override any property previously declared. + +```bash +get__alert() { + if [ "$whatever" ]; then + cat << EOF +style: success +ask: Your VPN is running :) +EOF + else + cat << EOF +style: danger +ask: Your VPN is down +EOF + fi +} +``` + +Or to inject a custom value: + +```toml +[panel.section.my_hidden_value] +type = "number" +bind = "null" +# option will act as an hidden variable that can be used in context evaluation +# (ie: `visible` or `enabled`) +readonly = true +visible = false +# `default` injected by a custom getter +``` + +```bash +get__my_hidden_value() { + if [ "$whatever" ]; then + # if only a value is needed + echo "10" + else + # or if we need to override some other props + # (use `default` or `value` to inject the value) + cat << EOF +ask: Here's a number +visible: true +default: 0 +EOF + fi +} +``` + +##### Setters + +Define an option's custom setter in a bash script `script/config`. +It has to be named after an option's id prepended by `set__`. + +```toml +[panel.section.my_value] +type = "string" +bind = "null" +ask = "gimme complex string" +``` + +```bash +set__my_value() { + if [ -n "$my_value" ]; then + # split the string into multiple elements or idk + fi + # To save the value or modified value as a setting: + ynh_app_setting_set --app=$app --key=my_value --value="$my_value" +} +``` + +##### Validators + +Define an option's custom validator in a bash script `script/config`. +It has to be named after an option's id prepended by `validate__`. + +Validators allows us to return custom error messages depending on the value. + +```toml +[panel.section.my_value] +type = "string" +bind = "null" +ask = "Gimme a long string" +default = "too short" +``` + +```bash +validate__my_value() { + if [[ "${#my_value}" -lt 12 ]]; then echo 'Too short!'; 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" +} +``` +""" +) diff --git a/src/utils/form.py b/src/utils/form.py index 07be55312..f030523d6 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -298,6 +298,76 @@ class Pattern(BaseModel): class BaseOption(BaseModel): + """ + ##### Examples + + ```toml + [my_option_id] + type = "string" + # ask as `str` + ask = "The text in english" + # ask as `dict` + ask.en = "The text in english" + ask.fr = "Le texte en français" + # advanced props + visible = "my_other_option_id != 'success'" + readonly = true + # much advanced: config panel only? + bind = "null" + ``` + + ##### 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) + - `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 + - `visible` (optional): `bool` or `JSExpression` (default: `true`) + - define if the option is diplayed/asked + - if `false` and used alongside `readonly = true`, you get a context only value that can still be used in `JSExpression`s + - `readonly` (optional): `bool` (default: `false`, forced to `true` for readonly types): + - If `true` for input types: forbid mutation of its value + - `bind` (optional): `Binding` (default: `None`): + - (config panels only!) allow to choose where an option's is gathered/stored: + - if not specified, the value will be gathered/stored in the `settings.yml` + - if `"null"`: + - the value will not be stored at all (can still be used in context evaluations) + - if in `scripts/config` there's a function named: + - `get__my_option_id`: the value will be gathered from this custom getter + - `set__my_option_id`: the value will be passed to this custom setter where you can do whatever you want with the value + - `validate__my_option_id`: the value will be passed to this custom validator before any custom setter + - if `bind` is a file path: + - if the path starts with `:`, the value be saved as its id's variable/property counterpart + - this only works for first level variables/properties and simple types (no array) + - else the value will be stored as the whole content of the file + - you can use `__FINALPATH__` in your path to point to dynamic install paths + - FIXME are other global variables accessible? + """ + type: OptionType id: str ask: Union[Translation, None] @@ -364,10 +434,34 @@ class BaseReadonlyOption(BaseOption): class DisplayTextOption(BaseReadonlyOption): + """ + Display simple multi-line content. + + ##### Examples + + ```toml + [my_option_id] + type = "display_text" + ask = "Simple text rendered as is." + ``` + """ + type: Literal[OptionType.display_text] = OptionType.display_text class MarkdownOption(BaseReadonlyOption): + """ + Display markdown multi-line content. + Markdown is currently only rendered in the web-admin + + ##### Examples + ```toml + [my_option_id] + type = "display_text" + ask = "Text **rendered** in markdown." + ``` + """ + type: Literal[OptionType.markdown] = OptionType.markdown @@ -379,6 +473,27 @@ class State(str, Enum): 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 + + ```toml + [my_option_id] + type = "alert" + ask = "The configuration seems to be manually modified..." + style = "warning" + icon = "warning" + ``` + ##### Properties + + - [common properties](#common-option-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 + """ + type: Literal[OptionType.alert] = OptionType.alert style: State = State.info icon: Union[str, None] = None @@ -395,6 +510,45 @@ class AlertOption(BaseReadonlyOption): class ButtonOption(BaseReadonlyOption): + """ + Triggers actions. + Available only in config panels. + Renders as a `button` in the web-admin and can be called with `yunohost [app|domain|settings] action run ` in CLI. + + 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 + + ```toml + [my_action_id] + type = "button" + ask = "Break the system" + style = "danger" + icon = "bug" + # enabled only if another option's value (a `boolean` for example) is positive + enabled = "aknowledged" + ``` + + To be able to trigger an action we have to add a bash function starting with `run__` in your `scripts/config` + + ```bash + run__my_action_id() { + ynh_print_info "Running 'my_action_id' action" + } + ``` + + ##### Properties + + - [common properties](#common-option-properties) + - `bind`: forced to `"null"` + - `style`: any of `"success|info|warning|danger"` (default: `"success"`) + - `enabled`: `Binding` or `bool` (default: `true`) + - when used with `Binding` you can enable/disable the button depending on context + - `icon` (optional): any icon name from [Fork Awesome](https://forkaweso.me/Fork-Awesome/icons/) + - Currently only displayed in the web-admin + """ + type: Literal[OptionType.button] = OptionType.button bind: Literal["null"] = "null" help: Union[Translation, None] = None @@ -415,6 +569,35 @@ class ButtonOption(BaseReadonlyOption): class BaseInputOption(BaseOption): + """ + Rest of the option types available are considered `inputs`. + + ##### Examples + + ```toml + [my_option_id] + type = "string" + # …any common props… + + optional = false + redact = False + default = "some default string" + help = "You can enter almost anything!" + example = "an example string" + placeholder = "write something…" + ``` + + ##### Properties + + - [common properties](#common-option-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 + - in case of readonly values, you can use this `default` to assign a value (or return a dynamic `default` from a custom getter) + - `help` (optional): `Translation`, to display a short help message in cli and web-admin + - `example` (optional): `str`, to display an example value in web-admin only + - `placeholder` (optional): `str`, shown in the web-admin fields only + """ + help: Union[Translation, None] = None example: Union[str, None] = None placeholder: Union[str, None] = None @@ -563,10 +746,44 @@ class BaseStringOption(BaseInputOption): class StringOption(BaseStringOption): + """ + Ask for a simple string. + + ##### Examples + ```toml + [my_option_id] + type = "string" + default = "E10" + pattern.regexp = "^[A-F]\d\d$" + pattern.error = "Provide a room like F12 : one uppercase and 2 numbers" + ``` + + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + - `pattern` (optional): `Pattern`, a regex to match the value against + """ + type: Literal[OptionType.string] = OptionType.string 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 + ```toml + [my_option_id] + type = "text" + default = "multi\\nline\\ncontent" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + - `pattern` (optional): `Pattern`, a regex to match the value against + """ + type: Literal[OptionType.text] = OptionType.text @@ -574,6 +791,22 @@ FORBIDDEN_PASSWORD_CHARS = r"{}" class PasswordOption(BaseInputOption): + """ + Ask for a password. + The password is tested as a regular user password (at least 8 chars) + + ##### Examples + ```toml + [my_option_id] + type = "password" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: forced to `""` + - `redact`: forced to `true` + - `example`: forbidden + """ + type: Literal[OptionType.password] = OptionType.password example: Literal[None] = None default: Literal[None] = None @@ -610,6 +843,21 @@ class PasswordOption(BaseInputOption): 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 + ```toml + [my_option_id] + type = "color" + default = "#ff0" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + """ + type: Literal[OptionType.color] = OptionType.color default: Union[str, None] _annotation = Color @@ -642,6 +890,26 @@ class ColorOption(BaseInputOption): class NumberOption(BaseInputOption): + """ + Ask for a number (an integer). + + ##### Examples + ```toml + [my_option_id] + type = "number" + default = 100 + min = 50 + max = 200 + step = 5 + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `type`: `number` or `range` (input or slider in the web-admin) + - `min` (optional): minimal int value inclusive + - `max` (optional): maximal int value inclusive + - `step` (optional): currently only used in the webadmin as the `` step jump + """ + # `number` and `range` are exactly the same, but `range` does render as a slider in web-admin type: Literal[OptionType.number, OptionType.range] = OptionType.number default: Union[int, None] @@ -696,6 +964,27 @@ class NumberOption(BaseInputOption): class BooleanOption(BaseInputOption): + """ + Ask for a boolean. + Renders as a switch in the web-admin and a yes/no prompt in CLI. + + ##### Examples + ```toml + [my_option_id] + type = "boolean" + default = 1 + yes = "agree" + no = "disagree" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `0` + - `yes` (optional): (default: `1`) define as what the thruthy value should output + - can be `true`, `True`, `"yes"`, etc. + - `no` (optional): (default: `0`) define as what the thruthy value should output + - can be `0`, `"false"`, `"n"`, etc. + """ + type: Literal[OptionType.boolean] = OptionType.boolean yes: Any = 1 no: Any = 0 @@ -801,6 +1090,23 @@ class BooleanOption(BaseInputOption): class DateOption(BaseInputOption): + """ + Ask for a date in the form `"2025-06-14"`. + Renders as a date-picker in the web-admin and a regular prompt in CLI. + + Can also take a timestamp as value that will output as an ISO date string. + + ##### Examples + ```toml + [my_option_id] + type = "date" + default = "2070-12-31" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + """ + type: Literal[OptionType.date] = OptionType.date default: Union[str, None] _annotation = datetime.date @@ -816,6 +1122,21 @@ class DateOption(BaseInputOption): 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 + ```toml + [my_option_id] + type = "time" + default = "12:26" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + """ + type: Literal[OptionType.time] = OptionType.time default: Union[str, int, None] _annotation = datetime.time @@ -835,12 +1156,41 @@ class TimeOption(BaseInputOption): class EmailOption(BaseInputOption): + """ + Ask for an email. Validation made with [python-email-validator](https://github.com/JoshData/python-email-validator) + + ##### Examples + ```toml + [my_option_id] + type = "email" + default = "Abc.123@test-example.com" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + """ + type: Literal[OptionType.email] = OptionType.email default: Union[EmailStr, None] _annotation = EmailStr 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 + ```toml + [my_option_id] + type = "path" + default = "/" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + - `pattern` (optional): `Pattern`, a regex to match the value against + """ + type: Literal[OptionType.path] = OptionType.path @staticmethod @@ -874,6 +1224,21 @@ class WebPathOption(BaseStringOption): class URLOption(BaseStringOption): + """ + Ask for any url. + + ##### Examples + ```toml + [my_option_id] + type = "url" + default = "https://example.xn--zfr164b/@handle/" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + - `pattern` (optional): `Pattern`, a regex to match the value against + """ + type: Literal[OptionType.url] = OptionType.url _annotation = HttpUrl @@ -882,6 +1247,25 @@ class URLOption(BaseStringOption): class FileOption(BaseInputOption): + """ + Ask for file. + Renders a file prompt in the web-admin and ask for a path in CLI. + + ##### Examples + ```toml + [my_option_id] + type = "file" + accept = ".json" + # bind the file to a location to save the file there + bind = "/tmp/my_file.json" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + - `accept`: a comma separated list of extension to accept like `".conf, .ini` + - /!\ currently only work on the web-admin + """ + type: Literal[OptionType.file] = OptionType.file # `FilePath` for CLI (path must exists and must be a file) # `bytes` for API (a base64 encoded file actually) @@ -991,6 +1375,24 @@ class BaseChoicesOption(BaseInputOption): class SelectOption(BaseChoicesOption): + """ + Ask for value from a limited set of values. + Renders as a regular `` in the web-admin and as a regular prompt in CLI with autocompletion of `choices`. - ##### Examples + #### Examples ```toml [section.my_option_id] type = "select" @@ -1398,7 +1395,7 @@ class SelectOption(BaseChoicesOption): choices = "one,two,three" default = "two" ``` - ##### Properties + #### Properties - [common inputs properties](#common-inputs-properties) - `default`: `""`, obviously the default has to be empty or an available `choices` item. - `choices`: a (coma separated) list of values @@ -1418,7 +1415,7 @@ class TagsOption(BaseChoicesOption): This output as a coma separated list of strings `"one,two,three"` - ##### Examples + #### Examples ```toml [section.my_option_id] type = "tags" @@ -1430,7 +1427,7 @@ class TagsOption(BaseChoicesOption): # choices = "one,two,three" default = "two,three" ``` - ##### Properties + #### Properties - [common inputs properties](#common-inputs-properties) - `default`: `""`, obviously the default has to be empty or an available `choices` item. - `pattern` (optional): `Pattern`, a regex to match all the values against @@ -1523,12 +1520,12 @@ 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 + #### Examples ```toml [section.my_option_id] type = "domain" ``` - ##### Properties + #### Properties - [common inputs properties](#common-inputs-properties) - `default`: forced to the instance main domain """ @@ -1577,13 +1574,13 @@ 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 + #### Examples ```toml [section.my_option_id] type = "app" filter = "is_webapp" ``` - ##### Properties + #### Properties - [common inputs properties](#common-inputs-properties) - `default`: `""` - `filter` (optional): `JSExpression` with what `yunohost app info --full` returns as context (only first level keys) @@ -1630,12 +1627,12 @@ 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 + #### Examples ```toml [section.my_option_id] type = "user" ``` - ##### Properties + #### Properties - [common inputs properties](#common-inputs-properties) - `default`: the first admin user found """ @@ -1690,13 +1687,13 @@ 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 + #### Examples ```toml [section.my_option_id] type = "group" default = "visitors" ``` - ##### Properties + #### Properties - [common inputs properties](#common-inputs-properties) - `default`: `"all_users"`, `"visitors"` or `"admins"` (default: `"all_users"`) """ From 02619e8284bfa068202a5aa941448bd70cc92ecb Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 24 Oct 2023 15:05:26 +0200 Subject: [PATCH 7/9] doc:config fix missing aleks additions --- doc/generate_configpanel_doc.py | 58 +++++----- doc/generate_options_doc.py | 197 ++++++++++++++++---------------- src/utils/configpanel.py | 22 ++-- 3 files changed, 139 insertions(+), 138 deletions(-) diff --git a/doc/generate_configpanel_doc.py b/doc/generate_configpanel_doc.py index e29a80dbc..1eb7b5ebb 100644 --- a/doc/generate_configpanel_doc.py +++ b/doc/generate_configpanel_doc.py @@ -88,41 +88,41 @@ print( """ ## Full example -We supposed we have an upstream app with this simple config.yml file: +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 apps' +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\]`: +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] + [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.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] + [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" + [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. @@ -147,13 +147,13 @@ ynh_app_config_apply() { } ``` -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 +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 could be found by reading [vpnclient_ynh config script](https://github.com/YunoHost-Apps/vpnclient_ynh/blob/master/scripts/config) +More info on this can be found by reading [vpnclient_ynh config script](https://github.com/YunoHost-Apps/vpnclient_ynh/blob/master/scripts/config) """ ) diff --git a/doc/generate_options_doc.py b/doc/generate_options_doc.py index 88f6deb20..ea7febe6d 100644 --- a/doc/generate_options_doc.py +++ b/doc/generate_options_doc.py @@ -132,62 +132,70 @@ print( """ ---------------- -## Read and write values: the `bind` property +## 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. -`bind` allows us to alter the default behavior of applying option's values, which is: get from and set in the app `settings.yml`. +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. -We can: +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 settings. +- 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. +! 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 a configuration file +### 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`, `env`, `php`, `python`. +Configuration file format supported: `YAML`, `TOML`, `JSON`, `INI`, `PHP`, `.env`-like, `.py`. The feature probably works with others formats, but should be tested carefully. -Note that this feature only works with relatively simple cases such as `KEY: VALUE`, but won't properly work with complex data structures like multiline array/lists or dictionnaries. -It also doesn't work with XML format, custom config function call, php define(), … -If you need to save complex/multiline content in a configuration variable, you should do it via a specific getter/setter. - ```toml -[panel.section.config_value] +[main.main.theme] # Do not use `file` for this since we only want to insert/save a value type = "string" -bind = ":__FINALPATH__/config.ini" -default = "" +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" ``` -By default, `bind = ":FILENAME"` will use the option id as `KEY` but the option id may sometime not be the exact same `KEY` name in the configuration file. -For example, [In pepettes app](https://github.com/YunoHost-Apps/pepettes_ynh/blob/5cc2d3ffd6529cc7356ff93af92dbb6785c3ab9a/conf/settings.py##L11), the python variable is `name` and not `project_name`. In that case, the key name can be specified before the colon `:`. - -```toml -[panel.section.project_name] -bind = "name:__FINALPATH__/config.ini" +!!!! 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 + } +} ``` -Sometimes, you want to read and save a value in a variable name that appears several time in the configuration file (for example variables called `max`). The `bind` property allows you to change the value on the variable following a regex in a the file: +which we can `bind` to using: ```toml -bind = "importExportRateLimiting>max:__INSTALL_DIR__/conf.json" +bind = "foo>max:__INSTALL_DIR__/conf.json" ``` ### Read / write an entire file -You can bind a `text` or directly a `file` to a specific file by using `bind = "FILEPATH`. +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] @@ -207,17 +215,14 @@ default = "key: 'value'" 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 writen but not read (e.g. password) - * the data should be read but not writen (e.g. status information) + * 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 -For all of those use cases, there are the specific getter or setter mechanism for an option! +You can create specific getter/setters functions inside the `scripts/config` of your app to customize how the information is read/written. -To create specific getter/setter, you first need to create a `config` script inside the `scripts` directory - -`scripts/config` ```bash #!/bin/bash source /usr/share/yunohost/helpers @@ -232,54 +237,65 @@ ynh_app_config_run $1 ### Getters -Define an option's custom getter in a bash script `script/config`. -It has to be named after an option's `id` prepended by `get__`. +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. -The function should returns one of these two formats: - * a raw format, in this case the return is binded directly to the value of the question - * a yaml format, in this case you can rewrite several properties of your option (like the `style` of an `alert`, the list of `choices` of a `select`, etc.) +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="Basic example : Get the login inside the first line of a file " class="helper-card-subtitle text-muted"] -scripts/config -```bash -get__login_user() { - if [ -s /etc/openvpn/keys/credentials ] - then - echo "$(sed -n 1p /etc/openvpn/keys/credentials)" - else - echo "" - fi -} +[details summary="Basic example with raw stdout: get the timezone on the system" class="helper-card-subtitle text-muted"] + +`config_panel.toml` + +```toml +[main.main.timezone] +ask = "Timezone" +type = "string" ``` -config_panel.toml -```toml -[main.auth.login_user] -ask = "Username" -type = "string" +`scripts/config` + +```bash +get__timezone() { + echo "$(cat /etc/timezone)" +} ``` [/details] -[details summary="Advanced example 1 : Display a list of available plugins" class="helper-card-subtitle text-muted"] -scripts/config -```bash -get__plugins() { - echo "choices: [$(ls $install_dir/plugins/ | tr '\n' ',')]" -} -``` +[details summary="Basic example with yaml-formated stdout : Display a list of available plugins" class="helper-card-subtitle text-muted"] -config_panel.toml +`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="Example 2 : Display the status of a VPN" class="helper-card-subtitle text-muted"] -scripts/config +[details summary="Advanced example with yaml-formated stdout : Display the status of a VPN" 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" ] @@ -298,50 +314,38 @@ EOF fi } ``` - -config_panel.toml -```toml -[main.cube.status] -ask = "Custom getter alert" -type = "alert" -style = "info" -bind = "null" # no behaviour on -``` [/details] ### Setters -Define an option's custom setter in a bash script `script/config`. -It has to be named after an option's id prepended by `set__`. +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`. -This function could access new values defined by the users by using bash variable with the same name as the short key of a question. - -You probably should use `ynh_print_info` in order to display info for user about change that has been made to help them to understand a bit what's going. +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="Basic example : Set the login into the first line of a file " class="helper-card-subtitle text-muted"] -scripts/config -```bash -set__login_user() { - if [ -z "${login_user}" ] - then - echo "${login_user}" > /etc/openvpn/keys/credentials - ynh_print_info "The user login has been registered in /etc/openvpn/keys/credentials" - fi -} +[details summary="Basic example : Set the system timezone" class="helper-card-subtitle text-muted"] + +`config_panel.toml` + +```toml +[main.main.timezone] +ask = "Timezone" +type = "string" ``` -config_panel.toml -```toml -[main.auth.login_user] -ask = "Username" -type = "string" +`scripts/config` + +```bash +set__timezone() { + echo "$timezone" > /etc/timezone + ynh_print_info "The timezone has been changed to $timezone" +} ``` [/details] -#### Validation +### Validation You will often need to validate data answered by the user before to save it somewhere. @@ -353,9 +357,9 @@ pattern.error = 'An email is required for this field' You can also restrict several types with a choices list. ```toml -choices.option1 = "Plop1" -choices.option2 = "Plop2" -choices.option3 = "Plop3" +choices.foo = "Foo (some explanation)" +choices.bar = "Bar (moar explanation)" +choices.loremipsum = "Lorem Ipsum Dolor Sit Amet" ``` Some other type specific argument exist like @@ -366,15 +370,12 @@ Some other type specific argument exist like | `boolean` | `yes` `no` | -If you need more control over validation, you can use custom validators. -Define an option's custom validator in a bash script `script/config`. -It has to be named after an option's id prepended by `validate__`. - +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 'Too short user login'; fi + if [[ "${#login_user}" -lt 4 ]]; then echo 'User login is too short, should be at least 4 chars'; fi } ``` diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 4f333cc5a..47c97a808 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -217,30 +217,30 @@ class PanelModel(ContainerModel): class ConfigPanelModel(BaseModel): """ - Configuration panels allows instances adminitrators to manage some parameters or runs some actions for which the app's upstream doesn't provide any configuration panels itself. It's a good way to reduce manual change on config files and avoid conflicts on it. + 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. - Those panels could also be used as interface generator to extend quickly capabilities of YunoHost (e.g. VPN Client, Hotspost, Borg, etc.). + Those panels can also be used to quickly create interfaces that extend the capabilities of YunoHost (e.g. VPN Client, Hotspost, Borg, etc.). 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! - ! IMPORTANT: Please: Keep in mind the YunoHost spirit, and try to build your panels in such a way as to expose only really useful parameters, and if there are many of them, to relegate those corresponding to rarer use cases to "Advanced" sub-sections. + ! 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. - ### How does `config_panel.toml` work - Basically, configuration panels for apps uses at least a `config_panel.toml` at the root of your package. For advanced usecases, this TOML file could also be paired with a `scripts/config` to define custom getters/setters/validators/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! + ### `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` file describes one or several panels, containing some sections, containing some options generally binded to a params in a configuration file. + 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 should be unique in all the `config_panel.toml` file, not just inside its panel or its section. - - So you can't have + 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] ``` - Indeed the real variable name is server_ip and here you have a conflict. + In which two questions have "real variable name" `is server_ip` and therefore conflict with each other. - ### Options + ! 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. From f02538cef05744c4103fcf1af31d0114e839351e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 30 Oct 2023 18:39:31 +0100 Subject: [PATCH 8/9] doc: iterate on configpanel/form documentation --- ...enerate_configpanel_and_formoptions_doc.py | 171 ++++++ doc/generate_configpanel_doc.py | 159 ------ doc/generate_options_doc.py | 486 ------------------ src/utils/configpanel.py | 57 +- src/utils/form.py | 83 ++- 5 files changed, 211 insertions(+), 745 deletions(-) create mode 100644 doc/generate_configpanel_and_formoptions_doc.py delete mode 100644 doc/generate_configpanel_doc.py delete mode 100644 doc/generate_options_doc.py diff --git a/doc/generate_configpanel_and_formoptions_doc.py b/doc/generate_configpanel_and_formoptions_doc.py new file mode 100644 index 000000000..061ebf77c --- /dev/null +++ b/doc/generate_configpanel_and_formoptions_doc.py @@ -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() diff --git a/doc/generate_configpanel_doc.py b/doc/generate_configpanel_doc.py deleted file mode 100644 index 1eb7b5ebb..000000000 --- a/doc/generate_configpanel_doc.py +++ /dev/null @@ -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) -""" -) diff --git a/doc/generate_options_doc.py b/doc/generate_options_doc.py deleted file mode 100644 index ea7febe6d..000000000 --- a/doc/generate_options_doc.py +++ /dev/null @@ -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="Basic example with raw stdout: get the timezone on the system" 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="Basic example with yaml-formated stdout : Display a list of available plugins" 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="Advanced example with yaml-formated stdout : Display the status of a VPN" 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="Basic example : Set the system timezone" 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! -``` -""" -) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 47c97a808..bfad41280 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -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 diff --git a/src/utils/form.py b/src/utils/form.py index 28c5c4d24..1197bae01 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -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 `