From 3cc768e991556f1fca110b35b6ce91d7f36db3ae Mon Sep 17 00:00:00 2001 From: Itai Gilo Date: Wed, 18 Sep 2024 18:17:25 +0300 Subject: [PATCH] Create Pull Details page --- webui/src/lib/api/index.js | 60 +++++++--- webui/src/pages/index.jsx | 8 +- .../repository/pulls/createPull.jsx | 29 +++++ .../repository/pulls/pullDetails.jsx | 106 ++++++++++++++++++ .../repositories/repository/pulls/pulls.jsx | 30 +++-- webui/src/styles/globals.css | 4 + 6 files changed, 207 insertions(+), 30 deletions(-) create mode 100644 webui/src/pages/repositories/repository/pulls/createPull.jsx create mode 100644 webui/src/pages/repositories/repository/pulls/pullDetails.jsx diff --git a/webui/src/lib/api/index.js b/webui/src/lib/api/index.js index c7da8d3ca11..5cbf99aad5e 100644 --- a/webui/src/lib/api/index.js +++ b/webui/src/lib/api/index.js @@ -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); @@ -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", @@ -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'))); @@ -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"); } @@ -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} }, } } @@ -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'); @@ -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)); @@ -1135,7 +1169,7 @@ class Import { "path": source, "destination": prepend, "type": "common_prefix", - }], + }], "commit": { "message": commitMessage }, diff --git a/webui/src/pages/index.jsx b/webui/src/pages/index.jsx index 1e5af414572..497cd5c039e 100644 --- a/webui/src/pages/index.jsx +++ b/webui/src/pages/index.jsx @@ -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"; @@ -63,7 +65,11 @@ export const IndexPage = () => { }/> }/> - }/> + + }/> + }/> + }/> + }/> }/> diff --git a/webui/src/pages/repositories/repository/pulls/createPull.jsx b/webui/src/pages/repositories/repository/pulls/createPull.jsx new file mode 100644 index 00000000000..4a865fbaf43 --- /dev/null +++ b/webui/src/pages/repositories/repository/pulls/createPull.jsx @@ -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 ; + if (error) return ; + + return ( +
+

Create Pull Request (in repo {repo.id})

+
TBD
+
+ ); +}; + + +const RepositoryCreatePullPage = () => { + const [setActivePage] = useOutletContext(); + useEffect(() => setActivePage("pulls"), [setActivePage]); + return ; +} + +export default RepositoryCreatePullPage; diff --git a/webui/src/pages/repositories/repository/pulls/pullDetails.jsx b/webui/src/pages/repositories/repository/pulls/pullDetails.jsx new file mode 100644 index 00000000000..f3267d25860 --- /dev/null +++ b/webui/src/pages/repositories/repository/pulls/pullDetails.jsx @@ -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}) => + + {branch} + ; + +const getStatusBadgeParams = status => { + switch (status) { + case pullsAPI.PullStatus.open: + return {bgColor: "success", icon: }; + case pullsAPI.PullStatus.closed: + return {bgColor: "purple", icon: }; + case pullsAPI.PullStatus.merged: + return {bgColor: "danger", icon: }; + default: + return {bgColor: "secondary", icon: null}; + } +}; + +const StatusBadge = ({status}) => { + const {bgColor, icon} = getStatusBadgeParams(status); + return + {icon} {status} + ; +}; + +const PullDetailsContent = ({repo, pull}) => { + const createdAt = dayjs.unix(pull.created_at); + + return ( +
+

{pull.title} {pull.id}

+
+ + + {pull.author} wants to merge {""} + {""} + into . + +
+ + + {pull.author} opened {""} + on {createdAt.format("MMM D, YYYY")} ({createdAt.fromNow()}). + + + {pull.description} + + +
+ + +
+
+ ); +}; + +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 ; + if (error) return ; + return ; +} + +const PullDetailsContainer = () => { + const router = useRouter() + const {repo, loading, error} = useRefs(); + const {pullId} = router.params; + + if (loading) return ; + if (error) return ; + + return ; +}; + + +const RepositoryPullDetailsPage = () => { + const [setActivePage] = useOutletContext(); + useEffect(() => setActivePage("pulls"), [setActivePage]); + return ; +} + +export default RepositoryPullDetailsPage; diff --git a/webui/src/pages/repositories/repository/pulls/pulls.jsx b/webui/src/pages/repositories/repository/pulls/pulls.jsx index 6d29c33922a..80e8143b51c 100644 --- a/webui/src/pages/repositories/repository/pulls/pulls.jsx +++ b/webui/src/pages/repositories/repository/pulls/pulls.jsx @@ -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"; @@ -26,7 +26,7 @@ const PullWidget = ({repo, pull}) => { pathname: '/repositories/:repoId/pulls/:pullId', params: {repoId: repo.id, pullId: pull.id} }}> - {pull.title} + {pull.title} @@ -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); @@ -87,8 +80,8 @@ const PullsList = ({repo, after, prefix, onPaginate}) => { onSelect={key => setPullsState(key)} className="mb-3 pt-2" > - - + + @@ -102,7 +95,14 @@ const PullsList = ({repo, after, prefix, onPaginate}) => { })}/> - + {content} @@ -110,7 +110,6 @@ const PullsList = ({repo, after, prefix, onPaginate}) => { ); }; - const PullsContainer = () => { const router = useRouter() const {repo, loading, error} = useRefs(); @@ -139,7 +138,6 @@ const PullsContainer = () => { ); }; - const RepositoryPullsPage = () => { const [setActivePage] = useOutletContext(); useEffect(() => setActivePage("pulls"), [setActivePage]); diff --git a/webui/src/styles/globals.css b/webui/src/styles/globals.css index 104cfd15d43..6478bb96223 100644 --- a/webui/src/styles/globals.css +++ b/webui/src/styles/globals.css @@ -919,3 +919,7 @@ td.entry-type-indicator { .upload-item-done { color: var(--bs-success); } + +.pull-details .description { + min-height: 160px; +} \ No newline at end of file