github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/webui/src/pages/repositories/repository/actions/run/index.jsx (about) 1 import React, {useEffect, useState} from "react"; 2 import {useOutletContext} from "react-router-dom"; 3 4 import {AlertError, FormattedDate, Loading, Na} from "../../../../../lib/components/controls"; 5 import {useRefs} from "../../../../../lib/hooks/repo"; 6 import {useAPI} from "../../../../../lib/hooks/api"; 7 import {actions} from "../../../../../lib/api"; 8 import Row from "react-bootstrap/Row"; 9 import Col from "react-bootstrap/Col"; 10 import ListGroup from "react-bootstrap/ListGroup"; 11 import { 12 ChevronDownIcon, ChevronRightIcon, 13 HomeIcon, 14 PlayIcon, 15 } from "@primer/octicons-react"; 16 import Button from "react-bootstrap/Button"; 17 import dayjs from "dayjs"; 18 import duration from "dayjs/plugin/duration"; 19 import {ActionStatusIcon} from "../../../../../lib/components/repository/actions"; 20 import Table from "react-bootstrap/Table"; 21 import {Link} from "../../../../../lib/components/nav"; 22 import {useRouter} from "../../../../../lib/hooks/router"; 23 24 dayjs.extend(duration) 25 26 const RunSummary = ({ repo, run }) => { 27 return ( 28 <Table size="lg"> 29 <tbody> 30 <tr> 31 <td><strong>ID</strong></td> 32 <td>{run.run_id}</td> 33 </tr> 34 <tr> 35 <td><strong>Event Type</strong></td> 36 <td>{run.event_type}</td> 37 </tr> 38 <tr> 39 <td><strong>Status</strong></td> 40 <td>{run.status}</td> 41 </tr> 42 <tr> 43 <td><strong>Branch</strong></td> 44 <td> 45 {(!run.branch) ? <Na/> : 46 <Link className="me-2" href={{ 47 pathname: '/repositories/:repoId/objects', 48 params: {repoId: repo.id}, 49 query: {ref: run.branch} 50 }}> 51 {run.branch} 52 </Link> 53 } 54 </td> 55 </tr> 56 <tr> 57 <td><strong>Commit</strong></td> 58 <td> 59 {(!run.commit_id) ? <Na/> : <Link className="me-2" href={{ 60 pathname: '/repositories/:repoId/commits/:commitId', 61 params: {repoId: repo.id, commitId: run.commit_id} 62 }}> 63 <code>{run.commit_id.substr(0, 12)}</code> 64 </Link> 65 } 66 </td> 67 </tr> 68 <tr> 69 <td><strong>Start Time</strong></td> 70 <td>{(!run.start_time) ? <Na/> :<FormattedDate dateValue={run.start_time}/>}</td> 71 </tr> 72 <tr> 73 <td><strong>End Time</strong></td> 74 <td>{(!run.end_time) ? <Na/> :<FormattedDate dateValue={run.end_time}/>}</td> 75 </tr> 76 </tbody> 77 </Table> 78 ); 79 }; 80 81 82 const HookLog = ({ repo, run, execution }) => { 83 const [expanded, setExpanded] = useState(false); 84 const {response, loading, error} = useAPI(() => { 85 if (!expanded) return ''; 86 return actions.getRunHookOutput(repo.id, run.run_id, execution.hook_run_id); 87 }, [repo.id, execution.hook_id, execution.hook_run_id, expanded]); 88 89 let content = <></>; 90 if (expanded) { 91 if (loading) { 92 content = <pre>Loading...</pre>; 93 } else if (error) { 94 content = <AlertError error={error}/>; 95 } else { 96 content = <pre>{response}</pre>; 97 } 98 } 99 100 let duration = '(running)'; 101 if (execution.status === 'completed' || execution.status === 'failed') { 102 const endTs = dayjs(execution.end_time); 103 const startTs = dayjs(execution.start_time); 104 const diff = dayjs.duration(endTs.diff(startTs)).asSeconds(); 105 duration = `(${execution.status} in ${diff}s)`; 106 } else if (execution.status === 'skipped') { 107 duration = '(skipped)' 108 } 109 110 return ( 111 <div className="hook-log"> 112 113 <p className="mb-3 hook-log-title"> 114 <Button variant="link" onClick={() => {setExpanded(!expanded)}} disabled={execution.status === "skipped"}> 115 {(expanded) ? <ChevronDownIcon size="small"/> : <ChevronRightIcon size="small"/>} 116 </Button> 117 {' '} 118 <ActionStatusIcon status={execution.status}/> 119 {' '} 120 {execution.hook_id} 121 122 <small> 123 {duration} 124 </small> 125 </p> 126 127 <div className="hook-log-content"> 128 {content} 129 </div> 130 </div> 131 ); 132 } 133 134 const ExecutionsExplorer = ({ repo, run, executions }) => { 135 return ( 136 <div className="hook-logs"> 137 {executions.map(exec => ( 138 <HookLog key={`${exec.hook_id}-${exec.hook_run_id}`} repo={repo} run={run} execution={exec}/> 139 ))} 140 </div> 141 ); 142 }; 143 144 const ActionBrowser = ({ repo, run, hooks, onSelectAction, selectedAction = null }) => { 145 146 const hookRuns = hooks.results; 147 148 // group by action 149 const actionNames = {}; 150 hookRuns.forEach(hookRun => { actionNames[hookRun.action] = true }); 151 const actions = Object.getOwnPropertyNames(actionNames).sort(); 152 153 let content = <RunSummary repo={repo} run={run}/> 154 if (selectedAction !== null) { 155 // we're looking at a specific action, let's filter 156 const actionRuns = hookRuns 157 .filter(hook => hook.action === selectedAction) 158 .sort((a, b) => { 159 if (a.hook_run_id > b.hook_run_id) return 1; 160 else if (a.hook_run_id < b.hook_run_id) return -1; 161 return 0; 162 }) 163 content = <ExecutionsExplorer run={run} repo={repo} executions={actionRuns}/>; 164 } 165 166 return ( 167 <Row className="mt-3"> 168 <Col md={{span: 3}}> 169 <ListGroup variant="flush"> 170 <ListGroup.Item action 171 onClick={() => onSelectAction(null)}> 172 <HomeIcon/> Summary 173 </ListGroup.Item> 174 </ListGroup> 175 176 <div className="mt-3"> 177 178 <h6>Actions</h6> 179 180 <ListGroup> 181 {actions.map(actionName => ( 182 <ListGroup.Item action 183 key={actionName} 184 onClick={() => onSelectAction(actionName)}> 185 <PlayIcon/> {actionName} 186 </ListGroup.Item> 187 ))} 188 </ListGroup> 189 </div> 190 </Col> 191 <Col md={{span: 9}}> 192 {content} 193 </Col> 194 </Row> 195 ); 196 }; 197 198 199 const RunContainer = ({ repo, runId, onSelectAction, selectedAction }) => { 200 const {response, error, loading} = useAPI(async () => { 201 const [ run, hooks ] = await Promise.all([ 202 actions.getRun(repo.id, runId), 203 actions.listRunHooks(repo.id, runId) 204 ]); 205 return {run, hooks}; 206 }, [repo.id, runId]); 207 208 if (loading) return <Loading/>; 209 if (error) return <AlertError error={error}/>; 210 211 return ( 212 <ActionBrowser 213 repo={repo} 214 run={response.run} 215 hooks={response.hooks} 216 onSelectAction={onSelectAction} 217 selectedAction={selectedAction} 218 /> 219 ) 220 } 221 222 const ActionContainer = () => { 223 const router = useRouter(); 224 const { action } = router.query; 225 const { runId } = router.params; 226 const {loading, error, repo} = useRefs(); 227 228 if (loading) return <Loading/>; 229 if (error) return <AlertError error={error}/>; 230 231 const params = {repoId: repo.id, runId}; 232 233 return <RunContainer 234 repo={repo} 235 runId={runId} 236 selectedAction={(action) ? action : null} 237 onSelectAction={action => { 238 const query = {}; 239 if (action) query.action = action; 240 router.push({ 241 pathname: '/repositories/:repoId/actions/:runId', query, params 242 }); 243 }} 244 /> 245 } 246 247 const RepositoryActionPage = () => { 248 const [setActivePage] = useOutletContext(); 249 useEffect(() => setActivePage('actions'), [setActivePage]); 250 return <ActionContainer/>; 251 }; 252 253 export default RepositoryActionPage;