Enhance terminal clear options and update note handling

Added a 'Clear (scrollback)' option to the terminal modal, allowing users to clear only the scrollback buffer without emitting a clear event. Updated backend and helper logic to use 'testNotes' instead of 'notes' for serial number updates, and improved note formatting with timezone support. Also improved email content formatting and error reporting.
This commit is contained in:
nguyentrungthat 2025-12-24 14:27:52 +07:00
parent fd5d1628a5
commit 42f67e5390
5 changed files with 98 additions and 31 deletions

View File

@ -54,7 +54,7 @@ export default class HealCheckController {
serialNumberA: dataSN?.serialNumberA, serialNumberA: dataSN?.serialNumberA,
productModelId: dataSN?.productModelId, productModelId: dataSN?.productModelId,
orgId: dataSN?.orgId, orgId: dataSN?.orgId,
notes: dataSN?.notes, testNotes: dataSN?.testNotes,
}, },
}, },
{ {
@ -88,6 +88,11 @@ export default class HealCheckController {
status: false, status: false,
message: error?.message || 'Checking api wiki fail', message: error?.message || 'Checking api wiki fail',
}, },
{
name: 'update-note-sn',
status: false,
message: error?.message || 'Checking api update note SN fail',
},
], ],
} }
} }

View File

@ -24,6 +24,7 @@ import redis from '@adonisjs/redis/services/main'
import Line from '#models/line' import Line from '#models/line'
import { ErrorRow, TestResult } from '../ultils/types.js' import { ErrorRow, TestResult } from '../ultils/types.js'
import moment from 'moment' import moment from 'moment'
import momentTZ from 'moment-timezone'
type Inventory = { type Inventory = {
pid: string pid: string
@ -872,13 +873,14 @@ export default class LineConnection {
.map( .map(
(r) => ` (r) => `
<tr> <tr>
<td style="padding:6px;"> <td style="padding:6px; text-align:center;">
<span style="color:${r.level === 'FAIL' ? 'red' : 'orange'};">${r.level}</span> <span style="color:${r.level === 'FAIL' ? 'red' : 'orange'};">${r.level}</span>
</td> </td>
<td style="padding:6px;">${r.rule}</td> <td style="padding:6px; text-align:center;">${r.rule}</td>
<td style="padding:6px; text-align:center;">${r.message}</td> <td style="padding:6px; text-align:center;">${r.message}</td>
<td style="padding:6px;font-family:monospace;"> <td style="padding:6px; font-family:monospace;">
${escapeHtml(r.log)} <div style="white-space: break-spaces;"><span style="color: black;">
${escapeHtml(r.log.trim())}</span></div>
</td> </td>
</tr> </tr>
` `
@ -911,18 +913,15 @@ export default class LineConnection {
buildEmailContent(result: TestResult, value: any): string { buildEmailContent(result: TestResult, value: any): string {
const rows = mapErrorsToRows(result.errors) const rows = mapErrorsToRows(result.errors)
const table = this.renderErrorTable(rows) const table = this.renderErrorTable(rows)
const tableAI = this.renderAIDetectTable(value) // const tableAI = this.renderAIDetectTable(value)
return ` return `
<h3>Cisco Device Log Result</h3> <h3>Cisco Device Log Result</h3>
<p>Line: <b>${this.config.lineNumber}</b> - Station: <b>${this.config.stationName}</b></p> <p>Line: <b>${this.config.lineNumber}</b> - Station: <b>${this.config.stationName}</b></p>
<p><b>Summary:</b> ${result.summary}</p> <p><b>Summary:</b> ${result.summary}. ${value.summary}</p>
<hr /> <hr />
${table} ${table}
<br /> <br />
<hr />
<p>AI Detected:</p>
${tableAI}
` `
} }
@ -932,7 +931,9 @@ export default class LineConnection {
: data.license : data.license
? [data.license] ? [data.license]
: [] : []
const note = `\n\n-------ATC-${moment(new Date()).format('YYYY/MM/DD')}-------\nLicense: ${licenses.join(', ')}\nIssues:\n${data.issues?.length ? `- ` + data.issues.join(`\n- `) : ''}` const timeZone = process.env.TIME_ZONE || 'Australia/Sydney'
const dataFormat = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm')
const note = `-------[ATC]-[${dataFormat}]-------\nLicense: ${licenses.join(', ')}\nSummary: ${data?.summary || ''}\nIssues:\n${data.issues?.length ? `- ` + data.issues.join(`\n- `) : ''}\n\n`
await updateNoteToERP(sn, note) await updateNoteToERP(sn, note)
} }
} }

View File

@ -650,7 +650,7 @@ export async function updateNoteToERP(sn: string, note: string) {
serialNumberA: dataSN?.serialNumberA, serialNumberA: dataSN?.serialNumberA,
productModelId: dataSN?.productModelId, productModelId: dataSN?.productModelId,
orgId: dataSN?.orgId, orgId: dataSN?.orgId,
notes: (dataSN?.notes || '') + note, testNotes: note + (dataSN?.testNotes || ''),
} }
// console.log(payload) // console.log(payload)
await axios.post( await axios.post(

View File

@ -27,6 +27,7 @@ import TerminalCLI from "../TerminalXTerm";
import type { Socket } from "socket.io-client"; import type { Socket } from "socket.io-client";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { import {
IconCaretRight,
IconCircleCheckFilled, IconCircleCheckFilled,
IconCircleDot, IconCircleDot,
IconInfoCircle, IconInfoCircle,
@ -82,6 +83,8 @@ const ModalTerminal = ({
const [valueBaud, setValueBaud] = useState<string>(""); const [valueBaud, setValueBaud] = useState<string>("");
const [valueIssue, setValueIssue] = useState<string>(""); const [valueIssue, setValueIssue] = useState<string>("");
const [dataTextfsm, setDataTextfsm] = useState<TextFSM[]>([]); const [dataTextfsm, setDataTextfsm] = useState<TextFSM[]>([]);
const [isClearKeepScrollBack, setIsClearKeepScrollBack] =
useState<boolean>(false);
useEffect(() => { useEffect(() => {
if (opened && line?.tickets && line?.tickets?.length > 0) { if (opened && line?.tickets && line?.tickets?.length > 0) {
@ -905,27 +908,65 @@ const ModalTerminal = ({
} }
line_status={line?.status || ""} line_status={line?.status || ""}
loadingClearTerminal={line?.loadingClearTerminal} loadingClearTerminal={line?.loadingClearTerminal}
isClearKeepScrollBack={isClearKeepScrollBack}
/> />
<Flex justify={"space-around"} mt={"md"} pt={"md"} pb={"md"}> <Flex justify={"space-around"} mt={"md"} pt={"md"} pb={"md"}>
<Button <Menu trigger="hover" withArrow shadow="md" position="right">
fw={400} <Menu.Target>
disabled={isDisable} <Button
variant="filled" fw={400}
color="red" disabled={isDisable}
size="xs" variant="filled"
onClick={() => { color="red"
socket?.emit("clear_terminal", { size="xs"
lineId: line?.id, // style={{ height: "30px", width: "100px" }}
stationId: stationItem?.id, onClick={() => {}}
}); >
setIsDisable(true); Clear Terminal <IconCaretRight size={"14px"} />
setTimeout(() => { </Button>
setIsDisable(false); </Menu.Target>
}, 1000); <Menu.Dropdown>
}} <Flex direction={"column"} gap={"8px"}>
> <Button
Clear Terminal fw={400}
</Button> disabled={isDisable}
variant="outline"
color="yellow"
size="xs"
onClick={() => {
setIsDisable(true);
setIsClearKeepScrollBack(true);
setTimeout(() => {
setIsDisable(false);
setIsClearKeepScrollBack(false);
}, 1000);
}}
>
Clear (scrollback)
</Button>
<Button
fw={400}
disabled={isDisable}
variant="outline"
color="red"
size="xs"
onClick={() => {
socket?.emit("clear_terminal", {
lineId: line?.id,
stationId: stationItem?.id,
});
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 1000);
}}
>
Clear all
</Button>
</Flex>
</Menu.Dropdown>
</Menu>
<ButtonDPELP <ButtonDPELP
socket={socket} socket={socket}
selectedLines={line ? [line] : []} selectedLines={line ? [line] : []}

View File

@ -32,6 +32,7 @@ interface TerminalCLIProps {
focusTerminal?: boolean; focusTerminal?: boolean;
isSelected?: boolean; isSelected?: boolean;
loadingClearTerminal?: boolean; loadingClearTerminal?: boolean;
isClearKeepScrollBack?: boolean;
} }
const TerminalCLI: React.FC<TerminalCLIProps> = ({ const TerminalCLI: React.FC<TerminalCLIProps> = ({
@ -52,6 +53,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
focusTerminal, focusTerminal,
isSelected, isSelected,
loadingClearTerminal, loadingClearTerminal,
isClearKeepScrollBack,
}) => { }) => {
const xtermRef = useRef<HTMLDivElement>(null); const xtermRef = useRef<HTMLDivElement>(null);
const terminal = useRef<Terminal>(null); const terminal = useRef<Terminal>(null);
@ -75,6 +77,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
}, },
rows: 24, rows: 24,
cols: 80, cols: 80,
scrollback: 3000,
}); });
const fitAddon = new FitAddon(); const fitAddon = new FitAddon();
fitRef.current = fitAddon; fitRef.current = fitAddon;
@ -227,6 +230,23 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
} }
}, [loadingClearTerminal]); }, [loadingClearTerminal]);
useEffect(() => {
if (isClearKeepScrollBack && terminal.current) {
// terminal.current?.clear();
terminal.current?.write(Array(70).fill("\r\n").join(""));
socket?.emit("write_command_line_from_web", {
lineIds: [line_id],
stationId: station_id,
command: "\r\n",
});
// setTimeout(() => {
// terminal.current?.scrollToLine(-100);
// terminal.current?.clear();
// }, 100);
if (!miniSize && !isDisabled) terminal.current?.focus();
}
}, [isClearKeepScrollBack]);
return ( return (
<> <>
<div <div