From 7f700d035b73d9d2dadfaf54af3fc537ceb6b35c Mon Sep 17 00:00:00 2001 From: wwayne <wayne.wang0821@gmail.com> Date: Sun, 7 Apr 2024 12:40:45 +0800 Subject: [PATCH 01/16] update --- .../analytics/components/analytic.tsx | 207 +++++++++++++ .../components/analyticAccptance.tsx | 102 +++++++ .../components/analyticDailyCompletion.tsx | 94 ++++++ .../components/analyticYearlyCompletion.tsx | 39 +++ .../app/(dashboard)/analytics/page.tsx | 11 + .../app/(dashboard)/analytics/types/stats.ts | 6 + .../app/(dashboard)/components/sidebar.tsx | 7 +- ee/tabby-ui/components/activity-calendar.tsx | 44 +++ ee/tabby-ui/components/date-range-picker.tsx | 80 +++++ ee/tabby-ui/components/ui/calendar.tsx | 66 ++++ ee/tabby-ui/components/ui/icons.tsx | 24 +- ee/tabby-ui/lib/hooks/use-all-members.tsx | 50 ++++ ee/tabby-ui/lib/tabby/query.ts | 53 ++++ ee/tabby-ui/package.json | 9 + ee/tabby-ui/yarn.lock | 282 +++++++++++++++++- 15 files changed, 1069 insertions(+), 5 deletions(-) create mode 100644 ee/tabby-ui/app/(dashboard)/analytics/components/analytic.tsx create mode 100644 ee/tabby-ui/app/(dashboard)/analytics/components/analyticAccptance.tsx create mode 100644 ee/tabby-ui/app/(dashboard)/analytics/components/analyticDailyCompletion.tsx create mode 100644 ee/tabby-ui/app/(dashboard)/analytics/components/analyticYearlyCompletion.tsx create mode 100644 ee/tabby-ui/app/(dashboard)/analytics/page.tsx create mode 100644 ee/tabby-ui/app/(dashboard)/analytics/types/stats.ts create mode 100644 ee/tabby-ui/components/activity-calendar.tsx create mode 100644 ee/tabby-ui/components/date-range-picker.tsx create mode 100644 ee/tabby-ui/components/ui/calendar.tsx create mode 100644 ee/tabby-ui/lib/hooks/use-all-members.tsx diff --git a/ee/tabby-ui/app/(dashboard)/analytics/components/analytic.tsx b/ee/tabby-ui/app/(dashboard)/analytics/components/analytic.tsx new file mode 100644 index 000000000000..079b699ba98a --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/analytics/components/analytic.tsx @@ -0,0 +1,207 @@ +'use client' + +import { useState } from 'react' +import moment from 'moment' +import { useQuery } from 'urql' +import { DateRange } from "react-day-picker" +import { sum } from "lodash-es" +import numeral from "numeral" + +import { queryDailyStatsInPastYear, queryDailyStats } from '@/lib/tabby/query' +import { Language } from '@/lib/gql/generates/graphql' +import { useAllMembers } from '@/lib/hooks/use-all-members' + +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Skeleton } from '@/components/ui/skeleton' +import DatePickerWithRange from '@/components/date-range-picker' +import LoadingWrapper from '@/components/loading-wrapper' +import { AnalyticDailyCompletion } from './analyticDailyCompletion' +import { AnlyticAcceptance } from './analyticAccptance' +import { AnalyticYearlyCompletion } from './analyticYearlyCompletion' + +import type { DailyStats } from '../types/stats' + +const INITIAL_DATE_RANGE = 14 +const KEY_SELECT_ALL = 'all' + +function AnalyticSummary({ + dailyStats, +}: { + dailyStats: DailyStats[] | undefined, +}) { + const totalCompletions = sum(dailyStats?.map(stats => stats.completions)) + return ( + <div className="flex items-center justify-center space-x-4 xl:justify-start"> + <div className="space-y-0.5 rounded-lg border bg-primary-foreground/30 p-4 lg:min-w-[250px]"> + <p className="text-xs text-muted-foreground lg:text-sm">Total completions</p> + <p className="font-bold lg:text-3xl"> + {numeral(totalCompletions).format('0,0')} + </p> + </div> + + <div className="space-y-0.5 rounded-lg border bg-primary-foreground/30 p-4 lg:min-w-[250px]"> + <p className="text-xs text-muted-foreground lg:text-sm">Minutes saved / completion</p> + <p className="font-bold lg:text-3xl">2</p> + </div> + + <div className="space-y-0.5 rounded-lg border bg-primary-foreground/30 p-4 lg:min-w-[250px]"> + <p className="text-xs text-muted-foreground lg:text-sm">Hours saved in total</p> + <p className="font-bold lg:text-3xl">100</p> + </div> + </div> + ) +} + +export function Analytic() { + const [members] = useAllMembers() + const [dateRange, setDateRange] = useState<DateRange>({ + from: moment().subtract(INITIAL_DATE_RANGE, 'day').toDate(), + to: moment().toDate() + }) + const [selectedMember, setSelectedMember] = useState(KEY_SELECT_ALL) + const [selectedLanguage, setSelectedLanguage] = useState<'all' | Language>(KEY_SELECT_ALL) + + // Query stats of selected date range + const [{ data: dailyStatsData, fetching: fetchingDailyState }] = useQuery({ + query: queryDailyStats, + variables: { + start: moment(dateRange.from).startOf('day').utc().format(), + end: moment(dateRange.to).endOf('day').utc().format(), + users: selectedMember === KEY_SELECT_ALL ? undefined : [selectedMember], + languages: selectedLanguage === KEY_SELECT_ALL ? undefined : [selectedLanguage], + } + }) + const dailyStats: DailyStats[] | undefined = dailyStatsData?.dailyStats.map(item => ({ + start: item.start, + end: item.end, + completions: item.completions, + selects: item.selects, + })) + console.log(dailyStats) + + // Query yearly stats + const [{ data: yearlyStatsData, fetching: fetchingYearlyStats }] = useQuery({ + query: queryDailyStatsInPastYear, + variables: { + users: selectedMember === KEY_SELECT_ALL ? undefined : selectedMember + } + }) + const yearlyStats: DailyStats[] | undefined = yearlyStatsData?.dailyStatsInPastYear.map(item => ({ + start: item.start, + end: item.end, + completions: item.completions, + selects: item.selects, + })) + + const onDateOpenChange = (isOpen: boolean, dateRange: DateRange | undefined) => { + if (!isOpen) { + if (dateRange) { + setDateRange(dateRange) + } + } + } + + return ( + <div className="flex flex-col gap-y-6"> + <div className="flex flex-col items-center justify-between gap-y-3 xl:flex-row xl:gap-y-0"> + <div className="flex flex-col justify-center xl:justify-start"> + <h1 className="mb-1.5 scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl xl:text-left"> + Analytics + </h1> + <p className="text-muted-foreground">Overview of code completion usage</p> + </div> + + <div className="flex flex-col items-center gap-y-2 lg:flex-row lg:gap-y-0 lg:space-x-4"> + <Select defaultValue={KEY_SELECT_ALL} onValueChange={setSelectedMember}> + <SelectTrigger className="w-[300px] lg:w-[180px]" > + <div className="flex w-full items-center truncate "> + <span className="mr-1.5 text-muted-foreground"> + Member: + </span> + <div className="overflow-hidden text-ellipsis"> + <SelectValue /> + </div> + </div> + </SelectTrigger> + <SelectContent> + <SelectGroup> + <SelectItem value={KEY_SELECT_ALL}>All</SelectItem> + {members.map(member => ( + <SelectItem value={member.id} key={member.id}>{member.email}</SelectItem> + ))} + </SelectGroup> + </SelectContent> + </Select> + + <Select defaultValue={KEY_SELECT_ALL} onValueChange={(value: 'all' | Language) => setSelectedLanguage(value)}> + <SelectTrigger className="w-[300px] lg:w-[180px]" > + <div className="flex w-full items-center truncate"> + <span className="mr-1.5 text-muted-foreground"> + Language: + </span> + <div className="overflow-hidden text-ellipsis"> + <SelectValue /> + </div> + </div> + </SelectTrigger> + <SelectContent> + <SelectGroup> + <SelectItem value={'all'}>All</SelectItem> + {Object.entries(Language).map(([key, value]) => ( + <SelectItem key={value} value={value}>{key}</SelectItem> + ))} + </SelectGroup> + </SelectContent> + </Select> + + <DatePickerWithRange + buttonClassName="h-full" + contentAlign="end" + dateRange={dateRange} + onOpenChange={onDateOpenChange} /> + </div> + </div> + + <LoadingWrapper + loading={fetchingDailyState} + fallback={<Skeleton className="h-24 w-1/2" />}> + <AnalyticSummary dailyStats={dailyStats} /> + </LoadingWrapper> + + <LoadingWrapper + loading={fetchingDailyState} + fallback={<Skeleton className="h-64 w-full" />}> + <AnalyticDailyCompletion + dailyStats={dailyStats} + dateRange={dateRange} /> + </LoadingWrapper> + + + <div className="flex flex-col gap-y-6 xl:flex-row xl:gap-x-6 xl:gap-y-0"> + <div className="flex-1"> + <LoadingWrapper + loading={fetchingDailyState} + fallback={<Skeleton className="h-64 w-full" />}> + <AnlyticAcceptance + dailyStats={dailyStats} + dateRange={dateRange} /> + </LoadingWrapper> + </div> + <div style={{ flex: 3 }}> + <LoadingWrapper + loading={fetchingDailyState || fetchingYearlyStats} + fallback={<Skeleton className="h-64 w-full" />}> + <AnalyticYearlyCompletion yearlyStats={yearlyStats} /> + </LoadingWrapper> + </div> + </div> + </div> + ) +} \ No newline at end of file diff --git a/ee/tabby-ui/app/(dashboard)/analytics/components/analyticAccptance.tsx b/ee/tabby-ui/app/(dashboard)/analytics/components/analyticAccptance.tsx new file mode 100644 index 000000000000..7bd0793741c4 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/analytics/components/analyticAccptance.tsx @@ -0,0 +1,102 @@ +import moment from "moment"; + +import type { DateRange } from "react-day-picker" +import type { DailyStats } from '../types/stats' + +import { + Legend, + ResponsiveContainer, + PieChart, Pie, Cell, +} from 'recharts' + +export function AnlyticAcceptance ({ + dailyStats, + dateRange, +}: { + dailyStats: DailyStats[] | undefined; + dateRange: DateRange; +}) { + const from = dateRange.from || new Date() + const to = dateRange.to || from + + let totalAccpet = 0 + let totalCompletions = 0 + dailyStats?.forEach(stats => { + totalCompletions += stats.completions + totalAccpet += stats.selects + }) + const totalPending = totalCompletions - totalAccpet + + const data = [ + { name: 'Accept', value: totalAccpet}, + { name: 'Pending', value: totalPending}, + ]; + + const COLORS = ['#8884d8', '#b9b7e2']; + + const RADIAN = Math.PI / 180; + const renderCustomizedLabel = ({ + cx, + cy, + midAngle, + innerRadius, + outerRadius, + percent, + name + }: { + cx: number; + cy: number; + midAngle: number; + innerRadius: number; + outerRadius: number; + percent: number; + name: string; + }) => { + const radius = innerRadius + (outerRadius - innerRadius) * 0.5; + const x = cx + radius * Math.cos(-midAngle * RADIAN); + const y = cy + radius * Math.sin(-midAngle * RADIAN); + + if (name.toLocaleLowerCase() === 'accept') { + return ( + <text + x={x} + y={y} + fill="white" + textAnchor={x > cx ? 'start' : 'end'} + dominantBaseline="central" + fontSize={12}> + {`${(percent * 100).toFixed(0)}%`} + </text> + ); + } + return + }; + + return ( + <div className="rounded-lg border bg-primary-foreground/30 p-4"> + <h1 className="text-xl font-bold">Acceptance</h1> + <p className="mt-0.5 text-xs text-muted-foreground"> + {moment(from).format('D MMM, YYYY')} - {moment(to).format('D MMM, YYYY')} + </p> + <ResponsiveContainer width="100%" height={250}> + <PieChart width={700} height={400}> + <Pie + data={data} + cx="50%" + cy="50%" + labelLine={false} + label={renderCustomizedLabel} + outerRadius={80} + fill="#8884d8" + dataKey="value" + > + {data.map((entry, index) => ( + <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} /> + ))} + </Pie> + {totalCompletions !== 0 && <Legend />} + </PieChart> + </ResponsiveContainer> + </div> + ) +} \ No newline at end of file diff --git a/ee/tabby-ui/app/(dashboard)/analytics/components/analyticDailyCompletion.tsx b/ee/tabby-ui/app/(dashboard)/analytics/components/analyticDailyCompletion.tsx new file mode 100644 index 000000000000..81d172dddc4f --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/analytics/components/analyticDailyCompletion.tsx @@ -0,0 +1,94 @@ +import { eachDayOfInterval } from 'date-fns' +import moment from 'moment' + +import { + Bar, + BarChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts' +import { Card, CardContent } from '@/components/ui/card' + +import type { DateRange } from "react-day-picker" +import type { DailyStats } from '../types/stats' + +function BarTooltip ({ active, payload, label }: { + active?: boolean; + label?: string + payload?: { + name: string; + value: number; + }[] +}) { + if (active && payload && payload.length) { + const completion = payload[0].value + if (!completion) return null; + return ( + <Card> + <CardContent className="px-4 py-2 text-sm"> + <p>Completions: <b>{completion}</b></p> + <p className="text-muted-foreground">{label}</p> + </CardContent> + </Card> + ); + } + + return null; +}; + +export function AnalyticDailyCompletion ({ + dailyStats, + dateRange, +}: { + dailyStats: DailyStats[] | undefined; + dateRange: DateRange; +}) { + const from = dateRange.from || new Date() + const to = dateRange.to || from + + const dailyCompletionMap: Record<string, number> = dailyStats?.reduce((acc, cur) => { + const date = moment(cur.start).format('YYYY-MM-DD') + return { ...acc, [date]: cur.completions } + }, {}) || {} + + const daysBetweenRange = eachDayOfInterval({ + start: from, + end: to + }); + + const chartData = daysBetweenRange.map(date => { + const completionKey = moment(date).format('YYYY-MM-DD') + const value = dailyCompletionMap[completionKey] || 0 + return { + name: moment(date).format('D MMM'), + value + } + }) + return ( + <div className="rounded-lg border bg-primary-foreground/30 p-4"> + <h1 className="mb-5 text-xl font-bold">Completions</h1> + <ResponsiveContainer width="100%" height={350}> + <BarChart + width={500} + height={300} + data={chartData} + margin={{ + top: 5, + right: 20, + left: 20, + bottom: 5 + }} + > + <Bar dataKey="value" fill="#8884d8" /> + <XAxis dataKey="name" fontSize={12} /> + <YAxis fontSize={12} /> + <Tooltip + cursor={{ fill: 'transparent' }} + content={<BarTooltip />} /> + </BarChart> + </ResponsiveContainer> + </div> + ) +} \ No newline at end of file diff --git a/ee/tabby-ui/app/(dashboard)/analytics/components/analyticYearlyCompletion.tsx b/ee/tabby-ui/app/(dashboard)/analytics/components/analyticYearlyCompletion.tsx new file mode 100644 index 000000000000..bad3fcf21529 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/analytics/components/analyticYearlyCompletion.tsx @@ -0,0 +1,39 @@ +import moment from "moment"; + +import ActivityCalendar from '@/components/activity-calendar' + +import type { DailyStats } from '../types/stats' + +export function AnalyticYearlyCompletion ({ + 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 p-4"> + <h1 className="text-xl font-bold">Activity</h1> + <p className="mt-0.5 text-xs text-muted-foreground">{lastYearCompletions} completions in the last year</p> + <div className="mt-5 flex flex-1 items-center justify-center xl:mt-0"> + <ActivityCalendar data={data} /> + </div> + </div> + ); +} \ No newline at end of file diff --git a/ee/tabby-ui/app/(dashboard)/analytics/page.tsx b/ee/tabby-ui/app/(dashboard)/analytics/page.tsx new file mode 100644 index 000000000000..98b0c6afc3c9 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/analytics/page.tsx @@ -0,0 +1,11 @@ +import { Metadata } from 'next' + +import { Analytic } from './components/analyticAccptance' + +export const metadata: Metadata = { + title: 'Analytic dashboard' +} + +export default function Page() { + return <Analytic /> +} \ No newline at end of file diff --git a/ee/tabby-ui/app/(dashboard)/analytics/types/stats.ts b/ee/tabby-ui/app/(dashboard)/analytics/types/stats.ts new file mode 100644 index 000000000000..3205afa5db11 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/analytics/types/stats.ts @@ -0,0 +1,6 @@ +export type DailyStats = { + start: Date; + end: Date; + completions: number; + selects: number; +} \ No newline at end of file diff --git a/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx b/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx index 7b0d8211a370..6c742c72ccb5 100644 --- a/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx +++ b/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx @@ -22,7 +22,8 @@ import { IconLightingBolt, IconNetwork, IconScrollText, - IconUser + IconUser, + IconBarChart } from '@/components/ui/icons' export interface SidebarProps { @@ -70,6 +71,10 @@ export default function Sidebar({ children, className }: SidebarProps) { <IconScrollText /> Jobs </SidebarButton> + <SidebarButton href="/analytic"> + <IconBarChart /> + Analytic + </SidebarButton> <SidebarCollapsible title={ <> diff --git a/ee/tabby-ui/components/activity-calendar.tsx b/ee/tabby-ui/components/activity-calendar.tsx new file mode 100644 index 000000000000..9d09e20fe6e0 --- /dev/null +++ b/ee/tabby-ui/components/activity-calendar.tsx @@ -0,0 +1,44 @@ +'use client' + +import dynamic from 'next/dynamic'; +import { useTheme } from 'next-themes' +import { useWindowSize } from "@uidotdev/usehooks"; + +// withou using dynamic, we got error "Error: calcTextDimensions() requires browser APIs" +const ReactActivityCalendar = dynamic(() => import('react-activity-calendar'), { + ssr: false, +}); + +export default function ActivityCalendar ({ + data +}: { + data: { + date: string; + count: number; + level: number; + }[] +}) { + const { theme } = useTheme() + const size = useWindowSize() + const width = size.width || 0 + const blockSize = width >= 1600 + ? 13 + : width >= 1400 + ? 10 + : width >= 1000 + ? 8 + : 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 /> + ); +} \ No newline at end of file diff --git a/ee/tabby-ui/components/date-range-picker.tsx b/ee/tabby-ui/components/date-range-picker.tsx new file mode 100644 index 000000000000..b81e588a1f30 --- /dev/null +++ b/ee/tabby-ui/components/date-range-picker.tsx @@ -0,0 +1,80 @@ +"use client" + +import * as React from "react" +import { CalendarIcon } from "@radix-ui/react-icons" +import { addDays, format } from "date-fns" +import { DateRange } from "react-day-picker" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Calendar } from "@/components/ui/calendar" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" + +export default function DatePickerWithRange({ + dateRange, + className, + buttonClassName, + contentAlign, + onOpenChange +}: React.HTMLAttributes<HTMLDivElement> & { + dateRange?: DateRange + buttonClassName?: string; + contentAlign?: "start" | "end" | "center"; + onOpenChange?: (isOpen: boolean, date: DateRange | undefined) => void, + onSelectDateRange?: (date: DateRange | undefined) => void, +}) { + const [date, setDate] = React.useState<DateRange | undefined>({ + from: dateRange?.from || new Date(2022, 0, 20), + to: dateRange?.to || addDays(new Date(2022, 0, 20), 20), + }) + + const toggleOpen = (isOpen: boolean) => { + if (onOpenChange) onOpenChange(isOpen, date) + } + + return ( + <div className={cn("grid gap-2", className)}> + <Popover onOpenChange={toggleOpen}> + <PopoverTrigger asChild> + <Button + id="date" + variant={"outline"} + className={cn( + "w-[300px] justify-start text-left font-normal ", + !date && "text-muted-foreground", + buttonClassName + )} + > + <CalendarIcon className="size-4 mr-2" /> + {date?.from ? ( + date.to ? ( + <> + {format(date.from, "LLL dd, y")} -{" "} + {format(date.to, "LLL dd, y")} + </> + ) : ( + format(date.from, "LLL dd, y") + ) + ) : ( + <span>Pick a date</span> + )} + </Button> + </PopoverTrigger> + <PopoverContent className="w-auto p-0" align={contentAlign}> + <Calendar + initialFocus + mode="range" + defaultMonth={date?.from} + selected={date} + onSelect={setDate} + numberOfMonths={2} + /> + </PopoverContent> + </Popover> + </div> + ) +} \ No newline at end of file diff --git a/ee/tabby-ui/components/ui/calendar.tsx b/ee/tabby-ui/components/ui/calendar.tsx new file mode 100644 index 000000000000..43868fea1cf7 --- /dev/null +++ b/ee/tabby-ui/components/ui/calendar.tsx @@ -0,0 +1,66 @@ +"use client" + +import * as React from "react" +import { ChevronLeft, ChevronRight } from "lucide-react" +import { DayPicker } from "react-day-picker" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +export type CalendarProps = React.ComponentProps<typeof DayPicker> + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}: CalendarProps) { + return ( + <DayPicker + showOutsideDays={showOutsideDays} + className={cn("p-3", className)} + classNames={{ + months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", + month: "space-y-4", + caption: "flex justify-center pt-1 relative items-center", + caption_label: "text-sm font-medium", + nav: "space-x-1 flex items-center", + nav_button: cn( + buttonVariants({ variant: "outline" }), + "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100" + ), + nav_button_previous: "absolute left-1", + nav_button_next: "absolute right-1", + table: "w-full border-collapse space-y-1", + head_row: "flex", + head_cell: + "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]", + row: "flex w-full mt-2", + cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20", + day: cn( + buttonVariants({ variant: "ghost" }), + "h-9 w-9 p-0 font-normal aria-selected:opacity-100" + ), + day_range_end: "day-range-end", + day_selected: + "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", + day_today: "bg-accent text-accent-foreground", + day_outside: + "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30", + day_disabled: "text-muted-foreground opacity-50", + day_range_middle: + "aria-selected:bg-accent aria-selected:text-accent-foreground", + day_hidden: "invisible", + ...classNames, + }} + components={{ + IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />, + IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />, + }} + {...props} + /> + ) +} +Calendar.displayName = "Calendar" + +export { Calendar } \ No newline at end of file diff --git a/ee/tabby-ui/components/ui/icons.tsx b/ee/tabby-ui/components/ui/icons.tsx index 5958e4248cec..eb61e8eca493 100644 --- a/ee/tabby-ui/components/ui/icons.tsx +++ b/ee/tabby-ui/components/ui/icons.tsx @@ -1270,6 +1270,27 @@ function IconStopWatch({ className, ...props }: React.ComponentProps<'svg'>) { ) } +function IconBarChart({ className, ...props }: React.ComponentProps<'svg'>) { + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" + className={cn('h-4 w-4', className)} + {...props} + > + <path d="M3 3v18h18" /> + <path d="M18 17V9" /> + <path d="M13 17V5" /> + <path d="M8 17v-3" /> + </svg> + ) +} + export { IconEdit, IconNextChat, @@ -1334,5 +1355,6 @@ export { IconCloudUpload, IconHistory, IconClock, - IconStopWatch + IconStopWatch, + IconBarChart } diff --git a/ee/tabby-ui/lib/hooks/use-all-members.tsx b/ee/tabby-ui/lib/hooks/use-all-members.tsx new file mode 100644 index 000000000000..336c8d45e01d --- /dev/null +++ b/ee/tabby-ui/lib/hooks/use-all-members.tsx @@ -0,0 +1,50 @@ +import { useState, useEffect } from 'react' +import { useQuery } from 'urql' + +import { QueryVariables } from '@/lib/tabby/gql' +import { listUsers } from '@/lib/tabby/query' +import { DEFAULT_PAGE_SIZE } from '@/lib/constants' + +type Member = { + id: string; + email: string; +} + +export function useAllMembers() { + const [queryVariables, setQueryVariables] = useState< + QueryVariables<typeof listUsers> + >({ first: DEFAULT_PAGE_SIZE }) + + const [list, setList] = useState<Member[]>([]) + const [isAllLoaded, setIsAllLoaded] = useState(false) + + const [{ data, fetching }] = useQuery({ + query: listUsers, + variables: queryVariables + }) + + useEffect(() => { + if (isAllLoaded) return + if (!fetching && data) { + const members: Member[] = data?.users.edges.map(edge => ({ + id: edge.node.id, + email: edge.node.email + })) + const cursor = data?.users.pageInfo.endCursor || "" + const hasMore = data?.users.pageInfo.hasNextPage + const currentList = [...list] + + setList(currentList.concat(members)) + if (hasMore) { + setQueryVariables({ + first: DEFAULT_PAGE_SIZE, + after: cursor + }) + } else { + setIsAllLoaded(true) + } + } + }, [queryVariables, fetching]) + + return [list] +} diff --git a/ee/tabby-ui/lib/tabby/query.ts b/ee/tabby-ui/lib/tabby/query.ts index 2efcc08a8d72..895094464651 100644 --- a/ee/tabby-ui/lib/tabby/query.ts +++ b/ee/tabby-ui/lib/tabby/query.ts @@ -102,3 +102,56 @@ export const listJobs = graphql(/* GraphQL */ ` jobs } `) + +export const listUsers = graphql(/* GraphQL */ ` + query ListUsers($after: String, $before: String, $first: Int, $last: Int) { + users(after: $after, before: $before, first: $first, last: $last) { + edges { + node { + id + email + isAdmin + isOwner + createdAt + active + } + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } +`) + +export const queryDailyStatsInPastYear = graphql(/* GraphQL */ ` + query DailyStatsInPastYear( + $users: [ID!] + ) { + dailyStatsInPastYear(users: $users) { + start + end + completions + selects + } + } +`) + +export const queryDailyStats = graphql(/* GraphQL */ ` + query DailyStats( + $start: DateTimeUtc!, + $end: DateTimeUtc!, + $users: [ID!], + $languages: [Language!] + ) { + dailyStats(start: $start, end: $end, users: $users, languages: $languages) { + start + end + completions + selects + } + } +`) diff --git a/ee/tabby-ui/package.json b/ee/tabby-ui/package.json index 1ed6a1a5f2a4..a72fa217670d 100644 --- a/ee/tabby-ui/package.json +++ b/ee/tabby-ui/package.json @@ -32,6 +32,7 @@ "@radix-ui/react-dialog": "1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-hover-card": "^1.0.7", + "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-radio-group": "^1.1.3", @@ -42,6 +43,7 @@ "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.6", + "@uidotdev/usehooks": "^2.4.1", "@uiw/codemirror-extensions-langs": "^4.21.21", "@urql/core": "^4.2.3", "@urql/exchange-auth": "^2.1.6", @@ -54,6 +56,7 @@ "clsx": "^1.2.1", "compare-versions": "^6.1.0", "copy-to-clipboard": "^3.3.3", + "date-fns": "^3.6.0", "downshift": "^8.2.2", "filename2prism": "^3.0.0", "focus-trap-react": "^10.1.1", @@ -61,14 +64,18 @@ "humanize-duration": "^3.31.0", "jwt-decode": "^4.0.0", "lodash-es": "^4.17.21", + "lucide-react": "^0.365.0", "mitt": "^3.0.1", "moment": "^2.29.4", "nanoid": "^4.0.2", "next": "^13.4.7", "next-themes": "^0.2.1", + "numeral": "^2.0.6", "openai-edge": "^0.5.1", "pretty-bytes": "^6.1.1", "react": "^18.2.0", + "react-activity-calendar": "^2.2.8", + "react-day-picker": "^8.10.0", "react-dom": "^18.2.0", "react-hook-form": "^7.48.2", "react-intersection-observer": "^9.4.4", @@ -78,6 +85,7 @@ "react-syntax-highlighter": "^15.5.0", "react-textarea-autosize": "^8.4.1", "react-topbar-progress-indicator": "^4.1.1", + "recharts": "^2.12.4", "rehype-raw": "6.1.1", "rehype-sanitize": "^6.0.0", "remark-gfm": "^3.0.1", @@ -98,6 +106,7 @@ "@types/humanize-duration": "^3.27.4", "@types/lodash-es": "^4.17.10", "@types/node": "^17.0.12", + "@types/numeral": "^2.0.5", "@types/react": "^18.0.22", "@types/react-dom": "^18.0.7", "@types/react-syntax-highlighter": "^15.5.6", diff --git a/ee/tabby-ui/yarn.lock b/ee/tabby-ui/yarn.lock index 25737c430d4a..6a67a9cb1ee7 100644 --- a/ee/tabby-ui/yarn.lock +++ b/ee/tabby-ui/yarn.lock @@ -559,6 +559,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" + integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" @@ -2049,6 +2056,11 @@ "@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-use-controllable-state" "1.0.1" +"@radix-ui/react-icons@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.0.tgz#c61af8f323d87682c5ca76b856d60c2312dbcb69" + integrity sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw== + "@radix-ui/react-id@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0" @@ -2448,6 +2460,62 @@ lodash.merge "^4.6.2" postcss-selector-parser "6.0.10" +"@types/chroma-js@^2.4.3": + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-2.4.4.tgz#254dddff54568ff8e5d0dcdb071871a458fdfd31" + integrity sha512-/DTccpHTaKomqussrn+ciEvfW4k6NAHzNzs/sts1TCqg333qNxOhy8TNIoQCmbGG3Tl8KdEhkGAssb1n3mTXiQ== + +"@types/d3-array@^3.0.3": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" + integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== + +"@types/d3-color@*": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" + integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== + +"@types/d3-ease@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" + integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== + +"@types/d3-interpolate@^3.0.1": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a" + integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ== + +"@types/d3-scale@^4.0.2": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" + integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== + dependencies: + "@types/d3-time" "*" + +"@types/d3-shape@^3.1.0": + version "3.1.6" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72" + integrity sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time@*", "@types/d3-time@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be" + integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw== + +"@types/d3-timer@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" + integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== + "@types/debug@^4.0.0": version "4.1.9" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.9.tgz#906996938bc672aaf2fb8c0d3733ae1dda05b005" @@ -2546,6 +2614,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.1.tgz#80b22f3df719f15c9736207980e95f35d01ec1aa" integrity sha512-3G42sxmm0fF2+Vtb9TJQpnjmP+uKlWvFa8KoEGquh4gqRmoUG/N0ufuhikw6HEsdG2G2oIKhog1GCTfz9v5NdQ== +"@types/numeral@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-2.0.5.tgz#388e5c4ff4b0e1787f130753cbbe83d3ba770858" + integrity sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw== + "@types/parse5@^6.0.0": version "6.0.3" resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb" @@ -2725,6 +2798,11 @@ "@typescript-eslint/types" "7.4.0" eslint-visitor-keys "^3.4.1" +"@uidotdev/usehooks@^2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@uidotdev/usehooks/-/usehooks-2.4.1.tgz#4b733eaeae09a7be143c6c9ca158b56cc1ea75bf" + integrity sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg== + "@uiw/codemirror-extensions-langs@^4.21.21": version "4.21.21" resolved "https://registry.yarnpkg.com/@uiw/codemirror-extensions-langs/-/codemirror-extensions-langs-4.21.21.tgz#c0aee1719a69e62b133b01d26c7a21bb3d1b559a" @@ -3438,7 +3516,7 @@ chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" -chroma-js@^2.1.2: +chroma-js@^2.1.2, chroma-js@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.4.2.tgz#dffc214ed0c11fa8eefca2c36651d8e57cbfb2b0" integrity sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A== @@ -3511,6 +3589,11 @@ clsx@^1.2.1: resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== +clsx@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb" + integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg== + codemirror-lang-mermaid@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/codemirror-lang-mermaid/-/codemirror-lang-mermaid-0.5.0.tgz#7e31bd474128febf6296d72412e85d86a9a493eb" @@ -3705,6 +3788,77 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== +"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6: + version "3.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + +"d3-color@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +d3-ease@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +"d3-format@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + +"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + +d3-scale@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + dependencies: + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" + +d3-shape@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + +"d3-time-format@2 - 4": + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + dependencies: + d3-array "2 - 3" + +d3-timer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + damerau-levenshtein@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" @@ -3715,6 +3869,11 @@ dataloader@^2.2.2: resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.2.2.tgz#216dc509b5abe39d43a9b9d97e6e5e473dfbe3e0" integrity sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g== +date-fns@^3.2.0, date-fns@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf" + integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== + debounce@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" @@ -3739,6 +3898,11 @@ decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== +decimal.js-light@^2.4.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" + integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== + decode-named-character-reference@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" @@ -3859,6 +4023,14 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + dot-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" @@ -4285,6 +4457,11 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +eventemitter3@^4.0.1: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + eventsource-parser@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-1.0.0.tgz#6332e37fd5512e3c8d9df05773b2bf9e152ccc04" @@ -4319,6 +4496,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-equals@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.0.1.tgz#a4eefe3c5d1c0d021aeed0bc10ba5e0c12ee405d" + integrity sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ== + fast-glob@^3.2.12, fast-glob@^3.2.5, fast-glob@^3.2.9, fast-glob@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" @@ -5025,6 +5207,11 @@ internal-slot@^1.0.5: has "^1.0.3" side-channel "^1.0.4" +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -5634,6 +5821,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lucide-react@^0.365.0: + version "0.365.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.365.0.tgz#aa15b62e004bf35d557451f4b3aed2f6da5e2d84" + integrity sha512-sJYpPyyzGHI4B3pys+XSFnE4qtSWc68rFnDLxbNNKjkLST5XSx9DNn5+1Z3eFgFiw39PphNRiVBSVb+AL3oKwA== + map-cache@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -6322,6 +6514,11 @@ nullthrows@^1.1.1: resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== +numeral@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/numeral/-/numeral-2.0.6.tgz#4ad080936d443c2561aed9f2197efffe25f4e506" + integrity sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA== + object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -6771,7 +6968,7 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.0.0, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.0.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -6819,6 +7016,20 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +react-activity-calendar@^2.2.8: + version "2.2.8" + resolved "https://registry.yarnpkg.com/react-activity-calendar/-/react-activity-calendar-2.2.8.tgz#8742d607588f1b17d094fd09c96832b4f781beb2" + integrity sha512-+Zr6UlPC4zLMSkXyg/+bQ0Fd10E8V+nT+AU0FHfCIQi6/dLJe73cR+CkFFf8zPE59XhGO24U7FfUyFgbWqV0Bg== + dependencies: + "@types/chroma-js" "^2.4.3" + chroma-js "^2.4.2" + date-fns "^3.2.0" + +react-day-picker@^8.10.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-8.10.0.tgz#729c5b9564967a924213978fb9c0751884a60595" + integrity sha512-mz+qeyrOM7++1NCb1ARXmkjMkzWVh2GL9YiPbRjKe0zHccvekk4HE+0MPOZOrosn8r8zTHIIeOUXTmXRqmkRmg== + react-dom@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" @@ -6837,7 +7048,7 @@ react-intersection-observer@^9.4.4: resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.5.2.tgz#f68363a1ff292323c0808201b58134307a1626d0" integrity sha512-EmoV66/yvksJcGa1rdW0nDNc4I1RifDWkT50gXSFnPLYQ4xUptuDD4V7k+Rj1OgVAlww628KLGcxPXFlOkkU/Q== -react-is@^16.13.1: +react-is@^16.10.2, react-is@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -6901,6 +7112,15 @@ react-resizable-panels@^1.0.7: resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-1.0.7.tgz#f48b4dd42f143bde01a0441580578806fadc16eb" integrity sha512-CluJkHQheeNqIJly2FYDfri3ME+2h2nCXpf0Y+hTO1K1eVtNxXFA5hVp5cUD6NS70iiufswOmnku9QZiLr1hYg== +react-smooth@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-4.0.1.tgz#6200d8699bfe051ae40ba187988323b1449eab1a" + integrity sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w== + dependencies: + fast-equals "^5.0.1" + prop-types "^15.8.1" + react-transition-group "^4.4.5" + react-style-singleton@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" @@ -6937,6 +7157,16 @@ react-topbar-progress-indicator@^4.1.1: dependencies: topbar "^0.1.3" +react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" @@ -6976,6 +7206,27 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +recharts-scale@^0.4.4: + version "0.4.5" + resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9" + integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w== + dependencies: + decimal.js-light "^2.4.1" + +recharts@^2.12.4: + version "2.12.4" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.12.4.tgz#e560a57cd44ab554c99a0d93bdd58d059b309a2e" + integrity sha512-dM4skmk4fDKEDjL9MNunxv6zcTxePGVEzRnLDXALRpfJ85JoQ0P0APJ/CoJlmnQI0gPjBlOkjzrwrfQrRST3KA== + dependencies: + clsx "^2.0.0" + eventemitter3 "^4.0.1" + lodash "^4.17.21" + react-is "^16.10.2" + react-smooth "^4.0.0" + recharts-scale "^0.4.4" + tiny-invariant "^1.3.1" + victory-vendor "^36.6.8" + reflect.getprototypeof@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz#aaccbf41aca3821b87bb71d9dcbc7ad0ba50a3f3" @@ -7723,6 +7974,11 @@ tiny-inflate@^1.0.0: resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== +tiny-invariant@^1.3.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + title-case@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/title-case/-/title-case-3.0.3.tgz#bc689b46f02e411f1d1e1d081f7c3deca0489982" @@ -8123,6 +8379,26 @@ vfile@^5.0.0: unist-util-stringify-position "^3.0.0" vfile-message "^3.0.0" +victory-vendor@^36.6.8: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.9.2.tgz#668b02a448fa4ea0f788dbf4228b7e64669ff801" + integrity sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ== + dependencies: + "@types/d3-array" "^3.0.3" + "@types/d3-ease" "^3.0.0" + "@types/d3-interpolate" "^3.0.1" + "@types/d3-scale" "^4.0.2" + "@types/d3-shape" "^3.1.0" + "@types/d3-time" "^3.0.0" + "@types/d3-timer" "^3.0.0" + d3-array "^3.1.6" + d3-ease "^3.0.1" + d3-interpolate "^3.0.1" + d3-scale "^4.0.2" + d3-shape "^3.1.0" + d3-time "^3.0.0" + d3-timer "^3.0.1" + w3c-keyname@^2.2.4: version "2.2.8" resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" From 817b79612989b7a9b0b63111a8a8f5ee42e54512 Mon Sep 17 00:00:00 2001 From: wwayne <wayne.wang0821@gmail.com> Date: Sun, 7 Apr 2024 14:22:08 +0800 Subject: [PATCH 02/16] polishment --- .../app/(dashboard)/analytics/components/analytic.tsx | 1 - .../(dashboard)/analytics/components/analyticAccptance.tsx | 7 +++++-- ee/tabby-ui/app/(dashboard)/analytics/page.tsx | 4 ++-- ee/tabby-ui/app/(dashboard)/components/sidebar.tsx | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/ee/tabby-ui/app/(dashboard)/analytics/components/analytic.tsx b/ee/tabby-ui/app/(dashboard)/analytics/components/analytic.tsx index 079b699ba98a..bdd5ec502d0c 100644 --- a/ee/tabby-ui/app/(dashboard)/analytics/components/analytic.tsx +++ b/ee/tabby-ui/app/(dashboard)/analytics/components/analytic.tsx @@ -84,7 +84,6 @@ export function Analytic() { completions: item.completions, selects: item.selects, })) - console.log(dailyStats) // Query yearly stats const [{ data: yearlyStatsData, fetching: fetchingYearlyStats }] = useQuery({ diff --git a/ee/tabby-ui/app/(dashboard)/analytics/components/analyticAccptance.tsx b/ee/tabby-ui/app/(dashboard)/analytics/components/analyticAccptance.tsx index 7bd0793741c4..a4ee4b858ff0 100644 --- a/ee/tabby-ui/app/(dashboard)/analytics/components/analyticAccptance.tsx +++ b/ee/tabby-ui/app/(dashboard)/analytics/components/analyticAccptance.tsx @@ -1,3 +1,6 @@ + +'use client' + import moment from "moment"; import type { DateRange } from "react-day-picker" @@ -78,7 +81,7 @@ export function AnlyticAcceptance ({ <p className="mt-0.5 text-xs text-muted-foreground"> {moment(from).format('D MMM, YYYY')} - {moment(to).format('D MMM, YYYY')} </p> - <ResponsiveContainer width="100%" height={250}> + <ResponsiveContainer width="100%" height={210}> <PieChart width={700} height={400}> <Pie data={data} @@ -94,7 +97,7 @@ export function AnlyticAcceptance ({ <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} /> ))} </Pie> - {totalCompletions !== 0 && <Legend />} + {totalCompletions !== 0 && <Legend wrapperStyle={{ fontSize: '12px' }} />} </PieChart> </ResponsiveContainer> </div> diff --git a/ee/tabby-ui/app/(dashboard)/analytics/page.tsx b/ee/tabby-ui/app/(dashboard)/analytics/page.tsx index 98b0c6afc3c9..93be631a3150 100644 --- a/ee/tabby-ui/app/(dashboard)/analytics/page.tsx +++ b/ee/tabby-ui/app/(dashboard)/analytics/page.tsx @@ -1,9 +1,9 @@ import { Metadata } from 'next' -import { Analytic } from './components/analyticAccptance' +import { Analytic } from './components/analytic' export const metadata: Metadata = { - title: 'Analytic dashboard' + title: 'Analytics Dashboard' } export default function Page() { diff --git a/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx b/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx index 6c742c72ccb5..e2d6fd34b3d2 100644 --- a/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx +++ b/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx @@ -71,9 +71,9 @@ export default function Sidebar({ children, className }: SidebarProps) { <IconScrollText /> Jobs </SidebarButton> - <SidebarButton href="/analytic"> + <SidebarButton href="/analytics"> <IconBarChart /> - Analytic + Analytics </SidebarButton> <SidebarCollapsible title={ From 309abe415cb81649a0079ca853c4e28989cf9944 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 7 Apr 2024 06:23:17 +0000 Subject: [PATCH 03/16] [autofix.ci] apply automated fixes --- .../analytics/components/analytic.tsx | 137 +++++++------ .../components/analyticAccptance.tsx | 185 +++++++++--------- .../components/analyticDailyCompletion.tsx | 151 +++++++------- .../components/analyticYearlyCompletion.tsx | 50 ++--- .../app/(dashboard)/analytics/page.tsx | 2 +- .../app/(dashboard)/analytics/types/stats.ts | 10 +- .../app/(dashboard)/components/sidebar.tsx | 4 +- ee/tabby-ui/components/activity-calendar.tsx | 34 ++-- ee/tabby-ui/components/date-range-picker.tsx | 46 ++--- ee/tabby-ui/components/ui/calendar.tsx | 68 +++---- ee/tabby-ui/lib/hooks/use-all-members.tsx | 12 +- ee/tabby-ui/lib/tabby/query.ts | 10 +- 12 files changed, 371 insertions(+), 338 deletions(-) diff --git a/ee/tabby-ui/app/(dashboard)/analytics/components/analytic.tsx b/ee/tabby-ui/app/(dashboard)/analytics/components/analytic.tsx index bdd5ec502d0c..036989e0e5fa 100644 --- a/ee/tabby-ui/app/(dashboard)/analytics/components/analytic.tsx +++ b/ee/tabby-ui/app/(dashboard)/analytics/components/analytic.tsx @@ -1,58 +1,63 @@ 'use client' import { useState } from 'react' +import { sum } from 'lodash-es' import moment from 'moment' +import numeral from 'numeral' +import { DateRange } from 'react-day-picker' import { useQuery } from 'urql' -import { DateRange } from "react-day-picker" -import { sum } from "lodash-es" -import numeral from "numeral" -import { queryDailyStatsInPastYear, queryDailyStats } from '@/lib/tabby/query' import { Language } from '@/lib/gql/generates/graphql' import { useAllMembers } from '@/lib/hooks/use-all-members' - +import { queryDailyStats, queryDailyStatsInPastYear } from '@/lib/tabby/query' import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, - SelectValue, -} from "@/components/ui/select" + SelectValue +} from '@/components/ui/select' import { Skeleton } from '@/components/ui/skeleton' import DatePickerWithRange from '@/components/date-range-picker' import LoadingWrapper from '@/components/loading-wrapper' -import { AnalyticDailyCompletion } from './analyticDailyCompletion' -import { AnlyticAcceptance } from './analyticAccptance' -import { AnalyticYearlyCompletion } from './analyticYearlyCompletion' import type { DailyStats } from '../types/stats' +import { AnlyticAcceptance } from './analyticAccptance' +import { AnalyticDailyCompletion } from './analyticDailyCompletion' +import { AnalyticYearlyCompletion } from './analyticYearlyCompletion' const INITIAL_DATE_RANGE = 14 const KEY_SELECT_ALL = 'all' function AnalyticSummary({ - dailyStats, + dailyStats }: { - dailyStats: DailyStats[] | undefined, + dailyStats: DailyStats[] | undefined }) { const totalCompletions = sum(dailyStats?.map(stats => stats.completions)) return ( <div className="flex items-center justify-center space-x-4 xl:justify-start"> <div className="space-y-0.5 rounded-lg border bg-primary-foreground/30 p-4 lg:min-w-[250px]"> - <p className="text-xs text-muted-foreground lg:text-sm">Total completions</p> + <p className="text-xs text-muted-foreground lg:text-sm"> + Total completions + </p> <p className="font-bold lg:text-3xl"> {numeral(totalCompletions).format('0,0')} </p> </div> <div className="space-y-0.5 rounded-lg border bg-primary-foreground/30 p-4 lg:min-w-[250px]"> - <p className="text-xs text-muted-foreground lg:text-sm">Minutes saved / completion</p> + <p className="text-xs text-muted-foreground lg:text-sm"> + Minutes saved / completion + </p> <p className="font-bold lg:text-3xl">2</p> </div> <div className="space-y-0.5 rounded-lg border bg-primary-foreground/30 p-4 lg:min-w-[250px]"> - <p className="text-xs text-muted-foreground lg:text-sm">Hours saved in total</p> + <p className="text-xs text-muted-foreground lg:text-sm"> + Hours saved in total + </p> <p className="font-bold lg:text-3xl">100</p> </div> </div> @@ -66,7 +71,9 @@ export function Analytic() { to: moment().toDate() }) const [selectedMember, setSelectedMember] = useState(KEY_SELECT_ALL) - const [selectedLanguage, setSelectedLanguage] = useState<'all' | Language>(KEY_SELECT_ALL) + const [selectedLanguage, setSelectedLanguage] = useState<'all' | Language>( + KEY_SELECT_ALL + ) // Query stats of selected date range const [{ data: dailyStatsData, fetching: fetchingDailyState }] = useQuery({ @@ -75,15 +82,18 @@ export function Analytic() { start: moment(dateRange.from).startOf('day').utc().format(), end: moment(dateRange.to).endOf('day').utc().format(), users: selectedMember === KEY_SELECT_ALL ? undefined : [selectedMember], - languages: selectedLanguage === KEY_SELECT_ALL ? undefined : [selectedLanguage], + languages: + selectedLanguage === KEY_SELECT_ALL ? undefined : [selectedLanguage] } }) - const dailyStats: DailyStats[] | undefined = dailyStatsData?.dailyStats.map(item => ({ - start: item.start, - end: item.end, - completions: item.completions, - selects: item.selects, - })) + const dailyStats: DailyStats[] | undefined = dailyStatsData?.dailyStats.map( + item => ({ + start: item.start, + end: item.end, + completions: item.completions, + selects: item.selects + }) + ) // Query yearly stats const [{ data: yearlyStatsData, fetching: fetchingYearlyStats }] = useQuery({ @@ -92,14 +102,18 @@ export function Analytic() { users: selectedMember === KEY_SELECT_ALL ? undefined : selectedMember } }) - const yearlyStats: DailyStats[] | undefined = yearlyStatsData?.dailyStatsInPastYear.map(item => ({ - start: item.start, - end: item.end, - completions: item.completions, - selects: item.selects, - })) - - const onDateOpenChange = (isOpen: boolean, dateRange: DateRange | undefined) => { + const yearlyStats: DailyStats[] | undefined = + yearlyStatsData?.dailyStatsInPastYear.map(item => ({ + start: item.start, + end: item.end, + completions: item.completions, + selects: item.selects + })) + + const onDateOpenChange = ( + isOpen: boolean, + dateRange: DateRange | undefined + ) => { if (!isOpen) { if (dateRange) { setDateRange(dateRange) @@ -114,16 +128,19 @@ export function Analytic() { <h1 className="mb-1.5 scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl xl:text-left"> Analytics </h1> - <p className="text-muted-foreground">Overview of code completion usage</p> + <p className="text-muted-foreground"> + Overview of code completion usage + </p> </div> <div className="flex flex-col items-center gap-y-2 lg:flex-row lg:gap-y-0 lg:space-x-4"> - <Select defaultValue={KEY_SELECT_ALL} onValueChange={setSelectedMember}> - <SelectTrigger className="w-[300px] lg:w-[180px]" > + <Select + defaultValue={KEY_SELECT_ALL} + onValueChange={setSelectedMember} + > + <SelectTrigger className="w-[300px] lg:w-[180px]"> <div className="flex w-full items-center truncate "> - <span className="mr-1.5 text-muted-foreground"> - Member: - </span> + <span className="mr-1.5 text-muted-foreground">Member:</span> <div className="overflow-hidden text-ellipsis"> <SelectValue /> </div> @@ -133,18 +150,23 @@ export function Analytic() { <SelectGroup> <SelectItem value={KEY_SELECT_ALL}>All</SelectItem> {members.map(member => ( - <SelectItem value={member.id} key={member.id}>{member.email}</SelectItem> + <SelectItem value={member.id} key={member.id}> + {member.email} + </SelectItem> ))} </SelectGroup> </SelectContent> </Select> - <Select defaultValue={KEY_SELECT_ALL} onValueChange={(value: 'all' | Language) => setSelectedLanguage(value)}> - <SelectTrigger className="w-[300px] lg:w-[180px]" > + <Select + defaultValue={KEY_SELECT_ALL} + onValueChange={(value: 'all' | Language) => + setSelectedLanguage(value) + } + > + <SelectTrigger className="w-[300px] lg:w-[180px]"> <div className="flex w-full items-center truncate"> - <span className="mr-1.5 text-muted-foreground"> - Language: - </span> + <span className="mr-1.5 text-muted-foreground">Language:</span> <div className="overflow-hidden text-ellipsis"> <SelectValue /> </div> @@ -154,7 +176,9 @@ export function Analytic() { <SelectGroup> <SelectItem value={'all'}>All</SelectItem> {Object.entries(Language).map(([key, value]) => ( - <SelectItem key={value} value={value}>{key}</SelectItem> + <SelectItem key={value} value={value}> + {key} + </SelectItem> ))} </SelectGroup> </SelectContent> @@ -164,43 +188,46 @@ export function Analytic() { buttonClassName="h-full" contentAlign="end" dateRange={dateRange} - onOpenChange={onDateOpenChange} /> + onOpenChange={onDateOpenChange} + /> </div> </div> <LoadingWrapper loading={fetchingDailyState} - fallback={<Skeleton className="h-24 w-1/2" />}> + fallback={<Skeleton className="h-24 w-1/2" />} + > <AnalyticSummary dailyStats={dailyStats} /> </LoadingWrapper> <LoadingWrapper loading={fetchingDailyState} - fallback={<Skeleton className="h-64 w-full" />}> + fallback={<Skeleton className="h-64 w-full" />} + > <AnalyticDailyCompletion dailyStats={dailyStats} - dateRange={dateRange} /> + dateRange={dateRange} + /> </LoadingWrapper> - <div className="flex flex-col gap-y-6 xl:flex-row xl:gap-x-6 xl:gap-y-0"> <div className="flex-1"> <LoadingWrapper loading={fetchingDailyState} - fallback={<Skeleton className="h-64 w-full" />}> - <AnlyticAcceptance - dailyStats={dailyStats} - dateRange={dateRange} /> + fallback={<Skeleton className="h-64 w-full" />} + > + <AnlyticAcceptance dailyStats={dailyStats} dateRange={dateRange} /> </LoadingWrapper> </div> <div style={{ flex: 3 }}> <LoadingWrapper loading={fetchingDailyState || fetchingYearlyStats} - fallback={<Skeleton className="h-64 w-full" />}> + fallback={<Skeleton className="h-64 w-full" />} + > <AnalyticYearlyCompletion yearlyStats={yearlyStats} /> </LoadingWrapper> </div> </div> </div> ) -} \ No newline at end of file +} diff --git a/ee/tabby-ui/app/(dashboard)/analytics/components/analyticAccptance.tsx b/ee/tabby-ui/app/(dashboard)/analytics/components/analyticAccptance.tsx index a4ee4b858ff0..61e79f072a42 100644 --- a/ee/tabby-ui/app/(dashboard)/analytics/components/analyticAccptance.tsx +++ b/ee/tabby-ui/app/(dashboard)/analytics/components/analyticAccptance.tsx @@ -1,105 +1,106 @@ - 'use client' -import moment from "moment"; +import moment from 'moment' +import type { DateRange } from 'react-day-picker' +import { Cell, Legend, Pie, PieChart, ResponsiveContainer } from 'recharts' -import type { DateRange } from "react-day-picker" import type { DailyStats } from '../types/stats' -import { - Legend, - ResponsiveContainer, - PieChart, Pie, Cell, -} from 'recharts' - -export function AnlyticAcceptance ({ - dailyStats, - dateRange, +export function AnlyticAcceptance({ + dailyStats, + dateRange }: { - dailyStats: DailyStats[] | undefined; - dateRange: DateRange; + dailyStats: DailyStats[] | undefined + dateRange: DateRange }) { - const from = dateRange.from || new Date() - const to = dateRange.to || from + const from = dateRange.from || new Date() + const to = dateRange.to || from - let totalAccpet = 0 - let totalCompletions = 0 - dailyStats?.forEach(stats => { - totalCompletions += stats.completions - totalAccpet += stats.selects - }) - const totalPending = totalCompletions - totalAccpet + let totalAccpet = 0 + let totalCompletions = 0 + dailyStats?.forEach(stats => { + totalCompletions += stats.completions + totalAccpet += stats.selects + }) + const totalPending = totalCompletions - totalAccpet - const data = [ - { name: 'Accept', value: totalAccpet}, - { name: 'Pending', value: totalPending}, - ]; + const data = [ + { name: 'Accept', value: totalAccpet }, + { name: 'Pending', value: totalPending } + ] - const COLORS = ['#8884d8', '#b9b7e2']; + const COLORS = ['#8884d8', '#b9b7e2'] - const RADIAN = Math.PI / 180; - const renderCustomizedLabel = ({ - cx, - cy, - midAngle, - innerRadius, - outerRadius, - percent, - name - }: { - cx: number; - cy: number; - midAngle: number; - innerRadius: number; - outerRadius: number; - percent: number; - name: string; - }) => { - const radius = innerRadius + (outerRadius - innerRadius) * 0.5; - const x = cx + radius * Math.cos(-midAngle * RADIAN); - const y = cy + radius * Math.sin(-midAngle * RADIAN); + const RADIAN = Math.PI / 180 + const renderCustomizedLabel = ({ + cx, + cy, + midAngle, + innerRadius, + outerRadius, + percent, + name + }: { + cx: number + cy: number + midAngle: number + innerRadius: number + outerRadius: number + percent: number + name: string + }) => { + const radius = innerRadius + (outerRadius - innerRadius) * 0.5 + const x = cx + radius * Math.cos(-midAngle * RADIAN) + const y = cy + radius * Math.sin(-midAngle * RADIAN) - if (name.toLocaleLowerCase() === 'accept') { - return ( - <text - x={x} - y={y} - fill="white" - textAnchor={x > cx ? 'start' : 'end'} - dominantBaseline="central" - fontSize={12}> - {`${(percent * 100).toFixed(0)}%`} - </text> - ); - } - return - }; + if (name.toLocaleLowerCase() === 'accept') { + return ( + <text + x={x} + y={y} + fill="white" + textAnchor={x > cx ? 'start' : 'end'} + dominantBaseline="central" + fontSize={12} + > + {`${(percent * 100).toFixed(0)}%`} + </text> + ) + } + return + } - return ( - <div className="rounded-lg border bg-primary-foreground/30 p-4"> - <h1 className="text-xl font-bold">Acceptance</h1> - <p className="mt-0.5 text-xs text-muted-foreground"> - {moment(from).format('D MMM, YYYY')} - {moment(to).format('D MMM, YYYY')} - </p> - <ResponsiveContainer width="100%" height={210}> - <PieChart width={700} height={400}> - <Pie - data={data} - cx="50%" - cy="50%" - labelLine={false} - label={renderCustomizedLabel} - outerRadius={80} - fill="#8884d8" - dataKey="value" - > - {data.map((entry, index) => ( - <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} /> - ))} - </Pie> - {totalCompletions !== 0 && <Legend wrapperStyle={{ fontSize: '12px' }} />} - </PieChart> - </ResponsiveContainer> - </div> - ) -} \ No newline at end of file + return ( + <div className="rounded-lg border bg-primary-foreground/30 p-4"> + <h1 className="text-xl font-bold">Acceptance</h1> + <p className="mt-0.5 text-xs text-muted-foreground"> + {moment(from).format('D MMM, YYYY')} -{' '} + {moment(to).format('D MMM, YYYY')} + </p> + <ResponsiveContainer width="100%" height={210}> + <PieChart width={700} height={400}> + <Pie + data={data} + cx="50%" + cy="50%" + labelLine={false} + label={renderCustomizedLabel} + outerRadius={80} + fill="#8884d8" + dataKey="value" + > + {data.map((entry, index) => ( + <Cell + key={`cell-${index}`} + fill={COLORS[index % COLORS.length]} + /> + ))} + </Pie> + {totalCompletions !== 0 && ( + <Legend wrapperStyle={{ fontSize: '12px' }} /> + )} + </PieChart> + </ResponsiveContainer> + </div> + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/analytics/components/analyticDailyCompletion.tsx b/ee/tabby-ui/app/(dashboard)/analytics/components/analyticDailyCompletion.tsx index 81d172dddc4f..6490049c0b5b 100644 --- a/ee/tabby-ui/app/(dashboard)/analytics/components/analyticDailyCompletion.tsx +++ b/ee/tabby-ui/app/(dashboard)/analytics/components/analyticDailyCompletion.tsx @@ -1,94 +1,99 @@ import { eachDayOfInterval } from 'date-fns' import moment from 'moment' - +import type { DateRange } from 'react-day-picker' import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, - YAxis, + YAxis } from 'recharts' + import { Card, CardContent } from '@/components/ui/card' -import type { DateRange } from "react-day-picker" import type { DailyStats } from '../types/stats' -function BarTooltip ({ active, payload, label }: { - active?: boolean; - label?: string - payload?: { - name: string; - value: number; - }[] +function BarTooltip({ + active, + payload, + label +}: { + active?: boolean + label?: string + payload?: { + name: string + value: number + }[] }) { - if (active && payload && payload.length) { - const completion = payload[0].value - if (!completion) return null; - return ( - <Card> - <CardContent className="px-4 py-2 text-sm"> - <p>Completions: <b>{completion}</b></p> - <p className="text-muted-foreground">{label}</p> - </CardContent> - </Card> - ); - } + if (active && payload && payload.length) { + const completion = payload[0].value + if (!completion) return null + return ( + <Card> + <CardContent className="px-4 py-2 text-sm"> + <p> + Completions: <b>{completion}</b> + </p> + <p className="text-muted-foreground">{label}</p> + </CardContent> + </Card> + ) + } - return null; -}; + return null +} -export function AnalyticDailyCompletion ({ - dailyStats, - dateRange, +export function AnalyticDailyCompletion({ + dailyStats, + dateRange }: { - dailyStats: DailyStats[] | undefined; - dateRange: DateRange; + dailyStats: DailyStats[] | undefined + dateRange: DateRange }) { - const from = dateRange.from || new Date() - const to = dateRange.to || from + const from = dateRange.from || new Date() + const to = dateRange.to || from - const dailyCompletionMap: Record<string, number> = dailyStats?.reduce((acc, cur) => { - const date = moment(cur.start).format('YYYY-MM-DD') - return { ...acc, [date]: cur.completions } - }, {}) || {} + const dailyCompletionMap: Record<string, number> = + dailyStats?.reduce((acc, cur) => { + const date = moment(cur.start).format('YYYY-MM-DD') + return { ...acc, [date]: cur.completions } + }, {}) || {} - const daysBetweenRange = eachDayOfInterval({ - start: from, - end: to - }); + const daysBetweenRange = eachDayOfInterval({ + start: from, + end: to + }) - const chartData = daysBetweenRange.map(date => { - const completionKey = moment(date).format('YYYY-MM-DD') - const value = dailyCompletionMap[completionKey] || 0 - return { - name: moment(date).format('D MMM'), - value - } - }) - return ( - <div className="rounded-lg border bg-primary-foreground/30 p-4"> - <h1 className="mb-5 text-xl font-bold">Completions</h1> - <ResponsiveContainer width="100%" height={350}> - <BarChart - width={500} - height={300} - data={chartData} - margin={{ - top: 5, - right: 20, - left: 20, - bottom: 5 - }} - > - <Bar dataKey="value" fill="#8884d8" /> - <XAxis dataKey="name" fontSize={12} /> - <YAxis fontSize={12} /> - <Tooltip - cursor={{ fill: 'transparent' }} - content={<BarTooltip />} /> - </BarChart> - </ResponsiveContainer> - </div> - ) -} \ No newline at end of file + const chartData = daysBetweenRange.map(date => { + const completionKey = moment(date).format('YYYY-MM-DD') + const value = dailyCompletionMap[completionKey] || 0 + return { + name: moment(date).format('D MMM'), + value + } + }) + return ( + <div className="rounded-lg border bg-primary-foreground/30 p-4"> + <h1 className="mb-5 text-xl font-bold">Completions</h1> + <ResponsiveContainer width="100%" height={350}> + <BarChart + width={500} + height={300} + data={chartData} + margin={{ + top: 5, + right: 20, + left: 20, + bottom: 5 + }} + > + <Bar dataKey="value" fill="#8884d8" /> + <XAxis dataKey="name" fontSize={12} /> + <YAxis fontSize={12} /> + <Tooltip cursor={{ fill: 'transparent' }} content={<BarTooltip />} /> + </BarChart> + </ResponsiveContainer> + </div> + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/analytics/components/analyticYearlyCompletion.tsx b/ee/tabby-ui/app/(dashboard)/analytics/components/analyticYearlyCompletion.tsx index bad3fcf21529..bd217f75ea1f 100644 --- a/ee/tabby-ui/app/(dashboard)/analytics/components/analyticYearlyCompletion.tsx +++ b/ee/tabby-ui/app/(dashboard)/analytics/components/analyticYearlyCompletion.tsx @@ -1,39 +1,45 @@ -import moment from "moment"; +import moment from 'moment' import ActivityCalendar from '@/components/activity-calendar' import type { DailyStats } from '../types/stats' -export function AnalyticYearlyCompletion ({ - yearlyStats, +export function AnalyticYearlyCompletion({ + yearlyStats }: { - yearlyStats: DailyStats[] | undefined; + 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 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() + 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 p-4"> <h1 className="text-xl font-bold">Activity</h1> - <p className="mt-0.5 text-xs text-muted-foreground">{lastYearCompletions} completions in the last year</p> + <p className="mt-0.5 text-xs text-muted-foreground"> + {lastYearCompletions} completions in the last year + </p> <div className="mt-5 flex flex-1 items-center justify-center xl:mt-0"> <ActivityCalendar data={data} /> </div> </div> - ); -} \ No newline at end of file + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/analytics/page.tsx b/ee/tabby-ui/app/(dashboard)/analytics/page.tsx index 93be631a3150..16dd86eb4e97 100644 --- a/ee/tabby-ui/app/(dashboard)/analytics/page.tsx +++ b/ee/tabby-ui/app/(dashboard)/analytics/page.tsx @@ -8,4 +8,4 @@ export const metadata: Metadata = { export default function Page() { return <Analytic /> -} \ No newline at end of file +} diff --git a/ee/tabby-ui/app/(dashboard)/analytics/types/stats.ts b/ee/tabby-ui/app/(dashboard)/analytics/types/stats.ts index 3205afa5db11..0998078bb19f 100644 --- a/ee/tabby-ui/app/(dashboard)/analytics/types/stats.ts +++ b/ee/tabby-ui/app/(dashboard)/analytics/types/stats.ts @@ -1,6 +1,6 @@ export type DailyStats = { - start: Date; - end: Date; - completions: number; - selects: number; -} \ No newline at end of file + start: Date + end: Date + completions: number + selects: number +} diff --git a/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx b/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx index e2d6fd34b3d2..ef2c9febebe2 100644 --- a/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx +++ b/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx @@ -16,14 +16,14 @@ import { CollapsibleTrigger } from '@/components/ui/collapsible' import { + IconBarChart, IconChevronRight, IconGear, IconHome, IconLightingBolt, IconNetwork, IconScrollText, - IconUser, - IconBarChart + IconUser } from '@/components/ui/icons' export interface SidebarProps { diff --git a/ee/tabby-ui/components/activity-calendar.tsx b/ee/tabby-ui/components/activity-calendar.tsx index 9d09e20fe6e0..ab01762a29d6 100644 --- a/ee/tabby-ui/components/activity-calendar.tsx +++ b/ee/tabby-ui/components/activity-calendar.tsx @@ -1,33 +1,28 @@ 'use client' -import dynamic from 'next/dynamic'; +import dynamic from 'next/dynamic' +import { useWindowSize } from '@uidotdev/usehooks' import { useTheme } from 'next-themes' -import { useWindowSize } from "@uidotdev/usehooks"; // withou using dynamic, we got error "Error: calcTextDimensions() requires browser APIs" const ReactActivityCalendar = dynamic(() => import('react-activity-calendar'), { - ssr: false, -}); + ssr: false +}) -export default function ActivityCalendar ({ +export default function ActivityCalendar({ data }: { data: { - date: string; - count: number; - level: number; + date: string + count: number + level: number }[] }) { const { theme } = useTheme() const size = useWindowSize() const width = size.width || 0 - const blockSize = width >= 1600 - ? 13 - : width >= 1400 - ? 10 - : width >= 1000 - ? 8 - : 5 + const blockSize = + width >= 1600 ? 13 : width >= 1400 ? 10 : width >= 1000 ? 8 : 5 return ( <ReactActivityCalendar @@ -35,10 +30,11 @@ export default function ActivityCalendar ({ colorScheme={theme === 'dark' ? 'dark' : 'light'} theme={{ light: ['#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39'], - dark: ['rgb(45, 51, 59)', '#0e4429', '#006d32', '#26a641', '#39d353'], + dark: ['rgb(45, 51, 59)', '#0e4429', '#006d32', '#26a641', '#39d353'] }} blockSize={blockSize} hideTotalCount - showWeekdayLabels /> - ); -} \ No newline at end of file + showWeekdayLabels + /> + ) +} diff --git a/ee/tabby-ui/components/date-range-picker.tsx b/ee/tabby-ui/components/date-range-picker.tsx index b81e588a1f30..686237c7f15d 100644 --- a/ee/tabby-ui/components/date-range-picker.tsx +++ b/ee/tabby-ui/components/date-range-picker.tsx @@ -1,18 +1,18 @@ -"use client" +'use client' -import * as React from "react" -import { CalendarIcon } from "@radix-ui/react-icons" -import { addDays, format } from "date-fns" -import { DateRange } from "react-day-picker" +import * as React from 'react' +import { CalendarIcon } from '@radix-ui/react-icons' +import { addDays, format } from 'date-fns' +import { DateRange } from 'react-day-picker' -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { Calendar } from "@/components/ui/calendar" +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { Calendar } from '@/components/ui/calendar' import { Popover, PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" + PopoverTrigger +} from '@/components/ui/popover' export default function DatePickerWithRange({ dateRange, @@ -22,14 +22,14 @@ export default function DatePickerWithRange({ onOpenChange }: React.HTMLAttributes<HTMLDivElement> & { dateRange?: DateRange - buttonClassName?: string; - contentAlign?: "start" | "end" | "center"; - onOpenChange?: (isOpen: boolean, date: DateRange | undefined) => void, - onSelectDateRange?: (date: DateRange | undefined) => void, + buttonClassName?: string + contentAlign?: 'start' | 'end' | 'center' + onOpenChange?: (isOpen: boolean, date: DateRange | undefined) => void + onSelectDateRange?: (date: DateRange | undefined) => void }) { const [date, setDate] = React.useState<DateRange | undefined>({ from: dateRange?.from || new Date(2022, 0, 20), - to: dateRange?.to || addDays(new Date(2022, 0, 20), 20), + to: dateRange?.to || addDays(new Date(2022, 0, 20), 20) }) const toggleOpen = (isOpen: boolean) => { @@ -37,15 +37,15 @@ export default function DatePickerWithRange({ } return ( - <div className={cn("grid gap-2", className)}> + <div className={cn('grid gap-2', className)}> <Popover onOpenChange={toggleOpen}> <PopoverTrigger asChild> <Button id="date" - variant={"outline"} + variant={'outline'} className={cn( - "w-[300px] justify-start text-left font-normal ", - !date && "text-muted-foreground", + 'w-[300px] justify-start text-left font-normal ', + !date && 'text-muted-foreground', buttonClassName )} > @@ -53,11 +53,11 @@ export default function DatePickerWithRange({ {date?.from ? ( date.to ? ( <> - {format(date.from, "LLL dd, y")} -{" "} - {format(date.to, "LLL dd, y")} + {format(date.from, 'LLL dd, y')} -{' '} + {format(date.to, 'LLL dd, y')} </> ) : ( - format(date.from, "LLL dd, y") + format(date.from, 'LLL dd, y') ) ) : ( <span>Pick a date</span> @@ -77,4 +77,4 @@ export default function DatePickerWithRange({ </Popover> </div> ) -} \ No newline at end of file +} diff --git a/ee/tabby-ui/components/ui/calendar.tsx b/ee/tabby-ui/components/ui/calendar.tsx index 43868fea1cf7..974e302d9096 100644 --- a/ee/tabby-ui/components/ui/calendar.tsx +++ b/ee/tabby-ui/components/ui/calendar.tsx @@ -1,11 +1,11 @@ -"use client" +'use client' -import * as React from "react" -import { ChevronLeft, ChevronRight } from "lucide-react" -import { DayPicker } from "react-day-picker" +import * as React from 'react' +import { ChevronLeft, ChevronRight } from 'lucide-react' +import { DayPicker } from 'react-day-picker' -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" +import { cn } from '@/lib/utils' +import { buttonVariants } from '@/components/ui/button' export type CalendarProps = React.ComponentProps<typeof DayPicker> @@ -18,49 +18,49 @@ function Calendar({ return ( <DayPicker showOutsideDays={showOutsideDays} - className={cn("p-3", className)} + className={cn('p-3', className)} classNames={{ - months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", - month: "space-y-4", - caption: "flex justify-center pt-1 relative items-center", - caption_label: "text-sm font-medium", - nav: "space-x-1 flex items-center", + months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0', + month: 'space-y-4', + caption: 'flex justify-center pt-1 relative items-center', + caption_label: 'text-sm font-medium', + nav: 'space-x-1 flex items-center', nav_button: cn( - buttonVariants({ variant: "outline" }), - "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100" + buttonVariants({ variant: 'outline' }), + 'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100' ), - nav_button_previous: "absolute left-1", - nav_button_next: "absolute right-1", - table: "w-full border-collapse space-y-1", - head_row: "flex", + nav_button_previous: 'absolute left-1', + nav_button_next: 'absolute right-1', + table: 'w-full border-collapse space-y-1', + head_row: 'flex', head_cell: - "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]", - row: "flex w-full mt-2", - cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20", + 'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]', + row: 'flex w-full mt-2', + cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20', day: cn( - buttonVariants({ variant: "ghost" }), - "h-9 w-9 p-0 font-normal aria-selected:opacity-100" + buttonVariants({ variant: 'ghost' }), + 'h-9 w-9 p-0 font-normal aria-selected:opacity-100' ), - day_range_end: "day-range-end", + day_range_end: 'day-range-end', day_selected: - "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", - day_today: "bg-accent text-accent-foreground", + 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground', + day_today: 'bg-accent text-accent-foreground', day_outside: - "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30", - day_disabled: "text-muted-foreground opacity-50", + 'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30', + day_disabled: 'text-muted-foreground opacity-50', day_range_middle: - "aria-selected:bg-accent aria-selected:text-accent-foreground", - day_hidden: "invisible", - ...classNames, + 'aria-selected:bg-accent aria-selected:text-accent-foreground', + day_hidden: 'invisible', + ...classNames }} components={{ IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />, - IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />, + IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" /> }} {...props} /> ) } -Calendar.displayName = "Calendar" +Calendar.displayName = 'Calendar' -export { Calendar } \ No newline at end of file +export { Calendar } diff --git a/ee/tabby-ui/lib/hooks/use-all-members.tsx b/ee/tabby-ui/lib/hooks/use-all-members.tsx index 336c8d45e01d..b179ebe37797 100644 --- a/ee/tabby-ui/lib/hooks/use-all-members.tsx +++ b/ee/tabby-ui/lib/hooks/use-all-members.tsx @@ -1,13 +1,13 @@ -import { useState, useEffect } from 'react' +import { useEffect, useState } from 'react' import { useQuery } from 'urql' +import { DEFAULT_PAGE_SIZE } from '@/lib/constants' import { QueryVariables } from '@/lib/tabby/gql' import { listUsers } from '@/lib/tabby/query' -import { DEFAULT_PAGE_SIZE } from '@/lib/constants' type Member = { - id: string; - email: string; + id: string + email: string } export function useAllMembers() { @@ -30,14 +30,14 @@ export function useAllMembers() { id: edge.node.id, email: edge.node.email })) - const cursor = data?.users.pageInfo.endCursor || "" + const cursor = data?.users.pageInfo.endCursor || '' const hasMore = data?.users.pageInfo.hasNextPage const currentList = [...list] setList(currentList.concat(members)) if (hasMore) { setQueryVariables({ - first: DEFAULT_PAGE_SIZE, + first: DEFAULT_PAGE_SIZE, after: cursor }) } else { diff --git a/ee/tabby-ui/lib/tabby/query.ts b/ee/tabby-ui/lib/tabby/query.ts index 895094464651..83be636224fd 100644 --- a/ee/tabby-ui/lib/tabby/query.ts +++ b/ee/tabby-ui/lib/tabby/query.ts @@ -128,9 +128,7 @@ export const listUsers = graphql(/* GraphQL */ ` `) export const queryDailyStatsInPastYear = graphql(/* GraphQL */ ` - query DailyStatsInPastYear( - $users: [ID!] - ) { + query DailyStatsInPastYear($users: [ID!]) { dailyStatsInPastYear(users: $users) { start end @@ -142,9 +140,9 @@ export const queryDailyStatsInPastYear = graphql(/* GraphQL */ ` export const queryDailyStats = graphql(/* GraphQL */ ` query DailyStats( - $start: DateTimeUtc!, - $end: DateTimeUtc!, - $users: [ID!], + $start: DateTimeUtc! + $end: DateTimeUtc! + $users: [ID!] $languages: [Language!] ) { dailyStats(start: $start, end: $end, users: $users, languages: $languages) { From e86ad97b16b079f9ece85b4958a6262df5e0b065 Mon Sep 17 00:00:00 2001 From: Meng Zhang <meng@tabbyml.com> Date: Sun, 7 Apr 2024 00:04:48 -0700 Subject: [PATCH 04/16] improve report page (#1776) --- .../app/(dashboard)/components/sidebar.tsx | 4 ++-- .../components/analytic.tsx | 24 +++++++++++-------- .../components/analyticAccptance.tsx | 0 .../components/analyticDailyCompletion.tsx | 0 .../components/analyticYearlyCompletion.tsx | 2 +- .../{analytics => reports}/page.tsx | 0 .../{analytics => reports}/types/stats.ts | 0 7 files changed, 17 insertions(+), 13 deletions(-) rename ee/tabby-ui/app/(dashboard)/{analytics => reports}/components/analytic.tsx (95%) rename ee/tabby-ui/app/(dashboard)/{analytics => reports}/components/analyticAccptance.tsx (100%) rename ee/tabby-ui/app/(dashboard)/{analytics => reports}/components/analyticDailyCompletion.tsx (100%) rename ee/tabby-ui/app/(dashboard)/{analytics => reports}/components/analyticYearlyCompletion.tsx (95%) rename ee/tabby-ui/app/(dashboard)/{analytics => reports}/page.tsx (100%) rename ee/tabby-ui/app/(dashboard)/{analytics => reports}/types/stats.ts (100%) diff --git a/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx b/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx index ef2c9febebe2..e049b58fc8a0 100644 --- a/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx +++ b/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx @@ -71,9 +71,9 @@ export default function Sidebar({ children, className }: SidebarProps) { <IconScrollText /> Jobs </SidebarButton> - <SidebarButton href="/analytics"> + <SidebarButton href="/reports"> <IconBarChart /> - Analytics + Reports </SidebarButton> <SidebarCollapsible title={ diff --git a/ee/tabby-ui/app/(dashboard)/analytics/components/analytic.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx similarity index 95% rename from ee/tabby-ui/app/(dashboard)/analytics/components/analytic.tsx rename to ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx index 036989e0e5fa..f54b3f94b0bc 100644 --- a/ee/tabby-ui/app/(dashboard)/analytics/components/analytic.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx @@ -40,25 +40,29 @@ function AnalyticSummary({ <div className="flex items-center justify-center space-x-4 xl:justify-start"> <div className="space-y-0.5 rounded-lg border bg-primary-foreground/30 p-4 lg:min-w-[250px]"> <p className="text-xs text-muted-foreground lg:text-sm"> - Total completions + Accept Rate </p> <p className="font-bold lg:text-3xl"> - {numeral(totalCompletions).format('0,0')} + TBD </p> </div> <div className="space-y-0.5 rounded-lg border bg-primary-foreground/30 p-4 lg:min-w-[250px]"> <p className="text-xs text-muted-foreground lg:text-sm"> - Minutes saved / completion + Total completions + </p> + <p className="font-bold lg:text-3xl"> + {numeral(totalCompletions).format('0,0')} </p> - <p className="font-bold lg:text-3xl">2</p> </div> <div className="space-y-0.5 rounded-lg border bg-primary-foreground/30 p-4 lg:min-w-[250px]"> <p className="text-xs text-muted-foreground lg:text-sm"> - Hours saved in total + Total acceptances + </p> + <p className="font-bold lg:text-3xl"> + TBD </p> - <p className="font-bold lg:text-3xl">100</p> </div> </div> ) @@ -126,10 +130,10 @@ export function Analytic() { <div className="flex flex-col items-center justify-between gap-y-3 xl:flex-row xl:gap-y-0"> <div className="flex flex-col justify-center xl:justify-start"> <h1 className="mb-1.5 scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl xl:text-left"> - Analytics + Reports </h1> <p className="text-muted-foreground"> - Overview of code completion usage + Statistics around Tabby IDE / Extensions </p> </div> @@ -211,14 +215,14 @@ export function Analytic() { </LoadingWrapper> <div className="flex flex-col gap-y-6 xl:flex-row xl:gap-x-6 xl:gap-y-0"> - <div className="flex-1"> + {false && <div className="flex-1"> <LoadingWrapper loading={fetchingDailyState} fallback={<Skeleton className="h-64 w-full" />} > <AnlyticAcceptance dailyStats={dailyStats} dateRange={dateRange} /> </LoadingWrapper> - </div> + </div>} <div style={{ flex: 3 }}> <LoadingWrapper loading={fetchingDailyState || fetchingYearlyStats} diff --git a/ee/tabby-ui/app/(dashboard)/analytics/components/analyticAccptance.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/analyticAccptance.tsx similarity index 100% rename from ee/tabby-ui/app/(dashboard)/analytics/components/analyticAccptance.tsx rename to ee/tabby-ui/app/(dashboard)/reports/components/analyticAccptance.tsx diff --git a/ee/tabby-ui/app/(dashboard)/analytics/components/analyticDailyCompletion.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/analyticDailyCompletion.tsx similarity index 100% rename from ee/tabby-ui/app/(dashboard)/analytics/components/analyticDailyCompletion.tsx rename to ee/tabby-ui/app/(dashboard)/reports/components/analyticDailyCompletion.tsx diff --git a/ee/tabby-ui/app/(dashboard)/analytics/components/analyticYearlyCompletion.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/analyticYearlyCompletion.tsx similarity index 95% rename from ee/tabby-ui/app/(dashboard)/analytics/components/analyticYearlyCompletion.tsx rename to ee/tabby-ui/app/(dashboard)/reports/components/analyticYearlyCompletion.tsx index bd217f75ea1f..75b2b8182244 100644 --- a/ee/tabby-ui/app/(dashboard)/analytics/components/analyticYearlyCompletion.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/analyticYearlyCompletion.tsx @@ -35,7 +35,7 @@ export function AnalyticYearlyCompletion({ <div className="flex h-full flex-col rounded-lg border bg-primary-foreground/30 p-4"> <h1 className="text-xl font-bold">Activity</h1> <p className="mt-0.5 text-xs text-muted-foreground"> - {lastYearCompletions} completions in the last year + {lastYearCompletions} activities in the last year </p> <div className="mt-5 flex flex-1 items-center justify-center xl:mt-0"> <ActivityCalendar data={data} /> diff --git a/ee/tabby-ui/app/(dashboard)/analytics/page.tsx b/ee/tabby-ui/app/(dashboard)/reports/page.tsx similarity index 100% rename from ee/tabby-ui/app/(dashboard)/analytics/page.tsx rename to ee/tabby-ui/app/(dashboard)/reports/page.tsx diff --git a/ee/tabby-ui/app/(dashboard)/analytics/types/stats.ts b/ee/tabby-ui/app/(dashboard)/reports/types/stats.ts similarity index 100% rename from ee/tabby-ui/app/(dashboard)/analytics/types/stats.ts rename to ee/tabby-ui/app/(dashboard)/reports/types/stats.ts From 85ebc2b72d6ffb2e0e2ee5691a6afe0151d6d6ce Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 7 Apr 2024 07:05:54 +0000 Subject: [PATCH 05/16] [autofix.ci] apply automated fixes --- .../reports/components/analytic.tsx | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx index f54b3f94b0bc..a070ea589a3b 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx @@ -39,12 +39,8 @@ function AnalyticSummary({ return ( <div className="flex items-center justify-center space-x-4 xl:justify-start"> <div className="space-y-0.5 rounded-lg border bg-primary-foreground/30 p-4 lg:min-w-[250px]"> - <p className="text-xs text-muted-foreground lg:text-sm"> - Accept Rate - </p> - <p className="font-bold lg:text-3xl"> - TBD - </p> + <p className="text-xs text-muted-foreground lg:text-sm">Accept Rate</p> + <p className="font-bold lg:text-3xl">TBD</p> </div> <div className="space-y-0.5 rounded-lg border bg-primary-foreground/30 p-4 lg:min-w-[250px]"> @@ -60,9 +56,7 @@ function AnalyticSummary({ <p className="text-xs text-muted-foreground lg:text-sm"> Total acceptances </p> - <p className="font-bold lg:text-3xl"> - TBD - </p> + <p className="font-bold lg:text-3xl">TBD</p> </div> </div> ) @@ -215,14 +209,19 @@ export function Analytic() { </LoadingWrapper> <div className="flex flex-col gap-y-6 xl:flex-row xl:gap-x-6 xl:gap-y-0"> - {false && <div className="flex-1"> - <LoadingWrapper - loading={fetchingDailyState} - fallback={<Skeleton className="h-64 w-full" />} - > - <AnlyticAcceptance dailyStats={dailyStats} dateRange={dateRange} /> - </LoadingWrapper> - </div>} + {false && ( + <div className="flex-1"> + <LoadingWrapper + loading={fetchingDailyState} + fallback={<Skeleton className="h-64 w-full" />} + > + <AnlyticAcceptance + dailyStats={dailyStats} + dateRange={dateRange} + /> + </LoadingWrapper> + </div> + )} <div style={{ flex: 3 }}> <LoadingWrapper loading={fetchingDailyState || fetchingYearlyStats} From a7325cb6b1e01d979c35ab1847d39942622f0977 Mon Sep 17 00:00:00 2001 From: wwayne <wayne.wang0821@gmail.com> Date: Sun, 7 Apr 2024 16:19:53 +0800 Subject: [PATCH 06/16] code detail polishment --- .../reports/components/analytic.tsx | 4 +-- .../reports}/hooks/use-all-members.tsx | 0 ee/tabby-ui/app/(dashboard)/reports/query.ts | 28 +++++++++++++++++++ .../settings/team/components/user-table.tsx | 25 +---------------- ee/tabby-ui/components/ui/icons.tsx | 5 ++++ ee/tabby-ui/lib/tabby/query.ts | 27 ------------------ 6 files changed, 36 insertions(+), 53 deletions(-) rename ee/tabby-ui/{lib => app/(dashboard)/reports}/hooks/use-all-members.tsx (100%) create mode 100644 ee/tabby-ui/app/(dashboard)/reports/query.ts diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx index a070ea589a3b..7f499e9f4a82 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx @@ -8,8 +8,8 @@ import { DateRange } from 'react-day-picker' import { useQuery } from 'urql' import { Language } from '@/lib/gql/generates/graphql' -import { useAllMembers } from '@/lib/hooks/use-all-members' -import { queryDailyStats, queryDailyStatsInPastYear } from '@/lib/tabby/query' +import { useAllMembers } from '@/app/(dashboard)/reports/hooks/use-all-members' +import { queryDailyStats, queryDailyStatsInPastYear } from '../query' import { Select, SelectContent, diff --git a/ee/tabby-ui/lib/hooks/use-all-members.tsx b/ee/tabby-ui/app/(dashboard)/reports/hooks/use-all-members.tsx similarity index 100% rename from ee/tabby-ui/lib/hooks/use-all-members.tsx rename to ee/tabby-ui/app/(dashboard)/reports/hooks/use-all-members.tsx diff --git a/ee/tabby-ui/app/(dashboard)/reports/query.ts b/ee/tabby-ui/app/(dashboard)/reports/query.ts new file mode 100644 index 000000000000..936164f4f748 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/reports/query.ts @@ -0,0 +1,28 @@ +import { graphql } from '@/lib/gql/generates' + +export const queryDailyStatsInPastYear = graphql(/* GraphQL */ ` + query DailyStatsInPastYear($users: [ID!]) { + dailyStatsInPastYear(users: $users) { + start + end + completions + selects + } + } +`) + +export const queryDailyStats = graphql(/* GraphQL */ ` + query DailyStats( + $start: DateTimeUtc! + $end: DateTimeUtc! + $users: [ID!] + $languages: [Language!] + ) { + dailyStats(start: $start, end: $end, users: $users, languages: $languages) { + start + end + completions + selects + } + } +`) diff --git a/ee/tabby-ui/app/(dashboard)/settings/team/components/user-table.tsx b/ee/tabby-ui/app/(dashboard)/settings/team/components/user-table.tsx index a4c230f18510..3a30b29159e2 100644 --- a/ee/tabby-ui/app/(dashboard)/settings/team/components/user-table.tsx +++ b/ee/tabby-ui/app/(dashboard)/settings/team/components/user-table.tsx @@ -10,6 +10,7 @@ import { graphql } from '@/lib/gql/generates' import type { ListUsersQuery } from '@/lib/gql/generates/graphql' import { useMe } from '@/lib/hooks/use-me' import { QueryVariables, useMutation } from '@/lib/tabby/gql' +import { listUsers } from '@/lib/tabby/query' import type { ArrayElementType } from '@/lib/types' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' @@ -39,30 +40,6 @@ import LoadingWrapper from '@/components/loading-wrapper' import { UpdateUserRoleDialog } from './user-role-dialog' -const listUsers = graphql(/* GraphQL */ ` - query ListUsers($after: String, $before: String, $first: Int, $last: Int) { - users(after: $after, before: $before, first: $first, last: $last) { - edges { - node { - id - email - isAdmin - isOwner - createdAt - active - } - cursor - } - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - } - } -`) - const updateUserActiveMutation = graphql(/* GraphQL */ ` mutation UpdateUserActive($id: ID!, $active: Boolean!) { updateUserActive(id: $id, active: $active) diff --git a/ee/tabby-ui/components/ui/icons.tsx b/ee/tabby-ui/components/ui/icons.tsx index eb61e8eca493..ab7caeff5b09 100644 --- a/ee/tabby-ui/components/ui/icons.tsx +++ b/ee/tabby-ui/components/ui/icons.tsx @@ -184,6 +184,8 @@ function IconArrowRight({ className, ...props }: React.ComponentProps<'svg'>) { ) } +// FIXME: Remove to use ChevronRight from lucide-react +// currently ui/calendar is using ChevronRight as the default option. function IconChevronRight({ className, ...props @@ -204,6 +206,9 @@ function IconChevronRight({ </svg> ) } + +// FIXME: Remove to use ChevronLeft from lucide-react +// currently ui/calendar is using ChevronLeft as the default option. function IconChevronLeft({ className, ...props }: React.ComponentProps<'svg'>) { return ( <svg diff --git a/ee/tabby-ui/lib/tabby/query.ts b/ee/tabby-ui/lib/tabby/query.ts index 83be636224fd..464e479b970f 100644 --- a/ee/tabby-ui/lib/tabby/query.ts +++ b/ee/tabby-ui/lib/tabby/query.ts @@ -126,30 +126,3 @@ export const listUsers = graphql(/* GraphQL */ ` } } `) - -export const queryDailyStatsInPastYear = graphql(/* GraphQL */ ` - query DailyStatsInPastYear($users: [ID!]) { - dailyStatsInPastYear(users: $users) { - start - end - completions - selects - } - } -`) - -export const queryDailyStats = graphql(/* GraphQL */ ` - query DailyStats( - $start: DateTimeUtc! - $end: DateTimeUtc! - $users: [ID!] - $languages: [Language!] - ) { - dailyStats(start: $start, end: $end, users: $users, languages: $languages) { - start - end - completions - selects - } - } -`) From 254598b39548e4471a5632a009a81d6e86f03b0b Mon Sep 17 00:00:00 2001 From: wwayne <wayne.wang0821@gmail.com> Date: Sun, 7 Apr 2024 19:10:14 +0800 Subject: [PATCH 07/16] update ui --- .../reports/components/analytic.tsx | 227 ++++++++++-------- .../components/analyticDailyCompletion.tsx | 70 ++++-- .../reports/components/analyticStats.tsx | 12 + .../components/analyticYearlyCompletion.tsx | 11 +- ee/tabby-ui/components/ui/icons.tsx | 21 +- 5 files changed, 211 insertions(+), 130 deletions(-) create mode 100644 ee/tabby-ui/app/(dashboard)/reports/components/analyticStats.tsx diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx index 7f499e9f4a82..f15ac8bdd31f 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx @@ -8,8 +8,10 @@ import { DateRange } from 'react-day-picker' import { useQuery } from 'urql' import { Language } from '@/lib/gql/generates/graphql' -import { useAllMembers } from '@/app/(dashboard)/reports/hooks/use-all-members' +import { useAllMembers } from '../hooks/use-all-members' import { queryDailyStats, queryDailyStatsInPastYear } from '../query' + +import { IconActivity, IconCode, IconCheck } from '@/components/ui/icons' import { Select, SelectContent, @@ -18,15 +20,15 @@ import { SelectTrigger, SelectValue } from '@/components/ui/select' -import { Skeleton } from '@/components/ui/skeleton' import DatePickerWithRange from '@/components/date-range-picker' import LoadingWrapper from '@/components/loading-wrapper' +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card' -import type { DailyStats } from '../types/stats' -import { AnlyticAcceptance } from './analyticAccptance' import { AnalyticDailyCompletion } from './analyticDailyCompletion' import { AnalyticYearlyCompletion } from './analyticYearlyCompletion' +import type { DailyStats } from '../types/stats' + const INITIAL_DATE_RANGE = 14 const KEY_SELECT_ALL = 'all' @@ -37,27 +39,51 @@ function AnalyticSummary({ }) { const totalCompletions = sum(dailyStats?.map(stats => stats.completions)) return ( - <div className="flex items-center justify-center space-x-4 xl:justify-start"> - <div className="space-y-0.5 rounded-lg border bg-primary-foreground/30 p-4 lg:min-w-[250px]"> - <p className="text-xs text-muted-foreground lg:text-sm">Accept Rate</p> - <p className="font-bold lg:text-3xl">TBD</p> - </div> + <div className="flex w-full items-center justify-center space-x-6 xl:justify-start"> + <Card className="flex-1 bg-primary-foreground/30"> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium"> + Accept Rate + </CardTitle> + <IconActivity className="text-muted-foreground" /> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">TBD</div> + <p className="text-xs text-muted-foreground"> + +TBD from last week + </p> + </CardContent> + </Card> - <div className="space-y-0.5 rounded-lg border bg-primary-foreground/30 p-4 lg:min-w-[250px]"> - <p className="text-xs text-muted-foreground lg:text-sm"> - Total completions - </p> - <p className="font-bold lg:text-3xl"> - {numeral(totalCompletions).format('0,0')} - </p> - </div> + <Card className="flex-1 bg-primary-foreground/30"> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium"> + Total completions + </CardTitle> + <IconCode className="text-muted-foreground" /> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">{numeral(totalCompletions).format('0,0')}</div> + <p className="text-xs text-muted-foreground"> + +TBD from last week + </p> + </CardContent> + </Card> - <div className="space-y-0.5 rounded-lg border bg-primary-foreground/30 p-4 lg:min-w-[250px]"> - <p className="text-xs text-muted-foreground lg:text-sm"> - Total acceptances - </p> - <p className="font-bold lg:text-3xl">TBD</p> - </div> + <Card className="flex-1 bg-primary-foreground/30"> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium"> + Total acceptances + </CardTitle> + <IconCheck className="h-4 w-4 text-muted-foreground" /> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">TBD</div> + <p className="text-xs text-muted-foreground"> + +TBD from last week + </p> + </CardContent> + </Card> </div> ) } @@ -120,8 +146,8 @@ export function Analytic() { } return ( - <div className="flex flex-col gap-y-6"> - <div className="flex flex-col items-center justify-between gap-y-3 xl:flex-row xl:gap-y-0"> + <div className="mx-auto max-w-5xl"> + <div className="mb-3 flex flex-col items-center justify-between gap-y-3 xl:flex-row xl:gap-y-0"> <div className="flex flex-col justify-center xl:justify-start"> <h1 className="mb-1.5 scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl xl:text-left"> Reports @@ -131,84 +157,89 @@ export function Analytic() { </p> </div> - <div className="flex flex-col items-center gap-y-2 lg:flex-row lg:gap-y-0 lg:space-x-4"> - <Select - defaultValue={KEY_SELECT_ALL} - onValueChange={setSelectedMember} - > - <SelectTrigger className="w-[300px] lg:w-[180px]"> - <div className="flex w-full items-center truncate "> - <span className="mr-1.5 text-muted-foreground">Member:</span> - <div className="overflow-hidden text-ellipsis"> - <SelectValue /> - </div> - </div> - </SelectTrigger> - <SelectContent> - <SelectGroup> - <SelectItem value={KEY_SELECT_ALL}>All</SelectItem> - {members.map(member => ( - <SelectItem value={member.id} key={member.id}> - {member.email} - </SelectItem> - ))} - </SelectGroup> - </SelectContent> - </Select> - - <Select - defaultValue={KEY_SELECT_ALL} - onValueChange={(value: 'all' | Language) => - setSelectedLanguage(value) - } - > - <SelectTrigger className="w-[300px] lg:w-[180px]"> - <div className="flex w-full items-center truncate"> - <span className="mr-1.5 text-muted-foreground">Language:</span> - <div className="overflow-hidden text-ellipsis"> - <SelectValue /> - </div> + <Select + defaultValue={KEY_SELECT_ALL} + onValueChange={setSelectedMember} + > + <SelectTrigger className="w-[300px] lg:w-[150px]"> + <div className="flex w-full items-center truncate "> + <span className="mr-1.5 text-muted-foreground">Member:</span> + <div className="overflow-hidden text-ellipsis"> + <SelectValue /> </div> - </SelectTrigger> - <SelectContent> - <SelectGroup> - <SelectItem value={'all'}>All</SelectItem> - {Object.entries(Language).map(([key, value]) => ( - <SelectItem key={value} value={value}> - {key} - </SelectItem> - ))} - </SelectGroup> - </SelectContent> - </Select> - - <DatePickerWithRange - buttonClassName="h-full" - contentAlign="end" + </div> + </SelectTrigger> + <SelectContent align='end'> + <SelectGroup> + <SelectItem value={KEY_SELECT_ALL}>All</SelectItem> + {members.map(member => ( + <SelectItem value={member.id} key={member.id}> + {member.email} + </SelectItem> + ))} + </SelectGroup> + </SelectContent> + </Select> + </div> + + <LoadingWrapper> + <div className="mb-10 flex flex-col gap-y-5"> + <div className="flex items-center justify-between"> + <h1 className="text-xl font-semibold">Usage</h1> + + <div className="flex items-center gap-x-3"> + <Select + defaultValue={KEY_SELECT_ALL} + onValueChange={(value: 'all' | Language) => + setSelectedLanguage(value) + } + > + <SelectTrigger className="w-[300px] lg:w-[180px]"> + <div className="flex w-full items-center truncate"> + <span className="mr-1.5 text-muted-foreground">Language:</span> + <div className="overflow-hidden text-ellipsis"> + <SelectValue /> + </div> + </div> + </SelectTrigger> + <SelectContent> + <SelectGroup> + <SelectItem value={'all'}>All</SelectItem> + {Object.entries(Language).map(([key, value]) => ( + <SelectItem key={value} value={value}> + {key} + </SelectItem> + ))} + </SelectGroup> + </SelectContent> + </Select> + + <DatePickerWithRange + buttonClassName="h-full" + contentAlign="end" + dateRange={dateRange} + onOpenChange={onDateOpenChange} + /> + </div> + </div> + + <AnalyticSummary dailyStats={dailyStats} /> + + <AnalyticDailyCompletion + dailyStats={dailyStats} dateRange={dateRange} - onOpenChange={onDateOpenChange} /> </div> - </div> - - <LoadingWrapper - loading={fetchingDailyState} - fallback={<Skeleton className="h-24 w-1/2" />} - > - <AnalyticSummary dailyStats={dailyStats} /> </LoadingWrapper> - - <LoadingWrapper - loading={fetchingDailyState} - fallback={<Skeleton className="h-64 w-full" />} - > - <AnalyticDailyCompletion - dailyStats={dailyStats} - dateRange={dateRange} - /> + + <LoadingWrapper> + <div className="mb-10"> + <h1 className="mb-3 text-xl font-semibold">Activity</h1> + <AnalyticYearlyCompletion yearlyStats={yearlyStats} /> + </div> </LoadingWrapper> - <div className="flex flex-col gap-y-6 xl:flex-row xl:gap-x-6 xl:gap-y-0"> + {/* <div className="flex flex-col gap-y-6 xl:flex-row xl:gap-x-6 xl:gap-y-0"> {false && ( <div className="flex-1"> <LoadingWrapper @@ -227,10 +258,10 @@ export function Analytic() { loading={fetchingDailyState || fetchingYearlyStats} fallback={<Skeleton className="h-64 w-full" />} > - <AnalyticYearlyCompletion yearlyStats={yearlyStats} /> + </LoadingWrapper> </div> - </div> + </div> */} </div> ) } diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/analyticDailyCompletion.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/analyticDailyCompletion.tsx index 6490049c0b5b..a8c7dbc6c977 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/analyticDailyCompletion.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/analyticDailyCompletion.tsx @@ -1,6 +1,10 @@ +'use client' + import { eachDayOfInterval } from 'date-fns' import moment from 'moment' -import type { DateRange } from 'react-day-picker' +import { sum } from 'lodash-es' +import { useTheme } from 'next-themes' + import { Bar, BarChart, @@ -9,9 +13,9 @@ import { XAxis, YAxis } from 'recharts' - import { Card, CardContent } from '@/components/ui/card' +import type { DateRange } from 'react-day-picker' import type { DailyStats } from '../types/stats' function BarTooltip({ @@ -19,21 +23,29 @@ function BarTooltip({ payload, label }: { - active?: boolean - label?: string + active?: boolean; + label?: string; payload?: { - name: string - value: number + name: string; + payload: { + completion: number; + select: number; + pending: number; + }; + }[] }) { if (active && payload && payload.length) { - const completion = payload[0].value + const {completion, select} = payload[0].payload if (!completion) return null return ( <Card> - <CardContent className="px-4 py-2 text-sm"> - <p> - Completions: <b>{completion}</b> + <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> @@ -51,14 +63,19 @@ export function AnalyticDailyCompletion({ 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> = - dailyStats?.reduce((acc, cur) => { - const date = moment(cur.start).format('YYYY-MM-DD') - return { ...acc, [date]: cur.completions } - }, {}) || {} + 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, @@ -66,20 +83,22 @@ export function AnalyticDailyCompletion({ }) const chartData = daysBetweenRange.map(date => { - const completionKey = moment(date).format('YYYY-MM-DD') - const value = dailyCompletionMap[completionKey] || 0 + 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'), - value + completion, + select, + pending } }) return ( - <div className="rounded-lg border bg-primary-foreground/30 p-4"> - <h1 className="mb-5 text-xl font-bold">Completions</h1> - <ResponsiveContainer width="100%" height={350}> + <div className="rounded-lg border bg-primary-foreground/30 px-6 py-4"> + <h3 className="mb-5 text-sm font-medium tracking-tight">Completions</h3> + <ResponsiveContainer width="100%" height={300}> <BarChart - width={500} - height={300} data={chartData} margin={{ top: 5, @@ -88,9 +107,10 @@ export function AnalyticDailyCompletion({ bottom: 5 }} > - <Bar dataKey="value" fill="#8884d8" /> + <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} /> + <YAxis fontSize={12} width={20} /> <Tooltip cursor={{ fill: 'transparent' }} content={<BarTooltip />} /> </BarChart> </ResponsiveContainer> diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/analyticStats.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/analyticStats.tsx new file mode 100644 index 000000000000..afae66fff980 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/reports/components/analyticStats.tsx @@ -0,0 +1,12 @@ +import type { DateRange } from 'react-day-picker' +import type { DailyStats } from '../types/stats' + +export function AnalyticStats ({ + dailyStats, + dateRange +}: { + dailyStats: DailyStats[] | undefined + dateRange: DateRange +}) { + +} \ No newline at end of file diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/analyticYearlyCompletion.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/analyticYearlyCompletion.tsx index 75b2b8182244..d46c34f2295d 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/analyticYearlyCompletion.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/analyticYearlyCompletion.tsx @@ -32,12 +32,11 @@ export function AnalyticYearlyCompletion({ .reverse() return ( - <div className="flex h-full flex-col rounded-lg border bg-primary-foreground/30 p-4"> - <h1 className="text-xl font-bold">Activity</h1> - <p className="mt-0.5 text-xs text-muted-foreground"> - {lastYearCompletions} activities in the last year - </p> - <div className="mt-5 flex flex-1 items-center justify-center xl:mt-0"> + <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> diff --git a/ee/tabby-ui/components/ui/icons.tsx b/ee/tabby-ui/components/ui/icons.tsx index ab7caeff5b09..dc53281e45c1 100644 --- a/ee/tabby-ui/components/ui/icons.tsx +++ b/ee/tabby-ui/components/ui/icons.tsx @@ -1296,6 +1296,24 @@ function IconBarChart({ className, ...props }: React.ComponentProps<'svg'>) { ) } +function IconActivity({ className, ...props }: React.ComponentProps<'svg'>) { + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" + className={cn('h-4 w-4', className)} + {...props}> + <path d="M22 12h-4l-3 9L9 3l-3 9H2"/> + </svg> + ) +} + + export { IconEdit, IconNextChat, @@ -1361,5 +1379,6 @@ export { IconHistory, IconClock, IconStopWatch, - IconBarChart + IconBarChart, + IconActivity } From 4437aaba75cf10d1c6913f987dd615b57845b57c Mon Sep 17 00:00:00 2001 From: wwayne <wayne.wang0821@gmail.com> Date: Sun, 7 Apr 2024 19:24:28 +0800 Subject: [PATCH 08/16] adjust for different size --- .../reports/components/analytic.tsx | 42 ++++--------------- ee/tabby-ui/components/activity-calendar.tsx | 6 ++- 2 files changed, 14 insertions(+), 34 deletions(-) diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx index f15ac8bdd31f..803425087f16 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx @@ -40,7 +40,7 @@ function AnalyticSummary({ const totalCompletions = sum(dailyStats?.map(stats => stats.completions)) return ( <div className="flex w-full items-center justify-center space-x-6 xl:justify-start"> - <Card className="flex-1 bg-primary-foreground/30"> + <Card className="flex-1 bg-primary-foreground/30 self-stretch flex flex-col justify-between md:block"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle className="text-sm font-medium"> Accept Rate @@ -55,7 +55,7 @@ function AnalyticSummary({ </CardContent> </Card> - <Card className="flex-1 bg-primary-foreground/30"> + <Card className="flex-1 bg-primary-foreground/30 self-stretch flex flex-col justify-between md:block"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle className="text-sm font-medium"> Total completions @@ -70,7 +70,7 @@ function AnalyticSummary({ </CardContent> </Card> - <Card className="flex-1 bg-primary-foreground/30"> + <Card className="flex-1 bg-primary-foreground/30 self-stretch flex flex-col justify-between md:block"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle className="text-sm font-medium"> Total acceptances @@ -147,9 +147,9 @@ export function Analytic() { return ( <div className="mx-auto max-w-5xl"> - <div className="mb-3 flex flex-col items-center justify-between gap-y-3 xl:flex-row xl:gap-y-0"> - <div className="flex flex-col justify-center xl:justify-start"> - <h1 className="mb-1.5 scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl xl:text-left"> + <div className="mb-3 flex items-center justify-between "> + <div className="flex flex-col "> + <h1 className="mb-1.5 scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl"> Reports </h1> <p className="text-muted-foreground"> @@ -161,7 +161,7 @@ export function Analytic() { defaultValue={KEY_SELECT_ALL} onValueChange={setSelectedMember} > - <SelectTrigger className="w-[300px] lg:w-[150px]"> + <SelectTrigger className="w-[150px]"> <div className="flex w-full items-center truncate "> <span className="mr-1.5 text-muted-foreground">Member:</span> <div className="overflow-hidden text-ellipsis"> @@ -184,7 +184,7 @@ export function Analytic() { <LoadingWrapper> <div className="mb-10 flex flex-col gap-y-5"> - <div className="flex items-center justify-between"> + <div className="flex gap-y-1 md:gap-y-0 md:items-center justify-between flex-col md:flex-row"> <h1 className="text-xl font-semibold">Usage</h1> <div className="flex items-center gap-x-3"> @@ -194,7 +194,7 @@ export function Analytic() { setSelectedLanguage(value) } > - <SelectTrigger className="w-[300px] lg:w-[180px]"> + <SelectTrigger className="w-[180px]"> <div className="flex w-full items-center truncate"> <span className="mr-1.5 text-muted-foreground">Language:</span> <div className="overflow-hidden text-ellipsis"> @@ -238,30 +238,6 @@ export function Analytic() { <AnalyticYearlyCompletion yearlyStats={yearlyStats} /> </div> </LoadingWrapper> - - {/* <div className="flex flex-col gap-y-6 xl:flex-row xl:gap-x-6 xl:gap-y-0"> - {false && ( - <div className="flex-1"> - <LoadingWrapper - loading={fetchingDailyState} - fallback={<Skeleton className="h-64 w-full" />} - > - <AnlyticAcceptance - dailyStats={dailyStats} - dateRange={dateRange} - /> - </LoadingWrapper> - </div> - )} - <div style={{ flex: 3 }}> - <LoadingWrapper - loading={fetchingDailyState || fetchingYearlyStats} - fallback={<Skeleton className="h-64 w-full" />} - > - - </LoadingWrapper> - </div> - </div> */} </div> ) } diff --git a/ee/tabby-ui/components/activity-calendar.tsx b/ee/tabby-ui/components/activity-calendar.tsx index ab01762a29d6..cdb76d60c513 100644 --- a/ee/tabby-ui/components/activity-calendar.tsx +++ b/ee/tabby-ui/components/activity-calendar.tsx @@ -22,7 +22,11 @@ export default function ActivityCalendar({ const size = useWindowSize() const width = size.width || 0 const blockSize = - width >= 1600 ? 13 : width >= 1400 ? 10 : width >= 1000 ? 8 : 5 + width >= 1300 + ? 13 + : width >= 1000 + ? 9 + : 5 return ( <ReactActivityCalendar From c1718f2665694ab4e8cd2111545598e5ad6374bf Mon Sep 17 00:00:00 2001 From: wwayne <wayne.wang0821@gmail.com> Date: Sun, 7 Apr 2024 19:35:03 +0800 Subject: [PATCH 09/16] update --- .../reports/components/analytic.tsx | 74 +++++++----- .../reports/components/analyticAccptance.tsx | 106 ------------------ .../components/analyticDailyCompletion.tsx | 2 +- .../reports/components/analyticStats.tsx | 12 -- 4 files changed, 47 insertions(+), 147 deletions(-) delete mode 100644 ee/tabby-ui/app/(dashboard)/reports/components/analyticAccptance.tsx delete mode 100644 ee/tabby-ui/app/(dashboard)/reports/components/analyticStats.tsx diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx index 803425087f16..899a46e1d598 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx @@ -28,6 +28,7 @@ import { AnalyticDailyCompletion } from './analyticDailyCompletion' import { AnalyticYearlyCompletion } from './analyticYearlyCompletion' import type { DailyStats } from '../types/stats' +import { Skeleton } from '@/components/ui/skeleton' const INITIAL_DATE_RANGE = 14 const KEY_SELECT_ALL = 'all' @@ -40,7 +41,7 @@ function AnalyticSummary({ const totalCompletions = sum(dailyStats?.map(stats => stats.completions)) return ( <div className="flex w-full items-center justify-center space-x-6 xl:justify-start"> - <Card className="flex-1 bg-primary-foreground/30 self-stretch flex flex-col justify-between md:block"> + <Card className="flex flex-1 flex-col justify-between self-stretch bg-primary-foreground/30 md:block"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle className="text-sm font-medium"> Accept Rate @@ -55,7 +56,7 @@ function AnalyticSummary({ </CardContent> </Card> - <Card className="flex-1 bg-primary-foreground/30 self-stretch flex flex-col justify-between md:block"> + <Card className="flex flex-1 flex-col justify-between self-stretch bg-primary-foreground/30 md:block"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle className="text-sm font-medium"> Total completions @@ -70,7 +71,7 @@ function AnalyticSummary({ </CardContent> </Card> - <Card className="flex-1 bg-primary-foreground/30 self-stretch flex flex-col justify-between md:block"> + <Card className="flex flex-1 flex-col justify-between self-stretch bg-primary-foreground/30 md:block"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle className="text-sm font-medium"> Total acceptances @@ -157,34 +158,49 @@ export function Analytic() { </p> </div> - <Select - defaultValue={KEY_SELECT_ALL} - onValueChange={setSelectedMember} - > - <SelectTrigger className="w-[150px]"> - <div className="flex w-full items-center truncate "> - <span className="mr-1.5 text-muted-foreground">Member:</span> - <div className="overflow-hidden text-ellipsis"> - <SelectValue /> + <LoadingWrapper + loading={fetchingDailyState} + fallback={<Skeleton className="h-8 w-32" />}> + <Select + defaultValue={KEY_SELECT_ALL} + onValueChange={setSelectedMember} + > + <SelectTrigger className="w-[150px]"> + <div className="flex w-full items-center truncate "> + <span className="mr-1.5 text-muted-foreground">Member:</span> + <div className="overflow-hidden text-ellipsis"> + <SelectValue /> + </div> </div> - </div> - </SelectTrigger> - <SelectContent align='end'> - <SelectGroup> - <SelectItem value={KEY_SELECT_ALL}>All</SelectItem> - {members.map(member => ( - <SelectItem value={member.id} key={member.id}> - {member.email} - </SelectItem> - ))} - </SelectGroup> - </SelectContent> - </Select> + </SelectTrigger> + <SelectContent align='end'> + <SelectGroup> + <SelectItem value={KEY_SELECT_ALL}>All</SelectItem> + {members.map(member => ( + <SelectItem value={member.id} key={member.id}> + {member.email} + </SelectItem> + ))} + </SelectGroup> + </SelectContent> + </Select> + </LoadingWrapper> </div> - <LoadingWrapper> + <LoadingWrapper + loading={fetchingDailyState} + fallback={ + <div className="flex flex-col gap-5"> + <div className="flex justify-between gap-5"> + <Skeleton className="h-32 flex-1" /> + <Skeleton className="h-32 flex-1" /> + <Skeleton className="h-32 flex-1" /> + </div> + <Skeleton className="h-56" /> + </div> + }> <div className="mb-10 flex flex-col gap-y-5"> - <div className="flex gap-y-1 md:gap-y-0 md:items-center justify-between flex-col md:flex-row"> + <div className="flex flex-col justify-between gap-y-1 md:flex-row md:items-center md:gap-y-0"> <h1 className="text-xl font-semibold">Usage</h1> <div className="flex items-center gap-x-3"> @@ -232,7 +248,9 @@ export function Analytic() { </div> </LoadingWrapper> - <LoadingWrapper> + <LoadingWrapper + loading={fetchingYearlyStats} + fallback={<Skeleton className="mt-5 h-48" />}> <div className="mb-10"> <h1 className="mb-3 text-xl font-semibold">Activity</h1> <AnalyticYearlyCompletion yearlyStats={yearlyStats} /> diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/analyticAccptance.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/analyticAccptance.tsx deleted file mode 100644 index 61e79f072a42..000000000000 --- a/ee/tabby-ui/app/(dashboard)/reports/components/analyticAccptance.tsx +++ /dev/null @@ -1,106 +0,0 @@ -'use client' - -import moment from 'moment' -import type { DateRange } from 'react-day-picker' -import { Cell, Legend, Pie, PieChart, ResponsiveContainer } from 'recharts' - -import type { DailyStats } from '../types/stats' - -export function AnlyticAcceptance({ - dailyStats, - dateRange -}: { - dailyStats: DailyStats[] | undefined - dateRange: DateRange -}) { - const from = dateRange.from || new Date() - const to = dateRange.to || from - - let totalAccpet = 0 - let totalCompletions = 0 - dailyStats?.forEach(stats => { - totalCompletions += stats.completions - totalAccpet += stats.selects - }) - const totalPending = totalCompletions - totalAccpet - - const data = [ - { name: 'Accept', value: totalAccpet }, - { name: 'Pending', value: totalPending } - ] - - const COLORS = ['#8884d8', '#b9b7e2'] - - const RADIAN = Math.PI / 180 - const renderCustomizedLabel = ({ - cx, - cy, - midAngle, - innerRadius, - outerRadius, - percent, - name - }: { - cx: number - cy: number - midAngle: number - innerRadius: number - outerRadius: number - percent: number - name: string - }) => { - const radius = innerRadius + (outerRadius - innerRadius) * 0.5 - const x = cx + radius * Math.cos(-midAngle * RADIAN) - const y = cy + radius * Math.sin(-midAngle * RADIAN) - - if (name.toLocaleLowerCase() === 'accept') { - return ( - <text - x={x} - y={y} - fill="white" - textAnchor={x > cx ? 'start' : 'end'} - dominantBaseline="central" - fontSize={12} - > - {`${(percent * 100).toFixed(0)}%`} - </text> - ) - } - return - } - - return ( - <div className="rounded-lg border bg-primary-foreground/30 p-4"> - <h1 className="text-xl font-bold">Acceptance</h1> - <p className="mt-0.5 text-xs text-muted-foreground"> - {moment(from).format('D MMM, YYYY')} -{' '} - {moment(to).format('D MMM, YYYY')} - </p> - <ResponsiveContainer width="100%" height={210}> - <PieChart width={700} height={400}> - <Pie - data={data} - cx="50%" - cy="50%" - labelLine={false} - label={renderCustomizedLabel} - outerRadius={80} - fill="#8884d8" - dataKey="value" - > - {data.map((entry, index) => ( - <Cell - key={`cell-${index}`} - fill={COLORS[index % COLORS.length]} - /> - ))} - </Pie> - {totalCompletions !== 0 && ( - <Legend wrapperStyle={{ fontSize: '12px' }} /> - )} - </PieChart> - </ResponsiveContainer> - </div> - ) -} diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/analyticDailyCompletion.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/analyticDailyCompletion.tsx index a8c7dbc6c977..6a53c0a721d7 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/analyticDailyCompletion.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/analyticDailyCompletion.tsx @@ -110,7 +110,7 @@ export function AnalyticDailyCompletion({ <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} /> + <YAxis fontSize={12} width={20} allowDecimals={false} /> <Tooltip cursor={{ fill: 'transparent' }} content={<BarTooltip />} /> </BarChart> </ResponsiveContainer> diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/analyticStats.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/analyticStats.tsx deleted file mode 100644 index afae66fff980..000000000000 --- a/ee/tabby-ui/app/(dashboard)/reports/components/analyticStats.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { DateRange } from 'react-day-picker' -import type { DailyStats } from '../types/stats' - -export function AnalyticStats ({ - dailyStats, - dateRange -}: { - dailyStats: DailyStats[] | undefined - dateRange: DateRange -}) { - -} \ No newline at end of file From 35120f6e66f5477f9861998b87dbe80cdc00ca7f Mon Sep 17 00:00:00 2001 From: wwayne <wayne.wang0821@gmail.com> Date: Mon, 8 Apr 2024 12:15:28 +0800 Subject: [PATCH 10/16] update --- ...arlyCompletion.tsx => annual-activity.tsx} | 43 ++++++++++- ...DailyCompletion.tsx => daily-activity.tsx} | 2 +- .../components/{analytic.tsx => report.tsx} | 75 +++++++++---------- ee/tabby-ui/app/(dashboard)/reports/page.tsx | 6 +- .../reports/{hooks => }/use-all-members.tsx | 0 ee/tabby-ui/components/activity-calendar.tsx | 44 ----------- .../components/{ => ui}/date-range-picker.tsx | 0 ee/tabby-ui/components/ui/icons.tsx | 6 +- 8 files changed, 80 insertions(+), 96 deletions(-) rename ee/tabby-ui/app/(dashboard)/reports/components/{analyticYearlyCompletion.tsx => annual-activity.tsx} (56%) rename ee/tabby-ui/app/(dashboard)/reports/components/{analyticDailyCompletion.tsx => daily-activity.tsx} (98%) rename ee/tabby-ui/app/(dashboard)/reports/components/{analytic.tsx => report.tsx} (82%) rename ee/tabby-ui/app/(dashboard)/reports/{hooks => }/use-all-members.tsx (100%) delete mode 100644 ee/tabby-ui/components/activity-calendar.tsx rename ee/tabby-ui/components/{ => ui}/date-range-picker.tsx (100%) diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/analyticYearlyCompletion.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/annual-activity.tsx similarity index 56% rename from ee/tabby-ui/app/(dashboard)/reports/components/analyticYearlyCompletion.tsx rename to ee/tabby-ui/app/(dashboard)/reports/components/annual-activity.tsx index d46c34f2295d..037123fa6202 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/analyticYearlyCompletion.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/annual-activity.tsx @@ -1,10 +1,47 @@ -import moment from 'moment' +'use client' -import ActivityCalendar from '@/components/activity-calendar' +import moment from 'moment' +import { useWindowSize } from '@uidotdev/usehooks' +import { useTheme } from 'next-themes' +import ReactActivityCalendar from 'react-activity-calendar' import type { DailyStats } from '../types/stats' -export function AnalyticYearlyCompletion({ +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 diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/analyticDailyCompletion.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/daily-activity.tsx similarity index 98% rename from ee/tabby-ui/app/(dashboard)/reports/components/analyticDailyCompletion.tsx rename to ee/tabby-ui/app/(dashboard)/reports/components/daily-activity.tsx index 6a53c0a721d7..52fa36d8c815 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/analyticDailyCompletion.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/daily-activity.tsx @@ -56,7 +56,7 @@ function BarTooltip({ return null } -export function AnalyticDailyCompletion({ +export function DailyActivity({ dailyStats, dateRange }: { diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx similarity index 82% rename from ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx rename to ee/tabby-ui/app/(dashboard)/reports/components/report.tsx index 899a46e1d598..aeb9db176bc3 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/analytic.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx @@ -8,10 +8,10 @@ import { DateRange } from 'react-day-picker' import { useQuery } from 'urql' import { Language } from '@/lib/gql/generates/graphql' -import { useAllMembers } from '../hooks/use-all-members' +import { useAllMembers } from '../use-all-members' import { queryDailyStats, queryDailyStatsInPastYear } from '../query' -import { IconActivity, IconCode, IconCheck } from '@/components/ui/icons' +import { IconActivity, IconCode, IconCheck, IconUsers } from '@/components/ui/icons' import { Select, SelectContent, @@ -20,12 +20,12 @@ import { SelectTrigger, SelectValue } from '@/components/ui/select' -import DatePickerWithRange from '@/components/date-range-picker' +import DatePickerWithRange from '@/components/ui/date-range-picker' import LoadingWrapper from '@/components/loading-wrapper' import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card' -import { AnalyticDailyCompletion } from './analyticDailyCompletion' -import { AnalyticYearlyCompletion } from './analyticYearlyCompletion' +import { DailyActivity } from './daily-activity' +import { AnnualActivity } from './annual-activity' import type { DailyStats } from '../types/stats' import { Skeleton } from '@/components/ui/skeleton' @@ -33,12 +33,16 @@ import { Skeleton } from '@/components/ui/skeleton' const INITIAL_DATE_RANGE = 14 const KEY_SELECT_ALL = 'all' -function AnalyticSummary({ +function StatsSummary({ dailyStats }: { dailyStats: DailyStats[] | undefined }) { const totalCompletions = sum(dailyStats?.map(stats => stats.completions)) + const totalAcceptances = sum(dailyStats?.map(stats => stats.selects)) + const acceptRate = totalAcceptances === 0 + ? 0 + : Math.round((totalAcceptances / totalCompletions)* 100) return ( <div className="flex w-full items-center justify-center space-x-6 xl:justify-start"> <Card className="flex flex-1 flex-col justify-between self-stretch bg-primary-foreground/30 md:block"> @@ -49,10 +53,7 @@ function AnalyticSummary({ <IconActivity className="text-muted-foreground" /> </CardHeader> <CardContent> - <div className="text-2xl font-bold">TBD</div> - <p className="text-xs text-muted-foreground"> - +TBD from last week - </p> + <div className="text-2xl font-bold">{acceptRate}%</div> </CardContent> </Card> @@ -65,9 +66,6 @@ function AnalyticSummary({ </CardHeader> <CardContent> <div className="text-2xl font-bold">{numeral(totalCompletions).format('0,0')}</div> - <p className="text-xs text-muted-foreground"> - +TBD from last week - </p> </CardContent> </Card> @@ -79,17 +77,14 @@ function AnalyticSummary({ <IconCheck className="h-4 w-4 text-muted-foreground" /> </CardHeader> <CardContent> - <div className="text-2xl font-bold">TBD</div> - <p className="text-xs text-muted-foreground"> - +TBD from last week - </p> + <div className="text-2xl font-bold">{totalAcceptances}</div> </CardContent> </Card> </div> ) } -export function Analytic() { +export function Report() { const [members] = useAllMembers() const [dateRange, setDateRange] = useState<DateRange>({ from: moment().subtract(INITIAL_DATE_RANGE, 'day').toDate(), @@ -148,7 +143,7 @@ export function Analytic() { return ( <div className="mx-auto max-w-5xl"> - <div className="mb-3 flex items-center justify-between "> + <div className="mb-3 flex items-center justify-between md:items-end"> <div className="flex flex-col "> <h1 className="mb-1.5 scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl"> Reports @@ -157,21 +152,19 @@ export function Analytic() { Statistics around Tabby IDE / Extensions </p> </div> - - <LoadingWrapper - loading={fetchingDailyState} - fallback={<Skeleton className="h-8 w-32" />}> + + {!fetchingDailyState && <Select defaultValue={KEY_SELECT_ALL} onValueChange={setSelectedMember} > - <SelectTrigger className="w-[150px]"> - <div className="flex w-full items-center truncate "> - <span className="mr-1.5 text-muted-foreground">Member:</span> - <div className="overflow-hidden text-ellipsis"> + <SelectTrigger + className="w-auto border-none shadow-none"> + <div className="flex items-center pr-3"> + <IconUsers className="mr-1" /> + <p className="mr-1.5">Member:</p> <SelectValue /> </div> - </div> </SelectTrigger> <SelectContent align='end'> <SelectGroup> @@ -184,9 +177,18 @@ export function Analytic() { </SelectGroup> </SelectContent> </Select> - </LoadingWrapper> + } </div> + <LoadingWrapper + loading={fetchingYearlyStats} + fallback={<Skeleton className="mb-8 h-48" />}> + <div className="mb-10"> + <h1 className="mb-3 text-xl font-semibold">Activity</h1> + <AnnualActivity yearlyStats={yearlyStats} /> + </div> + </LoadingWrapper> + <LoadingWrapper loading={fetchingDailyState} fallback={ @@ -200,7 +202,7 @@ export function Analytic() { </div> }> <div className="mb-10 flex flex-col gap-y-5"> - <div className="flex flex-col justify-between gap-y-1 md:flex-row md:items-center md:gap-y-0"> + <div className="flex flex-col justify-between gap-y-1 md:flex-row md:items-end md:gap-y-0 "> <h1 className="text-xl font-semibold">Usage</h1> <div className="flex items-center gap-x-3"> @@ -239,23 +241,14 @@ export function Analytic() { </div> </div> - <AnalyticSummary dailyStats={dailyStats} /> + <StatsSummary dailyStats={dailyStats} /> - <AnalyticDailyCompletion + <DailyActivity dailyStats={dailyStats} dateRange={dateRange} /> </div> </LoadingWrapper> - - <LoadingWrapper - loading={fetchingYearlyStats} - fallback={<Skeleton className="mt-5 h-48" />}> - <div className="mb-10"> - <h1 className="mb-3 text-xl font-semibold">Activity</h1> - <AnalyticYearlyCompletion yearlyStats={yearlyStats} /> - </div> - </LoadingWrapper> </div> ) } diff --git a/ee/tabby-ui/app/(dashboard)/reports/page.tsx b/ee/tabby-ui/app/(dashboard)/reports/page.tsx index 16dd86eb4e97..ba6d8d8083c9 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/page.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/page.tsx @@ -1,11 +1,11 @@ import { Metadata } from 'next' -import { Analytic } from './components/analytic' +import { Report } from './components/report' export const metadata: Metadata = { - title: 'Analytics Dashboard' + title: 'Reports' } export default function Page() { - return <Analytic /> + return <Report /> } diff --git a/ee/tabby-ui/app/(dashboard)/reports/hooks/use-all-members.tsx b/ee/tabby-ui/app/(dashboard)/reports/use-all-members.tsx similarity index 100% rename from ee/tabby-ui/app/(dashboard)/reports/hooks/use-all-members.tsx rename to ee/tabby-ui/app/(dashboard)/reports/use-all-members.tsx diff --git a/ee/tabby-ui/components/activity-calendar.tsx b/ee/tabby-ui/components/activity-calendar.tsx deleted file mode 100644 index cdb76d60c513..000000000000 --- a/ee/tabby-ui/components/activity-calendar.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client' - -import dynamic from 'next/dynamic' -import { useWindowSize } from '@uidotdev/usehooks' -import { useTheme } from 'next-themes' - -// withou using dynamic, we got error "Error: calcTextDimensions() requires browser APIs" -const ReactActivityCalendar = dynamic(() => import('react-activity-calendar'), { - ssr: false -}) - -export default 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 - /> - ) -} diff --git a/ee/tabby-ui/components/date-range-picker.tsx b/ee/tabby-ui/components/ui/date-range-picker.tsx similarity index 100% rename from ee/tabby-ui/components/date-range-picker.tsx rename to ee/tabby-ui/components/ui/date-range-picker.tsx diff --git a/ee/tabby-ui/components/ui/icons.tsx b/ee/tabby-ui/components/ui/icons.tsx index dc53281e45c1..0a886cc735e4 100644 --- a/ee/tabby-ui/components/ui/icons.tsx +++ b/ee/tabby-ui/components/ui/icons.tsx @@ -1,3 +1,5 @@ +// FIXME(wwayne): Review each icons and consider re-export from `lucide-react`. + 'use client' import * as React from 'react' @@ -184,8 +186,6 @@ function IconArrowRight({ className, ...props }: React.ComponentProps<'svg'>) { ) } -// FIXME: Remove to use ChevronRight from lucide-react -// currently ui/calendar is using ChevronRight as the default option. function IconChevronRight({ className, ...props @@ -207,8 +207,6 @@ function IconChevronRight({ ) } -// FIXME: Remove to use ChevronLeft from lucide-react -// currently ui/calendar is using ChevronLeft as the default option. function IconChevronLeft({ className, ...props }: React.ComponentProps<'svg'>) { return ( <svg From d46842964f1f270ceef73d47ed71f5a930ad5d67 Mon Sep 17 00:00:00 2001 From: wwayne <wayne.wang0821@gmail.com> Date: Mon, 8 Apr 2024 12:28:03 +0800 Subject: [PATCH 11/16] update --- .../(dashboard)/reports/components/report.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx index aeb9db176bc3..77ed467c879b 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx @@ -143,7 +143,7 @@ export function Report() { return ( <div className="mx-auto max-w-5xl"> - <div className="mb-3 flex items-center justify-between md:items-end"> + <div className="mb-3 flex items-end justify-between"> <div className="flex flex-col "> <h1 className="mb-1.5 scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl"> Reports @@ -153,17 +153,21 @@ export function Report() { </p> </div> - {!fetchingDailyState && + <LoadingWrapper + loading={fetchingDailyState} + fallback={<Skeleton className="h-8 w-32" />}> <Select defaultValue={KEY_SELECT_ALL} onValueChange={setSelectedMember} > <SelectTrigger - className="w-auto border-none shadow-none"> - <div className="flex items-center pr-3"> + className="h-auto w-auto border-none py-0 shadow-none"> + <div className="flex h-6 items-center pr-3"> <IconUsers className="mr-1" /> <p className="mr-1.5">Member:</p> - <SelectValue /> + <div className="max-w-[80px] overflow-hidden text-ellipsis"> + <SelectValue /> + </div> </div> </SelectTrigger> <SelectContent align='end'> @@ -177,7 +181,7 @@ export function Report() { </SelectGroup> </SelectContent> </Select> - } + </LoadingWrapper> </div> <LoadingWrapper From 395dd1f482765f4b5317b5b37c8000b3a4aaaf64 Mon Sep 17 00:00:00 2001 From: wwayne <wayne.wang0821@gmail.com> Date: Mon, 8 Apr 2024 12:38:14 +0800 Subject: [PATCH 12/16] update --- ee/tabby-ui/app/(dashboard)/reports/components/report.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx index 77ed467c879b..3940224a5b5b 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx @@ -144,7 +144,7 @@ export function Report() { return ( <div className="mx-auto max-w-5xl"> <div className="mb-3 flex items-end justify-between"> - <div className="flex flex-col "> + <div className="flex flex-col"> <h1 className="mb-1.5 scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl"> Reports </h1> @@ -187,8 +187,8 @@ export function Report() { <LoadingWrapper loading={fetchingYearlyStats} fallback={<Skeleton className="mb-8 h-48" />}> - <div className="mb-10"> - <h1 className="mb-3 text-xl font-semibold">Activity</h1> + <div className="mb-8"> + <h1 className="mb-2 text-xl font-semibold">Activity</h1> <AnnualActivity yearlyStats={yearlyStats} /> </div> </LoadingWrapper> @@ -206,7 +206,7 @@ export function Report() { </div> }> <div className="mb-10 flex flex-col gap-y-5"> - <div className="flex flex-col justify-between gap-y-1 md:flex-row md:items-end md:gap-y-0 "> + <div className="-mb-2 flex flex-col justify-between gap-y-1 md:flex-row md:items-end md:gap-y-0"> <h1 className="text-xl font-semibold">Usage</h1> <div className="flex items-center gap-x-3"> From e2235159a0271035edd31214626f39bc6335326e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 04:39:35 +0000 Subject: [PATCH 13/16] [autofix.ci] apply automated fixes --- .../reports/components/annual-activity.tsx | 9 +-- .../reports/components/daily-activity.tsx | 43 ++++++---- .../(dashboard)/reports/components/report.tsx | 79 ++++++++++--------- ee/tabby-ui/components/ui/icons.tsx | 8 +- 4 files changed, 75 insertions(+), 64 deletions(-) diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/annual-activity.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/annual-activity.tsx index 037123fa6202..437bb422180b 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/annual-activity.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/annual-activity.tsx @@ -1,7 +1,7 @@ 'use client' -import moment from 'moment' import { useWindowSize } from '@uidotdev/usehooks' +import moment from 'moment' import { useTheme } from 'next-themes' import ReactActivityCalendar from 'react-activity-calendar' @@ -19,12 +19,7 @@ function ActivityCalendar({ const { theme } = useTheme() const size = useWindowSize() const width = size.width || 0 - const blockSize = - width >= 1300 - ? 13 - : width >= 1000 - ? 9 - : 5 + const blockSize = width >= 1300 ? 13 : width >= 1000 ? 9 : 5 return ( <ReactActivityCalendar diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/daily-activity.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/daily-activity.tsx index 52fa36d8c815..ea5af8a18a3b 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/daily-activity.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/daily-activity.tsx @@ -1,10 +1,10 @@ 'use client' import { eachDayOfInterval } from 'date-fns' -import moment from 'moment' import { sum } from 'lodash-es' +import moment from 'moment' import { useTheme } from 'next-themes' - +import type { DateRange } from 'react-day-picker' import { Bar, BarChart, @@ -13,9 +13,9 @@ import { XAxis, YAxis } from 'recharts' + import { Card, CardContent } from '@/components/ui/card' -import type { DateRange } from 'react-day-picker' import type { DailyStats } from '../types/stats' function BarTooltip({ @@ -23,29 +23,30 @@ function BarTooltip({ payload, label }: { - active?: boolean; - label?: string; + active?: boolean + label?: string payload?: { - name: string; + name: string payload: { - completion: number; - select: number; - pending: number; - }; - + completion: number + select: number + pending: number + } }[] }) { if (active && payload && payload.length) { - const {completion, select} = payload[0].payload + 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> + <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> + <span className="mr-3 inline-block w-20">Acceptance:</span> + <b>{select}</b> </p> <p className="text-muted-foreground">{label}</p> </CardContent> @@ -107,8 +108,18 @@ export function DailyActivity({ 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} /> + <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 />} /> diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx index 3940224a5b5b..3449d076e0fd 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx @@ -8,10 +8,14 @@ import { DateRange } from 'react-day-picker' import { useQuery } from 'urql' import { Language } from '@/lib/gql/generates/graphql' -import { useAllMembers } from '../use-all-members' -import { queryDailyStats, queryDailyStatsInPastYear } from '../query' - -import { IconActivity, IconCode, IconCheck, IconUsers } from '@/components/ui/icons' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import DatePickerWithRange from '@/components/ui/date-range-picker' +import { + IconActivity, + IconCheck, + IconCode, + IconUsers +} from '@/components/ui/icons' import { Select, SelectContent, @@ -20,15 +24,14 @@ import { SelectTrigger, SelectValue } from '@/components/ui/select' -import DatePickerWithRange from '@/components/ui/date-range-picker' +import { Skeleton } from '@/components/ui/skeleton' import LoadingWrapper from '@/components/loading-wrapper' -import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card' - -import { DailyActivity } from './daily-activity' -import { AnnualActivity } from './annual-activity' +import { queryDailyStats, queryDailyStatsInPastYear } from '../query' import type { DailyStats } from '../types/stats' -import { Skeleton } from '@/components/ui/skeleton' +import { useAllMembers } from '../use-all-members' +import { AnnualActivity } from './annual-activity' +import { DailyActivity } from './daily-activity' const INITIAL_DATE_RANGE = 14 const KEY_SELECT_ALL = 'all' @@ -40,16 +43,15 @@ function StatsSummary({ }) { const totalCompletions = sum(dailyStats?.map(stats => stats.completions)) const totalAcceptances = sum(dailyStats?.map(stats => stats.selects)) - const acceptRate = totalAcceptances === 0 - ? 0 - : Math.round((totalAcceptances / totalCompletions)* 100) + const acceptRate = + totalAcceptances === 0 + ? 0 + : Math.round((totalAcceptances / totalCompletions) * 100) return ( <div className="flex w-full items-center justify-center space-x-6 xl:justify-start"> <Card className="flex flex-1 flex-col justify-between self-stretch bg-primary-foreground/30 md:block"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> - <CardTitle className="text-sm font-medium"> - Accept Rate - </CardTitle> + <CardTitle className="text-sm font-medium">Accept Rate</CardTitle> <IconActivity className="text-muted-foreground" /> </CardHeader> <CardContent> @@ -65,7 +67,9 @@ function StatsSummary({ <IconCode className="text-muted-foreground" /> </CardHeader> <CardContent> - <div className="text-2xl font-bold">{numeral(totalCompletions).format('0,0')}</div> + <div className="text-2xl font-bold"> + {numeral(totalCompletions).format('0,0')} + </div> </CardContent> </Card> @@ -152,25 +156,25 @@ export function Report() { Statistics around Tabby IDE / Extensions </p> </div> - + <LoadingWrapper loading={fetchingDailyState} - fallback={<Skeleton className="h-8 w-32" />}> + fallback={<Skeleton className="h-8 w-32" />} + > <Select defaultValue={KEY_SELECT_ALL} onValueChange={setSelectedMember} > - <SelectTrigger - className="h-auto w-auto border-none py-0 shadow-none"> - <div className="flex h-6 items-center pr-3"> - <IconUsers className="mr-1" /> - <p className="mr-1.5">Member:</p> - <div className="max-w-[80px] overflow-hidden text-ellipsis"> - <SelectValue /> - </div> + <SelectTrigger className="h-auto w-auto border-none py-0 shadow-none"> + <div className="flex h-6 items-center pr-3"> + <IconUsers className="mr-1" /> + <p className="mr-1.5">Member:</p> + <div className="max-w-[80px] overflow-hidden text-ellipsis"> + <SelectValue /> </div> + </div> </SelectTrigger> - <SelectContent align='end'> + <SelectContent align="end"> <SelectGroup> <SelectItem value={KEY_SELECT_ALL}>All</SelectItem> {members.map(member => ( @@ -181,12 +185,13 @@ export function Report() { </SelectGroup> </SelectContent> </Select> - </LoadingWrapper> + </LoadingWrapper> </div> <LoadingWrapper loading={fetchingYearlyStats} - fallback={<Skeleton className="mb-8 h-48" />}> + fallback={<Skeleton className="mb-8 h-48" />} + > <div className="mb-8"> <h1 className="mb-2 text-xl font-semibold">Activity</h1> <AnnualActivity yearlyStats={yearlyStats} /> @@ -204,11 +209,12 @@ export function Report() { </div> <Skeleton className="h-56" /> </div> - }> + } + > <div className="mb-10 flex flex-col gap-y-5"> <div className="-mb-2 flex flex-col justify-between gap-y-1 md:flex-row md:items-end md:gap-y-0"> <h1 className="text-xl font-semibold">Usage</h1> - + <div className="flex items-center gap-x-3"> <Select defaultValue={KEY_SELECT_ALL} @@ -218,7 +224,9 @@ export function Report() { > <SelectTrigger className="w-[180px]"> <div className="flex w-full items-center truncate"> - <span className="mr-1.5 text-muted-foreground">Language:</span> + <span className="mr-1.5 text-muted-foreground"> + Language: + </span> <div className="overflow-hidden text-ellipsis"> <SelectValue /> </div> @@ -247,10 +255,7 @@ export function Report() { <StatsSummary dailyStats={dailyStats} /> - <DailyActivity - dailyStats={dailyStats} - dateRange={dateRange} - /> + <DailyActivity dailyStats={dailyStats} dateRange={dateRange} /> </div> </LoadingWrapper> </div> diff --git a/ee/tabby-ui/components/ui/icons.tsx b/ee/tabby-ui/components/ui/icons.tsx index 0a886cc735e4..60cee055a3f4 100644 --- a/ee/tabby-ui/components/ui/icons.tsx +++ b/ee/tabby-ui/components/ui/icons.tsx @@ -1305,13 +1305,13 @@ function IconActivity({ className, ...props }: React.ComponentProps<'svg'>) { strokeLinecap="round" strokeLinejoin="round" className={cn('h-4 w-4', className)} - {...props}> - <path d="M22 12h-4l-3 9L9 3l-3 9H2"/> - </svg> + {...props} + > + <path d="M22 12h-4l-3 9L9 3l-3 9H2" /> + </svg> ) } - export { IconEdit, IconNextChat, From 7667da785416f7f1dda0dae44a054bffd1d3db58 Mon Sep 17 00:00:00 2001 From: Meng Zhang <meng@tabbyml.com> Date: Sun, 7 Apr 2024 22:01:31 -0700 Subject: [PATCH 14/16] Update ee/tabby-ui/app/(dashboard)/reports/components/daily-activity.tsx --- .../app/(dashboard)/reports/components/daily-activity.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/daily-activity.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/daily-activity.tsx index ea5af8a18a3b..dee3da7ef44b 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/daily-activity.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/daily-activity.tsx @@ -97,7 +97,7 @@ export function DailyActivity({ }) return ( <div className="rounded-lg border bg-primary-foreground/30 px-6 py-4"> - <h3 className="mb-5 text-sm font-medium tracking-tight">Completions</h3> + <h3 className="mb-5 text-sm font-medium tracking-tight">Daily Statistics</h3> <ResponsiveContainer width="100%" height={300}> <BarChart data={chartData} From 307d8f621c3f513752208495fc490e2600eaf1f3 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 05:02:28 +0000 Subject: [PATCH 15/16] [autofix.ci] apply automated fixes --- .../app/(dashboard)/reports/components/daily-activity.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/daily-activity.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/daily-activity.tsx index dee3da7ef44b..90a8963f82ca 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/daily-activity.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/daily-activity.tsx @@ -97,7 +97,9 @@ export function DailyActivity({ }) 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> + <h3 className="mb-5 text-sm font-medium tracking-tight"> + Daily Statistics + </h3> <ResponsiveContainer width="100%" height={300}> <BarChart data={chartData} From 9b31c72111076621ccfd7d3abe1accbfe949e63a Mon Sep 17 00:00:00 2001 From: wwayne <wayne.wang0821@gmail.com> Date: Mon, 8 Apr 2024 14:59:45 +0800 Subject: [PATCH 16/16] update --- ee/tabby-ui/app/(dashboard)/reports/components/report.tsx | 8 ++++---- ee/tabby-ui/lib/tabby/gql.ts | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx index 3449d076e0fd..6e341cc93bc3 100644 --- a/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx +++ b/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx @@ -147,9 +147,9 @@ export function Report() { return ( <div className="mx-auto max-w-5xl"> - <div className="mb-3 flex items-end justify-between"> + <div className="mb-3 flex flex-col items-center justify-between gap-y-2 md:flex-row md:items-end md:gap-y-0"> <div className="flex flex-col"> - <h1 className="mb-1.5 scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl"> + <h1 className="mb-1.5 scroll-m-20 text-center text-4xl font-extrabold tracking-tight md:text-left lg:text-5xl"> Reports </h1> <p className="text-muted-foreground"> @@ -166,10 +166,10 @@ export function Report() { onValueChange={setSelectedMember} > <SelectTrigger className="h-auto w-auto border-none py-0 shadow-none"> - <div className="flex h-6 items-center pr-3"> + <div className="flex h-6 items-center"> <IconUsers className="mr-1" /> <p className="mr-1.5">Member:</p> - <div className="max-w-[80px] overflow-hidden text-ellipsis"> + <div className="w-[80px] overflow-hidden text-ellipsis text-left"> <SelectValue /> </div> </div> diff --git a/ee/tabby-ui/lib/tabby/gql.ts b/ee/tabby-ui/lib/tabby/gql.ts index f41350f469c4..8283c63e9022 100644 --- a/ee/tabby-ui/lib/tabby/gql.ts +++ b/ee/tabby-ui/lib/tabby/gql.ts @@ -97,6 +97,10 @@ const client = new Client({ requestPolicy: 'cache-and-network', exchanges: [ cacheExchange({ + keys: { + CompletionStats: () => null, + ServerInfo: () => null + }, resolvers: { Query: { invitations: relayPagination(),