@burdenoff/microfe-admin 2026.805.1 → 2026.829.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/AdminRoot.js +77 -72
- package/dist/admin/AdminRoot.js.map +1 -1
- package/dist/admin/AdminRoutes.js +28 -24
- package/dist/admin/AdminRoutes.js.map +1 -1
- package/dist/admin/components/TenantMembersPanel.js +105 -106
- package/dist/admin/components/TenantMembersPanel.js.map +1 -1
- package/dist/admin/hooks/useAdminPermissions.js +1 -0
- package/dist/admin/hooks/useAdminPermissions.js.map +1 -1
- package/dist/admin/hooks/useTr.js +14 -0
- package/dist/admin/hooks/useTr.js.map +1 -0
- package/dist/admin/pages/DashboardPage.js +71 -72
- package/dist/admin/pages/DashboardPage.js.map +1 -1
- package/dist/admin/pages/ExploreConversationsPage.js +419 -0
- package/dist/admin/pages/ExploreConversationsPage.js.map +1 -0
- package/dist/admin/pages/OrganizationsListPage.js +63 -62
- package/dist/admin/pages/OrganizationsListPage.js.map +1 -1
- package/dist/admin/pages/TenantDetailPage.js +148 -149
- package/dist/admin/pages/TenantDetailPage.js.map +1 -1
- package/dist/admin/pages/TenantsListPage.js +106 -107
- package/dist/admin/pages/TenantsListPage.js.map +1 -1
- package/dist/admin/pages/UsersListPage.js +87 -88
- package/dist/admin/pages/UsersListPage.js.map +1 -1
- package/dist/admin/pages/index.js +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TenantDetailPage.js","names":[],"sources":["../../../src/admin/pages/TenantDetailPage.tsx"],"sourcesContent":["import { useParams, useNavigate } from 'react-router';\nimport {\n ArrowLeft,\n Building2,\n Users,\n Mail,\n Calendar,\n Globe,\n Settings,\n Ban,\n CheckCircle,\n Loader2,\n} from 'lucide-react';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Button,\n Card,\n CardHeader,\n CardTitle,\n CardContent,\n Avatar,\n TenantStatusBadge,\n Separator,\n useToast,\n PermissionGuard,\n} from '../components';\nimport { TenantMembersPanel } from '../components/TenantMembersPanel';\nimport { TenantSsoPanel } from '../components/TenantSsoPanel';\nimport { PagePurpose, NextSteps, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { joinPath } from '../utils/navigation';\nimport { formatDate } from '../utils/formatters';\nimport { mapTenantToUi } from '../utils/mappers';\nimport type { Tenant } from '../types';\nimport {\n useGetTenantQuery,\n useSuspendTenantMutation,\n useActivateTenantMutation,\n} from '../../generated/global-operations';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useFeatureFlag } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { tWithFallback } from '../utils/i18n';\nimport { emitTenantLifecycle } from '../utils/tenantEvents';\nimport { TENANT_FEATURE_FLAGS } from '../constants/featureFlags';\n\n/**\n * Tenant detail page\n */\nexport function TenantDetailPage() {\n const { canViewTenants, canManageTenants, isLoading: permissionsLoading } = useAdminPermissions();\n\n const { tenantId } = useParams<{ tenantId: string }>();\n const navigate = useNavigate();\n const { basePath = '' } = useAdmin();\n const toast = useToast();\n const { t } = useI18n();\n const managementFeatureEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.management, true);\n const detailFeatureEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.detail, true);\n const editFeatureEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.edit, true);\n const pageFeatureEnabled = managementFeatureEnabled && detailFeatureEnabled;\n const canWriteTenants = canManageTenants && editFeatureEnabled;\n const canLifecycleTenants = canManageTenants && managementFeatureEnabled;\n const tr = (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params);\n\n const { data, loading, error } = useGetTenantQuery({\n variables: { id: tenantId! },\n skip: !tenantId || permissionsLoading || !canViewTenants || !pageFeatureEnabled,\n fetchPolicy: 'cache-and-network',\n });\n\n const [suspendTenant, { loading: suspending }] = useSuspendTenantMutation();\n const [activateTenant, { loading: activating }] = useActivateTenantMutation();\n const events = useEventBus();\n\n const tenant: Tenant | null = data?.tenant ? mapTenantToUi(data.tenant) : null;\n\n const handleSuspend = async () => {\n if (!tenantId) return;\n // Capture before the await: a refetch could null `tenant` mid-mutation,\n // and we want consumers of `tenant.tenant.suspended` to see the slug as\n // it was when the user clicked suspend.\n const slugAtClick = tenant?.slug;\n try {\n await suspendTenant({ variables: { id: tenantId } });\n emitTenantLifecycle(events, 'tenant.tenant.suspended', {\n tenantId,\n tenantSlug: slugAtClick,\n status: 'SUSPENDED',\n });\n toast.success(\n tr('admin.tenantDetail.toast.suspendedTitle', 'Tenant suspended'),\n tr('admin.tenantDetail.toast.suspendedDescription', '{{name}} has been suspended.', {\n name: tenant?.name ?? '',\n })\n );\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.toast.suspendFailed', 'Failed to suspend tenant.')\n );\n }\n };\n\n const handleActivate = async () => {\n if (!tenantId) return;\n const slugAtClick = tenant?.slug;\n try {\n await activateTenant({ variables: { id: tenantId } });\n emitTenantLifecycle(events, 'tenant.tenant.activated', {\n tenantId,\n tenantSlug: slugAtClick,\n status: 'ACTIVE',\n });\n toast.success(\n tr('admin.tenantDetail.toast.activatedTitle', 'Tenant activated'),\n tr('admin.tenantDetail.toast.activatedDescription', '{{name}} has been activated.', {\n name: tenant?.name ?? '',\n })\n );\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.toast.activateFailed', 'Failed to activate tenant.')\n );\n }\n };\n\n const workspaces = data?.tenant?.workspaces ?? [];\n\n if (!permissionsLoading && !canViewTenants) {\n return (\n <PermissionGuard\n allowed={false}\n isLoading={false}\n pageTitle={tr('admin.tenantDetail.pageTitle', 'Tenant Details')}\n denyMessage={tr(\n 'admin.tenantDetail.denyMessage',\n \"You don't have permission to view tenant details.\"\n )}\n >\n {null}\n </PermissionGuard>\n );\n }\n\n if (!pageFeatureEnabled) {\n return (\n <PageLayout\n title={tr('admin.tenantDetail.featureDisabledTitle', 'Tenant management unavailable')}\n description={tr(\n 'admin.tenantDetail.featureDisabled',\n 'This feature is currently disabled.'\n )}\n >\n {null}\n </PageLayout>\n );\n }\n\n return (\n <PermissionGuard\n allowed={canViewTenants}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.tenantDetail.pageTitle', 'Tenant Details')}\n denyMessage={tr(\n 'admin.tenantDetail.denyMessage',\n \"You don't have permission to view tenant details.\"\n )}\n >\n {loading ? (\n <PageLayout title={tr('admin.tenantDetail.loadingTitle', 'Loading...')}>\n <div className=\"flex h-64 items-center justify-center\">\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n </div>\n </PageLayout>\n ) : error || !tenant ? (\n <PageLayout\n title={tr('admin.tenantDetail.notFoundTitle', 'Tenant Not Found')}\n description={tr(\n 'admin.tenantDetail.notFoundDescription',\n 'The requested tenant could not be found'\n )}\n >\n {error && (\n <div className=\"mb-4 rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n {error.message}\n </div>\n )}\n <Button variant=\"outline\" onClick={() => navigate(joinPath(basePath, 'tenants'))}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.backToTenants', 'Back to Tenants')}\n </Button>\n </PageLayout>\n ) : (\n <PageLayout\n title={tenant.name}\n description={tr('admin.tenantDetail.tenantId', 'Tenant ID: {{id}}', { id: tenant.id })}\n actions={\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" onClick={() => navigate(joinPath(basePath, 'tenants'))}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.back', 'Back')}\n </Button>\n {canWriteTenants && (\n <Button\n variant=\"outline\"\n onClick={() => navigate(joinPath(basePath, `tenants/${tenant.id}/edit`))}\n >\n <Settings className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.edit', 'Edit')}\n </Button>\n )}\n {canLifecycleTenants &&\n (tenant.status === 'active' ? (\n <Button\n variant=\"outline\"\n className=\"text-status-warning-text\"\n onClick={handleSuspend}\n disabled={suspending}\n >\n {suspending ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <Ban className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantDetail.suspend', 'Suspend')}\n </Button>\n ) : (\n <Button\n variant=\"outline\"\n className=\"text-status-success-text\"\n onClick={handleActivate}\n disabled={activating}\n >\n {activating ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantDetail.activate', 'Activate')}\n </Button>\n ))}\n </div>\n }\n >\n <PagePurpose className=\"mb-6\">\n {tr(\n 'admin.tenantDetail.purpose',\n 'Everything about a single tenant in one place — their profile, the people inside it, and their single sign-on setup. From here you manage that one customer account: review who has access, adjust their SSO, and suspend or reactivate them when needed.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey={`admin-tenant-detail`}\n steps={[\n {\n id: 'edit-tenant',\n label: tr('admin.tenantDetail.nextSteps.edit', 'Edit tenant details'),\n description: tr(\n 'admin.tenantDetail.nextSteps.editDesc',\n 'Update the name, domain or plan for this tenant.'\n ),\n onClick: () => navigate(joinPath(basePath, `tenants/${tenant.id}/edit`)),\n done: !canWriteTenants,\n },\n {\n id: 'review-members',\n label: tr('admin.tenantDetail.nextSteps.members', 'Review members'),\n description: tr(\n 'admin.tenantDetail.nextSteps.membersDesc',\n 'Check who has access inside this tenant.'\n ),\n href: '#tenant-members',\n },\n {\n id: 'configure-sso',\n label: tr('admin.tenantDetail.nextSteps.sso', 'Configure SSO'),\n description: tr(\n 'admin.tenantDetail.nextSteps.ssoDesc',\n 'Set up single sign-on so their team logs in with your IdP.'\n ),\n href: '#tenant-sso',\n },\n ]}\n />\n\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Main Info */}\n <div className=\"space-y-6 lg:col-span-2\">\n {/* Overview — the primary emphasis zone for this page. */}\n <EmphasisPanel className=\"p-0\">\n <Card className=\"border-0 bg-transparent shadow-none hover:border-0\">\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.overview.title', 'Overview')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"flex items-start gap-4\">\n <Avatar fallback={tenant.name.slice(0, 2).toUpperCase()} size=\"lg\" />\n <div className=\"flex-1 space-y-4\">\n <div>\n <h2 className=\"text-xl font-semibold text-text-primary\">{tenant.name}</h2>\n <p className=\"text-sm text-text-secondary\">@{tenant.slug}</p>\n </div>\n <div className=\"flex flex-wrap items-center gap-4\">\n <TenantStatusBadge status={tenant.status} />\n {tenant.plan && (\n <span className=\"rounded-md bg-action-primary-bg/10 px-2 py-1 text-xs font-medium text-primary\">\n {tenant.plan}\n </span>\n )}\n </div>\n </div>\n </div>\n\n <Separator className=\"my-6\" />\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {tenant.ownerEmail && (\n <div className=\"flex items-center gap-3\">\n <Mail className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.ownerEmail', 'Owner Email')}\n </p>\n <p className=\"text-sm font-medium\">{tenant.ownerEmail}</p>\n </div>\n </div>\n )}\n {tenant.domain && (\n <div className=\"flex items-center gap-3\">\n <Globe className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.domain', 'Domain')}\n </p>\n <p className=\"text-sm font-medium\">{tenant.domain}</p>\n </div>\n </div>\n )}\n <div className=\"flex items-center gap-3\">\n <Calendar className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.created', 'Created')}\n </p>\n <p className=\"text-sm font-medium\">{formatDate(tenant.createdAt)}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <Calendar className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.lastUpdated', 'Last Updated')}\n </p>\n <p className=\"text-sm font-medium\">{formatDate(tenant.updatedAt)}</p>\n </div>\n </div>\n </div>\n </CardContent>\n </Card>\n </EmphasisPanel>\n\n {/* Workspaces */}\n {workspaces.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.workspaces.title', 'Workspaces ({{count}})', {\n count: workspaces.length,\n })}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-2\">\n {workspaces.map(\n (ws: { id: string; name: string; status?: string | null }) => (\n <div\n key={ws.id}\n className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\"\n >\n <span className=\"text-sm font-medium\">{ws.name}</span>\n <span className=\"text-xs text-text-secondary capitalize\">\n {ws.status?.toLowerCase()}\n </span>\n </div>\n )\n )}\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Config */}\n {tenant.settings && (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.settings.title', 'Settings')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.allowSignup', 'Allow Signup')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.allowSignup\n ? 'text-status-success-text'\n : 'text-text-secondary'\n }`}\n >\n {tenant.settings?.allowSignup\n ? tr('admin.tenantDetail.settings.enabled', 'Enabled')\n : tr('admin.tenantDetail.settings.disabled', 'Disabled')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.sso', 'SSO')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.ssoEnabled\n ? 'text-status-success-text'\n : 'text-text-secondary'\n }`}\n >\n {tenant.settings?.ssoEnabled\n ? tr('admin.tenantDetail.settings.enabled', 'Enabled')\n : tr('admin.tenantDetail.settings.disabled', 'Disabled')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.mfaRequired', 'MFA Required')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.mfaRequired\n ? 'text-status-success-text'\n : 'text-text-secondary'\n }`}\n >\n {tenant.settings?.mfaRequired\n ? tr('admin.tenantDetail.settings.yes', 'Yes')\n : tr('admin.tenantDetail.settings.no', 'No')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.dataRetention', 'Data Retention')}\n </span>\n <span className=\"text-sm font-medium\">\n {tr('admin.tenantDetail.settings.dataRetentionDays', '{{days}} days', {\n days: tenant.settings?.dataRetentionDays ?? 0,\n })}\n </span>\n </div>\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* SSO Configuration */}\n <div id=\"tenant-sso\">\n <TenantSsoPanel tenantId={tenant.id} canManage={canWriteTenants} />\n </div>\n\n {/* Tenant Members (BOFF-2652) — gates createOrganization on DEDICATED tenants */}\n <div id=\"tenant-members\">\n <TenantMembersPanel\n tenantId={tenant.id}\n tenantType={tenant.type ?? ''}\n canManage={canWriteTenants}\n />\n </div>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6\">\n {/* Actions */}\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.actions.title', 'Actions')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n <Button\n variant=\"outline\"\n className=\"w-full justify-start\"\n onClick={() => navigate(joinPath(basePath, `users?tenantId=${tenant.id}`))}\n >\n <Users className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.actions.viewUsers', 'View Users')}\n </Button>\n <Button\n variant=\"outline\"\n className=\"w-full justify-start\"\n onClick={() =>\n navigate(joinPath(basePath, `billing/subscriptions?tenantId=${tenant.id}`))\n }\n >\n <Building2 className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.actions.viewBilling', 'View Billing')}\n </Button>\n </CardContent>\n </Card>\n </div>\n </div>\n </PageLayout>\n )}\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TenantDetailPage.js","names":[],"sources":["../../../src/admin/pages/TenantDetailPage.tsx"],"sourcesContent":["import { useParams, useNavigate } from 'react-router';\nimport {\n ArrowLeft,\n Building2,\n Users,\n Mail,\n Calendar,\n Globe,\n Settings,\n Ban,\n CheckCircle,\n Loader2,\n} from 'lucide-react';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Button,\n Card,\n CardHeader,\n CardTitle,\n CardContent,\n Avatar,\n TenantStatusBadge,\n Separator,\n useToast,\n PermissionGuard,\n} from '../components';\nimport { TenantMembersPanel } from '../components/TenantMembersPanel';\nimport { TenantSsoPanel } from '../components/TenantSsoPanel';\nimport { PagePurpose, NextSteps, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { joinPath } from '../utils/navigation';\nimport { formatDate } from '../utils/formatters';\nimport { mapTenantToUi } from '../utils/mappers';\nimport type { Tenant } from '../types';\nimport {\n useGetTenantQuery,\n useSuspendTenantMutation,\n useActivateTenantMutation,\n} from '../../generated/global-operations';\nimport { useFeatureFlag } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { useTr } from '../hooks/useTr';\nimport { emitTenantLifecycle } from '../utils/tenantEvents';\nimport { TENANT_FEATURE_FLAGS } from '../constants/featureFlags';\n\n/**\n * Tenant detail page\n */\nexport function TenantDetailPage() {\n const { canViewTenants, canManageTenants, isLoading: permissionsLoading } = useAdminPermissions();\n\n const { tenantId } = useParams<{ tenantId: string }>();\n const navigate = useNavigate();\n const { basePath = '' } = useAdmin();\n const toast = useToast();\n const tr = useTr();\n const managementFeatureEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.management, true);\n const detailFeatureEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.detail, true);\n const editFeatureEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.edit, true);\n const pageFeatureEnabled = managementFeatureEnabled && detailFeatureEnabled;\n const canWriteTenants = canManageTenants && editFeatureEnabled;\n const canLifecycleTenants = canManageTenants && managementFeatureEnabled;\n\n const { data, loading, error } = useGetTenantQuery({\n variables: { id: tenantId! },\n skip: !tenantId || permissionsLoading || !canViewTenants || !pageFeatureEnabled,\n fetchPolicy: 'cache-and-network',\n });\n\n const [suspendTenant, { loading: suspending }] = useSuspendTenantMutation();\n const [activateTenant, { loading: activating }] = useActivateTenantMutation();\n const events = useEventBus();\n\n const tenant: Tenant | null = data?.tenant ? mapTenantToUi(data.tenant) : null;\n\n const handleSuspend = async () => {\n if (!tenantId) return;\n // Capture before the await: a refetch could null `tenant` mid-mutation,\n // and we want consumers of `tenant.tenant.suspended` to see the slug as\n // it was when the user clicked suspend.\n const slugAtClick = tenant?.slug;\n try {\n await suspendTenant({ variables: { id: tenantId } });\n emitTenantLifecycle(events, 'tenant.tenant.suspended', {\n tenantId,\n tenantSlug: slugAtClick,\n status: 'SUSPENDED',\n });\n toast.success(\n tr('admin.tenantDetail.toast.suspendedTitle', 'Tenant suspended'),\n tr('admin.tenantDetail.toast.suspendedDescription', '{{name}} has been suspended.', {\n name: tenant?.name ?? '',\n })\n );\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.toast.suspendFailed', 'Failed to suspend tenant.')\n );\n }\n };\n\n const handleActivate = async () => {\n if (!tenantId) return;\n const slugAtClick = tenant?.slug;\n try {\n await activateTenant({ variables: { id: tenantId } });\n emitTenantLifecycle(events, 'tenant.tenant.activated', {\n tenantId,\n tenantSlug: slugAtClick,\n status: 'ACTIVE',\n });\n toast.success(\n tr('admin.tenantDetail.toast.activatedTitle', 'Tenant activated'),\n tr('admin.tenantDetail.toast.activatedDescription', '{{name}} has been activated.', {\n name: tenant?.name ?? '',\n })\n );\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.toast.activateFailed', 'Failed to activate tenant.')\n );\n }\n };\n\n const workspaces = data?.tenant?.workspaces ?? [];\n\n if (!permissionsLoading && !canViewTenants) {\n return (\n <PermissionGuard\n allowed={false}\n isLoading={false}\n pageTitle={tr('admin.tenantDetail.pageTitle', 'Tenant Details')}\n denyMessage={tr(\n 'admin.tenantDetail.denyMessage',\n \"You don't have permission to view tenant details.\"\n )}\n >\n {null}\n </PermissionGuard>\n );\n }\n\n if (!pageFeatureEnabled) {\n return (\n <PageLayout\n title={tr('admin.tenantDetail.featureDisabledTitle', 'Tenant management unavailable')}\n description={tr(\n 'admin.tenantDetail.featureDisabled',\n 'This feature is currently disabled.'\n )}\n >\n {null}\n </PageLayout>\n );\n }\n\n return (\n <PermissionGuard\n allowed={canViewTenants}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.tenantDetail.pageTitle', 'Tenant Details')}\n denyMessage={tr(\n 'admin.tenantDetail.denyMessage',\n \"You don't have permission to view tenant details.\"\n )}\n >\n {loading ? (\n <PageLayout title={tr('admin.tenantDetail.loadingTitle', 'Loading...')}>\n <div className=\"flex h-64 items-center justify-center\">\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n </div>\n </PageLayout>\n ) : error || !tenant ? (\n <PageLayout\n title={tr('admin.tenantDetail.notFoundTitle', 'Tenant Not Found')}\n description={tr(\n 'admin.tenantDetail.notFoundDescription',\n 'The requested tenant could not be found'\n )}\n >\n {error && (\n <div className=\"mb-4 rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n {error.message}\n </div>\n )}\n <Button variant=\"outline\" onClick={() => navigate(joinPath(basePath, 'tenants'))}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.backToTenants', 'Back to Tenants')}\n </Button>\n </PageLayout>\n ) : (\n <PageLayout\n title={tenant.name}\n description={tr('admin.tenantDetail.tenantId', 'Tenant ID: {{id}}', { id: tenant.id })}\n actions={\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" onClick={() => navigate(joinPath(basePath, 'tenants'))}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.back', 'Back')}\n </Button>\n {canWriteTenants && (\n <Button\n variant=\"outline\"\n onClick={() => navigate(joinPath(basePath, `tenants/${tenant.id}/edit`))}\n >\n <Settings className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.edit', 'Edit')}\n </Button>\n )}\n {canLifecycleTenants &&\n (tenant.status === 'active' ? (\n <Button\n variant=\"outline\"\n className=\"text-status-warning-text\"\n onClick={handleSuspend}\n disabled={suspending}\n >\n {suspending ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <Ban className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantDetail.suspend', 'Suspend')}\n </Button>\n ) : (\n <Button\n variant=\"outline\"\n className=\"text-status-success-text\"\n onClick={handleActivate}\n disabled={activating}\n >\n {activating ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantDetail.activate', 'Activate')}\n </Button>\n ))}\n </div>\n }\n >\n <PagePurpose className=\"mb-6\">\n {tr(\n 'admin.tenantDetail.purpose',\n 'Everything about a single tenant in one place — their profile, the people inside it, and their single sign-on setup. From here you manage that one customer account: review who has access, adjust their SSO, and suspend or reactivate them when needed.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey={`admin-tenant-detail`}\n steps={[\n {\n id: 'edit-tenant',\n label: tr('admin.tenantDetail.nextSteps.edit', 'Edit tenant details'),\n description: tr(\n 'admin.tenantDetail.nextSteps.editDesc',\n 'Update the name, domain or plan for this tenant.'\n ),\n onClick: () => navigate(joinPath(basePath, `tenants/${tenant.id}/edit`)),\n done: !canWriteTenants,\n },\n {\n id: 'review-members',\n label: tr('admin.tenantDetail.nextSteps.members', 'Review members'),\n description: tr(\n 'admin.tenantDetail.nextSteps.membersDesc',\n 'Check who has access inside this tenant.'\n ),\n href: '#tenant-members',\n },\n {\n id: 'configure-sso',\n label: tr('admin.tenantDetail.nextSteps.sso', 'Configure SSO'),\n description: tr(\n 'admin.tenantDetail.nextSteps.ssoDesc',\n 'Set up single sign-on so their team logs in with your IdP.'\n ),\n href: '#tenant-sso',\n },\n ]}\n />\n\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Main Info */}\n <div className=\"space-y-6 lg:col-span-2\">\n {/* Overview — the primary emphasis zone for this page. */}\n <EmphasisPanel className=\"p-0\">\n <Card className=\"border-0 bg-transparent shadow-none hover:border-0\">\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.overview.title', 'Overview')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"flex items-start gap-4\">\n <Avatar fallback={tenant.name.slice(0, 2).toUpperCase()} size=\"lg\" />\n <div className=\"flex-1 space-y-4\">\n <div>\n <h2 className=\"text-xl font-semibold text-text-primary\">{tenant.name}</h2>\n <p className=\"text-sm text-text-secondary\">@{tenant.slug}</p>\n </div>\n <div className=\"flex flex-wrap items-center gap-4\">\n <TenantStatusBadge status={tenant.status} />\n {tenant.plan && (\n <span className=\"rounded-md bg-action-primary-bg/10 px-2 py-1 text-xs font-medium text-primary\">\n {tenant.plan}\n </span>\n )}\n </div>\n </div>\n </div>\n\n <Separator className=\"my-6\" />\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {tenant.ownerEmail && (\n <div className=\"flex items-center gap-3\">\n <Mail className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.ownerEmail', 'Owner Email')}\n </p>\n <p className=\"text-sm font-medium\">{tenant.ownerEmail}</p>\n </div>\n </div>\n )}\n {tenant.domain && (\n <div className=\"flex items-center gap-3\">\n <Globe className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.domain', 'Domain')}\n </p>\n <p className=\"text-sm font-medium\">{tenant.domain}</p>\n </div>\n </div>\n )}\n <div className=\"flex items-center gap-3\">\n <Calendar className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.created', 'Created')}\n </p>\n <p className=\"text-sm font-medium\">{formatDate(tenant.createdAt)}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <Calendar className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.lastUpdated', 'Last Updated')}\n </p>\n <p className=\"text-sm font-medium\">{formatDate(tenant.updatedAt)}</p>\n </div>\n </div>\n </div>\n </CardContent>\n </Card>\n </EmphasisPanel>\n\n {/* Workspaces */}\n {workspaces.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.workspaces.title', 'Workspaces ({{count}})', {\n count: workspaces.length,\n })}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-2\">\n {workspaces.map(\n (ws: { id: string; name: string; status?: string | null }) => (\n <div\n key={ws.id}\n className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\"\n >\n <span className=\"text-sm font-medium\">{ws.name}</span>\n <span className=\"text-xs text-text-secondary capitalize\">\n {ws.status?.toLowerCase()}\n </span>\n </div>\n )\n )}\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Config */}\n {tenant.settings && (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.settings.title', 'Settings')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.allowSignup', 'Allow Signup')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.allowSignup\n ? 'text-status-success-text'\n : 'text-text-secondary'\n }`}\n >\n {tenant.settings?.allowSignup\n ? tr('admin.tenantDetail.settings.enabled', 'Enabled')\n : tr('admin.tenantDetail.settings.disabled', 'Disabled')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.sso', 'SSO')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.ssoEnabled\n ? 'text-status-success-text'\n : 'text-text-secondary'\n }`}\n >\n {tenant.settings?.ssoEnabled\n ? tr('admin.tenantDetail.settings.enabled', 'Enabled')\n : tr('admin.tenantDetail.settings.disabled', 'Disabled')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.mfaRequired', 'MFA Required')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.mfaRequired\n ? 'text-status-success-text'\n : 'text-text-secondary'\n }`}\n >\n {tenant.settings?.mfaRequired\n ? tr('admin.tenantDetail.settings.yes', 'Yes')\n : tr('admin.tenantDetail.settings.no', 'No')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.dataRetention', 'Data Retention')}\n </span>\n <span className=\"text-sm font-medium\">\n {tr('admin.tenantDetail.settings.dataRetentionDays', '{{days}} days', {\n days: tenant.settings?.dataRetentionDays ?? 0,\n })}\n </span>\n </div>\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* SSO Configuration */}\n <div id=\"tenant-sso\">\n <TenantSsoPanel tenantId={tenant.id} canManage={canWriteTenants} />\n </div>\n\n {/* Tenant Members (BOFF-2652) — gates createOrganization on DEDICATED tenants */}\n <div id=\"tenant-members\">\n <TenantMembersPanel\n tenantId={tenant.id}\n tenantType={tenant.type ?? ''}\n canManage={canWriteTenants}\n />\n </div>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6\">\n {/* Actions */}\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.actions.title', 'Actions')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n <Button\n variant=\"outline\"\n className=\"w-full justify-start\"\n onClick={() => navigate(joinPath(basePath, `users?tenantId=${tenant.id}`))}\n >\n <Users className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.actions.viewUsers', 'View Users')}\n </Button>\n <Button\n variant=\"outline\"\n className=\"w-full justify-start\"\n onClick={() =>\n navigate(joinPath(basePath, `billing/subscriptions?tenantId=${tenant.id}`))\n }\n >\n <Building2 className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.actions.viewBilling', 'View Billing')}\n </Button>\n </CardContent>\n </Card>\n </div>\n </div>\n </PageLayout>\n )}\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
@@ -5,30 +5,29 @@ import { Avatar as r, Button as i, EmptyState as ee, Input as te } from "../comp
|
|
|
5
5
|
import { PageLayout as a } from "../components/PageLayout.js";
|
|
6
6
|
import { TenantStatusBadge as ne } from "../components/StatusBadge.js";
|
|
7
7
|
import { formatBytes as re, formatDate as ie, formatNumber as o } from "../utils/formatters.js";
|
|
8
|
-
import { DataTable as ae, Pagination as
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { PermissionGuard as c } from "../components/PermissionGuard.js";
|
|
8
|
+
import { DataTable as ae, Pagination as s } from "../components/DataTable.js";
|
|
9
|
+
import { TenantForm as c } from "../components/forms/TenantForm.js";
|
|
10
|
+
import { RowActions as oe, createRowActions as l } from "../components/RowActions.js";
|
|
11
|
+
import { PermissionGuard as u } from "../components/PermissionGuard.js";
|
|
13
12
|
import "../components/index.js";
|
|
14
|
-
import { joinPath as
|
|
15
|
-
import { mapTenantToUi as
|
|
16
|
-
import { useActivateTenantMutation as
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
13
|
+
import { joinPath as d } from "../utils/navigation.js";
|
|
14
|
+
import { mapTenantToUi as se } from "../utils/mappers.js";
|
|
15
|
+
import { useActivateTenantMutation as ce, useCreateTenantMutation as le, useListTenantsQuery as ue, useSuspendTenantMutation as de, useUpdateTenantMutation as fe } from "../../generated/global-operations.js";
|
|
16
|
+
import { useTr as pe } from "../hooks/useTr.js";
|
|
17
|
+
import { useAdminStore as me } from "../store/adminStore.js";
|
|
18
|
+
import { emitTenantLifecycle as f } from "../utils/tenantEvents.js";
|
|
19
|
+
import { TENANT_FEATURE_FLAGS as p } from "../constants/featureFlags.js";
|
|
20
|
+
import { useCallback as m, useMemo as h, useState as g } from "react";
|
|
21
|
+
import { Building2 as _, Plus as he, Search as v } from "lucide-react";
|
|
22
|
+
import { useNavigate as ge } from "react-router";
|
|
23
23
|
import { jsx as y, jsxs as b } from "react/jsx-runtime";
|
|
24
24
|
import { useFeatureFlag as x } from "@burdenoff/fe-libs/shared/feature-flags";
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import { useEventBus as be } from "@burdenoff/fe-libs/shared/events";
|
|
25
|
+
import { EmphasisPanel as _e, PagePurpose as ve } from "@burdenoff/fe-libs/ui";
|
|
26
|
+
import { useEventBus as ye } from "@burdenoff/fe-libs/shared/events";
|
|
28
27
|
//#region src/admin/pages/TenantsListPage.tsx
|
|
29
28
|
var S = 20;
|
|
30
29
|
function C() {
|
|
31
|
-
let { canViewTenants: C, canManageTenants: w, isLoading: T } = n(), E =
|
|
30
|
+
let { canViewTenants: C, canManageTenants: w, isLoading: T } = n(), E = ge(), { basePath: D = "" } = t(), { tenantFilter: O, updateTenantFilter: k, resetTenantFilter: A } = me(), j = e(), M = pe(), N = x(p.management, !0), P = x(p.list, !0), be = x(p.create, !0), F = x(p.edit, !0), I = ye(), [L, R] = g(1), [xe, z] = g(!1), [Se, B] = g(void 0), Ce = h(() => {
|
|
32
31
|
let e = {};
|
|
33
32
|
O.search && (e.search = O.search);
|
|
34
33
|
let t = {
|
|
@@ -37,57 +36,57 @@ function C() {
|
|
|
37
36
|
deleted: "DELETED"
|
|
38
37
|
};
|
|
39
38
|
return O.status !== "all" && t[O.status] && (e.status = t[O.status]), e;
|
|
40
|
-
}, [O.search, O.status]), V = N &&
|
|
39
|
+
}, [O.search, O.status]), V = N && P, H = w && N, we = T || !C || !V, { data: U, loading: Te, error: W, refetch: G } = ue({
|
|
41
40
|
variables: {
|
|
42
|
-
filter:
|
|
41
|
+
filter: Ce,
|
|
43
42
|
limit: S,
|
|
44
43
|
offset: (L - 1) * S
|
|
45
44
|
},
|
|
46
45
|
fetchPolicy: "cache-and-network",
|
|
47
|
-
skip:
|
|
48
|
-
}), [K, { loading:
|
|
46
|
+
skip: we
|
|
47
|
+
}), [K, { loading: Ee }] = le({ onCompleted: () => {
|
|
49
48
|
G();
|
|
50
|
-
} }), [q, { loading:
|
|
49
|
+
} }), [q, { loading: De }] = fe({ onCompleted: () => {
|
|
51
50
|
G();
|
|
52
|
-
} }), [J] =
|
|
51
|
+
} }), [J] = de({ onCompleted: () => {
|
|
53
52
|
G();
|
|
54
|
-
} }), [Y] =
|
|
53
|
+
} }), [Y] = ce({ onCompleted: () => {
|
|
55
54
|
G();
|
|
56
|
-
} }),
|
|
55
|
+
} }), Oe = h(() => (U?.tenants?.items ?? []).map(se), [U]), X = U?.tenants?.total ?? 0, ke = Math.ceil(X / S), Ae = m(() => {
|
|
57
56
|
B(void 0), z(!0);
|
|
58
|
-
}, []), Z =
|
|
57
|
+
}, []), Z = m((e) => {
|
|
59
58
|
B(e), z(!0);
|
|
60
|
-
}, []), Q =
|
|
59
|
+
}, []), Q = m(async (e) => {
|
|
61
60
|
try {
|
|
62
|
-
await J({ variables: { id: e.id } }),
|
|
61
|
+
await J({ variables: { id: e.id } }), f(I, "tenant.tenant.suspended", {
|
|
63
62
|
tenantId: e.id,
|
|
64
63
|
tenantSlug: e.slug,
|
|
65
64
|
status: "SUSPENDED"
|
|
66
|
-
}), j.success(
|
|
65
|
+
}), j.success(M("admin.tenants.toast.suspendedTitle", "Tenant suspended"), M("admin.tenants.toast.suspendedDescription", "{{name}} has been suspended.", { name: e.name }));
|
|
67
66
|
} catch {
|
|
68
|
-
j.error(
|
|
67
|
+
j.error(M("admin.tenants.toast.errorTitle", "Error"), M("admin.tenants.toast.suspendFailed", "Failed to suspend tenant."));
|
|
69
68
|
}
|
|
70
69
|
}, [
|
|
71
70
|
J,
|
|
72
71
|
j,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
]), $ =
|
|
72
|
+
M,
|
|
73
|
+
I
|
|
74
|
+
]), $ = m(async (e) => {
|
|
76
75
|
try {
|
|
77
|
-
await Y({ variables: { id: e.id } }),
|
|
76
|
+
await Y({ variables: { id: e.id } }), f(I, "tenant.tenant.activated", {
|
|
78
77
|
tenantId: e.id,
|
|
79
78
|
tenantSlug: e.slug,
|
|
80
79
|
status: "ACTIVE"
|
|
81
|
-
}), j.success(
|
|
80
|
+
}), j.success(M("admin.tenants.toast.activatedTitle", "Tenant activated"), M("admin.tenants.toast.activatedDescription", "{{name}} has been activated.", { name: e.name }));
|
|
82
81
|
} catch {
|
|
83
|
-
j.error(
|
|
82
|
+
j.error(M("admin.tenants.toast.errorTitle", "Error"), M("admin.tenants.toast.activateFailed", "Failed to activate tenant."));
|
|
84
83
|
}
|
|
85
84
|
}, [
|
|
86
85
|
Y,
|
|
87
86
|
j,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
]),
|
|
87
|
+
M,
|
|
88
|
+
I
|
|
89
|
+
]), je = m(async (e) => {
|
|
91
90
|
let t = "id" in e && e.id;
|
|
92
91
|
try {
|
|
93
92
|
if (t) {
|
|
@@ -103,10 +102,10 @@ function C() {
|
|
|
103
102
|
}
|
|
104
103
|
}
|
|
105
104
|
} }), n = t.data?.updateTenant?.id ?? e.id, r = t.data?.updateTenant?.slug;
|
|
106
|
-
|
|
105
|
+
f(I, "tenant.tenant.updated", {
|
|
107
106
|
tenantId: n,
|
|
108
107
|
tenantSlug: r
|
|
109
|
-
}), j.success(
|
|
108
|
+
}), j.success(M("admin.tenants.toast.updatedTitle", "Tenant updated"), M("admin.tenants.toast.updatedDescription", "{{name}} has been updated.", { name: e.name ?? "" }));
|
|
110
109
|
} else {
|
|
111
110
|
let t = (await K({ variables: { input: {
|
|
112
111
|
name: e.name,
|
|
@@ -118,32 +117,32 @@ function C() {
|
|
|
118
117
|
plan: e.plan
|
|
119
118
|
}
|
|
120
119
|
} } })).data?.createTenant;
|
|
121
|
-
t?.id &&
|
|
120
|
+
t?.id && f(I, "tenant.tenant.created", {
|
|
122
121
|
tenantId: t.id,
|
|
123
122
|
tenantSlug: t.slug
|
|
124
|
-
}), j.success(
|
|
123
|
+
}), j.success(M("admin.tenants.toast.createdTitle", "Tenant created"), M("admin.tenants.toast.createdDescription", "{{name}} has been created.", { name: e.name ?? "" }));
|
|
125
124
|
}
|
|
126
125
|
z(!1);
|
|
127
126
|
} catch (e) {
|
|
128
127
|
let n = e instanceof Error ? e.message : "Unknown error";
|
|
129
|
-
throw j.error(t ?
|
|
128
|
+
throw j.error(t ? M("admin.tenants.toast.updateFailedTitle", "Update failed") : M("admin.tenants.toast.createFailedTitle", "Create failed"), n), e;
|
|
130
129
|
}
|
|
131
130
|
}, [
|
|
132
131
|
K,
|
|
133
132
|
q,
|
|
134
133
|
j,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
]),
|
|
138
|
-
E(
|
|
139
|
-
},
|
|
140
|
-
let t =
|
|
141
|
-
onView: () => E(
|
|
142
|
-
onEdit: w &&
|
|
134
|
+
M,
|
|
135
|
+
I
|
|
136
|
+
]), Me = (e) => {
|
|
137
|
+
E(d(D, `tenants/${e.id}`));
|
|
138
|
+
}, Ne = m((e) => {
|
|
139
|
+
let t = l({
|
|
140
|
+
onView: () => E(d(D, `tenants/${e.id}`)),
|
|
141
|
+
onEdit: w && F ? () => Z(e) : void 0
|
|
143
142
|
});
|
|
144
143
|
return H && t.push({
|
|
145
144
|
key: e.status === "active" ? "suspend" : "activate",
|
|
146
|
-
label: e.status === "active" ?
|
|
145
|
+
label: e.status === "active" ? M("admin.tenants.actions.suspend", "Suspend") : M("admin.tenants.actions.activate", "Activate"),
|
|
147
146
|
separator: !0,
|
|
148
147
|
onClick: e.status === "active" ? () => Q(e) : () => $(e)
|
|
149
148
|
}), t;
|
|
@@ -152,15 +151,15 @@ function C() {
|
|
|
152
151
|
D,
|
|
153
152
|
w,
|
|
154
153
|
H,
|
|
155
|
-
|
|
154
|
+
F,
|
|
156
155
|
Z,
|
|
157
156
|
Q,
|
|
158
157
|
$,
|
|
159
|
-
|
|
160
|
-
]),
|
|
158
|
+
M
|
|
159
|
+
]), Pe = [
|
|
161
160
|
{
|
|
162
161
|
key: "name",
|
|
163
|
-
header:
|
|
162
|
+
header: M("admin.tenants.table.tenant", "Tenant"),
|
|
164
163
|
sortable: !0,
|
|
165
164
|
render: (e) => /* @__PURE__ */ b("div", {
|
|
166
165
|
className: "flex items-center gap-3",
|
|
@@ -181,18 +180,18 @@ function C() {
|
|
|
181
180
|
},
|
|
182
181
|
{
|
|
183
182
|
key: "status",
|
|
184
|
-
header:
|
|
183
|
+
header: M("admin.tenants.table.status", "Status"),
|
|
185
184
|
width: "120px",
|
|
186
185
|
render: (e) => /* @__PURE__ */ y(ne, { status: e.status })
|
|
187
186
|
},
|
|
188
187
|
{
|
|
189
188
|
key: "plan",
|
|
190
|
-
header:
|
|
189
|
+
header: M("admin.tenants.table.plan", "Plan"),
|
|
191
190
|
width: "100px"
|
|
192
191
|
},
|
|
193
192
|
{
|
|
194
193
|
key: "userCount",
|
|
195
|
-
header:
|
|
194
|
+
header: M("admin.tenants.table.users", "Users"),
|
|
196
195
|
width: "80px",
|
|
197
196
|
align: "right",
|
|
198
197
|
sortable: !0,
|
|
@@ -200,14 +199,14 @@ function C() {
|
|
|
200
199
|
},
|
|
201
200
|
{
|
|
202
201
|
key: "storageUsed",
|
|
203
|
-
header:
|
|
202
|
+
header: M("admin.tenants.table.storage", "Storage"),
|
|
204
203
|
width: "100px",
|
|
205
204
|
align: "right",
|
|
206
205
|
render: (e) => re(e.storageUsed)
|
|
207
206
|
},
|
|
208
207
|
{
|
|
209
208
|
key: "createdAt",
|
|
210
|
-
header:
|
|
209
|
+
header: M("admin.tenants.table.created", "Created"),
|
|
211
210
|
width: "120px",
|
|
212
211
|
sortable: !0,
|
|
213
212
|
render: (e) => ie(e.createdAt)
|
|
@@ -217,55 +216,55 @@ function C() {
|
|
|
217
216
|
header: "",
|
|
218
217
|
width: "50px",
|
|
219
218
|
align: "right",
|
|
220
|
-
render: (e) => /* @__PURE__ */ y(
|
|
221
|
-
actions:
|
|
222
|
-
ariaLabel:
|
|
219
|
+
render: (e) => /* @__PURE__ */ y(oe, {
|
|
220
|
+
actions: Ne(e),
|
|
221
|
+
ariaLabel: M("admin.tenants.table.actionsFor", "Actions for {{name}}", { name: e.name })
|
|
223
222
|
})
|
|
224
223
|
}
|
|
225
224
|
];
|
|
226
|
-
return !T && !C ? /* @__PURE__ */ y(
|
|
225
|
+
return !T && !C ? /* @__PURE__ */ y(u, {
|
|
227
226
|
allowed: !1,
|
|
228
227
|
isLoading: !1,
|
|
229
|
-
pageTitle:
|
|
230
|
-
denyMessage:
|
|
228
|
+
pageTitle: M("admin.tenants.title", "Tenants"),
|
|
229
|
+
denyMessage: M("admin.tenants.denyMessage", "You don't have permission to view tenants."),
|
|
231
230
|
children: null
|
|
232
|
-
}) : V ? /* @__PURE__ */ y(
|
|
231
|
+
}) : V ? /* @__PURE__ */ y(u, {
|
|
233
232
|
allowed: C,
|
|
234
233
|
isLoading: T,
|
|
235
|
-
pageTitle:
|
|
236
|
-
denyMessage:
|
|
234
|
+
pageTitle: M("admin.tenants.title", "Tenants"),
|
|
235
|
+
denyMessage: M("admin.tenants.denyMessage", "You don't have permission to view tenants."),
|
|
237
236
|
children: /* @__PURE__ */ b(a, {
|
|
238
|
-
title:
|
|
239
|
-
gradientWord:
|
|
240
|
-
description: X ?
|
|
241
|
-
actions: w &&
|
|
242
|
-
onClick:
|
|
243
|
-
disabled:
|
|
244
|
-
children: [/* @__PURE__ */ y(
|
|
237
|
+
title: M("admin.tenants.title", "Tenants"),
|
|
238
|
+
gradientWord: M("admin.tenants.title", "Tenants"),
|
|
239
|
+
description: X ? M("admin.tenants.descriptionWithTotal", "Manage all tenants on the platform · {{total}} total", { total: X }) : M("admin.tenants.description", "Manage all tenants on the platform"),
|
|
240
|
+
actions: w && be ? /* @__PURE__ */ b(i, {
|
|
241
|
+
onClick: Ae,
|
|
242
|
+
disabled: Ee || De,
|
|
243
|
+
children: [/* @__PURE__ */ y(he, { className: "mr-2 h-4 w-4" }), M("admin.tenants.actions.createTenant", "Create Tenant")]
|
|
245
244
|
}) : void 0,
|
|
246
245
|
children: [/* @__PURE__ */ b("div", {
|
|
247
246
|
className: "space-y-4",
|
|
248
247
|
children: [
|
|
249
|
-
/* @__PURE__ */ y(
|
|
250
|
-
/* @__PURE__ */ b(
|
|
248
|
+
/* @__PURE__ */ y(ve, { children: M("admin.tenants.purpose", "Tenants are the isolated customer accounts on the platform — each with its own users, data, plan and billing. Use this page to find a tenant, onboard a new one, and suspend or reactivate access when a customer churns or returns.") }),
|
|
249
|
+
/* @__PURE__ */ b(_e, {
|
|
251
250
|
className: "flex items-center gap-4 p-5",
|
|
252
251
|
children: [/* @__PURE__ */ y("div", {
|
|
253
252
|
className: "flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-action-primary-bg",
|
|
254
|
-
children: /* @__PURE__ */ y(
|
|
253
|
+
children: /* @__PURE__ */ y(_, { className: "h-6 w-6" })
|
|
255
254
|
}), /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("div", {
|
|
256
255
|
className: "text-3xl font-bold tabular-nums text-text-primary",
|
|
257
256
|
children: o(X)
|
|
258
257
|
}), /* @__PURE__ */ y("p", {
|
|
259
258
|
className: "text-sm text-text-secondary",
|
|
260
|
-
children:
|
|
259
|
+
children: M("admin.tenants.emphasis.label", "tenants on the platform")
|
|
261
260
|
})] })]
|
|
262
261
|
}),
|
|
263
262
|
/* @__PURE__ */ b("div", {
|
|
264
263
|
className: "flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between",
|
|
265
264
|
children: [/* @__PURE__ */ b("div", {
|
|
266
265
|
className: "relative max-w-sm flex-1",
|
|
267
|
-
children: [/* @__PURE__ */ y(
|
|
268
|
-
placeholder:
|
|
266
|
+
children: [/* @__PURE__ */ y(v, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" }), /* @__PURE__ */ y(te, {
|
|
267
|
+
placeholder: M("admin.tenants.searchPlaceholder", "Search tenants..."),
|
|
269
268
|
value: O.search,
|
|
270
269
|
onChange: (e) => {
|
|
271
270
|
R(1), k({ search: e.target.value });
|
|
@@ -283,19 +282,19 @@ function C() {
|
|
|
283
282
|
children: [
|
|
284
283
|
/* @__PURE__ */ y("option", {
|
|
285
284
|
value: "all",
|
|
286
|
-
children:
|
|
285
|
+
children: M("admin.tenants.filters.status.all", "All Status")
|
|
287
286
|
}),
|
|
288
287
|
/* @__PURE__ */ y("option", {
|
|
289
288
|
value: "active",
|
|
290
|
-
children:
|
|
289
|
+
children: M("admin.tenants.filters.status.active", "Active")
|
|
291
290
|
}),
|
|
292
291
|
/* @__PURE__ */ y("option", {
|
|
293
292
|
value: "suspended",
|
|
294
|
-
children:
|
|
293
|
+
children: M("admin.tenants.filters.status.suspended", "Suspended")
|
|
295
294
|
}),
|
|
296
295
|
/* @__PURE__ */ y("option", {
|
|
297
296
|
value: "deleted",
|
|
298
|
-
children:
|
|
297
|
+
children: M("admin.tenants.filters.status.deleted", "Deleted")
|
|
299
298
|
})
|
|
300
299
|
]
|
|
301
300
|
}), (O.search || O.status !== "all") && /* @__PURE__ */ y(i, {
|
|
@@ -303,62 +302,62 @@ function C() {
|
|
|
303
302
|
onClick: () => {
|
|
304
303
|
R(1), A();
|
|
305
304
|
},
|
|
306
|
-
children:
|
|
305
|
+
children: M("admin.tenants.filters.clear", "Clear")
|
|
307
306
|
})]
|
|
308
307
|
})]
|
|
309
308
|
}),
|
|
310
309
|
W && /* @__PURE__ */ b("div", {
|
|
311
310
|
className: "rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text",
|
|
312
311
|
children: [
|
|
313
|
-
|
|
312
|
+
M("admin.tenants.error.loadFailed", "Failed to load tenants"),
|
|
314
313
|
": ",
|
|
315
314
|
W.message
|
|
316
315
|
]
|
|
317
316
|
}),
|
|
318
317
|
/* @__PURE__ */ y(ae, {
|
|
319
|
-
columns:
|
|
320
|
-
data:
|
|
318
|
+
columns: Pe,
|
|
319
|
+
data: Oe,
|
|
321
320
|
keyExtractor: (e) => e.id,
|
|
322
|
-
onRowClick:
|
|
321
|
+
onRowClick: Me,
|
|
323
322
|
sortBy: O.sortBy,
|
|
324
323
|
sortOrder: O.sortOrder,
|
|
325
324
|
onSort: (e, t) => k({
|
|
326
325
|
sortBy: e,
|
|
327
326
|
sortOrder: t
|
|
328
327
|
}),
|
|
329
|
-
loading:
|
|
328
|
+
loading: Te,
|
|
330
329
|
emptyState: /* @__PURE__ */ y(ee, {
|
|
331
330
|
illustration: "empty-search",
|
|
332
|
-
icon: /* @__PURE__ */ y(
|
|
333
|
-
title:
|
|
334
|
-
description:
|
|
331
|
+
icon: /* @__PURE__ */ y(_, { className: "h-8 w-8" }),
|
|
332
|
+
title: M("admin.tenants.empty.title", "No tenants found"),
|
|
333
|
+
description: M("admin.tenants.empty.description", "Try adjusting your search or filters"),
|
|
335
334
|
action: /* @__PURE__ */ y(i, {
|
|
336
335
|
variant: "outline",
|
|
337
336
|
onClick: () => {
|
|
338
337
|
R(1), A();
|
|
339
338
|
},
|
|
340
|
-
children:
|
|
339
|
+
children: M("admin.tenants.empty.clearFilters", "Clear filters")
|
|
341
340
|
})
|
|
342
341
|
})
|
|
343
342
|
}),
|
|
344
|
-
X > S && /* @__PURE__ */ y(
|
|
343
|
+
X > S && /* @__PURE__ */ y(s, {
|
|
345
344
|
page: L,
|
|
346
|
-
totalPages:
|
|
345
|
+
totalPages: ke,
|
|
347
346
|
totalItems: X,
|
|
348
347
|
pageSize: S,
|
|
349
348
|
onPageChange: R
|
|
350
349
|
})
|
|
351
350
|
]
|
|
352
|
-
}), /* @__PURE__ */ y(
|
|
353
|
-
open:
|
|
351
|
+
}), /* @__PURE__ */ y(c, {
|
|
352
|
+
open: xe,
|
|
354
353
|
onClose: () => z(!1),
|
|
355
|
-
tenant:
|
|
356
|
-
onSave:
|
|
354
|
+
tenant: Se,
|
|
355
|
+
onSave: je
|
|
357
356
|
})]
|
|
358
357
|
})
|
|
359
358
|
}) : /* @__PURE__ */ y(a, {
|
|
360
|
-
title:
|
|
361
|
-
description:
|
|
359
|
+
title: M("admin.tenants.featureDisabledTitle", "Tenant management unavailable"),
|
|
360
|
+
description: M("admin.tenants.featureDisabled", "This feature is currently disabled."),
|
|
362
361
|
children: null
|
|
363
362
|
});
|
|
364
363
|
}
|