Skip to content

Commit e042691

Browse files
committed
new dashboard for workspace
1 parent 1b6fc8d commit e042691

34 files changed

Lines changed: 4442 additions & 751 deletions

ANALYTICS_DASHBOARD_SPECS.md

Lines changed: 0 additions & 485 deletions
This file was deleted.

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [6.7] - 2025-09-19
6+
7+
- Add new workspace dashboard
8+
59
## [6.6] - 2025-09-12
610

711
- Bulk update contacts functionality to console

api

261 KB
Binary file not shown.

config/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"github.com/spf13/viper"
1111
)
1212

13-
const VERSION = "6.6"
13+
const VERSION = "6.7"
1414

1515
type Config struct {
1616
Server ServerConfig

console/package-lock.json

Lines changed: 53 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

console/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
"@types/uuid": "^10.0.0",
3838
"antd": "^5.14.0",
3939
"dayjs": "^1.11.13",
40+
"echarts": "^5.6.0",
41+
"echarts-for-react": "^3.0.2",
4042
"emoji-mart": "^5.6.0",
4143
"filesize": "^10.1.6",
4244
"html2canvas": "^1.4.1",

console/src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
33
import { RouterProvider } from '@tanstack/react-router'
44
import { router } from './router'
55
import { AuthProvider } from './contexts/AuthContext'
6+
import { initializeAnalytics } from './utils/analytics-config'
67

78
const queryClient = new QueryClient({
89
defaultOptions: {
@@ -45,6 +46,10 @@ const theme: ThemeConfig = {
4546
}
4647
}
4748
}
49+
50+
// Initialize analytics service
51+
initializeAnalytics()
52+
4853
export function App() {
4954
return (
5055
<QueryClientProvider client={queryClient}>
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import React from 'react'
2+
import { Row, Col, Statistic, Button, Spin } from 'antd'
3+
import { useNavigate } from '@tanstack/react-router'
4+
import { useQuery } from '@tanstack/react-query'
5+
import numbro from 'numbro'
6+
import { EmailMetricsChart } from './EmailMetricsChart'
7+
// import { NewContactsTable } from './NewContactsTable'
8+
import { Workspace } from '../../services/api/types'
9+
import { FailedMessagesTable } from './FailedMessagesTable'
10+
import { NewContactsTable } from './NewContactsTable'
11+
import { emailProviders } from '../integrations/EmailProviders'
12+
import { analyticsService } from '../../services/api/analytics'
13+
14+
interface AnalyticsDashboardProps {
15+
workspace: Workspace
16+
timeRange: [string, string]
17+
timezone?: string
18+
}
19+
20+
export const AnalyticsDashboard: React.FC<AnalyticsDashboardProps> = ({
21+
workspace,
22+
timeRange,
23+
timezone
24+
}) => {
25+
const navigate = useNavigate()
26+
27+
// Use timeRange and timezone as refresh key to update components when they change
28+
const refreshKey = `${timeRange[0]}-${timeRange[1]}-${timezone || ''}`
29+
30+
// Query for total contacts count
31+
const { data: totalContactsData, isLoading: totalContactsLoading } = useQuery({
32+
queryKey: ['analytics', 'total-contacts', workspace.id],
33+
queryFn: async () => {
34+
return analyticsService.query(
35+
{
36+
schema: 'contacts',
37+
measures: ['count'],
38+
dimensions: [],
39+
filters: []
40+
},
41+
workspace.id
42+
)
43+
},
44+
refetchInterval: 60000 // Refetch every minute
45+
})
46+
47+
// Query for new contacts in the given date range
48+
const { data: newContactsData, isLoading: newContactsLoading } = useQuery({
49+
queryKey: ['analytics', 'new-contacts', workspace.id, timeRange[0], timeRange[1]],
50+
queryFn: async () => {
51+
return analyticsService.query(
52+
{
53+
schema: 'contacts',
54+
measures: ['count'],
55+
dimensions: [],
56+
filters: [
57+
{
58+
member: 'created_at',
59+
operator: 'inDateRange',
60+
values: timeRange
61+
}
62+
]
63+
},
64+
workspace.id
65+
)
66+
},
67+
refetchInterval: 60000 // Refetch every minute
68+
})
69+
70+
// Get provider information
71+
const transactionalProvider = workspace.settings.transactional_email_provider_id
72+
? workspace.integrations?.find(
73+
(i) => i.id === workspace.settings.transactional_email_provider_id
74+
)
75+
: null
76+
77+
const marketingProvider = workspace.settings.marketing_email_provider_id
78+
? workspace.integrations?.find((i) => i.id === workspace.settings.marketing_email_provider_id)
79+
: null
80+
81+
const getProviderInfo = (provider: any) => {
82+
if (!provider) return null
83+
return emailProviders.find((p) => p.kind === provider.email_provider.kind)
84+
}
85+
86+
const transactionalProviderInfo = getProviderInfo(transactionalProvider)
87+
const marketingProviderInfo = getProviderInfo(marketingProvider)
88+
89+
const getDefaultSender = (provider: any) => {
90+
if (!provider?.email_provider?.senders) return null
91+
return (
92+
provider.email_provider.senders.find((s: any) => s.is_default) ||
93+
provider.email_provider.senders[0]
94+
)
95+
}
96+
97+
const transactionalSender = getDefaultSender(transactionalProvider)
98+
const marketingSender = getDefaultSender(marketingProvider)
99+
100+
// Calculate totals
101+
const totalContacts = totalContactsData?.data?.[0]?.['count'] || 0
102+
const newContactsCount = newContactsData?.data?.[0]?.['count'] || 0
103+
104+
// Formatter function for statistics that handles loading state
105+
const formatStat = (value: number | string, isLoading: boolean) => {
106+
if (isLoading) {
107+
return <Spin size="small" />
108+
}
109+
return numbro(value).format({ thousandSeparated: true })
110+
}
111+
112+
const handleNavigateToSettings = () => {
113+
navigate({
114+
to: '/workspace/$workspaceId/settings',
115+
params: { workspaceId: workspace.id }
116+
})
117+
}
118+
119+
return (
120+
<div>
121+
{/* Statistics Row - 4 columns */}
122+
<Row gutter={[16, 16]} className="mb-8">
123+
{/* Total Contacts */}
124+
<Col xs={24} sm={12} md={6}>
125+
<div className="bg-gray-50 p-4 rounded-lg" style={{ height: '110px' }}>
126+
<Statistic
127+
title="Total Contacts"
128+
value={totalContacts}
129+
valueStyle={{ fontSize: '24px', fontWeight: 'bold' }}
130+
formatter={(value) => formatStat(value as number, totalContactsLoading)}
131+
/>
132+
</div>
133+
</Col>
134+
135+
{/* New Contacts */}
136+
<Col xs={24} sm={12} md={6}>
137+
<div className="bg-gray-50 p-4 rounded-lg" style={{ height: '110px' }}>
138+
<Statistic
139+
title="New Contacts"
140+
value={newContactsCount}
141+
valueStyle={{ fontSize: '24px', fontWeight: 'bold' }}
142+
formatter={(value) => formatStat(value as number, newContactsLoading)}
143+
/>
144+
</div>
145+
</Col>
146+
147+
{/* Transactional Email Provider */}
148+
<Col xs={24} sm={12} md={6}>
149+
<div className="bg-gray-50 p-4 rounded-lg" style={{ height: '110px' }}>
150+
<div className="text-gray-500 text-sm mb-2">Transactional Provider</div>
151+
{transactionalProvider ? (
152+
<div>
153+
<div className="mb-1">
154+
<span className="font-medium">{transactionalProviderInfo?.name}</span>
155+
</div>
156+
{transactionalSender && (
157+
<div className="text-sm text-gray-600">{transactionalSender.email}</div>
158+
)}
159+
</div>
160+
) : (
161+
<div>
162+
<div className="text-gray-400 mb-2">Not configured</div>
163+
<Button size="small" type="primary" onClick={handleNavigateToSettings}>
164+
Configure
165+
</Button>
166+
</div>
167+
)}
168+
</div>
169+
</Col>
170+
171+
{/* Marketing Email Provider */}
172+
<Col xs={24} sm={12} md={6}>
173+
<div className="bg-gray-50 p-4 rounded-lg" style={{ height: '110px' }}>
174+
<div className="text-gray-500 text-sm mb-2">Marketing Provider</div>
175+
{marketingProvider ? (
176+
<div>
177+
<div className="mb-1">
178+
<span className="font-medium">{marketingProviderInfo?.name}</span>
179+
</div>
180+
{marketingSender && (
181+
<div className="text-sm text-gray-600">{marketingSender.email}</div>
182+
)}
183+
</div>
184+
) : (
185+
<div>
186+
<div className="text-gray-400 mb-2">Not configured</div>
187+
<Button size="small" type="primary" onClick={handleNavigateToSettings}>
188+
Configure
189+
</Button>
190+
</div>
191+
)}
192+
</div>
193+
</Col>
194+
</Row>
195+
196+
{/* Email Metrics Chart - Full Width */}
197+
<EmailMetricsChart
198+
key={`email-metrics-${refreshKey}`}
199+
workspace={workspace}
200+
timeRange={timeRange}
201+
timezone={timezone}
202+
/>
203+
204+
<div className="mt-8">
205+
<NewContactsTable key={`new-contacts-${refreshKey}`} workspace={workspace} />
206+
</div>
207+
208+
<div className="mt-8">
209+
<FailedMessagesTable key={`failed-messages-${refreshKey}`} workspace={workspace} />
210+
</div>
211+
</div>
212+
)
213+
}

0 commit comments

Comments
 (0)