feat: "itens sem permissão não aparecem" — filtro de menu por permission (secao 169)
getUserPermissionKeys(userId, tenantId?) (novo, packages/auth) devolve todas as permission keys do usuário no contexto atual, mesma query de userHasPermission só que retornando o conjunto inteiro. GET /auth/me agora inclui permissionKeys — leitura adicional só pra UX, nunca decisão de autorização (isso continua sendo o PermissionGuard em cada endpoint). NavLeaf/NavSection ganham campo opcional permission; todos os ~35 itens de menu (tenant e platform) anotados com a permission key mínima que o endpoint GET correspondente já exige. filterNavByPermissions() (novo, nav-types.ts) esconde o item, ou a seção inteira se nenhum filho sobrar. TenantSidebar/PlatformSidebar/*Topbar são client components que importam TENANT_NAV/PLATFORM_NAV direto (não recebem como prop do server) porque os ícones (LucideIcon, funções) não são serializáveis através da fronteira RSC — restrição já documentada desde a PHASE 23. O filtro roda dentro desses wrappers, client-side, recebendo só permissionKeys: string[] (serializável) como prop. Testado ponta a ponta com um supervisor de teste de verdade (convidado, senha trocada, logado via UI): menu perde Telefonia > Dialplan e a seção Administração inteira (nenhum permission bate). Confirmado que a autorização real continua valendo sem o item no menu: acesso direto a /app/administracao/usuarios continua batendo 403 no backend. Regressão: tenant_admin e platform_super_admin continuam vendo todo item do próprio menu. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
@@ -10,6 +10,7 @@ interface Me {
|
||||
name: string;
|
||||
isPlatformUser: boolean;
|
||||
tenant: { id: string; code: string; name: string } | null;
|
||||
permissionKeys: string[];
|
||||
}
|
||||
|
||||
export default async function TenantAppLayout({ children }: { children: React.ReactNode }) {
|
||||
@@ -23,9 +24,9 @@ export default async function TenantAppLayout({ children }: { children: React.Re
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh overflow-hidden bg-background">
|
||||
<TenantSidebar railLabel={me.tenant.name} />
|
||||
<TenantSidebar railLabel={me.tenant.name} permissionKeys={me.permissionKeys} />
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<TenantTopbar fallbackTitle={me.tenant.name} userLabel={me.email} />
|
||||
<TenantTopbar fallbackTitle={me.tenant.name} userLabel={me.email} permissionKeys={me.permissionKeys} />
|
||||
<main className="flex-1 overflow-y-auto p-4 sm:p-6">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ interface Me {
|
||||
email: string;
|
||||
name: string;
|
||||
isPlatformUser: boolean;
|
||||
permissionKeys: string[];
|
||||
}
|
||||
|
||||
export default async function PlatformLayout({ children }: { children: React.ReactNode }) {
|
||||
@@ -18,9 +19,9 @@ export default async function PlatformLayout({ children }: { children: React.Rea
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh overflow-hidden bg-background">
|
||||
<PlatformSidebar />
|
||||
<PlatformSidebar permissionKeys={me.permissionKeys} />
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<PlatformTopbar userLabel={me.email} />
|
||||
<PlatformTopbar userLabel={me.email} permissionKeys={me.permissionKeys} />
|
||||
<main className="flex-1 overflow-y-auto p-4 sm:p-6">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { LayoutDashboard, Building2, CreditCard, ServerCog, Sparkles, Settings2 } from "lucide-react";
|
||||
import type { NavSection } from "../shell/nav-types";
|
||||
|
||||
/** IA fixa do menu Platform (agente.md secao 168) — "Visão Geral" e
|
||||
* "Billing > Tarifas" têm página construída até agora, o resto existe pra
|
||||
* provar a arquitetura completa (Product Principle #5 em PRODUCT.md: esta
|
||||
* é a implementação de referência do design system pras próximas telas),
|
||||
* renderizado como indisponível em vez de virar link morto. Os outros 3
|
||||
* itens de Billing (Consumo/Fechamentos/Relatórios) esperam um seletor de
|
||||
* tenant real — não existe endpoint de listagem de tenants ainda (ver
|
||||
* docs/BILLING.md), então ficam "em breve" até essa peça existir. */
|
||||
/** IA fixa do menu Platform (agente.md secao 168). Todo item aqui tem uma
|
||||
* tela real por trás (fechado na PHASE 46) — "permission" em cada um bate
|
||||
* com o `@RequirePermission` do endpoint GET que a tela chama, usado por
|
||||
* `filterNavByPermissions` (secao 169: "itens sem permissão não
|
||||
* aparecem"). Só platform_super_admin chega neste menu (o layout redireciona
|
||||
* quem não é platform user antes de renderizar), mas a filtragem continua
|
||||
* valendo caso um dia exista uma role de plataforma mais restrita. */
|
||||
export const PLATFORM_NAV: NavSection[] = [
|
||||
{
|
||||
label: "Visão Geral",
|
||||
icon: LayoutDashboard,
|
||||
href: "/platform",
|
||||
description: "Estado agregado de todos os tenants, agora",
|
||||
permission: "tenants.view",
|
||||
},
|
||||
{
|
||||
label: "Clientes",
|
||||
@@ -24,21 +24,25 @@ export const PLATFORM_NAV: NavSection[] = [
|
||||
label: "Tenants",
|
||||
href: "/platform/clientes/tenants",
|
||||
description: "Clientes da plataforma — criar cria também o primeiro usuário",
|
||||
permission: "tenants.view",
|
||||
},
|
||||
{
|
||||
label: "Planos",
|
||||
href: "/platform/clientes/planos",
|
||||
description: "Catálogo de limites e recursos por plano",
|
||||
permission: "pricing.manage",
|
||||
},
|
||||
{
|
||||
label: "Assinaturas",
|
||||
href: "/platform/clientes/assinaturas",
|
||||
description: "Versão de preço assinada e ciclo de billing por tenant",
|
||||
permission: "billing.manage",
|
||||
},
|
||||
{
|
||||
label: "Quotas",
|
||||
href: "/platform/clientes/quotas",
|
||||
description: "Uso vs. limite do plano — todos os tenants, quem está perto do limite",
|
||||
permission: "tenants.view",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -50,21 +54,25 @@ export const PLATFORM_NAV: NavSection[] = [
|
||||
label: "Consumo",
|
||||
href: "/platform/billing/consumo",
|
||||
description: "Uso bruto de todos os tenants no mês corrente",
|
||||
permission: "billing.view",
|
||||
},
|
||||
{
|
||||
label: "Tarifas",
|
||||
href: "/platform/billing/tarifas",
|
||||
description: "Price books e rate decks — catálogos globais de preço",
|
||||
permission: "pricing.manage",
|
||||
},
|
||||
{
|
||||
label: "Fechamentos",
|
||||
href: "/platform/billing/fechamentos",
|
||||
description: "Fechar/reabrir período de billing por tenant",
|
||||
permission: "billing.manage",
|
||||
},
|
||||
{
|
||||
label: "Relatórios",
|
||||
href: "/platform/billing/relatorios",
|
||||
description: "Statements gerados por tenant",
|
||||
permission: "billing.view",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -76,21 +84,25 @@ export const PLATFORM_NAV: NavSection[] = [
|
||||
label: "FreeSWITCH",
|
||||
href: "/platform/infraestrutura/freeswitch",
|
||||
description: "Canais e chamadas ativas agora, direto do ESL",
|
||||
permission: "freeswitch.view",
|
||||
},
|
||||
{
|
||||
label: "SIP Profiles",
|
||||
href: "/platform/infraestrutura/sip-profiles",
|
||||
description: "sofia status e registros SIP ativos",
|
||||
permission: "freeswitch.view",
|
||||
},
|
||||
{
|
||||
label: "Nodes",
|
||||
href: "/platform/infraestrutura/nodes",
|
||||
description: "Nodes FreeSWITCH desta implantação (hoje: 1)",
|
||||
permission: "freeswitch.view",
|
||||
},
|
||||
{
|
||||
label: "Saúde",
|
||||
href: "/platform/infraestrutura/saude",
|
||||
description: "Postgres, Redis e FreeSWITCH — verificação ao vivo",
|
||||
permission: "freeswitch.view",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -102,21 +114,25 @@ export const PLATFORM_NAV: NavSection[] = [
|
||||
label: "Providers",
|
||||
href: "/platform/ia/providers",
|
||||
description: "Providers de IA globais, visíveis por qualquer tenant",
|
||||
permission: "ai.view",
|
||||
},
|
||||
{
|
||||
label: "Modelos",
|
||||
href: "/platform/ia/modelos",
|
||||
description: "Modelos dos providers globais, com custo unitário",
|
||||
permission: "ai.view",
|
||||
},
|
||||
{
|
||||
label: "Uso",
|
||||
href: "/platform/ia/uso",
|
||||
description: "Uso bruto de IA de todos os tenants no mês corrente",
|
||||
permission: "tenants.view",
|
||||
},
|
||||
{
|
||||
label: "Custos",
|
||||
href: "/platform/ia/custos",
|
||||
description: "Uso × custo unitário — estimado, nunca um número fixo",
|
||||
permission: "tenants.view",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -128,21 +144,25 @@ export const PLATFORM_NAV: NavSection[] = [
|
||||
label: "Usuários",
|
||||
href: "/platform/sistema/usuarios",
|
||||
description: "Todos os usuários da plataforma, cross-tenant",
|
||||
permission: "users.manage",
|
||||
},
|
||||
{
|
||||
label: "Permissões",
|
||||
href: "/platform/sistema/permissoes",
|
||||
description: "Catálogo de roles e permissions do sistema",
|
||||
permission: "roles.manage",
|
||||
},
|
||||
{
|
||||
label: "Auditoria",
|
||||
href: "/platform/sistema/auditoria",
|
||||
description: "Log de eventos de todos os tenants",
|
||||
permission: "audit.view",
|
||||
},
|
||||
{
|
||||
label: "Configurações",
|
||||
href: "/platform/sistema/configuracoes",
|
||||
description: "Flags de segurança/infra do .env — somente leitura",
|
||||
permission: "tenants.view",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Sidebar } from "@/components/shell/sidebar";
|
||||
import { filterNavByPermissions } from "@/components/shell/nav-types";
|
||||
import { PLATFORM_NAV } from "./nav-data";
|
||||
|
||||
/**
|
||||
@@ -10,8 +11,10 @@ import { PLATFORM_NAV } from "./nav-data";
|
||||
* (client) um valor que já nasceu no bundle client; importar `nav-data`
|
||||
* aqui dentro (em vez de receber `items` como prop vindo do server)
|
||||
* resolve isso. Bug real, achado testando o app do tenant: "Functions
|
||||
* cannot be passed directly to Client Components".
|
||||
* cannot be passed directly to Client Components". Mesma razão pra
|
||||
* filtrar por permission (secao 169) aqui dentro, sobre o array já no
|
||||
* bundle client, em vez de receber os itens já filtrados como prop.
|
||||
*/
|
||||
export function PlatformSidebar() {
|
||||
return <Sidebar items={PLATFORM_NAV} railLabel="PLATFORM" />;
|
||||
export function PlatformSidebar({ permissionKeys }: { permissionKeys: string[] }) {
|
||||
return <Sidebar items={filterNavByPermissions(PLATFORM_NAV, permissionKeys)} railLabel="PLATFORM" />;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { Topbar } from "@/components/shell/topbar";
|
||||
import { filterNavByPermissions } from "@/components/shell/nav-types";
|
||||
import { PLATFORM_NAV } from "./nav-data";
|
||||
|
||||
/** Mesma razão do `PlatformSidebar` — ver comentário lá. */
|
||||
export function PlatformTopbar({ userLabel }: { userLabel: string }) {
|
||||
return <Topbar items={PLATFORM_NAV} fallbackTitle="Platform" userLabel={userLabel} />;
|
||||
export function PlatformTopbar({ userLabel, permissionKeys }: { userLabel: string; permissionKeys: string[] }) {
|
||||
return <Topbar items={filterNavByPermissions(PLATFORM_NAV, permissionKeys)} fallbackTitle="Platform" userLabel={userLabel} />;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ export interface NavLeaf {
|
||||
href?: string;
|
||||
/** Subtítulo mostrado na topbar quando esta rota está ativa. */
|
||||
description?: string;
|
||||
/** Permission key mínima pra ver este item (secao 169: "itens sem
|
||||
* permissão não aparecem") — bate com o `@RequirePermission` do
|
||||
* endpoint GET que a tela chama. Omitido = sempre visível (ex.:
|
||||
* Dashboard, que todo usuário autenticado do tenant enxerga). */
|
||||
permission?: string;
|
||||
}
|
||||
|
||||
export interface NavSection {
|
||||
@@ -12,9 +17,29 @@ export interface NavSection {
|
||||
icon: LucideIcon;
|
||||
href?: string;
|
||||
description?: string;
|
||||
permission?: string;
|
||||
children?: NavLeaf[];
|
||||
}
|
||||
|
||||
/** Filtra o menu pelas permissions do usuário (secao 169) — só UX, nunca
|
||||
* autorização de verdade (isso continua sendo o `PermissionGuard` em
|
||||
* cada endpoint). Uma seção com `children` só aparece se sobrar pelo
|
||||
* menos 1 filho visível; uma seção com `href` direto (sem children) é
|
||||
* filtrada pela própria `permission`. */
|
||||
export function filterNavByPermissions(items: NavSection[], permissionKeys: string[]): NavSection[] {
|
||||
const has = (permission?: string) => !permission || permissionKeys.includes(permission);
|
||||
|
||||
return items
|
||||
.map((section) => {
|
||||
if (section.children) {
|
||||
const children = section.children.filter((child) => has(child.permission));
|
||||
return children.length > 0 ? { ...section, children } : null;
|
||||
}
|
||||
return has(section.permission) ? section : null;
|
||||
})
|
||||
.filter((section): section is NavSection => section != null);
|
||||
}
|
||||
|
||||
/** Título+descrição da topbar pra rota atual — casa pelo `href` mais longo
|
||||
* (uma subrota como `/price-books/123` ainda resolve pro item de menu
|
||||
* "Tarifas"), nunca deixa a topbar com um título de outra página (bug
|
||||
|
||||
@@ -11,16 +11,11 @@ import {
|
||||
} from "lucide-react";
|
||||
import type { NavSection } from "../shell/nav-types";
|
||||
|
||||
/** IA fixa do menu Tenant (agente.md secao 169). Páginas construídas até
|
||||
* agora: Dashboard (PHASE 23), Telefonia > Ramais (PHASE 25), Discador >
|
||||
* Campanhas (PHASE 26), Call Center > Filas/Pausas/Disposições, Telefonia
|
||||
* > Troncos e Discador > Lista de Bloqueio (PHASE 27) — o resto existe pra
|
||||
* provar a arquitetura completa (Product Principle #5 em PRODUCT.md),
|
||||
* renderizado como indisponível em vez de virar link morto. "Itens sem
|
||||
* permissão não aparecem" (secao 169) ainda não é aplicado aqui — este
|
||||
* primeiro corte mostra a IA inteira pra qualquer usuário tenant
|
||||
* autenticado; filtrar por permission real fica pra quando mais telas
|
||||
* existirem pra testar contra. */
|
||||
/** IA fixa do menu Tenant (agente.md secao 169). Todo item aqui tem uma
|
||||
* tela real por trás (fechado na PHASE 46) — "permission" em cada um bate
|
||||
* com o `@RequirePermission` do endpoint GET que a tela chama, usado por
|
||||
* `filterNavByPermissions` (secao 169: "itens sem permissão não
|
||||
* aparecem"). Item sem `permission` = sempre visível (Dashboard). */
|
||||
export const TENANT_NAV: NavSection[] = [
|
||||
{
|
||||
label: "Dashboard",
|
||||
@@ -36,21 +31,25 @@ export const TENANT_NAV: NavSection[] = [
|
||||
label: "Campanhas",
|
||||
href: "/app/discador/campanhas",
|
||||
description: "Discador preditivo — campanhas deste tenant",
|
||||
permission: "campaigns.view",
|
||||
},
|
||||
{
|
||||
label: "Leads",
|
||||
href: "/app/discador/leads",
|
||||
description: "Leads de uma campanha — buscar, adicionar, remover",
|
||||
permission: "campaigns.view",
|
||||
},
|
||||
{
|
||||
label: "Callbacks",
|
||||
href: "/app/discador/callbacks",
|
||||
description: "Leads que pediram retorno em outro horário — remarcar, tentar agora ou cancelar",
|
||||
permission: "campaigns.view",
|
||||
},
|
||||
{
|
||||
label: "Lista de Bloqueio",
|
||||
href: "/app/discador/bloqueio",
|
||||
description: "Números que o discador nunca chama",
|
||||
permission: "campaigns.view",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -62,21 +61,25 @@ export const TENANT_NAV: NavSection[] = [
|
||||
label: "Agentes",
|
||||
href: "/app/callcenter/agentes",
|
||||
description: "Identidade de call center — liga um usuário a um agente e a um ramal",
|
||||
permission: "agents.view",
|
||||
},
|
||||
{
|
||||
label: "Filas",
|
||||
href: "/app/callcenter/filas",
|
||||
description: "Filas de atendimento deste tenant",
|
||||
permission: "queues.view",
|
||||
},
|
||||
{
|
||||
label: "Pausas",
|
||||
href: "/app/callcenter/pausas",
|
||||
description: "Motivos de pausa que o agente pode escolher",
|
||||
permission: "agents.view",
|
||||
},
|
||||
{
|
||||
label: "Disposições",
|
||||
href: "/app/callcenter/disposicoes",
|
||||
description: "Motivos de encerramento de chamada",
|
||||
permission: "agents.view",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -88,16 +91,19 @@ export const TENANT_NAV: NavSection[] = [
|
||||
label: "Ramais",
|
||||
href: "/app/telefonia/ramais",
|
||||
description: "Ramais SIP deste tenant — números, senha, caller ID",
|
||||
permission: "extensions.view",
|
||||
},
|
||||
{
|
||||
label: "Troncos",
|
||||
href: "/app/telefonia/troncos",
|
||||
description: "Troncos SIP deste tenant",
|
||||
permission: "trunks.view",
|
||||
},
|
||||
{
|
||||
label: "Dialplan",
|
||||
href: "/app/telefonia/dialplan",
|
||||
description: "Editor de regras do contexto default — condição, ações, versões",
|
||||
permission: "freeswitch.view",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -106,12 +112,14 @@ export const TENANT_NAV: NavSection[] = [
|
||||
icon: Radar,
|
||||
href: "/app/monitoramento",
|
||||
description: "Eventos ao vivo — filas, agentes e chamadas em tempo real",
|
||||
permission: "monitoring.view",
|
||||
},
|
||||
{
|
||||
label: "Gravações",
|
||||
icon: Mic,
|
||||
href: "/app/gravacoes",
|
||||
description: "Chamadas gravadas deste tenant — player autenticado, sem URL direta pro storage",
|
||||
permission: "recordings.view",
|
||||
},
|
||||
{
|
||||
label: "IA",
|
||||
@@ -121,21 +129,25 @@ export const TENANT_NAV: NavSection[] = [
|
||||
label: "Análises",
|
||||
href: "/app/relatorios/ia",
|
||||
description: "Dashboard de IA — score, sentimento, assuntos, compliance",
|
||||
permission: "reports.view",
|
||||
},
|
||||
{
|
||||
label: "Scorecards",
|
||||
href: "/app/ia/scorecards",
|
||||
description: "Critérios ponderados que a IA avalia em toda chamada analisada",
|
||||
permission: "ai.view",
|
||||
},
|
||||
{
|
||||
label: "Prompts",
|
||||
href: "/app/ia/prompts",
|
||||
description: "Templates de prompt usados na análise e no scorecard",
|
||||
permission: "ai.view",
|
||||
},
|
||||
{
|
||||
label: "Configurações",
|
||||
href: "/app/ia/configuracoes",
|
||||
description: "Providers e modelos de IA — BYOK deste tenant",
|
||||
permission: "ai.view",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -147,26 +159,31 @@ export const TENANT_NAV: NavSection[] = [
|
||||
label: "Chamadas",
|
||||
href: "/app/relatorios/chamadas",
|
||||
description: "Lista de chamadas — direção, fila, agente, duração, encerramento",
|
||||
permission: "reports.view",
|
||||
},
|
||||
{
|
||||
label: "Agentes",
|
||||
href: "/app/relatorios/agentes",
|
||||
description: "Tempo logado/pausado/em chamada e TMA por agente",
|
||||
permission: "reports.view",
|
||||
},
|
||||
{
|
||||
label: "Filas",
|
||||
href: "/app/relatorios/filas",
|
||||
description: "Recebidas/atendidas/abandonadas, TME/TMA, Service Level",
|
||||
permission: "reports.view",
|
||||
},
|
||||
{
|
||||
label: "Campanhas",
|
||||
href: "/app/relatorios/campanhas",
|
||||
description: "Leads, tentativas, taxas de atendimento e contato",
|
||||
permission: "reports.view",
|
||||
},
|
||||
{
|
||||
label: "Consumo",
|
||||
href: "/app/relatorios/consumo",
|
||||
description: "Uso bruto do mês — minutos, dias ativos, armazenamento, tokens de IA",
|
||||
permission: "reports.view",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -178,16 +195,19 @@ export const TENANT_NAV: NavSection[] = [
|
||||
label: "Usuários",
|
||||
href: "/app/administracao/usuarios",
|
||||
description: "Quem tem acesso a este tenant — convidar e trocar papel",
|
||||
permission: "users.manage",
|
||||
},
|
||||
{
|
||||
label: "Perfis",
|
||||
href: "/app/administracao/perfis",
|
||||
description: "O que cada papel pode fazer neste tenant",
|
||||
permission: "users.manage",
|
||||
},
|
||||
{
|
||||
label: "Configurações",
|
||||
href: "/app/administracao/configuracoes",
|
||||
description: "Dados do tenant — nome fantasia, fuso, idioma, privacidade de IA",
|
||||
permission: "users.manage",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { Sidebar } from "@/components/shell/sidebar";
|
||||
import { filterNavByPermissions } from "@/components/shell/nav-types";
|
||||
import { TENANT_NAV } from "./nav-data";
|
||||
|
||||
/** Ver comentário em `platform-shell/platform-sidebar.tsx` — mesma razão
|
||||
* (ícones não são serializáveis através da fronteira RSC). */
|
||||
export function TenantSidebar({ railLabel }: { railLabel: string }) {
|
||||
return <Sidebar items={TENANT_NAV} railLabel={railLabel} />;
|
||||
* (ícones não são serializáveis através da fronteira RSC) — por isso o
|
||||
* filtro por permission (secao 169) acontece aqui, client-side, sobre o
|
||||
* `TENANT_NAV` importado no bundle, nunca recebendo os itens já
|
||||
* filtrados como prop do server. */
|
||||
export function TenantSidebar({ railLabel, permissionKeys }: { railLabel: string; permissionKeys: string[] }) {
|
||||
return <Sidebar items={filterNavByPermissions(TENANT_NAV, permissionKeys)} railLabel={railLabel} />;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { Topbar } from "@/components/shell/topbar";
|
||||
import { filterNavByPermissions } from "@/components/shell/nav-types";
|
||||
import { TENANT_NAV } from "./nav-data";
|
||||
|
||||
/** Ver comentário em `platform-shell/platform-sidebar.tsx` — mesma razão. */
|
||||
export function TenantTopbar({ fallbackTitle, userLabel }: { fallbackTitle: string; userLabel: string }) {
|
||||
return <Topbar items={TENANT_NAV} fallbackTitle={fallbackTitle} userLabel={userLabel} />;
|
||||
export function TenantTopbar({
|
||||
fallbackTitle,
|
||||
userLabel,
|
||||
permissionKeys,
|
||||
}: {
|
||||
fallbackTitle: string;
|
||||
userLabel: string;
|
||||
permissionKeys: string[];
|
||||
}) {
|
||||
return <Topbar items={filterNavByPermissions(TENANT_NAV, permissionKeys)} fallbackTitle={fallbackTitle} userLabel={userLabel} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user