Merge pull request #384 from keboola/dr/homepage-small-changes
feat(web): operator-owned Support callout in welcome hero
This commit is contained in:
commit
9dbe36fc38
7 changed files with 196 additions and 7 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -11,6 +11,17 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- `instance.support`: operator-authored HTML body rendered in a
|
||||||
|
mint-accent callout panel inside the welcome hero on `/home`,
|
||||||
|
below the Overview footnotes. Designed for a one-line invitation
|
||||||
|
pointing analysts at a chat space, mailing list, or runbook so
|
||||||
|
every user sees where to ask for help. HTML in, HTML out (same
|
||||||
|
`| safe` filter as `instance.overview`); empty default keeps the
|
||||||
|
OSS vendor-neutral. Resolved by
|
||||||
|
`app/instance_config.py::get_instance_support()`; surfaced in
|
||||||
|
`/admin/server-config` via `_KNOWN_FIELDS["instance"]` so it
|
||||||
|
appears as "Available but unset" for operators who haven't
|
||||||
|
populated it yet. Env override: `AGNES_INSTANCE_SUPPORT`.
|
||||||
- `instance.custom_scripts`: operator-injected HTML/JS blocks rendered
|
- `instance.custom_scripts`: operator-injected HTML/JS blocks rendered
|
||||||
into every page that extends `base.html`. Each entry takes `name`,
|
into every page that extends `base.html`. Each entry takes `name`,
|
||||||
`enabled`, `placement` (`head_start` | `head_end` | `body_end`), and
|
`enabled`, `placement` (`head_start` | `head_end` | `body_end`), and
|
||||||
|
|
|
||||||
|
|
@ -325,6 +325,26 @@ _KNOWN_FIELDS: dict[str, dict[str, dict]] = {
|
||||||
"capture session data). Restart required after save."
|
"capture session data). Restart required after save."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
# Operator-authored Support HTML rendered inside the welcome
|
||||||
|
# hero on /home, below the operator-owned Overview footnotes.
|
||||||
|
# Resolved by `app/instance_config.py::get_instance_support()`.
|
||||||
|
# Typical content: a one-line invitation pointing at a chat
|
||||||
|
# space, mailing list, or internal runbook. Empty value =
|
||||||
|
# block hidden (OSS stays vendor-neutral).
|
||||||
|
"support": {
|
||||||
|
"kind": "string",
|
||||||
|
"hint": (
|
||||||
|
"HTML body rendered inside the welcome hero's Support "
|
||||||
|
"block on /home (mint-accent panel below the Overview "
|
||||||
|
"footnotes). Typically a one-line invitation linking to "
|
||||||
|
"a chat space, mailing list, or runbook — e.g. "
|
||||||
|
"'<p><strong>Need help?</strong> Drop into our "
|
||||||
|
"<a href=\"https://chat.example.com/room/xxx\">Support</a> "
|
||||||
|
"chat space.</p>'. Rendered with | safe — admin trust "
|
||||||
|
"boundary (link target is operator-controlled). Empty "
|
||||||
|
"value hides the block."
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"data_source": {
|
"data_source": {
|
||||||
"bigquery": {
|
"bigquery": {
|
||||||
|
|
|
||||||
|
|
@ -365,6 +365,28 @@ def get_instance_overview() -> str:
|
||||||
return (raw or "").strip()
|
return (raw or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def get_instance_support() -> str:
|
||||||
|
"""Operator-authored Support body rendered inside the welcome hero
|
||||||
|
on ``/home``. Same ``| safe``-filter shape as
|
||||||
|
:func:`get_instance_overview` — operators paste HTML. Distinct
|
||||||
|
config field so help/chat pointers can be updated independently
|
||||||
|
from the product framing in ``instance.overview``.
|
||||||
|
|
||||||
|
Typical content: a one-line invitation pointing at a chat space
|
||||||
|
(Google Chat / Slack / Teams), a mailing list, or an internal
|
||||||
|
runbook. Empty default = block hidden, keeping the OSS
|
||||||
|
vendor-neutral when an instance ships without an operator-defined
|
||||||
|
support channel.
|
||||||
|
|
||||||
|
Resolution: ``AGNES_INSTANCE_SUPPORT`` env > ``instance.support``
|
||||||
|
YAML > ``""``. Mirrors :func:`get_instance_overview`.
|
||||||
|
"""
|
||||||
|
raw = os.environ.get("AGNES_INSTANCE_SUPPORT")
|
||||||
|
if raw is None:
|
||||||
|
raw = get_value("instance", "support", default="")
|
||||||
|
return (raw or "").strip()
|
||||||
|
|
||||||
|
|
||||||
_CUSTOM_SCRIPT_PLACEMENTS = ("head_start", "head_end", "body_end")
|
_CUSTOM_SCRIPT_PLACEMENTS = ("head_start", "head_end", "body_end")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ from app.instance_config import (
|
||||||
get_gws_oauth_credentials, get_home_automode_visibility,
|
get_gws_oauth_credentials, get_home_automode_visibility,
|
||||||
get_instance_admin_email, get_atlassian_base_url,
|
get_instance_admin_email, get_atlassian_base_url,
|
||||||
get_instance_brand, get_workspace_dir_name,
|
get_instance_brand, get_workspace_dir_name,
|
||||||
get_instance_logo_svg, get_instance_overview,
|
get_instance_logo_svg, get_instance_overview, get_instance_support,
|
||||||
get_instance_theme, get_custom_scripts,
|
get_instance_theme, get_custom_scripts,
|
||||||
)
|
)
|
||||||
from app.web.connector_prompts import all_connector_prompts
|
from app.web.connector_prompts import all_connector_prompts
|
||||||
|
|
@ -378,6 +378,7 @@ def _build_context(
|
||||||
INSTANCE_COPYRIGHT = ""
|
INSTANCE_COPYRIGHT = ""
|
||||||
LOGO_SVG = get_instance_logo_svg()
|
LOGO_SVG = get_instance_logo_svg()
|
||||||
INSTANCE_OVERVIEW = get_instance_overview()
|
INSTANCE_OVERVIEW = get_instance_overview()
|
||||||
|
INSTANCE_SUPPORT = get_instance_support()
|
||||||
TELEGRAM_BOT_USERNAME = os.environ.get("TELEGRAM_BOT_USERNAME", "")
|
TELEGRAM_BOT_USERNAME = os.environ.get("TELEGRAM_BOT_USERNAME", "")
|
||||||
SSH_ALIAS = "data-analyst"
|
SSH_ALIAS = "data-analyst"
|
||||||
SERVER_HOST = os.environ.get("SERVER_HOST", "")
|
SERVER_HOST = os.environ.get("SERVER_HOST", "")
|
||||||
|
|
|
||||||
|
|
@ -1131,6 +1131,59 @@
|
||||||
font-family: var(--ds-font-mono);
|
font-family: var(--ds-font-mono);
|
||||||
font-size: 11.5px;
|
font-size: 11.5px;
|
||||||
}
|
}
|
||||||
|
/* Support callout — operator-owned panel rendered inside the welcome
|
||||||
|
hero, below the Overview footnotes. Mint-tint background + brand-
|
||||||
|
accent border reads as a complementary help affordance (distinct
|
||||||
|
from the flat hairline-separated footnotes above it). Speech-bubble
|
||||||
|
icon on the left, operator HTML on the right. */
|
||||||
|
.home-mock .home-hero-support {
|
||||||
|
margin-top: 22px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: rgba(84, 211, 160, 0.10);
|
||||||
|
border: 1px solid rgba(84, 211, 160, 0.28);
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 14px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.home-mock .home-hero-support-icon {
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1.2;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--ds-brand-accent);
|
||||||
|
}
|
||||||
|
.home-mock .home-hero-support-body {
|
||||||
|
font-size: 13.5px;
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
line-height: 1.55;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.home-mock .home-hero-support-body p { margin: 0 0 8px; }
|
||||||
|
.home-mock .home-hero-support-body p:last-child { margin-bottom: 0; }
|
||||||
|
.home-mock .home-hero-support-body strong { color: #ffffff; font-weight: 600; }
|
||||||
|
.home-mock .home-hero-support-body a {
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: var(--ds-brand-accent);
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.home-mock .home-hero-support-body a:hover,
|
||||||
|
.home-mock .home-hero-support-body a:focus {
|
||||||
|
text-decoration-color: #ffffff;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.home-mock .home-hero-support-body code {
|
||||||
|
background: rgba(15, 23, 42, 0.55);
|
||||||
|
color: #FBBF24;
|
||||||
|
border: 1px solid rgba(251, 191, 36, 0.30);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: var(--ds-font-mono);
|
||||||
|
font-size: 11.5px;
|
||||||
|
}
|
||||||
@media (max-width: 880px) {
|
@media (max-width: 880px) {
|
||||||
.home-mock .home-hero-intro { padding: 32px 24px 28px; }
|
.home-mock .home-hero-intro { padding: 32px 24px 28px; }
|
||||||
.home-mock .home-hero-intro h1 { font-size: 26px; }
|
.home-mock .home-hero-intro h1 { font-size: 26px; }
|
||||||
|
|
@ -2310,6 +2363,20 @@
|
||||||
{% if config.INSTANCE_OVERVIEW %}
|
{% if config.INSTANCE_OVERVIEW %}
|
||||||
<div class="home-hero-footnotes">{{ config.INSTANCE_OVERVIEW | safe }}</div>
|
<div class="home-hero-footnotes">{{ config.INSTANCE_OVERVIEW | safe }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{# Support callout — operator-owned, opt-in. Separate
|
||||||
|
`instance.support` (`AGNES_INSTANCE_SUPPORT` env override)
|
||||||
|
field so help/chat pointers can be updated independently
|
||||||
|
from the product framing in `instance.overview`. Body is
|
||||||
|
operator-authored HTML rendered through the same | safe
|
||||||
|
filter — typical content is a one-line invitation linking
|
||||||
|
a chat space, mailing list, or runbook. Empty value →
|
||||||
|
block hidden, keeping the OSS vendor-neutral. #}
|
||||||
|
{% if config.INSTANCE_SUPPORT %}
|
||||||
|
<div class="home-hero-support" role="note" aria-label="Support">
|
||||||
|
<div class="home-hero-support-icon" aria-hidden="true">💬</div>
|
||||||
|
<div class="home-hero-support-body">{{ config.INSTANCE_SUPPORT | safe }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{# Homepage status frame — five counters with 24h/7d toggle.
|
{# Homepage status frame — five counters with 24h/7d toggle.
|
||||||
|
|
|
||||||
|
|
@ -36,14 +36,23 @@ instance:
|
||||||
# # `name` above still drives browser <title> text and page
|
# # `name` above still drives browser <title> text and page
|
||||||
# # headings — keep it populated. Env override:
|
# # headings — keep it populated. Env override:
|
||||||
# # AGNES_INSTANCE_LOGO_SVG.
|
# # AGNES_INSTANCE_LOGO_SVG.
|
||||||
# overview: | # Operator-authored Overview body rendered in the new
|
# overview: | # Operator-authored Overview body rendered inside the
|
||||||
# <p>Free-form HTML — paragraphs, links, lists.</p>
|
# <p>Free-form HTML — paragraphs, links, lists.</p>
|
||||||
# # Overview section on /home (between Getting Started and
|
# # welcome hero on /home (hairline-separated footnotes
|
||||||
# # Usage modes). Use for product framing, privacy posture,
|
# # below the four-pillar row). Use for product framing,
|
||||||
# # what-data-flows summary — operator-specific copy stays
|
# # privacy posture, what-data-flows summary —
|
||||||
# # out of the OSS this way. HTML in, HTML out (same `| safe`
|
# # operator-specific copy stays out of the OSS this way.
|
||||||
# # filter as news_intro). Empty/unset = section hidden.
|
# # HTML in, HTML out (same `| safe` filter as news_intro).
|
||||||
|
# # Empty/unset = block hidden.
|
||||||
# # Env override: AGNES_INSTANCE_OVERVIEW.
|
# # Env override: AGNES_INSTANCE_OVERVIEW.
|
||||||
|
# support: | # Operator-authored Support body rendered in a
|
||||||
|
# <p><strong>Need help?</strong> Drop into our <a href="...">Support</a> chat space.</p>
|
||||||
|
# # mint-accent panel inside the welcome hero on /home
|
||||||
|
# # (below the Overview footnotes). Use to point analysts
|
||||||
|
# # at a chat space, mailing list, or runbook so they know
|
||||||
|
# # where to ask for help. HTML in, HTML out (same `| safe`
|
||||||
|
# # filter as overview). Empty/unset = block hidden.
|
||||||
|
# # Env override: AGNES_INSTANCE_SUPPORT.
|
||||||
# sync_interval: "1 hour" # Cadence shown in analyst CLAUDE.md (e.g., "1 hour", "30 minutes", "daily")
|
# sync_interval: "1 hour" # Cadence shown in analyst CLAUDE.md (e.g., "1 hour", "30 minutes", "daily")
|
||||||
# admin_email: "ops@acme.com" # Operator contact shown on /home GWS connector tile as
|
# admin_email: "ops@acme.com" # Operator contact shown on /home GWS connector tile as
|
||||||
# an "Email admin" mailto button (analysts whose operator
|
# an "Email admin" mailto button (analysts whose operator
|
||||||
|
|
|
||||||
|
|
@ -398,3 +398,62 @@ def test_welcome_footnotes_hidden_when_overview_unset(fresh_db, monkeypatch):
|
||||||
close_system_db()
|
close_system_db()
|
||||||
body = _client().get("/home", cookies={"access_token": sess}).text
|
body = _client().get("/home", cookies={"access_token": sess}).text
|
||||||
assert '<div class="home-hero-footnotes">' not in body
|
assert '<div class="home-hero-footnotes">' not in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_welcome_support_renders_when_set(fresh_db, monkeypatch):
|
||||||
|
"""Setting `AGNES_INSTANCE_SUPPORT` (mirrors `instance.support`
|
||||||
|
yaml) injects raw HTML into the mint-accent Support callout
|
||||||
|
inside the welcome hero. The marker text MUST appear inside
|
||||||
|
`.home-hero-support-body`. Separate field from
|
||||||
|
`instance.overview` so support/help pointers can be updated
|
||||||
|
independently from the operator's product framing."""
|
||||||
|
monkeypatch.setenv("AGNES_INSTANCE_SUPPORT", "<p>SUPPORT_TEST_MARKER</p>")
|
||||||
|
from src.db import get_system_db, close_system_db
|
||||||
|
|
||||||
|
conn = get_system_db()
|
||||||
|
try:
|
||||||
|
_, sess = _make_user_and_session(conn)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
close_system_db()
|
||||||
|
body = _client().get("/home", cookies={"access_token": sess}).text
|
||||||
|
assert '<div class="home-hero-support"' in body
|
||||||
|
assert "SUPPORT_TEST_MARKER" in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_welcome_support_hidden_when_unset(fresh_db, monkeypatch):
|
||||||
|
"""Default empty `instance.support` (no env override) hides the
|
||||||
|
Support callout entirely so the OSS ships without a stray
|
||||||
|
empty mint panel in the welcome card."""
|
||||||
|
monkeypatch.delenv("AGNES_INSTANCE_SUPPORT", raising=False)
|
||||||
|
from src.db import get_system_db, close_system_db
|
||||||
|
|
||||||
|
conn = get_system_db()
|
||||||
|
try:
|
||||||
|
_, sess = _make_user_and_session(conn)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
close_system_db()
|
||||||
|
body = _client().get("/home", cookies={"access_token": sess}).text
|
||||||
|
assert '<div class="home-hero-support"' not in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_welcome_support_independent_of_overview(fresh_db, monkeypatch):
|
||||||
|
"""The Support callout MUST render even when `instance.overview`
|
||||||
|
is empty — the two fields are independent. Catches a regression
|
||||||
|
where the Support gate was accidentally wired to
|
||||||
|
INSTANCE_OVERVIEW instead of INSTANCE_SUPPORT."""
|
||||||
|
monkeypatch.delenv("AGNES_INSTANCE_OVERVIEW", raising=False)
|
||||||
|
monkeypatch.setenv("AGNES_INSTANCE_SUPPORT", "<p>SUPPORT_ONLY_MARKER</p>")
|
||||||
|
from src.db import get_system_db, close_system_db
|
||||||
|
|
||||||
|
conn = get_system_db()
|
||||||
|
try:
|
||||||
|
_, sess = _make_user_and_session(conn)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
close_system_db()
|
||||||
|
body = _client().get("/home", cookies={"access_token": sess}).text
|
||||||
|
assert '<div class="home-hero-footnotes">' not in body
|
||||||
|
assert '<div class="home-hero-support"' in body
|
||||||
|
assert "SUPPORT_ONLY_MARKER" in body
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue