@magnet-cms/plugin-sentry 1.0.0
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/backend/index.cjs +921 -0
- package/dist/backend/index.d.cts +21 -0
- package/dist/backend/index.d.ts +21 -0
- package/dist/backend/index.js +7 -0
- package/dist/chunk-NDBQHOLK.js +907 -0
- package/dist/frontend/bundle.iife.js +24998 -0
- package/dist/frontend/bundle.iife.js.map +1 -0
- package/dist/index.cjs +922 -0
- package/dist/index.d.cts +305 -0
- package/dist/index.d.ts +305 -0
- package/dist/index.js +6 -0
- package/package.json +87 -0
- package/src/admin/components/error-metrics.tsx +54 -0
- package/src/admin/components/feedback-widget.tsx +76 -0
- package/src/admin/components/recent-issues.tsx +100 -0
- package/src/admin/hooks/use-project-filter.ts +59 -0
- package/src/admin/index.ts +121 -0
- package/src/admin/pages/sentry-dashboard.tsx +149 -0
- package/src/admin/pages/sentry-issues.tsx +269 -0
- package/src/admin/pages/sentry-settings.tsx +182 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Card } from '@magnet-cms/ui/components'
|
|
2
|
+
import { AlertTriangle, BarChart3, Bug } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
interface MetricCardProps {
|
|
5
|
+
title: string
|
|
6
|
+
value: string | number
|
|
7
|
+
icon: React.ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function MetricCard({ title, value, icon }: MetricCardProps) {
|
|
11
|
+
return (
|
|
12
|
+
<Card className="p-6">
|
|
13
|
+
<div className="flex items-center justify-between">
|
|
14
|
+
<div>
|
|
15
|
+
<p className="text-sm font-medium text-muted-foreground">{title}</p>
|
|
16
|
+
<p className="text-2xl font-bold mt-1">{value}</p>
|
|
17
|
+
</div>
|
|
18
|
+
<div className="text-muted-foreground">{icon}</div>
|
|
19
|
+
</div>
|
|
20
|
+
</Card>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ErrorMetricsProps {
|
|
25
|
+
totalErrors: number
|
|
26
|
+
unresolvedIssues: number
|
|
27
|
+
errorsLast24h: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function ErrorMetrics({
|
|
31
|
+
totalErrors,
|
|
32
|
+
unresolvedIssues,
|
|
33
|
+
errorsLast24h,
|
|
34
|
+
}: ErrorMetricsProps) {
|
|
35
|
+
return (
|
|
36
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
|
37
|
+
<MetricCard
|
|
38
|
+
title="Unresolved Issues"
|
|
39
|
+
value={unresolvedIssues}
|
|
40
|
+
icon={<AlertTriangle className="h-5 w-5" />}
|
|
41
|
+
/>
|
|
42
|
+
<MetricCard
|
|
43
|
+
title="Errors (Last 24h)"
|
|
44
|
+
value={errorsLast24h}
|
|
45
|
+
icon={<Bug className="h-5 w-5" />}
|
|
46
|
+
/>
|
|
47
|
+
<MetricCard
|
|
48
|
+
title="Total Errors"
|
|
49
|
+
value={totalErrors}
|
|
50
|
+
icon={<BarChart3 className="h-5 w-5" />}
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useAdapter } from '@magnet-cms/admin'
|
|
2
|
+
import { useEffect, useRef } from 'react'
|
|
3
|
+
|
|
4
|
+
interface SentryClientConfig {
|
|
5
|
+
dsn: string
|
|
6
|
+
enabled: boolean
|
|
7
|
+
environment: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Mounts the Sentry User Feedback widget via the @sentry/browser feedbackIntegration.
|
|
12
|
+
*
|
|
13
|
+
* Fetches the public DSN from the backend (/sentry/config), initializes the
|
|
14
|
+
* Sentry Browser SDK if not already initialized, and attaches a floating
|
|
15
|
+
* feedback button to the page.
|
|
16
|
+
*
|
|
17
|
+
* The widget uses Sentry's shadow DOM isolation so it is style-independent
|
|
18
|
+
* from the admin UI theme.
|
|
19
|
+
*/
|
|
20
|
+
export function SentryFeedbackWidget() {
|
|
21
|
+
const adapter = useAdapter()
|
|
22
|
+
const feedbackRef = useRef<{ remove: () => void } | null>(null)
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
let cancelled = false
|
|
26
|
+
|
|
27
|
+
async function init() {
|
|
28
|
+
try {
|
|
29
|
+
const config =
|
|
30
|
+
await adapter.request<SentryClientConfig>('/sentry/config')
|
|
31
|
+
|
|
32
|
+
if (cancelled || !config.enabled || !config.dsn) return
|
|
33
|
+
|
|
34
|
+
// Dynamically import @sentry/browser to keep it out of the main bundle
|
|
35
|
+
const Sentry = await import('@sentry/browser')
|
|
36
|
+
|
|
37
|
+
if (cancelled) return
|
|
38
|
+
|
|
39
|
+
// Initialize Sentry Browser SDK if not already done
|
|
40
|
+
if (!Sentry.getClient()) {
|
|
41
|
+
Sentry.init({
|
|
42
|
+
dsn: config.dsn,
|
|
43
|
+
environment: config.environment,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Attach the feedback widget — uses shadow DOM for style isolation
|
|
48
|
+
const feedback = Sentry.feedbackIntegration({
|
|
49
|
+
colorScheme: 'system',
|
|
50
|
+
buttonLabel: 'Report an Issue',
|
|
51
|
+
submitButtonLabel: 'Send Report',
|
|
52
|
+
messagePlaceholder: 'Describe the issue you encountered...',
|
|
53
|
+
showBranding: false,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const widget = feedback.createWidget?.()
|
|
57
|
+
if (widget) {
|
|
58
|
+
feedbackRef.current = widget
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Sentry is optional — never crash the admin UI
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
init()
|
|
66
|
+
|
|
67
|
+
return () => {
|
|
68
|
+
cancelled = true
|
|
69
|
+
feedbackRef.current?.remove()
|
|
70
|
+
feedbackRef.current = null
|
|
71
|
+
}
|
|
72
|
+
}, [adapter])
|
|
73
|
+
|
|
74
|
+
// Widget is rendered by Sentry SDK into the DOM directly — no React output needed
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Badge,
|
|
3
|
+
DataTable,
|
|
4
|
+
type DataTableColumn,
|
|
5
|
+
} from '@magnet-cms/ui/components'
|
|
6
|
+
import { ExternalLink } from 'lucide-react'
|
|
7
|
+
|
|
8
|
+
interface SentryIssue {
|
|
9
|
+
id: string
|
|
10
|
+
shortId: string
|
|
11
|
+
title: string
|
|
12
|
+
status: string
|
|
13
|
+
count: string
|
|
14
|
+
lastSeen: string
|
|
15
|
+
permalink: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface RecentIssuesProps {
|
|
19
|
+
issues: SentryIssue[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getStatusVariant(
|
|
23
|
+
status: string,
|
|
24
|
+
): 'default' | 'secondary' | 'destructive' | 'outline' {
|
|
25
|
+
switch (status) {
|
|
26
|
+
case 'resolved':
|
|
27
|
+
return 'secondary'
|
|
28
|
+
case 'ignored':
|
|
29
|
+
return 'outline'
|
|
30
|
+
default:
|
|
31
|
+
return 'destructive'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatRelativeTime(dateStr: string): string {
|
|
36
|
+
const diff = Date.now() - new Date(dateStr).getTime()
|
|
37
|
+
const minutes = Math.floor(diff / 60_000)
|
|
38
|
+
if (minutes < 60) return `${minutes}m ago`
|
|
39
|
+
const hours = Math.floor(minutes / 60)
|
|
40
|
+
if (hours < 24) return `${hours}h ago`
|
|
41
|
+
return `${Math.floor(hours / 24)}d ago`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const columns: DataTableColumn<SentryIssue>[] = [
|
|
45
|
+
{
|
|
46
|
+
type: 'custom',
|
|
47
|
+
header: 'Title',
|
|
48
|
+
cell: (row) => (
|
|
49
|
+
<div className="flex items-center gap-2">
|
|
50
|
+
<span className="font-medium truncate max-w-[300px]">
|
|
51
|
+
{row.original.title}
|
|
52
|
+
</span>
|
|
53
|
+
<a
|
|
54
|
+
href={row.original.permalink}
|
|
55
|
+
target="_blank"
|
|
56
|
+
rel="noopener noreferrer"
|
|
57
|
+
onClick={(e) => e.stopPropagation()}
|
|
58
|
+
>
|
|
59
|
+
<ExternalLink className="h-3 w-3 text-muted-foreground shrink-0" />
|
|
60
|
+
</a>
|
|
61
|
+
</div>
|
|
62
|
+
),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
type: 'custom',
|
|
66
|
+
header: 'Status',
|
|
67
|
+
cell: (row) => (
|
|
68
|
+
<Badge variant={getStatusVariant(row.original.status)}>
|
|
69
|
+
{row.original.status}
|
|
70
|
+
</Badge>
|
|
71
|
+
),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: 'text',
|
|
75
|
+
header: 'Events',
|
|
76
|
+
accessorKey: 'count',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: 'text',
|
|
80
|
+
header: 'Last Seen',
|
|
81
|
+
accessorKey: 'lastSeen',
|
|
82
|
+
format: (value) => (
|
|
83
|
+
<span className="text-muted-foreground">
|
|
84
|
+
{formatRelativeTime(value as string)}
|
|
85
|
+
</span>
|
|
86
|
+
),
|
|
87
|
+
},
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
export function RecentIssues({ issues }: RecentIssuesProps) {
|
|
91
|
+
if (issues.length === 0) {
|
|
92
|
+
return (
|
|
93
|
+
<p className="text-sm text-muted-foreground text-center py-4">
|
|
94
|
+
No recent issues
|
|
95
|
+
</p>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return <DataTable columns={columns} data={issues} />
|
|
100
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { useAdapter } from '@magnet-cms/admin'
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
export interface SentryProject {
|
|
5
|
+
id: string
|
|
6
|
+
slug: string
|
|
7
|
+
name: string
|
|
8
|
+
platform: string | null
|
|
9
|
+
isActive: boolean
|
|
10
|
+
errorCount: number | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Sentinel value for the "All Projects" aggregate option */
|
|
14
|
+
export const ALL_PROJECTS = '__all__'
|
|
15
|
+
|
|
16
|
+
type Adapter = ReturnType<typeof useAdapter>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Shared hook for project filter functionality used by Dashboard and Issues pages.
|
|
20
|
+
*
|
|
21
|
+
* Fetches the org's project list on mount, defaults to ALL_PROJECTS (aggregate view),
|
|
22
|
+
* and invokes `onProjectChange` whenever the selection changes.
|
|
23
|
+
* Pass ALL_PROJECTS ('') to get org-level aggregate data (no ?project= param).
|
|
24
|
+
*/
|
|
25
|
+
export function useProjectFilter(
|
|
26
|
+
adapter: Adapter,
|
|
27
|
+
onProjectChange: (slug: string) => Promise<void>,
|
|
28
|
+
) {
|
|
29
|
+
const [projects, setProjects] = useState<SentryProject[]>([])
|
|
30
|
+
const [selectedProject, setSelectedProject] = useState<string>(ALL_PROJECTS)
|
|
31
|
+
const [loading, setLoading] = useState(true)
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
async function init() {
|
|
35
|
+
try {
|
|
36
|
+
const [projectsData] = await Promise.all([
|
|
37
|
+
adapter.request<SentryProject[]>('/sentry/admin/projects'),
|
|
38
|
+
// Initial data fetch for "All Projects"
|
|
39
|
+
onProjectChange(ALL_PROJECTS),
|
|
40
|
+
])
|
|
41
|
+
setProjects(projectsData)
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('[Sentry] Failed to load projects:', error)
|
|
44
|
+
} finally {
|
|
45
|
+
setLoading(false)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
init()
|
|
49
|
+
// onProjectChange is intentionally excluded — stable callback ref not needed
|
|
50
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
51
|
+
}, [adapter])
|
|
52
|
+
|
|
53
|
+
function handleProjectChange(slug: string) {
|
|
54
|
+
setSelectedProject(slug)
|
|
55
|
+
onProjectChange(slug).catch(console.error)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { projects, selectedProject, loading, handleProjectChange }
|
|
59
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentry Plugin — Frontend Entry
|
|
3
|
+
*
|
|
4
|
+
* Self-registers the plugin when loaded via script injection.
|
|
5
|
+
* The admin app loads plugin bundles at runtime and plugins self-register
|
|
6
|
+
* on window.__MAGNET_PLUGINS__.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ComponentType } from 'react'
|
|
10
|
+
|
|
11
|
+
interface FrontendPluginManifest {
|
|
12
|
+
pluginName: string
|
|
13
|
+
routes?: {
|
|
14
|
+
path: string
|
|
15
|
+
componentId: string
|
|
16
|
+
requiresAuth?: boolean
|
|
17
|
+
children?: { path: string; componentId: string }[]
|
|
18
|
+
}[]
|
|
19
|
+
sidebar?: {
|
|
20
|
+
id: string
|
|
21
|
+
title: string
|
|
22
|
+
url: string
|
|
23
|
+
icon: string
|
|
24
|
+
order?: number
|
|
25
|
+
items?: { id: string; title: string; url: string; icon: string }[]
|
|
26
|
+
}[]
|
|
27
|
+
widgets?: {
|
|
28
|
+
componentId: string
|
|
29
|
+
position: 'global' | 'dashboard' | 'header'
|
|
30
|
+
}[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface PluginRegistration {
|
|
34
|
+
manifest: FrontendPluginManifest
|
|
35
|
+
components: Record<string, () => Promise<{ default: ComponentType<unknown> }>>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const manifest: FrontendPluginManifest = {
|
|
39
|
+
pluginName: 'sentry',
|
|
40
|
+
routes: [
|
|
41
|
+
{
|
|
42
|
+
path: 'sentry',
|
|
43
|
+
componentId: 'SentryDashboard',
|
|
44
|
+
requiresAuth: true,
|
|
45
|
+
children: [
|
|
46
|
+
{ path: '', componentId: 'SentryDashboard' },
|
|
47
|
+
{ path: 'issues', componentId: 'SentryIssues' },
|
|
48
|
+
{ path: 'settings', componentId: 'SentrySettings' },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
sidebar: [
|
|
53
|
+
{
|
|
54
|
+
id: 'sentry',
|
|
55
|
+
title: 'Sentry',
|
|
56
|
+
url: '/sentry',
|
|
57
|
+
icon: 'AlertTriangle',
|
|
58
|
+
order: 80,
|
|
59
|
+
items: [
|
|
60
|
+
{
|
|
61
|
+
id: 'sentry-dashboard',
|
|
62
|
+
title: 'Dashboard',
|
|
63
|
+
url: '/sentry',
|
|
64
|
+
icon: 'BarChart3',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'sentry-issues',
|
|
68
|
+
title: 'Issues',
|
|
69
|
+
url: '/sentry/issues',
|
|
70
|
+
icon: 'Bug',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'sentry-settings',
|
|
74
|
+
title: 'Settings',
|
|
75
|
+
url: '/sentry/settings',
|
|
76
|
+
icon: 'Settings',
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
// Global widget — the feedback button is mounted once on the page
|
|
82
|
+
widgets: [
|
|
83
|
+
{
|
|
84
|
+
componentId: 'SentryFeedbackWidget',
|
|
85
|
+
position: 'global',
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const components: Record<
|
|
91
|
+
string,
|
|
92
|
+
() => Promise<{ default: ComponentType<unknown> }>
|
|
93
|
+
> = {
|
|
94
|
+
SentryDashboard: () => import('./pages/sentry-dashboard'),
|
|
95
|
+
SentryIssues: () => import('./pages/sentry-issues'),
|
|
96
|
+
SentrySettings: () => import('./pages/sentry-settings'),
|
|
97
|
+
SentryFeedbackWidget: () =>
|
|
98
|
+
import('./components/feedback-widget').then((m) => ({
|
|
99
|
+
default: m.SentryFeedbackWidget as ComponentType<unknown>,
|
|
100
|
+
})),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function registerPlugin() {
|
|
104
|
+
if (!window.__MAGNET_PLUGINS__) {
|
|
105
|
+
window.__MAGNET_PLUGINS__ = []
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const alreadyRegistered = window.__MAGNET_PLUGINS__.some(
|
|
109
|
+
(p: PluginRegistration) => p.manifest.pluginName === manifest.pluginName,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if (!alreadyRegistered) {
|
|
113
|
+
window.__MAGNET_PLUGINS__.push({ manifest, components })
|
|
114
|
+
console.log(`[Magnet] Plugin registered: ${manifest.pluginName}`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
registerPlugin()
|
|
119
|
+
|
|
120
|
+
export const sentryPlugin = () => ({ manifest, components })
|
|
121
|
+
export default sentryPlugin
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { PageContent, PageHeader, useAdapter } from '@magnet-cms/admin'
|
|
2
|
+
import {
|
|
3
|
+
Alert,
|
|
4
|
+
AlertDescription,
|
|
5
|
+
AlertTitle,
|
|
6
|
+
Card,
|
|
7
|
+
Select,
|
|
8
|
+
SelectContent,
|
|
9
|
+
SelectItem,
|
|
10
|
+
SelectTrigger,
|
|
11
|
+
SelectValue,
|
|
12
|
+
Skeleton,
|
|
13
|
+
} from '@magnet-cms/ui/components'
|
|
14
|
+
import { useState } from 'react'
|
|
15
|
+
import { ErrorMetrics } from '../components/error-metrics'
|
|
16
|
+
import { RecentIssues } from '../components/recent-issues'
|
|
17
|
+
import { ALL_PROJECTS, useProjectFilter } from '../hooks/use-project-filter'
|
|
18
|
+
|
|
19
|
+
interface SentryStats {
|
|
20
|
+
isConfigured: boolean
|
|
21
|
+
apiError?: string
|
|
22
|
+
totalErrors: number
|
|
23
|
+
unresolvedIssues: number
|
|
24
|
+
errorsLast24h: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SentryIssue {
|
|
28
|
+
id: string
|
|
29
|
+
shortId: string
|
|
30
|
+
title: string
|
|
31
|
+
status: string
|
|
32
|
+
count: string
|
|
33
|
+
lastSeen: string
|
|
34
|
+
permalink: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function DashboardSkeleton() {
|
|
38
|
+
return (
|
|
39
|
+
<div className="space-y-6 p-6">
|
|
40
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
|
41
|
+
{['unresolved', '24h', 'total'].map((id) => (
|
|
42
|
+
<Skeleton key={id} className="h-24" />
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
<Skeleton className="h-[200px]" />
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function SetupCard() {
|
|
51
|
+
return (
|
|
52
|
+
<Card className="p-6">
|
|
53
|
+
<h3 className="text-lg font-semibold mb-2">Sentry API Not Configured</h3>
|
|
54
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
55
|
+
To enable the error dashboard, add the following environment variables:
|
|
56
|
+
</p>
|
|
57
|
+
<ul className="text-sm font-mono space-y-1 bg-muted p-3 rounded-md">
|
|
58
|
+
<li>SENTRY_AUTH_TOKEN=your-auth-token</li>
|
|
59
|
+
<li>SENTRY_ORG=your-org-slug</li>
|
|
60
|
+
<li>SENTRY_PROJECT=your-project-slug</li>
|
|
61
|
+
</ul>
|
|
62
|
+
</Card>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const SentryDashboard = () => {
|
|
67
|
+
const adapter = useAdapter()
|
|
68
|
+
const [stats, setStats] = useState<SentryStats | null>(null)
|
|
69
|
+
const [issues, setIssues] = useState<SentryIssue[]>([])
|
|
70
|
+
const [dataLoading, setDataLoading] = useState(false)
|
|
71
|
+
|
|
72
|
+
const { projects, selectedProject, loading, handleProjectChange } =
|
|
73
|
+
useProjectFilter(adapter, async (slug) => {
|
|
74
|
+
setDataLoading(true)
|
|
75
|
+
try {
|
|
76
|
+
const params =
|
|
77
|
+
slug && slug !== ALL_PROJECTS
|
|
78
|
+
? `?project=${encodeURIComponent(slug)}`
|
|
79
|
+
: ''
|
|
80
|
+
const [statsData, issuesData] = await Promise.all([
|
|
81
|
+
adapter.request<SentryStats>(`/sentry/admin/stats${params}`),
|
|
82
|
+
adapter.request<SentryIssue[]>(`/sentry/admin/issues${params}`),
|
|
83
|
+
])
|
|
84
|
+
setStats(statsData)
|
|
85
|
+
setIssues(issuesData.slice(0, 5))
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error('[Sentry] Failed to fetch dashboard data:', error)
|
|
88
|
+
} finally {
|
|
89
|
+
setDataLoading(false)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const projectSelector =
|
|
94
|
+
projects.length > 0 ? (
|
|
95
|
+
<Select value={selectedProject} onValueChange={handleProjectChange}>
|
|
96
|
+
<SelectTrigger className="w-[180px]">
|
|
97
|
+
<SelectValue />
|
|
98
|
+
</SelectTrigger>
|
|
99
|
+
<SelectContent>
|
|
100
|
+
<SelectItem value={ALL_PROJECTS}>All Projects</SelectItem>
|
|
101
|
+
{projects.map((p) => (
|
|
102
|
+
<SelectItem key={p.slug} value={p.slug}>
|
|
103
|
+
{p.name}
|
|
104
|
+
</SelectItem>
|
|
105
|
+
))}
|
|
106
|
+
</SelectContent>
|
|
107
|
+
</Select>
|
|
108
|
+
) : null
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<>
|
|
112
|
+
<PageHeader
|
|
113
|
+
title="Sentry Dashboard"
|
|
114
|
+
actions={projectSelector ?? undefined}
|
|
115
|
+
/>
|
|
116
|
+
<PageContent>
|
|
117
|
+
{loading || dataLoading || !stats ? (
|
|
118
|
+
<DashboardSkeleton />
|
|
119
|
+
) : !stats.isConfigured ? (
|
|
120
|
+
<div className="p-6">
|
|
121
|
+
<SetupCard />
|
|
122
|
+
</div>
|
|
123
|
+
) : (
|
|
124
|
+
<div className="space-y-6 p-6">
|
|
125
|
+
{stats.apiError ? (
|
|
126
|
+
<Alert variant="destructive">
|
|
127
|
+
<AlertTitle>Could not load Sentry data</AlertTitle>
|
|
128
|
+
<AlertDescription className="text-sm whitespace-pre-wrap">
|
|
129
|
+
{stats.apiError}
|
|
130
|
+
</AlertDescription>
|
|
131
|
+
</Alert>
|
|
132
|
+
) : null}
|
|
133
|
+
<ErrorMetrics
|
|
134
|
+
totalErrors={stats.totalErrors}
|
|
135
|
+
unresolvedIssues={stats.unresolvedIssues}
|
|
136
|
+
errorsLast24h={stats.errorsLast24h}
|
|
137
|
+
/>
|
|
138
|
+
<div>
|
|
139
|
+
<h3 className="text-lg font-semibold mb-4">Recent Issues</h3>
|
|
140
|
+
<RecentIssues issues={issues} />
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
</PageContent>
|
|
145
|
+
</>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default SentryDashboard
|