Update UI page List log
This commit is contained in:
parent
3d41d03f79
commit
9e0543db89
|
|
@ -215,11 +215,24 @@ export default class LogsController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAllLogDetect({ response }: HttpContextContract) {
|
public async getAllLogDetect({ request, response }: HttpContextContract) {
|
||||||
try {
|
try {
|
||||||
const files = await LogDetectFile.all();
|
const page = request.input('page', 1); // mặc định page 1
|
||||||
const fileNames = files.map((file) => file.file_name);
|
const limit = request.input('limit', 100); // mặc định 50 record / page
|
||||||
response.status(200).send(fileNames);
|
const keyword = request.input('search', '');
|
||||||
|
|
||||||
|
const logs = await LogDetectFile
|
||||||
|
.query()
|
||||||
|
.if(keyword, (query) => {
|
||||||
|
query.where('file_name', 'like', `%${keyword}%`)
|
||||||
|
})
|
||||||
|
.orderBy('created_at', 'desc')
|
||||||
|
.paginate(page, limit);
|
||||||
|
|
||||||
|
return response.status(200).send({
|
||||||
|
data: logs.all().map(file => file.file_name),
|
||||||
|
meta: logs.getMeta()
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
response.status(203).send("NO FILE");
|
response.status(203).send("NO FILE");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,3 +44,333 @@
|
||||||
.inputSearch input{
|
.inputSearch input{
|
||||||
width: 30%;
|
width: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Segoe UI", Roboto, sans-serif;
|
||||||
|
background: #f4f6f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== SIDEBAR ===== */
|
||||||
|
.sidebar {
|
||||||
|
width: 530px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-right: 1px solid #e5e7eb;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar h2,
|
||||||
|
.sidebar h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #111827;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileList {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 72vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileItem {
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #374151;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileItem:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #111827;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== MAIN CONTENT ===== */
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== TOP BAR ===== */
|
||||||
|
.topBar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
padding-bottom: 15px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainerRun {
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== BUTTONS ===== */
|
||||||
|
.btn {
|
||||||
|
padding: 8px 14px;
|
||||||
|
background: #2563eb;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.success {
|
||||||
|
background: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.success:hover {
|
||||||
|
background: #15803d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.full {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== INPUT ===== */
|
||||||
|
.input {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
outline: none;
|
||||||
|
transition: 0.2s;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus {
|
||||||
|
border-color: #2563eb;
|
||||||
|
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== TERMINAL (Search Result) ===== */
|
||||||
|
.terminal {
|
||||||
|
height: 130px;
|
||||||
|
background: #f9fafb;
|
||||||
|
color: #111827;
|
||||||
|
padding: 15px;
|
||||||
|
font-family: monospace;
|
||||||
|
border: none;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== LOG VIEWER ===== */
|
||||||
|
.logViewer {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 15px 20px;
|
||||||
|
font-family: monospace;
|
||||||
|
background: #fafafa;
|
||||||
|
max-width: 66vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logLine {
|
||||||
|
display: flex;
|
||||||
|
padding: 3px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logLine:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lineNumber {
|
||||||
|
width: 40px;
|
||||||
|
color: #9ca3af;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== ISSUE PANEL ===== */
|
||||||
|
.issuePanel {
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: #fff7ed;
|
||||||
|
border-bottom: 1px solid #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issueItem a {
|
||||||
|
color: #b91c1c;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issueItem a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dangerTitle {
|
||||||
|
color: #dc2626;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== FILE NAME ===== */
|
||||||
|
.fileName {
|
||||||
|
padding: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activeFile {
|
||||||
|
background: #e0f2fe;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0369a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
padding: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detectedPanel {
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detectedTitle {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detectedTitle.info {
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detectedTitle.danger {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detectedItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
/* gap: 10px; */
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dangerItem {
|
||||||
|
background: #fff1f2;
|
||||||
|
border-color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lineNumberSmall {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jumpLink {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #2563eb;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jumpLink:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noIssue {
|
||||||
|
padding: 10px;
|
||||||
|
background: #ecfdf5;
|
||||||
|
border: 1px solid #a7f3d0;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #065f46;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemDetected{
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemDetectedModel {
|
||||||
|
margin-left: 4px;
|
||||||
|
background-color: rgb(155, 223, 240);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemDetectedModel:hover {
|
||||||
|
background-color: rgba(151, 190, 216, 0.583);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageBtn {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageBtn:hover:not(:disabled) {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageBtn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageInfo {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isDisabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
-moz-user-focus: none;
|
||||||
|
-webkit-user-focus: none;
|
||||||
|
-ms-user-focus: none;
|
||||||
|
-moz-user-modify: read-only;
|
||||||
|
-webkit-user-modify: read-only;
|
||||||
|
-ms-user-modify: read-only;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
@ -1,105 +1,240 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Link, Navigate, useParams } from "react-router-dom";
|
import { getListLog, getLog, findValue } from "../../api/apiLog";
|
||||||
import { findValue, getListLog } from "../../api/apiLog";
|
|
||||||
import "./ListLog.css";
|
import "./ListLog.css";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
const ListLog = () => {
|
const ListLog = () => {
|
||||||
const [listFile, setListFile] = useState([]);
|
const [listFile, setListFile] = useState([]);
|
||||||
const [status, setStatus] = useState(200);
|
const [meta, setMeta] = useState(null);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [limit] = useState(100);
|
||||||
|
const [selectedFile, setSelectedFile] = useState(null);
|
||||||
|
const [log, setLog] = useState(null);
|
||||||
|
const [loadingListFile, setLoadingListFile] = useState(true);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [loadingSearchValue, setLoadingSearchValue] = useState(false);
|
||||||
|
|
||||||
const [nameSearch, setNameSearch] = useState("");
|
const [nameSearch, setNameSearch] = useState("");
|
||||||
const [valueSearch, setValueSearch] = useState("");
|
const [valueSearch, setValueSearch] = useState("");
|
||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState("");
|
||||||
const getListFile = async () => {
|
|
||||||
|
/* ========================= */
|
||||||
|
/* Load file list */
|
||||||
|
/* ========================= */
|
||||||
|
const getListFile = async (pageNumber = 1) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(getListLog);
|
setLoadingListFile(true)
|
||||||
setListFile(res.data);
|
const res = await axios.get(
|
||||||
setStatus(res.status);
|
`${getListLog}?page=${pageNumber}&limit=${limit}&search=${nameSearch}`
|
||||||
} catch (error) {
|
);
|
||||||
console.log(error);
|
|
||||||
|
setListFile(res.data.data);
|
||||||
|
setMeta(res.data.meta);
|
||||||
|
setPage(pageNumber);
|
||||||
|
setLoadingListFile(false)
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* ========================= */
|
||||||
|
/* Load content when select */
|
||||||
|
/* ========================= */
|
||||||
|
const loadFileContent = async (fileName) => {
|
||||||
|
setSelectedFile(fileName);
|
||||||
|
setLoading(true);
|
||||||
|
setLog(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await axios.get(getLog + "/" + fileName);
|
||||||
|
setLog(res.data);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ========================= */
|
||||||
|
/* Search value */
|
||||||
|
/* ========================= */
|
||||||
const findValueInLog = async () => {
|
const findValueInLog = async () => {
|
||||||
try {
|
try {
|
||||||
|
setLoadingSearchValue(true)
|
||||||
const res = await axios.post(findValue, { value: valueSearch });
|
const res = await axios.post(findValue, { value: valueSearch });
|
||||||
|
setValue(res.data);
|
||||||
setValue(res.data)
|
setLoadingSearchValue(false)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
setValue("Searching value is failed");
|
||||||
|
setLoadingSearchValue(false)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getListFile();
|
getListFile(page);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (status === 200) {
|
|
||||||
return (
|
return (
|
||||||
<div className="mainList">
|
<div className="layout">
|
||||||
<div className="inputSearch">
|
{/* ================= Sidebar ================= */}
|
||||||
<Link to={"/"}>
|
<div className="sidebar">
|
||||||
<button
|
<div className="topBar">
|
||||||
style={{
|
<div >
|
||||||
color: "white",
|
<Link to="/">
|
||||||
backgroundColor: "blue",
|
<button className="btn">Home</button>
|
||||||
cursor: "pointer",
|
|
||||||
float: "left",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Home
|
|
||||||
</button>
|
|
||||||
</Link>
|
</Link>
|
||||||
<div>
|
</div>
|
||||||
<label>Search file name: </label>
|
<h2>📁 Log Files</h2>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="inputContainer">
|
||||||
<input
|
<input
|
||||||
|
className="input"
|
||||||
|
placeholder="Search file..."
|
||||||
value={nameSearch}
|
value={nameSearch}
|
||||||
placeholder={"Enter a file name"}
|
onChange={(e) => setNameSearch(e.target.value)}
|
||||||
onChange={(e) => {
|
/>
|
||||||
setNameSearch(e.target.value);
|
<button className="btn success" onClick={() => getListFile(page)}>
|
||||||
}}
|
Search
|
||||||
></input>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="fileList">
|
||||||
<label>Search value: </label>
|
{/* ===== Loading ===== */}
|
||||||
<input
|
{loadingListFile && <div className="loading">Loading list file...</div>}
|
||||||
value={valueSearch}
|
|
||||||
placeholder={"Enter value"}
|
{!loadingListFile && listFile?.map((file, i) => (
|
||||||
onChange={(e) => {
|
<div
|
||||||
setValueSearch(e.target.value);
|
key={i}
|
||||||
}}
|
className={`fileItem ${selectedFile === file ? "activeFile" : ""
|
||||||
></input>
|
}`}
|
||||||
<button style={{backgroundColor:"#6cff00", cursor:"pointer"}} onClick={()=>findValueInLog()}>Run</button>
|
onClick={() => loadFileContent(file)}
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
style={{ height: "90vh", width: "50%", backgroundColor: "black", float:"right", color:"#6cff00", padding:10, display:value!==""?"block":"none" }}
|
|
||||||
value={value}
|
|
||||||
>
|
>
|
||||||
|
{file}
|
||||||
</textarea>
|
|
||||||
</div>
|
|
||||||
{listFile
|
|
||||||
?.filter(
|
|
||||||
(f) =>
|
|
||||||
f.toLocaleLowerCase().search(nameSearch.toLocaleLowerCase()) !==
|
|
||||||
-1
|
|
||||||
)
|
|
||||||
.map((file) => (
|
|
||||||
<div>
|
|
||||||
<Link to={"/logs/" + file}>{file}</Link>
|
|
||||||
<br></br>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="pagination">
|
||||||
} else {
|
<button
|
||||||
|
className="pageBtn"
|
||||||
|
disabled={!meta?.previous_page_url}
|
||||||
|
onClick={() => getListFile(page - 1)}
|
||||||
|
>
|
||||||
|
← Prev
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span className="pageInfo">
|
||||||
|
Page {meta?.current_page} / {meta?.last_page}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="pageBtn"
|
||||||
|
disabled={!meta?.next_page_url}
|
||||||
|
onClick={() => getListFile(page + 1)}
|
||||||
|
>
|
||||||
|
Next →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ================= Content ================= */}
|
||||||
|
<div className="content">
|
||||||
|
<div className="inputContainerRun">
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
placeholder="Search value inside logs..."
|
||||||
|
value={valueSearch}
|
||||||
|
onChange={(e) => setValueSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button className={`btn success ${!valueSearch ? "isDisabled" : ""}`} onClick={findValueInLog}>
|
||||||
|
Run
|
||||||
|
</button>
|
||||||
|
<button className={`btn ${!value || loadingSearchValue ? "isDisabled" : ""}`} onClick={() => {
|
||||||
|
setValueSearch("")
|
||||||
|
setValue("")
|
||||||
|
}}>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* ===== Search result ===== */}
|
||||||
|
{loadingSearchValue && <div className="terminal"><div className="loading">Searching...</div></div>}
|
||||||
|
{!loadingSearchValue && value && (
|
||||||
|
<textarea className="terminal" value={value} readOnly />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ===== Loading ===== */}
|
||||||
|
{loading && <div className="loading">Loading file...</div>}
|
||||||
|
|
||||||
|
{/* ===== DETECTED PANEL ===== */}
|
||||||
|
{!loading && log && (
|
||||||
|
<div className="detectedPanel">
|
||||||
|
{/* No errors */}
|
||||||
|
{!log?.modelSpecial && !log?.issueItem && (
|
||||||
|
<div className="noIssue">✅ No errors were found</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Extra items */}
|
||||||
|
{log?.modelSpecial && (
|
||||||
|
<>
|
||||||
|
<div className="detectedTitle info">Extra items</div>
|
||||||
|
{log.modelSpecial.split("\n").reverse().map((line, i) => {
|
||||||
|
const parts = line.split("|-|");
|
||||||
return (
|
return (
|
||||||
<div>
|
<div key={i} className="detectedItem">
|
||||||
<h1>
|
<span className="lineNumberSmall">{parts[0]}</span>
|
||||||
<i>No files</i>
|
<span>{parts[1]}</span>
|
||||||
</h1>
|
<span className="itemDetectedModel"> <a className="jumpLink" href={`#line-${parts[0]}`} style={{ textDecoration: "none", color: "black" }}>{parts[2]}</a></span>
|
||||||
|
<span>{parts[3]}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Issue found */}
|
||||||
|
{log?.issueItem && (
|
||||||
|
<>
|
||||||
|
<div className="detectedTitle danger">Issue found</div>
|
||||||
|
{log.issueItem.split("\n").reverse().map((line, i) => {
|
||||||
|
const parts = line.split("|-|");
|
||||||
|
return (
|
||||||
|
<div key={i} className="detectedItem dangerItem">
|
||||||
|
<span className="lineNumberSmall">{parts[0]}</span>
|
||||||
|
<span>{parts[1]}</span>
|
||||||
|
<span className="itemDetected"> <a className="jumpLink" href={`#line-${parts[0]}`} style={{ textDecoration: "none", color: "black" }}>{parts[2]}</a></span>
|
||||||
|
<span>{parts[3]}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ===== Log Content ===== */}
|
||||||
|
{!loading && log?.contentFile && (
|
||||||
|
<div className="logViewer">
|
||||||
|
{log.contentFile.split("\n").map((line, i) => {
|
||||||
|
const parts = line.split("|-|");
|
||||||
|
return (
|
||||||
|
<div id={`line-${parts[0]}`} key={i} className="logLine">
|
||||||
|
<span className="lineNumber">{parts[0]}</span>
|
||||||
|
<span>{parts[1]}</span>
|
||||||
|
<span className="itemDetected"> {parts[2]}</span>
|
||||||
|
<span>{parts[3]}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ListLog;
|
export default ListLog;
|
||||||
Loading…
Reference in New Issue