diff --git a/app/Controllers/Http/LogsController.ts b/app/Controllers/Http/LogsController.ts index e661bdc..706bfdd 100644 --- a/app/Controllers/Http/LogsController.ts +++ b/app/Controllers/Http/LogsController.ts @@ -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"); } diff --git a/manage-view/src/components/ListLog/ListLog.css b/manage-view/src/components/ListLog/ListLog.css index f9071f2..77d5738 100644 --- a/manage-view/src/components/ListLog/ListLog.css +++ b/manage-view/src/components/ListLog/ListLog.css @@ -43,4 +43,334 @@ .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; } \ No newline at end of file diff --git a/manage-view/src/components/ListLog/ListLog.jsx b/manage-view/src/components/ListLog/ListLog.jsx index 6720695..4fff0c1 100644 --- a/manage-view/src/components/ListLog/ListLog.jsx +++ b/manage-view/src/components/ListLog/ListLog.jsx @@ -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); } }; - const findValueInLog = async () =>{ - try { - const res = await axios.post(findValue,{value: valueSearch}); + /* ========================= */ + /* Load content when select */ + /* ========================= */ + const loadFileContent = async (fileName) => { + setSelectedFile(fileName); + setLoading(true); + setLog(null); - setValue(res.data) + 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); + setLoadingSearchValue(false) } catch (error) { console.log(error) + setValue("Searching value is failed"); + setLoadingSearchValue(false) } - } + + }; + useEffect(() => { - getListFile(); + getListFile(page); }, []); - if (status === 200) { - return ( -