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();