From fd151da86ff8ca09f58fe1ba538d699cc6c22db1 Mon Sep 17 00:00:00 2001 From: Andy Pols Date: Wed, 4 Mar 2026 18:44:25 +0000 Subject: [PATCH 1/9] chore: add CustomTabs to show different push data --- src/ui/views/PushDetails/PushDetails.tsx | 64 ++++++------------- .../components/CommitDataTable.tsx | 40 ++++++++++++ 2 files changed, 60 insertions(+), 44 deletions(-) create mode 100644 src/ui/views/PushDetails/components/CommitDataTable.tsx diff --git a/src/ui/views/PushDetails/PushDetails.tsx b/src/ui/views/PushDetails/PushDetails.tsx index 05392958d..f3b1a81b0 100644 --- a/src/ui/views/PushDetails/PushDetails.tsx +++ b/src/ui/views/PushDetails/PushDetails.tsx @@ -8,25 +8,22 @@ import Card from '../../components/Card/Card'; import CardIcon from '../../components/Card/CardIcon'; import CardBody from '../../components/Card/CardBody'; import CardHeader, { CardHeaderColor } from '../../components/Card/CardHeader'; -import CardFooter from '../../components/Card/CardFooter'; import Button from '../../components/CustomButtons/Button'; +import CustomTabs from '../../components/CustomTabs/CustomTabs'; +import CommitDataTable from './components/CommitDataTable'; import Diff from './components/Diff'; import Attestation from './components/Attestation'; import AttestationInfo from './components/AttestationInfo'; import RejectionInfo from './components/RejectionInfo'; import Reject from './components/Reject'; -import Table from '@material-ui/core/Table'; -import TableBody from '@material-ui/core/TableBody'; -import TableHead from '@material-ui/core/TableHead'; -import TableRow from '@material-ui/core/TableRow'; -import TableCell from '@material-ui/core/TableCell'; import { getPush, authorisePush, rejectPush, cancelPush } from '../../services/git-push'; import type { ServiceResult } from '../../services/errors'; -import { CheckCircle, Visibility, Cancel, Block } from '@material-ui/icons'; +import { CheckCircle, Visibility, Cancel, Block, List as ListIcon } from '@material-ui/icons'; +import CodeIcon from '@material-ui/icons/Code'; import Snackbar from '@material-ui/core/Snackbar'; import { PushActionView } from '../../types'; import { trimPrefixRefsHeads, trimTrailingDotGit } from '../../../db/helper'; -import { generateEmailLink, getGitProvider } from '../../utils'; +import { getGitProvider } from '../../utils'; const Dashboard: React.FC = () => { const { id } = useParams<{ id: string }>(); @@ -233,44 +230,23 @@ const Dashboard: React.FC = () => { - - -

{headerData.title}

-
- - - - - Timestamp - Committer - Author - Message - - - - {push.commitData?.map((c) => ( - - - {moment.unix(Number(c.commitTimestamp || 0)).toString()} - - {generateEmailLink(c.committer, c.committerEmail)} - {generateEmailLink(c.author, c.authorEmail)} - {c.message} - - ))} - -
-
-
- - - - - - - + , + }, + { + tabName: 'Changes', + tabIcon: CodeIcon, + tabContent: , + }, + ]} + /> diff --git a/src/ui/views/PushDetails/components/CommitDataTable.tsx b/src/ui/views/PushDetails/components/CommitDataTable.tsx new file mode 100644 index 000000000..a1964b88a --- /dev/null +++ b/src/ui/views/PushDetails/components/CommitDataTable.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import moment from 'moment'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import TableCell from '@material-ui/core/TableCell'; +import { generateEmailLink } from '../../../utils'; +import { CommitData } from '../../../../proxy/processors/types'; + +interface CommitDataTableProps { + commitData: CommitData[]; +} + +const CommitDataTable: React.FC = ({ commitData }) => { + return ( + + + + Timestamp + Committer + Author + Message + + + + {commitData.map((c) => ( + + {moment.unix(Number(c.commitTimestamp || 0)).toString()} + {generateEmailLink(c.committer, c.committerEmail)} + {generateEmailLink(c.author, c.authorEmail)} + {c.message} + + ))} + +
+ ); +}; + +export default CommitDataTable; From 7b504f5e012849996e3bb547bdb53d0637390eb2 Mon Sep 17 00:00:00 2001 From: Andy Pols Date: Wed, 4 Mar 2026 18:56:24 +0000 Subject: [PATCH 2/9] chore: add StepsTimeline to show results from each push action --- src/ui/views/PushDetails/PushDetails.tsx | 7 + .../PushDetails/components/StepsTimeline.tsx | 291 ++++++++++++++++++ 2 files changed, 298 insertions(+) create mode 100644 src/ui/views/PushDetails/components/StepsTimeline.tsx diff --git a/src/ui/views/PushDetails/PushDetails.tsx b/src/ui/views/PushDetails/PushDetails.tsx index f3b1a81b0..11c57cbd1 100644 --- a/src/ui/views/PushDetails/PushDetails.tsx +++ b/src/ui/views/PushDetails/PushDetails.tsx @@ -12,6 +12,7 @@ import Button from '../../components/CustomButtons/Button'; import CustomTabs from '../../components/CustomTabs/CustomTabs'; import CommitDataTable from './components/CommitDataTable'; import Diff from './components/Diff'; +import StepsTimeline from './components/StepsTimeline'; import Attestation from './components/Attestation'; import AttestationInfo from './components/AttestationInfo'; import RejectionInfo from './components/RejectionInfo'; @@ -20,6 +21,7 @@ import { getPush, authorisePush, rejectPush, cancelPush } from '../../services/g import type { ServiceResult } from '../../services/errors'; import { CheckCircle, Visibility, Cancel, Block, List as ListIcon } from '@material-ui/icons'; import CodeIcon from '@material-ui/icons/Code'; +import TimelineIcon from '@material-ui/icons/Timeline'; import Snackbar from '@material-ui/core/Snackbar'; import { PushActionView } from '../../types'; import { trimPrefixRefsHeads, trimTrailingDotGit } from '../../../db/helper'; @@ -245,6 +247,11 @@ const Dashboard: React.FC = () => { tabIcon: CodeIcon, tabContent: , }, + { + tabName: 'Steps', + tabIcon: TimelineIcon, + tabContent: , + }, ]} /> diff --git a/src/ui/views/PushDetails/components/StepsTimeline.tsx b/src/ui/views/PushDetails/components/StepsTimeline.tsx new file mode 100644 index 000000000..2f34b38fe --- /dev/null +++ b/src/ui/views/PushDetails/components/StepsTimeline.tsx @@ -0,0 +1,291 @@ +import React, { useState } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Accordion from '@material-ui/core/Accordion'; +import AccordionSummary from '@material-ui/core/AccordionSummary'; +import AccordionDetails from '@material-ui/core/AccordionDetails'; +import Typography from '@material-ui/core/Typography'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import CheckCircleIcon from '@material-ui/icons/CheckCircle'; +import ErrorIcon from '@material-ui/icons/Error'; +import WarningIcon from '@material-ui/icons/Warning'; +import Chip from '@material-ui/core/Chip'; +import Box from '@material-ui/core/Box'; +import { StepData } from '../../../../proxy/actions/Step'; + +const useStyles = makeStyles((theme) => ({ + root: { + width: '100%', + }, + summary: { + padding: theme.spacing(2), + marginBottom: theme.spacing(2), + backgroundColor: '#f5f5f5', + borderRadius: theme.spacing(1), + }, + summaryTitle: { + marginTop: 0, + marginBottom: theme.spacing(1), + }, + summaryStats: { + display: 'flex', + gap: theme.spacing(2), + flexWrap: 'wrap', + }, + timeline: { + position: 'relative', + paddingLeft: theme.spacing(4), + '&::before': { + content: '""', + position: 'absolute', + left: '19px', + top: '20px', + bottom: '20px', + width: '2px', + backgroundColor: '#e0e0e0', + }, + }, + stepAccordion: { + marginBottom: theme.spacing(2), + boxShadow: '0 2px 4px rgba(0,0,0,0.1)', + '&::before': { + display: 'none', + }, + }, + stepSummary: { + minHeight: '56px !important', + '& .MuiAccordionSummary-content': { + alignItems: 'center', + margin: '12px 0', + }, + }, + stepIcon: { + position: 'absolute', + left: '-28px', + backgroundColor: 'white', + borderRadius: '50%', + padding: '2px', + zIndex: 1, + }, + stepContent: { + display: 'flex', + alignItems: 'center', + gap: theme.spacing(2), + flex: 1, + }, + stepName: { + fontWeight: 500, + fontFamily: 'monospace', + fontSize: '14px', + }, + stepDetails: { + display: 'block', + padding: theme.spacing(2), + backgroundColor: '#fafafa', + }, + messageBox: { + padding: theme.spacing(1.5), + marginBottom: theme.spacing(2), + borderRadius: theme.spacing(0.5), + fontFamily: 'monospace', + fontSize: '13px', + }, + errorMessage: { + backgroundColor: '#ffebee', + color: '#c62828', + border: '1px solid #ef9a9a', + }, + blockedMessage: { + backgroundColor: '#fff3e0', + color: '#e65100', + border: '1px solid #ffb74d', + }, + logsContainer: { + marginTop: theme.spacing(1), + }, + logsTitle: { + fontWeight: 'bold', + marginBottom: theme.spacing(1), + }, + logItem: { + padding: theme.spacing(1), + marginBottom: theme.spacing(0.5), + backgroundColor: '#f5f5f5', + borderLeft: '3px solid #9e9e9e', + fontFamily: 'monospace', + fontSize: '12px', + wordBreak: 'break-word', + }, +})); + +interface StepsTimelineProps { + steps: StepData[]; + expandStepId?: string; +} + +const StepsTimeline: React.FC = ({ steps, expandStepId }) => { + const classes = useStyles(); + const [expanded, setExpanded] = useState(false); + + React.useEffect(() => { + if (expandStepId) { + setExpanded(expandStepId); + } + }, [expandStepId]); + + const isLargeStep = (stepName: string) => stepName === 'writePack' || stepName === 'diff'; + + const handleChange = + (panel: string, stepName: string) => + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + (event: React.ChangeEvent<{}>, isExpanded: boolean) => { + if (isLargeStep(stepName)) { + return; + } + setExpanded(isExpanded ? panel : false); + }; + + const getStepIcon = (step: StepData) => { + if (step.error) { + return ; + } + if (step.blocked) { + return ; + } + return ; + }; + + const getStepStatus = (step: StepData) => { + if (step.error) { + return ( + + ); + } + if (step.blocked) { + return ( + + ); + } + return ( + + ); + }; + + const totalSteps = steps.length; + const errorSteps = steps.filter((s) => s.error).length; + const blockedSteps = steps.filter((s) => s.blocked).length; + const successSteps = totalSteps - errorSteps - blockedSteps; + + return ( +
+ +

Push Validation Steps Summary

+
+ } + label={`${successSteps} Successful`} + style={{ backgroundColor: '#388e3c', color: 'white' }} + /> + {errorSteps > 0 && ( + } + label={`${errorSteps} Error${errorSteps > 1 ? 's' : ''}`} + style={{ backgroundColor: '#d32f2f', color: 'white' }} + /> + )} + {blockedSteps > 0 && ( + } + label={`${blockedSteps} Blocked`} + style={{ backgroundColor: '#f57c00', color: 'white' }} + /> + )} + +
+
+ +
+ {steps.map((step) => ( + + } + className={classes.stepSummary} + > +
{getStepIcon(step)}
+
+ {step.stepName} + {getStepStatus(step)} +
+
+ +
+ {step.error && step.errorMessage && ( +
+ Error: {step.errorMessage} +
+ )} + {step.blocked && step.blockedMessage && ( +
+ Blocked: {step.blockedMessage} +
+ )} + {step.content && ( +
+ + Content: + +
+                      {typeof step.content === 'string'
+                        ? step.content
+                        : JSON.stringify(step.content, null, 2)}
+                    
+
+ )} + {step.logs && step.logs.length > 0 && ( +
+ + Logs ({step.logs.length}): + + {step.logs.map((log: string, logIndex: number) => ( +
+ {log} +
+ ))} +
+ )} + {!step.error && + !step.blocked && + !step.content && + (!step.logs || step.logs.length === 0) && ( + + This step completed successfully with no additional details. + + )} +
+
+
+ ))} +
+
+ ); +}; + +export default StepsTimeline; From 6706274914fa170b50cf717c954e6db649d43123 Mon Sep 17 00:00:00 2001 From: Andy Pols Date: Wed, 4 Mar 2026 19:04:14 +0000 Subject: [PATCH 3/9] chore: add optional numeric badge --- src/ui/components/CustomTabs/CustomTabs.tsx | 11 ++++++++++- src/ui/views/PushDetails/PushDetails.tsx | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/ui/components/CustomTabs/CustomTabs.tsx b/src/ui/components/CustomTabs/CustomTabs.tsx index f811139bd..047840d2e 100644 --- a/src/ui/components/CustomTabs/CustomTabs.tsx +++ b/src/ui/components/CustomTabs/CustomTabs.tsx @@ -8,6 +8,7 @@ import CardBody from '../Card/CardBody'; import CardHeader from '../Card/CardHeader'; import styles from '../../assets/jss/material-dashboard-react/components/customTabsStyle'; import { SvgIconProps } from '@material-ui/core'; +import Badge from '@material-ui/core/Badge'; const useStyles = makeStyles(styles as any); @@ -17,6 +18,7 @@ export type TabItem = { tabName: string; tabIcon?: React.ComponentType; tabContent: React.ReactNode; + badge?: number; }; interface CustomTabsProps { @@ -65,6 +67,13 @@ const CustomTabs: React.FC = ({ > {tabs.map((prop, key) => { const icon = prop.tabIcon ? { icon: } : {}; + const label = prop.badge ? ( + + {prop.tabName} + + ) : ( + prop.tabName + ); return ( = ({ wrapper: classes.tabWrapper, }} key={key} - label={prop.tabName} + label={label} {...icon} /> ); diff --git a/src/ui/views/PushDetails/PushDetails.tsx b/src/ui/views/PushDetails/PushDetails.tsx index 11c57cbd1..f34987f46 100644 --- a/src/ui/views/PushDetails/PushDetails.tsx +++ b/src/ui/views/PushDetails/PushDetails.tsx @@ -98,6 +98,8 @@ const Dashboard: React.FC = () => { if (isError) throw new Error(message || 'Something went wrong ...'); if (!push) return
No push data found
; + const errorCount = push.steps.filter((step) => step.error).length; + let headerData: { title: string; color: CardHeaderColor } = { title: 'Pending', color: 'warning', @@ -251,6 +253,7 @@ const Dashboard: React.FC = () => { tabName: 'Steps', tabIcon: TimelineIcon, tabContent: , + badge: errorCount > 0 ? errorCount : undefined, }, ]} /> From 8f95c5c52d335795548163af7789a550448e84dc Mon Sep 17 00:00:00 2001 From: Andy Pols Date: Wed, 4 Mar 2026 19:25:22 +0000 Subject: [PATCH 4/9] chore: make header the primary colour to match dashboard --- src/ui/views/PushDetails/PushDetails.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/views/PushDetails/PushDetails.tsx b/src/ui/views/PushDetails/PushDetails.tsx index f34987f46..f77971a05 100644 --- a/src/ui/views/PushDetails/PushDetails.tsx +++ b/src/ui/views/PushDetails/PushDetails.tsx @@ -237,7 +237,7 @@ const Dashboard: React.FC = () => { Date: Mon, 9 Mar 2026 12:43:13 +0000 Subject: [PATCH 5/9] chore: add missing license headers --- .../PushDetails/components/CommitDataTable.tsx | 16 ++++++++++++++++ .../PushDetails/components/StepsTimeline.tsx | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/ui/views/PushDetails/components/CommitDataTable.tsx b/src/ui/views/PushDetails/components/CommitDataTable.tsx index a1964b88a..938c6ef81 100644 --- a/src/ui/views/PushDetails/components/CommitDataTable.tsx +++ b/src/ui/views/PushDetails/components/CommitDataTable.tsx @@ -1,3 +1,19 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import React from 'react'; import moment from 'moment'; import Table from '@material-ui/core/Table'; diff --git a/src/ui/views/PushDetails/components/StepsTimeline.tsx b/src/ui/views/PushDetails/components/StepsTimeline.tsx index 2f34b38fe..3a39ff6f0 100644 --- a/src/ui/views/PushDetails/components/StepsTimeline.tsx +++ b/src/ui/views/PushDetails/components/StepsTimeline.tsx @@ -1,3 +1,19 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import React, { useState } from 'react'; import { makeStyles } from '@material-ui/core/styles'; import Accordion from '@material-ui/core/Accordion'; From c9a8bb331c8c2b5948d041e2a5f23aa592a1cc7c Mon Sep 17 00:00:00 2001 From: Andy Pols Date: Wed, 11 Mar 2026 16:50:21 +0000 Subject: [PATCH 6/9] chore: ensure Timeline gets steps --- src/ui/views/PushDetails/PushDetails.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/views/PushDetails/PushDetails.tsx b/src/ui/views/PushDetails/PushDetails.tsx index 5e2c38737..6ece97756 100644 --- a/src/ui/views/PushDetails/PushDetails.tsx +++ b/src/ui/views/PushDetails/PushDetails.tsx @@ -268,7 +268,7 @@ const Dashboard: React.FC = () => { { tabName: 'Steps', tabIcon: TimelineIcon, - tabContent: , + tabContent: , badge: errorCount > 0 ? errorCount : undefined, }, ]} From a94864adf57a4eb0964a7f89cf2463e99c7b5fd2 Mon Sep 17 00:00:00 2001 From: Andy Pols Date: Wed, 11 Mar 2026 17:15:52 +0000 Subject: [PATCH 7/9] chore: ensure errorCount does not fail in steps undefined --- src/ui/views/PushDetails/PushDetails.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/views/PushDetails/PushDetails.tsx b/src/ui/views/PushDetails/PushDetails.tsx index 6ece97756..8ca0065c8 100644 --- a/src/ui/views/PushDetails/PushDetails.tsx +++ b/src/ui/views/PushDetails/PushDetails.tsx @@ -114,7 +114,7 @@ const Dashboard: React.FC = () => { if (isError) throw new Error(message || 'Something went wrong ...'); if (!push) return
No push data found
; - const errorCount = push.steps.filter((step) => step.error).length; + const errorCount = push.steps?.filter((step) => step.error).length ?? 0; let headerData: { title: string; color: CardHeaderColor } = { title: 'Pending', From 17dc7f2485d550540edd96b5ccbb0602c5d6400e Mon Sep 17 00:00:00 2001 From: Andy Pols Date: Wed, 11 Mar 2026 17:26:53 +0000 Subject: [PATCH 8/9] chore: open first error step --- .../views/PushDetails/components/StepsTimeline.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/ui/views/PushDetails/components/StepsTimeline.tsx b/src/ui/views/PushDetails/components/StepsTimeline.tsx index 3a39ff6f0..2480b180d 100644 --- a/src/ui/views/PushDetails/components/StepsTimeline.tsx +++ b/src/ui/views/PushDetails/components/StepsTimeline.tsx @@ -135,18 +135,13 @@ const useStyles = makeStyles((theme) => ({ interface StepsTimelineProps { steps: StepData[]; - expandStepId?: string; } -const StepsTimeline: React.FC = ({ steps, expandStepId }) => { +const StepsTimeline: React.FC = ({ steps }) => { const classes = useStyles(); - const [expanded, setExpanded] = useState(false); - - React.useEffect(() => { - if (expandStepId) { - setExpanded(expandStepId); - } - }, [expandStepId]); + const [expanded, setExpanded] = useState( + () => steps.find((s) => s.error)?.id ?? false, + ); const isLargeStep = (stepName: string) => stepName === 'writePack' || stepName === 'diff'; From 00b6a06827f203a3b05355ba5da612289fa93828 Mon Sep 17 00:00:00 2001 From: Andy Pols Date: Thu, 19 Mar 2026 13:28:13 +0000 Subject: [PATCH 9/9] chore: show message when no commit data --- src/ui/views/PushDetails/components/CommitDataTable.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ui/views/PushDetails/components/CommitDataTable.tsx b/src/ui/views/PushDetails/components/CommitDataTable.tsx index 938c6ef81..5253cec77 100644 --- a/src/ui/views/PushDetails/components/CommitDataTable.tsx +++ b/src/ui/views/PushDetails/components/CommitDataTable.tsx @@ -29,6 +29,10 @@ interface CommitDataTableProps { } const CommitDataTable: React.FC = ({ commitData }) => { + if (commitData.length === 0) { + return

No commits found for this push.

; + } + return (