Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions web-server/pages/api/internal/team/[team_id]/dora_metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { endOfDay, startOfDay } from 'date-fns';
import * as yup from 'yup';

import { getTeamRepos } from '@/api/resources/team_repos';
import { getBookmarkedRepos } from '@/api/resources/teams/[team_id]/bookmarked_repos';
import { Endpoint } from '@/api-helpers/global';
import {
repoFiltersFromTeamProdBranches,
Expand Down Expand Up @@ -48,8 +49,10 @@ endpoint.handle.GET(getSchema, async (req, res) => {
branches
} = req.payload;

const teamProdBranchesMap =
await getAllTeamsReposProdBranchesForOrgAsMap(org_id);
const [teamProdBranchesMap, bookmarkedRepos] = await Promise.all([
getAllTeamsReposProdBranchesForOrgAsMap(org_id),
getBookmarkedRepos(teamId)
]);
const teamRepoFiltersMap =
repoFiltersFromTeamProdBranches(teamProdBranchesMap);

Expand Down Expand Up @@ -169,7 +172,8 @@ endpoint.handle.GET(getSchema, async (req, res) => {
deployment_frequency_trends:
deploymentFrequencyResponse.deployment_frequency_trends,
lead_time_prs: leadtimePrs,
assigned_repos: teamRepos
assigned_repos: teamRepos,
bookmarked_repos: bookmarkedRepos
} as TeamDoraMetricsApiResponseType);
});

Expand Down
40 changes: 40 additions & 0 deletions web-server/pages/api/resources/teams/[team_id]/bookmarked_repos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as yup from 'yup';

import { getTeamRepos } from '@/api/resources/team_repos';
import { Endpoint, nullSchema } from '@/api-helpers/global';
import { Table } from '@/constants/db';
import { uuid } from '@/utils/datatype';
import { db } from '@/utils/db';

const pathSchema = yup.object().shape({
team_id: yup.string().uuid().required()
});

const endpoint = new Endpoint(pathSchema);

endpoint.handle.GET(nullSchema, async (req, res) => {
if (req.meta?.features?.use_mock_data) {
return res.send([uuid(), uuid()]);
}

res.send(await getBookmarkedRepos(req.payload.team_id));
});

export const getBookmarkedRepos = async (teamId?: ID) => {
const query = db(Table.Bookmark).select('repo_id');

if (!teamId)
return (await query.then((res) =>
res.map((item) => item?.repo_id)
)) as ID[];

const teamRepoIds = await getTeamRepos(teamId).then((res) =>
res.map((repo) => repo.id)
);

return (await query
.whereIn('repo_id', teamRepoIds)
.then((res) => res.map((item) => item?.repo_id))) as ID[];
};

export default endpoint.serve();
2 changes: 0 additions & 2 deletions web-server/pages/dora-metrics/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Chip } from '@mui/material';
import ExtendedSidebarLayout from 'src/layouts/ExtendedSidebarLayout';

import { Authenticated } from '@/components/Authenticated';
Expand All @@ -11,7 +10,6 @@ import { PageWrapper } from '@/content/PullRequests/PageWrapper';
import { useAuth } from '@/hooks/useAuth';
import { useSelector } from '@/store';
import { PageLayout } from '@/types/resources';

function Page() {
useRedirectWithSession();
const isLoading = useSelector(
Expand Down
4 changes: 2 additions & 2 deletions web-server/src/components/Teams/CreateTeams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ const TeamsCRUD: FC<CRUDProps> = ({
);
};

export const Loader = () => {
export const Loader: FC<{ label?: string }> = ({ label = 'Loading...' }) => {
return (
<FlexBox alignCenter gap2>
<CircularProgress size="20px" />
<Line>Loading...</Line>
<Line>{label}</Line>
</FlexBox>
);
};
Expand Down
139 changes: 134 additions & 5 deletions web-server/src/content/DoraMetrics/DoraMetricsBody.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Grid, Divider, Button } from '@mui/material';
import Link from 'next/link';
import { useEffect } from 'react';
import { useEffect, useMemo } from 'react';

import { DoraMetricsConfigurationSettings } from '@/components/DoraMetricsConfigurationSettings';
import { DoraScore } from '@/components/DoraScore';
Expand All @@ -14,13 +14,16 @@ import { ROUTES } from '@/constants/routes';
import { FetchState } from '@/constants/ui-states';
import { useDoraStats } from '@/content/DoraMetrics/DoraCards/sharedHooks';
import { useAuth } from '@/hooks/useAuth';
import { useBoolState, useEasyState } from '@/hooks/useEasyState';
import { usePageRefreshCallback } from '@/hooks/usePageRefreshCallback';
import {
useSingleTeamConfig,
useStateBranchConfig
} from '@/hooks/useStateTeamConfig';
import { fetchTeamDoraMetrics } from '@/slices/dora_metrics';
import { useDispatch, useSelector } from '@/store';
import { ActiveBranchMode } from '@/types/resources';
import { depFn } from '@/utils/fn';
import { getRandomLoadMsg } from '@/utils/loading-messages';

import { ClassificationPills } from './ClassificationPills';
Expand Down Expand Up @@ -89,7 +92,7 @@ export const DoraMetricsBody = () => {

const stats = useDoraStats();

const { isFreshOrg } = useFreshOrgCalculator();
const { isSyncing } = useSyncedRepos();

if (isErrored)
return (
Expand All @@ -100,12 +103,18 @@ export const DoraMetricsBody = () => {
);
if (!firstLoadDone) return <MiniLoader label={getRandomLoadMsg()} />;
if (isTeamInsightsEmpty)
if (isFreshOrg)
if (isSyncing)
return (
<EmptyState
type="SYNC_IN_PROGRESS"
title="Sync in progress"
desc="Your data is syncing. Please reload in a few minutes."
desc={
<FlexBox gap={1}>
<MiniLoader
label={'Your repos are syncing, please wait for a few minutes'}
/>
</FlexBox>
}
>
<FixedContentRefreshLoader show={isLoading} />
</EmptyState>
Expand Down Expand Up @@ -135,7 +144,8 @@ export const DoraMetricsBody = () => {
<DoraMetricsConfigurationSettings />
</FlexBox>
</FlexBox>
<Divider sx={{ mt: 2 }} />
<Syncing />
<Divider />
<Grid container spacing={4}>
<Grid item xs={12} md={6} order={1}>
<ChangeTimeCard />
Expand Down Expand Up @@ -184,3 +194,122 @@ export const calculateIsFreshOrg = (createdAt: string | Date): boolean => {

return timeDiffMs <= FRESH_ORG_THRESHOLD * 60 * 1000;
};

export const useSyncedRepos = () => {
const pageRefreshCallback = usePageRefreshCallback();

const reposMap = useSelector((s) => s.team.teamReposMaps);
const { singleTeamId } = useSingleTeamConfig();
const syncedRepos = useSelector((s) => s.doraMetrics.bookmarkedRepos);

const isSyncing = useMemo(() => {
const teamRepos = reposMap[singleTeamId] || [];
return !teamRepos.every((repo) => syncedRepos.includes(repo.id));
}, [reposMap, singleTeamId, syncedRepos]);

useEffect(() => {
if (!isSyncing) return;

const interval = setInterval(() => {
pageRefreshCallback();
}, 10_000);

return () => clearInterval(interval);
}, [isSyncing, pageRefreshCallback]);

return {
isSyncing,
syncedRepos,
teamRepos: reposMap[singleTeamId] || []
};
};

const ANIMATON_DURATION = 700;

const Syncing = () => {
const flickerAnimation = useBoolState(false);
const { isSyncing } = useSyncedRepos();

useEffect(() => {
if (!isSyncing) return;
const interval = setInterval(() => {
depFn(flickerAnimation.toggle);
}, ANIMATON_DURATION);
return () => clearInterval(interval);
}, [isSyncing, flickerAnimation.toggle]);

return (
<FlexBox
gap={4}
sx={{
height: isSyncing ? '66px' : '0px',
opacity: flickerAnimation.value ? 1 : 0.6,
transition: `all ${isSyncing ? ANIMATON_DURATION : 200}ms linear`
}}
overflow={'hidden'}
>
<FlexBox
relative
gap={2}
alignCenter
p={2}
borderRadius={'8px'}
overflow={'hidden'}
flex={1}
>
<FlexBox
bgcolor={'#14AE5C'}
sx={{ opacity: 0.1 }}
position={'absolute'}
left={0}
top={0}
fullWidth
height={'100%'}
/>
<FlexBox height={'25px'} centered>
{isSyncing && <LoadingWrapper />}
</FlexBox>
<Line color={'#1AE579'} medium big>
Calculating Dora
</Line>
<Line>We’re processing your data, it usually takes ~ 5 mins</Line>
</FlexBox>
<FlexBox p={2} flex1 />
</FlexBox>
);
};

const LoadingWrapper = () => {
const rotation = useEasyState(0);
useEffect(() => {
const interval = setInterval(() => {
rotation.set((r) => r + 1);
}, 10);
return () => clearInterval(interval);
}, [rotation]);
return (
<FlexBox
sx={{
transform: `rotate(${rotation.value}deg)`,
transition: 'transform 100ms linear',
borderRadius: '50%'
}}
>
<svg
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.0002 2.50195V5.00195M21.2502 4.17695L20.0002 6.34195M25.8252 8.75195L23.6602 10.002M27.5002 15.002H25.0002M25.8252 21.252L23.6602 20.002M21.2502 25.827L20.0002 23.662M15.0002 27.502V25.002M8.75024 25.827L10.0002 23.662M4.17524 21.252L6.34024 20.002M2.50024 15.002H5.00024M4.17524 8.75195L6.34024 10.002M8.75024 4.17695L10.0002 6.34195"
stroke="#1AE579"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</FlexBox>
);
};
5 changes: 4 additions & 1 deletion web-server/src/content/PullRequests/PageWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const PageWrapper: FC<{
headerChildren?: ReactNode;
isLoading?: boolean;
showEvenIfNoTeamSelected?: boolean;
additionalFilters?: ReactNode[];
}> = ({
title = 'Collaborate',
hideAllSelectors,
Expand All @@ -27,7 +28,8 @@ export const PageWrapper: FC<{
teamDateSelectorMode,
headerChildren,
isLoading,
showEvenIfNoTeamSelected = false
showEvenIfNoTeamSelected = false,
additionalFilters = []
}) => {
const { noTeamSelected } = useSingleTeamConfig();
// TODO: use fetchState
Expand All @@ -45,6 +47,7 @@ export const PageWrapper: FC<{
teamDateSelectorMode={
teamDateSelectorMode || (showDate ? 'single' : 'single-only')
}
additionalFilters={additionalFilters}
selectBranch
>
{headerChildren}
Expand Down
5 changes: 4 additions & 1 deletion web-server/src/slices/dora_metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type State = StateFetchConfig<{
deployments_map: Record<string, Deployment>;
revert_prs: PR[];
summary_prs: PR[];
bookmarkedRepos: ID[];
}>;

const initialState: State = {
Expand All @@ -56,7 +57,8 @@ const initialState: State = {
prs_map: {},
deployments_map: {},
revert_prs: [],
summary_prs: []
summary_prs: [],
bookmarkedRepos: []
};

export const doraMetricsSlice = createSlice({
Expand Down Expand Up @@ -99,6 +101,7 @@ export const doraMetricsSlice = createSlice({
);
state.allReposAssignedToTeam = action.payload.assigned_repos;
state.summary_prs = action.payload.lead_time_prs;
state.bookmarkedRepos = action.payload.bookmarked_repos;
}
);
addFetchCasesToReducer(
Expand Down
2 changes: 1 addition & 1 deletion web-server/src/slices/team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const initialState: State = {
teamReposProductionBranches: [],
teamIncidentFilters: null,
excludedPrs: [],
teamReposMaps: null
teamReposMaps: {}
};

export const teamSlice = createSlice({
Expand Down
1 change: 1 addition & 0 deletions web-server/src/types/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@ export type TeamDoraMetricsApiResponseType = {
};
lead_time_prs: PR[];
assigned_repos: (Row<'TeamRepos'> & Row<'OrgRepo'>)[];
bookmarked_repos: ID[];
};

export enum ActiveBranchMode {
Expand Down