From 7e199ade2b30e2960bc2aa3a75332c3ba1b986e8 Mon Sep 17 00:00:00 2001 From: Patrik Korytar Date: Wed, 24 Apr 2024 13:39:13 +0200 Subject: [PATCH 1/4] NCL-8675 Implement GET Builds with Brew Push REST API services --- src/services/buildApi.ts | 34 ++++++++++++++++++++++++++++++++- src/services/groupBuildApi.ts | 35 +++++++++++++++++++++++++++++++++- src/utils/entityRecognition.ts | 4 ++++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/services/buildApi.ts b/src/services/buildApi.ts index 0687d23c..332a7b15 100644 --- a/src/services/buildApi.ts +++ b/src/services/buildApi.ts @@ -1,4 +1,4 @@ -import { AxiosRequestConfig } from 'axios'; +import axios, { AxiosRequestConfig, AxiosResponse, isAxiosError } from 'axios'; import { ArtifactPage, @@ -37,6 +37,38 @@ export const getBuilds = (requestConfig: AxiosRequestConfig = {}) => { return pncClient.getHttpClient().get('/builds', requestConfig); }; +export type BuildWithBrewPush = Build & { brewPush?: BuildPushResult }; +export type BuildWithBrewPushPage = Omit & { content?: BuildWithBrewPush[] }; + +/** + * Gets all Builds along with latest Brew Push result. + * + * @param requestConfig - Axios based request config + */ +export const getBuildsWithBrewPush = async ( + requestConfig: AxiosRequestConfig = {} +): Promise> => { + const buildsResponse = await getBuilds(requestConfig); + if (!buildsResponse.data.content?.length) return buildsResponse; + + const buildsWithBrewPush = await axios.all( + buildsResponse.data.content.map(async (build) => { + try { + const { data } = await getBrewPush({ id: build.id }); + return data ? { ...build, brewPush: data } : build; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + return build; + } + + throw error; + } + }) + ); + + return { ...buildsResponse, data: { ...buildsResponse.data, content: buildsWithBrewPush } }; +}; + /** * Gets Builds of a User. * diff --git a/src/services/groupBuildApi.ts b/src/services/groupBuildApi.ts index b31eedc2..e9e28729 100644 --- a/src/services/groupBuildApi.ts +++ b/src/services/groupBuildApi.ts @@ -1,7 +1,10 @@ -import { AxiosRequestConfig } from 'axios'; +import axios, { AxiosRequestConfig, AxiosResponse, isAxiosError } from 'axios'; import { BuildPage, BuildsGraph, GroupBuild, GroupBuildPage } from 'pnc-api-types-ts'; +import { BuildWithBrewPushPage } from 'services/buildApi'; +import * as buildApi from 'services/buildApi'; + import { extendRequestConfig } from 'utils/requestConfigHelper'; import { pncClient } from './pncClient'; @@ -66,6 +69,36 @@ export const getBuilds = ({ id }: IGroupBuildApiData, requestConfig: AxiosReques return pncClient.getHttpClient().get(`/group-builds/${id}/builds`, requestConfig); }; +/** + * Gets Builds contained in the Group Build along with latest Brew Push result. + * + * @param requestConfig - Axios based request config + */ +export const getBuildsWithBrewPush = async ( + { id }: IGroupBuildApiData, + requestConfig: AxiosRequestConfig = {} +): Promise> => { + const buildsResponse = await getBuilds({ id }, requestConfig); + if (!buildsResponse.data.content?.length) return buildsResponse; + + const buildsWithBrewPush = await axios.all( + buildsResponse.data.content.map(async (build) => { + try { + const { data } = await buildApi.getBrewPush({ id: build.id }); + return data ? { ...build, brewPush: data } : build; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + return build; + } + + throw error; + } + }) + ); + + return { ...buildsResponse, data: { ...buildsResponse.data, content: buildsWithBrewPush } }; +}; + /** * Gets dependency graph for a group build. * diff --git a/src/utils/entityRecognition.ts b/src/utils/entityRecognition.ts index 4b88cb98..b0dd6186 100644 --- a/src/utils/entityRecognition.ts +++ b/src/utils/entityRecognition.ts @@ -9,6 +9,8 @@ import { ProductVersion, } from 'pnc-api-types-ts'; +import { BuildWithBrewPush } from 'services/buildApi'; + export const isBoolean = (value: unknown): value is boolean => typeof value === 'boolean'; export const isString = (value: unknown): value is string => typeof value === 'string'; @@ -37,3 +39,5 @@ interface ArtifactWithProductMilestone extends Artifact { export const isArtifactWithProductMilestone = (artifact: Artifact): artifact is ArtifactWithProductMilestone => 'product' in artifact && 'productVersion' in artifact && 'productMilestone' in artifact; + +export const isBuildWithBrewPush = (build: Build): build is BuildWithBrewPush => 'brewPush' in build; From 33a39c1119348316064843b84aafd8764cf3516a Mon Sep 17 00:00:00 2001 From: Patrik Korytar Date: Wed, 24 Apr 2024 13:42:45 +0200 Subject: [PATCH 2/4] NCL-8675 Extend Builds list to support latest Brew Push result column --- src/common/buildEntityAttributes.ts | 5 +++++ src/components/BuildsList/BuildsList.tsx | 22 +++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/common/buildEntityAttributes.ts b/src/common/buildEntityAttributes.ts index 7ab8a537..ba93de95 100644 --- a/src/common/buildEntityAttributes.ts +++ b/src/common/buildEntityAttributes.ts @@ -28,6 +28,7 @@ interface IExtendedBuild extends Build { 'buildConfigRevision.buildScript': string; 'buildConfigRevision.brewPullActive': boolean; parameters: any; + brewPush: any; // fetched externally } export const buildEntityAttributes = { @@ -140,6 +141,10 @@ export const buildEntityAttributes = { id: 'buildConfigRevision.brewPullActive', title: 'Brew Pull Active', }, + brewPush: { + id: 'brewPush', + title: 'Latest Brew Push', + }, parameters: { id: 'parameters', title: 'Parameters', diff --git a/src/components/BuildsList/BuildsList.tsx b/src/components/BuildsList/BuildsList.tsx index 2edb40c1..452e77dc 100644 --- a/src/components/BuildsList/BuildsList.tsx +++ b/src/components/BuildsList/BuildsList.tsx @@ -19,6 +19,7 @@ import { IServiceContainerState } from 'hooks/useServiceContainer'; import { ISortOptions, useSorting } from 'hooks/useSorting'; import { BuildName } from 'components/BuildName/BuildName'; +import { BuildPushStatusLabelMapper } from 'components/BuildPushStatusLabelMapper/BuildPushStatusLabelMapper'; import { BuildStatusIcon } from 'components/BuildStatusIcon/BuildStatusIcon'; import { ContentBox } from 'components/ContentBox/ContentBox'; import { DateTime } from 'components/DateTime/DateTime'; @@ -31,6 +32,7 @@ import { ToolbarItem } from 'components/Toolbar/ToolbarItem'; import { TooltipWrapper } from 'components/TooltipWrapper/TooltipWrapper'; import { Username } from 'components/Username/Username'; +import { isBuildWithBrewPush } from 'utils/entityRecognition'; import { areDatesEqual, calculateDuration } from 'utils/utils'; type TColumns = Array; @@ -120,8 +122,8 @@ const TimesList = ({ build, isCompactMode }: ITimesListProps) => { ); }; -interface IBuildsListProps { - serviceContainerBuilds: IServiceContainerState; +interface IBuildsListProps { + serviceContainerBuilds: IServiceContainerState; columns?: TColumns; componentId: string; } @@ -133,7 +135,11 @@ interface IBuildsListProps { * @param columns - The columns to be displayed * @param componentId - Component ID */ -export const BuildsList = ({ serviceContainerBuilds, columns = defaultColumns, componentId }: IBuildsListProps) => { +export const BuildsList = ({ + serviceContainerBuilds, + columns = defaultColumns, + componentId, +}: IBuildsListProps) => { const sortOptions: ISortOptions = useMemo( () => getSortOptions({ @@ -227,6 +233,9 @@ export const BuildsList = ({ serviceContainerBuilds, columns = defaultColumns, c {buildEntityAttributes['user.username'].title} )} + {columns.includes(buildEntityAttributes.brewPush.id) && ( + {buildEntityAttributes.brewPush.title} + )} @@ -257,6 +266,13 @@ export const BuildsList = ({ serviceContainerBuilds, columns = defaultColumns, c {columns.includes(buildEntityAttributes['user.username'].id) && ( {build.user?.username && } )} + {columns.includes(buildEntityAttributes.brewPush.id) && ( + + {isBuildWithBrewPush(build) && build.brewPush && ( + + )} + + )} ))} From 458a4cea970530ed779497a5ecef895d3fccb5a9 Mon Sep 17 00:00:00 2001 From: Patrik Korytar Date: Wed, 24 Apr 2024 13:55:05 +0200 Subject: [PATCH 3/4] NCL-8675 Extend hasBrewPushFinished to accept Build IDs list check --- src/hooks/usePncWebSocketEffect.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/hooks/usePncWebSocketEffect.ts b/src/hooks/usePncWebSocketEffect.ts index 36c8e2cf..a75edbab 100644 --- a/src/hooks/usePncWebSocketEffect.ts +++ b/src/hooks/usePncWebSocketEffect.ts @@ -261,6 +261,7 @@ export const hasGroupBuildStarted = (wsData: any, parameters: IGroupBuildParamet */ interface IBrewPushParameters { buildId?: string; + buildIds?: string[]; } /** @@ -270,7 +271,7 @@ interface IBrewPushParameters { * @param parameters - See {@link IBrewPushParameters} * @returns true when Brew Push finished, otherwise false */ -export const hasBrewPushFinished = (wsData: any, { buildId }: IBrewPushParameters = {}): boolean => { +export const hasBrewPushFinished = (wsData: any, { buildId, buildIds }: IBrewPushParameters = {}): boolean => { if (wsData.job !== 'BREW_PUSH' || wsData.notificationType !== 'BREW_PUSH_RESULT') { return false; } @@ -280,7 +281,9 @@ export const hasBrewPushFinished = (wsData: any, { buildId }: IBrewPushParameter return false; // ignore changes when 'buildPushResult' is not available } - return !buildId || buildId === wsData.buildPushResult?.buildId; + return ( + (!buildId || buildId === wsData.buildPushResult.buildId) && (!buildIds || buildIds.includes(wsData.buildPushResult.buildId)) + ); }; /** From 5ce2ab0a77dc1298be5cf56f45652bc1e9b40c87 Mon Sep 17 00:00:00 2001 From: Patrik Korytar Date: Wed, 24 Apr 2024 13:55:53 +0200 Subject: [PATCH 4/4] NCL-8675 Extend Group Build detail page Builds list with latest Brew Push status --- .../GroupBuildDetailPage.tsx | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/components/GroupBuildDetailPage/GroupBuildDetailPage.tsx b/src/components/GroupBuildDetailPage/GroupBuildDetailPage.tsx index 6e1fdd76..ac8bf8f8 100644 --- a/src/components/GroupBuildDetailPage/GroupBuildDetailPage.tsx +++ b/src/components/GroupBuildDetailPage/GroupBuildDetailPage.tsx @@ -1,13 +1,15 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { Build, GroupBuild } from 'pnc-api-types-ts'; import { breadcrumbData } from 'common/breadcrumbData'; +import { buildEntityAttributes } from 'common/buildEntityAttributes'; import { groupBuildEntityAttributes } from 'common/groupBuildEntityAttributes'; import { useComponentQueryParams } from 'hooks/useComponentQueryParams'; import { useParamsRequired } from 'hooks/useParamsRequired'; import { + hasBrewPushFinished, hasBuildStarted, hasBuildStatusChanged, hasGroupBuildStatusChanged, @@ -39,7 +41,18 @@ import * as groupBuildApi from 'services/groupBuildApi'; import { refreshPage } from 'utils/refreshHelper'; import { generatePageTitle } from 'utils/titleHelper'; -import { createDateTime } from 'utils/utils'; +import { createDateTime, debounce } from 'utils/utils'; + +const buildsListColumns = [ + buildEntityAttributes.status.id, + buildEntityAttributes.name.id, + buildEntityAttributes.buildConfigName.id, + buildEntityAttributes.submitTime.id, + buildEntityAttributes.startTime.id, + buildEntityAttributes.endTime.id, + buildEntityAttributes['user.username'].id, + buildEntityAttributes.brewPush.id, +]; interface IGroupBuildDetailPageProps { componentId?: string; @@ -56,10 +69,15 @@ export const GroupBuildDetailPage = ({ componentId = 'gb2' }: IGroupBuildDetailP const serviceContainerGroupBuildRunner = serviceContainerGroupBuild.run; const serviceContainerGroupBuildSetter = serviceContainerGroupBuild.setData; - const serviceContainerGroupBuildBuilds = useServiceContainer(groupBuildApi.getBuilds); + const serviceContainerGroupBuildBuilds = useServiceContainer(groupBuildApi.getBuildsWithBrewPush); const serviceContainerGroupBuildBuildsRunner = serviceContainerGroupBuildBuilds.run; const serviceContainerGroupBuildBuildsSetter = serviceContainerGroupBuildBuilds.setData; + const serviceContainerGroupBuildBuildsRunnerDebounced = useMemo( + () => debounce(serviceContainerGroupBuildBuildsRunner), + [serviceContainerGroupBuildBuildsRunner] + ); + const serviceContainerDependencyGraph = useServiceContainer(groupBuildApi.getDependencyGraph); const serviceContainerDependencyGraphRunner = serviceContainerDependencyGraph.run; const serviceContainerDependencyGraphSetter = serviceContainerDependencyGraph.setData; @@ -83,7 +101,7 @@ export const GroupBuildDetailPage = ({ componentId = 'gb2' }: IGroupBuildDetailP serviceContainerGroupBuildSetter(wsGroupBuild); } else if (hasBuildStarted(wsData, { groupBuildId })) { // very exceptional use case, mostly it means backend issues - serviceContainerGroupBuildBuildsRunner({ + serviceContainerGroupBuildBuildsRunnerDebounced({ serviceData: { id: groupBuildId }, requestConfig: { params: groupBuildBuildsComponentQueryParamsObject }, }); @@ -102,15 +120,25 @@ export const GroupBuildDetailPage = ({ componentId = 'gb2' }: IGroupBuildDetailP vertices: { ...serviceContainerDependencyGraph.data.vertices, [wsBuild.id]: updatedVertex }, }); } + } else if ( + hasBrewPushFinished(wsData, { + buildIds: serviceContainerGroupBuildBuilds.data?.content?.map((build) => build.id) ?? [], + }) + ) { + serviceContainerGroupBuildBuildsRunnerDebounced({ + serviceData: { id: groupBuildId }, + requestConfig: { params: groupBuildBuildsComponentQueryParamsObject }, + }); } }, [ serviceContainerGroupBuildSetter, groupBuildId, - serviceContainerGroupBuildBuildsRunner, + serviceContainerGroupBuildBuildsRunnerDebounced, serviceContainerGroupBuildBuildsSetter, serviceContainerDependencyGraphSetter, serviceContainerDependencyGraph.data, + serviceContainerGroupBuildBuilds.data, groupBuildBuildsComponentQueryParamsObject, ] ) @@ -187,6 +215,7 @@ export const GroupBuildDetailPage = ({ componentId = 'gb2' }: IGroupBuildDetailP {...{ serviceContainerBuilds: serviceContainerGroupBuildBuilds, componentId, + columns: buildsListColumns, }} />