|
|
|
|
@ -0,0 +1,725 @@
|
|
|
|
|
from html import escape
|
|
|
|
|
|
|
|
|
|
from admin_app.view.view_models import (
|
|
|
|
|
AdminLoginPageView,
|
|
|
|
|
AdminPanelHomeView,
|
|
|
|
|
AdminPanelMetric,
|
|
|
|
|
AdminPanelModuleCard,
|
|
|
|
|
AdminPanelNavigationItem,
|
|
|
|
|
AdminPanelQuickAction,
|
|
|
|
|
AdminPanelRoadmapItem,
|
|
|
|
|
AdminPanelSurfaceLink,
|
|
|
|
|
AdminToolReviewPageView,
|
|
|
|
|
AdminToolReviewWorkflowStep,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
BOOTSTRAP_CSS_HREF = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
|
|
|
|
BOOTSTRAP_JS_HREF = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
|
|
|
|
_BADGE_CLASS_MAP = {
|
|
|
|
|
"success": "bg-success-subtle text-success-emphasis border border-success-subtle",
|
|
|
|
|
"warning": "bg-warning-subtle text-warning-emphasis border border-warning-subtle",
|
|
|
|
|
"info": "bg-info-subtle text-info-emphasis border border-info-subtle",
|
|
|
|
|
"primary": "bg-primary-subtle text-primary-emphasis border border-primary-subtle",
|
|
|
|
|
"secondary": "bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle",
|
|
|
|
|
"dark": "bg-dark-subtle text-dark-emphasis border border-dark-subtle",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_panel_home(
|
|
|
|
|
view: AdminPanelHomeView,
|
|
|
|
|
*,
|
|
|
|
|
css_href: str,
|
|
|
|
|
js_href: str,
|
|
|
|
|
) -> str:
|
|
|
|
|
navigation_markup = _render_navigation(view.navigation)
|
|
|
|
|
quick_actions_markup = _render_quick_actions(view.quick_actions)
|
|
|
|
|
metrics_markup = _render_metrics(view.metrics)
|
|
|
|
|
modules_markup = _render_modules(view.modules)
|
|
|
|
|
surface_links_markup = _render_surface_links(view.surface_links)
|
|
|
|
|
roadmap_markup = _render_roadmap(view.roadmap)
|
|
|
|
|
|
|
|
|
|
panel_title = escape(view.panel_title)
|
|
|
|
|
app_name = escape(view.app_name)
|
|
|
|
|
panel_subtitle = escape(view.panel_subtitle)
|
|
|
|
|
environment = escape(view.environment)
|
|
|
|
|
version = escape(view.version)
|
|
|
|
|
api_prefix = escape(view.api_prefix)
|
|
|
|
|
service = escape(view.service)
|
|
|
|
|
release_label = escape(view.release_label)
|
|
|
|
|
|
|
|
|
|
return f"""<!DOCTYPE html>
|
|
|
|
|
<html lang="pt-BR" data-bs-theme="light">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
|
<title>{panel_title}</title>
|
|
|
|
|
<meta name="description" content="{panel_subtitle}">
|
|
|
|
|
<link rel="stylesheet" href="{BOOTSTRAP_CSS_HREF}">
|
|
|
|
|
<link rel="stylesheet" href="{escape(css_href, quote=True)}">
|
|
|
|
|
</head>
|
|
|
|
|
<body class="admin-view-body">
|
|
|
|
|
<div class="container-xxl py-4 py-lg-5">
|
|
|
|
|
<div class="row g-4 align-items-start">
|
|
|
|
|
<aside class="col-12 col-xl-4 col-xxl-3">
|
|
|
|
|
<div class="card border-0 shadow-sm admin-shell-card admin-sidebar-sticky">
|
|
|
|
|
<div class="card-body p-4 p-lg-4">
|
|
|
|
|
<span class="badge rounded-pill text-bg-dark px-3 py-2">{release_label}</span>
|
|
|
|
|
<h1 class="display-6 fw-semibold mt-3 mb-2">{panel_title}</h1>
|
|
|
|
|
<p class="text-secondary mb-4">{panel_subtitle}</p>
|
|
|
|
|
|
|
|
|
|
<nav class="d-grid gap-2">
|
|
|
|
|
{navigation_markup}
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
<div id="runtime" class="admin-runtime-block rounded-4 p-3 mt-4">
|
|
|
|
|
<p class="text-uppercase small fw-semibold text-secondary mb-3">Runtime atual</p>
|
|
|
|
|
<div class="d-grid gap-3 small">
|
|
|
|
|
<div class="d-flex justify-content-between gap-3">
|
|
|
|
|
<span class="text-secondary">Aplicacao</span>
|
|
|
|
|
<strong class="text-end">{app_name}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="d-flex justify-content-between gap-3">
|
|
|
|
|
<span class="text-secondary">Servico</span>
|
|
|
|
|
<strong class="text-end">{service}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="d-flex justify-content-between gap-3">
|
|
|
|
|
<span class="text-secondary">Ambiente</span>
|
|
|
|
|
<strong class="text-end text-uppercase">{environment}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="d-flex justify-content-between gap-3">
|
|
|
|
|
<span class="text-secondary">Versao</span>
|
|
|
|
|
<strong class="text-end">{version}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="d-flex justify-content-between gap-3">
|
|
|
|
|
<span class="text-secondary">Prefixo API</span>
|
|
|
|
|
<strong class="text-end">{api_prefix}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
<section class="col-12 col-xl-8 col-xxl-9">
|
|
|
|
|
<div id="overview" class="card border-0 shadow-sm admin-hero-card overflow-hidden mb-4">
|
|
|
|
|
<div class="card-body p-4 p-lg-5">
|
|
|
|
|
<div class="d-flex flex-wrap gap-2 mb-3">
|
|
|
|
|
<span class="badge rounded-pill bg-white text-dark border">Area interna protegida</span>
|
|
|
|
|
<span class="badge rounded-pill bg-dark-subtle text-dark-emphasis border border-dark-subtle">Minimal Bootstrap</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="row g-4 align-items-end">
|
|
|
|
|
<div class="col-lg-7">
|
|
|
|
|
<h2 class="display-5 fw-semibold mb-3">Dashboard do administrador</h2>
|
|
|
|
|
<p class="lead text-secondary mb-0">
|
|
|
|
|
A home protegida organiza o trabalho do time interno por fluxo, com foco no que realmente importa depois do login.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-lg-5">
|
|
|
|
|
<div class="d-grid gap-2 admin-quick-actions">
|
|
|
|
|
{quick_actions_markup}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="row g-3 mb-4">
|
|
|
|
|
{metrics_markup}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="row g-4">
|
|
|
|
|
<div class="col-12 col-xxl-7">
|
|
|
|
|
<div id="modules" class="card border-0 shadow-sm admin-surface-card h-100">
|
|
|
|
|
<div class="card-body p-4">
|
|
|
|
|
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-4">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-uppercase small fw-semibold text-secondary mb-2">Areas do sistema</p>
|
|
|
|
|
<h3 class="h2 fw-semibold mb-2">Onde o time interno opera</h3>
|
|
|
|
|
<p class="text-secondary mb-0">
|
|
|
|
|
A dashboard agora funciona como ponto de orientacao para entrar nas areas certas sem expor atalhos desnecessarios.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="row g-3">
|
|
|
|
|
{modules_markup}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="col-12 col-xxl-5">
|
|
|
|
|
<div id="surfaces" class="card border-0 shadow-sm admin-surface-card mb-4">
|
|
|
|
|
<div class="card-body p-4">
|
|
|
|
|
<p class="text-uppercase small fw-semibold text-secondary mb-2">Acessos disponiveis</p>
|
|
|
|
|
<h3 class="h3 fw-semibold mb-3">Entradas claras para as areas protegidas</h3>
|
|
|
|
|
<div class="list-group list-group-flush admin-link-list">
|
|
|
|
|
{surface_links_markup}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div id="roadmap" class="card border-0 shadow-sm admin-surface-card">
|
|
|
|
|
<div class="card-body p-4">
|
|
|
|
|
<p class="text-uppercase small fw-semibold text-secondary mb-2">Fluxo recomendado</p>
|
|
|
|
|
<h3 class="h3 fw-semibold mb-3">Como navegar no painel</h3>
|
|
|
|
|
<div class="vstack gap-3">
|
|
|
|
|
{roadmap_markup}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script src="{BOOTSTRAP_JS_HREF}" defer></script>
|
|
|
|
|
<script src="{escape(js_href, quote=True)}" defer></script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_login_page(
|
|
|
|
|
view: AdminLoginPageView,
|
|
|
|
|
*,
|
|
|
|
|
css_href: str,
|
|
|
|
|
js_href: str,
|
|
|
|
|
) -> str:
|
|
|
|
|
security_markup = _render_text_list(view.security_highlights)
|
|
|
|
|
notes_markup = _render_text_list(view.integration_notes)
|
|
|
|
|
|
|
|
|
|
return f"""<!DOCTYPE html>
|
|
|
|
|
<html lang="pt-BR" data-bs-theme="light">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
|
<title>{escape(view.title)}</title>
|
|
|
|
|
<meta name="description" content="{escape(view.subtitle)}">
|
|
|
|
|
<link rel="stylesheet" href="{BOOTSTRAP_CSS_HREF}">
|
|
|
|
|
<link rel="stylesheet" href="{escape(css_href, quote=True)}">
|
|
|
|
|
</head>
|
|
|
|
|
<body class="admin-view-body admin-login-page">
|
|
|
|
|
<div class="container-xxl py-4 py-lg-5">
|
|
|
|
|
<div class="row justify-content-center">
|
|
|
|
|
<div class="col-12 col-xl-10">
|
|
|
|
|
<div class="row g-4 align-items-stretch">
|
|
|
|
|
<div class="col-12 col-lg-5">
|
|
|
|
|
<div class="card border-0 shadow-sm admin-login-card h-100">
|
|
|
|
|
<div class="card-body p-4 p-lg-5 d-flex flex-column justify-content-center">
|
|
|
|
|
<div class="mb-4">
|
|
|
|
|
<div class="d-flex flex-wrap gap-2 mb-3">
|
|
|
|
|
<span class="badge rounded-pill text-bg-dark">Acesso restrito</span>
|
|
|
|
|
<span class="badge rounded-pill bg-white text-dark border">{escape(view.app_name)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="text-uppercase small fw-semibold text-secondary mb-2">Entrada do painel</p>
|
|
|
|
|
<h1 class="display-6 fw-semibold mb-3">{escape(view.title)}</h1>
|
|
|
|
|
<p class="text-secondary mb-0">{escape(view.subtitle)}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<form class="admin-login-form" data-admin-login-form="true" data-auth-endpoint="{escape(view.auth_endpoint, quote=True)}" data-dashboard-href="{escape(view.dashboard_href, quote=True)}">
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label class="form-label fw-semibold" for="admin-login-email">Email administrativo</label>
|
|
|
|
|
<input class="form-control form-control-lg rounded-4" id="admin-login-email" name="email" type="email" placeholder="{escape(view.email_placeholder, quote=True)}" autocomplete="username" required>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label class="form-label fw-semibold" for="admin-login-password">Senha</label>
|
|
|
|
|
<input class="form-control form-control-lg rounded-4" id="admin-login-password" name="password" type="password" placeholder="{escape(view.password_placeholder, quote=True)}" autocomplete="current-password" required>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="small text-secondary rounded-4 admin-login-policy mb-4">
|
|
|
|
|
<strong>Politica atual:</strong> {escape(view.password_policy_label)}
|
|
|
|
|
</div>
|
|
|
|
|
<button class="btn btn-dark btn-lg rounded-pill w-100 d-inline-flex align-items-center justify-content-center gap-2" type="submit">
|
|
|
|
|
<span data-submit-label>Entrar no painel</span>
|
|
|
|
|
<span class="spinner-border spinner-border-sm d-none" data-submit-spinner aria-hidden="true"></span>
|
|
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
<div class="alert d-none mt-4 mb-0 rounded-4" id="admin-login-feedback" role="status"></div>
|
|
|
|
|
|
|
|
|
|
<p class="small text-secondary mt-4 mb-0">
|
|
|
|
|
O restante do sistema administrativo so fica disponivel depois da autenticacao do StaffAccount.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="col-12 col-lg-7">
|
|
|
|
|
<div class="card border-0 shadow-sm admin-login-info-card h-100">
|
|
|
|
|
<div class="card-body p-4 p-lg-5 d-flex flex-column gap-4">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="d-flex flex-wrap gap-2 mb-3">
|
|
|
|
|
<span class="badge rounded-pill bg-body-tertiary text-secondary border">Ambiente {escape(view.environment)}</span>
|
|
|
|
|
<span class="badge rounded-pill bg-body-tertiary text-secondary border">Versao {escape(view.version)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<h2 class="display-6 fw-semibold mb-3">Depois do login, o painel organiza o fluxo por voce</h2>
|
|
|
|
|
<p class="lead text-secondary mb-0">
|
|
|
|
|
Primeiro vem a dashboard administrativa protegida. A partir dela, o time acessa revisao, governanca e acompanhamento do ambiente sem atalhos confusos antes da autenticacao.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="row g-3">
|
|
|
|
|
<div class="col-12 col-md-4">
|
|
|
|
|
<div class="admin-login-kpi rounded-4 p-3 h-100">
|
|
|
|
|
<p class="small text-uppercase fw-semibold text-secondary mb-2">Access token</p>
|
|
|
|
|
<div class="h4 fw-semibold mb-1">{escape(view.access_token_ttl_label)}</div>
|
|
|
|
|
<p class="small text-secondary mb-0">Janela curta para a sessao ativa.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-12 col-md-4">
|
|
|
|
|
<div class="admin-login-kpi rounded-4 p-3 h-100">
|
|
|
|
|
<p class="small text-uppercase fw-semibold text-secondary mb-2">Refresh token</p>
|
|
|
|
|
<div class="h4 fw-semibold mb-1">{escape(view.refresh_token_ttl_label)}</div>
|
|
|
|
|
<p class="small text-secondary mb-0">Continuidade controlada da sessao web.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-12 col-md-4">
|
|
|
|
|
<div class="admin-login-kpi rounded-4 p-3 h-100">
|
|
|
|
|
<p class="small text-uppercase fw-semibold text-secondary mb-2">Acesso</p>
|
|
|
|
|
<div class="h4 fw-semibold mb-1">Protegido</div>
|
|
|
|
|
<p class="small text-secondary mb-0">Liberado apenas apos autenticacao.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="row g-4 mt-0">
|
|
|
|
|
<div class="col-12 col-xl-6">
|
|
|
|
|
<div class="admin-login-note rounded-4 p-4 h-100">
|
|
|
|
|
<p class="text-uppercase small fw-semibold text-secondary mb-2">O que fica liberado</p>
|
|
|
|
|
<h3 class="h4 fw-semibold mb-3">Fluxo apos o login</h3>
|
|
|
|
|
<ul class="small text-secondary ps-3 mb-0">
|
|
|
|
|
{notes_markup}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-12 col-xl-6">
|
|
|
|
|
<div class="admin-login-note rounded-4 p-4 h-100">
|
|
|
|
|
<p class="text-uppercase small fw-semibold text-secondary mb-2">Base de seguranca</p>
|
|
|
|
|
<h3 class="h4 fw-semibold mb-3">Como a entrada esta protegida</h3>
|
|
|
|
|
<ul class="small text-secondary ps-3 mb-0">
|
|
|
|
|
{security_markup}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script src="{BOOTSTRAP_JS_HREF}" defer></script>
|
|
|
|
|
<script src="{escape(js_href, quote=True)}" defer></script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _render_navigation(items: tuple[AdminPanelNavigationItem, ...]) -> str:
|
|
|
|
|
links: list[str] = []
|
|
|
|
|
for item in items:
|
|
|
|
|
badge_markup = ""
|
|
|
|
|
if item.badge:
|
|
|
|
|
badge_markup = (
|
|
|
|
|
f'<span class="badge rounded-pill bg-body-tertiary text-secondary border">{escape(item.badge)}</span>'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
active_class = " active shadow-sm" if item.is_active else ""
|
|
|
|
|
links.append(
|
|
|
|
|
f"""
|
|
|
|
|
<a class="btn btn-link text-decoration-none text-start rounded-4 px-3 py-3 admin-nav-link{active_class}" href="{escape(item.href, quote=True)}">
|
|
|
|
|
<div class="d-flex justify-content-between align-items-start gap-3">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="fw-semibold text-dark">{escape(item.label)}</div>
|
|
|
|
|
<div class="small text-secondary mt-1">{escape(item.description)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
{badge_markup}
|
|
|
|
|
</div>
|
|
|
|
|
</a>
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
return "\n".join(links)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _render_quick_actions(items: tuple[AdminPanelQuickAction, ...]) -> str:
|
|
|
|
|
return "\n".join(
|
|
|
|
|
f'<a class="btn {escape(item.button_class)} btn-lg rounded-pill px-4" href="{escape(item.href, quote=True)}">{escape(item.label)}</a>'
|
|
|
|
|
for item in items
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _render_metrics(items: tuple[AdminPanelMetric, ...]) -> str:
|
|
|
|
|
cards: list[str] = []
|
|
|
|
|
for item in items:
|
|
|
|
|
cards.append(
|
|
|
|
|
f"""
|
|
|
|
|
<div class="col-12 col-md-6 col-xxl-3">
|
|
|
|
|
<div class="card border-0 shadow-sm h-100 admin-metric-card">
|
|
|
|
|
<div class="card-body p-4">
|
|
|
|
|
<p class="small text-uppercase fw-semibold text-secondary mb-3">{escape(item.label)}</p>
|
|
|
|
|
<div class="display-6 fw-semibold mb-2">{escape(item.value)}</div>
|
|
|
|
|
<p class="text-secondary mb-0">{escape(item.description)}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
return "\n".join(cards)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _render_modules(items: tuple[AdminPanelModuleCard, ...]) -> str:
|
|
|
|
|
cards: list[str] = []
|
|
|
|
|
for item in items:
|
|
|
|
|
highlights = "".join(
|
|
|
|
|
f'<li class="mb-2">{escape(highlight)}</li>'
|
|
|
|
|
for highlight in item.highlights
|
|
|
|
|
)
|
|
|
|
|
cta_markup = '<span class="small fw-semibold text-secondary">Em preparacao</span>'
|
|
|
|
|
if item.href and item.cta_label and item.is_available:
|
|
|
|
|
cta_markup = (
|
|
|
|
|
f'<a class="btn btn-sm btn-outline-dark rounded-pill px-3" '
|
|
|
|
|
f'href="{escape(item.href, quote=True)}">{escape(item.cta_label)}</a>'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
badge_classes = _BADGE_CLASS_MAP.get(item.status_variant, _BADGE_CLASS_MAP["secondary"])
|
|
|
|
|
cards.append(
|
|
|
|
|
f"""
|
|
|
|
|
<div class="col-12 col-md-6">
|
|
|
|
|
<article class="card border-0 h-100 admin-module-card">
|
|
|
|
|
<div class="card-body p-4 d-flex flex-column gap-3">
|
|
|
|
|
<div class="d-flex flex-wrap justify-content-between gap-3 align-items-start">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="small text-uppercase fw-semibold text-secondary mb-2">{escape(item.eyebrow)}</p>
|
|
|
|
|
<h4 class="h5 fw-semibold mb-0">{escape(item.title)}</h4>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="badge rounded-pill {badge_classes}">{escape(item.status_label)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="text-secondary mb-0">{escape(item.description)}</p>
|
|
|
|
|
<ul class="small text-secondary ps-3 mb-0">
|
|
|
|
|
{highlights}
|
|
|
|
|
</ul>
|
|
|
|
|
<div class="pt-1 mt-auto">
|
|
|
|
|
{cta_markup}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</article>
|
|
|
|
|
</div>
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
return "\n".join(cards)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _render_surface_links(items: tuple[AdminPanelSurfaceLink, ...]) -> str:
|
|
|
|
|
cards: list[str] = []
|
|
|
|
|
for item in items:
|
|
|
|
|
cards.append(
|
|
|
|
|
f"""
|
|
|
|
|
<a class="list-group-item list-group-item-action border-0 rounded-4 px-0 admin-surface-link" href="{escape(item.href, quote=True)}">
|
|
|
|
|
<div class="d-flex justify-content-between align-items-start gap-3">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="small text-uppercase fw-semibold text-secondary mb-2">{escape(item.method)}</div>
|
|
|
|
|
<div class="fw-semibold text-dark">{escape(item.label)}</div>
|
|
|
|
|
<div class="small text-secondary mt-1">{escape(item.description)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="badge rounded-pill bg-body-tertiary text-secondary border">Abrir</span>
|
|
|
|
|
</div>
|
|
|
|
|
</a>
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
return "\n".join(cards)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _render_roadmap(items: tuple[AdminPanelRoadmapItem, ...]) -> str:
|
|
|
|
|
cards: list[str] = []
|
|
|
|
|
for item in items:
|
|
|
|
|
cards.append(
|
|
|
|
|
f"""
|
|
|
|
|
<div class="admin-roadmap-item rounded-4 p-3">
|
|
|
|
|
<div class="d-flex justify-content-between align-items-start gap-3">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="small text-uppercase fw-semibold text-secondary mb-2">Etapa {escape(item.step)}</div>
|
|
|
|
|
<div class="fw-semibold text-dark">{escape(item.title)}</div>
|
|
|
|
|
<div class="small text-secondary mt-1">{escape(item.description)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="badge rounded-pill bg-body-tertiary text-secondary border">{escape(item.status_label)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
return "\n".join(cards)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _render_text_list(items: tuple[str, ...]) -> str:
|
|
|
|
|
return "\n".join(
|
|
|
|
|
f"<li class=\"mb-2\">{escape(item)}</li>"
|
|
|
|
|
for item in items
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_tool_review_page(
|
|
|
|
|
view: AdminToolReviewPageView,
|
|
|
|
|
*,
|
|
|
|
|
css_href: str,
|
|
|
|
|
js_href: str,
|
|
|
|
|
) -> str:
|
|
|
|
|
workflow_markup = _render_tool_review_workflow(view.workflow)
|
|
|
|
|
review_notes_markup = _render_text_list(view.review_notes)
|
|
|
|
|
approval_notes_markup = _render_text_list(view.approval_notes)
|
|
|
|
|
activation_notes_markup = _render_text_list(view.activation_notes)
|
|
|
|
|
|
|
|
|
|
return f"""<!DOCTYPE html>
|
|
|
|
|
<html lang="pt-BR" data-bs-theme="light">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
|
<title>{escape(view.title)}</title>
|
|
|
|
|
<meta name="description" content="{escape(view.subtitle)}">
|
|
|
|
|
<link rel="stylesheet" href="{BOOTSTRAP_CSS_HREF}">
|
|
|
|
|
<link rel="stylesheet" href="{escape(css_href, quote=True)}">
|
|
|
|
|
</head>
|
|
|
|
|
<body class="admin-view-body admin-tool-review-page">
|
|
|
|
|
<div class="container-xxl py-4 py-lg-5" data-admin-tool-review-board="true" data-overview-endpoint="{escape(view.overview_endpoint, quote=True)}" data-contracts-endpoint="{escape(view.contracts_endpoint, quote=True)}" data-review-queue-endpoint="{escape(view.review_queue_endpoint, quote=True)}" data-publications-endpoint="{escape(view.publications_endpoint, quote=True)}">
|
|
|
|
|
<div class="row g-4 align-items-start">
|
|
|
|
|
<aside class="col-12 col-xl-4 col-xxl-3">
|
|
|
|
|
<div class="card border-0 shadow-sm admin-shell-card admin-sidebar-sticky">
|
|
|
|
|
<div class="card-body p-4">
|
|
|
|
|
<div class="d-flex flex-wrap gap-2 mb-3">
|
|
|
|
|
<span class="badge rounded-pill text-bg-dark">Review hub</span>
|
|
|
|
|
<span class="badge rounded-pill bg-body-tertiary text-secondary border">Bootstrap UI</span>
|
|
|
|
|
</div>
|
|
|
|
|
<h1 class="display-6 fw-semibold mb-3">{escape(view.title)}</h1>
|
|
|
|
|
<p class="text-secondary mb-4">{escape(view.subtitle)}</p>
|
|
|
|
|
|
|
|
|
|
<div class="d-grid gap-2 mb-4">
|
|
|
|
|
<a class="btn btn-dark rounded-pill" href="{escape(view.dashboard_href, quote=True)}">Voltar ao dashboard</a>
|
|
|
|
|
<span class="badge rounded-pill bg-body-tertiary text-secondary border px-3 py-2">Acesso ja autenticado</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="admin-runtime-block p-3 mb-3">
|
|
|
|
|
<p class="text-uppercase small fw-semibold text-secondary mb-3">Resumo do workspace</p>
|
|
|
|
|
<div class="d-grid gap-3 small">
|
|
|
|
|
<div class="d-flex justify-content-between gap-3">
|
|
|
|
|
<span class="text-secondary">Aplicacao</span>
|
|
|
|
|
<strong class="text-end">{escape(view.app_name)}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="d-flex justify-content-between gap-3">
|
|
|
|
|
<span class="text-secondary">Ambiente</span>
|
|
|
|
|
<strong class="text-end text-uppercase">{escape(view.environment)}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="d-flex justify-content-between gap-3">
|
|
|
|
|
<span class="text-secondary">Versao</span>
|
|
|
|
|
<strong class="text-end">{escape(view.version)}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="d-flex justify-content-between gap-3">
|
|
|
|
|
<span class="text-secondary">Atualizacao</span>
|
|
|
|
|
<strong class="text-end" data-tool-review-last-sync>Pendente</strong>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="admin-tool-review-note p-4 mb-3">
|
|
|
|
|
<p class="text-uppercase small fw-semibold text-secondary mb-2">Revisao humana</p>
|
|
|
|
|
<h2 class="h5 fw-semibold mb-3">O que observar na fila</h2>
|
|
|
|
|
<ul class="small text-secondary ps-3 mb-0">
|
|
|
|
|
{review_notes_markup}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="admin-tool-review-note p-4">
|
|
|
|
|
<p class="text-uppercase small fw-semibold text-secondary mb-2">Ativacao controlada</p>
|
|
|
|
|
<h2 class="h5 fw-semibold mb-3">Como fechar a publicacao</h2>
|
|
|
|
|
<ul class="small text-secondary ps-3 mb-0">
|
|
|
|
|
{activation_notes_markup}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
<section class="col-12 col-xl-8 col-xxl-9">
|
|
|
|
|
<div class="card border-0 shadow-sm admin-hero-card overflow-hidden mb-4">
|
|
|
|
|
<div class="card-body p-4 p-lg-5">
|
|
|
|
|
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="d-flex flex-wrap gap-2 mb-3">
|
|
|
|
|
<span class="badge rounded-pill bg-white text-dark border">Governanca de tools</span>
|
|
|
|
|
<span class="badge rounded-pill bg-success-subtle text-success-emphasis border border-success-subtle">Revisao e ativacao</span>
|
|
|
|
|
</div>
|
|
|
|
|
<h2 class="display-5 fw-semibold mb-3">Fluxo visual de aprovacao no painel</h2>
|
|
|
|
|
<p class="lead text-secondary mb-0">
|
|
|
|
|
Esta tela conecta a sessao web do painel aos snapshots administrativos de tools para que o time consiga revisar a fila, conferir contratos e acompanhar o catalogo ativo.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="d-grid gap-2 admin-quick-actions">
|
|
|
|
|
<button class="btn btn-dark btn-lg rounded-pill px-4" type="button" data-admin-tool-refresh>
|
|
|
|
|
<span data-tool-refresh-label>Atualizar leitura</span>
|
|
|
|
|
<span class="spinner-border spinner-border-sm d-none" data-tool-refresh-spinner aria-hidden="true"></span>
|
|
|
|
|
</button>
|
|
|
|
|
<a class="btn btn-outline-dark btn-lg rounded-pill px-4" href="{escape(view.dashboard_href, quote=True)}">Voltar ao overview</a>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="alert d-none rounded-4 mb-4" id="admin-tool-review-feedback" role="status"></div>
|
|
|
|
|
|
|
|
|
|
<div class="row g-3 mb-4">
|
|
|
|
|
<div class="col-12 col-md-4">
|
|
|
|
|
<div class="card border-0 shadow-sm admin-metric-card h-100">
|
|
|
|
|
<div class="card-body p-4">
|
|
|
|
|
<p class="small text-uppercase fw-semibold text-secondary mb-3">Fila de revisao</p>
|
|
|
|
|
<div class="display-6 fw-semibold mb-2" data-tool-review-queue-count>0</div>
|
|
|
|
|
<p class="text-secondary mb-0">Items aguardando leitura tecnica ou aprovacao humana.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-12 col-md-4">
|
|
|
|
|
<div class="card border-0 shadow-sm admin-metric-card h-100">
|
|
|
|
|
<div class="card-body p-4">
|
|
|
|
|
<p class="small text-uppercase fw-semibold text-secondary mb-3">Publicacoes ativas</p>
|
|
|
|
|
<div class="display-6 fw-semibold mb-2" data-tool-review-publication-count>0</div>
|
|
|
|
|
<p class="text-secondary mb-0">Catalogo publicado e pronto para abastecer o runtime de produto.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-12 col-md-4">
|
|
|
|
|
<div class="card border-0 shadow-sm admin-metric-card h-100">
|
|
|
|
|
<div class="card-body p-4">
|
|
|
|
|
<p class="small text-uppercase fw-semibold text-secondary mb-3">Etapas do contrato</p>
|
|
|
|
|
<div class="display-6 fw-semibold mb-2" data-tool-review-lifecycle-count>{len(view.workflow)}</div>
|
|
|
|
|
<p class="text-secondary mb-0">Workflow compartilhado entre revisao, aprovacao e ativacao.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="card border-0 shadow-sm admin-surface-card mb-4">
|
|
|
|
|
<div class="card-body p-4">
|
|
|
|
|
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-4">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-uppercase small fw-semibold text-secondary mb-2">Pipeline visual</p>
|
|
|
|
|
<h3 class="h3 fw-semibold mb-2">Etapas que a tela acompanha</h3>
|
|
|
|
|
<p class="text-secondary mb-0">Os cards abaixo resumem o trajeto de uma tool desde a analise ate a ativacao no produto.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="row g-3">
|
|
|
|
|
{workflow_markup}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="row g-4">
|
|
|
|
|
<div class="col-12 col-xxl-7">
|
|
|
|
|
<div class="card border-0 shadow-sm admin-surface-card h-100">
|
|
|
|
|
<div class="card-body p-4">
|
|
|
|
|
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-uppercase small fw-semibold text-secondary mb-2">Fila atual</p>
|
|
|
|
|
<h3 class="h3 fw-semibold mb-2">Revisao tecnica e aprovacao</h3>
|
|
|
|
|
<p class="text-secondary mb-0">A fila abaixo e lida da superficie web do painel e respeita o papel da sessao autenticada.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="badge rounded-pill bg-body-tertiary text-secondary border" data-tool-review-queue-mode>Bootstrap</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="admin-tool-review-grid" data-tool-review-queue-list>
|
|
|
|
|
<div class="admin-tool-empty-state rounded-4 p-4">
|
|
|
|
|
<h4 class="h5 fw-semibold mb-2">Nenhum item carregado ainda</h4>
|
|
|
|
|
<p class="text-secondary mb-0">Clique em atualizar leitura para sincronizar a fila de revisao do painel.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="col-12 col-xxl-5">
|
|
|
|
|
<div class="card border-0 shadow-sm admin-surface-card h-100">
|
|
|
|
|
<div class="card-body p-4 d-flex flex-column gap-4">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-uppercase small fw-semibold text-secondary mb-2">Checklist de aprovacao</p>
|
|
|
|
|
<h3 class="h3 fw-semibold mb-2">Playbook para a decisao humana</h3>
|
|
|
|
|
<p class="text-secondary mb-0">Aprovacao e ativacao continuam controladas pelo papel administrativo e pela leitura do contrato compartilhado.</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="admin-tool-review-note p-4">
|
|
|
|
|
<p class="text-uppercase small fw-semibold text-secondary mb-2">Antes de aprovar</p>
|
|
|
|
|
<ul class="small text-secondary ps-3 mb-0">
|
|
|
|
|
{approval_notes_markup}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-uppercase small fw-semibold text-secondary mb-2">Lifecycle disponivel</p>
|
|
|
|
|
<div class="vstack gap-2" data-tool-contract-lifecycle>
|
|
|
|
|
<div class="small text-secondary">Aguardando leitura do contrato compartilhado...</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-uppercase small fw-semibold text-secondary mb-2">Tipos de parametro</p>
|
|
|
|
|
<div class="d-flex flex-wrap gap-2" data-tool-parameter-types>
|
|
|
|
|
<span class="badge rounded-pill bg-body-tertiary text-secondary border">Aguardando</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card border-0 shadow-sm admin-surface-card mt-4">
|
|
|
|
|
<div class="card-body p-4">
|
|
|
|
|
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-uppercase small fw-semibold text-secondary mb-2">Catalogo ativo</p>
|
|
|
|
|
<h3 class="h3 fw-semibold mb-2">Ativacao e superficie publicada</h3>
|
|
|
|
|
<p class="text-secondary mb-0">Quando a sessao tem permissao de publicacao, o painel tambem exibe o catalogo conhecido de tools ativas.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="badge rounded-pill bg-body-tertiary text-secondary border" data-tool-publication-source>Catalogo</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="row g-3" data-tool-publication-list>
|
|
|
|
|
<div class="col-12">
|
|
|
|
|
<div class="admin-tool-empty-state rounded-4 p-4">
|
|
|
|
|
<h4 class="h5 fw-semibold mb-2">Catalogo ainda nao sincronizado</h4>
|
|
|
|
|
<p class="text-secondary mb-0">A leitura da ativacao aparece aqui assim que a sessao web carregar as publicacoes disponiveis.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script src="{BOOTSTRAP_JS_HREF}" defer></script>
|
|
|
|
|
<script src="{escape(js_href, quote=True)}" defer></script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _render_tool_review_workflow(items: tuple[AdminToolReviewWorkflowStep, ...]) -> str:
|
|
|
|
|
cards: list[str] = []
|
|
|
|
|
for item in items:
|
|
|
|
|
badge_classes = _BADGE_CLASS_MAP.get(item.status_variant, _BADGE_CLASS_MAP["secondary"])
|
|
|
|
|
cards.append(
|
|
|
|
|
f"""
|
|
|
|
|
<div class="col-12 col-md-6 col-xxl-3">
|
|
|
|
|
<article class="admin-tool-workflow-card p-4 h-100">
|
|
|
|
|
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="small text-uppercase fw-semibold text-secondary mb-2">{escape(item.eyebrow)}</p>
|
|
|
|
|
<h4 class="h5 fw-semibold mb-0">{escape(item.title)}</h4>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="badge rounded-pill {badge_classes}">{escape(item.status_label)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="text-secondary mb-0">{escape(item.description)}</p>
|
|
|
|
|
</article>
|
|
|
|
|
</div>
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
return "\n".join(cards)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|