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;