diff --git a/.gitignore b/.gitignore index 7a1c2ec..2119570 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store .venv .tinyb +.tb_error.txt .tmp .tb_error* tinybird/fixtures diff --git a/apps/web/.env.example b/apps/web/.env.example index 3aafeac..d9a8089 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,2 +1,3 @@ ANTHROPIC_API_KEY= -NEXT_PUBLIC_VERCEL_URL= \ No newline at end of file +NEXT_PUBLIC_VERCEL_URL= +NEXT_PUBLIC_TINYBIRD_API_HOST= \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index 9e6ce95..2c9b943 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -21,6 +21,7 @@ "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-dialog": "^1.1.4", + "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-popover": "^1.1.4", "@radix-ui/react-scroll-area": "^1.2.2", diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index d7dbecf..b369a02 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@radix-ui/react-dialog': specifier: ^1.1.4 version: 1.1.4(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.4 + version: 2.1.4(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-label': specifier: ^2.1.1 version: 2.1.1(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -616,6 +619,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dropdown-menu@2.1.4': + resolution: {integrity: sha512-iXU1Ab5ecM+yEepGAWK8ZhMyKX4ubFdCNtol4sT9D0OVErG9PNElfx3TQhjw7n7BC5nFVz68/5//clWy+8TXzA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-guards@1.1.1': resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} peerDependencies: @@ -660,6 +676,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-menu@2.1.4': + resolution: {integrity: sha512-BnOgVoL6YYdHAG6DtXONaR29Eq4nvbi8rutrV/xlr3RQCMMb3yqP85Qiw/3NReozrSW+4dfLkK+rc1hb4wPU/A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popover@1.1.4': resolution: {integrity: sha512-aUACAkXx8LaFymDma+HQVji7WhvEhpFJ7+qPz17Nf4lLZqtreGOFRiNQWQmhzp7kEWg9cOyyQJpdIMUMPc/CPw==} peerDependencies: @@ -725,6 +754,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-roving-focus@1.1.1': + resolution: {integrity: sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-scroll-area@1.2.2': resolution: {integrity: sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==} peerDependencies: @@ -3196,6 +3238,21 @@ snapshots: '@types/react': 19.0.2 '@types/react-dom': 19.0.2(@types/react@19.0.2) + '@radix-ui/react-dropdown-menu@2.1.4(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-menu': 2.1.4(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.2)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.2 + '@types/react-dom': 19.0.2(@types/react@19.0.2) + '@radix-ui/react-focus-guards@1.1.1(@types/react@19.0.2)(react@19.0.0)': dependencies: react: 19.0.0 @@ -3229,6 +3286,32 @@ snapshots: '@types/react': 19.0.2 '@types/react-dom': 19.0.2(@types/react@19.0.2) + '@radix-ui/react-menu@2.1.4(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-popper': 1.2.1(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-roving-focus': 1.1.1(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.1.1(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.2)(react@19.0.0) + aria-hidden: 1.2.4 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-remove-scroll: 2.6.2(@types/react@19.0.2)(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.2 + '@types/react-dom': 19.0.2(@types/react@19.0.2) + '@radix-ui/react-popover@1.1.4(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -3299,6 +3382,23 @@ snapshots: '@types/react': 19.0.2 '@types/react-dom': 19.0.2(@types/react@19.0.2) + '@radix-ui/react-roving-focus@1.1.1(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.2)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.2 + '@types/react-dom': 19.0.2(@types/react@19.0.2) + '@radix-ui/react-scroll-area@1.2.2(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/number': 1.1.0 diff --git a/apps/web/src/components/tools/vercel/dashboard.tsx b/apps/web/src/components/tools/vercel/dashboard.tsx index d99bbef..7ea35b0 100644 --- a/apps/web/src/components/tools/vercel/dashboard.tsx +++ b/apps/web/src/components/tools/vercel/dashboard.tsx @@ -4,14 +4,14 @@ import { useQueryState } from 'nuqs' import { useEffect, useState } from 'react' import { addDays, format } from 'date-fns' import { DateRange } from 'react-day-picker' -import { pipe } from '@/lib/tinybird' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { TimeRange, type TimeRange as TR } from '@/components/time-range' import MetricCard from '@/components/metric-card' -import { DeploymentsChart, DeploymentsData } from './deployments-chart' -import { DurationChart, DurationData } from './duration-chart' -import { ProjectsChart, ProjectData } from './projects-chart' -import { GitAnalyticsChart, GitData } from './git-analytics-chart' +import { VercelDeploymentDuration } from './deployment-duration-ts' +import { VercelDeploymentsOverTime } from './deployments-over-time' +import { GitAnalytics } from './git-analytics' +import { VercelProjects } from './project-stats' +import { pipe } from '@/lib/tinybird' interface VercelMetrics { total_deployments: number @@ -26,13 +26,9 @@ export default function VercelDashboard() { from: addDays(new Date(), -7), to: new Date() }) + const [metrics, setMetrics] = useState() const [, setIsLoading] = useState(true) - const [metrics, setMetrics] = useState() - const [deploymentsData, setDeploymentsData] = useState([]) - const [durationData, setDurationData] = useState([]) - const [projectsData, setProjectsData] = useState([]) - const [gitData, setGitData] = useState([]) useEffect(() => { async function fetchData() { @@ -48,24 +44,10 @@ export default function VercelDashboard() { setIsLoading(true) const [ metricsResult, - deploymentsResult, - durationResult, - projectsResult, - gitAnalyticsResult, - ] = await Promise.all([ pipe<{ data: VercelMetrics[] }>(token, 'vercel_deployment_metrics', params), - pipe<{ data: DeploymentsData[] }>(token, 'vercel_deployments_over_time', params), - pipe<{ data: DurationData[] }>(token, 'vercel_deployment_duration', params), - pipe<{ data: ProjectData[] }>(token, 'vercel_project_stats', params), - pipe<{ data: GitData[] }>(token, 'vercel_git_analytics', params) ]) - setMetrics(metricsResult?.data?.[0]) - setDeploymentsData(deploymentsResult?.data ?? []) - setDurationData(durationResult?.data ?? []) - setProjectsData(projectsResult?.data ?? []) - setGitData(gitAnalyticsResult?.data ?? []) } catch (error) { console.error('Failed to fetch data:', error) } finally { @@ -78,31 +60,29 @@ export default function VercelDashboard() { return (
- setDateRange(range || { from: addDays(new Date(), -7), to: new Date() })} - className="mb-8" - /> +
+ setDateRange(range || { from: addDays(new Date(), -7), to: new Date() })} + className="mb-8" + /> +
{/* Metrics Row */} -
+
- 0 ? `${Math.round(durationData.reduce((acc, curr) => acc + curr.avg_duration, 0) / durationData.length)}s` : 'N/A'} + value={metrics?.success_rate ?? 'N/A'} />
@@ -113,9 +93,11 @@ export default function VercelDashboard() { Deployments Over Time - @@ -125,22 +107,26 @@ export default function VercelDashboard() { Deploy Duration - + {/* Deploy Duration chart */} +
- {/* Tables and Additional Charts */}
Top Projects - @@ -150,8 +136,10 @@ export default function VercelDashboard() { Git Analytics - diff --git a/apps/web/src/components/tools/vercel/deployment-duration-ts.tsx b/apps/web/src/components/tools/vercel/deployment-duration-ts.tsx new file mode 100644 index 0000000..53c01c1 --- /dev/null +++ b/apps/web/src/components/tools/vercel/deployment-duration-ts.tsx @@ -0,0 +1,24 @@ +'use client' + +import { LineChart } from '@tinybirdco/charts' +import { useQueryState } from 'nuqs' + +export function VercelDeploymentDuration(params: { + date_from?: string + date_to?: string + time_range?: string +}) { + const [token] = useQueryState('token') + return ( + + ) +} \ No newline at end of file diff --git a/apps/web/src/components/tools/vercel/deployments-chart.tsx b/apps/web/src/components/tools/vercel/deployments-chart.tsx deleted file mode 100644 index 77ea1c7..0000000 --- a/apps/web/src/components/tools/vercel/deployments-chart.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { BarChart, Bar, XAxis, YAxis } from 'recharts' -import { ChartContainer, ChartTooltip, ChartConfig } from '@/components/ui/chart' -import { format } from 'date-fns' -import { type TimeRange } from '@/components/time-range' - -export interface DeploymentsData { - period: string - event_type: string - count: number -} - -const chartConfig = { - 'deployment.succeeded': { - color: "hsl(var(--chart-2))", - label: "Successful Deployments", - }, - 'deployment.error': { - color: "hsl(var(--chart-1))", - label: "Failed Deployments", - }, -} satisfies ChartConfig - -export function DeploymentsChart({ data, timeRange }: { data: DeploymentsData[]; timeRange: TimeRange }) { - if (!data.length) return
No data available
- - let dateFormat = 'yyyy-MM-dd HH:mm' - if (timeRange === 'hourly') { - dateFormat = 'yyyy-MM-dd HH:mm' - } else if (timeRange === 'daily') { - dateFormat = 'd MMMM' - } else if (timeRange === 'weekly') { - dateFormat = 'd MMMM' - } else if (timeRange === 'monthly') { - dateFormat = 'MMMM' - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const chartData = data.reduce((acc: any[], curr: DeploymentsData) => { - const existing = acc.find(d => d.period === curr.period) - if (existing) { - existing[curr.event_type] = curr.count - } else { - acc.push({ - period: curr.period, - [curr.event_type]: curr.count - }) - } - return acc - }, []) - - return ( - - - format(new Date(value), dateFormat)} - /> - - { - if (!active || !payload?.length) return null - const data = payload[0].payload - return ( -
-
- {format(new Date(data.period), dateFormat)} -
- { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - payload.map((p: any) => ( -
- {chartConfig[p.dataKey as keyof typeof chartConfig].label}: {p.value} -
- ))} -
- ) - }} - /> - - -
-
- ) -} \ No newline at end of file diff --git a/apps/web/src/components/tools/vercel/deployments-over-time.tsx b/apps/web/src/components/tools/vercel/deployments-over-time.tsx new file mode 100644 index 0000000..9c4218a --- /dev/null +++ b/apps/web/src/components/tools/vercel/deployments-over-time.tsx @@ -0,0 +1,25 @@ +'use client' + +import { BarChart } from '@tinybirdco/charts' +import { useQueryState } from 'nuqs' + +export function VercelDeploymentsOverTime(params: { + date_from?: string + date_to?: string + time_range?: string +}) { + const [token] = useQueryState('token') + return ( + + ) +} \ No newline at end of file diff --git a/apps/web/src/components/tools/vercel/duration-chart.tsx b/apps/web/src/components/tools/vercel/duration-chart.tsx deleted file mode 100644 index 831d189..0000000 --- a/apps/web/src/components/tools/vercel/duration-chart.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { format } from 'date-fns' -import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' -import { Card } from '@/components/ui/card' -import { type TimeRange } from '@/components/time-range' - -export interface DurationData { - period: string - avg_duration: number - p95_duration: number -} - -interface DurationChartProps { - data: DurationData[] - timeRange: TimeRange -} - -export function DurationChart({ data, timeRange }: DurationChartProps) { - - let dateFormat = 'yyyy-MM-dd HH:mm' - if (timeRange === 'hourly') { - dateFormat = 'yyyy-MM-dd HH:mm' - } else if (timeRange === 'daily') { - dateFormat = 'd MMMM' - } else if (timeRange === 'weekly') { - dateFormat = 'd MMMM' - } else if (timeRange === 'monthly') { - dateFormat = 'MMMM' - } - - return ( - - - format(new Date(value), dateFormat)} - /> - `${value}s`} - /> - { - if (!active || !payload?.length) return null - return ( - -
- {format(new Date(payload[0].payload.period), dateFormat)} -
-
- Avg: {payload[0].payload.avg_duration}s -
-
- P95: {payload[0].payload.p95_duration}s -
-
- ) - }} - /> - - -
-
- ) -} \ No newline at end of file diff --git a/apps/web/src/components/tools/vercel/git-analytics-chart.tsx b/apps/web/src/components/tools/vercel/git-analytics-chart.tsx deleted file mode 100644 index 7c490c8..0000000 --- a/apps/web/src/components/tools/vercel/git-analytics-chart.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { BarChart, Bar, XAxis, YAxis } from 'recharts' -import { ChartContainer, ChartTooltip, ChartConfig } from '@/components/ui/chart' - -export interface GitData { - author: string - commits: number - github_repo?: string - gitlab_repo?: string - branch: string -} - -const chartConfig = { - commits: { - color: "hsl(var(--primary))", - label: "Commits", - }, -} satisfies ChartConfig - -function transformData(data: GitData[]): (GitData & { fill: string })[] { - return data.map((item, index) => ({ - ...item, - fill: `hsl(var(--chart-${(index % 12) + 1}))` - })); -} - -export function GitAnalyticsChart({ data }: { data: GitData[] }) { - console.log(data) - data = transformData(data) - - return ( - - - - - { - if (!active || !payload?.length) return null - const data = payload[0].payload - return ( -
-
{data.author}
-
Commits: {data.commits}
- {data.github_repo &&
Repo: {data.github_repo}
} - {data.gitlab_repo &&
Repo: {data.gitlab_repo}
} -
- ) - }} - /> - -
-
- ) -} \ No newline at end of file diff --git a/apps/web/src/components/tools/vercel/git-analytics.tsx b/apps/web/src/components/tools/vercel/git-analytics.tsx new file mode 100644 index 0000000..4d5edd0 --- /dev/null +++ b/apps/web/src/components/tools/vercel/git-analytics.tsx @@ -0,0 +1,22 @@ +'use client' + +import { BarList } from '@tinybirdco/charts' +import { useQueryState } from 'nuqs' + +export function GitAnalytics(params: { + date_from?: string + date_to?: string +}) { + const [token] = useQueryState('token') + return ( + + ) +} \ No newline at end of file diff --git a/apps/web/src/components/tools/vercel/project-stats.tsx b/apps/web/src/components/tools/vercel/project-stats.tsx new file mode 100644 index 0000000..3ef68ad --- /dev/null +++ b/apps/web/src/components/tools/vercel/project-stats.tsx @@ -0,0 +1,22 @@ +'use client' + +import { BarList } from '@tinybirdco/charts' +import { useQueryState } from 'nuqs' + +export function VercelProjects(params: { + date_from?: string + date_to?: string +}) { + const [token] = useQueryState('token') + return ( + + ) +} \ No newline at end of file diff --git a/apps/web/src/components/tools/vercel/projects-chart.tsx b/apps/web/src/components/tools/vercel/projects-chart.tsx deleted file mode 100644 index 5501a73..0000000 --- a/apps/web/src/components/tools/vercel/projects-chart.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { BarChart, Bar, XAxis, YAxis } from 'recharts' -import { ChartContainer, ChartTooltip, ChartConfig } from '@/components/ui/chart' - -export interface ProjectData { - project_name: string - total_deployments: number - error_rate: number -} - -const chartConfig = { - total_deployments: { - color: "hsl(var(--chart-1))", - label: "Total Deployments", - }, - error_rate: { - color: "hsl(var(--chart-2))", - label: "Error Rate %", - }, -} satisfies ChartConfig - -function transformData(data: ProjectData[]): (ProjectData & { fill: string })[] { - return data.map((item, index) => ({ - ...item, - fill: `hsl(var(--chart-${(index % 12) + 1}))` - })); -} - -export function ProjectsChart({ data }: { data: ProjectData[] }) { - if (!data.length) return
No data available
- - data = transformData(data) - - return ( - - - - - { - if (!active || !payload?.length) return null - const data = payload[0].payload - return ( -
-
{data.project_name}
-
Deployments: {data.total_deployments}
-
Error Rate: {data.error_rate}%
-
- ) - }} - /> - -
-
- ) -} \ No newline at end of file diff --git a/apps/web/src/components/ui/dropdown-menu.tsx b/apps/web/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..082639f --- /dev/null +++ b/apps/web/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,201 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + svg]:size-4 [&>svg]:shrink-0", + inset && "pl-8", + className + )} + {...props} + /> +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/tinybird/pipes/vercel_git_analytics.pipe b/tinybird/pipes/vercel_git_analytics.pipe index 9ae28d0..eefb300 100644 --- a/tinybird/pipes/vercel_git_analytics.pipe +++ b/tinybird/pipes/vercel_git_analytics.pipe @@ -4,18 +4,23 @@ TAGS "vercel" NODE git_analytics SQL > % - SELECT - coalesce(event.payload.deployment.meta.githubCommitAuthorName::String, event.payload.deployment.meta.gitlabCommitAuthorName::String) as author, - count() as commits, - any(event.payload.deployment.meta.githubRepo::String) as github_repo, - any(event.payload.deployment.meta.gitlabProjectRepo::String) as gitlab_repo, - any(event.payload.deployment.meta.githubCommitRef::String) as branch + SELECT + coalesce( + nullIf(event.payload.deployment.meta.githubCommitAuthorName::String, ''), + coalesce( + nullIf(event.payload.deployment.meta.gitlabCommitAuthorLogin::String, ''), + nullIf(event.payload.deployment.meta.gitlabCommitAuthorName::String, '') + ), + 'user_not_found' + ) as author, + count() as commits FROM vercel - WHERE event_type = 'deployment.created' - AND event_time >= {{DateTime(date_from, '2024-01-01 00:00:00')}} - AND event_time <= {{DateTime(date_to, '2024-12-31 23:59:59')}} - AND author != '' + WHERE + event_type = 'deployment.created' + AND event_time >= {{ DateTime(date_from, '2024-01-01 00:00:00') }} + AND event_time <= {{ DateTime(date_to, '2024-12-31 23:59:59') }} GROUP BY author ORDER BY commits DESC + LIMIT 10 TYPE ENDPOINT diff --git a/tinybird/pipes/vercel_project_stats.pipe b/tinybird/pipes/vercel_project_stats.pipe index 503df61..35c17ac 100644 --- a/tinybird/pipes/vercel_project_stats.pipe +++ b/tinybird/pipes/vercel_project_stats.pipe @@ -5,8 +5,8 @@ NODE project_stats SQL > % SELECT - event.payload.project.id as project_id, - event.payload.name as project_name, + event.payload.project.id::String as project_id, + event.payload.name::String as project_name, count() as total_deployments, countIf(event_type = 'deployment.error') as errors, round(countIf(event_type = 'deployment.error') * 100.0 / count(), 2) as error_rate @@ -16,5 +16,6 @@ SQL > AND event_time <= {{DateTime(date_to, '2024-12-31 23:59:59')}} GROUP BY project_id, project_name ORDER BY total_deployments DESC + LIMIT 10 TYPE ENDPOINT