mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
feat: activity widget (#8628)
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 <kwasniewski.mateusz@gmail.com>
This commit is contained in:
parent
bfa9e0d6b4
commit
1cedc374c1
@ -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",
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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',
|
||||
@ -17,6 +18,7 @@ export const ProjectStatusModal = ({ open, close }: Props) => {
|
||||
<SidebarModal open={open} onClose={close} label='Project status'>
|
||||
<ModalContentContainer>
|
||||
<ProjectResources />
|
||||
<ProjectActivity />
|
||||
</ModalContentContainer>
|
||||
</SidebarModal>
|
||||
);
|
||||
|
@ -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 };
|
||||
|
@ -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 };
|
||||
};
|
@ -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"
|
||||
@ -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"
|
||||
@ -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"
|
||||
@ -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"
|
||||
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user