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 {
|
||||
const files = await LogDetectFile.all();
|
||||
const fileNames = files.map((file) => file.file_name);
|
||||
response.status(200).send(fileNames);
|
||||
const page = request.input('page', 1); // mặc định page 1
|
||||
const limit = request.input('limit', 100); // mặc định 50 record / page
|
||||
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 {
|
||||
response.status(203).send("NO FILE");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,3 +44,333 @@
|
|||
.inputSearch input{
|
||||
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 React, { useEffect, useState } from "react";
|
||||
import { Link, Navigate, useParams } from "react-router-dom";
|
||||
import { findValue, getListLog } from "../../api/apiLog";
|
||||
import { getListLog, getLog, findValue } from "../../api/apiLog";
|
||||
import "./ListLog.css";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const ListLog = () => {
|
||||
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 [valueSearch, setValueSearch] = useState("");
|
||||
const [value, setValue] = useState("");
|
||||
const getListFile = async () => {
|
||||
|
||||
/* ========================= */
|
||||
/* Load file list */
|
||||
/* ========================= */
|
||||
const getListFile = async (pageNumber = 1) => {
|
||||
try {
|
||||
const res = await axios.get(getListLog);
|
||||
setListFile(res.data);
|
||||
setStatus(res.status);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setLoadingListFile(true)
|
||||
const res = await axios.get(
|
||||
`${getListLog}?page=${pageNumber}&limit=${limit}&search=${nameSearch}`
|
||||
);
|
||||
|
||||
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 () => {
|
||||
try {
|
||||
setLoadingSearchValue(true)
|
||||
const res = await axios.post(findValue, { value: valueSearch });
|
||||
|
||||
setValue(res.data)
|
||||
setValue(res.data);
|
||||
setLoadingSearchValue(false)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
setValue("Searching value is failed");
|
||||
setLoadingSearchValue(false)
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getListFile();
|
||||
getListFile(page);
|
||||
}, []);
|
||||
|
||||
if (status === 200) {
|
||||
return (
|
||||
<div className="mainList">
|
||||
<div className="inputSearch">
|
||||
<Link to={"/"}>
|
||||
<button
|
||||
style={{
|
||||
color: "white",
|
||||
backgroundColor: "blue",
|
||||
cursor: "pointer",
|
||||
float: "left",
|
||||
}}
|
||||
>
|
||||
Home
|
||||
</button>
|
||||
<div className="layout">
|
||||
{/* ================= Sidebar ================= */}
|
||||
<div className="sidebar">
|
||||
<div className="topBar">
|
||||
<div >
|
||||
<Link to="/">
|
||||
<button className="btn">Home</button>
|
||||
</Link>
|
||||
<div>
|
||||
<label>Search file name: </label>
|
||||
</div>
|
||||
<h2>📁 Log Files</h2>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="inputContainer">
|
||||
<input
|
||||
className="input"
|
||||
placeholder="Search file..."
|
||||
value={nameSearch}
|
||||
placeholder={"Enter a file name"}
|
||||
onChange={(e) => {
|
||||
setNameSearch(e.target.value);
|
||||
}}
|
||||
></input>
|
||||
onChange={(e) => setNameSearch(e.target.value)}
|
||||
/>
|
||||
<button className="btn success" onClick={() => getListFile(page)}>
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Search value: </label>
|
||||
<input
|
||||
value={valueSearch}
|
||||
placeholder={"Enter value"}
|
||||
onChange={(e) => {
|
||||
setValueSearch(e.target.value);
|
||||
}}
|
||||
></input>
|
||||
<button style={{backgroundColor:"#6cff00", cursor:"pointer"}} onClick={()=>findValueInLog()}>Run</button>
|
||||
</div>
|
||||
<textarea
|
||||
style={{ height: "90vh", width: "50%", backgroundColor: "black", float:"right", color:"#6cff00", padding:10, display:value!==""?"block":"none" }}
|
||||
value={value}
|
||||
<div className="fileList">
|
||||
{/* ===== Loading ===== */}
|
||||
{loadingListFile && <div className="loading">Loading list file...</div>}
|
||||
|
||||
{!loadingListFile && listFile?.map((file, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`fileItem ${selectedFile === file ? "activeFile" : ""
|
||||
}`}
|
||||
onClick={() => loadFileContent(file)}
|
||||
>
|
||||
|
||||
</textarea>
|
||||
</div>
|
||||
{listFile
|
||||
?.filter(
|
||||
(f) =>
|
||||
f.toLocaleLowerCase().search(nameSearch.toLocaleLowerCase()) !==
|
||||
-1
|
||||
)
|
||||
.map((file) => (
|
||||
<div>
|
||||
<Link to={"/logs/" + file}>{file}</Link>
|
||||
<br></br>
|
||||
{file}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
<div className="pagination">
|
||||
<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 (
|
||||
<div>
|
||||
<h1>
|
||||
<i>No files</i>
|
||||
</h1>
|
||||
<div key={i} className="detectedItem">
|
||||
<span className="lineNumberSmall">{parts[0]}</span>
|
||||
<span>{parts[1]}</span>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ListLog;
|
||||
Loading…
Reference in New Issue