Skip to content

Commit

Permalink
feat: activity widget (#8628)
Browse files Browse the repository at this point in the history
It is still raw, next PRs add styling and date filtering for only single
year.


![image](https://github.com/user-attachments/assets/8cd4e74f-3ed4-4179-a193-a45a191c4933)

---------

Co-authored-by: kwasniew <[email protected]>
  • Loading branch information
sjaanus and kwasniew authored Nov 5, 2024
1 parent bfa9e0d commit 1cedc37
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 1 deletion.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"react-dom": "18.3.1",
"react-dropzone": "14.2.10",
"react-error-boundary": "3.1.4",
"react-github-calendar": "^4.5.1",
"react-hooks-global-state": "2.1.0",
"react-joyride": "^2.5.3",
"react-markdown": "^8.0.4",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useProjectStatus } from 'hooks/api/getters/useProjectStatus/useProjectStatus';
import ActivityCalendar, { type ThemeInput } from 'react-activity-calendar';
import type { ProjectActivitySchema } from '../../../../openapi';
import { styled, Tooltip } from '@mui/material';

const StyledContainer = styled('div')(({ theme }) => ({
gap: theme.spacing(1),
}));

type Output = { date: string; count: number; level: number };

export function transformData(inputData: ProjectActivitySchema): Output[] {
const resultMap: Record<string, number> = {};

// Step 1: Count the occurrences of each date
inputData.forEach((item) => {
const formattedDate = new Date(item.date).toISOString().split('T')[0];
resultMap[formattedDate] = (resultMap[formattedDate] || 0) + 1;
});

// Step 2: Get all counts, sort them, and find the cut-off values for percentiles
const counts = Object.values(resultMap).sort((a, b) => a - b);

const percentile = (percent: number) => {
const index = Math.floor((percent / 100) * counts.length);
return counts[index] || counts[counts.length - 1];
};

const thresholds = [
percentile(25), // 25th percentile
percentile(50), // 50th percentile
percentile(75), // 75th percentile
percentile(100), // 100th percentile
];

// Step 3: Assign a level based on the percentile thresholds
const calculateLevel = (count: number): number => {
if (count <= thresholds[0]) return 1; // 1-25%
if (count <= thresholds[1]) return 2; // 26-50%
if (count <= thresholds[2]) return 3; // 51-75%
return 4; // 76-100%
};

// Step 4: Convert the map back to an array and assign levels
return Object.entries(resultMap)
.map(([date, count]) => ({
date,
count,
level: calculateLevel(count),
}))
.reverse(); // Optional: reverse the order if needed
}

export const ProjectActivity = () => {
const projectId = useRequiredPathParam('projectId');
const { data } = useProjectStatus(projectId);

const explicitTheme: ThemeInput = {
light: ['#f1f0fc', '#ceccfd', '#8982ff', '#6c65e5', '#615bc2'],
dark: ['#f1f0fc', '#ceccfd', '#8982ff', '#6c65e5', '#615bc2'],
};

const levelledData = transformData(data.activityCountByDate);

return (
<StyledContainer>
{data.activityCountByDate.length > 0 ? (
<>
<span>Activity in project</span>
<ActivityCalendar
theme={explicitTheme}
data={levelledData}
maxLevel={4}
showWeekdayLabels={true}
renderBlock={(block, activity) => (
<Tooltip
title={`${activity.count} activities on ${activity.date}`}
>
{block}
</Tooltip>
)}
/>
</>
) : (
<span>No activity</span>
)}
</StyledContainer>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { styled } from '@mui/material';
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
import { ProjectResources } from './ProjectResources';
import { ProjectActivity } from './ProjectActivity';

const ModalContentContainer = styled('div')(({ theme }) => ({
minHeight: '100vh',
Expand All @@ -17,6 +18,7 @@ export const ProjectStatusModal = ({ open, close }: Props) => {
<SidebarModal open={open} onClose={close} label='Project status'>
<ModalContentContainer>
<ProjectResources />
<ProjectActivity />
</ModalContentContainer>
</SidebarModal>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const useProjectInsights = (projectId: string) => {
const projectPath = formatApiPath(path(projectId));
const { data, refetch, loading, error } =
useApiGetter<ProjectInsightsSchema>(projectPath, () =>
fetcher(projectPath, 'Outdated SDKs'),
fetcher(projectPath, 'Project Insights'),
);

return { data: data || placeholderData, refetch, loading, error };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { fetcher, useApiGetter } from '../useApiGetter/useApiGetter';
import type { ProjectStatusSchema } from '../../../../openapi';
import { formatApiPath } from 'utils/formatPath';

const path = (projectId: string) => `api/admin/projects/${projectId}/status`;

const placeholderData: ProjectStatusSchema = {
activityCountByDate: [],
};

export const useProjectStatus = (projectId: string) => {
const projectPath = formatApiPath(path(projectId));
const { data, refetch, loading, error } = useApiGetter<ProjectStatusSchema>(
projectPath,
() => fetcher(projectPath, 'Project Status'),
);

return { data: data || placeholderData, refetch, loading, error };
};
44 changes: 44 additions & 0 deletions frontend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4578,6 +4578,13 @@ __metadata:
languageName: node
linkType: hard

"date-fns@npm:^4.1.0":
version: 4.1.0
resolution: "date-fns@npm:4.1.0"
checksum: 10c0/b79ff32830e6b7faa009590af6ae0fb8c3fd9ffad46d930548fbb5acf473773b4712ae887e156ba91a7b3dc30591ce0f517d69fd83bd9c38650fdc03b4e0bac8
languageName: node
linkType: hard

"dayjs@npm:^1.10.4":
version: 1.11.11
resolution: "dayjs@npm:1.11.11"
Expand Down Expand Up @@ -8366,6 +8373,18 @@ __metadata:
languageName: node
linkType: hard

"react-activity-calendar@npm:^2.7.1":
version: 2.7.1
resolution: "react-activity-calendar@npm:2.7.1"
dependencies:
date-fns: "npm:^4.1.0"
peerDependencies:
react: ^18.0.0
react-dom: ^18.0.0
checksum: 10c0/2d4c9dd688c1187b75b8878094365ee7ef8cec361ac6a2a8088cf83296025c7849fe909487c415ce3e739642821800a6452f7ce4873b52270a3b47834ca2498e
languageName: node
linkType: hard

"react-archer@npm:4.4.0":
version: 4.4.0
resolution: "react-archer@npm:4.4.0"
Expand Down Expand Up @@ -8437,6 +8456,17 @@ __metadata:
languageName: node
linkType: hard

"react-error-boundary@npm:^4.1.2":
version: 4.1.2
resolution: "react-error-boundary@npm:4.1.2"
dependencies:
"@babel/runtime": "npm:^7.12.5"
peerDependencies:
react: ">=16.13.1"
checksum: 10c0/0737e5259bed40ce14eb0823b3c7b152171921f2179e604f48f3913490cdc594d6c22d43d7abb4ffb1512c832850228db07aa69d3b941db324953a5e393cb399
languageName: node
linkType: hard

"react-fast-compare@npm:^2.0.4":
version: 2.0.4
resolution: "react-fast-compare@npm:2.0.4"
Expand All @@ -8460,6 +8490,19 @@ __metadata:
languageName: node
linkType: hard

"react-github-calendar@npm:^4.5.1":
version: 4.5.1
resolution: "react-github-calendar@npm:4.5.1"
dependencies:
react-activity-calendar: "npm:^2.7.1"
react-error-boundary: "npm:^4.1.2"
peerDependencies:
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
checksum: 10c0/51688983b28da92718cf2276e6c947b9639b529a93212d77a6c65040af2768b74153b32dee44eed4f706cf0cb4c025864327c51c9c666d1f5b88080f523ef672
languageName: node
linkType: hard

"react-hooks-global-state@npm:2.1.0":
version: 2.1.0
resolution: "react-hooks-global-state@npm:2.1.0"
Expand Down Expand Up @@ -10152,6 +10195,7 @@ __metadata:
react-dom: "npm:18.3.1"
react-dropzone: "npm:14.2.10"
react-error-boundary: "npm:3.1.4"
react-github-calendar: "npm:^4.5.1"
react-hooks-global-state: "npm:2.1.0"
react-joyride: "npm:^2.5.3"
react-markdown: "npm:^8.0.4"
Expand Down

0 comments on commit 1cedc37

Please sign in to comment.