@allbluecn/admin-app 0.1.0 → 0.2.1
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/package.json +9 -9
- package/src/components/admin/admin-user-menu.tsx +12 -6
- package/src/components/admin/developer-log.tsx +42 -44
- package/src/components/ai-provider/model-list-editor.tsx +5 -4
- package/src/components/ai-provider/provider-form.tsx +63 -64
- package/src/components/ai-provider/provider-table.tsx +35 -36
- package/src/components/auth/admin-login-form.tsx +23 -24
- package/src/components/auth/oauth-section.tsx +3 -5
- package/src/components/auth/password-input.tsx +2 -3
- package/src/components/common/placeholder-page.tsx +2 -4
- package/src/components/component/component-form-dialog.tsx +48 -49
- package/src/components/component/component-import-dialog.tsx +5 -5
- package/src/components/component/component-table.tsx +13 -14
- package/src/components/data-table/data-table-pagination.tsx +4 -6
- package/src/components/data-table/table-detail-layout.tsx +2 -3
- package/src/components/dependency/changelog-dialog.tsx +3 -4
- package/src/components/dependency/columns.tsx +32 -24
- package/src/components/dependency/dependency-table.tsx +37 -29
- package/src/components/dependency/edit-dialog.tsx +24 -25
- package/src/components/knowledge-public/bookmark-form-dialog.tsx +7 -7
- package/src/components/knowledge-public/compliance-form-dialog.tsx +13 -13
- package/src/components/knowledge-public/legal-form-dialog.tsx +51 -46
- package/src/components/layout/sidebar-02/app-sidebar.tsx +33 -34
- package/src/components/layout/sidebar-02/nav-main.tsx +1 -1
- package/src/components/market/columns.tsx +20 -21
- package/src/components/market/market-app-form.tsx +45 -38
- package/src/components/market/market-app-list.tsx +16 -17
- package/src/components/monitoring/error-list.tsx +18 -19
- package/src/components/monitoring/health-cards.tsx +11 -13
- package/src/components/monitoring/metrics-stats.tsx +11 -12
- package/src/components/monitoring/request-log-table.tsx +8 -9
- package/src/components/ui/pagination.tsx +0 -2
- package/src/components/ui/sidebar.tsx +1 -1
- package/src/components/user/columns.tsx +9 -9
- package/src/components/user/settings-activity-log.tsx +39 -29
- package/src/components/user/settings-profile.tsx +36 -36
- package/src/components/user/user-create-dialog.tsx +4 -4
- package/src/components/user/user-filters.tsx +16 -18
- package/src/lib/i18n/config.ts +0 -32
- package/src/providers/index.tsx +6 -16
- package/src/routes/__root.tsx +9 -23
- package/src/routes/_nav/apps/route.lazy.tsx +2 -3
- package/src/routes/_nav/audit/route.lazy.tsx +10 -11
- package/src/routes/_nav/billing/invoices/route.lazy.tsx +6 -7
- package/src/routes/_nav/billing/route.lazy.tsx +3 -4
- package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.lazy.tsx +16 -17
- package/src/routes/_nav/billing/subscriptions/route.lazy.tsx +9 -10
- package/src/routes/_nav/compliance/route.lazy.tsx +34 -35
- package/src/routes/_nav/components/route.lazy.tsx +6 -6
- package/src/routes/_nav/dashboard/route.lazy.tsx +3 -5
- package/src/routes/_nav/dependencies/route.lazy.tsx +7 -8
- package/src/routes/_nav/docs/route.lazy.tsx +2 -3
- package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +40 -41
- package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +32 -33
- package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +32 -33
- package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +24 -31
- package/src/routes/_nav/licenses/new/route.lazy.tsx +25 -26
- package/src/routes/_nav/licenses/route.lazy.tsx +17 -18
- package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +26 -27
- package/src/routes/_nav/monitoring/errors/route.lazy.tsx +11 -12
- package/src/routes/_nav/monitoring/health/route.lazy.tsx +18 -20
- package/src/routes/_nav/monitoring/logs/route.lazy.tsx +12 -13
- package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +13 -15
- package/src/routes/_nav/monitoring/overview/route.lazy.tsx +12 -13
- package/src/routes/_nav/monitoring/route.lazy.tsx +8 -9
- package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +55 -57
- package/src/routes/_nav/permissions/route.lazy.tsx +48 -50
- package/src/routes/_nav/settings/route.lazy.tsx +2 -3
- package/src/routes/_nav/sso/route.lazy.tsx +20 -18
- package/src/routes/_nav/users/$userId/route.lazy.tsx +86 -82
- package/src/routes/_nav/users/route.lazy.tsx +4 -5
- package/src/routes/setup/route.lazy.tsx +15 -16
- package/src/start.ts +2 -2
- package/src/lib/i18n/client.ts +0 -32
- package/src/lib/i18n/locales/en/admin-ai.json +0 -128
- package/src/lib/i18n/locales/en/admin-billing.json +0 -56
- package/src/lib/i18n/locales/en/admin-users.json +0 -88
- package/src/lib/i18n/locales/en/common.json +0 -877
- package/src/lib/i18n/locales/en/enterprise.json +0 -208
- package/src/lib/i18n/locales/zh/admin-ai.json +0 -128
- package/src/lib/i18n/locales/zh/admin-billing.json +0 -56
- package/src/lib/i18n/locales/zh/admin-users.json +0 -88
- package/src/lib/i18n/locales/zh/common.json +0 -877
- package/src/lib/i18n/locales/zh/enterprise.json +0 -208
- package/src/lib/i18n/server.ts +0 -18
- package/src/lib/i18n/types.ts +0 -5
package/src/providers/index.tsx
CHANGED
|
@@ -18,28 +18,18 @@
|
|
|
18
18
|
|
|
19
19
|
import { ThemeProvider } from "next-themes";
|
|
20
20
|
import { type ReactNode } from "react";
|
|
21
|
-
import { I18nextProvider } from "react-i18next";
|
|
22
|
-
import { getClientI18n } from "@/lib/i18n/client";
|
|
23
|
-
import { createServerI18n } from "@/lib/i18n/server";
|
|
24
21
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
25
22
|
|
|
26
23
|
export function Providers({
|
|
27
24
|
children,
|
|
28
|
-
locale,
|
|
29
25
|
}: {
|
|
30
26
|
children: ReactNode;
|
|
31
|
-
locale?: string;
|
|
32
27
|
}) {
|
|
33
|
-
const isServer = typeof window === 'undefined'
|
|
34
|
-
const i18n = isServer && locale ? createServerI18n(locale as 'zh' | 'en') : getClientI18n()
|
|
35
|
-
|
|
36
28
|
return (
|
|
37
|
-
<
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
</ThemeProvider>
|
|
43
|
-
</I18nextProvider>
|
|
29
|
+
<ThemeProvider attribute="class" defaultTheme="light" enableSystem>
|
|
30
|
+
<TooltipProvider>
|
|
31
|
+
{children}
|
|
32
|
+
</TooltipProvider>
|
|
33
|
+
</ThemeProvider>
|
|
44
34
|
);
|
|
45
|
-
}
|
|
35
|
+
}
|
package/src/routes/__root.tsx
CHANGED
|
@@ -16,8 +16,7 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { Link, Outlet, createRootRouteWithContext, HeadContent, Scripts } from '@tanstack/react-router'
|
|
19
|
-
import {
|
|
20
|
-
import { useTranslation } from 'react-i18next'
|
|
19
|
+
import { m } from '@/paraglide/messages.js'
|
|
21
20
|
import globalsCss from '../styles/globals.css?url'
|
|
22
21
|
import type { RouterContext } from '@/lib/router-context'
|
|
23
22
|
import type { AppLocale } from '@/lib/i18n/config'
|
|
@@ -26,8 +25,7 @@ import { Toaster } from '@/components/ui/sonner'
|
|
|
26
25
|
import { getSessionFn } from '@/server/serverFns/auth/session'
|
|
27
26
|
import { getLocaleFn } from '@/server/serverFns/locale'
|
|
28
27
|
import { reportClientError } from '@/server/serverFns/monitoring/report-error'
|
|
29
|
-
import { useEffect
|
|
30
|
-
import { createServerI18n } from '@/lib/i18n/server'
|
|
28
|
+
import { useEffect } from 'react'
|
|
31
29
|
|
|
32
30
|
export const Route = createRootRouteWithContext<RouterContext>()({
|
|
33
31
|
beforeLoad: async () => {
|
|
@@ -103,7 +101,7 @@ function RootComponent() {
|
|
|
103
101
|
<HeadContent />
|
|
104
102
|
</head>
|
|
105
103
|
<body className="w-full h-full min-h-screen antialiased">
|
|
106
|
-
<Providers
|
|
104
|
+
<Providers>
|
|
107
105
|
<Outlet />
|
|
108
106
|
<Toaster position="top-center" />
|
|
109
107
|
</Providers>
|
|
@@ -114,38 +112,26 @@ function RootComponent() {
|
|
|
114
112
|
}
|
|
115
113
|
|
|
116
114
|
function NotFoundPage() {
|
|
117
|
-
|
|
118
|
-
return (
|
|
119
|
-
<I18nextProvider i18n={i18n}>
|
|
120
|
-
<NotFoundPageInner />
|
|
121
|
-
</I18nextProvider>
|
|
122
|
-
)
|
|
115
|
+
return <NotFoundPageInner />
|
|
123
116
|
}
|
|
124
117
|
|
|
125
118
|
function NotFoundPageInner() {
|
|
126
|
-
const { t } = useTranslation('common')
|
|
127
119
|
return (
|
|
128
120
|
<div className="flex h-screen w-full items-center justify-center">
|
|
129
121
|
<div className="text-center">
|
|
130
122
|
<h1 className="text-6xl font-bold">404</h1>
|
|
131
|
-
<p className="mt-4 text-muted-foreground">{
|
|
132
|
-
<Link to="/login" className="mt-6 inline-block text-primary hover:underline">{
|
|
123
|
+
<p className="mt-4 text-muted-foreground">{m.common_errorPage_notFound()}</p>
|
|
124
|
+
<Link to="/login" className="mt-6 inline-block text-primary hover:underline">{m.common_errorPage_goLogin()}</Link>
|
|
133
125
|
</div>
|
|
134
126
|
</div>
|
|
135
127
|
)
|
|
136
128
|
}
|
|
137
129
|
|
|
138
130
|
function ErrorBoundary({ error }: { error: Error }) {
|
|
139
|
-
|
|
140
|
-
return (
|
|
141
|
-
<I18nextProvider i18n={i18n}>
|
|
142
|
-
<ErrorBoundaryInner error={error} />
|
|
143
|
-
</I18nextProvider>
|
|
144
|
-
)
|
|
131
|
+
return <ErrorBoundaryInner error={error} />
|
|
145
132
|
}
|
|
146
133
|
|
|
147
134
|
function ErrorBoundaryInner({ error }: { error: Error }) {
|
|
148
|
-
const { t } = useTranslation('common')
|
|
149
135
|
useEffect(() => {
|
|
150
136
|
reportClientError({
|
|
151
137
|
data: {
|
|
@@ -161,9 +147,9 @@ function ErrorBoundaryInner({ error }: { error: Error }) {
|
|
|
161
147
|
return (
|
|
162
148
|
<div className="flex h-screen w-full items-center justify-center">
|
|
163
149
|
<div className="text-center">
|
|
164
|
-
<h1 className="text-4xl font-bold text-destructive">{
|
|
150
|
+
<h1 className="text-4xl font-bold text-destructive">{m.common_errorPage_error()}</h1>
|
|
165
151
|
<p className="mt-4 text-muted-foreground">{error.message}</p>
|
|
166
|
-
<Link to="/login" className="mt-6 inline-block text-primary hover:underline">{
|
|
152
|
+
<Link to="/login" className="mt-6 inline-block text-primary hover:underline">{m.common_errorPage_goLogin()}</Link>
|
|
167
153
|
</div>
|
|
168
154
|
</div>
|
|
169
155
|
)
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { createLazyFileRoute } from '@tanstack/react-router'
|
|
19
|
-
import {
|
|
19
|
+
import { m } from '@/paraglide/messages.js'
|
|
20
20
|
import { Package2 } from 'lucide-react'
|
|
21
21
|
import { PlaceholderPage } from '@/components/common/placeholder-page'
|
|
22
22
|
|
|
@@ -25,6 +25,5 @@ export const Route = createLazyFileRoute('/_nav/apps')({
|
|
|
25
25
|
})
|
|
26
26
|
|
|
27
27
|
function AppsPage() {
|
|
28
|
-
|
|
29
|
-
return <PlaceholderPage title={t('apps.title')} icon={Package2} />
|
|
28
|
+
return <PlaceholderPage title={m.common_apps_title()} icon={Package2} />
|
|
30
29
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { createLazyFileRoute } from '@tanstack/react-router'
|
|
19
|
-
import {
|
|
19
|
+
import { m } from '@/paraglide/messages.js'
|
|
20
20
|
import type React from 'react'
|
|
21
21
|
import { Badge } from '@/components/ui/badge'
|
|
22
22
|
|
|
@@ -30,7 +30,6 @@ export const Route = createLazyFileRoute('/_nav/audit')({
|
|
|
30
30
|
})
|
|
31
31
|
|
|
32
32
|
function AuditLogPage(): React.ReactElement {
|
|
33
|
-
const { t } = useTranslation('enterprise')
|
|
34
33
|
const { entries, total } = Route.useLoaderData() as {
|
|
35
34
|
entries: Array<{
|
|
36
35
|
id: string
|
|
@@ -50,25 +49,25 @@ function AuditLogPage(): React.ReactElement {
|
|
|
50
49
|
return (
|
|
51
50
|
<div className="space-y-4">
|
|
52
51
|
<div>
|
|
53
|
-
<h1 className="text-2xl font-bold">{
|
|
54
|
-
<p className="text-sm text-muted-foreground mt-1">{
|
|
52
|
+
<h1 className="text-2xl font-bold">{m.enterprise_audit_title()}</h1>
|
|
53
|
+
<p className="text-sm text-muted-foreground mt-1">{m.enterprise_audit_subtitle({ count: total })}</p>
|
|
55
54
|
</div>
|
|
56
55
|
|
|
57
56
|
{entries.length === 0 ? (
|
|
58
57
|
<div className="rounded-lg border border-border p-8 text-center text-muted-foreground">
|
|
59
|
-
{
|
|
58
|
+
{m.enterprise_audit_empty()}
|
|
60
59
|
</div>
|
|
61
60
|
) : (
|
|
62
61
|
<div className="rounded-lg border border-border overflow-hidden">
|
|
63
62
|
<table className="w-full text-sm">
|
|
64
63
|
<thead>
|
|
65
64
|
<tr className="border-b border-border bg-muted/50">
|
|
66
|
-
<th className="p-3 text-left font-medium">{
|
|
67
|
-
<th className="p-3 text-left font-medium">{
|
|
68
|
-
<th className="p-3 text-left font-medium">{
|
|
69
|
-
<th className="p-3 text-left font-medium">{
|
|
70
|
-
<th className="p-3 text-left font-medium">{
|
|
71
|
-
<th className="p-3 text-left font-medium">{
|
|
65
|
+
<th className="p-3 text-left font-medium">{m.enterprise_audit_timestamp()}</th>
|
|
66
|
+
<th className="p-3 text-left font-medium">{m.enterprise_audit_action()}</th>
|
|
67
|
+
<th className="p-3 text-left font-medium">{m.enterprise_audit_target()}</th>
|
|
68
|
+
<th className="p-3 text-left font-medium">{m.enterprise_audit_actor()}</th>
|
|
69
|
+
<th className="p-3 text-left font-medium">{m.enterprise_audit_ip()}</th>
|
|
70
|
+
<th className="p-3 text-left font-medium">{m.enterprise_audit_userAgent()}</th>
|
|
72
71
|
</tr>
|
|
73
72
|
</thead>
|
|
74
73
|
<tbody>
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { createLazyFileRoute } from '@tanstack/react-router'
|
|
19
|
-
import {
|
|
19
|
+
import { m } from '@/paraglide/messages.js'
|
|
20
20
|
import { formatDate } from 'date-fns'
|
|
21
21
|
import type React from 'react'
|
|
22
22
|
|
|
@@ -30,7 +30,6 @@ export const Route = createLazyFileRoute('/_nav/billing/invoices')({
|
|
|
30
30
|
})
|
|
31
31
|
|
|
32
32
|
function InvoicesList(): React.ReactElement {
|
|
33
|
-
const { t } = useTranslation('admin-billing')
|
|
34
33
|
const { items } = Route.useLoaderData() as {
|
|
35
34
|
items: Array<{
|
|
36
35
|
id: string
|
|
@@ -49,16 +48,16 @@ function InvoicesList(): React.ReactElement {
|
|
|
49
48
|
<thead>
|
|
50
49
|
<tr className="bg-muted/50">
|
|
51
50
|
<th className="text-left px-4 py-2 font-medium">
|
|
52
|
-
{
|
|
51
|
+
{m['admin-billing_subscription_workspace']()}
|
|
53
52
|
</th>
|
|
54
53
|
<th className="text-left px-4 py-2 font-medium">
|
|
55
|
-
{
|
|
54
|
+
{m['admin-billing_subscription_invoiceDate']()}
|
|
56
55
|
</th>
|
|
57
56
|
<th className="text-right px-4 py-2 font-medium">
|
|
58
|
-
{
|
|
57
|
+
{m['admin-billing_subscription_invoiceAmount']()}
|
|
59
58
|
</th>
|
|
60
59
|
<th className="text-center px-4 py-2 font-medium">
|
|
61
|
-
{
|
|
60
|
+
{m['admin-billing_subscription_invoiceStatus']()}
|
|
62
61
|
</th>
|
|
63
62
|
</tr>
|
|
64
63
|
</thead>
|
|
@@ -72,7 +71,7 @@ function InvoicesList(): React.ReactElement {
|
|
|
72
71
|
{formatDate(inv.periodStart, 'yyyy-MM-dd')}
|
|
73
72
|
</td>
|
|
74
73
|
<td className="px-4 py-2 text-right">
|
|
75
|
-
{
|
|
74
|
+
{(inv.currency === 'usd' ? m['admin-billing_invoice_usd']() : m['admin-billing_invoice_cny']())}
|
|
76
75
|
{(inv.amountDue / 100).toFixed(2)}
|
|
77
76
|
</td>
|
|
78
77
|
<td className="px-4 py-2 text-center">{inv.status}</td>
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { createLazyFileRoute, Outlet, useMatches } from '@tanstack/react-router'
|
|
19
|
-
import {
|
|
19
|
+
import { m } from '@/paraglide/messages.js'
|
|
20
20
|
import type React from 'react'
|
|
21
21
|
|
|
22
22
|
export const Route = createLazyFileRoute('/_nav/billing')({
|
|
@@ -29,17 +29,16 @@ export const Route = createLazyFileRoute('/_nav/billing')({
|
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
function BillingLazyLayout() {
|
|
32
|
-
const { t } = useTranslation('admin-billing')
|
|
33
32
|
const matches = useMatches()
|
|
34
33
|
const lastMatch = matches[matches.length - 1]
|
|
35
34
|
const isIndex = lastMatch?.routeId === '/_nav/billing'
|
|
36
35
|
|
|
37
36
|
return (
|
|
38
37
|
<div className="space-y-4 p-6">
|
|
39
|
-
<h1 className="text-2xl font-bold">{
|
|
38
|
+
<h1 className="text-2xl font-bold">{m['admin-billing_page_title']()}</h1>
|
|
40
39
|
{isIndex && (
|
|
41
40
|
<p className="text-sm text-muted-foreground">
|
|
42
|
-
{
|
|
41
|
+
{m['admin-billing_page_selectSubmenu']()}
|
|
43
42
|
</p>
|
|
44
43
|
)}
|
|
45
44
|
<Outlet />
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { createLazyFileRoute, Link } from '@tanstack/react-router'
|
|
19
|
-
import {
|
|
19
|
+
import { m } from '@/paraglide/messages.js'
|
|
20
20
|
import { formatDate } from 'date-fns'
|
|
21
21
|
import type React from 'react'
|
|
22
22
|
|
|
@@ -30,7 +30,6 @@ export const Route = createLazyFileRoute('/_nav/billing/subscriptions/$subscript
|
|
|
30
30
|
})
|
|
31
31
|
|
|
32
32
|
function SubscriptionDetail(): React.ReactElement {
|
|
33
|
-
const { t } = useTranslation('admin-billing')
|
|
34
33
|
const detail = Route.useLoaderData() as {
|
|
35
34
|
id: string
|
|
36
35
|
workspace?: { name?: string } | null
|
|
@@ -50,46 +49,46 @@ function SubscriptionDetail(): React.ReactElement {
|
|
|
50
49
|
} | null
|
|
51
50
|
|
|
52
51
|
if (!detail) {
|
|
53
|
-
return <div className="text-muted-foreground">{
|
|
52
|
+
return <div className="text-muted-foreground">{m['admin-billing_subscription_notFound']()}</div>
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
return (
|
|
57
56
|
<div className="space-y-6">
|
|
58
57
|
<div className="flex items-center gap-4">
|
|
59
58
|
<Link to="/billing/subscriptions" className="text-primary hover:underline">
|
|
60
|
-
← {
|
|
59
|
+
← {m['admin-billing_subscription_back']()}
|
|
61
60
|
</Link>
|
|
62
|
-
<h2 className="text-xl font-bold">{
|
|
61
|
+
<h2 className="text-xl font-bold">{m['admin-billing_subscription_detailTitle']()}</h2>
|
|
63
62
|
</div>
|
|
64
63
|
|
|
65
64
|
<div className="grid grid-cols-2 gap-4 rounded-lg border border-border p-4">
|
|
66
65
|
<div>
|
|
67
66
|
<p className="text-sm text-muted-foreground">
|
|
68
|
-
{
|
|
67
|
+
{m['admin-billing_subscription_workspace']()}
|
|
69
68
|
</p>
|
|
70
69
|
<p className="font-medium">{detail.workspace?.name || '—'}</p>
|
|
71
70
|
</div>
|
|
72
71
|
<div>
|
|
73
72
|
<p className="text-sm text-muted-foreground">
|
|
74
|
-
{
|
|
73
|
+
{m['admin-billing_subscription_plan']()}
|
|
75
74
|
</p>
|
|
76
75
|
<p className="font-medium">{detail.plan?.name || '—'}</p>
|
|
77
76
|
</div>
|
|
78
77
|
<div>
|
|
79
78
|
<p className="text-sm text-muted-foreground">
|
|
80
|
-
{
|
|
79
|
+
{m['admin-billing_subscription_status']()}
|
|
81
80
|
</p>
|
|
82
81
|
<p className="font-medium">{detail.status}</p>
|
|
83
82
|
</div>
|
|
84
83
|
<div>
|
|
85
84
|
<p className="text-sm text-muted-foreground">
|
|
86
|
-
{
|
|
85
|
+
{m['admin-billing_subscription_seats']()}
|
|
87
86
|
</p>
|
|
88
87
|
<p className="font-medium">{detail.seats}</p>
|
|
89
88
|
</div>
|
|
90
89
|
<div>
|
|
91
90
|
<p className="text-sm text-muted-foreground">
|
|
92
|
-
{
|
|
91
|
+
{m['admin-billing_subscription_periodStart']()}
|
|
93
92
|
</p>
|
|
94
93
|
<p className="font-medium">
|
|
95
94
|
{detail.currentPeriodStart
|
|
@@ -99,7 +98,7 @@ function SubscriptionDetail(): React.ReactElement {
|
|
|
99
98
|
</div>
|
|
100
99
|
<div>
|
|
101
100
|
<p className="text-sm text-muted-foreground">
|
|
102
|
-
{
|
|
101
|
+
{m['admin-billing_subscription_periodEnd']()}
|
|
103
102
|
</p>
|
|
104
103
|
<p className="font-medium">
|
|
105
104
|
{detail.currentPeriodEnd
|
|
@@ -110,7 +109,7 @@ function SubscriptionDetail(): React.ReactElement {
|
|
|
110
109
|
{detail.providerSubscriptionId && (
|
|
111
110
|
<div>
|
|
112
111
|
<p className="text-sm text-muted-foreground">
|
|
113
|
-
{
|
|
112
|
+
{m['admin-billing_subscription_providerSubscription']()}
|
|
114
113
|
</p>
|
|
115
114
|
<p className="font-medium">{detail.providerSubscriptionId}</p>
|
|
116
115
|
</div>
|
|
@@ -120,20 +119,20 @@ function SubscriptionDetail(): React.ReactElement {
|
|
|
120
119
|
{detail.invoices.length > 0 && (
|
|
121
120
|
<div>
|
|
122
121
|
<h3 className="text-lg font-semibold mb-4">
|
|
123
|
-
{
|
|
122
|
+
{m['admin-billing_subscription_invoicesTitle']()}
|
|
124
123
|
</h3>
|
|
125
124
|
<div className="rounded-lg border border-border overflow-hidden">
|
|
126
125
|
<table className="w-full text-sm">
|
|
127
126
|
<thead>
|
|
128
127
|
<tr className="bg-muted/50">
|
|
129
128
|
<th className="text-left px-4 py-2 font-medium">
|
|
130
|
-
{
|
|
129
|
+
{m['admin-billing_subscription_invoiceDate']()}
|
|
131
130
|
</th>
|
|
132
131
|
<th className="text-right px-4 py-2 font-medium">
|
|
133
|
-
{
|
|
132
|
+
{m['admin-billing_subscription_invoiceAmount']()}
|
|
134
133
|
</th>
|
|
135
134
|
<th className="text-center px-4 py-2 font-medium">
|
|
136
|
-
{
|
|
135
|
+
{m['admin-billing_subscription_invoiceStatus']()}
|
|
137
136
|
</th>
|
|
138
137
|
</tr>
|
|
139
138
|
</thead>
|
|
@@ -144,7 +143,7 @@ function SubscriptionDetail(): React.ReactElement {
|
|
|
144
143
|
{formatDate(inv.periodStart, 'yyyy-MM-dd')}
|
|
145
144
|
</td>
|
|
146
145
|
<td className="px-4 py-2 text-right">
|
|
147
|
-
{
|
|
146
|
+
{(inv.currency === 'usd' ? m['admin-billing_invoice_usd']() : m['admin-billing_invoice_cny']())}
|
|
148
147
|
{(inv.amountDue / 100).toFixed(2)}
|
|
149
148
|
</td>
|
|
150
149
|
<td className="px-4 py-2 text-center">{inv.status}</td>
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { createLazyFileRoute, Link } from '@tanstack/react-router'
|
|
19
|
-
import {
|
|
19
|
+
import { m } from '@/paraglide/messages.js'
|
|
20
20
|
import { formatDate } from 'date-fns'
|
|
21
21
|
import type React from 'react'
|
|
22
22
|
|
|
@@ -30,7 +30,6 @@ export const Route = createLazyFileRoute('/_nav/billing/subscriptions')({
|
|
|
30
30
|
})
|
|
31
31
|
|
|
32
32
|
function SubscriptionsList(): React.ReactElement {
|
|
33
|
-
const { t } = useTranslation('admin-billing')
|
|
34
33
|
const { items, total } = Route.useLoaderData() as {
|
|
35
34
|
items: Array<{
|
|
36
35
|
id: string
|
|
@@ -47,7 +46,7 @@ function SubscriptionsList(): React.ReactElement {
|
|
|
47
46
|
<div>
|
|
48
47
|
<div className="mb-4 flex items-center justify-between">
|
|
49
48
|
<p className="text-sm text-muted-foreground">
|
|
50
|
-
{
|
|
49
|
+
{m['admin-billing_subscription_count']({ count: total })}
|
|
51
50
|
</p>
|
|
52
51
|
</div>
|
|
53
52
|
<div className="rounded-lg border border-border overflow-hidden">
|
|
@@ -55,22 +54,22 @@ function SubscriptionsList(): React.ReactElement {
|
|
|
55
54
|
<thead>
|
|
56
55
|
<tr className="bg-muted/50">
|
|
57
56
|
<th className="text-left px-4 py-2 font-medium">
|
|
58
|
-
{
|
|
57
|
+
{m['admin-billing_subscription_workspace']()}
|
|
59
58
|
</th>
|
|
60
59
|
<th className="text-left px-4 py-2 font-medium">
|
|
61
|
-
{
|
|
60
|
+
{m['admin-billing_subscription_plan']()}
|
|
62
61
|
</th>
|
|
63
62
|
<th className="text-center px-4 py-2 font-medium">
|
|
64
|
-
{
|
|
63
|
+
{m['admin-billing_subscription_status']()}
|
|
65
64
|
</th>
|
|
66
65
|
<th className="text-center px-4 py-2 font-medium">
|
|
67
|
-
{
|
|
66
|
+
{m['admin-billing_subscription_seats']()}
|
|
68
67
|
</th>
|
|
69
68
|
<th className="text-left px-4 py-2 font-medium">
|
|
70
|
-
{
|
|
69
|
+
{m['admin-billing_subscription_expires']()}
|
|
71
70
|
</th>
|
|
72
71
|
<th className="text-right px-4 py-2 font-medium">
|
|
73
|
-
{
|
|
72
|
+
{m['admin-billing_subscription_action']()}
|
|
74
73
|
</th>
|
|
75
74
|
</tr>
|
|
76
75
|
</thead>
|
|
@@ -100,7 +99,7 @@ function SubscriptionsList(): React.ReactElement {
|
|
|
100
99
|
params={{ subscriptionId: sub.id }}
|
|
101
100
|
className="text-primary hover:underline"
|
|
102
101
|
>
|
|
103
|
-
{
|
|
102
|
+
{m['admin-billing_subscription_detail']()}
|
|
104
103
|
</Link>
|
|
105
104
|
</td>
|
|
106
105
|
</tr>
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { createLazyFileRoute } from '@tanstack/react-router'
|
|
19
|
+
import { m } from '@/paraglide/messages.js'
|
|
19
20
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
20
21
|
import { useServerFn } from '@tanstack/react-start'
|
|
21
|
-
import { useTranslation } from 'react-i18next'
|
|
22
22
|
import { toast } from 'sonner'
|
|
23
23
|
import type React from 'react'
|
|
24
24
|
import { listComplianceExportsFn, executeComplianceJobFn } from '@/server/serverFns/compliance'
|
|
@@ -38,7 +38,6 @@ export const Route = createLazyFileRoute('/_nav/compliance')({
|
|
|
38
38
|
const QUERY_KEY = ['compliance-exports']
|
|
39
39
|
|
|
40
40
|
function CompliancePage(): React.ReactElement {
|
|
41
|
-
const { t } = useTranslation('enterprise')
|
|
42
41
|
const queryClient = useQueryClient()
|
|
43
42
|
|
|
44
43
|
const { data } = useQuery({
|
|
@@ -55,68 +54,68 @@ function CompliancePage(): React.ReactElement {
|
|
|
55
54
|
const handleExecute = async (jobId: string) => {
|
|
56
55
|
try {
|
|
57
56
|
await executeFn({ data: { jobId } })
|
|
58
|
-
toast.success(
|
|
57
|
+
toast.success(m.enterprise_compliance_export_toast_executed())
|
|
59
58
|
await queryClient.invalidateQueries({ queryKey: QUERY_KEY })
|
|
60
59
|
} catch (err) {
|
|
61
|
-
toast.error(err instanceof Error ? err.message :
|
|
60
|
+
toast.error(err instanceof Error ? err.message : m.enterprise_compliance_export_toast_executeFailed())
|
|
62
61
|
}
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
const isExpired = (job: { status: string; expiresAt: string | Date | null }) =>
|
|
66
65
|
job.status === 'completed' && job.expiresAt != null && new Date(job.expiresAt) < new Date()
|
|
67
66
|
|
|
68
|
-
const
|
|
69
|
-
pending:
|
|
70
|
-
processing:
|
|
71
|
-
completed:
|
|
72
|
-
failed:
|
|
73
|
-
expired:
|
|
67
|
+
const STATUS_LABELS: Record<string, () => string> = {
|
|
68
|
+
pending: m.enterprise_compliance_export_status_pending,
|
|
69
|
+
processing: m.enterprise_compliance_export_status_processing,
|
|
70
|
+
completed: m.enterprise_compliance_export_status_completed,
|
|
71
|
+
failed: m.enterprise_compliance_export_status_failed,
|
|
72
|
+
expired: m.enterprise_compliance_export_status_expired,
|
|
74
73
|
}
|
|
75
74
|
|
|
76
|
-
const
|
|
77
|
-
user:
|
|
78
|
-
workspace:
|
|
75
|
+
const SCOPE_LABELS: Record<string, () => string> = {
|
|
76
|
+
user: m.enterprise_compliance_export_scope_user,
|
|
77
|
+
workspace: m.enterprise_compliance_export_scope_workspace,
|
|
79
78
|
}
|
|
80
79
|
|
|
81
|
-
const
|
|
82
|
-
export:
|
|
83
|
-
delete:
|
|
80
|
+
const TYPE_LABELS: Record<string, () => string> = {
|
|
81
|
+
export: m.enterprise_compliance_export_type_export,
|
|
82
|
+
delete: m.enterprise_compliance_export_type_delete,
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
return (
|
|
87
86
|
<div className="space-y-4">
|
|
88
87
|
<div>
|
|
89
|
-
<h1 className="text-2xl font-bold">{
|
|
90
|
-
<p className="text-sm text-muted-foreground mt-1">{
|
|
88
|
+
<h1 className="text-2xl font-bold">{m.enterprise_compliance_export_title()}</h1>
|
|
89
|
+
<p className="text-sm text-muted-foreground mt-1">{m.enterprise_compliance_export_subtitle({ count: total })}</p>
|
|
91
90
|
</div>
|
|
92
91
|
|
|
93
92
|
{items.length === 0 ? (
|
|
94
93
|
<div className="rounded-lg border border-border p-8 text-center text-muted-foreground">
|
|
95
|
-
{
|
|
94
|
+
{m.enterprise_compliance_export_empty()}
|
|
96
95
|
</div>
|
|
97
96
|
) : (
|
|
98
97
|
<div className="rounded-lg border border-border overflow-hidden">
|
|
99
98
|
<table className="w-full text-sm">
|
|
100
99
|
<thead>
|
|
101
100
|
<tr className="border-b border-border bg-muted/50">
|
|
102
|
-
<th className="p-3 text-left font-medium">{
|
|
103
|
-
<th className="p-3 text-left font-medium">{
|
|
104
|
-
<th className="p-3 text-left font-medium">{
|
|
105
|
-
<th className="p-3 text-left font-medium">{
|
|
106
|
-
<th className="p-3 text-left font-medium">{
|
|
107
|
-
<th className="p-3 text-left font-medium">{
|
|
108
|
-
<th className="p-3 text-left font-medium">{
|
|
109
|
-
<th className="p-3 text-left font-medium">{
|
|
101
|
+
<th className="p-3 text-left font-medium">{m.enterprise_compliance_export_table_type()}</th>
|
|
102
|
+
<th className="p-3 text-left font-medium">{m.enterprise_compliance_export_table_scope()}</th>
|
|
103
|
+
<th className="p-3 text-left font-medium">{m.enterprise_compliance_export_table_target()}</th>
|
|
104
|
+
<th className="p-3 text-left font-medium">{m.enterprise_compliance_export_table_status()}</th>
|
|
105
|
+
<th className="p-3 text-left font-medium">{m.enterprise_compliance_export_table_requester()}</th>
|
|
106
|
+
<th className="p-3 text-left font-medium">{m.enterprise_compliance_export_table_createdAt()}</th>
|
|
107
|
+
<th className="p-3 text-left font-medium">{m.enterprise_compliance_export_table_error()}</th>
|
|
108
|
+
<th className="p-3 text-left font-medium">{m.enterprise_compliance_export_table_actions()}</th>
|
|
110
109
|
</tr>
|
|
111
110
|
</thead>
|
|
112
111
|
<tbody>
|
|
113
|
-
{items.map((item) => (
|
|
112
|
+
{items.map((item: any) => (
|
|
114
113
|
<tr key={item.id} className="border-b border-border last:border-0">
|
|
115
114
|
<td className="p-3">
|
|
116
|
-
<Badge>{
|
|
115
|
+
<Badge>{(TYPE_LABELS[item.type] ?? (() => item.type))()}</Badge>
|
|
117
116
|
</td>
|
|
118
117
|
<td className="p-3">
|
|
119
|
-
{
|
|
118
|
+
{(SCOPE_LABELS[item.scope] ?? (() => item.scope))()}
|
|
120
119
|
</td>
|
|
121
120
|
<td className="p-3">{item.targetId.slice(0, 12)}</td>
|
|
122
121
|
<td className="p-3">
|
|
@@ -129,7 +128,7 @@ function CompliancePage(): React.ReactElement {
|
|
|
129
128
|
: 'bg-muted text-muted-foreground'
|
|
130
129
|
}`}
|
|
131
130
|
>
|
|
132
|
-
{
|
|
131
|
+
{(STATUS_LABELS[item.status] ?? (() => item.status))()}
|
|
133
132
|
</span>
|
|
134
133
|
</td>
|
|
135
134
|
<td className="p-3">{item.requestedBy?.slice(0, 8) || '—'}</td>
|
|
@@ -147,13 +146,13 @@ function CompliancePage(): React.ReactElement {
|
|
|
147
146
|
size="sm"
|
|
148
147
|
onClick={() => handleExecute(item.id)}
|
|
149
148
|
>
|
|
150
|
-
{
|
|
149
|
+
{m.enterprise_compliance_export_actions_execute()}
|
|
151
150
|
</Button>
|
|
152
151
|
)}
|
|
153
152
|
{item.status === 'processing' && (
|
|
154
153
|
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
|
|
155
154
|
<Loader2 className="size-3 animate-spin" />
|
|
156
|
-
{
|
|
155
|
+
{m.enterprise_compliance_export_actions_processing()}
|
|
157
156
|
</span>
|
|
158
157
|
)}
|
|
159
158
|
{item.status === 'completed' && !isExpired(item) && (
|
|
@@ -162,13 +161,13 @@ function CompliancePage(): React.ReactElement {
|
|
|
162
161
|
download={`allblue-export-${item.id}.json`}
|
|
163
162
|
>
|
|
164
163
|
<Button variant="outline" size="sm" asChild={false}>
|
|
165
|
-
<span>{
|
|
164
|
+
<span>{m.enterprise_compliance_export_actions_download()}</span>
|
|
166
165
|
</Button>
|
|
167
166
|
</a>
|
|
168
167
|
)}
|
|
169
168
|
{isExpired(item) && (
|
|
170
169
|
<span className="inline-flex items-center text-xs text-muted-foreground">
|
|
171
|
-
{
|
|
170
|
+
{m.enterprise_compliance_export_actions_expired()}
|
|
172
171
|
</span>
|
|
173
172
|
)}
|
|
174
173
|
</div>
|