github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/webui/src/pages/repositories/repository/settings/branches.jsx (about) 1 import React, {useEffect, useRef, useState} from "react"; 2 import { useOutletContext } from "react-router-dom"; 3 import {AlertError, Loading, RefreshButton} from "../../../../lib/components/controls"; 4 import {useRefs} from "../../../../lib/hooks/repo"; 5 import Card from "react-bootstrap/Card"; 6 import {Button, ListGroup, Row} from "react-bootstrap"; 7 import Col from "react-bootstrap/Col"; 8 import {useAPI} from "../../../../lib/hooks/api"; 9 import {branchProtectionRules} from "../../../../lib/api"; 10 import Modal from "react-bootstrap/Modal"; 11 import Form from "react-bootstrap/Form"; 12 import Alert from "react-bootstrap/Alert"; 13 14 const SettingsContainer = () => { 15 const {repo, loading, error} = useRefs(); 16 const [showCreateModal, setShowCreateModal] = useState(false); 17 const [refresh, setRefresh] = useState(false); 18 const [actionError, setActionError] = useState(null); 19 const [deleteButtonDisabled, setDeleteButtonDisabled] = useState(false) 20 21 const {response: rulesResponse, error: rulesError, loading: rulesLoading} = useAPI(async () => { 22 return branchProtectionRules.getRules(repo.id) 23 }, [repo, refresh]) 24 const deleteRule = (pattern) => { 25 let updatedRules = [...rulesResponse['rules']] 26 let lastKnownChecksum = rulesResponse['checksum'] 27 updatedRules = updatedRules.filter(r => r.pattern !== pattern) 28 branchProtectionRules.setRules(repo.id, updatedRules, lastKnownChecksum).then(() => { 29 setRefresh(!refresh) 30 setDeleteButtonDisabled(false) 31 }).catch(err => { 32 setDeleteButtonDisabled(false) 33 setActionError(err) 34 }) 35 } 36 if (error) return <AlertError error={error}/>; 37 if (rulesError) return <AlertError error={rulesError}/>; 38 if (actionError) return <AlertError error={actionError}/>; 39 return (<> 40 <div className="mt-3 mb-5"> 41 <div className={"section-title"}> 42 <h4 className={"mb-0"}> 43 <div className={"ms-1 me-1 pl-0 d-flex"}> 44 <div className="flex-grow-1">Branch protection rules</div> 45 <RefreshButton className={"ms-1"} onClick={() => {setRefresh(!refresh)}}/> 46 <Button className={"ms-2"} onClick={() => setShowCreateModal(true)}>Add</Button> 47 </div> 48 </h4> 49 </div> 50 <div> 51 Define branch protection rules to prevent direct changes. 52 Changes to protected branches can only be done by merging from other branches. 53 {/* eslint-disable-next-line react/jsx-no-target-blank */} 54 <a href="https://docs.lakefs.io/reference/protected_branches.html" target="_blank">Learn more.</a> 55 </div> 56 {loading || rulesLoading ? <div className={"mt-3 ms-1 pr-5"}><Loading/></div> : 57 <div className={"row mt-3 ms-1 pr-5"}> 58 <Card className={"w-100 rounded border-0"}> 59 <Card.Body className={"p-0 rounded"}> 60 <ListGroup> 61 {rulesResponse && rulesResponse['rules'].length > 0 ? rulesResponse['rules'].map((r) => { 62 return <ListGroup.Item key={r.pattern}> 63 <div className="d-flex"> 64 <code>{r.pattern}</code> 65 <Button disabled={deleteButtonDisabled} className="ms-auto" size="sm" variant="secondary" onClick={() => deleteRule(r.pattern)}>Delete</Button> 66 </div> 67 </ListGroup.Item> 68 }) : <Alert variant="info">There aren't any rules yet.</Alert>} 69 </ListGroup> 70 </Card.Body> 71 </Card> 72 </div>} 73 </div> 74 <CreateRuleModal show={showCreateModal} hideFn={() => setShowCreateModal(false)} currentRulesResponse={rulesResponse} onSuccess={() => { 75 setRefresh(!refresh) 76 setShowCreateModal(false) 77 }} repoID={repo.id}/> 78 </>); 79 } 80 const CreateRuleModal = ({show, hideFn, onSuccess, repoID, currentRulesResponse}) => { 81 const [error, setError] = useState(null); 82 const [createButtonDisabled, setCreateButtonDisabled] = useState(true); 83 const patternField = useRef(null); 84 85 const createRule = (pattern) => { 86 if (createButtonDisabled) { 87 return 88 } 89 setError(null) 90 setCreateButtonDisabled(true) 91 let updatedRules = [...currentRulesResponse['rules']] 92 let lastKnownChecksum = currentRulesResponse['checksum'] 93 updatedRules.push({pattern}) 94 branchProtectionRules.setRules(repoID, updatedRules, lastKnownChecksum).then(onSuccess).catch(err => { 95 setError(err) 96 setCreateButtonDisabled(false) 97 }) 98 } 99 return <Modal show={show} onHide={() => { 100 setCreateButtonDisabled(true) 101 setError(null) 102 hideFn() 103 }}> 104 <Modal.Header closeButton> 105 <Modal.Title>Create Branch Protection Rule</Modal.Title> 106 </Modal.Header> 107 108 <Modal.Body className={"w-100"}> 109 <Form onSubmit={(e) => { 110 e.preventDefault(); 111 createRule(patternField.current.value); 112 }}> 113 <Form.Group as={Row} controlId="pattern"> 114 <Form.Label column sm={4}>Branch name pattern</Form.Label> 115 <Col> 116 <Form.Control sm={8} type="text" autoFocus ref={patternField} 117 onChange={() => setCreateButtonDisabled(!patternField.current || !patternField.current.value)}/> 118 </Col> 119 </Form.Group> 120 </Form> 121 {error && <AlertError error={error}/>} 122 </Modal.Body> 123 <Modal.Footer> 124 <Button disabled={createButtonDisabled} onClick={() => createRule(patternField.current.value)} 125 variant="success">Create</Button> 126 <Button onClick={() => { 127 setCreateButtonDisabled(true) 128 setError(null) 129 hideFn() 130 }} variant="secondary">Cancel</Button> 131 </Modal.Footer> 132 </Modal> 133 } 134 135 const RepositorySettingsBranchesPage = () => { 136 const [setActiveTab] = useOutletContext(); 137 useEffect(() => setActiveTab("branches"), [setActiveTab]); 138 return <SettingsContainer />; 139 }; 140 141 export default RepositorySettingsBranchesPage;