diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index d6065fbd26..d9a6f86703 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -15,7 +15,7 @@ */ import { Page, Header, Lifecycle, Content } from '@backstage/core'; -import React, { useState, useEffect, memo } from 'react'; +import React, { useState, useEffect, memo, useMemo } from 'react'; import { makeStyles, Theme, @@ -62,52 +62,52 @@ const useStyles = makeStyles((theme: Theme) => ); type Steps = { - log: string[]; id: string; name: string; status: Status; }; -export const TaskStatusStepper = memo(({ steps }: { steps: Steps[] }) => { - const classes = useStyles(); - const [activeStep, setActiveStep] = useState(0); +export const TaskStatusStepper = memo( + ({ + steps, + currentStepId, + onUserStepChange, + }: { + steps: Steps[]; + currentStepId: string | undefined; + onUserStepChange: (id: string) => void; + }) => { + const classes = useStyles(); - const handleStep = (step: number) => { - setActiveStep(step); - }; - - useEffect(() => { - const activeIndex = steps.findIndex(step => - ['failed', 'processing'].includes(step.status), + return ( +
+ s.id === currentStepId)} + orientation="vertical" + nonLinear + > + {steps.map((step, index) => { + const isCompleted = step.status === 'completed'; + const isFailed = step.status === 'failed'; + return ( + + onUserStepChange(step.id)}> + + {step.name} + + + + ); + })} + +
); - setActiveStep(activeIndex); - }, [steps]); - - return ( -
- - {steps.map((step, index) => { - const isCompleted = step.status === 'completed'; - const isFailed = step.status === 'failed'; - return ( - - {}}> - - {step.name} - - - - ); - })} - -
- ); -}); + }, +); const TaskLogger = memo(({ log }: { log: string }) => { - console.log('rendering my log'); return (
@@ -125,13 +125,43 @@ const TaskActionsBar = () => { }; export const TaskPage = () => { + const [userSelectedStepId, setUserSelectedStepId] = useState< + string | undefined + >(undefined); + const [lastActiveStepId, setLastActiveStepId] = useState( + undefined, + ); const { taskId } = useParams(); const taskStream = useTaskEventStream(taskId); - const steps = - taskStream.task?.spec.steps.map(step => ({ - ...step, - ...taskStream?.steps?.[step.id], - })) ?? []; + + const steps = useMemo( + () => + taskStream.task?.spec.steps.map(step => ({ + ...step, + ...taskStream?.steps?.[step.id], + })) ?? [], + [taskStream], + ); + useEffect(() => { + const activeStep = steps.find(step => + ['failed', 'processing'].includes(step.status), + ); + setLastActiveStepId(activeStep?.id); + }, [steps]); + + const currentStepId = userSelectedStepId ?? lastActiveStepId; + + const logAsString = useMemo(() => { + if (!currentStepId) { + return 'Loading...'; + } + const log = taskStream.stepLogs[currentStepId]; + + if (!log?.length) { + return 'Waiting for logs...'; + } + return log.join('\n'); + }, [taskStream.stepLogs, currentStepId]); return ( @@ -145,17 +175,16 @@ export const TaskPage = () => { subtitle={`Activity for task: ${taskId}`} /> - - + - + diff --git a/plugins/scaffolder/src/components/hooks/useEventStream.ts b/plugins/scaffolder/src/components/hooks/useEventStream.ts index f7b366be56..8bd8bb4d6c 100644 --- a/plugins/scaffolder/src/components/hooks/useEventStream.ts +++ b/plugins/scaffolder/src/components/hooks/useEventStream.ts @@ -29,7 +29,7 @@ type Step = { export type TaskStream = { loading: boolean; error?: Error; - log: string[]; + stepLogs: { [stepId in string]: string[] }; completed: boolean; task?: ScaffolderTask; steps: { [stepId in string]: Step }; @@ -53,17 +53,21 @@ function reducer(draft: TaskStream, action: ReducerAction) { current[next.id] = { status: 'open', id: next.id }; return current; }, {} as { [stepId in string]: Step }); + draft.stepLogs = action.data.spec.steps.reduce((current, next) => { + current[next.id] = []; + return current; + }, {} as { [stepId in string]: string[] }); draft.loading = false; draft.error = undefined; draft.completed = false; draft.task = action.data; - draft.log = []; return; } case 'LOGS': { const entries = action.data; const logLines = []; + for (const entry of entries) { const logLine = `${entry.createdAt} ${entry.body.message}`; logLines.push(logLine); @@ -72,6 +76,7 @@ function reducer(draft: TaskStream, action: ReducerAction) { continue; } + const currentStepLog = draft.stepLogs?.[entry.body.stepId]; const currentStep = draft.steps?.[entry.body.stepId]; if (entry.body.status && entry.body.status !== currentStep.status) { @@ -87,9 +92,10 @@ function reducer(draft: TaskStream, action: ReducerAction) { currentStep.endedAt = entry.createdAt; } } + + currentStepLog?.push(logLine); } - draft.log.push(...logLines); return; } @@ -115,7 +121,7 @@ export const useTaskEventStream = (taskId: string): TaskStream => { const [state, dispatch] = useImmerReducer(reducer, { loading: true, completed: false, - log: [], + stepLogs: {} as { [stepId in string]: string[] }, steps: {} as { [stepId in string]: Step }, }); @@ -130,6 +136,13 @@ export const useTaskEventStream = (taskId: string): TaskStream => { return; } dispatch({ type: 'INIT', data: task }); + + // TODO(blam): Use a normal fetch to fetch the current log for the event stream + // and use that for an INIT_EVENTs dispatch event, and then + // use the last event ID to subscribe using after option to + // stream logs. Without this, if you have a lot of logs, it can look like the + // task is being rebuilt on load as it progresses through the steps at a slower + // rate whilst it builds the status from the event logs const observable = scaffolderApi.streamLogs({ taskId }); const collectedLogEvents = new Array();