From 2876700adb883d876183226cc6ca833f64434860 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 10 Feb 2021 15:24:00 +0100 Subject: [PATCH] Unifify logs --- plugins/scaffolder/src/api.ts | 10 +- .../src/components/TaskPage/TaskPage.tsx | 185 ++++-------------- .../components/TemplatePage/TemplatePage.tsx | 3 - .../src/components/hooks/useEventStream.ts | 106 +++++----- plugins/scaffolder/src/types.ts | 1 + 5 files changed, 104 insertions(+), 201 deletions(-) diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index c51bd4a8be..afb84fe173 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -17,16 +17,20 @@ import { JsonObject } from '@backstage/config'; import { createApiRef, DiscoveryApi, Observable } from '@backstage/core'; import ObservableImpl from 'zen-observable'; -import { ScaffolderTask } from './types'; +import { ScaffolderTask, Status } from './types'; export const scaffolderApiRef = createApiRef({ id: 'plugin.scaffolder.service', description: 'Used to make requests towards the scaffolder backend', }); -type LogEvent = { +export type LogEvent = { type: 'log' | 'completion'; - body: JsonObject; + body: { + message: string; + stepId?: string; + status?: Status; + }; createdAt: string; id: string; taskId: string; diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index 820e65d126..d6065fbd26 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -27,6 +27,7 @@ import Step from '@material-ui/core/Step'; import StepLabel from '@material-ui/core/StepLabel'; import StepContent from '@material-ui/core/StepContent'; import StepConnector from '@material-ui/core/StepConnector'; +import Grid from '@material-ui/core/Grid'; import Button from '@material-ui/core/Button'; import Paper from '@material-ui/core/Paper'; import clsx from 'clsx'; @@ -42,80 +43,6 @@ import { import LazyLog from 'react-lazylog/build/LazyLog'; import { StepButton, StepIconProps } from '@material-ui/core'; -const QontoConnector = withStyles({ - active: { - '& $line': { - borderColor: '#784af4', - }, - }, - completed: { - '& $line': { - borderColor: '#784af4', - }, - }, - line: { - borderColor: '#eaeaf0', - borderTopWidth: 3, - borderRadius: 1, - }, -})(StepConnector); - -const useQontoStepIconStyles = makeStyles({ - root: { - color: '#eaeaf0', - display: 'flex', - height: 22, - alignItems: 'center', - }, - active: { - color: 'grey', - }, - error: { - color: 'red', - }, - circle: { - width: 8, - height: 8, - borderRadius: '50%', - backgroundColor: 'currentColor', - }, - completed: { - color: 'green', - zIndex: 1, - fontSize: 18, - }, -}); - -function QontoStepIcon(props: StepIconProps) { - const classes = useQontoStepIconStyles(); - const { active, completed, error } = props; - - const getComponent = () => { - if (error) { - return ; - } - - if (completed) { - return ; - } - - if (active) { - return
; - } - return undefined; - }; - return ( -
- {getComponent()} -
- ); -} - const useStyles = makeStyles((theme: Theme) => createStyles({ root: { @@ -141,13 +68,11 @@ type Steps = { status: Status; }; -export const TaskStepper = ({ steps }: { steps: Steps[] }) => { +export const TaskStatusStepper = memo(({ steps }: { steps: Steps[] }) => { const classes = useStyles(); const [activeStep, setActiveStep] = useState(0); - const [expandAll, setExpandAll] = useState(false); const handleStep = (step: number) => { - setExpandAll(false); setActiveStep(step); }; @@ -155,104 +80,50 @@ export const TaskStepper = ({ steps }: { steps: Steps[] }) => { const activeIndex = steps.findIndex(step => ['failed', 'processing'].includes(step.status), ); - setActiveStep(2); + setActiveStep(activeIndex); }, [steps]); return (
- - - {steps.map((step, index) => { const isCompleted = step.status === 'completed'; const isFailed = step.status === 'failed'; - // return ( - // - // ); - return ( {}}> {step.name} - - -
- -
-
); })}
); +}); + +const TaskLogger = memo(({ log }: { log: string }) => { + console.log('rendering my log'); + return ( +
+ +
+ ); +}); + +const TaskActionsBar = () => { + return ( + <> + + + + ); }; -type TaskStepOptions = { - name: string; - isCompleted: boolean; - isFailed: boolean; - expanded: boolean; - handleStep: () => void; - index: number; - log: string; -}; - -export const TaskStep = memo( - ({ - log, - name, - isCompleted, - isFailed, - handleStep, - index, - expanded, - }: TaskStepOptions) => { - const onClick = React.useCallback(() => handleStep(index), [ - handleStep, - index, - ]); - return ( - - - - {name} - - - - -
- -
-
-
- ); - }, -); export const TaskPage = () => { const { taskId } = useParams(); const taskStream = useTaskEventStream(taskId); @@ -274,7 +145,19 @@ export const TaskPage = () => { subtitle={`Activity for task: ${taskId}`} /> - + + + + + + + + + ); diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 7239781b5c..85f3d955ac 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -89,12 +89,9 @@ export const TemplatePage = () => { [setFormState, formState], ); - const [taskId, setTaskId] = useState(undefined); - const handleCreate = async () => { try { const id = await scaffolderApi.scaffold(templateName, formState); - setTaskId(id); navigate(tasks({ taskId: id })); } catch (e) { errorApi.post(e); diff --git a/plugins/scaffolder/src/components/hooks/useEventStream.ts b/plugins/scaffolder/src/components/hooks/useEventStream.ts index 20338463c3..f7b366be56 100644 --- a/plugins/scaffolder/src/components/hooks/useEventStream.ts +++ b/plugins/scaffolder/src/components/hooks/useEventStream.ts @@ -14,13 +14,11 @@ * limitations under the License. */ import { useImmerReducer } from 'use-immer'; -import { useEffect, useState } from 'react'; -import { scaffolderApiRef } from '../../api'; -import { ScaffolderTask } from '../../types'; -import { useDebounce } from 'react-use'; +import { useEffect } from 'react'; +import { scaffolderApiRef, LogEvent } from '../../api'; +import { ScaffolderTask, Status } from '../../types'; import { Subscription, useApi } from '@backstage/core'; -export type Status = 'open' | 'processing' | 'failed' | 'completed'; type Step = { id: string; status: Status; @@ -37,18 +35,14 @@ export type TaskStream = { steps: { [stepId in string]: Step }; }; +type ReducerLogEntry = { + createdAt: string; + body: { stepId?: string; status?: Status; message: string }; +}; + type ReducerAction = - | { - type: 'INIT'; - data: ScaffolderTask; - } - | { - type: 'LOG'; - data: { - createdAt: string; - body: { stepId?: string; status?: Status; message: string }; - }; - } + | { type: 'INIT'; data: ScaffolderTask } + | { type: 'LOGS'; data: ReducerLogEntry[] } | { type: 'COMPLETED' } | { type: 'ERROR'; data: Error }; @@ -67,28 +61,35 @@ function reducer(draft: TaskStream, action: ReducerAction) { return; } - case 'LOG': { - const stepId = action.data.body.stepId ?? 'global'; - const currentStep = draft.steps?.[stepId]; - const logLine = `${action.data.createdAt} ${action.data.body.message}`; + case 'LOGS': { + const entries = action.data; + const logLines = []; + for (const entry of entries) { + const logLine = `${entry.createdAt} ${entry.body.message}`; + logLines.push(logLine); - draft.log.push(logLine); - - if ( - action.data.body.status && - action.data.body.status !== currentStep.status - ) { - currentStep.status = action.data.body.status; - - if (currentStep.status === 'processing') { - currentStep.startedAt = action.data.createdAt; + if (!entry.body.stepId || !draft.steps?.[entry.body.stepId]) { + continue; } - if (['cancelled', 'failed', 'completed'].includes(currentStep.status)) { - currentStep.endedAt = action.data.createdAt; + const currentStep = draft.steps?.[entry.body.stepId]; + + if (entry.body.status && entry.body.status !== currentStep.status) { + currentStep.status = entry.body.status; + + if (currentStep.status === 'processing') { + currentStep.startedAt = entry.createdAt; + } + + if ( + ['cancelled', 'failed', 'completed'].includes(currentStep.status) + ) { + currentStep.endedAt = entry.createdAt; + } } } + draft.log.push(...logLines); return; } @@ -117,17 +118,11 @@ export const useTaskEventStream = (taskId: string): TaskStream => { log: [], steps: {} as { [stepId in string]: Step }, }); - // const [debouncedState, setDebouncedState] = useState(state); - // useDebounce( - // () => { - // setDebouncedState(state); - // }, - // 1000, - // [state], - // ); + useEffect(() => { let didCancel = false; let subscription: Subscription | undefined; + let logPusher: NodeJS.Timeout | undefined; scaffolderApi.getTask(taskId).then( task => { @@ -136,19 +131,40 @@ export const useTaskEventStream = (taskId: string): TaskStream => { } dispatch({ type: 'INIT', data: task }); const observable = scaffolderApi.streamLogs({ taskId }); + + const collectedLogEvents = new Array(); + + function emitLogs() { + if (collectedLogEvents.length) { + const logs = collectedLogEvents.splice( + 0, + collectedLogEvents.length, + ); + dispatch({ type: 'LOGS', data: logs }); + } + } + + logPusher = setInterval(emitLogs, 500); + subscription = observable.subscribe({ next: event => { switch (event.type) { case 'log': - return dispatch({ type: 'LOG', data: event }); + return collectedLogEvents.push(event); default: throw new Error( `Unhandled event type ${event.type} in observer`, ); } }, - error: error => dispatch({ type: 'ERROR', data: error }), - complete: () => dispatch({ type: 'COMPLETED' }), + error: error => { + emitLogs(); + dispatch({ type: 'ERROR', data: error }); + }, + complete: () => { + emitLogs(); + dispatch({ type: 'COMPLETED' }); + }, }); }, error => { @@ -163,9 +179,11 @@ export const useTaskEventStream = (taskId: string): TaskStream => { if (subscription) { subscription.unsubscribe(); } + if (logPusher) { + clearInterval(logPusher); + } }; }, [scaffolderApi, dispatch, taskId]); return state; - // return debouncedState; }; diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index fe1aec5286..0499f4aca2 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -15,6 +15,7 @@ */ import { JsonValue } from '@backstage/config'; +export type Status = 'open' | 'processing' | 'failed' | 'completed'; export type JobStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; export type Job = { id: string;