Update UI page List log

This commit is contained in:
Joseph Le 2026-02-26 20:22:36 +11:00
parent 3d41d03f79
commit 9e0543db89
3 changed files with 560 additions and 82 deletions

View File

@ -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");
} }

View 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;
}

View File

@ -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;