Skip to content

Commit

Permalink
Create Pull Details page
Browse files Browse the repository at this point in the history
  • Loading branch information
itaigilo committed Sep 18, 2024
1 parent 53468aa commit 3cc768e
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 30 deletions.
60 changes: 47 additions & 13 deletions webui/src/lib/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,35 @@ class Tags {
}

class Pulls {
PullStatus = {
open: "open",
closed: "closed",
merged: "merged",
}

async get(repoId, pullId) {
// const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/pulls/${encodeURIComponent(pullId)}`);
// if (response.status === 404) {
// throw new NotFoundError(`could not find pull ${pullId}`);
// } else if (response.status !== 200) {
// throw new Error(`could not get pullId: ${await extractError(response)}`);
// }
// return response.json();

// TODO: this is for development purposes only
console.log("get pull", {repoId, pullId});
return {
"id": pullId,
"title": "Test PR 1",
"status": "open",
"created_at": 1726575741,
"author": "test-user-1",
"description": "This is a test PR",
"source_branch": "feature-branch-1",
"destination_branch": "main"
}
}

async list(repoId, state = "open", prefix = "", after = "", amount = DEFAULT_LISTING_AMOUNT) {
// const query = qs({prefix, after, amount});
// const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/pulls?` + query);
Expand All @@ -577,7 +606,7 @@ class Pulls {
// return response.json();

// TODO: this is for development purposes only
console.log("list pulls", {repoId, state, prefix, after, amount})
console.log("list pulls", {repoId, state, prefix, after, amount});
let results = [
{
"id": "test-pull-1",
Expand Down Expand Up @@ -634,13 +663,13 @@ export const uploadWithProgress = (url, file, method = 'POST', onProgress = null
}
});
xhr.addEventListener('load', () => {
resolve({
status: xhr.status,
body: xhr.responseText,
contentType: xhr.getResponseHeader('Content-Type'),
etag: xhr.getResponseHeader('ETag'),
contentMD5: xhr.getResponseHeader('Content-MD5'),
})
resolve({
status: xhr.status,
body: xhr.responseText,
contentType: xhr.getResponseHeader('Content-Type'),
etag: xhr.getResponseHeader('ETag'),
contentMD5: xhr.getResponseHeader('Content-MD5'),
})
});
xhr.addEventListener('error', () => reject(new Error('Upload Failed')));
xhr.addEventListener('abort', () => reject(new Error('Upload Aborted')));
Expand Down Expand Up @@ -683,7 +712,7 @@ class Objects {
next: async () => {
const query = qs({prefix, presign, after, amount: MAX_LISTING_AMOUNT});
const response = await apiRequest(
`/repositories/${encodeURIComponent(repoId)}/refs/${encodeURIComponent(ref)}/objects/ls?` + query);
`/repositories/${encodeURIComponent(repoId)}/refs/${encodeURIComponent(ref)}/objects/ls?` + query);
if (response.status === 404) {
throw new NotFoundError(response.message ?? "ref not found");
}
Expand All @@ -693,7 +722,7 @@ class Objects {
const responseBody = await response.json();
const done = !responseBody.pagination.has_more;
if (!done) after = responseBody.pagination.next_offset;
return {page:responseBody.results, done}
return {page: responseBody.results, done}
},
}
}
Expand Down Expand Up @@ -1031,7 +1060,7 @@ class Config {
let cfg;
switch (response.status) {
case 200:
cfg = await response.json();
cfg = await response.json();
return cfg.version_config
default:
throw new Error('Unknown');
Expand Down Expand Up @@ -1106,7 +1135,12 @@ class Staging {
const query = qs({path});
const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/branches/${encodeURIComponent(branchId)}/staging/backing?` + query, {
method: 'PUT',
body: JSON.stringify({staging: staging, checksum: checksum, size_bytes: sizeBytes, content_type: contentType})
body: JSON.stringify({
staging: staging,
checksum: checksum,
size_bytes: sizeBytes,
content_type: contentType
})
});
if (response.status !== 200) {
throw new Error(await extractError(response));
Expand Down Expand Up @@ -1135,7 +1169,7 @@ class Import {
"path": source,
"destination": prepend,
"type": "common_prefix",
}],
}],
"commit": {
"message": commitMessage
},
Expand Down
8 changes: 7 additions & 1 deletion webui/src/pages/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import RepositoryCommitPage from "./repositories/repository/commits/commit";
import RepositoryBranchesPage from "./repositories/repository/branches";
import RepositoryTagsPage from "./repositories/repository/tags";
import RepositoryPullsPage from "./repositories/repository/pulls/pulls";
import RepositoryCreatePullPage from "./repositories/repository/pulls/createPull";
import RepositoryPullDetailsPage from "./repositories/repository/pulls/pullDetails";
import RepositoryComparePage from "./repositories/repository/compare";
import RepositoryActionsPage from "./repositories/repository/actions";
import RepositoryGeneralSettingsPage from "./repositories/repository/settings/general";
Expand Down Expand Up @@ -63,7 +65,11 @@ export const IndexPage = () => {
</Route>
<Route path="branches" element={<RepositoryBranchesPage/>}/>
<Route path="tags" element={<RepositoryTagsPage/>}/>
<Route path="pulls" element={<RepositoryPullsPage/>}/>
<Route path="pulls">
<Route index element={<RepositoryPullsPage/>}/>
<Route path="create" element={<RepositoryCreatePullPage/>}/>
<Route path=":pullId" element={<RepositoryPullDetailsPage/>}/>
</Route>
<Route path="compare/*" element={<RepositoryComparePage/>}/>
<Route path="actions">
<Route index element={<RepositoryActionsPage/>}/>
Expand Down
29 changes: 29 additions & 0 deletions webui/src/pages/repositories/repository/pulls/createPull.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, {useEffect} from "react";
import {useOutletContext} from "react-router-dom";

import {Loading} from "../../../../lib/components/controls";
import {useRefs} from "../../../../lib/hooks/repo";
import {RepoError} from "../error";

const CreatePull = () => {
const {repo, loading, error} = useRefs();

if (loading) return <Loading/>;
if (error) return <RepoError error={error}/>;

return (
<div>
<h1>Create Pull Request (in repo {repo.id})</h1>
<div>TBD</div>
</div>
);
};


const RepositoryCreatePullPage = () => {
const [setActivePage] = useOutletContext();
useEffect(() => setActivePage("pulls"), [setActivePage]);
return <CreatePull/>;
}

export default RepositoryCreatePullPage;
106 changes: 106 additions & 0 deletions webui/src/pages/repositories/repository/pulls/pullDetails.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, {useEffect} from "react";
import {useOutletContext} from "react-router-dom";
import Badge from "react-bootstrap/Badge";
import Button from "react-bootstrap/Button";
import Card from "react-bootstrap/Card";
import {GitMergeIcon, GitPullRequestClosedIcon, GitPullRequestIcon} from "@primer/octicons-react";
import dayjs from "dayjs";

import {AlertError, Loading} from "../../../../lib/components/controls";
import {useRefs} from "../../../../lib/hooks/repo";
import {useRouter} from "../../../../lib/hooks/router";
import {RepoError} from "../error";
import {pulls as pullsAPI} from "../../../../lib/api";
import {useAPI} from "../../../../lib/hooks/api";
import {Link} from "../../../../lib/components/nav";

const BranchLink = ({repo, branch}) =>
<Link href={{
pathname: '/repositories/:repoId/objects',
params: {repoId: repo.id},
query: {ref: branch}
}}>
{branch}
</Link>;

const getStatusBadgeParams = status => {
switch (status) {
case pullsAPI.PullStatus.open:
return {bgColor: "success", icon: <GitPullRequestIcon/>};
case pullsAPI.PullStatus.closed:
return {bgColor: "purple", icon: <GitPullRequestClosedIcon/>};
case pullsAPI.PullStatus.merged:
return {bgColor: "danger", icon: <GitMergeIcon/>};
default:
return {bgColor: "secondary", icon: null};
}
};

const StatusBadge = ({status}) => {
const {bgColor, icon} = getStatusBadgeParams(status);
return <Badge pill bg={bgColor}>
{icon} <span className="text-capitalize">{status}</span>
</Badge>;
};

const PullDetailsContent = ({repo, pull}) => {
const createdAt = dayjs.unix(pull.created_at);

return (
<div className="pull-details mb-5">
<h1>{pull.title} <span className="fs-5 text-secondary">{pull.id}</span></h1>
<div className="mt-3">
<StatusBadge status={pull.status}/>
<span className="ms-2">
<strong>{pull.author}</strong> wants to merge {""}
<BranchLink repo={repo} branch={pull.source_branch}/> {""}
into <BranchLink repo={repo} branch={pull.destination_branch}/>.
</span>
</div>
<Card className="mt-4">
<Card.Header>
<strong>{pull.author}</strong> opened {""}
on {createdAt.format("MMM D, YYYY")} ({createdAt.fromNow()}).
</Card.Header>
<Card.Body className="description">
{pull.description}
</Card.Body>
</Card>
<div className="bottom-buttons-group mt-4 float-end">
<Button variant="outline-secondary" className="text-secondary-emphasis me-2">Close pull request</Button>
<Button variant="success">Merge pull request</Button>
</div>
</div>
);
};

const PullDetails = ({repo, pullId}) => {
const {response: pull, error, loading} = useAPI(async () => {
console.log({repo, pullId});
return pullsAPI.get(repo.id, pullId);
}, [repo.id, pullId]);

if (loading) return <Loading/>;
if (error) return <AlertError error={error}/>;
return <PullDetailsContent repo={repo} pull={pull}/>;
}

const PullDetailsContainer = () => {
const router = useRouter()
const {repo, loading, error} = useRefs();
const {pullId} = router.params;

if (loading) return <Loading/>;
if (error) return <RepoError error={error}/>;

return <PullDetails repo={repo} pullId={pullId}/>;
};


const RepositoryPullDetailsPage = () => {
const [setActivePage] = useOutletContext();
useEffect(() => setActivePage("pulls"), [setActivePage]);
return <PullDetailsContainer/>;
}

export default RepositoryPullDetailsPage;
30 changes: 14 additions & 16 deletions webui/src/pages/repositories/repository/pulls/pulls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Button from "react-bootstrap/Button";
import dayjs from "dayjs";

import {ActionGroup, AlertError, Loading, PrefixSearchWidget, RefreshButton} from "../../../../lib/components/controls";
import {pulls} from "../../../../lib/api";
import {pulls as pullsAPI} from "../../../../lib/api";
import {useRefs} from "../../../../lib/hooks/repo";
import {useAPIWithPagination} from "../../../../lib/hooks/api";
import {Paginator} from "../../../../lib/components/pagination";
Expand All @@ -26,7 +26,7 @@ const PullWidget = ({repo, pull}) => {
pathname: '/repositories/:repoId/pulls/:pullId',
params: {repoId: repo.id, pullId: pull.id}
}}>
<span>{pull.title}</span>
{pull.title}
</Link>
</h6>
<small>
Expand All @@ -42,20 +42,13 @@ const PullWidget = ({repo, pull}) => {
);
};

// TODO (gilo): is there a nicer place for this?
const PullStatus = {
open: "open",
closed: "closed",
merged: "merged",
}

const PullsList = ({repo, after, prefix, onPaginate}) => {
const router = useRouter()
const [refresh, setRefresh] = useState(true);
// TODO: pullState should be persistent in the url and saved as a url param?
const [pullsState, setPullsState] = useState(PullStatus.open);
const [pullsState, setPullsState] = useState(pullsAPI.PullStatus.open);
const {results, error, loading, nextPage} = useAPIWithPagination(async () => {
return pulls.list(repo.id, pullsState, prefix, after);
return pullsAPI.list(repo.id, pullsState, prefix, after);
}, [repo.id, pullsState, prefix, refresh, after]);

const doRefresh = () => setRefresh(true);
Expand Down Expand Up @@ -87,8 +80,8 @@ const PullsList = ({repo, after, prefix, onPaginate}) => {
onSelect={key => setPullsState(key)}
className="mb-3 pt-2"
>
<Tab eventKey={PullStatus.open} title="Open"/>
<Tab eventKey={PullStatus.closed} title="Closed"/>
<Tab eventKey={pullsAPI.PullStatus.open} title="Open"/>
<Tab eventKey={pullsAPI.PullStatus.closed} title="Closed"/>
</Tabs>
</div>
<ActionGroup orientation="right" className="position-absolute top-0 end-0 pb-2">
Expand All @@ -102,15 +95,21 @@ const PullsList = ({repo, after, prefix, onPaginate}) => {
})}/>

<RefreshButton onClick={doRefresh}/>
<Button variant="success">Create Pull Request</Button>
<Button variant="success"
onClick={() => router.push({
pathname: '/repositories/:repoId/pulls/create',
params: {repoId: repo.id},
})}
>
Create Pull Request
</Button>
</ActionGroup>
</div>
{content}
</div>
);
};


const PullsContainer = () => {
const router = useRouter()
const {repo, loading, error} = useRefs();
Expand Down Expand Up @@ -139,7 +138,6 @@ const PullsContainer = () => {
);
};


const RepositoryPullsPage = () => {
const [setActivePage] = useOutletContext();
useEffect(() => setActivePage("pulls"), [setActivePage]);
Expand Down
4 changes: 4 additions & 0 deletions webui/src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -919,3 +919,7 @@ td.entry-type-indicator {
.upload-item-done {
color: var(--bs-success);
}

.pull-details .description {
min-height: 160px;
}

0 comments on commit 3cc768e

Please sign in to comment.