Skip to content

Commit fca50b9

Browse files
committed
chore: refactor Executions page to show for-each group
1 parent bf8be6e commit fca50b9

File tree

10 files changed

+470
-90
lines changed

10 files changed

+470
-90
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useMemo } from 'react'
2+
import { FaCheck, FaTimes } from 'react-icons/fa'
3+
import { Flex, Icon, Tag } from '@chakra-ui/react'
4+
5+
import { SingleSelect } from '@/components/SingleSelect'
6+
import { type GroupedSteps } from '@/helpers/processExecutionSteps'
7+
8+
interface IterationSelectorProps {
9+
groupedSteps: GroupedSteps
10+
selectedIteration: string
11+
setSelectedIteration: (iteration: string) => void
12+
}
13+
14+
export default function IterationSelector({
15+
groupedSteps,
16+
selectedIteration,
17+
setSelectedIteration,
18+
}: IterationSelectorProps) {
19+
const selectedIterationStep = groupedSteps[Number(selectedIteration)]
20+
const isSelectedIterationSuccessful =
21+
selectedIterationStep?.status === 'success'
22+
23+
const items = useMemo(() => {
24+
return groupedSteps.map(({ iteration, status }) => ({
25+
label: `Iteration ${iteration}`,
26+
value: iteration.toString(),
27+
badge:
28+
status === 'success' ? (
29+
<Icon as={FaCheck} color="green" ml={4} />
30+
) : (
31+
<Icon as={FaTimes} color="red" ml={4} />
32+
),
33+
}))
34+
}, [groupedSteps])
35+
36+
return (
37+
<Flex justifyContent="space-between" alignItems="center">
38+
<SingleSelect
39+
items={items}
40+
isSearchable={false}
41+
onChange={setSelectedIteration}
42+
value={selectedIteration}
43+
name="forEachIteration"
44+
placeholder="Select an option"
45+
isClearable={false}
46+
colorScheme="secondary"
47+
/>
48+
<Tag
49+
colorScheme={isSelectedIterationSuccessful ? 'success' : 'critical'}
50+
size="lg"
51+
>
52+
{isSelectedIterationSuccessful ? 'Success' : 'Failure'}
53+
</Tag>
54+
</Flex>
55+
)
56+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import type { IExecution, IExecutionStep } from '@plumber/types'
2+
3+
import { useMemo, useState } from 'react'
4+
import { Card, CardBody, Flex, Grid, HStack, Text } from '@chakra-ui/react'
5+
import { Tag } from '@opengovsg/design-system-react'
6+
import get from 'lodash/get'
7+
8+
import { ExecutionStep } from '@/exports/components'
9+
import { type GroupedSteps } from '@/helpers/processExecutionSteps'
10+
11+
import AppIconWithStatus from '../ExecutionStep/components/AppIconWithStatus'
12+
import { useExecutionStepStatus } from '../ExecutionStep/hooks/useExecutionStepStatus'
13+
14+
import IterationSelector from './IterationSelector'
15+
16+
interface ExecutionGroupProps {
17+
execution: IExecution
18+
groupingStep: IExecutionStep
19+
groupedSteps: GroupedSteps
20+
groupStats: { success: number; failure: number }
21+
numStepsBeforeGroup: number
22+
page: number
23+
}
24+
25+
export default function ExecutionGroup(props: ExecutionGroupProps) {
26+
const {
27+
execution,
28+
groupingStep,
29+
groupStats,
30+
groupedSteps,
31+
page,
32+
numStepsBeforeGroup,
33+
} = props
34+
// NOTE: we use string here as the combobox value needs to be a string
35+
const [selectedIteration, setSelectedIteration] = useState('1')
36+
37+
const selectedIterationStep = useMemo(() => {
38+
return get(groupedSteps, Number(selectedIteration) - 1)
39+
}, [groupedSteps, selectedIteration])
40+
41+
const hasError = groupedSteps.some((iteration) =>
42+
iteration.steps.some((step) => step.errorDetails),
43+
)
44+
const allIterationsSuccessful = groupedSteps?.every(
45+
(iteration) => iteration.status === 'success',
46+
)
47+
48+
// const canRetry = groupStats.failure > 0
49+
50+
const { app, appName, statusIcon } = useExecutionStepStatus({
51+
appKey: groupingStep.appKey,
52+
stepKey: groupingStep.key,
53+
status: allIterationsSuccessful ? 'success' : 'failure',
54+
errorDetails: hasError ? {} : null,
55+
execution,
56+
jobId: groupingStep.jobId,
57+
})
58+
59+
if (!app) {
60+
return null
61+
}
62+
63+
return (
64+
<Card boxShadow="none" border="1px solid" borderColor="base.divider.medium">
65+
<CardBody p={0}>
66+
<HStack p={4} alignItems="center" justifyContent="space-between">
67+
<HStack
68+
gap={2}
69+
w="full"
70+
borderBottom="1px solid"
71+
borderColor="base.divider.medium"
72+
pb={2}
73+
>
74+
<AppIconWithStatus
75+
iconUrl={app.iconUrl}
76+
appName={appName}
77+
statusIcon={statusIcon}
78+
/>
79+
<Flex
80+
justifyContent="space-between"
81+
width="full"
82+
alignItems="center"
83+
>
84+
<Text textStyle="h5">
85+
{numStepsBeforeGroup + 1}. {appName}
86+
</Text>
87+
<Flex gap={2} alignItems="center">
88+
{groupStats.success > 0 && (
89+
<Tag colorScheme={'success'} size="lg">
90+
{groupStats.success} success
91+
</Tag>
92+
)}
93+
{groupStats.failure > 0 && (
94+
<Tag colorScheme={'critical'} size="lg">
95+
{groupStats.failure} failures
96+
</Tag>
97+
)}
98+
{/* TODO: add retry all iterations for this specific execution */}
99+
{/* {canRetry && (
100+
<RetryAllButton execution={execution} type="iteration" />
101+
)} */}
102+
</Flex>
103+
</Flex>
104+
</HStack>
105+
</HStack>
106+
<Flex p={4} pt={0} direction="column" gap={4}>
107+
<IterationSelector
108+
groupedSteps={groupedSteps}
109+
selectedIteration={selectedIteration}
110+
setSelectedIteration={setSelectedIteration}
111+
/>
112+
<Grid mb={{ base: '16px', sm: '40px' }} rowGap={6}>
113+
{selectedIterationStep &&
114+
selectedIterationStep.steps.map(
115+
(step: IExecutionStep, index: number) => {
116+
return (
117+
<ExecutionStep
118+
key={step.id}
119+
execution={execution}
120+
executionStep={step}
121+
index={index + 1 + numStepsBeforeGroup}
122+
page={page}
123+
isInForEach={true}
124+
/>
125+
)
126+
},
127+
)}
128+
</Grid>
129+
</Flex>
130+
</CardBody>
131+
</Card>
132+
)
133+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Box } from '@chakra-ui/react'
2+
3+
import AppIcon from '@/components/AppIcon'
4+
5+
interface AppIconWithStatusProps {
6+
iconUrl: string
7+
appName: string
8+
statusIcon: React.ReactElement
9+
}
10+
11+
export default function AppIconWithStatus({
12+
iconUrl,
13+
appName,
14+
statusIcon,
15+
}: AppIconWithStatusProps): React.ReactElement {
16+
return (
17+
<Box position="relative">
18+
<AppIcon url={iconUrl} name={appName} />
19+
<Box
20+
position="absolute"
21+
right="0"
22+
top="0"
23+
transform="translate(50%, -50%)"
24+
display="inline-flex"
25+
sx={{
26+
svg: {
27+
// to make it distinguishable over an app icon
28+
background: 'white',
29+
borderRadius: '100%',
30+
overflow: 'hidden',
31+
},
32+
}}
33+
>
34+
{statusIcon}
35+
</Box>
36+
</Box>
37+
)
38+
}

packages/frontend/src/components/ExecutionStep/RetryAllButton.tsx renamed to packages/frontend/src/components/ExecutionStep/components/RetryAllButton.tsx

File renamed without changes.

packages/frontend/src/components/ExecutionStep/RetryButton.tsx renamed to packages/frontend/src/components/ExecutionStep/components/RetryButton.tsx

File renamed without changes.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { BiSolidCheckCircle, BiSolidErrorCircle } from 'react-icons/bi'
2+
import { Icon } from '@chakra-ui/react'
3+
4+
const successIcon = (
5+
<Icon
6+
boxSize={6}
7+
as={BiSolidCheckCircle}
8+
color="interaction.success.default"
9+
/>
10+
)
11+
const failureIcon = (
12+
<Icon
13+
boxSize={6}
14+
as={BiSolidErrorCircle}
15+
color="interaction.critical.default"
16+
/>
17+
)
18+
19+
const partialIcon = (
20+
<Icon
21+
boxSize={6}
22+
as={BiSolidErrorCircle}
23+
color="interaction.warning.default"
24+
/>
25+
)
26+
27+
export { failureIcon, partialIcon, successIcon }
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { IApp, IExecution } from '@plumber/types'
2+
3+
import { useMemo } from 'react'
4+
import { useQuery } from '@apollo/client'
5+
6+
import { GET_APP } from '@/graphql/queries/get-app'
7+
8+
import {
9+
failureIcon,
10+
partialIcon,
11+
successIcon,
12+
} from '../components/StatusIcons'
13+
14+
export interface UseExecutionStepStatusProps {
15+
appKey: string
16+
stepKey: string
17+
status?: string
18+
errorDetails?: any
19+
execution?: IExecution
20+
jobId?: string
21+
}
22+
23+
export interface UseExecutionStepStatusReturn {
24+
app: IApp | null
25+
appName: string
26+
statusIcon: React.ReactElement
27+
isStepSuccessful: boolean
28+
hasError: boolean
29+
isPartialSuccess: boolean
30+
canRetry: boolean
31+
loading: boolean
32+
}
33+
34+
export function useExecutionStepStatus({
35+
appKey,
36+
// stepKey, // TODO: get more specific app name
37+
status,
38+
errorDetails,
39+
execution,
40+
jobId,
41+
}: UseExecutionStepStatusProps): UseExecutionStepStatusReturn {
42+
const { data, loading } = useQuery(GET_APP, {
43+
variables: { key: appKey },
44+
})
45+
const app: IApp = data?.getApp
46+
const appName = app?.name ?? appKey
47+
const hasError = !!errorDetails
48+
const isStepSuccessful = status === 'success'
49+
const hasExecutionFailed = execution?.status === 'failure'
50+
const isPartialSuccess = status === 'success' && hasError
51+
const canRetry = !isStepSuccessful && !!jobId && hasExecutionFailed
52+
53+
const statusIcon = useMemo(() => {
54+
if (isPartialSuccess) {
55+
return partialIcon
56+
}
57+
if (isStepSuccessful) {
58+
return successIcon
59+
}
60+
return failureIcon
61+
}, [isPartialSuccess, isStepSuccessful])
62+
63+
return {
64+
app,
65+
appName,
66+
statusIcon,
67+
isStepSuccessful,
68+
hasError,
69+
isPartialSuccess,
70+
canRetry,
71+
loading,
72+
}
73+
}

0 commit comments

Comments
 (0)