Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f74342f
chore: return reject success status
andypols Nov 29, 2025
4dd20ed
chore: return authorise success status
andypols Nov 29, 2025
6d87574
chore: extract StatusIcon
andypols Nov 29, 2025
9ec6c16
chore: extract ApprovalBadge
andypols Nov 29, 2025
3950cdc
chore: extract PushStatusHeader
andypols Nov 29, 2025
3a4e599
chore: add reject dialog to provide reason
andypols Nov 29, 2025
cd86556
chore: add RejectionBadge to show who rejected it and why
andypols Nov 29, 2025
c734811
chore: pass reason to api
andypols Nov 29, 2025
4ae8abd
chore: enforce reject in api
andypols Nov 29, 2025
4ee6b62
chore: save reason in DB
andypols Nov 29, 2025
25a5ecf
chore: temp skip broken plugin tests
andypols Nov 29, 2025
28e25b0
chore: update cli tests
andypols Nov 29, 2025
fd67500
Merge branch 'main' into add-reject-reason
andypols Dec 5, 2025
fcd54be
chore: convert push.test to vitest
andypols Dec 5, 2025
20c09ae
chore: use correct class for title
andypols Dec 5, 2025
422cf99
chore: use correct onClick property
andypols Dec 5, 2025
8d28c4c
fix: can't reject when no reason given
andypols Dec 5, 2025
1c9c54d
fix: remove js version of test
andypols Dec 5, 2025
9066df4
fix: clean up layout
andypols Dec 5, 2025
2f745d8
fix: fix test change lost in the merge
andypols Dec 5, 2025
733c0bc
fix: convert js test to vitest
andypols Dec 5, 2025
63b8d92
fix: rename file to match component
andypols Dec 5, 2025
5f88752
fix: fix import
andypols Dec 5, 2025
2384a91
fix: support custom default tabs
andypols Dec 5, 2025
751e48f
fix: extend Dashboard to include All and Error pushes
andypols Dec 5, 2025
50198de
fix: fix colour
andypols Dec 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions packages/git-proxy-cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,9 @@ async function authoriseGitPush(id: string) {
/**
* Reject git push by ID
* @param {string} id The ID of the git push to reject
* @param {string} reason The reason for rejecting the push
*/
async function rejectGitPush(id: string) {
async function rejectGitPush(id: string, reason: string) {
if (!fs.existsSync(GIT_PROXY_COOKIE_FILE)) {
console.error('Error: Reject: Authentication required');
process.exitCode = 1;
Expand All @@ -247,7 +248,11 @@ async function rejectGitPush(id: string) {

await axios.post(
`${baseUrl}/api/v1/push/${id}/reject`,
{},
{
params: {
reason,
},
},
{
headers: { Cookie: cookies },
},
Expand All @@ -261,13 +266,17 @@ async function rejectGitPush(id: string) {

if (error.response) {
switch (error.response.status) {
case 400:
errorMessage = `Error: Reject: ${error.response.data.message}`;
process.exitCode = 3;
break;
case 401:
errorMessage = 'Error: Reject: Authentication required';
process.exitCode = 3;
process.exitCode = 4;
break;
case 404:
errorMessage = `Error: Reject: ID: '${id}': Not Found`;
process.exitCode = 4;
process.exitCode = 5;
}
}
console.error(errorMessage);
Expand Down Expand Up @@ -553,9 +562,14 @@ yargs(hideBin(process.argv)) // eslint-disable-line @typescript-eslint/no-unused
demandOption: true,
type: 'string',
},
reason: {
describe: 'Reason for rejecting the push',
demandOption: true,
type: 'string',
},
},
handler(argv) {
rejectGitPush(argv.id);
rejectGitPush(argv.id, argv.reason);
},
})
.command({
Expand Down
14 changes: 7 additions & 7 deletions packages/git-proxy-cli/test/testCli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ describe('test git-proxy-cli', function () {
}

const id = GHOST_PUSH_ID;
const cli = `${CLI_PATH} reject --id ${id}`;
const cli = `${CLI_PATH} reject --id ${id} --reason "Test rejection"`;
const expectedExitCode = 2;
const expectedMessages = null;
const expectedErrorMessages = ['Error: Reject:'];
Expand All @@ -452,7 +452,7 @@ describe('test git-proxy-cli', function () {
await helper.removeCookiesFile();

const id = GHOST_PUSH_ID;
const cli = `${CLI_PATH} reject --id ${id}`;
const cli = `${CLI_PATH} reject --id ${id} --reason "Test rejection"`;
const expectedExitCode = 1;
const expectedMessages = null;
const expectedErrorMessages = ['Error: Reject: Authentication required'];
Expand All @@ -464,8 +464,8 @@ describe('test git-proxy-cli', function () {
await helper.createCookiesFileWithExpiredCookie();
await helper.startServer();
const id = pushId;
const cli = `${CLI_PATH} reject --id ${id}`;
const expectedExitCode = 3;
const cli = `${CLI_PATH} reject --id ${id} --reason "Test rejection"`;
const expectedExitCode = 4;
const expectedMessages = null;
const expectedErrorMessages = ['Error: Reject: Authentication required'];
await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages);
Expand All @@ -480,8 +480,8 @@ describe('test git-proxy-cli', function () {
await helper.runCli(`${CLI_PATH} login --username admin --password admin`);

const id = GHOST_PUSH_ID;
const cli = `${CLI_PATH} reject --id ${id}`;
const expectedExitCode = 4;
const cli = `${CLI_PATH} reject --id ${id} --reason "Test rejection"`;
const expectedExitCode = 5;
const expectedMessages = null;
const expectedErrorMessages = [`Error: Reject: ID: '${id}': Not Found`];
await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages);
Expand Down Expand Up @@ -757,7 +757,7 @@ describe('test git-proxy-cli', function () {
let expectedErrorMessages = null;
await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages);

cli = `${CLI_PATH} reject --id ${pushId}`;
cli = `${CLI_PATH} reject --id ${pushId} --reason "Test rejection"`;
expectedExitCode = 0;
expectedMessages = [`Reject: ID: '${pushId}': OK`];
expectedErrorMessages = null;
Expand Down
4 changes: 2 additions & 2 deletions src/db/file/pushes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export const authorise = async (id: string, attestation: any): Promise<{ message
return { message: `authorised ${id}` };
};

export const reject = async (id: string, attestation: any): Promise<{ message: string }> => {
export const reject = async (id: string, rejection: any): Promise<{ message: string }> => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to add the Rejection type here!

const action = await getPush(id);
if (!action) {
throw new Error(`push ${id} not found`);
Expand All @@ -121,7 +121,7 @@ export const reject = async (id: string, attestation: any): Promise<{ message: s
action.authorised = false;
action.canceled = false;
action.rejected = true;
action.attestation = attestation;
action.rejection = rejection;
await writeAudit(action);
return { message: `reject ${id}` };
};
Expand Down
4 changes: 2 additions & 2 deletions src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ export const deletePush = (id: string): Promise<void> => sink.deletePush(id);
export const authorise = (id: string, attestation: any): Promise<{ message: string }> =>
sink.authorise(id, attestation);
export const cancel = (id: string): Promise<{ message: string }> => sink.cancel(id);
export const reject = (id: string, attestation: any): Promise<{ message: string }> =>
sink.reject(id, attestation);
export const reject = (id: string, rejection: any): Promise<{ message: string }> =>
sink.reject(id, rejection);
export const getRepos = (query?: Partial<RepoQuery>): Promise<Repo[]> => sink.getRepos(query);
export const getRepo = (name: string): Promise<Repo | null> => sink.getRepo(name);
export const getRepoByUrl = (url: string): Promise<Repo | null> => sink.getRepoByUrl(url);
Expand Down
4 changes: 2 additions & 2 deletions src/db/mongo/pushes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const writeAudit = async (action: Action): Promise<void> => {
await collection.updateOne({ id: data.id }, { $set: data }, options);
};

export const authorise = async (id: string, attestation: any): Promise<{ message: string }> => {
export const authorise = async (id: string, rejection: any): Promise<{ message: string }> => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, except I'm wondering if it makes sense to include the rejection string here - since we're authorising (and the authorisation "reason" would be implicit in the attestation.questions).

Would be great to have an Approval object here instead (formerly Attestation type)

const action = await getPush(id);
if (!action) {
throw new Error(`push ${id} not found`);
Expand All @@ -72,7 +72,7 @@ export const authorise = async (id: string, attestation: any): Promise<{ message
action.authorised = true;
action.canceled = false;
action.rejected = false;
action.attestation = attestation;
action.rejection = rejection;
await writeAudit(action);
return { message: `authorised ${id}` };
};
Expand Down
2 changes: 1 addition & 1 deletion src/db/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export interface Sink {
deletePush: (id: string) => Promise<void>;
authorise: (id: string, attestation: any) => Promise<{ message: string }>;
cancel: (id: string) => Promise<{ message: string }>;
reject: (id: string, attestation: any) => Promise<{ message: string }>;
reject: (id: string, rejection: any) => Promise<{ message: string }>;
getRepos: (query?: Partial<RepoQuery>) => Promise<Repo[]>;
getRepo: (name: string) => Promise<Repo | null>;
getRepoByUrl: (url: string) => Promise<Repo | null>;
Expand Down
3 changes: 2 additions & 1 deletion src/proxy/actions/Action.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { processGitURLForNameAndOrg, processUrlPath } from '../routes/helper';
import { Step } from './Step';
import { Attestation, CommitData } from '../processors/types';
import { Attestation, CommitData, Rejection } from '../processors/types';

/**
* Class representing a Push.
Expand Down Expand Up @@ -34,6 +34,7 @@ class Action {
user?: string;
userEmail?: string;
attestation?: Attestation;
rejection?: Rejection;
lastStep?: Step;
proxyGitPath?: string;
newIdxFiles?: string[];
Expand Down
8 changes: 8 additions & 0 deletions src/proxy/processors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ export type Attestation = {
questions: Question[];
};

export type Rejection = {
reviewer: {
username: string;
};
timestamp: string | Date;
reason: string;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand a bit better now - perhaps we could have a Review base object, rename Attestation to Approval and keep Rejection? Then the database function and API routes only need one of them.


export type CommitContent = {
item: number;
type: number;
Expand Down
6 changes: 2 additions & 4 deletions src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import React from 'react';
import RouteGuard from './ui/components/RouteGuard/RouteGuard';
import Person from '@material-ui/icons/Person';
import OpenPushRequests from './ui/views/OpenPushRequests/OpenPushRequests';
import PushList from './ui/views/Dashboard/Dashboard';
import PushDetails from './ui/views/PushDetails/PushDetails';
import User from './ui/views/User/UserProfile';
import UserList from './ui/views/UserList/UserList';
Expand Down Expand Up @@ -55,9 +55,7 @@ const dashboardRoutes: Route[] = [
path: '/push',
name: 'Dashboard',
icon: Dashboard,
component: (props) => (
<RouteGuard component={OpenPushRequests} fullRoutePath={`/dashboard/push`} />
),
component: (props) => <RouteGuard component={PushList} fullRoutePath={`/dashboard/push`} />,
layout: '/dashboard',
visible: true,
},
Expand Down
32 changes: 30 additions & 2 deletions src/service/routes/push.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import express, { Request, Response } from 'express';
import * as db from '../../db';
import { PushQuery } from '../../db/types';
import { convertIgnorePatternToMinimatch } from '@eslint/compat';

const router = express.Router();

Expand Down Expand Up @@ -43,6 +44,14 @@ router.post('/:id/reject', async (req: Request, res: Response) => {
return;
}

const reason = req.body.params?.reason;
if (!reason || reason.trim().length === 0) {
res.status(400).send({
message: 'Rejection reason is required',
});
return;
}

const id = req.params.id;
const { username } = req.user as { username: string };

Expand Down Expand Up @@ -71,8 +80,27 @@ router.post('/:id/reject', async (req: Request, res: Response) => {
const isAllowed = await db.canUserApproveRejectPush(id, username);

if (isAllowed) {
const result = await db.reject(id, null);
console.log(`user ${username} rejected push request for ${id}`);
console.log(`user ${username} rejected push request for ${id} with reason: ${reason}`);

const reviewerList = await db.getUsers({ username });
const reviewerEmail = reviewerList[0].email;

if (!reviewerEmail) {
res.status(401).send({
message: `There was no registered email address for the reviewer: ${username}`,
});
return;
}

const rejection = {
reason,
timestamp: new Date(),
reviewer: {
username,
reviewerEmail,
},
};
const result = await db.reject(id, rejection);
res.send(result);
} else {
res.status(401).send({
Expand Down
19 changes: 15 additions & 4 deletions src/ui/components/CustomTabs/CustomTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type HeaderColor = 'warning' | 'success' | 'danger' | 'info' | 'primary' | 'rose
export type TabItem = {
tabName: string;
tabIcon?: React.ComponentType<SvgIconProps>;
tabContent: React.ReactNode;
tabContent?: React.ReactNode;
};

interface CustomTabsProps {
Expand All @@ -25,6 +25,9 @@ interface CustomTabsProps {
tabs: TabItem[];
rtlActive?: boolean;
plainTabs?: boolean;
defaultTab?: number;
value?: number;
onChange?: (value: number) => void;
}

const CustomTabs: React.FC<CustomTabsProps> = ({
Expand All @@ -33,12 +36,20 @@ const CustomTabs: React.FC<CustomTabsProps> = ({
tabs,
title,
rtlActive = false,
defaultTab = 0,
value: controlledValue,
onChange: controlledOnChange,
}) => {
const [value, setValue] = useState(0);
const [internalValue, setInternalValue] = useState(defaultTab);
const classes = useStyles();
const value = controlledValue !== undefined ? controlledValue : internalValue;

const handleChange = (event: React.ChangeEvent<unknown>, newValue: number) => {
setValue(newValue);
if (controlledOnChange) {
controlledOnChange(newValue);
} else {
setInternalValue(newValue);
}
};

const cardTitle = clsx({
Expand Down Expand Up @@ -80,7 +91,7 @@ const CustomTabs: React.FC<CustomTabsProps> = ({
</CardHeader>
<CardBody>
{tabs.map((prop, key) => (
<div key={key} style={{ display: key === value ? 'block' : 'none' }}>
<div key={key} style={{ display: key === internalValue ? 'block' : 'none' }}>
{prop.tabContent}
</div>
))}
Expand Down
Loading
Loading