mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-18 01:18:23 +02:00
feat: activity widget (#8628)
It is still raw, next PRs add styling and date filtering for only single year.  --------- 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-dom": "18.3.1",
|
||||||
"react-dropzone": "14.2.10",
|
"react-dropzone": "14.2.10",
|
||||||
"react-error-boundary": "3.1.4",
|
"react-error-boundary": "3.1.4",
|
||||||
|
"react-github-calendar": "^4.5.1",
|
||||||
"react-hooks-global-state": "2.1.0",
|
"react-hooks-global-state": "2.1.0",
|
||||||
"react-joyride": "^2.5.3",
|
"react-joyride": "^2.5.3",
|
||||||
"react-markdown": "^8.0.4",
|
"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 { styled } from '@mui/material';
|
||||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||||
import { ProjectResources } from './ProjectResources';
|
import { ProjectResources } from './ProjectResources';
|
||||||
|
import { ProjectActivity } from './ProjectActivity';
|
||||||
|
|
||||||
const ModalContentContainer = styled('div')(({ theme }) => ({
|
const ModalContentContainer = styled('div')(({ theme }) => ({
|
||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
@ -17,6 +18,7 @@ export const ProjectStatusModal = ({ open, close }: Props) => {
|
|||||||
<SidebarModal open={open} onClose={close} label='Project status'>
|
<SidebarModal open={open} onClose={close} label='Project status'>
|
||||||
<ModalContentContainer>
|
<ModalContentContainer>
|
||||||
<ProjectResources />
|
<ProjectResources />
|
||||||
|
<ProjectActivity />
|
||||||
</ModalContentContainer>
|
</ModalContentContainer>
|
||||||
</SidebarModal>
|
</SidebarModal>
|
||||||
);
|
);
|
||||||
|
@ -49,7 +49,7 @@ export const useProjectInsights = (projectId: string) => {
|
|||||||
const projectPath = formatApiPath(path(projectId));
|
const projectPath = formatApiPath(path(projectId));
|
||||||
const { data, refetch, loading, error } =
|
const { data, refetch, loading, error } =
|
||||||
useApiGetter<ProjectInsightsSchema>(projectPath, () =>
|
useApiGetter<ProjectInsightsSchema>(projectPath, () =>
|
||||||
fetcher(projectPath, 'Outdated SDKs'),
|
fetcher(projectPath, 'Project Insights'),
|
||||||
);
|
);
|
||||||
|
|
||||||
return { data: data || placeholderData, refetch, loading, error };
|
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
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"dayjs@npm:^1.10.4":
|
||||||
version: 1.11.11
|
version: 1.11.11
|
||||||
resolution: "dayjs@npm:1.11.11"
|
resolution: "dayjs@npm:1.11.11"
|
||||||
@ -8366,6 +8373,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"react-archer@npm:4.4.0":
|
||||||
version: 4.4.0
|
version: 4.4.0
|
||||||
resolution: "react-archer@npm:4.4.0"
|
resolution: "react-archer@npm:4.4.0"
|
||||||
@ -8437,6 +8456,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"react-fast-compare@npm:^2.0.4":
|
||||||
version: 2.0.4
|
version: 2.0.4
|
||||||
resolution: "react-fast-compare@npm:2.0.4"
|
resolution: "react-fast-compare@npm:2.0.4"
|
||||||
@ -8460,6 +8490,19 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"react-hooks-global-state@npm:2.1.0":
|
||||||
version: 2.1.0
|
version: 2.1.0
|
||||||
resolution: "react-hooks-global-state@npm:2.1.0"
|
resolution: "react-hooks-global-state@npm:2.1.0"
|
||||||
@ -10152,6 +10195,7 @@ __metadata:
|
|||||||
react-dom: "npm:18.3.1"
|
react-dom: "npm:18.3.1"
|
||||||
react-dropzone: "npm:14.2.10"
|
react-dropzone: "npm:14.2.10"
|
||||||
react-error-boundary: "npm:3.1.4"
|
react-error-boundary: "npm:3.1.4"
|
||||||
|
react-github-calendar: "npm:^4.5.1"
|
||||||
react-hooks-global-state: "npm:2.1.0"
|
react-hooks-global-state: "npm:2.1.0"
|
||||||
react-joyride: "npm:^2.5.3"
|
react-joyride: "npm:^2.5.3"
|
||||||
react-markdown: "npm:^8.0.4"
|
react-markdown: "npm:^8.0.4"
|
||||||
|
Loading…
Reference in New Issue
Block a user