mirror of
https://github.com/harness/drone.git
synced 2025-05-04 17:29:35 +08:00
WIP execution event streaming
This commit is contained in:
parent
c71ebc99f1
commit
c4f8dcbfdf
@ -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
|
||||
|
50
web/src/hooks/usePipelineEventStream.tsx
Normal file
50
web/src/hooks/usePipelineEventStream.tsx
Normal 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
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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}>
|
||||
|
@ -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}>
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user