Signal dependent tabs to re-fetch when plan ladders change. operator_plan_ladders handlers set HX-Trigger: planLadderMutation on create/update/delete and tier operations so Org Types' plan dropdowns refresh. operator.html adds explanatory comments and hx-trigger attrs so Org Types, Grants, and Products panes listen for productMutation, planLadderMutation, and entitlementMutation
342 lines
20 KiB
HTML
342 lines
20 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<title>Operator - Wiki Cafe</title>
|
|
<meta name="description" content="Operator console for Wiki Cafe member management">
|
|
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
<meta name="htmx-config" content='{"includeIndicatorStyles": false}'>
|
|
<link href="/static/bootstrap.css" rel="stylesheet">
|
|
<script defer src="/static/bootstrap.bundle.js"></script>
|
|
<script defer src="/static/htmx.min.js"></script>
|
|
<script defer src="/static/error-handler.js"></script>
|
|
<script defer src="/static/grant-toggle.js"></script>
|
|
<script defer src="/static/operator-tabs.js"></script>
|
|
<script defer src="/static/confirm-action-modal.js"></script>
|
|
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
|
<link rel="manifest" href="/static/site.webmanifest">
|
|
|
|
<link href="/static/app.css" rel="stylesheet">
|
|
</head>
|
|
|
|
<body class="d-flex flex-column vh-100" hx-headers='{"X-CSRF-Token": "{{ .CSRFToken }}"}'>
|
|
<!-- Navbar -->
|
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
|
<div class="container-fluid">
|
|
<a class="navbar-brand" href="#">Wiki Cafe Member Console</a>
|
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
|
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
|
<span class="navbar-toggler-icon"></span>
|
|
</button>
|
|
<div class="collapse navbar-collapse" id="navbarNav">
|
|
<ul class="navbar-nav ms-auto d-lg-none">
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="/">Dashboard</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="/products">Products</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link active" href="/operator">Operator</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="{{ .KeycloakAccountURL }}" target="_blank">Identity and Access</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link text-danger" href="/logout">Logout</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="d-flex flex-column flex-lg-row flex-grow-1">
|
|
<!-- Sidebar - only visible on lg screens and up -->
|
|
<div class="bg-light d-none d-lg-flex flex-column flex-shrink-0 h-lg-100 border-end">
|
|
<ul class="nav flex-column p-3">
|
|
<li class="nav-item"><a class="nav-link" href="/">Dashboard</a></li>
|
|
<li class="nav-item"><a class="nav-link" href="/products">Products</a></li>
|
|
<li class="nav-item"><a class="nav-link active" href="/operator"><strong>Operator</strong></a></li>
|
|
<li class="nav-item"><a class="nav-link" href="{{ .KeycloakAccountURL }}" target="_blank">Identity and
|
|
Access</a></li>
|
|
<li class="nav-item"><a class="nav-link text-danger" href="/logout">Logout</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<div class="flex-fill p-4">
|
|
<h1>Operator</h1>
|
|
|
|
<!-- Tabs -->
|
|
<ul class="nav nav-tabs mt-4" id="operatorTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="people-tab" data-bs-toggle="tab" data-bs-target="#people-pane"
|
|
type="button" role="tab" aria-controls="people-pane" aria-selected="true">People</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="orgs-tab" data-bs-toggle="tab" data-bs-target="#orgs-pane"
|
|
type="button" role="tab" aria-controls="orgs-pane" aria-selected="false">Organizations</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="org-types-tab" data-bs-toggle="tab" data-bs-target="#org-types-pane"
|
|
type="button" role="tab" aria-controls="org-types-pane" aria-selected="false">Org Types</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="grants-tab" data-bs-toggle="tab" data-bs-target="#grants-pane"
|
|
type="button" role="tab" aria-controls="grants-pane" aria-selected="false">Grants</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="products-tab" data-bs-toggle="tab" data-bs-target="#products-pane"
|
|
type="button" role="tab" aria-controls="products-pane" aria-selected="false">Products</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="plan-ladders-tab" data-bs-toggle="tab" data-bs-target="#plan-ladders-pane"
|
|
type="button" role="tab" aria-controls="plan-ladders-pane" aria-selected="false">Plan Ladders</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="entitlement-sets-tab" data-bs-toggle="tab" data-bs-target="#entitlement-sets-pane"
|
|
type="button" role="tab" aria-controls="entitlement-sets-pane" aria-selected="false">Entitlement Sets</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="billing-tab" data-bs-toggle="tab" data-bs-target="#billing-pane"
|
|
type="button" role="tab" aria-controls="billing-pane" aria-selected="false">Billing</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="sites-tab" data-bs-toggle="tab" data-bs-target="#sites-pane"
|
|
type="button" role="tab" aria-controls="sites-pane" aria-selected="false">FedWiki Sites</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="tab-content border border-top-0 rounded-bottom p-3" id="operatorTabsContent">
|
|
<!-- People Tab -->
|
|
<div class="tab-pane fade show active" id="people-pane" role="tabpanel" aria-labelledby="people-tab">
|
|
<div hx-get="/partials/operator/users" hx-trigger="load" hx-swap="innerHTML">
|
|
<div class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Loading people...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Organizations Tab -->
|
|
<div class="tab-pane fade" id="orgs-pane" role="tabpanel" aria-labelledby="orgs-tab">
|
|
<div id="orgsContent" hx-get="/partials/operator/organizations" hx-trigger="revealed" hx-swap="innerHTML">
|
|
<div class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Loading organizations...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Org Types Tab -->
|
|
<div class="tab-pane fade" id="org-types-pane" role="tabpanel" aria-labelledby="org-types-tab">
|
|
<!-- Cross-tab refresh: productMutation (operator_products.go) keeps the
|
|
default-product preview fresh; planLadderMutation (operator_plan_ladders.go)
|
|
keeps the Default Plan dropdown fresh after a ladder/tier mutation. -->
|
|
<div id="orgTypesContent" hx-get="/partials/operator/org-types" hx-trigger="revealed, productMutation from:body, planLadderMutation from:body" hx-swap="innerHTML">
|
|
<div class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Loading organization types...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Grants Tab -->
|
|
<div class="tab-pane fade" id="grants-pane" role="tabpanel" aria-labelledby="grants-tab">
|
|
<!-- Cross-tab refresh: productMutation (operator_products.go) keeps the
|
|
product picker fresh; entitlementMutation (operator_entitlement_sets.go)
|
|
keeps grant-derived entitlement displays fresh. -->
|
|
<div id="grantsContent" hx-get="/partials/operator/grants" hx-trigger="revealed, productMutation from:body, entitlementMutation from:body" hx-swap="innerHTML">
|
|
<div class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Loading grants...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Products Tab -->
|
|
<div class="tab-pane fade" id="products-pane" role="tabpanel" aria-labelledby="products-tab">
|
|
<!-- Cross-tab refresh: entitlementMutation (operator_entitlement_sets.go)
|
|
keeps the entitlement-set column on the products list fresh. -->
|
|
<div id="productsContent" hx-get="/partials/operator/products" hx-trigger="revealed, entitlementMutation from:body" hx-swap="innerHTML">
|
|
<div class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Loading products...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Plan Ladders Tab -->
|
|
<div class="tab-pane fade" id="plan-ladders-pane" role="tabpanel" aria-labelledby="plan-ladders-tab">
|
|
<div id="planLaddersContent" hx-get="/partials/operator/plan-ladders" hx-trigger="revealed" hx-swap="innerHTML">
|
|
<div class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Loading plan ladders...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Entitlement Sets Tab -->
|
|
<div class="tab-pane fade" id="entitlement-sets-pane" role="tabpanel" aria-labelledby="entitlement-sets-tab">
|
|
<div id="entitlementSetsContent" hx-get="/partials/operator/entitlement-sets" hx-trigger="revealed" hx-swap="innerHTML">
|
|
<div class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Loading entitlement sets...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Billing Tab -->
|
|
<div class="tab-pane fade" id="billing-pane" role="tabpanel" aria-labelledby="billing-tab">
|
|
<!-- Billing Sub-navigation -->
|
|
<ul class="nav nav-pills mb-3" id="billingSubTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="billing-accounts-tab" data-bs-toggle="pill" data-bs-target="#billing-accounts-pane"
|
|
type="button" role="tab" aria-controls="billing-accounts-pane" aria-selected="true">Accounts</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="billing-subscriptions-tab" data-bs-toggle="pill" data-bs-target="#billing-subscriptions-pane"
|
|
type="button" role="tab" aria-controls="billing-subscriptions-pane" aria-selected="false">Subscriptions</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="billing-invoices-tab" data-bs-toggle="pill" data-bs-target="#billing-invoices-pane"
|
|
type="button" role="tab" aria-controls="billing-invoices-pane" aria-selected="false">Invoices</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="billing-payments-tab" data-bs-toggle="pill" data-bs-target="#billing-payments-pane"
|
|
type="button" role="tab" aria-controls="billing-payments-pane" aria-selected="false">Payments</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- Billing Sub-tab Content -->
|
|
<div class="tab-content" id="billingSubTabsContent">
|
|
<!-- Billing Accounts -->
|
|
<div class="tab-pane fade show active" id="billing-accounts-pane" role="tabpanel" aria-labelledby="billing-accounts-tab">
|
|
<div id="billingAccountsContent" hx-get="/partials/operator/billing/accounts" hx-trigger="revealed" hx-swap="innerHTML">
|
|
<div class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Loading billing accounts...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Subscriptions -->
|
|
<div class="tab-pane fade" id="billing-subscriptions-pane" role="tabpanel" aria-labelledby="billing-subscriptions-tab">
|
|
<div id="billingSubscriptionsContent" hx-get="/partials/operator/billing/subscriptions" hx-trigger="revealed" hx-swap="innerHTML">
|
|
<div class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Loading subscriptions...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Invoices -->
|
|
<div class="tab-pane fade" id="billing-invoices-pane" role="tabpanel" aria-labelledby="billing-invoices-tab">
|
|
<div id="billingInvoicesContent" hx-get="/partials/operator/billing/invoices" hx-trigger="revealed" hx-swap="innerHTML">
|
|
<div class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Loading invoices...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Payments -->
|
|
<div class="tab-pane fade" id="billing-payments-pane" role="tabpanel" aria-labelledby="billing-payments-tab">
|
|
<div id="billingPaymentsContent" hx-get="/partials/operator/billing/payments" hx-trigger="revealed" hx-swap="innerHTML">
|
|
<div class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Loading payments...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- FedWiki Sites Tab -->
|
|
<div class="tab-pane fade" id="sites-pane" role="tabpanel" aria-labelledby="sites-tab">
|
|
<div hx-get="/partials/operator/sites" hx-trigger="revealed" hx-swap="innerHTML">
|
|
<div class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Loading sites...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error Toast Container -->
|
|
<div class="toast-container">
|
|
<div id="errorToast" class="toast align-items-center text-bg-danger border-0" role="alert" aria-live="assertive"
|
|
aria-atomic="true">
|
|
<div class="d-flex">
|
|
<div class="toast-body" id="errorToastBody">
|
|
An error occurred. Please try again.
|
|
</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"
|
|
aria-label="Close"></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Shared confirmation modal (driven by data-action-* attrs on triggers) -->
|
|
<div class="modal fade" id="confirmActionModal" tabindex="-1" aria-labelledby="confirmActionModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="confirmActionModalLabel">Confirm</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<form id="confirmActionForm"
|
|
hx-swap="innerHTML"
|
|
hx-disabled-elt="find button[type=submit]"
|
|
hx-indicator="find .htmx-indicator">
|
|
<div class="modal-body">
|
|
<p id="confirmActionBody" class="mb-0"></p>
|
|
<div id="confirmActionHiddenFields"></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-danger" id="confirmActionSubmit">
|
|
<span class="htmx-indicator spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
|
<span id="confirmActionLabel">Confirm</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</body>
|
|
|
|
</html> |