diff --git a/.gitignore b/.gitignore index fe678cb8a..35461118f 100644 --- a/.gitignore +++ b/.gitignore @@ -115,6 +115,7 @@ docs/manuals/gui/controls.md docs/manuals/gui/blocks.md docs/manuals/gui/viselements/*.md !docs/manuals/gui/viselements/index.md +docs/manuals/gui/corelements/*.md docs/manuals/xrefs !docs/manuals/gui/gui_example.ipynb docs/manuals/reference_rest/*.md diff --git a/Pipfile b/Pipfile index a99e8860d..6794eac62 100644 --- a/Pipfile +++ b/Pipfile @@ -8,8 +8,9 @@ name = "pypi" apispec = {extras = ["yaml"], version = "==5.0"} apispec-webframeworks = "==0.5.2" bcrypt = "==3.2.2" +cookiecutter = "==2.1.1" deepdiff = "==6.2.2" -flask = "==2.2.2" +flask = "==2.2.5" flask-cors = "==3.0.10" flask-jwt-extended = "==4.3" flask-marshmallow = "==0.14" @@ -31,7 +32,10 @@ pandas = "==1.5.1" passlib = "==1.7.4" pyarrow = "==10.0.1" pymongo = "==4.2.0" +pyngrok = "==5.1" python-dotenv = "==0.19" +python-magic = {version = "==0.4.24", sys_platform = "!= 'win32'"} +python-magic-bin = {version = "==0.4.14", sys_platform = "== 'win32'"} pytz = "==2021.3" simple-websocket = "==0.9" sqlalchemy = "==1.4.18" diff --git a/docs/manuals/gui/corelements/index.md_template b/docs/manuals/gui/corelements/index.md_template new file mode 100644 index 000000000..7f7993eab --- /dev/null +++ b/docs/manuals/gui/corelements/index.md_template @@ -0,0 +1,17 @@ +# Core elements + +!!! warning "Available in Taipy Community and Enterprise editions" + + The controls listed in this section are available only if the + [`taipy`](https://pypi.org/project/taipy/) Python package is installed. These controls are + **not** present if only [`taipy-gui`](https://pypi.org/project/taipy-gui/) is installed. + +Taipy provides a set of visual elements that are meant to simplify the use of Core +objects... TODO + +## Elements list + +Here is the list of all the available Core elements in Taipy: + +[TOC] + diff --git a/docs/manuals/gui/corelements/scenario_selector.md_template b/docs/manuals/gui/corelements/scenario_selector.md_template new file mode 100644 index 000000000..ea5bdb0a2 --- /dev/null +++ b/docs/manuals/gui/corelements/scenario_selector.md_template @@ -0,0 +1,8 @@ +Select scenarios from the list of all Taipy Core scenario entities. + +Shows all the scenario entities handled by Taipy Core, and lets the user +select them. + +## Usage + +TODO diff --git a/docs/manuals/gui/viselements/button.md_template b/docs/manuals/gui/viselements/button.md_template new file mode 100644 index 000000000..f2e58f0a9 --- /dev/null +++ b/docs/manuals/gui/viselements/button.md_template @@ -0,0 +1,91 @@ +A control that can trigger a function when pressed. + +## Styling + +All the button controls are generated with the "taipy-button" CSS class. You can use this class +name to select the buttons on your page and apply style. + +### [Stylekit](../styling/stylekit.md) support + +The [Stylekit](../styling/stylekit.md) provides specific classes that you can use to style buttons: + +* *secondary*
*error*
*warning*
*success*
+ Buttons are normally displayed using the value of the *color_primary* Stylekit variable. + These classes can be used to change the color used to draw the button, respectively, with + the *color_secondary*, *color_error*, *color_warning* and *color_success* Stylekit variable + values. + + The Markdown content: + ``` + <|Error|button|class_name=error|><|Secondary|button|class_name=secondary|> + ``` + + Renders like this: +
+ + +
Using color classes
+
+ +* *plain*
+ The button is filled with a plain color rather than just outlined. + + The Markdown content: + ``` + <|Button 1|button|><|Button 2|button|class_name=plain|> + ``` + + Renders like this: +
+ + +
Using the plain class
+
+ +* *fullwidth*: The button is rendered on its own line and expands across the entire available width. + +## Usage + +### Simple button + +The button label, which is the button control's default property, is simply displayed as the button +text. + +!!! example "Page content" + + === "Markdown" + + ``` + <|Button Label|button|> + ``` + + === "HTML" + + ```html + Button Label + ``` + +
+ + +
A simple button
+
+ +### Specific action callback + +Button can specify a callback function to be invoked when the button is pressed. + +!!! example "Page content" + + === "Markdown" + + ``` + <|Button Label|button|on_action=button_action_function_name|> + ``` + + === "HTML" + + ```html + Button Label + ``` + diff --git a/docs/manuals/gui/viselements/chart.md_template b/docs/manuals/gui/viselements/chart.md_template new file mode 100644 index 000000000..4154036b9 --- /dev/null +++ b/docs/manuals/gui/viselements/chart.md_template @@ -0,0 +1,126 @@ +Displays data sets in a chart or a group of charts. + +The chart control is based on the [plotly.js](https://plotly.com/javascript/) +graphs library. + +Plotly is a graphing library that provides a vast number of visual +representations of datasets with all sorts of customization capabilities. Taipy +exposes the Plotly components through the `chart` control and heavily depends on +the underlying implementation. + +The core principles of creating charts in Taipy are explained in the +[Basic concepts](charts/basics.md) section.
+Advanced concepts are described in the [Advanced features](charts/advanced.md) section. + +# Description + +The chart control has a large set of properties to deal with the many types of charts +it supports and the different kinds of customization that can be defined. + +### The *data* property + +All the data sets represented in the chart control must be assigned to +its [*data*](#p-data) property. + +The supported types for the [*data*](#p-data) property are: + +- A list of values:
+ Most chart types use two axes (*x*/*y* or *theta*/*r*). When receiving a *data* that is just + a series of values, Taipy sets the first axis values to the value index ([0, 1, ...]) and + the values of the second axis to the values of the collection. +- A [Pandas DataFrame](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html):
+ Taipy charts can be defined by setting the appropriate axis property value to the DataFrame + column name. +- A dictionary:
+ The value is converted into a Pandas DataFrame where each key/value pair is converted + to a column named *key* and the associated value. Note that this will work only when + all the values of the dictionary keys are series that have the same length. +- A list of lists of values:
+ If all the lists have the same length, Taipy creates a Pandas DataFrame with it.
+ If sizes differ, then a DataFrame is created for each list, with a single column + called "*<index>*/0" where *index* is the index of the current list in the *data* + array. Then an array is built using all those DataFrames and used as described + below. +- A Numpy series:
+ Taipy internally builds a Pandas DataFrame with the provided *data*. +- A list of Pandas DataFrames:
+ This can be used when your chart must represent data sets of different sizes. In this case, + you must set the axis property ([*x*](#p-x), [*y*](#p-y), [*r*](#p-r), etc.) value to a string + with the format: "*<index>*/*<column>*" where *index* is the index of the DataFrame + you want to refer to (starting at index 0) and *column* would be the column name of the + referenced DataFrame. +- A list of dictionaries
+ The *data* is converted to a list of Pandas DataFrames. + +### Indexed properties + +Chart controls can hold several traces that may display different data sets.
+To indicate properties for a given trace, you will use the indexed properties +(the ones whose type is *indexed(type)*). When setting the value of an indexed +property, you can specify which trace this property should apply to: you will +use the *property_name[index]* syntax, where the indices start at index 1, to +specify which trace is targeted for this property. + +Indexed properties can have a default value (using the *property_name* syntax with +no index) which is overridden by any specified indexed property:
+Here is an example where *i_property* is an indexed property: + +``` +# This value applies to all the traces of the chart control +general_value = +# This value applies to only the second trace of the chart control +specific_value = + +page = "<|...|chart|...|i_property={general_value}|i_property[2]={specific_value}|...|>" +``` + +In the definition for *page*, you can see that the value *general_value* is set to the +property without the index operator ([]) syntax. That means it applies to all the traces +of the chart control.
+*specific_value*, on the other hand, applies only to the second trace. + +An indexed property can also be assigned an array, without the index operator syntax. +Then each value of the array is set to the property at the appropriate index, in sequence: + +``` +values = [ + value1, + value2, + value3 +] + +page = "<|...|chart|...|i_property={values}|...|>" +``` + +is equivalent to + +``` +page = "<|...|chart|...|i_property[1]={value1}|i_property[2]={value2}|i_property[3]={value3}|...|>" +``` + +or slightly shorter (and if there are no more than three traces): + +``` +page = "<|...|chart|...|i_property={value1}|i_property[2]={value2}|i_property[3]={value3}|...|>" +``` + +## Styling + +All the chart controls are generated with the "taipy-chart" CSS class. You can use this class +name to select the charts on your page and apply style. + +### [Stylekit](../styling/stylekit.md) support + +The [Stylekit](../styling/stylekit.md) provides a specific class that you can use to style charts: + +* *has-background*
+ When the chart control uses the *has-background* class, the rendering of the chart + background is left to the charting library.
+ The default behavior is to render the chart transparently. + +## Usage + +Here is a list of several sub-sections that you can check to get more details on a specific +domain covered by the chart control: + +- [Basic concepts](charts/basics.md) diff --git a/docs/manuals/gui/viselements/date.md_template b/docs/manuals/gui/viselements/date.md_template new file mode 100644 index 000000000..add982489 --- /dev/null +++ b/docs/manuals/gui/viselements/date.md_template @@ -0,0 +1,55 @@ +A control that can display and specify a formatted date, with or without time. + +## Styling + +All the date controls are generated with the "taipy-date" CSS class. You can use this class +name to select the date selectors on your page and apply style. + +### [Stylekit](../styling/stylekit.md) support + +The [Stylekit](../styling/stylekit.md) provides a specific class that you can use to style date selectors: + +* *fullwidth*
+ If a date selector uses the *fullwidth* class, then it uses the whole available + horizontal space. + +## Usage + +### Using the full date and time + +Assuming a variable _dt_ contains a Python `datetime` object, you can create +a date selector that represents it: + +!!! example "Page content" + + === "Markdown" + + ``` + <|{dt}|date|> + ``` + + === "HTML" + + ```html + {dt} + ``` + +### Using only the date + +If you don't need to use the date, you can do so: + +!!! example "Page content" + + === "Markdown" + + ``` + <|{dt}|date|not with_time|> + ``` + + === "HTML" + + ```html + {dt} + ``` + + diff --git a/docs/manuals/gui/viselements/dialog.md_template b/docs/manuals/gui/viselements/dialog.md_template new file mode 100644 index 000000000..2cfecc2de --- /dev/null +++ b/docs/manuals/gui/viselements/dialog.md_template @@ -0,0 +1,133 @@ +A modal dialog. + +Dialog allows showing some content over the current page. +The dialog is closed when the user presses the Cancel or Validate buttons, or clicks outside the area of the dialog (triggering a Cancel action). + +## Styling + +All the dialogs are generated with the "taipy-dialog" CSS class. You can use this class +name to select the dialogs on your page and apply style. + +## Usage + +### Showing or hiding a dialog + +The default property, _open_, indicates whether the dialog is visible or not: + +!!! example "Page content" + + === "Markdown" + + ``` + <|{show_dialog}|dialog|on_action={lambda s: s.assign("show_dialog", False)}|> + ``` + + === "HTML" + + ```html + {show_dialog} + ``` + +With another action that would have previously shown the dialog with: + +```py3 +def button_action(state, id, action): + state.show_dialog = True +``` + + +### Specifying labels and actions + +Several properties let you specify the buttons to show, +and the action (callback functions) triggered when buttons are pressed: + +!!! example "Page content" + + === "Markdown" + + ``` + <|dialog|title=Dialog Title|open={show_dialog}|page_id=page1|on_action=dialog_action|labels=Validate;Cancel|> + ``` + + === "HTML" + + ```html + {show_dialog} + ``` + +The implementation of the dialog callback could be: + +```py3 +def dialog_action(state, id, action, payload): + with state as st: + ... + # depending on payload["args"][0]: -1 for close icon, 0 for Validate, 1 for Cancel + ... + st.show_dialog = False +``` + +### Dialog as block element + +The content of the dialog can be specified directly inside the dialog block. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{show_dialog}|dialog| + ... + <|{some content}|> + ... + |> + ``` + + === "HTML" + + ```html + + ... + {some content} + ... + + ``` + +### Dialog with page + +The content of the dialog can be specified as an existing page name using the _page_ property. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{show_dialog}|dialog|page=page_name|> + ``` + + === "HTML" + + ```html + {show_dialog} + ``` + +### Dialog with partial + +The content of the dialog can be specified as a `Partial^` instance using the _partial_ property. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{show_dialog}|dialog|partial={partial}|> + ``` + + === "HTML" + + ```html + {show_dialog} + ``` diff --git a/docs/manuals/gui/viselements/expandable.md_template b/docs/manuals/gui/viselements/expandable.md_template new file mode 100644 index 000000000..0341c3515 --- /dev/null +++ b/docs/manuals/gui/viselements/expandable.md_template @@ -0,0 +1,90 @@ +Displays its child elements in a collapsable area. + +Expandable is a block control. + +## Styling + +All the expandable blocks are generated with the "taipy-expandable" CSS class. You can use this class +name to select the expandable blocks on your page and apply style. + +## Usage + +### Defining a title and managing expanded state + +The default property _title_ defines the title shown when the visual element is collapsed. + +!!! example "Page content" + + === "Markdown" + + ``` + <|Title|expandable|expand={expand}|> + ``` + + === "HTML" + + ```html + Title + ``` + +### Content as block + +The content of `expandable` can be specified as the block content. + +!!! example "Page content" + + === "Markdown" + + ``` + <|Title|expandable| + ... + <|{some content}|> + ... + |> + ``` + + === "HTML" + + ```html + + ... + {some content} + ... + + ``` + +### Expandable with page + +The content of the expandable can be specified as an existing page name using the _page_ property. + +!!! example "Page content" + + === "Markdown" + + ``` + <|Title|expandable|page=page_name|> + ``` + + === "HTML" + + ```html + Title + ``` + +### Expandable with partial + +The content of the expandable can be specified as a `Partial^` instance using the _partial_ property. + +!!! example "Page content" + + === "Markdown" + + ``` + <|Title|expandable|partial={partial}|> + ``` + + === "HTML" + + ```html + Title + ``` diff --git a/docs/manuals/gui/viselements/file_download.md_template b/docs/manuals/gui/viselements/file_download.md_template new file mode 100644 index 000000000..f734f7471 --- /dev/null +++ b/docs/manuals/gui/viselements/file_download.md_template @@ -0,0 +1,94 @@ +Allows downloading of a file content. + + +!!! Note "Image format" + Note that if the content is provided as a buffer of bytes, it can be converted + to an image content if and only if you have installed the + [`python-magic`](https://pypi.org/project/python-magic/) Python package (as well + as [`python-magic-bin`](https://pypi.org/project/python-magic-bin/) if your + platform is Windows). + +The download can be triggered when clicking on a button, or can be performed automatically. + +## Styling + +All the file download controls are generated with the "taipy-file_download" CSS class. You can use this class +name to select the file download controls on your page and apply style. + +## Usage + +### Default behavior + +Allows downloading _content_ when content is a file path or some content. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{content}|file_download|> + ``` + + === "HTML" + + ```html + {content} + ``` + +### Standard configuration + +A specific _label_ can be shown beside the standard icon. + +The function name provided as _on_action_ is called when the user initiates the download. + +The _name_ provided will be the default name proposed to the user when downloading (depending on browser validation and rules). + +!!! example "Page content" + + === "Markdown" + + ``` + <|{content}|file_download|label=Download File|on_action=function_name|name=filename|> + ``` + + === "HTML" + + ```html + {content} + ``` + +### Preview file in the browser + +The file content can be visualized in the browser (if supported and in another tab) by setting _bypass_preview_ to False. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{content}|file_download|bypass_preview=False|> + ``` + + === "HTML" + + ```html + {content} + ``` + +### Automatic download + +The file content can be downloaded automatically (when the page shows and when the content is set). + +!!! example "Page content" + + === "Markdown" + + ``` + <|{content}|file_download|auto|> + ``` + + === "HTML" + + ```html + {content} + ``` diff --git a/docs/manuals/gui/viselements/file_selector.md_template b/docs/manuals/gui/viselements/file_selector.md_template new file mode 100644 index 000000000..8716d176d --- /dev/null +++ b/docs/manuals/gui/viselements/file_selector.md_template @@ -0,0 +1,67 @@ +Allows uploading a file content. + +The upload can be triggered by pressing a button, or drag-and-dropping a file on top of the control. + +## Styling + +All the file selector controls are generated with the "taipy-file_selector" CSS class. You can use this class +name to select the file selector controls on your page and apply style. + +## Usage + +### Default behavior + +The variable specified in _content_ is populated by a local filename when the transfer is completed. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{content}|file_selector|> + ``` + + === "HTML" + + ```html + {content} + ``` + +### Standard configuration + +A specific _label_ can be shown besides the standard icon. +The function name provided as _on_action_ is called when the transfer is completed. +The _extensions_ property can be used as a list of file name extensions that is used to filter the file selection box. This filter is not enforced: the user can select and upload any file. +Upon dragging a file over the button, the _drop_message_ content is displayed as a temporary label for the button. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{content}|file_selector|label=Download File|on_action=function_name|extensions=.csv,.xlsx|drop_message=Drop Message|> + ``` + + === "HTML" + + ```html + {content} + ``` + +### Multiple files upload + +The user can transfer multiple files at once by setting the _multiple_ property to True. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{content}|file_selector|multiple|> + ``` + + === "HTML" + + ```html + {content} + ``` diff --git a/docs/manuals/gui/viselements/image.md_template b/docs/manuals/gui/viselements/image.md_template new file mode 100644 index 000000000..b4977cc9c --- /dev/null +++ b/docs/manuals/gui/viselements/image.md_template @@ -0,0 +1,61 @@ +A control that can display an image. + +!!! Note "Image format" + Note that if the content is provided as a buffer of bytes, it can be converted + to an image content if and only if you have installed the + [`python-magic`](https://pypi.org/project/python-magic/) Python package (as well + as [`python-magic-bin`](https://pypi.org/project/python-magic-bin/) if your + platform is Windows). + +You can indicate a function to be called when the user clicks on the image. + +## Styling + +All the image controls are generated with the "taipy-image" CSS class. You can use this class +name to select the image controls on your page and apply style. + +The [Stylekit](../styling/stylekit.md) also provides a specific CSS class that you can use to style +images: + +- *inline*
+ Displays an image as inline and vertically centered. It would otherwise be displayed as a block. + This can be relevant when dealing with SVG images. + +## Usage + +### Default behavior + +Shows an image specified as a local file path or as raw content. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{content}|image|> + ``` + + === "HTML" + + ```html + {content} + ``` + +### Call a function on click + +A specific _label_ can be shown over the image. +The function name provided as _on_action_ is called when the image is clicked (same as button). + +!!! example "Page content" + + === "Markdown" + + ``` + <|{content}|image|label=this is an image|on_action=function_name|> + ``` + + === "HTML" + + ```html + {content} + ``` diff --git a/docs/manuals/gui/viselements/index.md b/docs/manuals/gui/viselements/index.md index e1d35fa4b..6bac2526b 100644 --- a/docs/manuals/gui/viselements/index.md +++ b/docs/manuals/gui/viselements/index.md @@ -7,6 +7,9 @@ or layout information. Most visual elements allow users to interact with the pag There are two types of _Visual Elements_: - _Controls_ typically represent user data that the user can interact with; + The `taipy` package come with a dedicated set of Taipy GUI controls that let users + display and interact with [Taipy Core entities](../../core/entities). These controls + are listed in the [Core back-end controls](../corelements) section. - _Blocks_ let you organize controls (or blocks) in pages to provide the best possible user experience. @@ -16,6 +19,8 @@ may want to jump directly to the list of the available visual elements: [:material-arrow-right: List of available controls](../controls.md) +[:material-arrow-right: List of available Core back-end controls](../corelements) + [:material-arrow-right: List of available blocks](../blocks.md) ## Properties diff --git a/docs/manuals/gui/viselements/indicator.md_template b/docs/manuals/gui/viselements/indicator.md_template new file mode 100644 index 000000000..9ca20265b --- /dev/null +++ b/docs/manuals/gui/viselements/indicator.md_template @@ -0,0 +1,88 @@ +Displays a label on a red to green scale at a specific position. + +The _min_ value can be greater than the _max_ value.
+The value will be maintained between min and max. + +## Styling + +All the indicator controls are generated with the "taipy-indicator" CSS class. You can use this class +name to select the indicator controls on your page and apply style. + +## Usage + +### Minimal usage + +Shows a message at a specified position between min and max. + +!!! example "Page content" + + === "Markdown" + + ``` + <|message|indicator|value={val}|min=0|max=100|> + ``` + + === "HTML" + + ```html + message + ``` + +### Formatting the message + +A _format_ can be applied to the message. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{50}|indicator|format=%.2f|value=10|> + ``` + + === "HTML" + + ```html + {50} + ``` + + +### Vertical indicators + +The _orientation_ can be specified to "vertical" (or "v") to create a vertical indicator. + +!!! example "Page content" + + === "Markdown" + + ``` + <|message|indicator|orientation=v|value=10|> + ``` + + === "HTML" + + ```html + message + ``` + +### Dimensions + +Properties _width_ and _height_ can be specified depending of the _orientation_. + +!!! example "Page content" + + === "Markdown" + + ``` + <|message|indicator|value={val}|width=50vw|> + + <|message|indicator|value={val}|orientation=vertical|height=50vh|> + ``` + + === "HTML" + + ```html + message + + message + ``` diff --git a/docs/manuals/gui/viselements/input.md_template b/docs/manuals/gui/viselements/input.md_template new file mode 100644 index 000000000..b1c082b4f --- /dev/null +++ b/docs/manuals/gui/viselements/input.md_template @@ -0,0 +1,35 @@ +A control that displays some text that can potentially be edited. + +## Styling + +All the input controls are generated with the "taipy-input" CSS class. You can use this class +name to select the input controls on your page and apply style. + +### [Stylekit](../styling/stylekit.md) support + +The [Stylekit](../styling/stylekit.md) provides a specific class that you can use to style input controls: + +* *fullwidth*
+ If an input control uses the *fullwidth* class, then it uses the whole available + horizontal space. + +## Usage + +### Get user input + +You can create an input field bound to a variable with the following content: + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|input|> + ``` + + === "HTML" + + ```html + {value} + ``` + diff --git a/docs/manuals/gui/viselements/layout.md_template b/docs/manuals/gui/viselements/layout.md_template new file mode 100644 index 000000000..12a60036c --- /dev/null +++ b/docs/manuals/gui/viselements/layout.md_template @@ -0,0 +1,169 @@ +Organizes its children into cells in a regular grid. + +The _columns_ property follows the [CSS standard](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns) syntax. +If the _columns_ property contains only digits and spaces, it is considered as flex-factor unit: +"1 1" => "1fr 1fr" + +## Styling + +All the layout blocks are generated with the "taipy-layout" CSS class. You can use this class +name to select the layout blocks on your page and apply style. + +### [Stylekit](../styling/stylekit.md) support + +The [Stylekit](../styling/stylekit.md) provides specific classes that you can use to style layout +blocks: + +- *align-columns-top*
+ Aligns the content to the top of each column. +- *align-columns-center*
+ Aligns the content to the center of each column. +- *align-columns-bottom*
+ Aligns the content to the bottom of each column. +- *align-columns-stretch*
+ Gives all columns the height of the highest column. + +Additional classes are defined for the [`part`](part.md) block element when inserted in +a layout block. Please see the [section on styling](part.md#stylekit-support) for parts +for more details. + +## Usage + +### Default layout + +The default layout contains 2 columns in desktop mode and 1 column in mobile mode. + +!!! example "Page content" + + === "Markdown" + + ``` + <|layout| + + <|{some content}|> + + |> + ``` + + === "HTML" + + ```html + + + {some content} + + + ``` + + +### Specifying gap + +The _gap_ between adjacent cells is set by default to 0.5rem and can be specified. + +!!! example "Page content" + + === "Markdown" + + ``` + <|layout|gap=20px| + ... + <|{some content}|> + ... + |> + ``` + + === "HTML" + + ```html + + ... + {some content}> + ... + + ``` + +### Layout with a central "greedy" column + +You can use the fr CSS unit so that the middle column use all the available space. + +!!! example "Page content" + + === "Markdown" + + ``` + <|layout|columns=50px 1fr 50px| + + <|{1st column content}|> + + <|{2nd column content}|> + + <|{3rd column content}|> + + <|{1st column and second row content}|> + + ... + |> + ``` + + === "HTML" + + ```html + + + {1st column content} + +
+ {2nd column content} +
+ + {3rd column content} + + + {1st column and second row content} + + ... +
+ ``` + +### Different layout for desktop and mobile devices + +The _columns[mobile]_ property allows to specify a different layout when running on a mobile device. + +!!! example "Page content" + + === "Markdown" + + ``` + <|layout|columns=50px 1fr 50px|columns[mobile]=1 1| + + <|{1st column content}|> + + <|{2nd column content}|> + + <|{3rd column content or 2nd row 1st column on mobile}|> + + <|{1st column and second row content or 2nd row 2nd column on mobile}|> + + ... + |> + ``` + + === "HTML" + + ```html + + + {1st column content} + +
+ {2nd column content} +
+ + {3rd column content or 2nd row 1st column on mobile} + + + {1st column and second row content or 2nd row 2nd column on mobile} + + ... +
+ ``` diff --git a/docs/manuals/gui/viselements/menu.md_template b/docs/manuals/gui/viselements/menu.md_template new file mode 100644 index 000000000..084583fac --- /dev/null +++ b/docs/manuals/gui/viselements/menu.md_template @@ -0,0 +1,110 @@ +Shows a left-side menu. + +This control is represented by a unique left-anchor and foldable vertical menu. + +## Styling + +All the menu controls are generated with the "taipy-menu" CSS class. You can use this class +name to select the menu controls on your page and apply style. + +## Usage + +### Defining a simple static menu + +!!! example "Page content" + + === "Markdown" + + ``` + <|menu|lov=menu 1;menu 2|> + ``` + + === "HTML" + + ```html + + ``` + +### Calling a user-defined function + +To have the selection of a menu item call a user-defined function, you must set the on_action +property to a function that you define: + +You page can define a menu control like: + +!!! example "Page content" + + === "Markdown" + + ``` + <|menu|lov=menu 1;menu 2|on_action=my_menu_action> + ``` + + === "HTML" + + ```html + + ``` + +Your Python script must define the my_menu_action function: + +```def my_menu_action(state, ...): + ... +``` + +### Disabling menu options + +The property _inactive_ids_ can be set to dynamically disable any specific menu options. + +!!! example "Page content" + + === "Markdown" + + ``` + <|menu|lov=menu 1;menu 2;menu 3|inactive_ids=menu 2;menu 3|> + ``` + + === "HTML" + + ```html + + ``` + +### Adjusting presentation + +The property _label_ defines the text associated with the main Icon. +The properties _width_ and _width[mobile]_ specify the requested width of the menu when expanded. + +!!! example "Page content" + + === "Markdown" + + ``` + <|menu|lov=menu 1;menu 2;menu 3|label=Menu title|width=15vw|width[mobile]=80vw|> + ``` + + === "HTML" + + ```html + + ``` + +### Menu icons + +As for every control that deals with lov, each menu option can display an image (see Icon^) and/or some text. + +!!! example "Page content" + + === "Markdown" + + ``` + <|menu|lov={[("id1", Icon("/images/icon.png", "Menu option 1")), ("id2", "Menu option 2")]}|> + ``` + + === "HTML" + + ```html + {[("id1", Icon("/images/icon.png", "Menu option 1")), ("id2", "Menu option 2")]} + ``` + + diff --git a/docs/manuals/gui/viselements/navbar.md_template b/docs/manuals/gui/viselements/navbar.md_template new file mode 100644 index 000000000..fed37c6ed --- /dev/null +++ b/docs/manuals/gui/viselements/navbar.md_template @@ -0,0 +1,55 @@ +A navigation bar control. + +This control is implemented as a list of links. + +## Styling + +All the navbar controls are generated with the "taipy-navbar" CSS class. You can use this class +name to select the navbar controls on your page and apply style. + +### [Stylekit](../styling/stylekit.md) support + +The [Stylekit](../styling/stylekit.md) provides a specific class that you can use to style navbar controls: + +* *fullheight*
+ Ensures the tabs fill the full height of their container (in a header bar for example). + +## Usage + +### Defining a default navbar + +The list of all pages registered in the Gui instance is used to build the navbar. + +!!! example "Page content" + + === "Markdown" + + ``` + <|navbar|> + ``` + + === "HTML" + + ```html + + ``` + + +### Defining a custom navbar + +The _lov_ property is used to define the list of elements that are displayed. +If a lov element id starts whith http, the page is opened in another tab. + +!!! example "Page content" + + === "Markdown" + + ``` + <|navbar|lov={[("page1", "Page 1"), ("http://www.google.com", "Google")]}|> + ``` + + === "HTML" + + ```html + + ``` diff --git a/docs/manuals/gui/viselements/number.md_template b/docs/manuals/gui/viselements/number.md_template new file mode 100644 index 000000000..0880f6b2f --- /dev/null +++ b/docs/manuals/gui/viselements/number.md_template @@ -0,0 +1,35 @@ +A kind of [`input`](input.md) that handles numbers. + + +## Styling + +All the number controls are generated with the "taipy-number" CSS class. You can use this class +name to select the number controls on your page and apply style. + +### [Stylekit](../styling/stylekit.md) support + +The [Stylekit](../styling/stylekit.md) provides a specific class that you can use to style number controls: + +* *fullwidth*
+ If a number control uses the *fullwidth* class, then it uses the whole available + horizontal space. + +## Usage + +### Simple + +You can create a number field bound to a numerical variable with the following content: + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|number|> + ``` + + === "HTML" + + ```html + {value} + ``` diff --git a/docs/manuals/gui/viselements/pane.md_template b/docs/manuals/gui/viselements/pane.md_template new file mode 100644 index 000000000..a17a021ef --- /dev/null +++ b/docs/manuals/gui/viselements/pane.md_template @@ -0,0 +1,145 @@ +A side pane. + +Pane allows showing some content on top of the current page. +The pane is closed when the user clicks outside the area of the pane (triggering a _on_close_ action). + +Pane is a block control. + +## Styling + +All the pane blocks are generated with the "taipy-pane" CSS class. You can use this class +name to select the pane blocks on your page and apply style. + +## Usage + +### Showing or hiding a pane + +The default property, _open_, indicates whether the pane is visible or not: + +!!! example "Page content" + + === "Markdown" + + ``` + <|{show}|pane|> + ``` + + === "HTML" + + ```html + {show} + ``` + +### Choosing where the pane appears + +The _anchor_ property defines on which side of the display the pane is shown. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{show}|pane|anchor=left|> + ``` + + === "HTML" + + ```html + {show} + ``` + +### Showing the pane beside the page content + +The pane is shown beside the page content instead of over it if the _persistent_ property evaluates to True. + +The parent element must have the *flex* display mode in CSS. To achieve this using +the Markdown syntax, you can leverage the +[*d-flex* class](../styling/stylekit.md#c-d-flex) provided in the +[Stylekit](../styling/stylekit.md). + +Here is a full example of how to do this: + +```py +from taipy.gui import Gui + +show_pane=True + +page=""" +<|d-flex| +<|{show_pane}|pane|persistent|width=100px| +Pane content +|> +This button can be pressed to open the persistent pane: +<|Open|button|on_action={lambda s: s.assign("show_pane", True)}|> +|> +""" + +Gui(page=page).run() +``` + +The pane is initially opened. If you close it, the bound variable *show_pane* is +updated (set to False).
+Pressing the button sets the variable *show_pane* to True using a lambda callback, which +opens the pane again. + +### Pane as block element + +The content of the pane can be specified directly inside the pane block. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{show}|pane| + ... + <|{some content}|> + ... + |> + ``` + + === "HTML" + + ```html + + ... + {some content} + ... + + ``` + +### Pane with page + +The content of the pane can be specified as an existing page name using the _page_ property. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{show}|pane|page=page_name|> + ``` + + === "HTML" + + ```html + {show} + ``` + +### Pane with partial + +The content of the pane can be specified as a `Partial^` instance using the _partial_ property. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{show}|pane|partial={partial}|> + ``` + + === "HTML" + + ```html + {show} + ``` diff --git a/docs/manuals/gui/viselements/part.md_template b/docs/manuals/gui/viselements/part.md_template new file mode 100644 index 000000000..cde2c19bb --- /dev/null +++ b/docs/manuals/gui/viselements/part.md_template @@ -0,0 +1,157 @@ +Displays its children in a block. + +The `part` block is used to group visual elements in a single element. +This allows to show or hide them in one action and be placed as a unique element in a [`Layout`](layout.md) cell. + +There is a simplified Markdown syntax to create a `part`, where the element name is optional: + +`<|` just before the end of the line indicates the beginning of a `part` element; +`|>` at the beginning of a line indicated the end of the `part` definition. + + +## Styling + +All the part blocks are generated with the "taipy-part" CSS class. You can use this class +name to select the part blocks on your page and apply style. + +### [Stylekit](../styling/stylekit.md) support + +The [Stylekit](../styling/stylekit.md) provides specific classes that you can use to style part +blocks: + +- *align-item-top*
+ If this part block is inside a [`layout`](layout.md) block, this CSS class aligns the part + content to the top the layout column it belongs to. +- *align-item-center*
+ If this part block is inside a [`layout`](layout.md) block, this CSS class vertically aligns + the part content to the center of the layout column it belongs to. +- *align-item-bottom*
+ If this part block is inside a [`layout`](layout.md) block, this CSS class vertically aligns + the part content to the bottom of the layout column it belongs to. +- *align-item-stretch*
+ If this part block is inside a [`layout`](layout.md) block, this CSS class + gives the part the same height as the highest item in the row where this part + appears in the layout. + +The Stylekit also has several classes that can be used to style part blocks, +as described in the [Styled Sections](../styling/stylekit.md#styled-sections) +documentation.
+Because the default property of the *part* block is *class_name*, you can use the +Markdown short syntax for parts: + +``` +<|card| +... + (card content) +... +|> +``` + +Creates a `part` that has the [*card*](../styling/stylekit.md#card) class defined +in the Stylekit. + +## Usage + +### Grouping controls + +!!! example "Page content" + + === "Markdown" + + ``` + <| + ... + <|{Some Content}|> + ... + |> + ``` + + === "HTML" + + ```html + + ... + {Some Content} + ... + + ``` + +### Showing and hiding controls + +!!! example "Page content" + + === "Markdown" + + ``` + <|part|don't render| + ... + <|{Some Content}|> + ... + |> + ``` + + === "HTML" + + ```html + + ... + {Some Content} + ... + + ``` + +If the *render* property is bound to a Boolean value, the `part` will show or hide its elements according to the value of the bound variable. + +### Styling parts + +The default property name of the `part` block is *class_name*. This allows for setting +a CSS class to a `part` with a very simple Markdown syntax: + +!!! example "Markdown content" + + ``` + <|css-class| + ... + (part content) + ... + |> + ``` + +This creates a `part` block that is applied the *css-class* CSS class defined in the +application stylesheets. + +### Part with page + +The content of the part can be specified as an existing page name or an URL using the *page* property. + +!!! example "Page content" + + === "Markdown" + + ``` + <|part|page=page_name|> + ``` + + === "HTML" + + ```html + + ``` + +### Part with partial + +The content of the part can be specified as a `Partial^` instance using the *partial* property. + +!!! example "Page content" + + === "Markdown" + + ``` + <|part|partial={partial}|> + ``` + + === "HTML" + + ```html + + ``` diff --git a/docs/manuals/gui/viselements/selector.md_template b/docs/manuals/gui/viselements/selector.md_template new file mode 100644 index 000000000..5031fc348 --- /dev/null +++ b/docs/manuals/gui/viselements/selector.md_template @@ -0,0 +1,138 @@ +A control that allows for selecting items from a list of choices. + +Each item is represented by a string, an image or both. + +The selector can let the user select multiple items. + +A filtering feature is available to display only a subset of the items. + +You can use an arbitrary type for all the items (see the [example](#binding-to-a-list-of-objects)). + +## Styling + +All the selector controls are generated with the "taipy-selector" CSS class. You can use this class +name to select the selector controls on your page and apply style. + +### [Stylekit](../styling/stylekit.md) support + +The [Stylekit](../styling/stylekit.md) provides a specific class that you can use to style selector controls: + +* *fullwidth*
+ If a selector control uses the *fullwidth* class, then it uses the whole available + horizontal space. + +## Usage + +### Display a list of string + +You can create a selector on a series of strings: + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|selector|lov=Item 1;Item 2;Item 3|> + ``` + + === "HTML" + + ```html + {value} + ``` + +### Display as a dropdown + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|selector|lov=Item 1;Item 2;Item 3|dropdown|> + ``` + + === "HTML" + + ```html + {value} + ``` + + +### Display with filter and multiple selection + +You can add a filter input field that lets you display only strings that match the filter value. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|selector|lov=Item 1;Item 2;Item 3|multiple|filter|> + ``` + + === "HTML" + + ```html + {value} + ``` + + +### Display a list of tuples + +A selector control that returns an id while selecting a label or `Icon^`. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{sel}|selector|lov={[("id1", "Label 1"), ("id2", Icon("/images/icon.png", "Label 2"),("id3", "Label 3")]}|> + ``` + + === "HTML" + + ```html + + ``` + +### Display a list of objects + +Assuming your Python code has created a list of object: +```py3 +class User: + def __init__(self, id, name, birth_year): + self.id, self.name, self.birth_year = (id, name, birth_year) + +users = [ + User(231, "Johanna", 1987), + User(125, "John", 1979), + User(4, "Peter", 1968), + User(31, "Mary", 1974) + ] + +user_sel = users[2] +``` + +If you want to create a selector control that lets you pick a specific user, you +can use the following fragment. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{user_sel}|selector|lov={users}|type=User|adapter={lambda u: (u.id, u.name)}|> + ``` + + === "HTML" + + ```html + {user_sel} + ``` + +In this example, we are using the Python list _users_ as the selector's _list of values_. +Because the control needs a way to convert the list items (which are instances of the class +_User_) into a string that can be displayed, we are using an _adapter_: a function that converts +an object, whose type must be provided to the _type_ property, to a tuple. The first element +of the tuple is used to reference the selection (therefore those elements should be unique +among all the items) and the second element is the string that turns out to be displayed. diff --git a/docs/manuals/gui/viselements/slider.md_template b/docs/manuals/gui/viselements/slider.md_template new file mode 100644 index 000000000..82082b7ea --- /dev/null +++ b/docs/manuals/gui/viselements/slider.md_template @@ -0,0 +1,67 @@ +Displays and allows the user to set a value within a range. + +The range is set by the values `min` and `max` which must be integer values. + +If the _lov_ property is used, then the slider can be used to select a value among the different choices. + + +## Styling + +All the slider controls are generated with the "taipy-slider" CSS class. You can use this class +name to select the sliders on your page and apply style. + +## Usage + +### Selecting a value between 0 and 100 + +A numeric value can easily be represented and interacted with using the +following content: + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|slider|> + ``` + + === "HTML" + + ```html + {value} + ``` + +### Constraining values + +You can specify what bounds the value should be restrained to: + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|slider|min=1|max=10|> + ``` + + === "HTML" + + ```html + {value} + ``` + +### Changing orientation + + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|slider|orientation=vert|> + ``` + + === "HTML" + + ```html + {value} + ``` diff --git a/docs/manuals/gui/viselements/status.md_template b/docs/manuals/gui/viselements/status.md_template new file mode 100644 index 000000000..41705e970 --- /dev/null +++ b/docs/manuals/gui/viselements/status.md_template @@ -0,0 +1,151 @@ +Displays a status or a list of statuses. + +## Details + +Every status line has a message to be displayed and a status priority. + +The status priority is defined by a string among "info" (or "i"), "success" (or "s"), "warning" (or "w"), and +"error" (or "e"). An unknown string value sets the priority to "info".
+These priorities are sorted from lower to higher as indicated here. + +The property [*value*](#p-value) can be set to a value with the following type: + +- A tuple: the status shows a single line; the first element of the tuple defines the *status* value, and the second + element holds the *message*. +- A dictionary: the status shows a single line; the key "status" of the dictionary holds the *status* value, and the + key "message" holds the *message*. +- A list of tuples: a list of status entries, each defined as described above. +- A list of dictionaries: a list of status entries, each defined as described above. + +When a list of statuses is provided, the status control can be expanded to show all individual +status entries. Users can then remove individual statuses if [*without_close*](#p-without_close) +is set to False (which is the default value). + +## Styling + +All the status controls are generated with the "taipy-status" CSS class. You can use this class +name to select the status controls on your page and apply style. + +## Usage + +### Show a simple status + +To show a simple `status` control, you would define a Python variable: + +```py +status = ("error", "An error has occurred.") +``` + +This variable can be used as the value of the property [*value*](#p-value) of +the `status` control: + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|status|> + ``` + + === "HTML" + + ```html + {value} + ``` + +The control is displayed as follows: +
+ + +
A simple status
+
+ +Note that the variable *status* could have been defined as a dictionary to achieve the +same result: + +```py +status = { + "status": "error", + "message": "An error has occurred." +} +``` + +### Show a list of statuses + +The `status` control can show several status items. They are initially collapsed, where the +control shows the number of statuses with a status priority corresponding to the highest priority +in the status list. + +You can create a list of status items as a Python variable: + +```py +status = [ + ("warning", "Task is launched."), + ("warning", "Taks is waiting."), + ("error", "Task timeout."), + ("info", "Process was cancelled.") +] +``` + +The declaration of the control remains the same: + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|status|> + ``` + + === "HTML" + + ```html + {value} + ``` + +The control is initially displayed as this: +
+ + +
A collapsed status list
+
+ +If the user clicks on the arrow button, the status list is expanded: +
+ + +
An expanded status list
+
+ +The user can remove a status entry by clicking on the cross button. Here, the user +has removed the third status entry: +
+ + +
After the removal of a status
+
+ +### Prevent status dismissal + +If you don't want the user to be allowed to dismiss the displayed statuses, you can set the *without_close* property to True: + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|status|without_close|> + ``` + + === "HTML" + + ```html + {value} + ``` + +With the same array as above, here is what the expanded control looks like: +
+ + +
Preventing removals
+
diff --git a/docs/manuals/gui/viselements/table.md_template b/docs/manuals/gui/viselements/table.md_template new file mode 100644 index 000000000..0168f2da8 --- /dev/null +++ b/docs/manuals/gui/viselements/table.md_template @@ -0,0 +1,353 @@ +Displays a data set as tabular data. + +## Details + +### Data types + +All the data sets represented in the table control must be assigned to +its [*data*](#p-data) property. + +The supported types for the [*data*](#p-data) property are: + +- A list of values:
+ When receiving a *data* that is just a series of values, the table is made of a single column holding + the values at the corresponding index. The column name is then "0". +- A [Pandas DataFrame](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html):
+ Taipy tables then use the same column names as the DataFrame's. +- A dictionary:
+ The value is converted into a Pandas DataFrame where each key/value pair is converted + to a column named *key* and the associated value. Note that this will work only when + all the values of the dictionary keys are series that have the same length. +- A list of lists of values:
+ All the lists must be the same length. The table control creates one row for each list in the + collection. +- A Numpy series:
+ Taipy internally builds a Pandas DataFrame with the provided *data*. + +### Display modes + +The table component supports three display modes: + +- *paginated*: you can choose the page size and page size options. The [*allow_all_rows*](#p-allow_all_rows) + property makes it possible to add an option to show a page with all rows. +- *unpaginated*: all rows and no page are shown. That is the setting when the [*show_all*](#p-show_all) + property is set to True. +- *auto_loading*: the pages are loaded on demand depending on the visible area. That is the behavior when + the [*auto_loading*](#p-auto_loading) property is set to True. + +## Styling + +All the table controls are generated with the "taipy-table" CSS class. You can use this class +name to select the tables on your page and apply style. + +### [Stylekit](../styling/stylekit.md) support + +The [Stylekit](../styling/stylekit.md) provides a CSS custom property: + +- *--table-stripe-opacity*
+ This property contains the opacity applied to odd lines of tables.
+ The default value is 0.5. + +The [Stylekit](../styling/stylekit.md) also provides specific CSS classes that you can use to style +tables: + +- *header-plain*
+ Adds a plain and contrasting background color to the table header. +- *rows-bordered*
+ Adds a bottom border to each row. +- *rows-similar*
+ Removes the even-odd striped background so all rows have the same background. + +### Dynamic styling + +You can modify the style of entire rows or specific table cells based on any criteria, including +the table data itself. + +When Taipy creates the rows and the cells, it can add a specific CSS class to the generated elements. +This class name is the string returned by the function set to the [*style*](#p-style) property for entire rows, +or [*style[column_name]*](#p-style[column_name]) for specific cells. + +The signature of this function depends on which *style* property you use: + + - [*style*](#p-style): this applies to entire rows.
+ The given function expects three optional parameters: + - *state*: the current state + - *index*: the index of the row in this table + - *row*: all the values for this row + - [*style[column_name]*](#p-style[column_name]): this applies to a specific cell.
+ The given function expects five optional parameters: + - *state*: the current state + - *value*: the value of the cell + - *index*: the index of the row in this table + - *row*: all the values for this row + - *column_name*: the name of the column for this cell + +Based on these parameters, the function must return a string that defines a CSS class name that will +be added to the CSS classes for this table row or this specific cell.
+The [example](#styling-rows) below shows how this works. + +## Usage + +### Show tabular data + +Suppose you want to display the data set defined as follows: + +```py +# x_range = [-10, -6, -2, 2, 6, 10] +x_range = range(-10, 11, 4) + +data = { + "x": x_range, + # y1 = x*x + "y1": [x*x for x in x_range], + # y2 = 100-x*x + "y2": [100-x*x for x in x_range] +} +``` + +You can use the following control declaration to display all these numbers +in a table: + +!!! example "Page content" + === "Markdown" + ``` + <|{data}|table|> + ``` + === "HTML" + ```html + + ``` + +The resulting image looks like this: +
+ + +
A simple table
+
+ +### Large data + +The example above had only six lines of data. If we change the *x_range* definition +to create far more data lines, we come up with a table with much more data to display: +```py +# x_range = [-10, -9.98, ..., 9.98, 10.0] - 1000 x values +x_range = [round(20*i/1000-10, 2) for i in range(0, 1001)] + +data = { + "x": large_x_range, + # y1 = x*x + "y1": [round(x*x, 5) for x in large_x_range], + # y2 = 100-x*x + "y2": [round(100-x*x, 5) for x in large_x_range] +} +``` + +We can use the same table control definition: + +!!! example "Page content" + === "Markdown" + ``` + <|{data}|table|> + ``` + === "HTML" + ```html + + ``` + +To get a rendering looking like this: +
+ + +
Paginated table (partial)
+
+ +Only the first 100 rows (as indicated in the 'Rows per page' selector) are visible.
+The table scroll bar lets you navigate across the 100 first rows.
+You can change how many rows are displayed simultaneously using the +[*page_size*](#p-page_size) and [*page_size_options*](#p-page_size_options) properties. + +If you want to display all the rows at the same time, you can change the control definition +to add the [*show_all](#p-show_all): + +!!! example "Page content" + === "Markdown" + ``` + <|{data}|table|show_all|> + ``` + === "HTML" + ```html + + ``` + +Now the table displays all the data rows, and the scrollbar lets you navigate among all of +them: + +
+ + +
Showing all the rows (partial)
+
+ +Setting the [*allow_all_rows*](#p-allow_all_rows) property to True for a paginated table +adds the 'All' option to the page size options, so the user can switch from one mode to +the other. + +### Show specific columns + +If you want to display a specific set of columns, you can use the [*columns*](#p-columns) +property to indicate what columns should be displayed. + +Here is how you would define the table control if you want to hide the column *y2* +from the examples above: + +!!! example "Page content" + === "Markdown" + ``` + <|{data}|table|columns=x;y1|> + ``` + + === "HTML" + ```html + {data} + ``` + +And the *y2* column is not displayed any longer: +
+ + +
Specifying the visible columns
+
+ +### Styling rows + +To give a specific style to a table row, you will use the [*style*](#p-style) property.
+This property holds a function that is invoked when each row is rendered, and it must return +the name of a style, defined in CSS. + +Here is how a row styling function can be defined: + +```py +def even_odd_style(_1, index, _2): + if index % 2: + return "blue-cell" + else: + return "red-cell" +``` + +We only use the second parameter since, in this straightforward case, we do not need the application +*state* (first parameter) or the values in the row (third parameter).
+Based on the row index (received in *index*), this function returns the name of the style to apply +to the row: "blue-cell" if the index is odd, "red-cell" if it is even. + +We need to define what these style names mean. This is done in a CSS stylesheet, where the following +CSS content would appear: + +```css +.blue-cell>td { + color: white; + background-color: blue; +} +.red-cell>td { + color: yellow; + background-color: red; +} +``` + +Note that the style selectors use the CSS child combinator selector ">" to target elements +that hold a `td` element (the cells themselves). + +To use this style, we can adjust the control definition used above so it looks like this: + +!!! example "Page content" + === "Markdown" + ``` + <|{data}|table|style=even_odd_style|> + ``` + + === "HTML" + ```html + + ``` + +The resulting display will be what we expected: + +
+ + +
Styling the rows
+
+ +Note that the styling function is so simple that we could have made it a lambda, directly +in the control definition: + +!!! example "Alternative page content" + === "Markdown" + ``` + <|{data}|table|style={lambda s, idx, r: "blue-cell" if idx % 2 == 0 else "red-cell"}|> + ``` + + === "HTML" + ```html + + ``` + + +### Aggregation + +To get the aggregation functionality in your table, you must indicate which columns can be aggregated +and how to perform the aggregation. + +This is done using the indexed [*group_by*](#p-group_by[column_name]) and +[*apply*](#p-apply[column_name]) properties. + +The [*group_by[column_name]*](#p-group_by[column_name]) property, when set to True indicates that the +column *column_name* can be aggregated. + +The function provided in the [*apply[column_name]*](#p-apply[column_name]) property indicates how to +perform this aggregation. The value of this property, which is a string, can be: + +- A built-in function. Available predefined functions are the following: `count`, `sum`, `mean`, `median`, + `min`, `max`, `std`, `first` (the default value), and `last`. +- The name of a user-defined function or a lambda function.
+ This function receives a single parameter which is the series to aggregate, and it must return a scalar + value that would result from the aggregation. + +!!! example "Page content" + === "Markdown" + ``` + <|{data}|table|group_by[Group column]|apply[Apply column]=count|> + ``` + + === "HTML" + ```html + + ``` + +### Cell tooltips + +You can specify a tooltip for specific table cells. + +When Taipy creates the cells, it can add a specific tooltip that you would have set as the +return value of the function set to the [*tooltip*](#p-tooltip) or +[*tooltip[column_name]*](#p-tooltip[column_name]) properties. + +The signature of this function expects five optional parameters: +- *state*: the current state. +- *value*: the value of the cell. +- *index*: the index of the row in this table. +- *row*: all the values for this row. +- *column_name*: the name of the column for this cell. + +Based on these parameters, the function must return a string that defines a tooltip used as the +cell's tooltip text. + +!!! example "Page content" + === "Markdown" + ``` + <|{data}|table|tooltip={lambda state, val, idx: "A tooltip" if idx % 2 == 0 else "Another tooltip"}|> + ``` + + === "HTML" + ```html + + ``` diff --git a/docs/manuals/gui/viselements/text.md_template b/docs/manuals/gui/viselements/text.md_template new file mode 100644 index 000000000..4be79da5d --- /dev/null +++ b/docs/manuals/gui/viselements/text.md_template @@ -0,0 +1,57 @@ +Displays a value as a static text. + +Note that in order to create a `text` control, you don't need to specify the control name +in the text template. See the documentation for [Controls](../controls.md) for more details. + +## Details + +The _format_ property uses a format string like the ones used by the string _format()_ function of Python. + +If the value is a `date` or a `datetime`, then _format_ can be set to a date/time formatting string. + + +## Styling + +All the text controls are generated with the "taipy-text" CSS class. You can use this class +name to select the text controls on your page and apply style. + +## Usage + +### Display value + +You can represent a variable value as a simple, static text: + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|> + ``` + + === "HTML" + + ```html + {value} + ``` + +### Formatted output + +If your value is a floating point value, you can use the _format_ property +to indicate what the output format should be used. + +To display a floating point value with two decimal places: + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|text|format=%.2f|> + ``` + + === "HTML" + + ```html + {value} + ``` diff --git a/docs/manuals/gui/viselements/toggle.md_template b/docs/manuals/gui/viselements/toggle.md_template new file mode 100644 index 000000000..8305ea6df --- /dev/null +++ b/docs/manuals/gui/viselements/toggle.md_template @@ -0,0 +1,119 @@ +A series of toggle buttons that the user can select. + +## Details + +Each button is represented by a string, an image or both. + +You can use an arbitrary type for all the items (see the [example](#use-arbitrary-objects)). + +## Styling + +All the toggle controls are generated with the "taipy-toggle" CSS class. You can use this class +name to select the toggle controls on your page and apply style. + +The [Stylekit](../styling/stylekit.md) also provides specific CSS classes that you can use to style +toggle controls: + +- *relative*
+ Resets the theme toggle position in the page flow (especially for the theme mode toggle). +- *nolabel*
+ Hides the toggle control's label. + +## Usage + +### Display a list of string + +You can create a list of toggle buttons from a series of strings: + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|toggle|lov=Item 1;Item 2;Item 3|> + ``` + + === "HTML" + + ```html + {value} + ``` + +### Unselect value + +In a toggle control, all buttons might be unselected. Therefore there is no value selected. +In that case, the value of the property _unselected_value_ is assigned if specified. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|toggle|lov=Item 1;Item 2;Item 3|unselected_value=No Value|> + ``` + + === "HTML" + + ```html + {value} + ``` + +### Display a list of tuples + +A toggle control that returns an id while selecting a label or `Icon^`. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{sel}|toggle|lov={[("id1", "Label 1"), ("id2", Icon("/images/icon.png", "Label 2"),("id3", "Label 3")]}|> + ``` + + === "HTML" + + ```html + + ``` + +### Use arbitrary objects + +Assuming your Python code has created a list of objects: +```py3 +class User: + def __init__(self, id, name, birth_year): + self.id, self.name, self.birth_year = (id, name, birth_year) + +users = [ + User(231, "Johanna", 1987), + User(125, "John", 1979), + User(4, "Peter", 1968), + User(31, "Mary", 1974) + ] + +user_sel = users[2] +``` + +If you want to create a toggle control that lets you pick a specific user, you +can use the following fragment: + +!!! example "Page content" + + === "Markdown" + + ``` + <|{user_sel}|toggle|lov={users}|type=User|adapter={lambda u: (u.id, u.name)}|> + ``` + + === "HTML" + + ```html + {user_sel} + ``` + +In this example, we are using the Python list _users_ as the toggle's _list of values_. +Because the control needs a way to convert the list items (which are instances of the class +_User_) into a string that can be displayed, we are using an _adapter_: a function that converts +an object, whose type must be provided to the _type_ property, to a tuple. The first element +of the tuple is used to reference the selection (therefore those elements should be unique +among all the items) and the second element is the string that turns out to be displayed. diff --git a/docs/manuals/gui/viselements/tree.md_template b/docs/manuals/gui/viselements/tree.md_template new file mode 100644 index 000000000..b8c311c08 --- /dev/null +++ b/docs/manuals/gui/viselements/tree.md_template @@ -0,0 +1,231 @@ +A control that allows for selecting items from a hierarchical view of items. + +Each item is represented by a string, an image or both. + +The tree can let the user select multiple items. + +A filtering feature is available to display only a subset of the items. + +You can use an arbitrary type for all the items (see the [example](#binding-to-a-list-of-objects)). + + +## Styling + +All the tree controls are generated with the "taipy-tree" CSS class. You can use this class +name to select the tree controls on your page and apply style. + +## Usage + +### Display a list of string + +You can create a tree on a series of strings: + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|tree|lov=Item 1;Item 2;Item 3|> + ``` + + === "HTML" + + ```html + {value} + ``` + +### Display with filter and multiple selection + +You can add a filter input field that lets you display only strings that match the filter value. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|tree|lov=Item 1;Item 2;Item 3|multiple|filter|> + ``` + + === "HTML" + + ```html + {value} + ``` + + +### Display a list of tuples + +A tree control that returns an id while selecting a label or `Icon^`. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{sel}|tree|lov={[("id1", "Label 1", [("id1.1", "Label 1.1"), ("id1.2", "Label 1.2")]), ("id2", Icon("/images/icon.png", "Label 2")), ("id3", "Label 3", [("id3.1", "Label 3.1"), ("id3.2", "Label 3.2")])]}|> + ``` + + === "HTML" + + ```html + + ``` + +### Display with filter and multiple selection + +You can add a filter input field that lets you display only strings that match the filter value. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|tree|lov=Item 1;Item 2;Item 3|multiple|filter|> + ``` + + === "HTML" + + ```html + {value} + ``` + + +### Manage expanded nodes + +The property _expanded_ must be used to control the expanded/collapse state of the nodes. +By default, the user can expand or collapse nodes. +If _expanded_ is set to False, there can be only one expanded node at any given level of the tree: +if a node is expanded at a certain level and the user click on another node at the same level, the first node will be automatically collapsed. + +The _expanded_ property can also hold a list of ids that are expanded. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{value}|tree|lov=Item 1;Item 2;Item 3|not expanded|> + + <|{value}|tree|lov=Item 1;Item 2;Item 3|expanded=Item 2|> + ``` + + === "HTML" + + ```html + + + + ``` + + +### Display a list of objects + +Assuming your Python code has created a list of object: +```py3 +class User: + def __init__(self, id, name, birth_year, children): + self.id, self.name, self.birth_year, self.children = (id, name, birth_year, children) + +users = [ + User(231, "Johanna", 1987, [User(231.1, "Johanna's son", 2006, [])]), + User(125, "John", 1979, []), + User(4, "Peter", 1968, []), + User(31, "Mary", 1974, []) + ] + +user_sel = users[2] +``` + +If you want to create a tree control that lets you pick a specific user, you +can use the following fragment. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{user_sel}|tree|lov={users}|type=User|adapter={lambda u: (u.id, u.name, u.children)}|> + ``` + + === "HTML" + + ```html + {user_sel} + ``` + +In this example, we are using the Python list _users_ as the tree's _list of values_. +Because the control needs a way to convert the list items (which are instances of the class +_User_) into a string that can be displayed, we are using an _adapter_: a function that converts +an object, whose type must be provided to the _type_ property, to a tuple. The first element +of the tuple is used to reference the selection (therefore those elements should be unique +among all the items) and the second element is the string that turns out to be displayed. + + +### Display a list of objects with built-in adapter + +Objects with attributes _id_, _label_ and _children_ (as a list) can de dealt directly by the built-in _lov_ adapter. + +Assuming your Python code has created a list of object: +```py3 +class User: + def __init__(self, id, label, birth_year, children): + self.id, self.label, self.birth_year, self.children = (id, label, birth_year, children) + +users = [ + User(231, "Johanna", 1987, [User(231.1, "Johanna's son", 2006, [])]), + User(125, "John", 1979, []), + User(4, "Peter", 1968, []), + User(31, "Mary", 1974, []) + ] + +user_sel = users[2] +``` + +If you want to create a tree control that lets you pick a specific user, you +can use the following fragment. + +!!! example "Page content" + + === "Markdown" + + ``` + <|{user_sel}|tree|lov={users}|> + ``` + + === "HTML" + + ```html + + ``` + +### Display a list of dictionary with built-in adapter + +Dictionaries with keys _id_, _label_ and _children_ (as a list) can de dealt directly by the built-in _lov_ adapter. + +Assuming your Python code has created a list of object: +```py3 +users = [ + {"id": "231", "label": "Johanna", "year": 1987, "children": [{"id": "231.1", "label": "Johanna's son", "year": 2006}]}, + {"id": "125", "label": "John", "year": 1979, "children": []}, + {"id": "4", "label": "Peter", "year": 1968, "children": []}, + {"id": "31", "label": "Mary", "year": 1974, "children": []} + ] + +user_sel = users[2] +``` +Displaying the data would be as simple as: + +!!! example "Page content" + + === "Markdown" + + ``` + <|{user_sel}|tree|lov={users}|> + ``` + + === "HTML" + + ```html + + ``` + diff --git a/mkdocs.yml_template b/mkdocs.yml_template index c681f5458..cfe006f08 100644 --- a/mkdocs.yml_template +++ b/mkdocs.yml_template @@ -21,6 +21,8 @@ nav: - "Visual Elements": - manuals/gui/viselements/index.md - "Controls": manuals/gui/controls.md + - "Core Back-end Controls": + - manuals/gui/corelements/index.md - "Blocks": manuals/gui/blocks.md - "Binding variables": manuals/gui/binding.md - "Callbacks": manuals/gui/callbacks.md diff --git a/tools/_setup_generation/elmnts_generator.py b/tools/_setup_generation/elmnts_generator.py new file mode 100644 index 000000000..03cea1a83 --- /dev/null +++ b/tools/_setup_generation/elmnts_generator.py @@ -0,0 +1,230 @@ +import json +import os +import re +from typing import Dict, List, Optional +from .setup import SetupStep + +class ElementsGenerator(SetupStep): + DEFAULT_PROPERTY = "default_property" + PROPERTIES = "properties" + NAME = "name" + INHERITS = "inherits" + + def get_doc_dir(self) -> str: + raise NotImplemented("ElementsGenerator get_doc_dir() must be defined.") + + # Load elements, test validity of doc and resolve inheritance + def load_elements(self, elements_json_path: str, categories: List[str]) -> None: + with open(elements_json_path) as elements_json_file: + loaded_elements = json.load(elements_json_file) + + self.elements = {} + self.categories = {} + for category, elements in loaded_elements.items(): + self.categories[category] = [] + for element in elements: + element_type = element[0] + self.categories[category].append(element_type) + if element_type in self.elements: + raise ValueError( + f"FATAL - Duplicate element type '{element_type}' in {elements_json_path}" + ) + element_desc = element[1] + if not __class__.PROPERTIES in element_desc and not __class__.INHERITS in element_desc: + raise ValueError( + f"FATAL - No properties in element type '{element_type}' in {elements_json_path}" + ) + self.elements[element_type] = element_desc + # Find default property for all element types + for element_type, element_desc in self.elements.items(): + default_property = None + if properties := element_desc.get(__class__.PROPERTIES, None): + for property in properties: + if __class__.DEFAULT_PROPERTY in property: + if property[__class__.DEFAULT_PROPERTY]: + default_property = property[__class__.NAME] + del property[__class__.DEFAULT_PROPERTY] + element_desc[__class__.DEFAULT_PROPERTY] = default_property + # Resolve inheritance + def merge(element_desc, parent_element_desc, default_property) -> Optional[str]: + element_properties = element_desc.get(__class__.PROPERTIES, []) + element_property_names = [p[__class__.NAME] for p in element_properties] + for property in parent_element_desc.get(__class__.PROPERTIES, []): + property_name = property[__class__.NAME] + if property_name in element_property_names: + element_property = element_properties[element_property_names.index(property_name)] + for n in ["type", "default_value", "doc"]: + if not n in element_property and n in property: + element_property[n] = property[n] + else: + element_property_names.append(property_name) + element_properties.append(property) + element_desc[__class__.PROPERTIES] = element_properties + if not default_property and parent_element_desc.get(__class__.DEFAULT_PROPERTY, False): + default_property = parent_element_desc[__class__.DEFAULT_PROPERTY] + return default_property + + for element_type, element_desc in self.elements.items(): + if parent_types := element_desc.get(__class__.INHERITS, None): + del element_desc[__class__.INHERITS] + default_property = element_desc[__class__.DEFAULT_PROPERTY] + for parent_type in parent_types: + parent_desc = self.elements[parent_type] + default_property = merge(element_desc, parent_desc, default_property) + element_desc[__class__.DEFAULT_PROPERTY] = default_property + # Check that documented elements have a default property and a doc file, + # and that their properties have the mandatory settings. + for element in [elm for cat in categories for elm in self.categories[cat]]: + element_desc = self.elements[element] + if not __class__.DEFAULT_PROPERTY in element_desc: + raise ValueError( + f"FATAL - No default property for element type '{element}'" + ) + if not __class__.PROPERTIES in element_desc: + raise ValueError( + f"FATAL - No properties for element type '{element}'" + ) + doc_path = self.get_element_template_path(element) + if not os.access(doc_path, os.R_OK): + raise FileNotFoundError( + f"FATAL - Could not find doc for element type '{element}' at {doc_path}" + ) + # Check completeness + for property in element_desc[__class__.PROPERTIES]: + for n in ["type", "doc"]: + if not n in property: + raise ValueError( + f"FATAL - No value for '{n}' in the '{property[__class__.NAME]}' properties of element type '{element_typ}' in {viselements_json_path}" + ) + + FIRST_PARA_RE = re.compile(r"(^.*?)(:?\n\n)", re.MULTILINE | re.DOTALL) + FIRST_HEADER1_RE = re.compile(r"(^.*?)(\n#\s+)", re.MULTILINE | re.DOTALL) + # Find first level 2 or 3 header + FIRST_HEADER2_RE = re.compile(r"(^.*?)(\n###?\s+)", re.MULTILINE | re.DOTALL) + + def get_element_template_path(self, element_type: str) -> str: + raise NotImplementedError(f"get_element_template_path() not implemented (element was {element_type}).") + + def get_element_md_path(self, element_type: str) -> str: + raise NotImplementedError(f"get_element_md_path() not implemented (element was {element_type}).") + + # Returns before_properties and after_properties if needed + # Returned tuple would be: (new_before_properties, after_properties) where each can be None, indicating + # we don't want to change them + def element_page_hook(self, element_type:str, doc:str, before_properties: str, after_properties: str) -> tuple[str, str]: + return (None, None) + + # Generate element doc pages for that category + def generate_pages(self, category: str, md_path: str, md_template_path: str) -> None: + def generate_element_doc(element_type: str, element_desc: Dict): + """ + Returns the entry for the Table of Contents that is inserted + in the global Visual Elements or Core Elements doc page. + """ + template_doc_path = self.get_element_template_path(element_type) + with open(template_doc_path, "r") as template_doc_file: + element_documentation = template_doc_file.read() + # Retrieve first paragraph from element documentation + match = ElementsGenerator.FIRST_PARA_RE.match(element_documentation) + if not match: + raise ValueError( + f"Couldn't locate first paragraph in documentation for element '{element_type}'" + ) + first_documentation_paragraph = match.group(1) + + # Build properties table + properties_table = """ +## Properties\n\n + + + + + + + + + + +""" + STAR = "(★)" + default_property_name = element_desc[__class__.DEFAULT_PROPERTY] + for property in element_desc[__class__.PROPERTIES]: + name = property[__class__.NAME] + type = property["type"] + default_value = property.get("default_value", None) + doc = property.get("doc", None) + if not default_value: + default_value = "Required" if property.get("required", False) else "" + full_name = f"]+>', '', name)}\">" + if name == default_property_name: + full_name += f"{name}{STAR}" + else: + full_name += f"{name}" + properties_table += ( + "\n" + + f"\n" + + f"\n" + + f"\n" + + f"\n" + + "\n" + ) + properties_table += " \n
NameTypeDefaultDescription
{full_name}{type}{default_value}

{doc}

\n\n" + if default_property_name: + properties_table += ( + f'

{STAR}' + + f'' + + f"{default_property_name}" + + " is the default property for this visual element.

\n" + ) + + # Insert title and properties in element documentation + match = ElementsGenerator.FIRST_HEADER2_RE.match(element_documentation) + if not match: + raise ValueError( + f"Couldn't locate first header2 in documentation for element '{element_type}'" + ) + before_properties = match.group(1) + after_properties = match.group(2) + element_documentation[match.end() :] + + # Process element hook + hook_values = self.element_page_hook(element_type, element_documentation, before_properties, after_properties) + if hook_values[0]: + before_properties = hook_values[0] + if hook_values[1]: + after_properties = hook_values[1] + + with open(self.get_element_md_path(element_type), "w") as md_file: + md_file.write( + "---\nhide:\n - navigation\n---\n\n" + + f"# {element_type}\n\n" + + before_properties + + properties_table + + after_properties + ) + e = element_type # Shortcut + d = self.get_doc_dir() + return ( + f'\n' + + f"
{e}
\n" + + f'\n' + + f'\n' + + f'\n' + + f'\n' + + f"

{first_documentation_paragraph}

\n" + + "
\n" + ) + # If you want a simple list, use + # f"
  • {e}: {first_documentation_paragraph}
  • \n" + # The toc header and footer must then be "" and "" respectively. + + md_template = "" + with open(md_template_path) as template_file: + md_template = template_file.read() + if not md_template: + raise FileNotFoundError(f"FATAL - Could not read {md_template_path} markdown template") + toc = '
    \n' + for element_type in self.categories[category]: + toc += generate_element_doc(element_type, self.elements[element_type]) + toc += "
    \n" + with open(md_path, "w") as md_file: + md_file.write(md_template.replace("[TOC]", toc)) diff --git a/tools/_setup_generation/setup.py b/tools/_setup_generation/setup.py index 8d1adaaac..61c994215 100644 --- a/tools/_setup_generation/setup.py +++ b/tools/_setup_generation/setup.py @@ -104,6 +104,7 @@ def setup(self, setup: Setup): from .step_viselements import VisElementsStep +from .step_corelements import CoreElementsStep from .step_refman import RefManStep from .step_rest_refman import RestRefManStep from .step_gui_ext_refman import GuiExtRefManStep @@ -115,6 +116,7 @@ def run_setup(root_dir: str, steps: List[SetupStep] = None): if steps is None: steps = [ VisElementsStep(), + CoreElementsStep(), RefManStep(), RestRefManStep(), GuiExtRefManStep(), diff --git a/tools/_setup_generation/step_corelements.py b/tools/_setup_generation/step_corelements.py new file mode 100644 index 000000000..ebc857bb6 --- /dev/null +++ b/tools/_setup_generation/step_corelements.py @@ -0,0 +1,47 @@ +# ################################################################################ +# Taipy GUI Core Elements documentation. +# +# This includes the update of the Table of Contents of the controls +# document pages. +# +# For each element, this script combines its property list and +# documentation (located in [CORELEMENTS_SRC_PATH]), and generates full +# Markdown files in [CORELEMENTS_DIR_PATH]. All these files ultimately get +# integrated in the global dos set. +# +# The template documentation files [CORELEMENTS_SRC_PATH]/[controls|blocks].md_template +# are also completed with generated table of contents. +# ################################################################################ +from .setup import Setup +from .elmnts_generator import ElementsGenerator +import os + +class CoreElementsStep(ElementsGenerator): + + def get_id(self) -> str: + return "corelements" + + def get_description(self) -> str: + return "Extraction of the Core elements documentation" + + def get_doc_dir(self) -> str: + return "corelements" + + def enter(self, setup: Setup): + self.CORELEMENTS_DIR_PATH = setup.manuals_dir + "/gui/corelements" + self.template_path = self.CORELEMENTS_DIR_PATH + "/index.md_template" + if not os.access(self.template_path, os.R_OK): + raise FileNotFoundError( + f"FATAL - Could not read {self.template_path} markdown template" + ) + self.load_elements(setup.root_dir + "/taipy/gui_core/viselements.json", + ["controls"]) + + def get_element_template_path(self, element_type: str) -> str: + return f"{self.CORELEMENTS_DIR_PATH}/{element_type}.md_template" + + def get_element_md_path(self, element_type: str) -> str: + return f"{self.CORELEMENTS_DIR_PATH}/{element_type}.md" + + def setup(self, setup: Setup) -> None: + self.generate_pages("controls", os.path.join(self.CORELEMENTS_DIR_PATH, "index.md"), self.template_path) diff --git a/tools/_setup_generation/step_refman.py b/tools/_setup_generation/step_refman.py index 4032a36a5..53691e740 100644 --- a/tools/_setup_generation/step_refman.py +++ b/tools/_setup_generation/step_refman.py @@ -21,6 +21,7 @@ class RefManStep(SetupStep): "taipy.config", "taipy.core", "taipy.gui", + "taipy.gui_core", "taipy.rest", "taipy.auth", "taipy.enterprise", @@ -95,7 +96,7 @@ class RefManStep(SetupStep): ("taipy.config.common.scope.Scope", "taipy.core.config"), ("taipy.config.common.frequency.Frequency", "taipy.core.config"), ("taipy.config.unique_section.*", "taipy.config"), - ("taipy.config.exceptions.exceptions.ConfigurationIssueError", "taipy.config.exceptions"), + #("taipy.config.exceptions.exceptions.ConfigurationIssueError", "taipy.config.exceptions"), # Rest ("taipy.rest.rest.Rest", "taipy.rest"), # Auth @@ -145,6 +146,7 @@ def generate_refman_pages(self, setup: Setup) -> None: REMOVE_LINE_SKIPS_RE = re.compile(r"\s*\n\s*", re.MULTILINE) os.mkdir(self.REFERENCE_DIR_PATH) + loaded_modules = set() # Entries: # full_entry_name -> @@ -157,6 +159,9 @@ def generate_refman_pages(self, setup: Setup) -> None: module_doc = {} def read_module(module): + if module in loaded_modules: + return + loaded_modules.add(module) if not module.__name__.startswith(Setup.ROOT_PACKAGE): return for entry in dir(module): diff --git a/tools/_setup_generation/step_viselements.py b/tools/_setup_generation/step_viselements.py index 459dd35fa..e2458d0a0 100644 --- a/tools/_setup_generation/step_viselements.py +++ b/tools/_setup_generation/step_viselements.py @@ -12,274 +12,85 @@ # The skeleton documentation files [GUI_DOC_PATH]/[controls|blocks].md_template # are also completed with generated table of contents. # ################################################################################ -import json -from typing import Dict, List, Optional -from .setup import Setup, SetupStep +from .setup import Setup +from .elmnts_generator import ElementsGenerator import os import re -class VisElementsStep(SetupStep): - DEFAULT_PROPERTY = "default_property" - PROPERTIES = "properties" - NAME = "name" - INHERITS = "inherits" - +class VisElementsStep(ElementsGenerator): def get_id(self) -> str: return "viselements" def get_description(self) -> str: return "Extraction of the visual elements documentation" + def get_doc_dir(self) -> str: + return "viselements" + def enter(self, setup: Setup): - self.VISELEMENTS_SRC_PATH = setup.root_dir + "/gui/doc" - self.GUI_DOC_PATH = setup.manuals_dir + "/gui/" - self.VISELEMENTS_DIR_PATH = self.GUI_DOC_PATH + "/viselements" - self.controls_md_template_path = self.GUI_DOC_PATH + "/controls.md_template" - if not os.access(self.controls_md_template_path, os.R_OK): + self.GUI_DIR_PATH = setup.manuals_dir + "/gui" + self.VISELEMENTS_DIR_PATH = self.GUI_DIR_PATH + "/viselements" + self.controls_template_path = self.GUI_DIR_PATH + "/controls.md_template" + if not os.access(self.controls_template_path, os.R_OK): raise FileNotFoundError( - f"FATAL - Could not read {self.controls_md_template_path} markdown template" + f"FATAL - Could not read {self.controls_template_path} Markdown template" ) - self.blocks_md_template_path = self.GUI_DOC_PATH + "/blocks.md_template" - if not os.access(self.blocks_md_template_path, os.R_OK): + self.blocks_template_path = self.GUI_DIR_PATH + "/blocks.md_template" + if not os.access(self.blocks_template_path, os.R_OK): raise FileNotFoundError( - f"FATAL - Could not read {self.blocks_md_template_path} markdown template" + f"FATAL - Could not read {self.blocks_template_path} Markdown template" ) self.charts_home_html_path = self.VISELEMENTS_DIR_PATH + "/charts/home.html_fragment" if not os.access(self.charts_home_html_path, os.R_OK): raise FileNotFoundError( f"FATAL - Could not read {self.charts_home_html_path} html fragment" ) - viselements_json_path = self.VISELEMENTS_SRC_PATH + "/viselements.json" - with open(viselements_json_path) as viselements_json_file: - self.viselements = json.load(viselements_json_file) - # Test validity of visual elements doc and resolve inheritance - self.controls = self.viselements["controls"] - self.blocks = self.viselements["blocks"] - undocumented = self.viselements["undocumented"] - self.all_elements = {} - for element in self.controls+self.blocks+undocumented: - element_type = element[0] - if element_type in self.all_elements: - raise ValueError( - f"FATAL - Duplicate element type '{element_type}' in {viselements_json_path}" - ) - element_desc = element[1] - if not __class__.PROPERTIES in element_desc and not __class__.INHERITS in element_desc: - raise ValueError( - f"FATAL - No properties in element type '{element_type}' in {viselements_json_path}" - ) - self.all_elements[element_type] = element_desc - # Find default property for all element types - for element_type, element_desc in self.all_elements.items(): - default_property = None - if properties := element_desc.get(__class__.PROPERTIES, None): - for property in properties: - if __class__.DEFAULT_PROPERTY in property: - if property[__class__.DEFAULT_PROPERTY]: - default_property = property[__class__.NAME] - del property[__class__.DEFAULT_PROPERTY] - element_desc[__class__.DEFAULT_PROPERTY] = default_property - # Resolve inheritance - def merge(element_desc, parent_element_desc, default_property) -> Optional[str]: - element_properties = element_desc.get(__class__.PROPERTIES, []) - element_property_names = [p[__class__.NAME] for p in element_properties] - for property in parent_element_desc.get(__class__.PROPERTIES, []): - property_name = property[__class__.NAME] - if property_name in element_property_names: - element_property = element_properties[element_property_names.index(property_name)] - for n in ["type", "default_value", "doc"]: - if not n in element_property and n in property: - element_property[n] = property[n] - else: - element_property_names.append(property_name) - element_properties.append(property) - element_desc[__class__.PROPERTIES] = element_properties - if not default_property and parent_element_desc.get(__class__.DEFAULT_PROPERTY, False): - default_property = parent_element_desc[__class__.DEFAULT_PROPERTY] - return default_property + self.load_elements(setup.root_dir + "/taipy/gui/viselements.json", + ["controls", "blocks"]) - for element_type, element_desc in self.all_elements.items(): - if parent_types := element_desc.get(__class__.INHERITS, None): - del element_desc[__class__.INHERITS] - default_property = element_desc[__class__.DEFAULT_PROPERTY] - for parent_type in parent_types: - parent_desc = self.all_elements[parent_type] - default_property = merge(element_desc, parent_desc, default_property) - element_desc[__class__.DEFAULT_PROPERTY] = default_property - # Check that documented elements have a default property and a doc file, - # and that their properties have the mandatory settings. - for element in self.controls+self.blocks: - element_type = element[0] - element_desc = element[1] - if not __class__.DEFAULT_PROPERTY in element_desc: - raise ValueError( - f"FATAL - No default property for element type '{element_type}' in {viselements_json_path}" - ) - if not __class__.PROPERTIES in element_desc: - raise ValueError( - f"FATAL - No properties for element type '{element_type}' in {viselements_json_path}" - ) - doc_path = self.VISELEMENTS_SRC_PATH + "/" + element_type + ".md" - if not os.access(doc_path, os.R_OK): - raise FileNotFoundError( - f"FATAL - Could not find doc for element type '{element_type}' in {self.VISELEMENTS_SRC_PATH}" - ) - # Check completeness - for property in element_desc[__class__.PROPERTIES]: - for n in ["type", "doc"]: - if not n in property: - raise ValueError( - f"FATAL - No value for '{n}' in the '{property[__class__.NAME]}' properties of element type '{element_type}' in {viselements_json_path}" - ) + + def get_element_template_path(self, element_type: str) -> str: + return f"{self.VISELEMENTS_DIR_PATH}/{element_type}.md_template" + + def get_element_md_path(self, element_type: str) -> str: + return f"{self.VISELEMENTS_DIR_PATH}/{element_type}.md" def setup(self, setup: Setup) -> None: # Create VISELEMS_DIR_PATH directory if necessary if not os.path.exists(self.VISELEMENTS_DIR_PATH): os.mkdir(self.VISELEMENTS_DIR_PATH) - - FIRST_PARA_RE = re.compile(r"(^.*?)(:?\n\n)", re.MULTILINE | re.DOTALL) - FIRST_HEADER1_RE = re.compile(r"(^.*?)(\n#\s+)", re.MULTILINE | re.DOTALL) - # Find first level 2 or 3 header - FIRST_HEADER2_RE = re.compile(r"(^.*?)(\n###?\s+)", re.MULTILINE | re.DOTALL) - - def generate_element_doc(element_type: str, element_desc: Dict): - """ - Returns the entry for the Table of Contents that is inserted - in the global Visual Elements doc page. - """ - doc_path = self.VISELEMENTS_SRC_PATH + "/" + element_type + ".md" - with open(doc_path, "r") as doc_file: - element_documentation = doc_file.read() - # Retrieve first paragraph from element documentation - match = FIRST_PARA_RE.match(element_documentation) + self.generate_pages("controls", self.get_element_md_path("controls"), self.controls_template_path) + self.generate_pages("blocks", self.get_element_md_path("blocks"), self.blocks_template_path) + + def element_page_hook(self, element_type:str, element_documentation: str, before: str, after: str) -> tuple[str, str]: + # Special case for charts: we want to insert the chart gallery that is stored in the + # file whose path is in self.charts_home_html_path + # This should be inserted before the first level 1 header + if element_type == "chart": + with open(self.charts_home_html_path, "r") as html_fragment_file: + chart_gallery = html_fragment_file.read() + # The chart_gallery begins with a comment where all sub-sections + # are listed. + SECTIONS_RE = re.compile(r"^(?:\s*)", re.MULTILINE | re.DOTALL) + match = SECTIONS_RE.match(chart_gallery) if not match: raise ValueError( - f"Couldn't locate first paragraph in documentation for element '{element_type}'" + f"{self.charts_home_html_path} should begin with an HTML comment that lists the chart types" ) - first_documentation_paragraph = match.group(1) - - # Build properties table - properties_table = """ -## Properties\n\n - - - - - - - - - - -""" - STAR = "(★)" - default_property_name = element_desc[__class__.DEFAULT_PROPERTY] - for property in element_desc[__class__.PROPERTIES]: - name = property[__class__.NAME] - type = property["type"] - default_value = property.get("default_value", None) - doc = property.get("doc", None) - if not default_value: - default_value = "Required" if property.get("required", False) else "" - full_name = f"]+>', '', name)}\">" - if name == default_property_name: - full_name += f"{name}{STAR}" - else: - full_name += f"{name}" - properties_table += ( - "\n" - + f"\n" - + f"\n" - + f"\n" - + f"\n" - + "\n" - ) - properties_table += " \n
    NameTypeDefaultDescription
    {full_name}{type}{default_value}

    {doc}

    \n\n" - if default_property_name: - properties_table += ( - f'

    {STAR}' - + f'' - + f"{default_property_name}" - + " is the default property for this visual element.

    \n" - ) - - # Insert title and properties in element documentation - match = FIRST_HEADER2_RE.match(element_documentation) + chart_gallery = "\n" + chart_gallery[match.end() :] + SECTION_RE = re.compile(r"^([\w-]+):(.*)$") + chart_sections = "" + for line in match.group(1).splitlines(): + match = SECTION_RE.match(line) + if match: + chart_sections += f"- [{match.group(2)}](charts/{match.group(1)}.md)\n" + + match = ElementsGenerator.FIRST_HEADER1_RE.match(element_documentation) if not match: raise ValueError( - f"Couldn't locate first header2 in documentation for element '{element_type}'" + f"Couldn't locate first header1 in documentation for element 'chart'" ) - before_properties = match.group(1) - after_properties = match.group(2) + element_documentation[match.end() :] - output_path = os.path.join(self.VISELEMENTS_DIR_PATH, element_type + ".md") - - # Special case for charts: we want to insert the chart gallery that is stored in the - # file whose path is in self.charts_home_html_path - # This should be inserted before the first level 1 header - if element_type == "chart": - with open(self.charts_home_html_path, "r") as html_fragment_file: - chart_gallery = html_fragment_file.read() - # The chart_gallery begins with a comment where all sub-sections - # are listed. - SECTIONS_RE = re.compile(r"^(?:\s*)", re.MULTILINE | re.DOTALL) - match = SECTIONS_RE.match(chart_gallery) - if not match: - raise ValueError( - f"{self.charts_home_html_path} should begin with an HTML comment that lists the chart types" - ) - chart_gallery = "\n" + chart_gallery[match.end() :] - SECTION_RE = re.compile(r"^([\w-]+):(.*)$") - chart_sections = "" - for line in match.group(1).splitlines(): - match = SECTION_RE.match(line) - if match: - chart_sections += f"- [{match.group(2)}](charts/{match.group(1)}.md)\n" - - match = FIRST_HEADER1_RE.match(element_documentation) - if not match: - raise ValueError( - f"Couldn't locate first header1 in documentation for element 'chart'" - ) - before_properties = match.group(1) + chart_gallery + match.group(2) + before_properties[match.end() :] - after_properties += chart_sections - - with open(output_path, "w") as output_file: - output_file.write( - "---\nhide:\n - navigation\n---\n\n" - + f"# {element_type}\n\n" - + before_properties - + properties_table - + after_properties - ) - e = element_type # Shortcut - return ( - f'\n' - + f"
    {e}
    \n" - + f'\n' - + f'\n' - + f'\n' - + f'\n' - + f"

    {first_documentation_paragraph}

    \n" - + "
    \n" - ) - # If you want a simple list, use - # f"
  • {e}: {first_documentation_paragraph}
  • \n" - # The toc header and footer must then be "" and "" respectively. - - # Generate element doc pages - def generate_doc_page(category: str, elements: List, md_template_path: str): - md_template = "" - with open(md_template_path) as template_file: - md_template = template_file.read() - if not md_template: - raise FileNotFoundError(f"FATAL - Could not read {md_template_path} markdown template") - toc = '
    \n' - for element_type, element_desc in elements: - toc += generate_element_doc(element_type, element_desc) - toc += "
    \n" - with open(os.path.join(self.GUI_DOC_PATH, f"{category}.md"), "w") as file: - file.write(md_template.replace("[TOC]", toc)) + return (match.group(1) + chart_gallery + match.group(2) + before[match.end() :], after + chart_sections) - generate_doc_page("controls", self.controls, self.controls_md_template_path) - generate_doc_page("blocks", self.blocks, self.blocks_md_template_path) + return super().element_page_hook(element_type, element_documentation, before, after) diff --git a/tools/fetch_source_files.py b/tools/fetch_source_files.py index 35322a853..0e6f1e6f5 100644 --- a/tools/fetch_source_files.py +++ b/tools/fetch_source_files.py @@ -13,7 +13,7 @@ # Where all the code from all directories/repositories is copied DEST_DIR_NAME = "taipy" -REPOS = ["config", "core", "gui", "getting-started", "getting-started-core", "getting-started-gui", "rest"] +REPOS = ["config", "core", "gui", "rest", "taipy", "getting-started", "getting-started-core", "getting-started-gui"] PRIVATE_REPOS = ["auth", "enterprise"] OPTIONAL_PACKAGES = { @@ -26,7 +26,7 @@ mkdocs_yml_version = read_doc_version_from_mkdocs_yml_template_file(ROOT_DIR) # Gather version information for each repository -repo_defs = {repo: {"version": "local", "tag": None} for repo in REPOS + PRIVATE_REPOS} +repo_defs = {repo if repo == "taipy" else f"taipy-{repo}": {"version": "local", "tag": None} for repo in REPOS + PRIVATE_REPOS} CATCH_VERSION_RE = re.compile(r"(^\d+\.\d+?)(?:(\.\d+)(\..*)?)?|develop|local$") for version in args.version: repo = None @@ -37,8 +37,8 @@ try: colon = version.index(":") repo = version[:colon] - if repo.startswith("taipy-"): - repo = repo[6:] + if not repo.startswith("taipy"): + repo = f"taipy-{repo}" version = version[colon + 1:] except ValueError as e: pass @@ -76,17 +76,17 @@ github_token = os.environ.get("GITHUB_TOKEN", "") if github_token: github_token += "@" -github_root = f"https://{github_token}github.com/Avaiga/taipy-" +github_root = f"https://{github_token}github.com/Avaiga/" for repo in repo_defs.keys(): version = repo_defs[repo]["version"] if version == "local": - repo_path = os.path.join(TOP_DIR, "taipy-" + repo) + repo_path = os.path.join(TOP_DIR, repo) repo_defs[repo]["path"] = repo_path if not os.path.isdir(repo_path): if repo in PRIVATE_REPOS: repo_defs[repo]["skip"] = True else: - raise IOError(f"Repository 'taipy-{repo}' must be cloned in \"{TOP_DIR}\".") + raise IOError(f"Repository '{repo}' must be cloned in \"{TOP_DIR}\".") elif version == "develop": with GitContext(repo, PRIVATE_REPOS): cmd = subprocess.run(f"{git_path} ls-remote -q -h {github_root}{repo}.git", shell=True, capture_output=True, @@ -108,13 +108,13 @@ else: raise SystemError(f"Couldn't query branches from {github_root}{repo}.") if not f"release/{version}\n" in cmd.stdout: - raise ValueError(f"No branch 'release/{version}' in repository 'taipy-{repo}'.") + raise ValueError(f"No branch 'release/{version}' in repository '{repo}'.") tag = repo_defs[repo]["tag"] if tag: cmd = subprocess.run(f"{git_path} ls-remote -t --refs {github_root}{repo}.git", shell=True, capture_output=True, text=True) if not f"refs/tags/{tag}\n" in cmd.stdout: - raise ValueError(f"No tag '{tag}' in repository 'taipy-{repo}'.") + raise ValueError(f"No tag '{tag}' in repository '{repo}'.") if args.check: print(f"Fetch should perform properly with the following settings:") @@ -125,7 +125,7 @@ tag = repo_defs[repo]["tag"] if tag: version += f" tag:{tag}" - print(f"- taipy-{repo} {version}") + print(f"- {repo} {version}") exit(0) DEST_DIR = os.path.join(ROOT_DIR, DEST_DIR_NAME) @@ -176,8 +176,8 @@ def move_files(repo: str, src_path: str): else: pipfile_packages[package] = {version: [repo]} # Copy relevant files for doc generation - if repo.startswith("getting-started"): - gs_dir = os.path.join(ROOT_DIR, "docs", "getting_started", repo) + if repo.startswith("taipy-getting-started"): + gs_dir = os.path.join(ROOT_DIR, "docs", "getting_started", repo[6:]) # safe_rmtree(os.path.join(gs_dir, "src")) for step_dir in [step_dir for step_dir in os.listdir(gs_dir) if step_dir.startswith("step_") and os.path.isdir(os.path.join(gs_dir, step_dir))]: @@ -189,7 +189,7 @@ def move_files(repo: str, src_path: str): shutil.copytree(os.path.join(src_path, "src"), os.path.join(gs_dir, "src")) shutil.copy(os.path.join(src_path, "index.md"), os.path.join(gs_dir, "index.md")) saved_dir = os.getcwd() - os.chdir(os.path.join(ROOT_DIR, "docs", "getting_started", repo)) + os.chdir(os.path.join(ROOT_DIR, "docs", "getting_started", repo[6:])) subprocess.run(f"python {os.path.join(src_path, 'generate_notebook.py')}", shell=True, capture_output=True, @@ -198,36 +198,56 @@ def move_files(repo: str, src_path: str): else: tmp_dir = os.path.join(ROOT_DIR, f"{repo}.tmp") safe_rmtree(tmp_dir) - gui_dir = os.path.join(ROOT_DIR, f"gui") if repo == "gui" else None - if gui_dir and os.path.isdir(gui_dir): + gui_dir = os.path.join(ROOT_DIR, f"gui") if repo == "taipy-gui" else None + if repo == "taipy-gui" and os.path.isdir(gui_dir): if os.path.isdir(os.path.join(gui_dir, "node_modules")): shutil.move(os.path.join(gui_dir, "node_modules"), os.path.join(ROOT_DIR, f"gui_node_modules")) shutil.rmtree(gui_dir) try: - def keep_py_files(dir, filenames): - return [name for name in filenames if not os.path.isdir(os.path.join(dir, name)) and not ( - name.endswith('.py') or name.endswith('.pyi') or name.endswith('.json') or name.endswith('.ipynb'))] + def copy_source(src_path: str, repo: str): + def copy(item: str, src: str, dst: str, rel_path: str): + full_src = os.path.join(src, item) + full_dst = os.path.join(dst, item) + if os.path.isdir(full_src): + if item in ["__pycache__", "node_modules"]: + return + if not os.path.isdir(full_dst): + os.makedirs(full_dst) + rel_path = f"{rel_path}/{item}" + for sub_item in os.listdir(full_src): + copy(sub_item, full_src, full_dst, rel_path) + elif any(item.endswith(ext) for ext in [".py", ".pyi", ".json", ".ipynb"]): + if os.path.isfile(full_dst): # File exists - compare + with open(full_src, "r") as f: + src = f.read() + with open(full_dst, "r") as f: + dst = f.read() + if src != dst: + raise FileExistsError(f"File {rel_path}/{item} already exists and is different (copying repository {repo})") + else: + shutil.copy(full_src, full_dst) + dest_path = os.path.join(ROOT_DIR, "taipy") + if not os.path.exists(dest_path): + os.makedirs(dest_path) + src_path = os.path.join(src_path, "src", "taipy") + for item in os.listdir(src_path): + copy(item, src_path, dest_path, "") + copy_source(src_path, repo) - shutil.copytree(os.path.join(src_path, "src", "taipy"), tmp_dir, ignore=keep_py_files) - entries = os.listdir(tmp_dir) - for entry in entries: - try: - if entry != "__pycache__": - shutil.move(os.path.join(tmp_dir, entry), DEST_DIR) - except shutil.Error as e: - if entry != "__init__.py": # Top-most __entry__.py gets overwritten over and over - raise e if gui_dir: - os.mkdir(gui_dir) + if not os.path.isdir(gui_dir): + os.mkdir(gui_dir) src_gui_dir = os.path.join(src_path, "gui") - shutil.copytree(os.path.join(src_gui_dir, "doc"), os.path.join(gui_dir, "doc")) shutil.copytree(os.path.join(src_gui_dir, "src"), os.path.join(gui_dir, "src")) for f in [f for f in os.listdir(src_gui_dir) if f.endswith(".md") or f.endswith(".json")]: shutil.copy(os.path.join(src_gui_dir, f), os.path.join(gui_dir, f)) if os.path.isdir(os.path.join(ROOT_DIR, "gui_node_modules")): shutil.move(os.path.join(ROOT_DIR, "gui_node_modules"), os.path.join(gui_dir, "node_modules")) finally: + pass + """ shutil.rmtree(tmp_dir) + """ for repo in repo_defs.keys(): diff --git a/tools/postprocess.py b/tools/postprocess.py index ceec608ca..4bb0e1bdc 100644 --- a/tools/postprocess.py +++ b/tools/postprocess.py @@ -85,8 +85,8 @@ def remove_dummy_h3(content: str, ids: Dict[str, str]) -> str: def on_post_build(env): "Post-build actions for Taipy documentation" - site_dir = env.conf["site_dir"] log = logging.getLogger("mkdocs") + site_dir = env.conf["site_dir"] xrefs = {} if os.path.exists(site_dir + "/manuals/xrefs"): with open(site_dir + "/manuals/xrefs") as xrefs_file: @@ -103,8 +103,11 @@ def on_post_build(env): fixed_cross_refs = {} for root, _, file_list in os.walk(site_dir): for f in file_list: + # Remove the *_template files + if f.endswith("_template"): + os.remove(os.path.join(root, f)) # Post-process generated '.html' files - if f.endswith(".html"): + elif f.endswith(".html"): filename = os.path.join(root, f) file_was_changed = False with open(filename) as html_file: