Skip to content

Commit

Permalink
feat: add repos/contributors treemap to list activity page (#1853)
Browse files Browse the repository at this point in the history
  • Loading branch information
nickytonline authored Oct 11, 2023
1 parent 5344c59 commit 70a67f0
Show file tree
Hide file tree
Showing 11 changed files with 253 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useSpring, animated } from "@react-spring/web";
import dynamic from "next/dynamic";
import Card from "components/atoms/Card/card";
import { SpecialNode } from "stories/molecules/treemap-prototype/special-node";
import { ContributorNode } from "stories/molecules/treemap-prototype/contributor-node";
import ClientOnly from "components/atoms/ClientOnly/client-only";
import type { NodeMouseEventHandler, NodeProps } from "@nivo/treemap";

interface ContributionsTreemapProps {
data: any;
color: string;
onClick: NodeMouseEventHandler<object>;
repoId: number | null;
setRepoId: (repoId: number | null) => void;
}

function BreadCrumb({ isActive, ...rest }: any) {
const separatorStyle = useSpring(isActive ? { opacity: 1 } : { opacity: 0 });
const textStyle = useSpring(isActive ? { opacity: 1, translateX: 0 } : { opacity: 0, translateX: 100 });

return (
<>
<animated.div className={"px-1"} style={separatorStyle}>
{"/"}
</animated.div>
<animated.div style={textStyle} {...rest} />
</>
);
}

const ResponsiveTreeMapHtml = dynamic(() => import("@nivo/treemap").then((module) => module.ResponsiveTreeMapHtml));

export const ContributionsTreemap = ({ setRepoId, repoId, data, color, onClick }: ContributionsTreemapProps) => {
return (
<Card className="grid place-content-stretch">
<div className="grid">
{/* Label: Text */}
<div className="text-lg text-slate-900 mb-2 flex">
<button className="cursor-pointer" onClick={() => setRepoId(null)}>
Repos
</button>
<div> </div>
<BreadCrumb isActive={repoId !== null}>Contributors</BreadCrumb>
</div>
<div className="rounded-md overflow-hidden grid place-content-stretch">
<div className="grid" style={{ gridArea: "1 / 1" }}>
<ClientOnly>
<ResponsiveTreeMapHtml
data={data}
tile="squarify"
labelSkipSize={12}
innerPadding={4}
leavesOnly
orientLabel={false}
nodeComponent={
repoId === null
? SpecialNode
: // TODO: Sort this out later
(ContributorNode as <Datum extends object>({
node,
animatedProps,
borderWidth,
enableLabel,
labelSkipSize,
}: NodeProps<Datum>) => JSX.Element)
}
colors={color}
nodeOpacity={1}
borderWidth={0}
onClick={onClick}
motionConfig={"default"}
/>
</ClientOnly>
</div>
</div>
</div>
</Card>
);
};
2 changes: 1 addition & 1 deletion helpers/fetchApiData.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export function validateListPath(path: string) {
// ()
const regex =
/^lists\/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\/(contributors|stats\/(most-active-contributors|contributions-evolution-by-type))))\/??/;
/^lists\/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\/(contributors|stats\/(most-active-contributors|contributions-evolution-by-type|contributions-by-project))))\/??/;

return regex.test(path);
}
Expand Down
27 changes: 27 additions & 0 deletions lib/hooks/api/useContributionsByProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import useSWR, { Fetcher } from "swr";
import publicApiFetcher from "lib/utils/public-api-fetcher";

export const useContributionsByProject = ({
listId,
range,
initialData,
}: {
listId: string;
range: number;
initialData?: DbProjectContributions[];
}) => {
const { data, error } = useSWR<DbProjectContributions[]>(
`lists/${listId}/stats/contributions-by-project?range=${range}`,
publicApiFetcher as Fetcher<DbProjectContributions[], Error>
// {
// fallbackData: {
// `lists/${listId}/stats/contributions-by-project?range=${range}`: initialData
// },
// }
);

return {
data,
error,
};
};
18 changes: 18 additions & 0 deletions lib/hooks/api/useContributorsByProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import useSWR, { Fetcher } from "swr";
import { useState } from "react";
import publicApiFetcher from "lib/utils/public-api-fetcher";

export const useContributorsByProject = (listId: string, range: number) => {
const [repoId, setRepoId] = useState<number | null>(null);
const { data, error } = useSWR<DBProjectContributor[]>(
`lists/${listId}/stats/top-project-contributions-by-contributor?repoId=${repoId}&range=${range}`,
publicApiFetcher as Fetcher<DBProjectContributor[], Error>
);

return {
data,
error,
setRepoId,
repoId,
};
};
6 changes: 2 additions & 4 deletions lib/hooks/api/useMostActiveContributors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,15 @@ const useMostActiveContributors = ({
listId,
initData,
intialLimit = 20,
initialRange = 30,
range = 30,
defaultContributorType = "all",
}: {
listId: string;
initData?: ContributorStat[];
intialLimit?: number;
initialRange?: number;
range?: number;
defaultContributorType?: ContributorType;
}) => {
const [range, setRange] = useState(initialRange);
const [limit, setLimit] = useState(intialLimit);
const [contributorType, setContributorType] = useState<ContributorType>(defaultContributorType);

Expand All @@ -52,7 +51,6 @@ const useMostActiveContributors = ({
data,
isLoading: !error && !data,
isError: !!error,
setRange,
contributorType,
setContributorType,
};
Expand Down
26 changes: 26 additions & 0 deletions lib/utils/color-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export function stringToHSLAColor({
id,
saturation = 90,
lightness = 48,
alpha = 1,
}: {
id: string;
saturation?: number;
lightness?: number;
alpha?: number;
}) {
// Ensure valid values for saturation and lightness (0-100) and alpha (0-1)
saturation = Math.min(Math.max(saturation, 0), 100);
lightness = Math.min(Math.max(lightness, 0), 100);
alpha = Math.min(Math.max(alpha, 0), 1);

// Use a simple hashing algorithm to generate H, S, and L values
let hash = 0;
for (let i = 0; i < id.length; i++) {
hash = id.charCodeAt(i) + ((hash << 5) - hash);
}

const h = ((hash % 360) + 360) % 360; // Ensure H value is between 0 and 360

return `hsla(${h}, ${saturation}%, ${lightness}%, ${alpha})`;
}
18 changes: 18 additions & 0 deletions lib/utils/nivo-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { SpringValue, to } from "@react-spring/web";

/**
There are several utility functions in the @nico/* packages that are ESM only. If you are
unable to dynamically import them for usage in your project, you can copy the source code
here and use this instead.
You'll know if you need to do this if you see an error like this:
Error: require() of ES Module /Users/nicktaylor/dev/work/app/node_modules/@nivo/treemap/node_modules/d3-color/src/index.js from /Users/nicktaylor/dev/work/app/node_modules/@nivo/treemap/node_modules/@nivo/colors/dist/nivo-colors.cjs.js not supported.
Instead change the require of index.js in /Users/nicktaylor/dev/work/app/node_modules/@nivo/treemap/node_modules/@nivo/colors/dist/nivo-colors.cjs.js to a dynamic import() which is available in all CommonJS modules.
**/

// See https://github.com/plouc/nivo/blob/master/packages/treemap/src/transitions.ts#L6-L7
export function htmlNodeTransform(x: SpringValue<number>, y: SpringValue<number>) {
return to([x, y], (x, y) => `translate(${x}px, ${y}px)`);
}
15 changes: 15 additions & 0 deletions next-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,18 @@ interface DbListContributorStat {
commits: number;
prsCreated: number;
}
interface DbProjectContributions {
org_id: string;
project_id: string;
repo_id: number;
contributions: number;
}

interface DBProjectContributor {
login: string;
commits: number;
prs_created: number;
prs_reviewed: number;
issues_created: number;
comments: number;
}
56 changes: 53 additions & 3 deletions pages/lists/[listId]/activity.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { createPagesServerClient } from "@supabase/auth-helpers-nextjs";
import { GetServerSidePropsContext } from "next";
import { useState } from "react";
import { NodeMouseEventHandler } from "@nivo/treemap";
import Error from "components/atoms/Error/Error";
import { fetchApiData, validateListPath } from "helpers/fetchApiData";
import ListPageLayout from "layouts/lists";
import MostActiveContributorsCard, {
ContributorStat,
} from "components/molecules/MostActiveContributorsCard/most-active-contributors-card";

import useMostActiveContributors from "lib/hooks/api/useMostActiveContributors";
import ClientOnly from "components/atoms/ClientOnly/client-only";
import { ContributionsTreemap } from "components/molecules/ContributionsTreemap/contributions-treemap";
import { useContributorsByProject } from "lib/hooks/api/useContributorsByProject";
import { useContributionsByProject } from "lib/hooks/api/useContributionsByProject";

interface ContributorListPageProps {
list?: DBList;
Expand All @@ -17,6 +21,7 @@ interface ContributorListPageProps {
activityData: {
contributorStats: { data: ContributorStat[]; meta: Meta };
topContributor: ContributorStat;
projectData: DbProjectContributions[];
};
}

Expand All @@ -33,6 +38,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
{ data, error: contributorListError },
{ data: list, error },
{ data: mostActiveData, error: mostActiveError },
{ data: projectData, error: projectError },
] = await Promise.all([
fetchApiData<PagedData<DBListContributor>>({
path: `lists/${listId}/contributors?limit=1`,
Expand All @@ -45,6 +51,11 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
bearerToken,
pathValidator: validateListPath,
}),
fetchApiData<DbProjectContributions>({
path: `lists/${listId}/stats/contributions-by-project?range=${range}`,
bearerToken,
pathValidator: validateListPath,
}),
]);

if (error?.status === 404 || error?.status === 401) {
Expand All @@ -61,21 +72,53 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
activityData: {
contributorStats: mostActiveData,
topContributor: mostActiveData?.data?.length ? mostActiveData.data[0] : null,
projectData: projectData ?? [],
},
},
};
};

const ListActivityPage = ({ list, numberOfContributors, isError, activityData }: ContributorListPageProps) => {
const [range, setRange] = useState(30);
const isOwner = false;
const {
data: contributorStats,
isLoading,
isError: isMostActiveError,
setRange,
setContributorType,
contributorType,
} = useMostActiveContributors({ listId: list!.id, initData: activityData.contributorStats.data });
} = useMostActiveContributors({ listId: list!.id, initData: activityData.contributorStats.data, range });

const { setRepoId, error, data: projectContributionsByUser, repoId } = useContributorsByProject(list!.id, range);

const { data: projectData, error: projectDataError } = useContributionsByProject({
listId: list!.id,
range,
initialData: activityData.projectData,
});

const onHandleClick: NodeMouseEventHandler<object> = (node) => {
// @ts-ignore TODO: fix this
setRepoId(Number(node.data.repoId));
};
const treemapData = {
id: "root",
children:
repoId === null
? (projectData ?? []).map(({ org_id, project_id, repo_id, contributions }) => {
return {
id: `${org_id}/${project_id}`,
value: contributions,
repoId: `${repo_id}`,
};
})
: projectContributionsByUser?.map(({ login, commits, prs_created, prs_reviewed, issues_created, comments }) => {
return {
id: login,
value: commits + prs_created, // Coming soon + prs_reviewed + issues_created + comments,
};
}),
};

return (
<ListPageLayout list={list} numberOfContributors={numberOfContributors} isOwner={isOwner} setRange={setRange}>
Expand All @@ -94,6 +137,13 @@ const ListActivityPage = ({ list, numberOfContributors, isError, activityData }:
isLoading={isLoading}
/>
</ClientOnly>
<ContributionsTreemap
setRepoId={setRepoId}
repoId={repoId}
onClick={onHandleClick}
data={treemapData}
color="hsla(21, 90%, 48%, 1)"
/>
</div>
)}
</ListPageLayout>
Expand Down
Loading

0 comments on commit 70a67f0

Please sign in to comment.