WIP execution event streaming

This commit is contained in:
Dan Wilson 2023-09-11 16:55:38 +01:00
parent c71ebc99f1
commit c4f8dcbfdf
7 changed files with 150 additions and 27 deletions

View File

@ -171,9 +171,12 @@ export interface StringsMap {
error404Text: string
'executions.completedTime': string
'executions.description': string
'executions.failed': string
'executions.name': string
'executions.newExecution': string
'executions.newExecutionButton': string
'executions.noData': string
'executions.started': string
'executions.time': string
executor: string
existingAccount: string

View File

@ -0,0 +1,50 @@
import { useEffect, useRef } from 'react'
type UsePipelineEventStreamProps = {
space: string
onEvent: (data: any) => void
onError?: (event: Event) => void
shouldRun?: boolean
}
const usePipelineEventStream = ({ space, onEvent, onError, shouldRun = true }: UsePipelineEventStreamProps) => {
//TODO - this is not working right - need to get to the bottom of too many streams being opened and closed... can miss events!
const eventSourceRef = useRef<EventSource | null>(null)
useEffect(() => {
// Conditionally establish the event stream - don't want to open on a finished execution
if (shouldRun) {
if (!eventSourceRef.current) {
eventSourceRef.current = new EventSource(`/api/v1/spaces/${space}/stream`)
const handleMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data)
onEvent(data)
}
const handleError = (event: Event) => {
if (onError) onError(event)
eventSourceRef?.current?.close()
}
eventSourceRef.current.addEventListener('message', handleMessage)
eventSourceRef.current.addEventListener('error', handleError)
return () => {
eventSourceRef.current?.removeEventListener('message', handleMessage)
eventSourceRef.current?.removeEventListener('error', handleError)
eventSourceRef.current?.close()
eventSourceRef.current = null
}
}
} else {
// If shouldRun is false, close and cleanup any existing stream
if (eventSourceRef.current) {
eventSourceRef.current.close()
eventSourceRef.current = null
}
}
}, [space, shouldRun, onEvent, onError])
}
export default usePipelineEventStream

View File

@ -633,6 +633,9 @@ executions:
description: Description
time: Time
completedTime: completed {{timeString}} ago
started: Execution started
failed: Failed to start build
newExecution: Run Pipeline
selectRange: Shift-click to select a range
allCommits: All Commits
secrets:

View File

@ -1,5 +1,5 @@
import { Container, PageBody } from '@harnessio/uicore'
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import cx from 'classnames'
import { useParams } from 'react-router-dom'
import { useGet } from 'restful-react'
@ -13,6 +13,7 @@ import { useStrings } from 'framework/strings'
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import { ExecutionPageHeader } from 'components/ExecutionPageHeader/ExecutionPageHeader'
import usePipelineEventStream from 'hooks/usePipelineEventStream'
import noExecutionImage from '../RepositoriesListing/no-repo.svg'
import css from './Execution.module.scss'
@ -20,18 +21,44 @@ const Execution = () => {
const { pipeline, execution: executionNum } = useParams<CODEProps>()
const { getString } = useStrings()
const { repoMetadata, error, loading, refetch } = useGetRepositoryMetadata()
const { repoMetadata, error, loading, refetch, space } = useGetRepositoryMetadata()
const {
data: execution,
error: executionError,
loading: executionLoading
loading: executionLoading,
refetch: executionRefetch
} = useGet<TypesExecution>({
path: `/api/v1/repos/${repoMetadata?.path}/+/pipelines/${pipeline}/executions/${executionNum}`,
lazy: !repoMetadata
})
//TODO remove null type here?
const [selectedStage, setSelectedStage] = useState<number | null>(1)
//TODO - do not want to show load between refetchs - remove if/when we move to event stream method
const [isInitialLoad, setIsInitialLoad] = useState(true)
useEffect(() => {
if (execution) {
setIsInitialLoad(false)
}
}, [execution])
usePipelineEventStream({
space,
onEvent: (data: any) => {
if (
(data.type === 'execution_updated' || data.type === 'execution_completed') &&
data.data?.repo_id === execution?.repo_id &&
data.data?.pipeline_id === execution?.pipeline_id &&
data.data?.number === execution?.number
) {
//TODO - revisit full refresh - can I use the message to update the execution?
executionRefetch()
}
},
shouldRun: execution?.status === 'running'
})
return (
<Container className={css.main}>
@ -72,7 +99,7 @@ const Execution = () => {
message: getString('executions.noData')
// button: NewExecutionButton
}}>
<LoadingSpinner visible={loading || executionLoading} withBorder={!!execution && executionLoading} />
<LoadingSpinner visible={loading || isInitialLoad} withBorder={!!execution && isInitialLoad} />
{execution && (
<SplitPane split="vertical" size={300} minSize={200} maxSize={400}>
<ExecutionStageList

View File

@ -1,4 +1,4 @@
import React, { useMemo } from 'react'
import React, { useEffect, useMemo, useState } from 'react'
import {
Avatar,
Button,
@ -33,6 +33,7 @@ import { RepositoryPageHeader } from 'components/RepositoryPageHeader/Repository
import { ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus'
import { getStatus } from 'utils/PipelineUtils'
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
import usePipelineEventStream from 'hooks/usePipelineEventStream'
import noExecutionImage from '../RepositoriesListing/no-repo.svg'
import css from './ExecutionList.module.scss'
@ -46,12 +47,11 @@ const ExecutionList = () => {
const [page, setPage] = usePageIndex(pageInit)
const { showError, showSuccess } = useToaster()
const { repoMetadata, error, loading, refetch } = useGetRepositoryMetadata()
const { repoMetadata, error, loading, refetch, space } = useGetRepositoryMetadata()
const {
data: executions,
error: executionsError,
loading: executionsLoading,
response,
refetch: executionsRefetch
} = useGet<TypesExecution[]>({
@ -60,19 +60,40 @@ const ExecutionList = () => {
lazy: !repoMetadata
})
//TODO - this should NOT be hardcoded to master branch - need a modal to insert branch
//TODO - do not want to show load between refetchs - remove if/when we move to event stream method
const [isInitialLoad, setIsInitialLoad] = useState(true)
useEffect(() => {
if (executions) {
setIsInitialLoad(false)
}
}, [executions])
usePipelineEventStream({
space,
onEvent: (data: any) => {
// ideally this would include number - so we only check for executions on the page - but what if new executions are kicked off? - could check for ids that are higher than the lowest id on the page?
if (
executions?.some(
execution => execution.repo_id === data.data?.repo_id && execution.pipeline_id === data.data?.pipeline_id
)
) {
//TODO - revisit full refresh - can I use the message to update the execution?
executionsRefetch()
}
}
})
const { mutate, loading: mutateLoading } = useMutate<TypesExecution>({
verb: 'POST',
path: `/api/v1/repos/${repoMetadata?.path}/+/pipelines/${pipeline}/executions`,
queryParams: { branch: 'master' }
path: `/api/v1/repos/${repoMetadata?.path}/+/pipelines/${pipeline}/executions`
})
const handleClick = async () => {
try {
//TODO - really this should be handled by the event bus
await mutate(null)
//TODO - this should NOT be hardcoded to master branch - need a modal to insert branch - but useful for testing until then
await mutate({ branch: 'master' })
showSuccess('Build started')
executionsRefetch()
} catch {
showError('Failed to start build')
}
@ -140,7 +161,7 @@ const ExecutionList = () => {
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
<Calendar color={Utils.getRealCSSColor(Color.GREY_500)} />
<Text inline color={Color.GREY_500} lineClamp={1} width={180} font={{ size: 'small' }}>
{timeDistance(record.created, Date.now())} ago
{timeDistance(record.started, Date.now())} ago
</Text>
</Layout.Horizontal>
</Layout.Vertical>
@ -177,7 +198,7 @@ const ExecutionList = () => {
message: getString('executions.noData'),
button: NewExecutionButton
}}>
<LoadingSpinner visible={loading || executionsLoading} withBorder={!!executions && executionsLoading} />
<LoadingSpinner visible={loading || isInitialLoad} withBorder={!!executions && isInitialLoad} />
<Container padding="xlarge">
<Layout.Horizontal spacing="large" className={css.layout}>

View File

@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react'
import React, { useEffect, useMemo, useState } from 'react'
import {
Avatar,
Button,
@ -34,6 +34,7 @@ import { ExecutionStatus, ExecutionState } from 'components/ExecutionStatus/Exec
import { getStatus } from 'utils/PipelineUtils'
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import usePipelineEventStream from 'hooks/usePipelineEventStream'
import noPipelineImage from '../RepositoriesListing/no-repo.svg'
import css from './PipelineList.module.scss'
@ -52,8 +53,8 @@ const PipelineList = () => {
const {
data: pipelines,
error: pipelinesError,
loading: pipelinesLoading,
response
response,
refetch: pipelinesRefetch
} = useGet<TypesPipeline[]>({
path: `/api/v1/repos/${repoMetadata?.path}/+/pipelines`,
queryParams: { page, limit: LIST_FETCHING_LIMIT, query: searchTerm, latest: true },
@ -61,6 +62,28 @@ const PipelineList = () => {
debounce: 500
})
//TODO - do not want to show load between refetchs - remove if/when we move to event stream method
const [isInitialLoad, setIsInitialLoad] = useState(true)
useEffect(() => {
if (pipelines) {
setIsInitialLoad(false)
}
}, [pipelines])
usePipelineEventStream({
space,
onEvent: (data: any) => {
// should I include pipeline id here? what if a new pipeline is created? coould check for ids that are higher than the lowest id on the page?
if (
pipelines?.some(pipeline => pipeline.repo_id === data.data?.repo_id && pipeline.id === data.data?.pipeline_id)
) {
//TODO - revisit full refresh - can I use the message to update the execution?
pipelinesRefetch()
}
}
})
const NewPipelineButton = (
<Button
text={getString('pipelines.newPipelineButton')}
@ -105,7 +128,7 @@ const PipelineList = () => {
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
<Text className={css.desc}>{`#${record.number}`}</Text>
<PipeSeparator height={7} />
<Text className={css.desc}>{record.title}</Text>
<Text className={css.desc}>{record.message}</Text>
</Layout.Horizontal>
<Layout.Horizontal spacing={'xsmall'} style={{ alignItems: 'center' }}>
<Avatar
@ -130,8 +153,7 @@ const PipelineList = () => {
onClick={e => {
e.stopPropagation()
}}>
{/* {record.after?.slice(0, 6)} */}
{'hardcoded'.slice(0, 6)}
{record.after?.slice(0, 6)}
</Link>
</Layout.Horizontal>
</Layout.Vertical>
@ -157,7 +179,7 @@ const PipelineList = () => {
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
<Calendar color={Utils.getRealCSSColor(Color.GREY_500)} />
<Text inline color={Color.GREY_500} lineClamp={1} width={180} font={{ size: 'small' }}>
{timeDistance(record.finished, Date.now())} ago
{timeDistance(record.started, Date.now())} ago
</Text>
</Layout.Horizontal>
</Layout.Vertical>
@ -188,10 +210,7 @@ const PipelineList = () => {
message: getString('pipelines.noData'),
button: NewPipelineButton
}}>
<LoadingSpinner
visible={(loading || pipelinesLoading) && !searchTerm}
withBorder={!!pipelines && pipelinesLoading}
/>
<LoadingSpinner visible={(loading || isInitialLoad) && !searchTerm} withBorder={!!pipelines && isInitialLoad} />
<Container padding="xlarge">
<Layout.Horizontal spacing="large" className={css.layout}>

View File

@ -102,7 +102,7 @@ const SecretList = () => {
Cell: ({ row }: CellProps<TypesSecret>) => {
const { mutate: deleteSecret } = useMutate({
verb: 'DELETE',
path: `/api/v1/secrets/${space}/${row.original.uid}`
path: `/api/v1/secrets/${space}/${row.original.uid}/+`
})
const { showSuccess, showError } = useToaster()
const confirmDeleteSecret = useConfirmAct()