mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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-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