Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Edit"
]
}
}
75 changes: 59 additions & 16 deletions apps/web/app/analyze-user/[login]/_charts/ChartWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import dynamic from 'next/dynamic';
import React, { useMemo } from 'react';
import type { EChartsOption } from 'echarts';
import { ShowSQLInline } from '@/components/Analyze/ShowSQL';

const EChartsWrapper = dynamic(() => import('@/components/Analyze/EChartsWrapper'), { ssr: false });

Expand All @@ -12,36 +13,78 @@ interface PersonalChartProps {
height?: number;
loading?: boolean;
noData?: boolean;
/** SQL string returned from the API, used for the SHOW SQL button */
sql?: string;
/** Query name for the SHOW SQL explain tab */
queryName?: string;
/** Query params for the SHOW SQL explain tab */
queryParams?: Record<string, any>;
}

export default function PersonalChart ({ title, option, height = 400, loading, noData }: PersonalChartProps) {
const mergedOption = useMemo<EChartsOption>(() => ({
backgroundColor: 'transparent',
title: { text: title, left: 'center', textStyle: { color: '#dadada', fontSize: 14, fontWeight: 'bold' } },
legend: { type: 'scroll', orient: 'horizontal', top: 32, textStyle: { color: '#aaa' } },
grid: { top: 64, left: 8, right: 8, bottom: 48, containLabel: true },
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
dataZoom: [{ type: 'slider', showDataShadow: false }],
...(noData
export default function PersonalChart ({ title, option, height = 400, loading, noData, sql, queryName, queryParams }: PersonalChartProps) {
const mergedOption = useMemo<EChartsOption>(() => {
const axisLine = { lineStyle: { color: '#2a2a2c' } };
const splitLine = { show: true, lineStyle: { color: '#2a2a2c', type: 'dashed' as const } };
const baseOption = noData
? {
graphic: [{ type: 'text', left: 'center', top: 'middle', style: { fontSize: 16, fontWeight: 'bold' as const, text: 'No relevant data yet', fill: '#7c7c7c' } }],
xAxis: { type: 'time' },
yAxis: { type: 'value' },
xAxis: { type: 'time' as const, splitLine, axisLine },
yAxis: { type: 'value' as const, splitLine, axisLine },
series: [],
}
: option),
}), [title, option, noData]);
: option;

// Inject grid lines into existing axes
const injectSplitLine = (axis: any) => {
if (!axis) return axis;
if (Array.isArray(axis)) return axis.map((a: any) => ({ ...a, splitLine: { ...splitLine, ...a?.splitLine }, axisLine: { ...axisLine, ...a?.axisLine } }));
return { ...axis, splitLine: { ...splitLine, ...axis?.splitLine }, axisLine: { ...axisLine, ...axis?.axisLine } };
};

return {
backgroundColor: 'transparent',
legend: { type: 'scroll', orient: 'horizontal', top: 8, textStyle: { color: '#aaa' } },
grid: { top: 40, left: 8, right: 8, bottom: 48, containLabel: true },
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
dataZoom: [{
type: 'slider',
showDataShadow: false,
height: 16,
borderColor: 'transparent',
backgroundColor: '#1a1a1b',
fillerColor: 'rgba(255,255,255,0.05)',
handleSize: 16,
handleStyle: { color: '#555', borderColor: '#555', borderWidth: 1 },
moveHandleSize: 4,
textStyle: { color: '#888', fontSize: 10 },
dataBackground: { lineStyle: { color: 'transparent' }, areaStyle: { color: 'transparent' } },
selectedDataBackground: { lineStyle: { color: 'transparent' }, areaStyle: { color: 'transparent' } },
}],
...baseOption,
xAxis: injectSplitLine((baseOption as any)?.xAxis),
yAxis: injectSplitLine((baseOption as any)?.yAxis),
};
}, [option, noData]);

if (loading) {
return (
<div className="mb-4 flex items-center justify-center rounded-2xl overflow-hidden" style={{ height, background: 'rgb(36, 35, 49)', boxShadow: '0px 4px 4px 0px rgba(36, 39, 56, 0.25)' }}>
<div className="text-[#fbe593] text-sm animate-pulse">Loading...</div>
<div className="mb-6">
<div className="flex items-center justify-between gap-4 mb-3">
<h4 className="text-[14px] font-medium text-[#e9eaee]">{title}</h4>
</div>
<div className="flex items-center justify-center" style={{ height }}>
<div className="text-[#7c7c7c] text-sm animate-pulse">Loading...</div>
</div>
</div>
);
}

return (
<div className="mb-4 rounded-2xl overflow-hidden" style={{ background: 'rgb(36, 35, 49)', boxShadow: '0px 4px 4px 0px rgba(36, 39, 56, 0.25)' }}>
<div className="mb-6">
<div className="flex items-center justify-between gap-4 mb-3">
<h4 className="text-[14px] font-medium text-[#e9eaee]">{title}</h4>
{sql && <ShowSQLInline sql={sql} queryName={queryName} queryParams={queryParams} />}
</div>
<EChartsWrapper
option={mergedOption}
notMerge
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { EChartsOption } from 'echarts';

export function createTimeSeriesOption (series: any[]): EChartsOption {
return {
xAxis: { type: 'time', min: '2011-01-01' },
yAxis: { type: 'value' },
series,
};
}
4 changes: 4 additions & 0 deletions apps/web/app/analyze-user/[login]/_hooks/usePersonal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export function usePersonalData<K extends keyof RequestMap> (key: K, userId: num

return {
data: data?.data as RequestMap[K][] | undefined,
sql: data?.sql as string | undefined,
queryName: data?.query as string | undefined,
loading: isLoading,
error,
};
Expand Down Expand Up @@ -101,6 +103,8 @@ export function usePersonalContributionActivities (userId: number | undefined, t

return {
data: data?.data as ContributionActivity[] | undefined,
sql: data?.sql as string | undefined,
queryName: data?.query as string | undefined,
loading: isLoading,
error,
};
Expand Down
37 changes: 15 additions & 22 deletions apps/web/app/analyze-user/[login]/_sections/activities.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import SectionTemplate from '@/components/Analyze/Section';
import { ScrollspySectionWrapper } from '@/components/Scrollspy/SectionWrapper';
import { SectionHeading } from '@/components/ui/SectionHeading';
import { AnalyzeOwnerContext } from '@/components/Context/Analyze/AnalyzeOwner';
import * as React from 'react';
import { useCallback, useMemo, useState } from 'react';
Expand All @@ -15,24 +16,25 @@ import {
usePersonalContributionActivities,
useRange,
} from '../_hooks/usePersonal';
import { TabBar } from '@/components/ui/TabBar';

export default function ActivitiesSection () {
const { id: userId } = React.useContext(AnalyzeOwnerContext);

return (
<SectionTemplate id="activities" title="Contribution Activities" level={2} className="pt-8"
description="All personal activities happened on all public repositories in GitHub since 2011. You can check each specific activity type by type with a timeline."
>
<ScrollspySectionWrapper anchor="activities" className="pt-8 pb-8">
<SectionHeading>Contribution Activities</SectionHeading>
<p className="text-sm text-[#8c8c8c] mb-4">All personal activities happened on all public repositories in GitHub since 2011. You can check each specific activity type by type with a timeline.</p>
<ActivityChart userId={userId} />
</SectionTemplate>
</ScrollspySectionWrapper>
);
}

function ActivityChart ({ userId }: { userId: number }) {
const [type, setType] = useState<ContributionActivityType>('all');
const [period, setPeriod] = useState<ContributionActivityRange>('last_28_days');

const { data, loading } = usePersonalContributionActivities(userId, type, period);
const { data, sql, queryName, loading } = usePersonalContributionActivities(userId, type, period);
const repoNames = useDimension(data ?? [], 'repo_name');

const [min, max] = useRange(period);
Expand All @@ -50,8 +52,8 @@ function ActivityChart ({ userId }: { userId: number }) {
const chartData = useMemo(() => (data ?? []).map(r => [r.event_period, r.repo_name, r.cnt]), [data]);

const option = useMemo(() => ({
legend: { type: 'scroll' as const, orient: 'horizontal' as const, top: 32, textStyle: { color: '#aaa' } },
grid: { top: 64, left: 8, right: 8, bottom: 8, containLabel: true },
legend: { type: 'scroll' as const, orient: 'horizontal' as const, top: 8, textStyle: { color: '#aaa' } },
grid: { top: 40, left: 8, right: 8, bottom: 8, containLabel: true },
tooltip: { trigger: 'item' as const },
dataZoom: undefined as any,
xAxis: { type: 'time' as const, min, max },
Expand All @@ -65,25 +67,16 @@ function ActivityChart ({ userId }: { userId: number }) {
}],
}), [chartData, repoNames, min, max, tooltipFormatter]);

const queryParams = useMemo(() => ({ userId }), [userId]);
const chartHeight = 240 + 30 * repoNames.length;

return (
<div>
<div className="flex flex-wrap gap-3 mb-4">
<label className="flex flex-col gap-1 text-xs text-[#8c8c8c]">
Contribution type
<select value={type} onChange={e => setType(e.target.value as ContributionActivityType)} className="rounded border border-[#363638] bg-[#2e2e2f] px-2 py-1 text-sm text-[#e0e0e0] outline-none focus:border-[#fbe593]">
{contributionActivityTypes.map(({ key, label }) => <option key={key} value={key}>{label}</option>)}
</select>
</label>
<label className="flex flex-col gap-1 text-xs text-[#8c8c8c]">
Period
<select value={period} onChange={e => setPeriod(e.target.value as ContributionActivityRange)} className="rounded border border-[#363638] bg-[#2e2e2f] px-2 py-1 text-sm text-[#e0e0e0] outline-none focus:border-[#fbe593]">
{contributionActivityRanges.map(({ key, label }) => <option key={key} value={key}>{label}</option>)}
</select>
</label>
<div className="flex flex-wrap gap-6 mb-4">
<TabBar items={contributionActivityTypes} value={type} onChange={(key) => setType(key as ContributionActivityType)} />
<TabBar items={contributionActivityRanges} value={period} onChange={(key) => setPeriod(key as ContributionActivityRange)} />
</div>
<PersonalChart title={title} option={option} height={chartHeight} loading={loading} noData={!loading && (!data || data.length === 0)} />
<PersonalChart title={title} option={option} height={chartHeight} loading={loading} noData={!loading && (!data || data.length === 0)} sql={sql} queryName={queryName} queryParams={queryParams} />
</div>
);
}
21 changes: 12 additions & 9 deletions apps/web/app/analyze-user/[login]/_sections/behaviour.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import SectionTemplate from '@/components/Analyze/Section';
import { ScrollspySectionWrapper } from '@/components/Scrollspy/SectionWrapper';
import { SectionHeading } from '@/components/ui/SectionHeading';
import { AnalyzeOwnerContext } from '@/components/Context/Analyze/AnalyzeOwner';
import * as React from 'react';
import { useMemo, useState } from 'react';
Expand All @@ -22,17 +23,17 @@ export default function BehaviourSection () {
const { id: userId } = React.useContext(AnalyzeOwnerContext);

return (
<SectionTemplate id="behaviour" title="Behaviour" level={2} className="pt-8"
description="You can see the total contributions in different repositories since 2011, as well as check the status of different contribution categories type by type."
>
<ScrollspySectionWrapper anchor="behaviour" className="pt-8 pb-8">
<SectionHeading>Behaviour</SectionHeading>
<p className="text-sm text-[#8c8c8c] mb-4">You can see the total contributions in different repositories since 2011, as well as check the status of different contribution categories type by type.</p>
<AllContributions userId={userId} />
<ContributionTime userId={userId} />
</SectionTemplate>
</ScrollspySectionWrapper>
);
}

function AllContributions ({ userId }: { userId: number }) {
const { data, loading } = usePersonalData('personal-contributions-for-repos', userId);
const { data, sql, queryName, loading } = usePersonalData('personal-contributions-for-repos', userId);

const { repos, seriesList } = useMemo(() => {
if (!data || data.length === 0) return { repos: [], seriesList: [] };
Expand Down Expand Up @@ -79,7 +80,9 @@ function AllContributions ({ userId }: { userId: number }) {
series: seriesList,
};

return <PersonalChart title="Type of total contributions" option={option} loading={loading} noData={repos.length === 0} />;
const queryParams = useMemo(() => ({ userId }), [userId]);

return <PersonalChart title="Type of total contributions" option={option} loading={loading} noData={repos.length === 0} sql={sql} queryName={queryName} queryParams={queryParams} />;
}

function ContributionTime ({ userId }: { userId: number }) {
Expand All @@ -93,7 +96,7 @@ function ContributionTime ({ userId }: { userId: number }) {

return (
<div className="mb-4">
<h3 className="text-sm font-medium text-[#dadada] mb-3">{title}</h3>
<h3 className="text-[18px] font-semibold text-[#e9eaee] mb-3">{title}</h3>
<div className="flex flex-wrap gap-3 mb-4">
<Select label="Period" value={period} onChange={setPeriod} options={periods.map(p => ({ value: p, label: toCamel(p) }))} />
<Select label="Contribution Type" value={type} onChange={setType} options={eventTypes.map(e => ({ value: e, label: toCamel(e) }))} />
Expand All @@ -110,7 +113,7 @@ function Select ({ label, value, onChange, options }: { label: string; value: st
return (
<label className="flex flex-col gap-1 text-xs text-[#8c8c8c]">
{label}
<select value={value} onChange={e => onChange(e.target.value)} className="rounded border border-[#363638] bg-[#2e2e2f] px-2 py-1 text-sm text-[#e0e0e0] outline-none focus:border-[#fbe593]">
<select value={value} onChange={e => onChange(e.target.value)} className="rounded border border-[#363638] bg-[#2e2e2f] px-2 py-1 text-sm text-[#e0e0e0] outline-none focus:border-white">
{options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
</label>
Expand Down
29 changes: 14 additions & 15 deletions apps/web/app/analyze-user/[login]/_sections/code-review.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,34 @@
'use client';

import SectionTemplate from '@/components/Analyze/Section';
import { ScrollspySectionWrapper } from '@/components/Scrollspy/SectionWrapper';
import { SectionHeading } from '@/components/ui/SectionHeading';
import { AnalyzeOwnerContext } from '@/components/Context/Analyze/AnalyzeOwner';
import * as React from 'react';
import { useMemo } from 'react';
import PersonalChart from '../_charts/ChartWrapper';
import { orange, primary } from '../_charts/colors';
import { createTimeSeriesOption } from '../_charts/createChartOption';
import { usePersonalData } from '../_hooks/usePersonal';

export default function CodeReviewSection () {
const { id: userId } = React.useContext(AnalyzeOwnerContext);

return (
<SectionTemplate id="code-review" title="Code Review" level={2} className="pt-8"
description="The history about the number of code review times and comments in pull requests since 2011."
>
<ScrollspySectionWrapper anchor="code-review" className="pt-8 pb-8">
<SectionHeading>Code Review</SectionHeading>
<CodeReviewHistory userId={userId} />
</SectionTemplate>
</ScrollspySectionWrapper>
);
}

function CodeReviewHistory ({ userId }: { userId: number }) {
const { data, loading } = usePersonalData('personal-pull-request-reviews-history', userId);
const option = useMemo(() => ({
xAxis: { type: 'time' as const, min: '2011-01-01' },
yAxis: { type: 'value' as const },
series: [
{ type: 'bar' as const, data: (data ?? []).map(r => [r.event_month, r.reviews]), name: 'review', color: orange, barMaxWidth: 10 },
{ type: 'bar' as const, data: (data ?? []).map(r => [r.event_month, r.review_comments]), name: 'review comments', color: primary, barMaxWidth: 10 },
],
}), [data]);
const { data, sql, queryName, loading } = usePersonalData('personal-pull-request-reviews-history', userId);
const option = useMemo(() => createTimeSeriesOption([
{ type: 'bar' as const, data: (data ?? []).map(r => [r.event_month, r.reviews]), name: 'review', color: orange, barMaxWidth: 10 },
{ type: 'bar' as const, data: (data ?? []).map(r => [r.event_month, r.review_comments]), name: 'review comments', color: primary, barMaxWidth: 10 },
]), [data]);

return <PersonalChart title="Code Review History" option={option} loading={loading} noData={!loading && (!data || data.length === 0)} />;
const queryParams = useMemo(() => ({ userId }), [userId]);

return <PersonalChart title="Code Review History" option={option} loading={loading} noData={!loading && (!data || data.length === 0)} sql={sql} queryName={queryName} queryParams={queryParams} />;
}
Loading
Loading