feat: refactor billing plans section and remove unused components
- Updated BillingPlansSection.vue to clean up unused code and improve readability. - Removed CardPopover.vue and VideoGrid.vue components as they were no longer needed. - Enhanced VideoTable.vue by integrating BaseTable for better table management and added loading states. - Introduced secure JSON transformer for enhanced data security in RPC routes. - Added key resolver for managing server key pairs. - Created a script to generate NaCl keys for secure communications. - Implemented admin page header management for better UI consistency.
This commit is contained in:
@@ -1,85 +1,142 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import PageHeader from "@/components/dashboard/PageHeader.vue";
|
||||
import { computed, provide } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { adminPageHeaderKey, createAdminPageHeaderState } from "./components/useAdminPageHeader";
|
||||
|
||||
const route = useRoute();
|
||||
const pageHeader = createAdminPageHeaderState();
|
||||
|
||||
const sections = [
|
||||
{ to: "/admin/overview", label: "Overview", description: "KPIs, usage and runtime pulse" },
|
||||
{ to: "/admin/users", label: "Users", description: "Accounts, plans and moderation" },
|
||||
{ to: "/admin/videos", label: "Videos", description: "Cross-user media inventory" },
|
||||
{ to: "/admin/payments", label: "Payments", description: "Revenue, invoices and state changes" },
|
||||
{ to: "/admin/plans", label: "Plans", description: "Catalog and subscription offers" },
|
||||
{ to: "/admin/ad-templates", label: "Ad Templates", description: "VAST templates and defaults" },
|
||||
{ to: "/admin/jobs", label: "Jobs", description: "Queue, retries and live logs" },
|
||||
{ to: "/admin/agents", label: "Agents", description: "Workers, health and maintenance" },
|
||||
{ to: "/admin/logs", label: "Logs", description: "Direct runtime log lookup" },
|
||||
provide(adminPageHeaderKey, pageHeader);
|
||||
|
||||
const menuSections = [
|
||||
{
|
||||
title: "Workspace",
|
||||
items: [
|
||||
{ to: "/admin/overview", label: "Overview", description: "KPIs, usage and runtime pulse" },
|
||||
{ to: "/admin/users", label: "Users", description: "Accounts, plans and moderation" },
|
||||
{ to: "/admin/videos", label: "Videos", description: "Cross-user media inventory" },
|
||||
{ to: "/admin/payments", label: "Payments", description: "Revenue, invoices and state changes" },
|
||||
{ to: "/admin/plans", label: "Plans", description: "Catalog and subscription offers" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Operations",
|
||||
items: [
|
||||
{ to: "/admin/ad-templates", label: "Ad Templates", description: "VAST templates and defaults" },
|
||||
{ to: "/admin/jobs", label: "Jobs", description: "Queue, retries and live logs" },
|
||||
{ to: "/admin/agents", label: "Agents", description: "Workers, health and maintenance" },
|
||||
{ to: "/admin/logs", label: "Logs", description: "Direct runtime log lookup" },
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
|
||||
const allSections = computed(() => menuSections.flatMap((section) => section.items));
|
||||
const activeSection = computed(() => {
|
||||
return sections.find((section) => route.path === section.to || route.path.startsWith(`${section.to}/`)) ?? sections[0];
|
||||
return allSections.value.find((section) => route.path === section.to || route.path.startsWith(`${section.to}/`)) ?? allSections.value[0];
|
||||
});
|
||||
|
||||
const breadcrumbs = computed(() => [
|
||||
{ label: "Dashboard", to: "/overview" },
|
||||
{ label: "Admin", to: "/admin/overview" },
|
||||
...(activeSection.value ? [{ label: activeSection.value.label }] : []),
|
||||
]);
|
||||
|
||||
const content = computed(() => ({
|
||||
"admin-overview": {
|
||||
title: "Overview",
|
||||
subtitle: "KPIs, usage and runtime pulse across the admin workspace.",
|
||||
},
|
||||
"admin-users": {
|
||||
title: "Users",
|
||||
subtitle: "Accounts, plans and moderation tools for the full user base.",
|
||||
},
|
||||
"admin-videos": {
|
||||
title: "Videos",
|
||||
subtitle: "Cross-user media inventory, review and operational controls.",
|
||||
},
|
||||
"admin-payments": {
|
||||
title: "Payments",
|
||||
subtitle: "Revenue records, invoices and payment state operations.",
|
||||
},
|
||||
"admin-plans": {
|
||||
title: "Plans",
|
||||
subtitle: "Subscription catalog management and offer maintenance.",
|
||||
},
|
||||
"admin-ad-templates": {
|
||||
title: "Ad Templates",
|
||||
subtitle: "VAST templates, ownership metadata and default assignments.",
|
||||
},
|
||||
"admin-jobs": {
|
||||
title: "Jobs",
|
||||
subtitle: "Queue state, retries and runtime execution tracking.",
|
||||
},
|
||||
"admin-agents": {
|
||||
title: "Agents",
|
||||
subtitle: "Connected workers, health checks and maintenance actions.",
|
||||
},
|
||||
"admin-logs": {
|
||||
title: "Logs",
|
||||
subtitle: "Persisted output lookup and live runtime tailing.",
|
||||
},
|
||||
}));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="space-y-5">
|
||||
<div class="overflow-hidden rounded-[28px] border border-slate-200 bg-[radial-gradient(circle_at_top_left,rgba(14,165,233,0.12),transparent_38%),linear-gradient(135deg,#020617,#0f172a_52%,#111827)] px-6 py-6 text-white shadow-[0_24px_80px_-40px_rgba(15,23,42,0.8)]">
|
||||
<div class="flex flex-col gap-6 xl:flex-row xl:items-end xl:justify-between">
|
||||
<div class="max-w-3xl space-y-3">
|
||||
<div class="inline-flex items-center rounded-full border border-white/15 bg-white/8 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-100">
|
||||
Admin Console
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-semibold tracking-tight text-white md:text-4xl">Operate the entire Stream workspace from one surface.</h1>
|
||||
<p class="mt-2 max-w-2xl text-sm leading-6 text-slate-300 md:text-base">
|
||||
Screen coverage is aligned around the current Tiny-RPC + gRPC admin contract. Use the navigation below to jump between CRUD workflows, runtime operations and diagnostics.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-3 xl:min-w-[420px]">
|
||||
<div class="rounded-2xl border border-white/10 bg-white/8 p-4 backdrop-blur-sm">
|
||||
<div class="text-[11px] uppercase tracking-[0.22em] text-slate-400">Current module</div>
|
||||
<div class="mt-2 text-lg font-semibold text-white">{{ activeSection.label }}</div>
|
||||
<div class="mt-1 text-sm text-slate-300">{{ activeSection.description }}</div>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-white/10 bg-white/8 p-4 backdrop-blur-sm">
|
||||
<div class="text-[11px] uppercase tracking-[0.22em] text-slate-400">Coverage</div>
|
||||
<div class="mt-2 text-lg font-semibold text-white">{{ sections.length }} screens</div>
|
||||
<div class="mt-1 text-sm text-slate-300">Overview, CRUD, runtime and logs.</div>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-white/10 bg-white/8 p-4 backdrop-blur-sm">
|
||||
<div class="text-[11px] uppercase tracking-[0.22em] text-slate-400">Data path</div>
|
||||
<div class="mt-2 text-lg font-semibold text-white">Tiny-RPC</div>
|
||||
<div class="mt-1 text-sm text-slate-300">Canonical gRPC-backed admin transport.</div>
|
||||
</div>
|
||||
</div>
|
||||
<section>
|
||||
<div class="space-y-3">
|
||||
<div v-if="pageHeader.eyebrow || pageHeader.badge" class="flex flex-wrap items-center gap-2">
|
||||
<span v-if="pageHeader.eyebrow" class="inline-flex items-center rounded-full border border-primary/15 bg-primary/8 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-primary">
|
||||
{{ pageHeader.eyebrow }}
|
||||
</span>
|
||||
<span v-if="pageHeader.badge" class="inline-flex items-center rounded-full border border-border bg-white px-2.5 py-1 text-[11px] font-medium text-foreground/60">
|
||||
{{ pageHeader.badge }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<PageHeader
|
||||
:title="content[route.name as keyof typeof content]?.title || 'Workspace administration'"
|
||||
:description="content[route.name as keyof typeof content]?.subtitle || 'Quản lý dữ liệu, vận hành và chẩn đoán hệ thống theo cùng bố cục với khu settings.'"
|
||||
:breadcrumbs="breadcrumbs"
|
||||
:actions="pageHeader.actions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-5 xl:grid-cols-[260px_minmax(0,1fr)]">
|
||||
<aside class="rounded-[28px] border border-slate-200 bg-white p-3 shadow-[0_20px_60px_-40px_rgba(15,23,42,0.35)]">
|
||||
<nav class="space-y-1">
|
||||
<router-link
|
||||
v-for="section in sections"
|
||||
:key="section.to"
|
||||
:to="section.to"
|
||||
class="group flex items-start gap-3 rounded-2xl px-4 py-3 transition-all duration-200"
|
||||
:class="route.path === section.to || route.path.startsWith(`${section.to}/`) ? 'bg-slate-950 text-white shadow-[0_18px_36px_-24px_rgba(15,23,42,0.8)]' : 'text-slate-700 hover:bg-slate-50'"
|
||||
>
|
||||
<div class="mt-0.5 h-2.5 w-2.5 rounded-full" :class="route.path === section.to || route.path.startsWith(`${section.to}/`) ? 'bg-sky-400' : 'bg-slate-300 group-hover:bg-slate-500'" />
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-semibold tracking-tight">{{ section.label }}</div>
|
||||
<div class="mt-1 text-xs leading-5" :class="route.path === section.to || route.path.startsWith(`${section.to}/`) ? 'text-slate-300' : 'text-slate-500'">
|
||||
{{ section.description }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</nav>
|
||||
</aside>
|
||||
<div class="max-w-7xl mx-auto pb-12">
|
||||
<div class="mt-6 flex flex-col gap-8 md:flex-row">
|
||||
<aside class="md:w-56 shrink-0">
|
||||
<div class="mb-8 rounded-lg border border-border bg-header px-4 py-4">
|
||||
<div class="text-sm font-semibold text-foreground">{{ activeSection?.label }}</div>
|
||||
<p class="mt-1 text-sm text-foreground/60">{{ activeSection?.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0">
|
||||
<router-view />
|
||||
<nav class="space-y-6">
|
||||
<div v-for="section in menuSections" :key="section.title">
|
||||
<h3 class="mb-2 pl-3 text-xs font-semibold uppercase tracking-wider text-foreground/50">
|
||||
{{ section.title }}
|
||||
</h3>
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="item in section.items" :key="item.to">
|
||||
<router-link
|
||||
:to="item.to"
|
||||
:class="[
|
||||
'flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-all duration-150',
|
||||
route.path === item.to || route.path.startsWith(`${item.to}/`)
|
||||
? 'bg-primary/10 text-primary font-semibold'
|
||||
: 'text-foreground/70 hover:bg-header hover:text-foreground'
|
||||
]"
|
||||
>
|
||||
{{ item.label }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="flex-1 min-w-0">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user