Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): add analytics page to the dashboard #1764

Merged
merged 16 commits into from
Apr 8, 2024
5 changes: 5 additions & 0 deletions ee/tabby-ui/app/(dashboard)/components/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
CollapsibleTrigger
} from '@/components/ui/collapsible'
import {
IconBarChart,
IconChevronRight,
IconGear,
IconHome,
Expand Down Expand Up @@ -70,6 +71,10 @@ export default function Sidebar({ children, className }: SidebarProps) {
<IconScrollText />
Jobs
</SidebarButton>
<SidebarButton href="/reports">
<IconBarChart />
Reports
</SidebarButton>
<SidebarCollapsible
title={
<>
Expand Down
76 changes: 76 additions & 0 deletions ee/tabby-ui/app/(dashboard)/reports/components/annual-activity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use client'

import { useWindowSize } from '@uidotdev/usehooks'
import moment from 'moment'
import { useTheme } from 'next-themes'
import ReactActivityCalendar from 'react-activity-calendar'

import type { DailyStats } from '../types/stats'

function ActivityCalendar({
data
}: {
data: {
date: string
count: number
level: number
}[]
}) {
const { theme } = useTheme()
const size = useWindowSize()
const width = size.width || 0
const blockSize = width >= 1300 ? 13 : width >= 1000 ? 9 : 5

return (
<ReactActivityCalendar
data={data}
colorScheme={theme === 'dark' ? 'dark' : 'light'}
theme={{
light: ['#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39'],
dark: ['rgb(45, 51, 59)', '#0e4429', '#006d32', '#26a641', '#39d353']
}}
blockSize={blockSize}
hideTotalCount
showWeekdayLabels
/>
)
}

export function AnnualActivity({
yearlyStats
}: {
yearlyStats: DailyStats[] | undefined
}) {
let lastYearCompletions = 0
const dailyCompletionMap: Record<string, number> =
yearlyStats?.reduce((acc, cur) => {
const date = moment(cur.start).format('YYYY-MM-DD')
lastYearCompletions += cur.completions
return { ...acc, [date]: cur.completions }
}, {}) || {}

const data = new Array(365)
.fill('')
.map((_, idx) => {
const date = moment().subtract(idx, 'days').format('YYYY-MM-DD')
const count = dailyCompletionMap[date] || 0
const level = Math.min(4, Math.ceil(count / 5))
return {
date: date,
count,
level
}
})
.reverse()

return (
<div className="flex h-full flex-col rounded-lg border bg-primary-foreground/30 px-6 py-4">
<h3 className="mb-5 text-sm font-medium tracking-tight">
<b>{lastYearCompletions}</b> activities in the last year
</h3>
<div className="flex flex-1 items-center justify-center">
<ActivityCalendar data={data} />
</div>
</div>
)
}
132 changes: 132 additions & 0 deletions ee/tabby-ui/app/(dashboard)/reports/components/daily-activity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
'use client'

import { eachDayOfInterval } from 'date-fns'
import { sum } from 'lodash-es'
import moment from 'moment'
import { useTheme } from 'next-themes'
import type { DateRange } from 'react-day-picker'
import {
Bar,
BarChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis
} from 'recharts'

import { Card, CardContent } from '@/components/ui/card'

import type { DailyStats } from '../types/stats'

function BarTooltip({
active,
payload,
label
}: {
active?: boolean
label?: string
payload?: {
name: string
payload: {
completion: number
select: number
pending: number
}
}[]
}) {
if (active && payload && payload.length) {
const { completion, select } = payload[0].payload
if (!completion) return null
return (
<Card>
<CardContent className="flex flex-col gap-y-0.5 px-4 py-2 text-sm">
<p className="flex items-center">
<span className="mr-3 inline-block w-20">Completion:</span>
<b>{completion}</b>
</p>
<p className="flex items-center">
<span className="mr-3 inline-block w-20">Acceptance:</span>
<b>{select}</b>
</p>
<p className="text-muted-foreground">{label}</p>
</CardContent>
</Card>
)
}

return null
}

export function DailyActivity({
dailyStats,
dateRange
}: {
dailyStats: DailyStats[] | undefined
dateRange: DateRange
}) {
const { theme } = useTheme()
const totalCompletions = sum(dailyStats?.map(stats => stats.completions))
const from = dateRange.from || new Date()
const to = dateRange.to || from

const dailyCompletionMap: Record<string, number> = {}
const dailySelectMap: Record<string, number> = {}

dailyStats?.forEach(stats => {
const date = moment(stats.start).format('YYYY-MM-DD')
dailyCompletionMap[date] = stats.completions
dailySelectMap[date] = stats.selects
}, {})

const daysBetweenRange = eachDayOfInterval({
start: from,
end: to
})

const chartData = daysBetweenRange.map(date => {
const dateKey = moment(date).format('YYYY-MM-DD')
const completion = dailyCompletionMap[dateKey] || 0
const select = dailySelectMap[dateKey] || 0
const pending = completion - select
return {
name: moment(date).format('D MMM'),
completion,
select,
pending
}
})
return (
<div className="rounded-lg border bg-primary-foreground/30 px-6 py-4">
<h3 className="mb-5 text-sm font-medium tracking-tight">
Daily Statistics
</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart
data={chartData}
margin={{
top: 5,
right: 20,
left: 20,
bottom: 5
}}
>
<Bar
dataKey="select"
stackId="stats"
fill={theme === 'dark' ? '#e8e1d3' : '#54452c'}
radius={3}
/>
<Bar
dataKey="pending"
stackId="stats"
fill={theme === 'dark' ? '#423929' : '#e8e1d3'}
radius={3}
/>
<XAxis dataKey="name" fontSize={12} />
<YAxis fontSize={12} width={20} allowDecimals={false} />
<Tooltip cursor={{ fill: 'transparent' }} content={<BarTooltip />} />
</BarChart>
</ResponsiveContainer>
</div>
)
}
Loading
Loading