mirror of
https://github.com/harness/drone.git
synced 2025-05-04 20:10:44 +08:00
WIP execution event streaming
This commit is contained in:
parent
c71ebc99f1
commit
c4f8dcbfdf
@ -171,9 +171,12 @@ export interface StringsMap {
|
|||||||
error404Text: string
|
error404Text: string
|
||||||
'executions.completedTime': string
|
'executions.completedTime': string
|
||||||
'executions.description': string
|
'executions.description': string
|
||||||
|
'executions.failed': string
|
||||||
'executions.name': string
|
'executions.name': string
|
||||||
|
'executions.newExecution': string
|
||||||
'executions.newExecutionButton': string
|
'executions.newExecutionButton': string
|
||||||
'executions.noData': string
|
'executions.noData': string
|
||||||
|
'executions.started': string
|
||||||
'executions.time': string
|
'executions.time': string
|
||||||
executor: string
|
executor: string
|
||||||
existingAccount: 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
|
description: Description
|
||||||
time: Time
|
time: Time
|
||||||
completedTime: completed {{timeString}} ago
|
completedTime: completed {{timeString}} ago
|
||||||
|
started: Execution started
|
||||||
|
failed: Failed to start build
|
||||||
|
newExecution: Run Pipeline
|
||||||
selectRange: Shift-click to select a range
|
selectRange: Shift-click to select a range
|
||||||
allCommits: All Commits
|
allCommits: All Commits
|
||||||
secrets:
|
secrets:
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Container, PageBody } from '@harnessio/uicore'
|
import { Container, PageBody } from '@harnessio/uicore'
|
||||||
import React, { useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import cx from 'classnames'
|
import cx from 'classnames'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { useGet } from 'restful-react'
|
import { useGet } from 'restful-react'
|
||||||
@ -13,6 +13,7 @@ import { useStrings } from 'framework/strings'
|
|||||||
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||||
import { ExecutionPageHeader } from 'components/ExecutionPageHeader/ExecutionPageHeader'
|
import { ExecutionPageHeader } from 'components/ExecutionPageHeader/ExecutionPageHeader'
|
||||||
|
import usePipelineEventStream from 'hooks/usePipelineEventStream'
|
||||||
import noExecutionImage from '../RepositoriesListing/no-repo.svg'
|
import noExecutionImage from '../RepositoriesListing/no-repo.svg'
|
||||||
import css from './Execution.module.scss'
|
import css from './Execution.module.scss'
|
||||||
|
|
||||||
@ -20,18 +21,44 @@ const Execution = () => {
|
|||||||
const { pipeline, execution: executionNum } = useParams<CODEProps>()
|
const { pipeline, execution: executionNum } = useParams<CODEProps>()
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
|
|
||||||
const { repoMetadata, error, loading, refetch } = useGetRepositoryMetadata()
|
const { repoMetadata, error, loading, refetch, space } = useGetRepositoryMetadata()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: execution,
|
data: execution,
|
||||||
error: executionError,
|
error: executionError,
|
||||||
loading: executionLoading
|
loading: executionLoading,
|
||||||
|
refetch: executionRefetch
|
||||||
} = useGet<TypesExecution>({
|
} = useGet<TypesExecution>({
|
||||||
path: `/api/v1/repos/${repoMetadata?.path}/+/pipelines/${pipeline}/executions/${executionNum}`,
|
path: `/api/v1/repos/${repoMetadata?.path}/+/pipelines/${pipeline}/executions/${executionNum}`,
|
||||||
lazy: !repoMetadata
|
lazy: !repoMetadata
|
||||||
})
|
})
|
||||||
|
|
||||||
|
//TODO remove null type here?
|
||||||
const [selectedStage, setSelectedStage] = useState<number | null>(1)
|
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 (
|
return (
|
||||||
<Container className={css.main}>
|
<Container className={css.main}>
|
||||||
@ -72,7 +99,7 @@ const Execution = () => {
|
|||||||
message: getString('executions.noData')
|
message: getString('executions.noData')
|
||||||
// button: NewExecutionButton
|
// button: NewExecutionButton
|
||||||
}}>
|
}}>
|
||||||
<LoadingSpinner visible={loading || executionLoading} withBorder={!!execution && executionLoading} />
|
<LoadingSpinner visible={loading || isInitialLoad} withBorder={!!execution && isInitialLoad} />
|
||||||
{execution && (
|
{execution && (
|
||||||
<SplitPane split="vertical" size={300} minSize={200} maxSize={400}>
|
<SplitPane split="vertical" size={300} minSize={200} maxSize={400}>
|
||||||
<ExecutionStageList
|
<ExecutionStageList
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo } from 'react'
|
import React, { useEffect, useMemo, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
@ -33,6 +33,7 @@ import { RepositoryPageHeader } from 'components/RepositoryPageHeader/Repository
|
|||||||
import { ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus'
|
import { ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus'
|
||||||
import { getStatus } from 'utils/PipelineUtils'
|
import { getStatus } from 'utils/PipelineUtils'
|
||||||
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
||||||
|
import usePipelineEventStream from 'hooks/usePipelineEventStream'
|
||||||
import noExecutionImage from '../RepositoriesListing/no-repo.svg'
|
import noExecutionImage from '../RepositoriesListing/no-repo.svg'
|
||||||
import css from './ExecutionList.module.scss'
|
import css from './ExecutionList.module.scss'
|
||||||
|
|
||||||
@ -46,12 +47,11 @@ const ExecutionList = () => {
|
|||||||
const [page, setPage] = usePageIndex(pageInit)
|
const [page, setPage] = usePageIndex(pageInit)
|
||||||
const { showError, showSuccess } = useToaster()
|
const { showError, showSuccess } = useToaster()
|
||||||
|
|
||||||
const { repoMetadata, error, loading, refetch } = useGetRepositoryMetadata()
|
const { repoMetadata, error, loading, refetch, space } = useGetRepositoryMetadata()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: executions,
|
data: executions,
|
||||||
error: executionsError,
|
error: executionsError,
|
||||||
loading: executionsLoading,
|
|
||||||
response,
|
response,
|
||||||
refetch: executionsRefetch
|
refetch: executionsRefetch
|
||||||
} = useGet<TypesExecution[]>({
|
} = useGet<TypesExecution[]>({
|
||||||
@ -60,19 +60,40 @@ const ExecutionList = () => {
|
|||||||
lazy: !repoMetadata
|
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>({
|
const { mutate, loading: mutateLoading } = useMutate<TypesExecution>({
|
||||||
verb: 'POST',
|
verb: 'POST',
|
||||||
path: `/api/v1/repos/${repoMetadata?.path}/+/pipelines/${pipeline}/executions`,
|
path: `/api/v1/repos/${repoMetadata?.path}/+/pipelines/${pipeline}/executions`
|
||||||
queryParams: { branch: 'master' }
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
try {
|
try {
|
||||||
//TODO - really this should be handled by the event bus
|
//TODO - this should NOT be hardcoded to master branch - need a modal to insert branch - but useful for testing until then
|
||||||
await mutate(null)
|
await mutate({ branch: 'master' })
|
||||||
showSuccess('Build started')
|
showSuccess('Build started')
|
||||||
executionsRefetch()
|
|
||||||
} catch {
|
} catch {
|
||||||
showError('Failed to start build')
|
showError('Failed to start build')
|
||||||
}
|
}
|
||||||
@ -140,7 +161,7 @@ const ExecutionList = () => {
|
|||||||
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
|
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
|
||||||
<Calendar color={Utils.getRealCSSColor(Color.GREY_500)} />
|
<Calendar color={Utils.getRealCSSColor(Color.GREY_500)} />
|
||||||
<Text inline color={Color.GREY_500} lineClamp={1} width={180} font={{ size: 'small' }}>
|
<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>
|
</Text>
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
</Layout.Vertical>
|
</Layout.Vertical>
|
||||||
@ -177,7 +198,7 @@ const ExecutionList = () => {
|
|||||||
message: getString('executions.noData'),
|
message: getString('executions.noData'),
|
||||||
button: NewExecutionButton
|
button: NewExecutionButton
|
||||||
}}>
|
}}>
|
||||||
<LoadingSpinner visible={loading || executionsLoading} withBorder={!!executions && executionsLoading} />
|
<LoadingSpinner visible={loading || isInitialLoad} withBorder={!!executions && isInitialLoad} />
|
||||||
|
|
||||||
<Container padding="xlarge">
|
<Container padding="xlarge">
|
||||||
<Layout.Horizontal spacing="large" className={css.layout}>
|
<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 {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
@ -34,6 +34,7 @@ import { ExecutionStatus, ExecutionState } from 'components/ExecutionStatus/Exec
|
|||||||
import { getStatus } from 'utils/PipelineUtils'
|
import { getStatus } from 'utils/PipelineUtils'
|
||||||
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
||||||
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
||||||
|
import usePipelineEventStream from 'hooks/usePipelineEventStream'
|
||||||
import noPipelineImage from '../RepositoriesListing/no-repo.svg'
|
import noPipelineImage from '../RepositoriesListing/no-repo.svg'
|
||||||
import css from './PipelineList.module.scss'
|
import css from './PipelineList.module.scss'
|
||||||
|
|
||||||
@ -52,8 +53,8 @@ const PipelineList = () => {
|
|||||||
const {
|
const {
|
||||||
data: pipelines,
|
data: pipelines,
|
||||||
error: pipelinesError,
|
error: pipelinesError,
|
||||||
loading: pipelinesLoading,
|
response,
|
||||||
response
|
refetch: pipelinesRefetch
|
||||||
} = useGet<TypesPipeline[]>({
|
} = useGet<TypesPipeline[]>({
|
||||||
path: `/api/v1/repos/${repoMetadata?.path}/+/pipelines`,
|
path: `/api/v1/repos/${repoMetadata?.path}/+/pipelines`,
|
||||||
queryParams: { page, limit: LIST_FETCHING_LIMIT, query: searchTerm, latest: true },
|
queryParams: { page, limit: LIST_FETCHING_LIMIT, query: searchTerm, latest: true },
|
||||||
@ -61,6 +62,28 @@ const PipelineList = () => {
|
|||||||
debounce: 500
|
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 = (
|
const NewPipelineButton = (
|
||||||
<Button
|
<Button
|
||||||
text={getString('pipelines.newPipelineButton')}
|
text={getString('pipelines.newPipelineButton')}
|
||||||
@ -105,7 +128,7 @@ const PipelineList = () => {
|
|||||||
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
|
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
|
||||||
<Text className={css.desc}>{`#${record.number}`}</Text>
|
<Text className={css.desc}>{`#${record.number}`}</Text>
|
||||||
<PipeSeparator height={7} />
|
<PipeSeparator height={7} />
|
||||||
<Text className={css.desc}>{record.title}</Text>
|
<Text className={css.desc}>{record.message}</Text>
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
<Layout.Horizontal spacing={'xsmall'} style={{ alignItems: 'center' }}>
|
<Layout.Horizontal spacing={'xsmall'} style={{ alignItems: 'center' }}>
|
||||||
<Avatar
|
<Avatar
|
||||||
@ -130,8 +153,7 @@ const PipelineList = () => {
|
|||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}}>
|
}}>
|
||||||
{/* {record.after?.slice(0, 6)} */}
|
{record.after?.slice(0, 6)}
|
||||||
{'hardcoded'.slice(0, 6)}
|
|
||||||
</Link>
|
</Link>
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
</Layout.Vertical>
|
</Layout.Vertical>
|
||||||
@ -157,7 +179,7 @@ const PipelineList = () => {
|
|||||||
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
|
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
|
||||||
<Calendar color={Utils.getRealCSSColor(Color.GREY_500)} />
|
<Calendar color={Utils.getRealCSSColor(Color.GREY_500)} />
|
||||||
<Text inline color={Color.GREY_500} lineClamp={1} width={180} font={{ size: 'small' }}>
|
<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>
|
</Text>
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
</Layout.Vertical>
|
</Layout.Vertical>
|
||||||
@ -188,10 +210,7 @@ const PipelineList = () => {
|
|||||||
message: getString('pipelines.noData'),
|
message: getString('pipelines.noData'),
|
||||||
button: NewPipelineButton
|
button: NewPipelineButton
|
||||||
}}>
|
}}>
|
||||||
<LoadingSpinner
|
<LoadingSpinner visible={(loading || isInitialLoad) && !searchTerm} withBorder={!!pipelines && isInitialLoad} />
|
||||||
visible={(loading || pipelinesLoading) && !searchTerm}
|
|
||||||
withBorder={!!pipelines && pipelinesLoading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Container padding="xlarge">
|
<Container padding="xlarge">
|
||||||
<Layout.Horizontal spacing="large" className={css.layout}>
|
<Layout.Horizontal spacing="large" className={css.layout}>
|
||||||
|
@ -102,7 +102,7 @@ const SecretList = () => {
|
|||||||
Cell: ({ row }: CellProps<TypesSecret>) => {
|
Cell: ({ row }: CellProps<TypesSecret>) => {
|
||||||
const { mutate: deleteSecret } = useMutate({
|
const { mutate: deleteSecret } = useMutate({
|
||||||
verb: 'DELETE',
|
verb: 'DELETE',
|
||||||
path: `/api/v1/secrets/${space}/${row.original.uid}`
|
path: `/api/v1/secrets/${space}/${row.original.uid}/+`
|
||||||
})
|
})
|
||||||
const { showSuccess, showError } = useToaster()
|
const { showSuccess, showError } = useToaster()
|
||||||
const confirmDeleteSecret = useConfirmAct()
|
const confirmDeleteSecret = useConfirmAct()
|
||||||
|
Loading…
Reference in New Issue
Block a user