From 0b2b1c022bd43a43988f472024498ec3a4d146ec Mon Sep 17 00:00:00 2001 From: itaigilo Date: Sun, 22 Sep 2024 07:37:34 +0300 Subject: [PATCH] PRfD - PR Details page (#8185) * Create Pull Details page * Create BranchesComparison component * Use BranchComparison for Compare page * Revert flag * Revert refID change * Change structure * Extract to files * Cleanup --- webui/src/constants.ts | 20 +- webui/src/lib/api/index.js | 60 +++- .../src/lib/components/repository/changes.jsx | 4 +- .../components/repository/compareBranches.jsx | 164 +++++++++ .../repository/compareBranchesActionBar.jsx | 183 ++++++++++ webui/src/pages/index.jsx | 8 +- .../pages/repositories/repository/compare.jsx | 317 ++---------------- .../repository/pulls/createPull.jsx | 29 ++ .../repository/pulls/pullDetails.jsx | 115 +++++++ .../repositories/repository/pulls/pulls.jsx | 27 +- webui/src/styles/globals.css | 4 + 11 files changed, 596 insertions(+), 335 deletions(-) create mode 100644 webui/src/lib/components/repository/compareBranches.jsx create mode 100644 webui/src/lib/components/repository/compareBranchesActionBar.jsx 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/constants.ts b/webui/src/constants.ts index b419c7a0371..41a041cdc9b 100644 --- a/webui/src/constants.ts +++ b/webui/src/constants.ts @@ -1,22 +1,16 @@ export const RefTypeBranch = 'branch'; export const RefTypeCommit = 'commit'; export const RefTypeTag = 'tag'; -export enum TreeItemType { - Object, - Prefix, - DeltaLakeTable -} -export enum OtfType { - Delta = "delta", -} -export enum OtfDiffType { - Created = "created", - Dropped = "dropped", - Changed = "changed", -} + export enum TreeRowType { Object, Prefix, Table } +export enum PullStatus { + open = "open", + closed = "closed", + merged = "merged", +} + diff --git a/webui/src/lib/api/index.js b/webui/src/lib/api/index.js index c7da8d3ca11..9e5d9d6a721 100644 --- a/webui/src/lib/api/index.js +++ b/webui/src/lib/api/index.js @@ -568,6 +568,29 @@ class Tags { } class Pulls { + 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", + "creation_date": 1726575741, + "author": "test-user-1", + "description": "This is a test PR", + "source_branch": "test-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,13 +600,13 @@ 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", "title": "Test PR 1", "status": "open", - "created_at": 1726575741, + "creation_date": 1726575741, "author": "test-user-1", "description": "This is a test PR", "source_branch": "feature-branch-1", @@ -593,7 +616,7 @@ class Pulls { "id": "test-pull-2", "title": "Next Test PR 2", "status": "closed", - "created_at": 1726402941, + "creation_date": 1726402941, "author": "test-user-2", "description": "This is a another test PR", "source_branch": "feature-branch-2", @@ -603,7 +626,7 @@ class Pulls { "id": "test-pull-3", "title": "Another Test PR 3", "status": "open", - "created_at": 1718454141, + "creation_date": 1718454141, "author": "test-user-1", "description": "This is also a test PR", "source_branch": "feature-branch-3", @@ -634,13 +657,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 +706,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 +716,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 +1054,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 +1129,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 +1163,7 @@ class Import { "path": source, "destination": prepend, "type": "common_prefix", - }], + }], "commit": { "message": commitMessage }, diff --git a/webui/src/lib/components/repository/changes.jsx b/webui/src/lib/components/repository/changes.jsx index 014c750d9f2..fc5e408faad 100644 --- a/webui/src/lib/components/repository/changes.jsx +++ b/webui/src/lib/components/repository/changes.jsx @@ -143,14 +143,14 @@ export const TreeEntryPaginator = ({ path, setAfterUpdated, nextPage, depth=0, l export const ChangesTreeContainer = ({results, delimiter, uriNavigator, leftDiffRefID, rightDiffRefID, repo, reference, internalRefresh, prefix, getMore, loading, nextPage, setAfterUpdated, onNavigate, onRevert, - changesTreeMessage= ""}) => { + changesTreeMessage}) => { if (results.length === 0) { return
No changes
} else { return
-
{changesTreeMessage}
+ {changesTreeMessage &&
{changesTreeMessage}
} diff --git a/webui/src/lib/components/repository/compareBranches.jsx b/webui/src/lib/components/repository/compareBranches.jsx new file mode 100644 index 00000000000..e6a8c50fe21 --- /dev/null +++ b/webui/src/lib/components/repository/compareBranches.jsx @@ -0,0 +1,164 @@ +import React, {useState} from "react"; +import {refs as refsAPI} from "../../../lib/api"; +import Alert from "react-bootstrap/Alert"; +import {RefTypeBranch, RefTypeCommit} from "../../../constants"; +import {useAPIWithPagination} from "../../hooks/api"; +import {appendMoreResults} from "../../../pages/repositories/repository/changes"; +import {AlertError, Loading} from "../controls"; +import {ChangesTreeContainer, defaultGetMoreChanges} from "./changes"; +import {URINavigator} from "./tree"; +import CompareBranchesActionsBar from "./compareBranchesActionBar"; + +const CompareBranches = ( + {repo, reference, compareReference, showActionsBar, prefix = "", onSelectRef, onSelectCompare} +) => { + const [internalRefresh, setInternalRefresh] = useState(true); + + const [afterUpdated, setAfterUpdated] = useState(""); // state of pagination of the item's children + const [resultsState, setResultsState] = useState({prefix, results: [], pagination: {}}); // current retrieved children of the item + + const delimiter = "/" + + const {nextPage, loading, error} = useAPIWithPagination(async () => { + if (!repo) return + + if (compareReference.id === reference.id) { + return {pagination: {has_more: false}, results: []}; // nothing to compare here. + } + + const getMoreResults = () => + refsAPI.diff(repo.id, reference.id, compareReference.id, afterUpdated, prefix, delimiter); + return await appendMoreResults(resultsState, prefix, afterUpdated, setAfterUpdated, setResultsState, getMoreResults); + }, [repo.id, reference.id, internalRefresh, afterUpdated, delimiter, prefix]) + + const {results} = resultsState; + const apiResult = {results, loading, error, nextPage}; + + const isEmptyDiff = (!loading && !error && !!results && results.length === 0); + + const doRefresh = () => { + setResultsState({prefix, results: [], pagination: {}}) + setInternalRefresh(!internalRefresh) + } + + return ( + <> + {showActionsBar && + + } + + + ); +}; + +const BranchChangesList = ( + {apiResult, repo, reference, compareReference, prefix, delimiter, refresh, setAfterUpdated} +) => { + const {results, loading, error, nextPage} = apiResult; + + if (loading) return ; + if (error) return ; + + const changesTreeMessage = +

+ Showing changes between {reference.id} {""} + and {compareReference.id} +

+ + if (compareReference.id === reference.id) { + return + There isn’t anything to compare. + You’ll need to use two different sources to get a valid comparison. + ; + } + + return ; +}; + +function getURINavigatorRelativeTitle(from, to) { + let fromId = from.id; + let toId = to.id; + if (from.type === RefTypeCommit) { + fromId = fromId.substr(0, 12); + } + if (to.type === RefTypeCommit) { + toId = toId.substr(0, 12); + } + return `${fromId}...${toId}` +} + +const CompareURINavigator = (prefix, reference, compareReference, repo) => + { + const q = { + delimiter: "/", + prefix: query.path, + }; + if (compareReference) { + q.compare = compareReference.id; + } + if (reference) { + q.ref = reference.id; + } + return { + pathname: '/repositories/:repoId/compare', + params: {repoId: repo.id}, + query: q + }; + }}/>; + +const getNavigatorToComparePage = (repo, ref, compareRef) => entry => ({ + pathname: `/repositories/:repoId/compare`, + params: {repoId: repo.id}, + query: { + ref: ref.id, + compare: compareRef.id, + prefix: entry.path, + } +}); + +function getRefID(reference) { + let refID = reference.id; + if (reference.type === RefTypeBranch) { + refID += "@"; + } + return refID; +} + +export default CompareBranches; diff --git a/webui/src/lib/components/repository/compareBranchesActionBar.jsx b/webui/src/lib/components/repository/compareBranchesActionBar.jsx new file mode 100644 index 00000000000..f30c8c812de --- /dev/null +++ b/webui/src/lib/components/repository/compareBranchesActionBar.jsx @@ -0,0 +1,183 @@ +import React, {useCallback, useRef, useState} from "react"; +import {refs as refsAPI} from "../../../lib/api"; +import {RefTypeBranch} from "../../../constants"; +import {ActionGroup, ActionsBar, AlertError, RefreshButton} from "../controls"; +import {MetadataFields} from "./changes"; +import {useRouter} from "../../hooks/router"; +import RefDropdown from "./refDropdown"; +import {ArrowLeftIcon, ArrowSwitchIcon, GitMergeIcon} from "@primer/octicons-react"; +import OverlayTrigger from "react-bootstrap/OverlayTrigger"; +import Tooltip from "react-bootstrap/Tooltip"; +import Button from "react-bootstrap/Button"; +import Modal from "react-bootstrap/Modal"; +import Form from "react-bootstrap/Form"; +import {FormControl, FormHelperText, InputLabel, MenuItem, Select} from "@mui/material"; + +const CompareBranchesActionsBar = ( + {repo, reference, onSelectRef, compareReference, onSelectCompare, doRefresh, isEmptyDiff} +) => { + const router = useRouter(); + const handleSwitchRefs = useCallback((e) => { + e.preventDefault(); + router.push({ + pathname: `/repositories/:repoId/compare`, params: {repoId: repo.id}, + query: {ref: compareReference.id, compare: reference.id} + }); + }, []); + + return + + + + + + + + Switch directions + }> + + + +    + + + + + + + {(compareReference.type === RefTypeBranch && reference.type === RefTypeBranch) && + + } + + ; +}; + +const MergeButton = ({repo, onDone, source, dest, disabled = false}) => { + const textRef = useRef(null); + const [metadataFields, setMetadataFields] = useState([]) + const initialMerge = { + merging: false, + show: false, + err: null, + strategy: "none", + } + const [mergeState, setMergeState] = useState(initialMerge); + + const onClickMerge = useCallback(() => { + setMergeState({merging: mergeState.merging, err: mergeState.err, show: true, strategy: mergeState.strategy}) + } + ); + + const onStrategyChange = (event) => { + setMergeState({ + merging: mergeState.merging, + err: mergeState.err, + show: mergeState.show, + strategy: event.target.value + }); + } + const hide = () => { + if (mergeState.merging) return; + setMergeState(initialMerge); + setMetadataFields([]) + } + + const onSubmit = async () => { + const message = textRef.current.value; + const metadata = {}; + metadataFields.forEach(pair => metadata[pair.key] = pair.value) + + let strategy = mergeState.strategy; + if (strategy === "none") { + strategy = ""; + } + setMergeState({merging: true, show: mergeState.show, err: mergeState.err, strategy: mergeState.strategy}) + try { + await refsAPI.merge(repo.id, source, dest, strategy, message, metadata); + setMergeState({ + merging: mergeState.merging, + show: mergeState.show, + err: null, + strategy: mergeState.strategy + }) + onDone(); + hide(); + } catch (err) { + setMergeState({merging: mergeState.merging, show: mergeState.show, err: err, strategy: mergeState.strategy}) + } + } + + return ( + <> + + + Merge branch {source} into {dest} + + +
+ + + + + + + + Strategy + + + In case of a merge conflict, this option will force the merge process + to automatically favor changes from {dest} (”dest-wins”) or + from {source} (”source-wins”). In case no selection is made, + the merge process will fail in case of a conflict. + {(mergeState.err) ? () : (<>)} +
+ + + + +
+ + + ); +} + +export default CompareBranchesActionsBar; 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/compare.jsx b/webui/src/pages/repositories/repository/compare.jsx index 60ddfd20385..e1b9150c94b 100644 --- a/webui/src/pages/repositories/repository/compare.jsx +++ b/webui/src/pages/repositories/repository/compare.jsx @@ -1,313 +1,52 @@ -import React, {useCallback, useEffect, useState, useRef} from "react"; -import { useOutletContext } from "react-router-dom"; -import {ActionGroup, ActionsBar, AlertError, Loading, RefreshButton} from "../../../lib/components/controls"; +import React, {useEffect} from "react"; +import {useOutletContext} from "react-router-dom"; +import {Loading} from "../../../lib/components/controls"; import {useRefs} from "../../../lib/hooks/repo"; -import RefDropdown from "../../../lib/components/repository/refDropdown"; -import {ArrowLeftIcon, ArrowSwitchIcon, GitMergeIcon} from "@primer/octicons-react"; -import {useAPIWithPagination} from "../../../lib/hooks/api"; -import {refs} from "../../../lib/api"; -import Alert from "react-bootstrap/Alert"; -import {ChangesTreeContainer, defaultGetMoreChanges, MetadataFields} from "../../../lib/components/repository/changes"; import {useRouter} from "../../../lib/hooks/router"; -import {URINavigator} from "../../../lib/components/repository/tree"; -import {appendMoreResults} from "./changes"; -import {RefTypeBranch, RefTypeCommit} from "../../../constants"; -import Button from "react-bootstrap/Button"; -import {FormControl, FormHelperText, InputLabel, MenuItem, Select} from "@mui/material"; -import Modal from "react-bootstrap/Modal"; import {RepoError} from "./error"; -import OverlayTrigger from "react-bootstrap/OverlayTrigger"; -import Tooltip from "react-bootstrap/Tooltip"; -import Form from "react-bootstrap/Form"; - -const CompareList = ({ repo, reference, compareReference, prefix, onSelectRef, onSelectCompare, onNavigate }) => { - const [internalRefresh, setInternalRefresh] = useState(true); - const [afterUpdated, setAfterUpdated] = useState(""); // state of pagination of the item's children - const [resultsState, setResultsState] = useState({prefix: prefix, results:[], pagination:{}}); // current retrieved children of the item - - const router = useRouter(); - const handleSwitchRefs = useCallback( - (e) => { - e.preventDefault(); - router.push({pathname: `/repositories/:repoId/compare`, params: {repoId: repo.id}, - query: {ref: compareReference.id, compare: reference.id}}); - },[] - ); - - const refresh = () => { - setResultsState({prefix: prefix, results:[], pagination:{}}) - setInternalRefresh(!internalRefresh) - } - - const delimiter = "/" - - const { error, loading, nextPage } = useAPIWithPagination(async () => { - if (!repo) return - if (compareReference.id === reference.id) - return {pagination: {has_more: false}, results: []}; // nothing to compare here. - - return await appendMoreResults(resultsState, prefix, afterUpdated, setAfterUpdated, setResultsState, - () => refs.diff(repo.id, reference.id, compareReference.id, afterUpdated, prefix, delimiter)); - }, [repo.id, reference.id, internalRefresh, afterUpdated, delimiter, prefix]) - - let results = resultsState.results - let content; - - const relativeTitle = (from, to) => { - let fromId = from.id; - let toId = to.id; - if (from.type === RefTypeCommit) { - fromId = fromId.substr(0, 12); - } - if (to.type === RefTypeCommit) { - toId = toId.substr(0, 12); - } - - return `${fromId}...${toId}` - } - const uriNavigator = { - const q = { - delimiter: "/", - prefix: query.path, - }; - if (compareReference) - q.compare = compareReference.id; - if (reference) - q.ref = reference.id; - return { - pathname: '/repositories/:repoId/compare', - params: {repoId: repo.id}, - query: q - }; - }}/> - - const changesTreeMessage =

Showing changes between {reference.id} and {compareReference.id}

- let leftCommittedRef = reference.id; - let rightCommittedRef = compareReference.id; - if (reference.type === RefTypeBranch) { - leftCommittedRef += "@"; - } - if (compareReference.type === RefTypeBranch) { - rightCommittedRef += "@"; - } - - if (loading) { - content = - } - else if (error) content = - else if (compareReference.id === reference.id) { - content = ( - - There isn’t anything to compare. - You’ll need to use two different sources to get a valid comparison. - - ) - } - else { - content = - } - - const emptyDiff = (!loading && !error && !!results && results.length === 0); - - return ( - <> - - - - - - - - - Switch directions - }> - - - -    - - - - - - - {(compareReference.type === RefTypeBranch && reference.type === RefTypeBranch) && - - } - - - {content} - - ); -}; - -const MergeButton = ({repo, onDone, source, dest, disabled = false}) => { - const textRef = useRef(null); - const [metadataFields, setMetadataFields] = useState([]) - const initialMerge = { - merging: false, - show: false, - err: null, - strategy: "none", - } - const [mergeState, setMergeState] = useState(initialMerge); - - const onClickMerge = useCallback(() => { - setMergeState({merging: mergeState.merging, err: mergeState.err, show: true, strategy: mergeState.strategy})} - ); - - const onStrategyChange = (event) => { - setMergeState({merging: mergeState.merging, err: mergeState.err, show: mergeState.show, strategy: event.target.value}); - } - const hide = () => { - if (mergeState.merging) return; - setMergeState(initialMerge); - setMetadataFields([]) - } - - const onSubmit = async () => { - const message = textRef.current.value; - const metadata = {}; - metadataFields.forEach(pair => metadata[pair.key] = pair.value) - - let strategy = mergeState.strategy; - if (strategy === "none") { - strategy = ""; - } - setMergeState({merging: true, show: mergeState.show, err: mergeState.err, strategy: mergeState.strategy}) - try { - await refs.merge(repo.id, source, dest, strategy, message, metadata); - setMergeState({merging: mergeState.merging, show: mergeState.show, err: null, strategy: mergeState.strategy}) - onDone(); - hide(); - } catch (err) { - setMergeState({merging: mergeState.merging, show: mergeState.show, err: err, strategy: mergeState.strategy}) - } - } - - return ( - <> - - - Merge branch {source} into {dest} - - -
- - - - - - - - Strategy - - - In case of a merge conflict, this option will force the merge process - to automatically favor changes from {dest} (”dest-wins”) or - from {source} (”source-wins”). In case no selection is made, - the merge process will fail in case of a conflict. - {(mergeState.err) ? () : (<>)} -
- - - - -
- - - ); -} +import CompareBranches from "../../../lib/components/repository/compareBranches"; const CompareContainer = () => { const router = useRouter(); - const { loading, error, repo, reference, compare } = useRefs(); + const {loading, error, repo, reference, compare} = useRefs(); - const { prefix } = router.query; + const {prefix} = router.query; if (loading) return ; if (error) return ; - const route = query => router.push({pathname: `/repositories/:repoId/compare`, params: {repoId: repo.id}, query: { - ...query, - }}); + const route = query => router.push({ + pathname: `/repositories/:repoId/compare`, + params: {repoId: repo.id}, + query + }); + + const onSelectRef = reference => route(compare ? + {ref: reference.id, compare: compare.id} : + {ref: reference.id} + ); + const onSelectCompare = compare => route(reference ? + {ref: reference.id, compare: compare.id} : + {compare: compare.id} + ); return ( - route(compare ? {ref: reference.id, compare: compare.id} : {ref: reference.id})} compareReference={compare} - onSelectCompare={compare => route(reference ? {ref: reference.id, compare: compare.id} : {compare: compare.id})} - onNavigate={entry => { - return { - pathname: `/repositories/:repoId/compare`, - params: {repoId: repo.id}, - query: { - ref: reference.id, - compare: compare.id, - prefix: entry.path, - } - } - }} + showActionsBar={true} + prefix={prefix} + onSelectRef={onSelectRef} + onSelectCompare={onSelectCompare} /> ); }; const RepositoryComparePage = () => { - const [setActivePage] = useOutletContext(); - useEffect(() => setActivePage("compare"), [setActivePage]); - return ; + const [setActivePage] = useOutletContext(); + useEffect(() => setActivePage("compare"), [setActivePage]); + return ; }; export default RepositoryComparePage; 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..9e637094134 --- /dev/null +++ b/webui/src/pages/repositories/repository/pulls/pullDetails.jsx @@ -0,0 +1,115 @@ +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"; +import CompareBranches from "../../../../lib/components/repository/compareBranches"; +import {PullStatus, RefTypeBranch} from "../../../../constants"; + +const BranchLink = ({repo, branch}) => + + {branch} + ; + +const StatusBadge = ({status}) => { + const text = {status}; + switch (status) { + case PullStatus.open: + return {} {text}; + case PullStatus.closed: + return {} {text}; + case PullStatus.merged: + return {} {text}; + default: + return {text}; + } +}; + +const PullDetailsContent = ({repo, pull}) => { + const createdAt = dayjs.unix(pull.creation_date); + + return ( +
+

{pull.title} {pull.id}

+
+ + + {pull.author} wants to merge {""} + {""} + into . + +
+ + + Opened on {createdAt.format("MMM D, YYYY")} ({createdAt.fromNow()}). + + + {pull.description} + + +
+
+ + +
+
+
+
+ +
+
+ ); +}; + +const PullDetails = ({repo, pullId}) => { + const {response: pull, error, loading} = useAPI(async () => { + 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..c4ba0af74a5 100644 --- a/webui/src/pages/repositories/repository/pulls/pulls.jsx +++ b/webui/src/pages/repositories/repository/pulls/pulls.jsx @@ -8,13 +8,14 @@ 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"; import {useRouter} from "../../../../lib/hooks/router"; import {RepoError} from "../error"; import {Link} from "../../../../lib/components/nav"; +import {PullStatus} from "../../../../constants"; const PullWidget = ({repo, pull}) => { @@ -26,11 +27,11 @@ const PullWidget = ({repo, pull}) => { pathname: '/repositories/:repoId/pulls/:pullId', params: {repoId: repo.id, pullId: pull.id} }}> - {pull.title} + {pull.title} - Opened {dayjs.unix(pull.created_at).fromNow()} by {pull.author} + Opened {dayjs.unix(pull.creation_date).fromNow()} by {pull.author}
@@ -42,20 +43,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 {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); @@ -102,7 +96,14 @@ const PullsList = ({repo, after, prefix, onPaginate}) => { })}/> - +
{content} @@ -110,7 +111,6 @@ const PullsList = ({repo, after, prefix, onPaginate}) => { ); }; - const PullsContainer = () => { const router = useRouter() const {repo, loading, error} = useRefs(); @@ -139,7 +139,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