Skip to content
4 changes: 2 additions & 2 deletions src/db/file/pushes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,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 }> => {
const action = await getPush(id);
if (!action) {
throw new Error(`push ${id} not found`);
Expand All @@ -120,7 +120,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 @@ -167,8 +167,8 @@ export const deletePush = (id: string): Promise<void> => start().deletePush(id);
export const authorise = (id: string, attestation: any): Promise<{ message: string }> =>
start().authorise(id, attestation);
export const cancel = (id: string): Promise<{ message: string }> => start().cancel(id);
export const reject = (id: string, attestation: any): Promise<{ message: string }> =>
start().reject(id, attestation);
export const reject = (id: string, rejection: any): Promise<{ message: string }> =>
start().reject(id, rejection);
export const getRepos = (query?: Partial<RepoQuery>): Promise<Repo[]> => start().getRepos(query);
export const getRepo = (name: string): Promise<Repo | null> => start().getRepo(name);
export const getRepoByUrl = (url: string): Promise<Repo | null> => start().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 @@ -77,15 +77,15 @@ 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 }> => {
const action = await getPush(id);
if (!action) {
throw new Error(`push ${id} not found`);
}
action.authorised = false;
action.canceled = false;
action.rejected = true;
action.attestation = attestation;
action.rejection = rejection;
await writeAudit(action);
return { message: `reject ${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
9 changes: 9 additions & 0 deletions src/proxy/processors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ export type Attestation = {
questions: Question[];
};

export type Rejection = {
reviewer: {
username: string;
reviewerEmail: string;
};
timestamp: string | Date;
reason: string;
};

export type CommitContent = {
item: number;
type: number;
Expand Down
33 changes: 31 additions & 2 deletions src/service/routes/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ router.post('/:id/reject', async (req: Request<{ id: string }>, res: Response) =

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

if (!reason || !reason.trim()) {
res.status(400).send({
message: 'Rejection reason is required',
});
return;
}

// Get the push request
const push = await getValidPushOrRespond(id, res);
Expand All @@ -71,8 +79,29 @@ router.post('/:id/reject', async (req: Request<{ id: string }>, 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}`);
const reviewerList = await db.getUsers({ username });
const reviewerEmail = reviewerList[0].email;

if (!reviewerEmail) {
res.status(404).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);
console.log(
`User ${username} rejected push request for ${id}${reason ? ` with reason: ${reason}` : ''}`,
);
res.send(result);
} else {
res.status(403).send({
Expand Down
3 changes: 2 additions & 1 deletion src/ui/services/git-push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,13 @@ const rejectPush = async (
id: string,
setMessage: (message: string) => void,
setUserAllowedToReject: (userAllowedToReject: boolean) => void,
reason?: string,
): Promise<void> => {
const apiV1Base = await getApiV1BaseUrl();
const url = `${apiV1Base}/push/${id}/reject`;
let errorMsg = '';
let isUserAllowedToReject = true;
await axios.post(url, {}, getAxiosConfig()).catch((error: any) => {
await axios.post(url, { reason }, getAxiosConfig()).catch((error: any) => {
if (error.response && error.response.status === 401) {
errorMsg = 'You are not authorised to reject...';
isUserAllowedToReject = false;
Expand Down
102 changes: 14 additions & 88 deletions src/ui/views/PushDetails/PushDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import CardFooter from '../../components/Card/CardFooter';
import Button from '../../components/CustomButtons/Button';
import Diff from './components/Diff';
import Attestation from './components/Attestation';
import AttestationView from './components/AttestationView';
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';
Expand All @@ -21,11 +23,9 @@ import TableCell from '@material-ui/core/TableCell';
import { getPush, authorisePush, rejectPush, cancelPush } from '../../services/git-push';
import { CheckCircle, Visibility, Cancel, Block } from '@material-ui/icons';
import Snackbar from '@material-ui/core/Snackbar';
import Tooltip from '@material-ui/core/Tooltip';
import { AttestationFormData, PushActionView } from '../../types';
import { PushActionView } from '../../types';
import { trimPrefixRefsHeads, trimTrailingDotGit } from '../../../db/helper';
import { generateEmailLink, getGitProvider } from '../../utils';
import UserLink from '../../components/UserLink/UserLink';

const Dashboard: React.FC = () => {
const { id } = useParams<{ id: string }>();
Expand Down Expand Up @@ -62,9 +62,9 @@ const Dashboard: React.FC = () => {
}
};

const reject = async () => {
const reject = async (reason: string) => {
if (!id) return;
await rejectPush(id, setMessage, setUserAllowedToReject);
await rejectPush(id, setMessage, setUserAllowedToReject, reason);
if (isUserAllowedToReject) {
navigate('/dashboard/push/');
}
Expand Down Expand Up @@ -153,91 +153,17 @@ const Dashboard: React.FC = () => {
<Button color='warning' onClick={cancel}>
Cancel
</Button>
<Button color='danger' onClick={reject}>
Reject
</Button>
<Reject rejectFn={reject} />
<Attestation approveFn={authorise} />
</div>
)}
{push.attestation && push.authorised && (
<div
style={{
background: '#eee',
padding: '10px 20px 10px 20px',
borderRadius: '10px',
color: 'black',
marginTop: '15px',
float: 'right',
position: 'relative',
textAlign: 'left',
}}
>
<span style={{ position: 'absolute', top: 0, right: 0 }}>
<CheckCircle
style={{
cursor: push.autoApproved ? 'default' : 'pointer',
transform: 'scale(0.65)',
opacity: push.autoApproved ? 0.5 : 1,
}}
onClick={() => {
if (!push.autoApproved) {
setAttestation(true);
}
}}
htmlColor='green'
/>
</span>

{push.autoApproved ? (
<div style={{ paddingTop: '15px' }}>
<p>
<strong>Auto-approved by system</strong>
</p>
</div>
) : (
<>
{isGitHub && (
<UserLink username={push.attestation.reviewer.username}>
<img
style={{ width: '45px', borderRadius: '20px' }}
src={`https://github.com/${push.attestation.reviewer.gitAccount}.png`}
/>
</UserLink>
)}
<div>
<p>
{isGitHub && (
<UserLink username={push.attestation.reviewer.username}>
{push.attestation.reviewer.gitAccount}
</UserLink>
)}
{!isGitHub && <UserLink username={push.attestation.reviewer.username} />}{' '}
approved this contribution
</p>
</div>
</>
)}

<Tooltip
title={moment(push.attestation.timestamp).format(
'dddd, MMMM Do YYYY, h:mm:ss a',
)}
arrow
>
<kbd style={{ color: 'black', float: 'right' }}>
{moment(push.attestation.timestamp).fromNow()}
</kbd>
</Tooltip>

{!push.autoApproved && (
<AttestationView
data={push.attestation as AttestationFormData}
attestation={attestation}
setAttestation={setAttestation}
/>
)}
</div>
)}
<AttestationInfo
push={push}
isGitHub={isGitHub}
attestation={attestation}
setAttestation={setAttestation}
/>
<RejectionInfo push={push} />
</CardHeader>
<CardBody>
<GridContainer>
Expand Down
105 changes: 105 additions & 0 deletions src/ui/views/PushDetails/components/AttestationInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React from 'react';
import moment from 'moment';
import { CheckCircle } from '@material-ui/icons';
import Tooltip from '@material-ui/core/Tooltip';
import UserLink from '../../../components/UserLink/UserLink';
import AttestationView from './AttestationView';
import { AttestationFormData, PushActionView } from '../../../types';

interface AttestationInfoProps {
push: PushActionView;
isGitHub: boolean;
attestation: boolean;
setAttestation: (value: boolean) => void;
}

const AttestationInfo: React.FC<AttestationInfoProps> = ({
push,
isGitHub,
attestation,
setAttestation,
}) => {
if (!push.attestation || !push.authorised) {
return null;
}

return (
<div
style={{
background: '#eee',
padding: '10px 20px 10px 20px',
borderRadius: '10px',
color: 'black',
marginTop: '15px',
float: 'right',
position: 'relative',
textAlign: 'left',
}}
>
<span style={{ position: 'absolute', top: 0, right: 0 }}>
<CheckCircle
style={{
cursor: push.autoApproved ? 'default' : 'pointer',
transform: 'scale(0.65)',
opacity: push.autoApproved ? 0.5 : 1,
}}
onClick={() => {
if (!push.autoApproved) {
setAttestation(true);
}
}}
htmlColor='green'
/>
</span>

{push.autoApproved ? (
<div style={{ paddingTop: '15px' }}>
<p>
<strong>Auto-approved by system</strong>
</p>
</div>
) : (
<>
{isGitHub && (
<UserLink username={push.attestation.reviewer.username}>
<img
style={{ width: '45px', borderRadius: '20px' }}
src={`https://github.com/${push.attestation.reviewer.gitAccount}.png`}
/>
</UserLink>
)}
<div>
<p>
{isGitHub && (
<UserLink username={push.attestation.reviewer.username}>
{push.attestation.reviewer.gitAccount}
</UserLink>
)}
{!isGitHub && <UserLink username={push.attestation.reviewer.username} />} approved
this contribution
</p>
</div>
</>
)}

<Tooltip
title={moment(push.attestation.timestamp).format('dddd, MMMM Do YYYY, h:mm:ss a')}
arrow
>
<kbd style={{ color: 'black', float: 'right' }}>
{moment(push.attestation.timestamp).fromNow()}
</kbd>
</Tooltip>

{!push.autoApproved && (
<AttestationView
data={push.attestation as AttestationFormData}
attestation={attestation}
setAttestation={setAttestation}
/>
)}
</div>
);
};

export default AttestationInfo;
Loading
Loading