feat(web): /admin/welcome editor page
This commit is contained in:
parent
93b713900b
commit
2b3048f77f
3 changed files with 114 additions and 1 deletions
|
|
@ -883,6 +883,28 @@ async def admin_marketplaces_page(
|
||||||
return templates.TemplateResponse(request, "admin_marketplaces.html", ctx)
|
return templates.TemplateResponse(request, "admin_marketplaces.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/admin/welcome", response_class=HTMLResponse)
|
||||||
|
async def admin_welcome_page(
|
||||||
|
request: Request,
|
||||||
|
user: dict = Depends(require_admin),
|
||||||
|
conn: duckdb.DuckDBPyConnection = Depends(_get_db),
|
||||||
|
):
|
||||||
|
from src.repositories.welcome_template import WelcomeTemplateRepository
|
||||||
|
from src.welcome_template import _load_default_template
|
||||||
|
|
||||||
|
row = WelcomeTemplateRepository(conn).get()
|
||||||
|
ctx = _build_context(
|
||||||
|
request,
|
||||||
|
user=user,
|
||||||
|
current=row["content"] or "",
|
||||||
|
default_template=_load_default_template(),
|
||||||
|
updated_at=row["updated_at"],
|
||||||
|
updated_by=row["updated_by"],
|
||||||
|
is_override=row["content"] is not None,
|
||||||
|
)
|
||||||
|
return templates.TemplateResponse(request, "admin_welcome.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tokens", response_class=HTMLResponse)
|
@router.get("/tokens", response_class=HTMLResponse)
|
||||||
async def my_tokens_page(
|
async def my_tokens_page(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<a class="app-nav-link {% if _path.startswith('/install') %}is-active{% endif %}" href="/install">Install CLI</a>
|
<a class="app-nav-link {% if _path.startswith('/install') %}is-active{% endif %}" href="/install">Install CLI</a>
|
||||||
{% if session.user.is_admin %}
|
{% if session.user.is_admin %}
|
||||||
<a class="app-nav-link {% if _path.startswith('/admin/marketplaces') %}is-active{% endif %}" href="/admin/marketplaces">Marketplaces</a>
|
<a class="app-nav-link {% if _path.startswith('/admin/marketplaces') %}is-active{% endif %}" href="/admin/marketplaces">Marketplaces</a>
|
||||||
{% set _admin_active = _path.startswith('/admin/tables') or _path.startswith('/admin/tokens') or _path.startswith('/admin/users') or _path.startswith('/admin/groups') or _path.startswith('/admin/access') or _path.startswith('/admin/server-config') %}
|
{% set _admin_active = _path.startswith('/admin/tables') or _path.startswith('/admin/tokens') or _path.startswith('/admin/users') or _path.startswith('/admin/groups') or _path.startswith('/admin/access') or _path.startswith('/admin/server-config') or _path.startswith('/admin/welcome') %}
|
||||||
<div class="app-nav-menu" id="adminNavMenu">
|
<div class="app-nav-menu" id="adminNavMenu">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="app-nav-link app-nav-menu-trigger {% if _admin_active %}is-active{% endif %}"
|
class="app-nav-link app-nav-menu-trigger {% if _admin_active %}is-active{% endif %}"
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
<a class="app-nav-menu-item {% if _path.startswith('/admin/groups') %}is-active{% endif %}" role="menuitem" href="/admin/groups">Groups</a>
|
<a class="app-nav-menu-item {% if _path.startswith('/admin/groups') %}is-active{% endif %}" role="menuitem" href="/admin/groups">Groups</a>
|
||||||
<a class="app-nav-menu-item {% if _path.startswith('/admin/access') %}is-active{% endif %}" role="menuitem" href="/admin/access">Resource access</a>
|
<a class="app-nav-menu-item {% if _path.startswith('/admin/access') %}is-active{% endif %}" role="menuitem" href="/admin/access">Resource access</a>
|
||||||
<a class="app-nav-menu-item {% if _path.startswith('/admin/server-config') %}is-active{% endif %}" role="menuitem" href="/admin/server-config">Server config</a>
|
<a class="app-nav-menu-item {% if _path.startswith('/admin/server-config') %}is-active{% endif %}" role="menuitem" href="/admin/server-config">Server config</a>
|
||||||
|
<a class="app-nav-menu-item {% if _path.startswith('/admin/welcome') %}is-active{% endif %}" role="menuitem" href="/admin/welcome">Welcome prompt</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
90
app/web/templates/admin_welcome.html
Normal file
90
app/web/templates/admin_welcome.html
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Welcome Prompt — Admin{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="admin-page">
|
||||||
|
<h1>Analyst Welcome Prompt</h1>
|
||||||
|
<p class="muted">
|
||||||
|
This is the CLAUDE.md generated for analysts when they run
|
||||||
|
<code>da analyst setup</code>. Edit it to customize the onboarding
|
||||||
|
instructions for this instance. Leave empty (or click <em>Reset to default</em>)
|
||||||
|
to use the OSS-shipped default.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if is_override %}
|
||||||
|
<p class="status">
|
||||||
|
Overridden by <strong>{{ updated_by }}</strong> on
|
||||||
|
{{ updated_at.strftime("%Y-%m-%d %H:%M UTC") if updated_at else "—" }}.
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="status">Using shipped default.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h2>Available placeholders</h2>
|
||||||
|
<pre class="placeholder-cheatsheet">
|
||||||
|
{{ "{{ instance.name }}" }} — instance display name
|
||||||
|
{{ "{{ instance.subtitle }}" }} — operator name
|
||||||
|
{{ "{{ server.url }}" }} — full server URL
|
||||||
|
{{ "{{ server.hostname }}" }} — host part
|
||||||
|
{{ "{{ sync_interval }}" }} — refresh cadence (instance.yaml)
|
||||||
|
{{ "{{ data_source.type }}" }} — keboola | bigquery | local
|
||||||
|
{{ "{{ tables }}" }} — list of {name, description, query_mode}
|
||||||
|
{{ "{{ metrics.count }}" }}, {{ "{{ metrics.categories }}" }}
|
||||||
|
{{ "{{ marketplaces }}" }} — RBAC-filtered list of {slug, name, plugins[]}
|
||||||
|
{{ "{{ user.email }}" }}, {{ "{{ user.name }}" }}, {{ "{{ user.is_admin }}" }}, {{ "{{ user.groups }}" }}
|
||||||
|
{{ "{{ now }}" }}, {{ "{{ today }}" }}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<form id="welcome-form" onsubmit="return false">
|
||||||
|
<textarea id="content" rows="30" cols="100">{{ current or default_template }}</textarea>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" id="save-btn">Save override</button>
|
||||||
|
<button type="button" id="reset-btn" class="secondary">Reset to default</button>
|
||||||
|
<button type="button" id="preview-btn" class="secondary">Preview</button>
|
||||||
|
</div>
|
||||||
|
<div id="result" class="result"></div>
|
||||||
|
<pre id="preview" class="preview" hidden></pre>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
const result = $("result");
|
||||||
|
|
||||||
|
$("save-btn").addEventListener("click", async () => {
|
||||||
|
result.textContent = "Saving…";
|
||||||
|
const r = await fetch("/api/admin/welcome-template", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({content: $("content").value}),
|
||||||
|
});
|
||||||
|
if (r.ok) {
|
||||||
|
result.textContent = "Saved.";
|
||||||
|
} else {
|
||||||
|
const err = await r.json();
|
||||||
|
result.textContent = "Error: " + (err.detail || r.statusText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("reset-btn").addEventListener("click", async () => {
|
||||||
|
if (!confirm("Reset to OSS default? Your override will be lost.")) return;
|
||||||
|
const r = await fetch("/api/admin/welcome-template", {method: "DELETE"});
|
||||||
|
if (r.ok) {
|
||||||
|
result.textContent = "Reset. Reload to see the default.";
|
||||||
|
} else {
|
||||||
|
result.textContent = "Error: " + r.statusText;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("preview-btn").addEventListener("click", async () => {
|
||||||
|
const r = await fetch("/api/welcome?server_url=" + encodeURIComponent(window.location.origin));
|
||||||
|
if (r.ok) {
|
||||||
|
const j = await r.json();
|
||||||
|
$("preview").textContent = j.content;
|
||||||
|
$("preview").hidden = false;
|
||||||
|
} else {
|
||||||
|
const err = await r.json();
|
||||||
|
result.textContent = "Render error: " + (err.detail || r.statusText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Loading…
Reference in a new issue