Unifify logs

This commit is contained in:
Johan Haals
2021-02-10 15:24:00 +01:00
parent efbd9c9a99
commit 2876700adb
5 changed files with 104 additions and 201 deletions
+7 -3
View File
@@ -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<ScaffolderApi>({
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;
@@ -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 <Cancel className={classes.error} />;
}
if (completed) {
return <Check className={classes.completed} />;
}
if (active) {
return <div className={classes.circle} />;
}
return undefined;
};
return (
<div
className={clsx(classes.root, {
[classes.active]: active,
[classes.error]: error,
})}
>
{getComponent()}
</div>
);
}
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 (
<div className={classes.root}>
<Button variant="outlined" onClick={() => setExpandAll(true)}>
Expand All
</Button>
<Button variant="outlined">Retry</Button>
<Button variant="outlined">Raw Log</Button>
<Stepper activeStep={activeStep} orientation="vertical" nonLinear>
{steps.map((step, index) => {
const isCompleted = step.status === 'completed';
const isFailed = step.status === 'failed';
// return (
// <TaskStep
// key={String(index)}
// log={step.log.length ? step.log.join('\n') : 'please wait'}
// expanded={expandAll}
// name={step.name}
// isFailed={isFailed}
// isCompleted={isCompleted}
// handleStep={handleStep}
// index={index}
// />
// );
return (
<Step key={String(index)} expanded>
<StepButton onClick={() => {}}>
<StepLabel
StepIconProps={{ completed: isCompleted, error: isFailed }}
StepIconComponent={QontoStepIcon}
>
{step.name}
</StepLabel>
</StepButton>
<StepContent>
<div style={{ height: '50vh' }}>
<LazyLog
extraLines={1}
text={step.log.length ? step.log.join('\n') : 'please wait'}
/>
</div>
</StepContent>
</Step>
);
})}
</Stepper>
</div>
);
});
const TaskLogger = memo(({ log }: { log: string }) => {
console.log('rendering my log');
return (
<div style={{ height: '80vh' }}>
<LazyLog text={log} extraLines={1} follow />
</div>
);
});
const TaskActionsBar = () => {
return (
<>
<Button variant="outlined">Retry</Button>
<Button variant="outlined">Raw Log</Button>
</>
);
};
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 (
<Step key={String(index)} expanded={expanded}>
<StepButton onClick={onClick}>
<StepLabel
StepIconProps={{ completed: isCompleted, error: isFailed }}
StepIconComponent={QontoStepIcon}
>
{name}
</StepLabel>
</StepButton>
<StepContent>
<div style={{ height: '50vh' }}>
<LazyLog extraLines={1} text={log} />
</div>
</StepContent>
</Step>
);
},
);
export const TaskPage = () => {
const { taskId } = useParams();
const taskStream = useTaskEventStream(taskId);
@@ -274,7 +145,19 @@ export const TaskPage = () => {
subtitle={`Activity for task: ${taskId}`}
/>
<Content>
<TaskStepper steps={steps} />
<TaskActionsBar />
<Grid container>
<Grid item xs={2}>
<TaskStatusStepper steps={steps} />
</Grid>
<Grid item xs={10}>
<TaskLogger
log={
taskStream.log.length ? taskStream.log.join('\n') : 'loading...'
}
/>
</Grid>
</Grid>
</Content>
</Page>
);
@@ -89,12 +89,9 @@ export const TemplatePage = () => {
[setFormState, formState],
);
const [taskId, setTaskId] = useState<string | undefined>(undefined);
const handleCreate = async () => {
try {
const id = await scaffolderApi.scaffold(templateName, formState);
setTaskId(id);
navigate(tasks({ taskId: id }));
} catch (e) {
errorApi.post(e);
@@ -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<LogEvent>();
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;
};
+1
View File
@@ -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;