|
| 1 | +import React, { Component } from "react"; |
| 2 | +import PropTypes from "prop-types"; |
| 3 | +import { Label, Container, Divider, Grid, Header, Icon, List, Message, Segment } from "semantic-ui-react"; |
| 4 | +import { http } from "react-invenio-forms"; |
| 5 | +import { withCancel, ErrorMessage } from "react-invenio-forms"; |
| 6 | +import { DateTime } from "luxon"; |
| 7 | + |
| 8 | +export class RunsLogs extends Component { |
| 9 | + constructor(props) { |
| 10 | + super(props); |
| 11 | + this.state = { |
| 12 | + error: null, |
| 13 | + logs: this.props.logs, |
| 14 | + run: this.props.run, |
| 15 | + sort: this.props.sort, |
| 16 | + runDuration: this.props.runDuration, |
| 17 | + }; |
| 18 | + } |
| 19 | + |
| 20 | + fetchLogs = async (runId, sort) => { |
| 21 | + try { |
| 22 | + const searchAfterParams = (sort || []).map((value) => `search_after=${value}`).join("&"); |
| 23 | + this.cancellableFetch = withCancel( |
| 24 | + http.get(`/api/logs/jobs?q=${runId}&${searchAfterParams}`) |
| 25 | + ); |
| 26 | + const response = await this.cancellableFetch.promise; |
| 27 | + if (response.status !== 200) { |
| 28 | + throw new Error(`Failed to fetch logs: ${response.statusText}`); |
| 29 | + } |
| 30 | + |
| 31 | + const formattedLogs = response.data.hits.hits.map((log) => ({ |
| 32 | + ...log, |
| 33 | + formatted_timestamp: DateTime.fromISO(log.timestamp).toFormat("yyyy-MM-dd HH:mm"), |
| 34 | + })); |
| 35 | + const newSort = response.data.hits.sort; |
| 36 | + |
| 37 | + this.setState((prevState) => ({ |
| 38 | + logs: [...prevState.logs, ...formattedLogs], |
| 39 | + error: null, |
| 40 | + sort: newSort || prevState.sort, // Update sort only if newSort exists |
| 41 | + })); // Append logs and clear error |
| 42 | + } catch (err) { |
| 43 | + console.error("Error fetching logs:", err); |
| 44 | + this.setState({ error: err.message }); |
| 45 | + } |
| 46 | + }; |
| 47 | + |
| 48 | + getDurationInMinutes(startedAt, finishedAt) { |
| 49 | + if (!startedAt) return 0; |
| 50 | + |
| 51 | + const start = DateTime.fromISO(startedAt); |
| 52 | + const end = finishedAt |
| 53 | + ? DateTime.fromISO(finishedAt) |
| 54 | + : DateTime.now(); |
| 55 | + |
| 56 | + const duration = end.diff(start, "minutes").minutes; |
| 57 | + |
| 58 | + return Math.floor(duration); |
| 59 | + } |
| 60 | + |
| 61 | + formatDatetime(timestamp) { |
| 62 | + if (!timestamp) return null; |
| 63 | + |
| 64 | + return DateTime.fromISO(timestamp).toFormat("yyyy-MM-dd HH:mm"); |
| 65 | + } |
| 66 | + |
| 67 | + checkRunStatus = async (runId, jobId) => { |
| 68 | + try { |
| 69 | + this.cancellableFetch = withCancel( |
| 70 | + http.get(`/api/jobs/${jobId}/runs/${runId}`) |
| 71 | + ); |
| 72 | + const response = await this.cancellableFetch.promise; |
| 73 | + if (response.status !== 200) { |
| 74 | + throw new Error(`Failed to fetch run status: ${response.statusText}`); |
| 75 | + } |
| 76 | + |
| 77 | + const run = response.data; |
| 78 | + run.formatted_started_at = this.formatDatetime(run.started_at); |
| 79 | + const runDuration = this.getDurationInMinutes(run.started_at, run.finished_at); |
| 80 | + this.setState({ run: run, runDuration: runDuration }); |
| 81 | + if (run.status === "SUCCESS" || run.status === "FAILED" || run.status === "PARTIAL_SUCCESS") { |
| 82 | + clearInterval(this.logsInterval); // Stop fetching logs if run finihsed |
| 83 | + } |
| 84 | + } catch (err) { |
| 85 | + console.error("Error checking run status:", err); |
| 86 | + this.setState({ error: err.message }); |
| 87 | + } |
| 88 | + }; |
| 89 | + |
| 90 | + componentDidMount() { |
| 91 | + this.logsInterval = setInterval(async () => { |
| 92 | + const { run, sort } = this.state; |
| 93 | + if (run.status === "RUNNING") { |
| 94 | + await this.fetchLogs(run.id, sort); // Fetch logs only if the run is running |
| 95 | + await this.checkRunStatus(run.id, run.job_id); // Check the run status |
| 96 | + } |
| 97 | + }, 2000); |
| 98 | + } |
| 99 | + |
| 100 | + componentWillUnmount() { |
| 101 | + clearInterval(this.logsInterval); |
| 102 | + } |
| 103 | + |
| 104 | + render() { |
| 105 | + const { error, logs, run, runDuration, sort } = this.state; |
| 106 | + const levelClassMapping = { |
| 107 | + DEBUG: "", |
| 108 | + INFO: "primary", |
| 109 | + WARNING: "warning", |
| 110 | + ERROR: "negative", |
| 111 | + CRITICAL: "negative", |
| 112 | + }; |
| 113 | + |
| 114 | + const getClassForLogLevel = (level) => levelClassMapping[level] || ""; |
| 115 | + return ( |
| 116 | + <Container> |
| 117 | + <Header as="h2" className="mt-20"> |
| 118 | + {run.title} |
| 119 | + </Header> |
| 120 | + <Divider /> |
| 121 | + {error && ( |
| 122 | + <Message negative> |
| 123 | + <Message.Header>Error Fetching Logs</Message.Header> |
| 124 | + <p>{error}</p> |
| 125 | + </Message> |
| 126 | + )} |
| 127 | + <Grid celled> |
| 128 | + <Grid.Row> |
| 129 | + <Grid.Column width={3}> |
| 130 | + <Header as="h4" color="grey"> |
| 131 | + Job run |
| 132 | + </Header> |
| 133 | + <List> |
| 134 | + <List.Item> |
| 135 | + <Icon |
| 136 | + name={ |
| 137 | + run.status === "SUCCESS" |
| 138 | + ? "check circle" |
| 139 | + : run.status === "FAILED" |
| 140 | + ? "times circle" |
| 141 | + : run.status === "RUNNING" |
| 142 | + ? "spinner" |
| 143 | + : run.status === "PARTIAL_SUCCESS" |
| 144 | + ? "exclamation circle" |
| 145 | + : "clock outline" |
| 146 | + } |
| 147 | + color={ |
| 148 | + run.status === "SUCCESS" |
| 149 | + ? "green" |
| 150 | + : run.status === "FAILED" |
| 151 | + ? "red" |
| 152 | + : run.status === "RUNNING" |
| 153 | + ? "blue" |
| 154 | + : run.status === "PARTIAL_SUCCESS" |
| 155 | + ? "orange" |
| 156 | + : "grey" |
| 157 | + } |
| 158 | + /> |
| 159 | + <List.Content> |
| 160 | + {run.formatted_started_at ? ( |
| 161 | + <> |
| 162 | + <p> |
| 163 | + <strong>{run.formatted_started_at}</strong> |
| 164 | + </p> |
| 165 | + <p className="description">{runDuration} mins</p> |
| 166 | + </> |
| 167 | + ) : ( |
| 168 | + <p className="description">Not yet started</p> |
| 169 | + )} |
| 170 | + {run.message && ( |
| 171 | + <Label basic |
| 172 | + color={ |
| 173 | + run.status === "SUCCESS" |
| 174 | + ? "green" |
| 175 | + : run.status === "FAILED" |
| 176 | + ? "red" |
| 177 | + : run.status === "PARTIAL_SUCCESS" |
| 178 | + ? "orange" |
| 179 | + : "blue" |
| 180 | + } |
| 181 | + > |
| 182 | + <p>{run.message}</p> |
| 183 | + </Label> |
| 184 | + )} |
| 185 | + </List.Content> |
| 186 | + </List.Item> |
| 187 | + </List> |
| 188 | + </Grid.Column> |
| 189 | + <Grid.Column className="log-table" width={13}> |
| 190 | + <Segment> |
| 191 | + {logs.map((log, index) => ( |
| 192 | + <div key={index} className={`log-line ${log.level.toLowerCase()}`}> |
| 193 | + <span className="log-timestamp">[{log.formatted_timestamp}]</span>{" "} |
| 194 | + <span className={getClassForLogLevel(log.level)}>{log.level}</span>{" "} |
| 195 | + <span className="log-message">{log.message}</span> |
| 196 | + </div> |
| 197 | + ))} |
| 198 | + </Segment> |
| 199 | + </Grid.Column> |
| 200 | + </Grid.Row> |
| 201 | + </Grid> |
| 202 | + </Container> |
| 203 | + ); |
| 204 | + } |
| 205 | +} |
| 206 | + |
| 207 | +RunsLogs.propTypes = { |
| 208 | + run: PropTypes.object.isRequired, |
| 209 | + logs: PropTypes.array.isRequired, |
| 210 | + runDuration: PropTypes.number.isRequired, |
| 211 | + sort: PropTypes.array.isRequired, |
| 212 | +}; |
0 commit comments