From 0feb087fa25a59d9b9fef9c551e0d1dfae1ab414 Mon Sep 17 00:00:00 2001 From: shivam-bit Date: Mon, 22 Apr 2024 15:27:48 +0530 Subject: [PATCH 1/6] Refactor team creation and update logic in v2.ts and team_repos.ts --- .../api/resources/orgs/[org_id]/teams/v2.ts | 101 +++++++++--------- web-server/pages/api/resources/team_repos.ts | 11 +- web-server/src/types/resources.ts | 2 + 3 files changed, 55 insertions(+), 59 deletions(-) diff --git a/web-server/pages/api/resources/orgs/[org_id]/teams/v2.ts b/web-server/pages/api/resources/orgs/[org_id]/teams/v2.ts index 51b89d949..aa15c4cec 100644 --- a/web-server/pages/api/resources/orgs/[org_id]/teams/v2.ts +++ b/web-server/pages/api/resources/orgs/[org_id]/teams/v2.ts @@ -1,7 +1,6 @@ import { groupBy, prop, mapObjIndexed, forEachObjIndexed } from 'ramda'; import * as yup from 'yup'; -import { enableOrgReposIfNotEnabled } from '@/api/integrations/orgs'; import { CodeSourceProvidersIntegration, getProviderOrgs, @@ -12,12 +11,14 @@ import { updateOnBoardingState } from '@/api/resources/orgs/[org_id]/onboarding'; import { getTeamRepos } from '@/api/resources/team_repos'; +import { handleRequest } from '@/api-helpers/axios'; +import { handleApi } from '@/api-helpers/axios-api-instance'; import { Endpoint } from '@/api-helpers/global'; -import { Columns, Table } from '@/constants/db'; +import { Columns, Table, Row } from '@/constants/db'; import { Integration } from '@/constants/integrations'; import { getTeamV2Mock } from '@/mocks/teams'; import { BaseTeam } from '@/types/api/teams'; -import { OnboardingStep, ReqOrgRepo } from '@/types/resources'; +import { OnboardingStep, ReqRepoWithProvider } from '@/types/resources'; import { db, getFirstRow } from '@/utils/db'; const getSchema = yup.object().shape({ @@ -111,29 +112,39 @@ endpoint.handle.POST(postSchema, async (req, res) => { } const { org_repos, org_id, provider, name } = req.payload; - const orgReposList: ReqOrgRepo[] = []; + const orgReposList: ReqRepoWithProvider[] = []; forEachObjIndexed( - (repos, org) => orgReposList.push({ org, repos }), + (repos, org) => + orgReposList.push({ + ...repos, + org, + provider + } as any as ReqRepoWithProvider), org_repos ); - const [team, repos, onboardingState] = await Promise.all([ + const [team, onboardingState] = await Promise.all([ createTeam(org_id, name, []), - enableOrgReposIfNotEnabled(org_id, provider as Integration, orgReposList), getOnBoardingState(org_id) ]); + const updatedOnboardingState = Array.from( new Set(onboardingState.onboarding_state).add(OnboardingStep.TEAM_CREATED) ); const [teamRepos] = await Promise.all([ - addReposToTeam( - team.id, - repos.map((r) => r.id) - ), + handleApi<(Row<'TeamRepos'> & Row<'OrgRepo'>)[]>( + `/teams/${team.id}/repos`, + { + method: 'PUT', + data: { + repos: orgReposList + } + } + ).then((repos) => repos.map((r) => ({ ...r, team_id: team.id }))), updateOnBoardingState(org_id, updatedOnboardingState) ]); - res.send({ team, teamReposMap: groupBy(prop('team_id'), teamRepos), repos }); + res.send({ team, teamReposMap: groupBy(prop('team_id'), teamRepos) }); }); endpoint.handle.PATCH(patchSchema, async (req, res) => { @@ -141,32 +152,27 @@ endpoint.handle.PATCH(patchSchema, async (req, res) => { return res.send(getTeamV2Mock); } - const { org_id, id, name, org_repos, provider } = req.payload; - const orgReposList: ReqOrgRepo[] = []; + const { id, name, org_repos, provider } = req.payload; + const orgReposList: ReqRepoWithProvider[] = []; forEachObjIndexed( - (repos, org) => orgReposList.push({ org, repos }), + (repos, org) => + orgReposList.push({ + ...repos, + org, + provider + } as any as ReqRepoWithProvider), org_repos ); - const upsertPayload: Record = { - id, - org_id, - updated_at: new Date(), - manager_id: null - }; - - if (name) { - upsertPayload.name = name; - } - - const [team, repos] = await Promise.all([ + const [team, teamRepos] = await Promise.all([ updateTeam(id, name, []), - enableOrgReposIfNotEnabled(org_id, provider as Integration, orgReposList) + handleApi<(Row<'TeamRepos'> & Row<'OrgRepo'>)[]>(`/teams/${id}/repos`, { + method: 'PUT', + data: { + repos: orgReposList + } + }).then((repos) => repos.map((r) => ({ ...r, team_id: id }))) ]); - const teamRepos = await addReposToTeam( - id, - repos.map((r) => r.id) - ); res.send({ team, teamReposMap: groupBy(prop('team_id'), teamRepos) }); }); @@ -193,18 +199,13 @@ const updateTeam = async ( team_name: string, member_ids: string[] ): Promise => { - return db('Team') - .insert({ - id: team_id, + return handleRequest(`/team/${team_id}`, { + method: 'PUT', + data: { name: team_name, - member_ids: member_ids, - is_deleted: false, - updated_at: new Date() - }) - .onConflict('id') - .merge() - .returning('*') - .then(getFirstRow); + member_ids + } + }); }; const createTeam = async ( @@ -212,15 +213,13 @@ const createTeam = async ( team_name: string, member_ids: string[] ): Promise => { - return db(Table.Team) - .insert({ - org_id, + return handleRequest(`/org/${org_id}/teams`, { + method: 'POST', + data: { name: team_name, - member_ids: member_ids, - is_deleted: false - }) - .returning('*') - .then(getFirstRow); + member_ids + } + }); }; const addReposToTeam = async (team_id: ID, repo_ids: ID[]) => { diff --git a/web-server/pages/api/resources/team_repos.ts b/web-server/pages/api/resources/team_repos.ts index 729c84e0b..7548be722 100644 --- a/web-server/pages/api/resources/team_repos.ts +++ b/web-server/pages/api/resources/team_repos.ts @@ -27,14 +27,9 @@ endpoint.handle.GET(getSchema, async (req, res) => { }); export const getTeamRepos = (team_id: ID) => - (team_id - ? db('TeamRepos') - .select('*', 'OrgRepo.* as org_repo') - .leftJoin('OrgRepo', 'OrgRepo.id', 'TeamRepos.org_repo_id') - .where('TeamRepos.is_active', true) - .andWhere('TeamRepos.team_id', team_id) - .orderBy('name', 'asc') - : []) as any as Promise<(Row<'TeamRepos'> & Row<'OrgRepo'>)[]>; + handleRequest<(Row<'TeamRepos'> & Row<'OrgRepo'>)[]>( + `/teams/${team_id}/repos` + ).then((repos) => repos.map((r) => ({ ...r, team_id: team_id }))); endpoint.handle.PATCH(patchSchema, async (req, res) => { if (req.meta?.features?.use_mock_data) { diff --git a/web-server/src/types/resources.ts b/web-server/src/types/resources.ts index c4b9442c8..c26ca40ce 100644 --- a/web-server/src/types/resources.ts +++ b/web-server/src/types/resources.ts @@ -961,6 +961,8 @@ export type ReqRepo = { slug: string; }; +export type ReqRepoWithProvider = ReqRepo & { provider: Integration }; + export type DoraPropsType = { count: number; bg: string; From bd73c158af9a55a398b5173040e8a0142f601627 Mon Sep 17 00:00:00 2001 From: shivam-bit Date: Tue, 23 Apr 2024 12:55:28 +0530 Subject: [PATCH 2/6] Refactor code to remove unused imports and simplify logic --- .../api/internal/[org_id]/git_provider_org.ts | 28 ++++--------------- .../internal/team/[team_id]/get_incidents.ts | 4 +-- .../src/components/Teams/useTeamsConfig.tsx | 2 +- 3 files changed, 8 insertions(+), 26 deletions(-) diff --git a/web-server/pages/api/internal/[org_id]/git_provider_org.ts b/web-server/pages/api/internal/[org_id]/git_provider_org.ts index 7d4af33f4..8f204e399 100644 --- a/web-server/pages/api/internal/[org_id]/git_provider_org.ts +++ b/web-server/pages/api/internal/[org_id]/git_provider_org.ts @@ -1,5 +1,4 @@ import { AxiosError } from 'axios'; -import { getTeamRepos } from 'pages/api/resources/team_repos'; import * as yup from 'yup'; import { internal } from '@/api-helpers/axios'; @@ -42,8 +41,7 @@ endpoint.handle.GET(getSchema, async (req, res) => { const response = await getRepos( org_id, provider as CodeSourceProvidersIntegration, - org_name, - team_id + org_name ); return res.status(200).send(response); }); @@ -72,25 +70,9 @@ export const getProviderOrgs = ( export const getRepos = async ( org_id: ID, provider: CodeSourceProvidersIntegration, - org_name: string, - team_id?: ID + org_name: string ) => { - let [repos, teamRepos] = await Promise.all([ - batchPaginatedListsRequest( - `/orgs/${org_id}/integrations/${provider}/${providerOrgBrandingMap[provider]}/${org_name}/repos` - ).then((rs) => rs.map(getBaseRepoFromUnionRepo)), - getTeamRepos(team_id) - ]); - - const teamReposSet = new Set( - teamRepos.map((tr) => `${tr.org_name}/${tr.name}`) - ); - - if (team_id) { - repos = repos.filter((repo) => - teamReposSet.has(`${repo.parent}/${repo.name}`) - ); - } - - return repos; + return await batchPaginatedListsRequest( + `/orgs/${org_id}/integrations/${provider}/${providerOrgBrandingMap[provider]}/${org_name}/repos` + ).then((rs) => rs.map(getBaseRepoFromUnionRepo)); }; diff --git a/web-server/pages/api/internal/team/[team_id]/get_incidents.ts b/web-server/pages/api/internal/team/[team_id]/get_incidents.ts index 24622cb8a..b3c19abb8 100644 --- a/web-server/pages/api/internal/team/[team_id]/get_incidents.ts +++ b/web-server/pages/api/internal/team/[team_id]/get_incidents.ts @@ -48,7 +48,7 @@ endpoint.handle.GET(getSchema, async (req, res) => { await getAllTeamsReposProdBranchesForOrgAsMap(org_id); const teamRepoFiltersMap = repoFiltersFromTeamProdBranches(teamProdBranchesMap); - const pr_filter = await updatePrFilterParams( + const prFilter = await updatePrFilterParams( team_id, {}, { @@ -63,7 +63,7 @@ endpoint.handle.GET(getSchema, async (req, res) => { team_id, from_date, to_date, - pr_filter + pr_filter: prFilter.pr_filter }); return res.send({ diff --git a/web-server/src/components/Teams/useTeamsConfig.tsx b/web-server/src/components/Teams/useTeamsConfig.tsx index cbaf306d9..a4863f650 100644 --- a/web-server/src/components/Teams/useTeamsConfig.tsx +++ b/web-server/src/components/Teams/useTeamsConfig.tsx @@ -103,7 +103,7 @@ export const TeamsCRUDProvider: React.FC<{ // team-repo selection logic const selections = useEasyState([]); - const repoOptions = orgRepos; + const repoOptions = orgRepos || []; const selectedRepos = selections.value; const teamRepoError = useBoolState(); const raiseTeamRepoError = useCallback(() => { From 8d6877185743053018e25ff6f347e0cc022facd4 Mon Sep 17 00:00:00 2001 From: shivam-bit Date: Tue, 23 Apr 2024 16:50:28 +0530 Subject: [PATCH 3/6] Fix null reference error in ResolvedIncidents.tsx --- web-server/src/content/DoraMetrics/ResolvedIncidents.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web-server/src/content/DoraMetrics/ResolvedIncidents.tsx b/web-server/src/content/DoraMetrics/ResolvedIncidents.tsx index 6bece0378..7e8c38a90 100644 --- a/web-server/src/content/DoraMetrics/ResolvedIncidents.tsx +++ b/web-server/src/content/DoraMetrics/ResolvedIncidents.tsx @@ -79,8 +79,8 @@ export const ResolvedIncidentsBody = () => { const { trendsSeriesMap } = useDoraMetricsGraph(); const isTrendsSeriesDataAvailable = head( - trendsSeriesMap.meanTimeToRestoreTrends - ).data.length; + trendsSeriesMap?.meanTimeToRestoreTrends || [] + )?.data?.length; if (isLoading || !team) return ; if (!incidents.length) From a30b6ccf80656b6a4892924e63172952b279e898 Mon Sep 17 00:00:00 2001 From: shivam-bit Date: Tue, 23 Apr 2024 17:28:39 +0530 Subject: [PATCH 4/6] Add INTERNAL_SYNC_API_BASE_URL to ambient.d.ts and update related code --- web-server/libdefs/ambient.d.ts | 1 + .../pages/api/internal/[org_id]/sync_repos.ts | 7 ++----- web-server/src/api-helpers/axios.ts | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/web-server/libdefs/ambient.d.ts b/web-server/libdefs/ambient.d.ts index fa529dd9c..0ebb25d00 100644 --- a/web-server/libdefs/ambient.d.ts +++ b/web-server/libdefs/ambient.d.ts @@ -65,6 +65,7 @@ declare namespace NodeJS { NEXT_PUBLIC_BUILD_TIME: string; NEXT_PUBLIC_APP_ENVIRONMENT: 'production' | 'development'; INTERNAL_API_BASE_URL: string; + INTERNAL_SYNC_API_BASE_URL: string; SECRET_PUBLIC_KEY: string; SECRET_PRIVATE_KEY: string; DB_HOST: string; diff --git a/web-server/pages/api/internal/[org_id]/sync_repos.ts b/web-server/pages/api/internal/[org_id]/sync_repos.ts index 8275a8e62..1ab25f3e9 100644 --- a/web-server/pages/api/internal/[org_id]/sync_repos.ts +++ b/web-server/pages/api/internal/[org_id]/sync_repos.ts @@ -1,6 +1,6 @@ import * as yup from 'yup'; -import { handleRequest } from '@/api-helpers/axios'; +import { handleSyncServerRequest } from '@/api-helpers/axios'; import { Endpoint, nullSchema } from '@/api-helpers/global'; const pathSchema = yup.object().shape({ @@ -18,9 +18,6 @@ endpoint.handle.POST(nullSchema, async (req, res) => { }); export const syncReposForOrg = (org_id: ID) => - Promise.all([ - handleRequest(`/orgs/${org_id}/sync_repos`, { method: 'POST' }), - handleRequest(`/orgs/${org_id}/sync_workflows`, { method: 'POST' }) - ]); + handleSyncServerRequest(`/orgs/${org_id}/sync`, { method: 'POST' }); export default endpoint.serve(); diff --git a/web-server/src/api-helpers/axios.ts b/web-server/src/api-helpers/axios.ts index 612566730..a5c8ac700 100644 --- a/web-server/src/api-helpers/axios.ts +++ b/web-server/src/api-helpers/axios.ts @@ -6,6 +6,10 @@ export const internal = axios.create({ baseURL: process.env.INTERNAL_API_BASE_URL }); +export const internalSyncServer = axios.create({ + baseURL: process.env.INTERNAL_SYNC_API_BASE_URL +}); + axiosRetry(internal, { retries: 2, retryCondition(error) { @@ -76,3 +80,15 @@ export const handleThen = (r: AxiosResponse) => r.data; export const handleCatch = (r: { response: AxiosResponse }) => { throw r.response; }; + +export const handleSyncServerRequest = ( + url: string, + params: AxiosRequestConfig = { method: 'get' } +): Promise => + internalSyncServer({ + url, + ...params, + headers: { 'Content-Type': 'application/json' } + }) + .then(handleThen) + .catch(handleCatch); From 65692c538abc7de56a891477dded9e073e5f8067 Mon Sep 17 00:00:00 2001 From: shivam-bit Date: Tue, 23 Apr 2024 20:23:45 +0530 Subject: [PATCH 5/6] Update code to handle team creation and integration in Integrations.tsx --- .../api/resources/orgs/[org_id]/teams/v2.ts | 70 +++++-------------- web-server/pages/integrations.tsx | 14 +++- .../src/components/Teams/useTeamsConfig.tsx | 9 +-- web-server/src/components/TeamsList.tsx | 7 +- web-server/src/types/resources.ts | 2 +- 5 files changed, 40 insertions(+), 62 deletions(-) diff --git a/web-server/pages/api/resources/orgs/[org_id]/teams/v2.ts b/web-server/pages/api/resources/orgs/[org_id]/teams/v2.ts index aa15c4cec..182cc9205 100644 --- a/web-server/pages/api/resources/orgs/[org_id]/teams/v2.ts +++ b/web-server/pages/api/resources/orgs/[org_id]/teams/v2.ts @@ -12,9 +12,8 @@ import { } from '@/api/resources/orgs/[org_id]/onboarding'; import { getTeamRepos } from '@/api/resources/team_repos'; import { handleRequest } from '@/api-helpers/axios'; -import { handleApi } from '@/api-helpers/axios-api-instance'; import { Endpoint } from '@/api-helpers/global'; -import { Columns, Table, Row } from '@/constants/db'; +import { Row } from '@/constants/db'; import { Integration } from '@/constants/integrations'; import { getTeamV2Mock } from '@/mocks/teams'; import { BaseTeam } from '@/types/api/teams'; @@ -113,16 +112,15 @@ endpoint.handle.POST(postSchema, async (req, res) => { const { org_repos, org_id, provider, name } = req.payload; const orgReposList: ReqRepoWithProvider[] = []; - forEachObjIndexed( - (repos, org) => + forEachObjIndexed((repos, org) => { + repos.forEach((repo) => { orgReposList.push({ - ...repos, + ...repo, org, provider - } as any as ReqRepoWithProvider), - org_repos - ); - + } as any as ReqRepoWithProvider); + }); + }, org_repos); const [team, onboardingState] = await Promise.all([ createTeam(org_id, name, []), getOnBoardingState(org_id) @@ -132,7 +130,7 @@ endpoint.handle.POST(postSchema, async (req, res) => { new Set(onboardingState.onboarding_state).add(OnboardingStep.TEAM_CREATED) ); const [teamRepos] = await Promise.all([ - handleApi<(Row<'TeamRepos'> & Row<'OrgRepo'>)[]>( + handleRequest<(Row<'TeamRepos'> & Row<'OrgRepo'>)[]>( `/teams/${team.id}/repos`, { method: 'PUT', @@ -154,26 +152,25 @@ endpoint.handle.PATCH(patchSchema, async (req, res) => { const { id, name, org_repos, provider } = req.payload; const orgReposList: ReqRepoWithProvider[] = []; - forEachObjIndexed( - (repos, org) => + forEachObjIndexed((repos, org) => { + repos.forEach((repo) => { orgReposList.push({ - ...repos, + ...repo, org, provider - } as any as ReqRepoWithProvider), - org_repos - ); + } as any as ReqRepoWithProvider); + }); + }, org_repos); const [team, teamRepos] = await Promise.all([ updateTeam(id, name, []), - handleApi<(Row<'TeamRepos'> & Row<'OrgRepo'>)[]>(`/teams/${id}/repos`, { + handleRequest<(Row<'TeamRepos'> & Row<'OrgRepo'>)[]>(`/teams/${id}/repos`, { method: 'PUT', data: { repos: orgReposList } }).then((repos) => repos.map((r) => ({ ...r, team_id: id }))) ]); - res.send({ team, teamReposMap: groupBy(prop('team_id'), teamRepos) }); }); @@ -200,7 +197,7 @@ const updateTeam = async ( member_ids: string[] ): Promise => { return handleRequest(`/team/${team_id}`, { - method: 'PUT', + method: 'PATCH', data: { name: team_name, member_ids @@ -213,7 +210,7 @@ const createTeam = async ( team_name: string, member_ids: string[] ): Promise => { - return handleRequest(`/org/${org_id}/teams`, { + return handleRequest(`/org/${org_id}/team`, { method: 'POST', data: { name: team_name, @@ -222,39 +219,6 @@ const createTeam = async ( }); }; -const addReposToTeam = async (team_id: ID, repo_ids: ID[]) => { - try { - await db(Table.TeamRepos) - .update({ - [Columns[Table.TeamRepos].is_active]: false, - updated_at: new Date() - }) - .where('team_id', team_id); - } catch (err) {} - - try { - const payload = repo_ids.map((repo_id) => ({ - [Columns[Table.TeamRepos].team_id]: team_id, - [Columns[Table.TeamRepos].org_repo_id]: repo_id, - [Columns[Table.TeamRepos].is_active]: true - })); - const data = - payload.length && - (await db('TeamRepos') - .insert( - repo_ids.map((repo_id) => ({ - [Columns[Table.TeamRepos].team_id]: team_id, - [Columns[Table.TeamRepos].org_repo_id]: repo_id, - [Columns[Table.TeamRepos].is_active]: true - })) - ) - .onConflict(['team_id', 'org_repo_id']) - .merge() - .returning('*')); - return data; - } catch (err) {} -}; - export const getAllOrgRepos = async ( org_id: ID, provider: CodeSourceProvidersIntegration diff --git a/web-server/pages/integrations.tsx b/web-server/pages/integrations.tsx index 7b03d2dc0..72c407603 100644 --- a/web-server/pages/integrations.tsx +++ b/web-server/pages/integrations.tsx @@ -7,6 +7,7 @@ import { FlexBox } from '@/components/FlexBox'; import { Line } from '@/components/Text'; import { Integration } from '@/constants/integrations'; import { ROUTES } from '@/constants/routes'; +import { FetchState } from '@/constants/ui-states'; import { GithubIntegrationCard } from '@/content/Dashboards/IntegrationCards'; import { PageWrapper } from '@/content/PullRequests/PageWrapper'; import { useAuth } from '@/hooks/useAuth'; @@ -18,6 +19,10 @@ import { PageLayout } from '@/types/resources'; import { depFn } from '@/utils/fn'; function Integrations() { + const isLoading = useSelector( + (s) => s.team.requests?.teams === FetchState.REQUEST + ); + return ( <> @@ -51,6 +57,9 @@ const Content = () => { const dispatch = useDispatch(); const loading = useBoolState(false); + const teamCount = teams.length; + const showCreationCTA = isLinked && !teamCount && !loading.value; + useEffect(() => { if (!orgId) return; depFn(loading.true); @@ -62,14 +71,13 @@ const Content = () => { ).finally(loading.false); }, [dispatch, loading.false, loading.true, orgId]); - const teamCount = teams.length; return ( Integrate your Github to fetch DORA for your team - {Boolean(teamCount) && ( + {Boolean(teamCount) && Boolean(isLinked) && (