From 7d574d2050cd69da88f777d24f686e150bdeac7b Mon Sep 17 00:00:00 2001 From: Rick van der Staaij Date: Thu, 12 Oct 2023 14:58:40 +0200 Subject: [PATCH 1/4] Add duration POC --- backend/status/helper.ts | 30 +++++++++++++++++-- backend/status/manager.ts | 4 +-- .../App/Statuses/Status/Process/Process.tsx | 2 +- types/status.ts | 10 +++++-- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/backend/status/helper.ts b/backend/status/helper.ts index 172a06b..58f0027 100644 --- a/backend/status/helper.ts +++ b/backend/status/helper.ts @@ -1,4 +1,4 @@ -import Status, { Process, Stage, State } from 'types/status'; +import Status, { Duration, Process, Stage, State } from 'types/status'; const statusesExpire = 60 * 60 * 24 * 7; // 7 days const statusesTimeout = 60 * 60 * 2; // 2 hours @@ -40,7 +40,7 @@ export const getStuckStatuses = (statuses: Status[]): Status[] => return false; }); -export const fixStatusStates = (status: Status): Status => { +export const processStatusChanges = (status: Status): Status => { const processes = status.processes // Sort processes by creation time .sort( @@ -53,10 +53,34 @@ export const fixStatusStates = (status: Status): Status => { return { ...status, state: determineStatusState(processes), - processes, + processes: patchProcessDurations(processes), }; }; +const setProcessDuration = (process: Process): Duration => { + if (process.state === 'warning') { + return { + ran: process.duration?.ran || 0, + start: process.duration?.start ? process.duration.start : new Date().toUTCString(), + }; + } + + let duration = process.duration?.ran || 0; + if (process.duration?.start) { + duration += Math.abs(new Date(process.duration.start).getTime() - new Date().getTime()); + } + return { + ran: duration, + }; +}; + +export const patchProcessDurations = (processes: Process[]): Process[] => { + return processes.map((process) => ({ + ...process, + duration: setProcessDuration(process), + })); +}; + export const fixStuckStatus = (status: Status): Status => ({ ...status, processes: status.processes.map((process) => { diff --git a/backend/status/manager.ts b/backend/status/manager.ts index 037e6c4..8a422ac 100644 --- a/backend/status/manager.ts +++ b/backend/status/manager.ts @@ -2,7 +2,7 @@ import StorageManager from 'backend/storage/manager'; import Status from 'types/status'; import StatusEvents from './events'; -import { fixStatusStates, fixStuckStatus, getExpiredStatuses, getStuckStatuses } from './helper'; +import { fixStuckStatus, getExpiredStatuses, getStuckStatuses, processStatusChanges } from './helper'; class StatusManager { statuses: Status[] = []; @@ -46,7 +46,7 @@ class StatusManager { } setStatus(status: Status): void { - status = fixStatusStates(status); + status = processStatusChanges(status); let replacedStatus = false; const statuses = [ diff --git a/frontend/App/Statuses/Status/Process/Process.tsx b/frontend/App/Statuses/Status/Process/Process.tsx index f4ba635..ed8d2b2 100644 --- a/frontend/App/Statuses/Status/Process/Process.tsx +++ b/frontend/App/Statuses/Status/Process/Process.tsx @@ -56,7 +56,7 @@ const Process = ({ process }: Props): ReactElement => { return (
- {process.title} + {process.title} ({process.duration?.ran})
{!!process.stages && process.stages.length > 0 && ( {process.stages.map((stage) => renderStage(stage))} diff --git a/types/status.ts b/types/status.ts index 230a179..3b7d45b 100644 --- a/types/status.ts +++ b/types/status.ts @@ -11,12 +11,17 @@ export type StepState = | 'timeout' | 'stopped'; +export type Duration = { + start?: string; + ran: number; +}; + export type Step = { id: string; title: string; state: StepState; time: string; - duration?: number; + duration?: Duration; }; export type Stage = { @@ -25,6 +30,7 @@ export type Stage = { state: StepState; steps: Step[]; time: string; + duration?: Duration; }; export type Process = { @@ -33,7 +39,7 @@ export type Process = { state: State; stages: Stage[]; time: string; - duration?: number; + duration?: Duration; }; export type Status = { From 56f2f9f1eb87b7cd4ae492b1a886fc5e1519c5d4 Mon Sep 17 00:00:00 2001 From: Rick van der Staaij Date: Fri, 13 Oct 2023 15:32:02 +0200 Subject: [PATCH 2/4] Add process duration timer --- backend/status/helper.ts | 31 +++++++----- cypress/integration/github/push.spec.js | 39 +++++++-------- .../Statuses/Status/Process/Process.style.tsx | 10 ++++ .../App/Statuses/Status/Process/Process.tsx | 20 +++++++- .../App/Statuses/Status/Process/RunTime.tsx | 48 +++++++++++++++++++ 5 files changed, 115 insertions(+), 33 deletions(-) create mode 100644 frontend/App/Statuses/Status/Process/RunTime.tsx diff --git a/backend/status/helper.ts b/backend/status/helper.ts index 58f0027..fdada36 100644 --- a/backend/status/helper.ts +++ b/backend/status/helper.ts @@ -1,4 +1,4 @@ -import Status, { Duration, Process, Stage, State } from 'types/status'; +import Status, { Duration, Process, Stage, State, StepState } from 'types/status'; const statusesExpire = 60 * 60 * 24 * 7; // 7 days const statusesTimeout = 60 * 60 * 2; // 2 hours @@ -57,29 +57,36 @@ export const processStatusChanges = (status: Status): Status => { }; }; -const setProcessDuration = (process: Process): Duration => { - if (process.state === 'warning') { +const setDuration = (state: StepState | Process['state'], currentDuration?: Duration): Duration => { + if (state === 'running' || state === 'warning') { return { - ran: process.duration?.ran || 0, - start: process.duration?.start ? process.duration.start : new Date().toUTCString(), + ran: currentDuration?.ran || 0, + start: currentDuration?.start ? currentDuration.start : new Date().toUTCString(), }; } - let duration = process.duration?.ran || 0; - if (process.duration?.start) { - duration += Math.abs(new Date(process.duration.start).getTime() - new Date().getTime()); + let duration = currentDuration?.ran || 0; + if (currentDuration?.start) { + duration += Math.abs(new Date(currentDuration.start).getTime() - new Date().getTime()); } return { ran: duration, }; }; -export const patchProcessDurations = (processes: Process[]): Process[] => { - return processes.map((process) => ({ +export const patchProcessDurations = (processes: Process[]): Process[] => + processes.map((process) => ({ ...process, - duration: setProcessDuration(process), + stages: process.stages.map((stage) => ({ + ...stage, + steps: stage.steps.map((step) => ({ + ...step, + duration: setDuration(step.state, step.duration), + })), + duration: setDuration(stage.state, stage.duration), + })), + duration: setDuration(process.state, process.duration), })); -}; export const fixStuckStatus = (status: Status): Status => ({ ...status, diff --git a/cypress/integration/github/push.spec.js b/cypress/integration/github/push.spec.js index ca22143..15415bf 100644 --- a/cypress/integration/github/push.spec.js +++ b/cypress/integration/github/push.spec.js @@ -1,26 +1,27 @@ /// context('A running GitHub push', () => { - it('opens the CIMonitor dashboard', () => { - cy.visit('/'); - }); + it('opens the CIMonitor dashboard', () => { + cy.visit('/'); + }); - it('pushes a GitHub action flow', () => { - for (let count = 1; count <= 8; count++) { - if (count > 1) { - cy.wait(1000); - } - cy.github(`push-failed/${count}`); - } + it('pushes a GitHub failed action flow', () => { + for (let count = 1; count <= 8; count++) { + if (count > 1) { + cy.wait(1000); + } + cy.github(`push-failed/${count}`); + } - cy.wait(2000); + cy.wait(2000); + }); - // Push all created events - for (let count = 1; count <= 14; count++) { - if (count > 1) { - cy.wait(1000); - } - cy.github(`push/${count}`); - } - }); + it('pushes a GitHub successful action flow', () => { + for (let count = 1; count <= 14; count++) { + if (count > 1) { + cy.wait(1000); + } + cy.github(`push/${count}`); + } + }); }); diff --git a/frontend/App/Statuses/Status/Process/Process.style.tsx b/frontend/App/Statuses/Status/Process/Process.style.tsx index cc316fd..40ad920 100644 --- a/frontend/App/Statuses/Status/Process/Process.style.tsx +++ b/frontend/App/Statuses/Status/Process/Process.style.tsx @@ -130,5 +130,15 @@ export const StageContainer = styled.div` `; export const Details = styled.div` + display: flex; + flex-direction: row; +`; + +export const DetailTitle = styled.div` + ${ellipsis}; + flex-grow: 1; +`; + +export const DetailRunTime = styled.div` ${ellipsis}; `; diff --git a/frontend/App/Statuses/Status/Process/Process.tsx b/frontend/App/Statuses/Status/Process/Process.tsx index ed8d2b2..93e9210 100644 --- a/frontend/App/Statuses/Status/Process/Process.tsx +++ b/frontend/App/Statuses/Status/Process/Process.tsx @@ -1,10 +1,21 @@ import { ReactElement } from 'react'; -import { Details, ProcessContainer, Stage, StageContainer, Stages, Step } from './Process.style'; +import { + DetailRunTime, + Details, + DetailTitle, + ProcessContainer, + Stage, + StageContainer, + Stages, + Step, +} from './Process.style'; import Icon from '/frontend/components/Icon'; import useSetting from '/frontend/hooks/useSetting'; +import RunTime from './RunTime'; + import { Process as ProcessType, Stage as StageType, State, Step as StepType, StepState } from '/types/status'; const getStateIcon = (state: StepState, processState: State = 'warning') => { @@ -56,7 +67,12 @@ const Process = ({ process }: Props): ReactElement => { return (
- {process.title} ({process.duration?.ran}) + + {process.title} + + + +
{!!process.stages && process.stages.length > 0 && ( {process.stages.map((stage) => renderStage(stage))} diff --git a/frontend/App/Statuses/Status/Process/RunTime.tsx b/frontend/App/Statuses/Status/Process/RunTime.tsx new file mode 100644 index 0000000..9db1a03 --- /dev/null +++ b/frontend/App/Statuses/Status/Process/RunTime.tsx @@ -0,0 +1,48 @@ +import { ReactElement, useEffect, useState } from 'react'; + +import { Duration } from '/types/status'; + +const getTimeRan = (milliseconds: number = 0) => { + if (milliseconds === 0) { + return ''; + } + + let seconds = Math.round(milliseconds / 1000); + const minutes = Math.floor(seconds / 60); + seconds = seconds - minutes * 60; + + return `${minutes}:${seconds < 10 ? `0${seconds}` : seconds}`; +}; + +type Props = { + duration?: Duration; +}; + +const RunTime = ({ duration }: Props): ReactElement | null => { + const [runDuration, setRunDuration] = useState(duration?.ran || 0); + + useEffect(() => { + let intervalId = undefined; + + const updateTime = (duration: Duration) => { + const ran = duration?.ran || 0; + + if (!duration?.start) { + return ran; + } + + setRunDuration(Math.abs(new Date(duration.start).getTime() - new Date().getTime()) + ran); + }; + + if (duration.start) { + intervalId = setInterval(() => updateTime(duration), 1000); + } + + updateTime(duration); + + return () => clearInterval(intervalId); + }, [duration, setRunDuration]); + return {getTimeRan(runDuration)}; +}; + +export default RunTime; From 2f39728d774ad76cb2b6094fab5519883af345f6 Mon Sep 17 00:00:00 2001 From: Rick van der Staaij Date: Fri, 13 Oct 2023 15:34:19 +0200 Subject: [PATCH 3/4] Don't shrink runtime --- frontend/App/Statuses/Status/Process/Process.style.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/App/Statuses/Status/Process/Process.style.tsx b/frontend/App/Statuses/Status/Process/Process.style.tsx index 40ad920..ecbb553 100644 --- a/frontend/App/Statuses/Status/Process/Process.style.tsx +++ b/frontend/App/Statuses/Status/Process/Process.style.tsx @@ -132,13 +132,17 @@ export const StageContainer = styled.div` export const Details = styled.div` display: flex; flex-direction: row; + gap: 0.5rem; `; export const DetailTitle = styled.div` ${ellipsis}; flex-grow: 1; + flex-shrink: 1; `; export const DetailRunTime = styled.div` ${ellipsis}; + flex-grow: 0; + flex-shrink: 0; `; From 9bca594b73f2a7d00f3cf3522e1c5d8acf158dda Mon Sep 17 00:00:00 2001 From: Rick van der Staaij Date: Fri, 13 Oct 2023 16:40:30 +0200 Subject: [PATCH 4/4] Show duration timer for the latest process only --- .../Statuses/Status/Process/Process.style.tsx | 14 ------------- .../App/Statuses/Status/Process/Process.tsx | 20 ++----------------- .../Statuses/Status/{Process => }/RunTime.tsx | 0 frontend/App/Statuses/Status/Status.tsx | 8 ++++++++ 4 files changed, 10 insertions(+), 32 deletions(-) rename frontend/App/Statuses/Status/{Process => }/RunTime.tsx (100%) diff --git a/frontend/App/Statuses/Status/Process/Process.style.tsx b/frontend/App/Statuses/Status/Process/Process.style.tsx index ecbb553..cc316fd 100644 --- a/frontend/App/Statuses/Status/Process/Process.style.tsx +++ b/frontend/App/Statuses/Status/Process/Process.style.tsx @@ -130,19 +130,5 @@ export const StageContainer = styled.div` `; export const Details = styled.div` - display: flex; - flex-direction: row; - gap: 0.5rem; -`; - -export const DetailTitle = styled.div` - ${ellipsis}; - flex-grow: 1; - flex-shrink: 1; -`; - -export const DetailRunTime = styled.div` ${ellipsis}; - flex-grow: 0; - flex-shrink: 0; `; diff --git a/frontend/App/Statuses/Status/Process/Process.tsx b/frontend/App/Statuses/Status/Process/Process.tsx index 93e9210..f4ba635 100644 --- a/frontend/App/Statuses/Status/Process/Process.tsx +++ b/frontend/App/Statuses/Status/Process/Process.tsx @@ -1,21 +1,10 @@ import { ReactElement } from 'react'; -import { - DetailRunTime, - Details, - DetailTitle, - ProcessContainer, - Stage, - StageContainer, - Stages, - Step, -} from './Process.style'; +import { Details, ProcessContainer, Stage, StageContainer, Stages, Step } from './Process.style'; import Icon from '/frontend/components/Icon'; import useSetting from '/frontend/hooks/useSetting'; -import RunTime from './RunTime'; - import { Process as ProcessType, Stage as StageType, State, Step as StepType, StepState } from '/types/status'; const getStateIcon = (state: StepState, processState: State = 'warning') => { @@ -67,12 +56,7 @@ const Process = ({ process }: Props): ReactElement => { return (
- - {process.title} - - - - + {process.title}
{!!process.stages && process.stages.length > 0 && ( {process.stages.map((stage) => renderStage(stage))} diff --git a/frontend/App/Statuses/Status/Process/RunTime.tsx b/frontend/App/Statuses/Status/RunTime.tsx similarity index 100% rename from frontend/App/Statuses/Status/Process/RunTime.tsx rename to frontend/App/Statuses/Status/RunTime.tsx diff --git a/frontend/App/Statuses/Status/Status.tsx b/frontend/App/Statuses/Status/Status.tsx index b051c4e..46802bd 100644 --- a/frontend/App/Statuses/Status/Status.tsx +++ b/frontend/App/Statuses/Status/Status.tsx @@ -2,6 +2,7 @@ import { ReactElement } from 'react'; import { Body, Box, Boxes, Container, Details, LinkBox, Project, UserImage } from './Status.style'; +import RunTime from '/frontend/App/Statuses/Status/RunTime'; import Icon from '/frontend/components/Icon'; import useSetting from '/frontend/hooks/useSetting'; @@ -28,6 +29,8 @@ const pettyUrl = (url: string) => const Statuses = ({ status }: Props): ReactElement => { const showAvatars = useSetting('showAvatars'); + const activeProcess = status.processes[0] || null; + return ( @@ -56,6 +59,11 @@ const Statuses = ({ status }: Props): ReactElement => { + {!!activeProcess && ( + + + + )} {!!status.userImage && showAvatars && (