diff --git a/.env.example b/.env.example index 998150c..63df700 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,10 @@ # When the environment is "development", DEBUG is set to True. SERVALA_ENVIRONMENT='development' +# Set to "False" to disable the beta testing banner at the top of every page. +# Defaults to "True". +SERVALA_SHOW_BETA_BANNER='True' + # Set SERVALA_PREVIOUS_SECRET_KEY when rotating to a new secret key in order to not expire all sessions and to remain able to read encrypted fields! # In order to retire the previous key, run the ``reencrypt_fields`` command. Once you drop the previous secret key from # the rotation, all sessions that still rely on that key will be invalidated (i.e., users will have to log in again). diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 78605f6..646849f 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -387,7 +387,8 @@ class OrganizationOrigin(ServalaModelMixin, models.Model): help_text=_( "If set, this billing entity will be used on new organizations with this origin." ), - null=True, blank=True, + null=True, + blank=True, ) limit_cloudproviders = models.ManyToManyField( to="CloudProvider", diff --git a/src/servala/frontend/context_processors.py b/src/servala/frontend/context_processors.py index 1ff2a14..78dc0a9 100644 --- a/src/servala/frontend/context_processors.py +++ b/src/servala/frontend/context_processors.py @@ -1,5 +1,12 @@ +from django.conf import settings + + def add_organizations(request): if not request.user.is_authenticated: return {"user_organizations": []} return {"user_organizations": request.user.organizations.all().order_by("name")} + + +def add_beta_banner(request): + return {"show_beta_banner": settings.SERVALA_SHOW_BETA_BANNER} diff --git a/src/servala/frontend/templates/frontend/base.html b/src/servala/frontend/templates/frontend/base.html index 7c6bc54..620cb6d 100644 --- a/src/servala/frontend/templates/frontend/base.html +++ b/src/servala/frontend/templates/frontend/base.html @@ -22,6 +22,7 @@
+ {% include 'includes/beta_banner.html' %} {% include 'includes/header.html' %}
diff --git a/src/servala/frontend/templates/includes/beta_banner.html b/src/servala/frontend/templates/includes/beta_banner.html new file mode 100644 index 0000000..04bd91d --- /dev/null +++ b/src/servala/frontend/templates/includes/beta_banner.html @@ -0,0 +1,13 @@ +{% if show_beta_banner %} +
+
+
+ BETA + The Servala Portal is currently in beta testing. Your feedback helps us improve! + +
+
+
+{% endif %} diff --git a/src/servala/settings.py b/src/servala/settings.py index eef44f2..f57ac0d 100644 --- a/src/servala/settings.py +++ b/src/servala/settings.py @@ -20,6 +20,7 @@ from servala.__about__ import __version__ as version SERVALA_ENVIRONMENT = os.environ.get("SERVALA_ENVIRONMENT", "development") DEBUG = SERVALA_ENVIRONMENT == "development" +SERVALA_SHOW_BETA_BANNER = os.environ.get("SERVALA_SHOW_BETA_BANNER", "True") == "True" SECRET_KEY = os.environ.get("SERVALA_SECRET_KEY") if previous_secret_key := os.environ.get("SERVALA_PREVIOUS_SECRET_KEY"): @@ -219,6 +220,7 @@ TEMPLATES = [ "django.contrib.messages.context_processors.messages", "django.template.context_processors.static", "servala.frontend.context_processors.add_organizations", + "servala.frontend.context_processors.add_beta_banner", ], "loaders": template_loaders, }, diff --git a/src/servala/static/css/servala.css b/src/servala/static/css/servala.css index 0ea8b28..cb7061a 100644 --- a/src/servala/static/css/servala.css +++ b/src/servala/static/css/servala.css @@ -332,3 +332,59 @@ html[data-bs-theme="dark"] @keyframes tab-pulse { .nav-tabs .nav-link.tab-flash { animation: tab-pulse 1s ease-in-out 2; } + +.beta-banner { + background: linear-gradient(135deg, var(--bs-primary) 0%, var(--brand-mid) 100%); + color: white; + padding: 0.75rem 0; + text-align: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} +.beta-banner-content { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 0.75rem; +} +.beta-banner-badge { + background-color: white; + color: var(--bs-primary); + padding: 0.25rem 0.75rem; + border-radius: 1rem; + font-weight: bold; + font-size: 0.875rem; + letter-spacing: 0.5px; +} +.beta-banner-text { + font-size: 0.95rem; +} +.beta-banner-button { + background-color: white; + color: var(--bs-primary); + border: none; + font-weight: 600; + padding: 0.375rem 1rem; + transition: all 0.2s ease; +} +.beta-banner-button:hover { + background-color: var(--brand-light); + color: var(--bs-primary); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +html[data-bs-theme="dark"] .beta-banner { + background: linear-gradient(135deg, var(--bs-primary) 0%, #7a4fc4 100%); +} +html[data-bs-theme="dark"] .beta-banner-badge { + background-color: rgba(255, 255, 255, 0.95); +} +html[data-bs-theme="dark"] .beta-banner-button { + background-color: rgba(255, 255, 255, 0.95); + color: var(--bs-primary); +} +html[data-bs-theme="dark"] .beta-banner-button:hover { + background-color: white; + color: var(--bs-primary); +}