diff --git a/tools/app_generator/app.py b/tools/app_generator/app.py index a4a204f97a..9dec9ae226 100644 --- a/tools/app_generator/app.py +++ b/tools/app_generator/app.py @@ -16,7 +16,6 @@ # Form libraries from flask_wtf import FlaskForm -from flask_bootstrap import Bootstrap from wtforms import ( StringField, RadioField, @@ -37,7 +36,7 @@ # Translations from flask_babel import Babel -from flask_babel import gettext, lazy_gettext +from flask_babel import lazy_gettext as _ from flask import redirect, request, make_response # Language swap by redirecting @@ -51,12 +50,11 @@ from secrets import token_urlsafe #### GLOBAL VARIABLES -YOLOGEN_VERSION = "0.9.2.1" +YOLOGEN_VERSION = "0.10" GENERATOR_DICT = {"GENERATOR_VERSION": YOLOGEN_VERSION} #### Create FLASK and Jinja Environments app = Flask(__name__) -Bootstrap(app) app.config["SECRET_KEY"] = token_urlsafe(16) # Necessary for the form CORS cors = CORS(app) @@ -67,7 +65,7 @@ babel = Babel() -LANGUAGES = {"en": gettext("English"), "fr": gettext("French")} +LANGUAGES = {"en": _("English"), "fr": _("French")} @app.context_processor @@ -126,7 +124,7 @@ def markdown_file_to_html_string(file): # Use it in the HTML with {{ form_field(main_form.generator_language) }} class Translations(FlaskForm): generator_language = SelectField( - gettext("Select language"), + _("Select language"), choices=[("none", "")] + [language for language in LANGUAGES.items()], default=["en"], id="selectLanguage", @@ -136,8 +134,8 @@ class Translations(FlaskForm): class GeneralInfos(FlaskForm): app_id = StringField( - Markup(lazy_gettext("Application identifier (id)")), - description=lazy_gettext("Small caps and without spaces"), + Markup(_("Application identifier (id)")), + description=_("Small caps and without spaces"), validators=[DataRequired(), Regexp("[a-z_1-9]+.*(? \ -Do not give the software name at the beginning, as it will be integrated an 'Overview' subpart""" + _( + """doc/DESCRIPTION.md: A comprehensive presentation of the app, possibly listing the main features, possible warnings and specific details on its functioning in Yunohost (e.g. warning about integration issues).""" ) ), validators=[Optional()], render_kw={ - "class": "form-control", "spellcheck": "false", }, ) pre_install = TextAreaField( - lazy_gettext("Type the PRE_INSTALL.md file content"), + _("doc/PRE_INSTALL.md: important info to be shown to the admin before installing the app"), + description=_("Leave empty if not relevant"), validators=[Optional()], render_kw={ - "class": "form-control", "spellcheck": "false", }, ) post_install = TextAreaField( - lazy_gettext("Type the POST_INSTALL.md file content"), + _("doc/POST_INSTALL.md: important info to be shown to the admin after installing the app"), + description=_("Leave empty if not relevant"), validators=[Optional()], render_kw={ - "class": "form-control", "spellcheck": "false", }, ) pre_upgrade = TextAreaField( - lazy_gettext("Type the PRE_UPGRADE.md file content"), + _("doc/PRE_UPGRADE.md: important info to be shown to the admin before upgrading the app"), + description=_("Leave empty if not relevant"), validators=[Optional()], render_kw={ - "class": "form-control", "spellcheck": "false", }, ) post_upgrade = TextAreaField( - lazy_gettext("Type the POST_UPGRADE.md file content"), + _("doc/POST_UPGRADE.md: important info to be shown to the admin after upgrading the app"), + description=_("Leave empty if not relevant"), validators=[Optional()], render_kw={ - "class": "form-control", "spellcheck": "false", }, ) admin = TextAreaField( - lazy_gettext("Type the ADMIN.md file content"), + _("doc/ADMIN.md: general tips on how to administrate this app"), + description=_("Leave empty if not relevant"), validators=[Optional()], render_kw={ - "class": "form-control", "spellcheck": "false", }, ) @@ -584,20 +580,20 @@ class Documentation(FlaskForm): class MoreAdvanced(FlaskForm): enable_change_url = BooleanField( - lazy_gettext("Handle app install URL change (change_url script)"), + _("Handle app install URL change (change_url script)"), default=True, render_kw={ - "title": lazy_gettext( + "title": _( "Should changing the app URL be allowed ? (change_url change)" ) }, ) use_logrotate = BooleanField( - lazy_gettext("Use logrotate for the app logs"), + _("Use logrotate for the app logs"), default=True, render_kw={ - "title": lazy_gettext( + "title": _( "If the app generates logs, this option permit to handle their archival. Recommended." ) }, @@ -605,23 +601,23 @@ class MoreAdvanced(FlaskForm): # TODO : specify custom log file # custom_log_file = "/var/log/$app/$app.log" "/var/log/nginx/${domain}-error.log" use_fail2ban = BooleanField( - lazy_gettext( + _( "Protect the application against brute force attacks (via fail2ban)" ), default=False, render_kw={ - "title": lazy_gettext( + "title": _( "If the app generates failed connexions logs, this option allows to automatically banish the related IP after a certain number of failed password tries. Recommended." ) }, ) use_cron = BooleanField( - lazy_gettext("Add a CRON task for this application"), - description=lazy_gettext("Corresponds to some app periodic operations"), + _("Add a CRON task for this application"), + description=_("Corresponds to some app periodic operations"), default=False, ) cron_config_file = TextAreaField( - lazy_gettext("Type the CRON file content"), + _("Type the CRON file content"), validators=[Optional()], render_kw={ "class": "form-control", @@ -630,13 +626,13 @@ class MoreAdvanced(FlaskForm): ) fail2ban_regex = StringField( - lazy_gettext("Regular expression for fail2ban"), + _("Regular expression for fail2ban"), # Regex to match into the log for a failed login validators=[Optional()], render_kw={ - "placeholder": lazy_gettext("A regular expression"), + "placeholder": _("A regular expression"), "class": "form-control", - "title": lazy_gettext( + "title": _( "Regular expression to check in the log file to activate failban (search for a line that indicates a credentials error)." ), }, @@ -660,25 +656,25 @@ class Meta: csrf = False generator_mode = SelectField( - lazy_gettext("Generator mode"), - description=lazy_gettext( + _("Generator mode"), + description=_( "In tutorial version, the generated app will contain additionnal comments to ease the understanding. In steamlined version, the generated app will only contain the necessary minimum." ), choices=[ - ("simple", lazy_gettext("Streamlined version")), - ("tutorial", lazy_gettext("Tutorial version")), + ("simple", _("Streamlined version")), + ("tutorial", _("Tutorial version")), ], default="true", validators=[DataRequired()], ) - submit_preview = SubmitField(lazy_gettext("Previsualise")) - submit_download = SubmitField(lazy_gettext("Download the .zip")) + submit_preview = SubmitField(_("Previsualise")) + submit_download = SubmitField(_("Download the .zip")) submit_demo = SubmitField( - lazy_gettext("Fill with demo values"), + _("Fill with demo values"), render_kw={ "onclick": "fillFormWithDefaultValues()", - "title": lazy_gettext( + "title": _( "Generate a complete and functionnal minimalistic app that you can iterate from" ), }, diff --git a/tools/app_generator/requirements.txt b/tools/app_generator/requirements.txt index 665d10bdf2..fa96e3911e 100644 --- a/tools/app_generator/requirements.txt +++ b/tools/app_generator/requirements.txt @@ -4,7 +4,6 @@ click==8.1.7 dominate==2.8.0 Flask==3.0.0 flask_babel~=4.0.0 -Flask-Bootstrap==3.3.7.1 Flask-Cors==4.0.0 Flask-Misaka==1.0.0 Flask-WTF==1.2.1 diff --git a/tools/app_generator/static/fetch_assets b/tools/app_generator/static/fetch_assets new file mode 100644 index 0000000000..b1a2689bd8 --- /dev/null +++ b/tools/app_generator/static/fetch_assets @@ -0,0 +1,6 @@ +# Download standalone tailwind to compile what we need +wget https://github.com/tailwindlabs/tailwindcss/releases/download/v3.3.3/tailwindcss-linux-x64 +chmod +x tailwindcss-linux-x64 +./tailwindcss-linux-x64 --input tailwind-local.css --output tailwind.css --minify + + diff --git a/tools/app_generator/static/stylesheet.css b/tools/app_generator/static/stylesheet.css deleted file mode 100644 index d21d076279..0000000000 --- a/tools/app_generator/static/stylesheet.css +++ /dev/null @@ -1,45 +0,0 @@ -.form-horizontal .form-group { - margin-left: 0; - margin-right: 0; -} - -h2 { - font-weight: bold; - font-size: 1.3em !important; -} - -.checkbox { - margin-bottom: 15px !important; -} - -.checkbox label { - font-weight: bold; -} - - - -.active, .collapse-button:hover { - background-color: #318ddc; -} - -.collapse-title:after { - content: '\002B'; - color: white; - font-weight: bold; - float: right; - margin-left: 5px; -} - -.expanded .collapse-title::after { - content: "\2212"; -} - -.collapsed { - padding: 0px 15px 0px 15px; -} - -.collapsible { - max-height: 0px; - overflow: hidden; - transition: max-height 0.2s ease-out; -} \ No newline at end of file diff --git a/tools/app_generator/static/tailwind-local.css b/tools/app_generator/static/tailwind-local.css new file mode 100644 index 0000000000..a81992f2cb --- /dev/null +++ b/tools/app_generator/static/tailwind-local.css @@ -0,0 +1,105 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer utilities { + + body { + @apply text-gray-800; + } + + h1 { + @apply text-2xl font-bold; + } + + h2 { + @apply text-xl font-bold; + } + + h3 { + @apply text-lg font-bold; + } + + .hide { + display: none; + } + + .btn { + @apply text-sm font-medium rounded-md px-4 py-2 transition bg-gray-200 m-1; + } + .btn-sm { + @apply text-xs font-medium rounded-md px-2 py-2 transition; + } + .btn-success { + @apply text-white bg-green-500 hover:bg-green-700; + } + .btn-primary { + @apply text-white bg-blue-500 hover:bg-blue-700; + } + .btn-link { + @apply bg-gray-400 hover:bg-gray-200; + } + .panel { + @apply block rounded-lg border border-gray-400 mb-2; + } + .panel-heading { + @apply text-white bg-blue-500 hover:bg-blue-700 p-2 font-bold; + } + .panel-body { + @apply p-2; + } + + .alert-info { + @apply text-blue-900 border-blue-900 bg-blue-200 rounded-lg p-4; + } + + .active, .collapse-button:hover { + background-color: #318ddc; + } + + .collapse-title:after { + content: '\002B'; + color: white; + font-weight: bold; + float: right; + margin-left: 5px; + } + + .expanded .collapse-title::after { + content: "\2212"; + } + + .collapsed { + padding: 0px 15px 0px 15px; + } + + .collapsible { + max-height: 0px; + overflow: hidden; + transition: max-height 0.2s ease-out; + } + + label { + @apply font-bold; + } + + input { + @apply rounded-lg; + } + + .form-group, .checkbox { + @apply px-2 py-4; + } + + .form-control { + @apply block w-full rounded-md border-gray-300; + } + + .help-block { + @apply text-gray-500 text-sm; + } + + .tip { + @apply italic p-2; + } +} diff --git a/tools/app_generator/static/tailwind.config.js b/tools/app_generator/static/tailwind.config.js new file mode 100644 index 0000000000..c5b0b2fc06 --- /dev/null +++ b/tools/app_generator/static/tailwind.config.js @@ -0,0 +1,17 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['../templates/*.html'], + theme: { + extend: {}, + }, + plugins: [ + require('@tailwindcss/forms'), + ], + safelist: [ + 'safelisted', + { + pattern: /^(text-[a-z]+-600|border-[a-z]+-400)$/, + }, + ] +} + diff --git a/tools/app_generator/static/yunohost-package.png b/tools/app_generator/static/yunohost-package.png new file mode 100644 index 0000000000..61434f40dc Binary files /dev/null and b/tools/app_generator/static/yunohost-package.png differ diff --git a/tools/app_generator/templates/base.html b/tools/app_generator/templates/base.html new file mode 100644 index 0000000000..a4597eae26 --- /dev/null +++ b/tools/app_generator/templates/base.html @@ -0,0 +1,17 @@ + + + + + {{ gettext("YunoHost app generator") }} + + + + + + +
+ {% block main -%} + {%- endblock main %} +
+ + diff --git a/tools/app_generator/templates/index.html b/tools/app_generator/templates/index.html index db8d8fe285..62e95eff3f 100644 --- a/tools/app_generator/templates/index.html +++ b/tools/app_generator/templates/index.html @@ -1,4 +1,4 @@ -{% import "bootstrap/wtf.html" as wtf %} +{% import "wtf.html" as wtf %} {% macro form_field(field, form_type="basic", @@ -19,64 +19,38 @@ {% endmacro %} -{% extends "bootstrap/base.html" %} +{% extends "base.html" %} -{% block title %} -YunoHost app generator -{% endblock %} - -{% block styles %} -{{super()}} - -{% endblock %} - - -{% block content %} -
-

{{ gettext("Yunohost application generation form") }}

-

Version: {{ generator_info['GENERATOR_VERSION'] }}

- - - - - - -
- {% for lang in AVAILABLE_LANGUAGES.items() %} - - {% endfor %} +{% block main %} +
+ YunoHost application logo +

+ {{ _("Yunohost application generation form") }} +

+

Version: {{ generator_info['GENERATOR_VERSION'] }}

+ +
+ {% for lang in AVAILABLE_LANGUAGES.items() %} + + {% endfor %} +
+
- {{ main_form.hidden_tag() }} -
+
{{ wtf.form_errors(main_form, hiddens="only") }}
{{ form_field(main_form.generator_mode) }} - -
+

{{ gettext("1/9 - General information") }}

@@ -91,12 +65,12 @@

{{ gettext("1/9 - General information") }

-
+
-

{{ gettext("2/9 - Informations about the application") }}

+

{{ gettext("2/9 - Upstream information") }}