update physical test
This commit is contained in:
parent
ab7db61608
commit
2f484e19b6
|
|
@ -12,6 +12,7 @@ import {
|
|||
LogStreamBuffer,
|
||||
mapErrorsToRows,
|
||||
mapToLineFormat,
|
||||
normalizeInterface,
|
||||
sendMessageToMail,
|
||||
sleep,
|
||||
TestSession,
|
||||
|
|
@ -25,6 +26,7 @@ import Line from '#models/line'
|
|||
import { ErrorRow, TestResult } from '../ultils/types.js'
|
||||
import moment from 'moment'
|
||||
import momentTZ from 'moment-timezone'
|
||||
import { PhysicalPortTest } from './physical_test_service.js'
|
||||
|
||||
type Inventory = {
|
||||
pid: string
|
||||
|
|
@ -66,7 +68,9 @@ interface LineConfig {
|
|||
output: string
|
||||
textfsm: string
|
||||
}[]
|
||||
commands: string[]
|
||||
ports: string[]
|
||||
runningScenario: string
|
||||
runningPhysical: boolean
|
||||
// history: string
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +115,6 @@ export default class LineConnection {
|
|||
public config: LineConfig
|
||||
public readonly socketIO: any
|
||||
private outputBuffer: string
|
||||
private isRunningScript: boolean
|
||||
private connecting: boolean
|
||||
private waitingScenario: boolean
|
||||
private outputInventory: string
|
||||
|
|
@ -121,13 +124,14 @@ export default class LineConnection {
|
|||
private listScenarios: number[]
|
||||
public handleClearLine: () => void
|
||||
private session: TestSession
|
||||
private physicalTest: PhysicalPortTest
|
||||
private outputPhysicalTest: string
|
||||
|
||||
constructor(config: LineConfig, socketIO: any, handleClearLine: () => void) {
|
||||
this.config = config
|
||||
this.socketIO = socketIO
|
||||
this.client = new net.Socket()
|
||||
this.outputBuffer = ''
|
||||
this.isRunningScript = false
|
||||
this.connecting = false
|
||||
this.waitingScenario = false
|
||||
this.outputInventory = ''
|
||||
|
|
@ -147,6 +151,8 @@ export default class LineConnection {
|
|||
this.listScenarios = []
|
||||
this.session = new TestSession()
|
||||
this.handleClearLine = handleClearLine
|
||||
this.physicalTest = new PhysicalPortTest([])
|
||||
this.outputPhysicalTest = ''
|
||||
}
|
||||
|
||||
connect(timeoutMs = 5000) {
|
||||
|
|
@ -182,13 +188,23 @@ export default class LineConnection {
|
|||
const lines = this.bufferLog.push(data)
|
||||
lines.forEach(this.handleLogLine)
|
||||
let rawData = ''
|
||||
if (this.isRunningScript) {
|
||||
if (this.config.runningScenario) {
|
||||
this.waitingScenario = true
|
||||
this.outputBuffer += message
|
||||
this.outputScenario += message
|
||||
if (!this.config.inventory)
|
||||
this.outputInventory = this.outputInventory.slice(-3000) + message
|
||||
}
|
||||
if (this.config.runningPhysical) {
|
||||
this.outputPhysicalTest += message
|
||||
const ports = this.physicalTest.handleLog(message)
|
||||
if (ports?.length)
|
||||
this.socketIO.emit('test_port_physical', {
|
||||
stationId,
|
||||
lineId: id,
|
||||
data: ports,
|
||||
})
|
||||
}
|
||||
if (data.toString().includes('More') || data.toString().includes('MORE'))
|
||||
this.writeCommand(' ')
|
||||
|
||||
|
|
@ -209,7 +225,7 @@ export default class LineConnection {
|
|||
stationId,
|
||||
lineId: id,
|
||||
data: message,
|
||||
commands: this.config.commands,
|
||||
ports: this.config.ports,
|
||||
})
|
||||
if (!this.config.inventory) {
|
||||
setTimeout(() => {
|
||||
|
|
@ -235,6 +251,7 @@ export default class LineConnection {
|
|||
lineId: id,
|
||||
error: '\r\n' + err.message + '\r\n',
|
||||
})
|
||||
this.endTesting()
|
||||
resolve()
|
||||
})
|
||||
|
||||
|
|
@ -256,6 +273,7 @@ export default class LineConnection {
|
|||
// } else {
|
||||
// this.retryConnect = 0
|
||||
// }
|
||||
this.endTesting()
|
||||
})
|
||||
|
||||
this.client.on('timeout', () => {
|
||||
|
|
@ -324,7 +342,7 @@ export default class LineConnection {
|
|||
async runScript(script: Scenario, userName: string) {
|
||||
if (!this.client || this.client.destroyed) {
|
||||
console.log('Not connected')
|
||||
this.isRunningScript = false
|
||||
this.config.runningScenario = ''
|
||||
this.socketIO.emit('running_scenario', {
|
||||
stationId: this.config.stationId,
|
||||
lineId: this.config.id,
|
||||
|
|
@ -333,7 +351,7 @@ export default class LineConnection {
|
|||
this.outputBuffer = ''
|
||||
return
|
||||
}
|
||||
if (this.isRunningScript) {
|
||||
if (this.config.runningScenario || this.config.runningPhysical) {
|
||||
console.log('Script already running')
|
||||
return
|
||||
}
|
||||
|
|
@ -341,7 +359,7 @@ export default class LineConnection {
|
|||
console.log(
|
||||
`Run scenario "${script?.title}" to line ${this.config.lineNumber} of ${this.config.stationName}`
|
||||
)
|
||||
this.isRunningScript = true
|
||||
this.config.runningScenario = ''
|
||||
this.socketIO.emit('running_scenario', {
|
||||
stationId: this.config.stationId,
|
||||
lineId: this.config.id,
|
||||
|
|
@ -378,7 +396,7 @@ export default class LineConnection {
|
|||
return new Promise((resolve, reject) => {
|
||||
const timeoutTimer = setTimeout(
|
||||
() => {
|
||||
this.isRunningScript = false
|
||||
this.config.runningScenario = ''
|
||||
this.socketIO.emit('running_scenario', {
|
||||
stationId: this.config.stationId,
|
||||
lineId: this.config.id,
|
||||
|
|
@ -425,7 +443,7 @@ export default class LineConnection {
|
|||
}, 5000)
|
||||
return
|
||||
} else clearTimeout(timeoutTimer)
|
||||
this.isRunningScript = false
|
||||
this.config.runningScenario = ''
|
||||
this.socketIO.emit('running_scenario', {
|
||||
stationId: this.config.stationId,
|
||||
lineId: this.config.id,
|
||||
|
|
@ -936,4 +954,80 @@ export default class LineConnection {
|
|||
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)
|
||||
}
|
||||
|
||||
async runPhysicalTest() {
|
||||
if (this.config.runningPhysical) {
|
||||
console.log('Running physical test')
|
||||
return
|
||||
}
|
||||
this.config.runningPhysical = true
|
||||
this.config.runningScenario = 'Physical Test'
|
||||
this.socketIO.emit('running_scenario', {
|
||||
stationId: this.config.stationId,
|
||||
lineId: this.config.id,
|
||||
title: 'Physical Test',
|
||||
physical: true,
|
||||
})
|
||||
const listPorts = await this.getPorts()
|
||||
this.socketIO.emit('running_scenario', {
|
||||
stationId: this.config.stationId,
|
||||
lineId: this.config.id,
|
||||
title: 'Physical Test',
|
||||
physical: true,
|
||||
ports: listPorts,
|
||||
})
|
||||
if (listPorts.length === 0) {
|
||||
console.log('End physical test')
|
||||
this.endTesting()
|
||||
return
|
||||
}
|
||||
|
||||
this.physicalTest.start(listPorts)
|
||||
const interval = setInterval(async () => {
|
||||
if (!this.physicalTest.done) {
|
||||
const result = this.physicalTest.getResult()
|
||||
// console.warn('⚠️ Missing ports:', result.missingPorts)
|
||||
} else {
|
||||
clearInterval(interval)
|
||||
this.endTesting()
|
||||
}
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
endTesting() {
|
||||
this.physicalTest.done = true
|
||||
this.config.runningPhysical = false
|
||||
this.config.runningScenario = ''
|
||||
this.outputBuffer = ''
|
||||
this.outputScenario = ''
|
||||
this.outputPhysicalTest = ''
|
||||
this.config.ports = []
|
||||
this.socketIO.emit('running_scenario', {
|
||||
stationId: this.config.stationId,
|
||||
lineId: this.config.id,
|
||||
title: '',
|
||||
})
|
||||
}
|
||||
|
||||
async getPorts(): Promise<string[]> {
|
||||
this.writeCommand(' show power inline\r\n')
|
||||
this.writeCommand(' \r\n')
|
||||
await this.sleep(3000)
|
||||
const statusOutput = this.outputPhysicalTest
|
||||
this.outputPhysicalTest = ''
|
||||
|
||||
const lines = statusOutput.split('\n')
|
||||
const ports = []
|
||||
for (const line of lines) {
|
||||
// Match: "Gi0/1 is up, line protocol is up"
|
||||
const match = line.match(/^(\S+)\s+\S+\s+(on|off)/i)
|
||||
|
||||
if (match) {
|
||||
const name = match[1]
|
||||
ports.push(normalizeInterface(name))
|
||||
}
|
||||
}
|
||||
this.config.ports = [...new Set(ports)]
|
||||
return [...new Set(ports)]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
import { normalizeInterface } from '../ultils/helper.js'
|
||||
import { PhysicalTestResult, PortState } from '../ultils/types.js'
|
||||
const LINK_UPDOWN_REGEX =
|
||||
/Interface\s+((?:FastEthernet|GigabitEthernet|TenGigabitEthernet|TwentyFiveGigE|FortyGigabitEthernet|HundredGigE|Ethernet|Port-channel|Fa|Gi|Te|Hu|Eth)[\w\/.-]+),\s+changed state to\s+(up|down)/i
|
||||
|
||||
export class PhysicalPortTest {
|
||||
public ports = new Map<string, PortState>()
|
||||
private expectedPorts: string[]
|
||||
public done = false
|
||||
|
||||
constructor(expectedPorts: string[]) {
|
||||
this.expectedPorts = expectedPorts
|
||||
|
||||
expectedPorts.forEach((p) => {
|
||||
this.ports.set(normalizeInterface(p), {
|
||||
name: normalizeInterface(p),
|
||||
tested: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
start(expectedPorts: string[]) {
|
||||
this.ports.clear()
|
||||
this.expectedPorts = expectedPorts
|
||||
this.done = false
|
||||
expectedPorts.forEach((p) => {
|
||||
this.ports.set(normalizeInterface(p), {
|
||||
name: normalizeInterface(p),
|
||||
tested: false,
|
||||
})
|
||||
})
|
||||
// this.connection.writeCommand('terminal length 0')
|
||||
// this.connection.writeCommand('terminal monitor')
|
||||
// this.connection.onLog((line) => {
|
||||
// this.handleLog(line);
|
||||
// });
|
||||
}
|
||||
|
||||
handleLog(line: string) {
|
||||
const match = line.match(LINK_UPDOWN_REGEX)
|
||||
if (!match) return
|
||||
|
||||
const rawIface = match[1]
|
||||
const state = match[2] as 'up' | 'down'
|
||||
const iface = normalizeInterface(rawIface)
|
||||
|
||||
const port = this.ports.get(iface)
|
||||
if (!port) return
|
||||
|
||||
// tránh update trùng state liên tiếp
|
||||
if (port.lastState === state) return
|
||||
|
||||
port.lastState = state
|
||||
port.lastSeen = new Date()
|
||||
|
||||
// chỉ cần UP 1 lần là pass
|
||||
if (state === 'up' && !port.tested) {
|
||||
port.tested = true
|
||||
this.checkDone()
|
||||
}
|
||||
|
||||
return this.getTestedPorts()
|
||||
}
|
||||
|
||||
getTestedPorts(): string[] {
|
||||
return Array.from(this.ports.values())
|
||||
.filter((p) => p.tested)
|
||||
.map((p) => p.name)
|
||||
.sort()
|
||||
}
|
||||
|
||||
private checkDone() {
|
||||
const testedCount = [...this.ports.values()].filter((p) => p.tested).length
|
||||
|
||||
if (testedCount === this.expectedPorts.length) {
|
||||
this.done = true
|
||||
this.onDone()
|
||||
}
|
||||
}
|
||||
|
||||
onDone() {
|
||||
this.ports.clear()
|
||||
console.log('✅ Physical Test DONE')
|
||||
}
|
||||
|
||||
getResult(): PhysicalTestResult {
|
||||
const tested = [...this.ports.values()].filter((p) => p.tested)
|
||||
const missing = [...this.ports.values()].filter((p) => !p.tested).map((p) => p.name)
|
||||
|
||||
return {
|
||||
expected: this.expectedPorts.length,
|
||||
tested: tested.length,
|
||||
missingPorts: missing,
|
||||
status: this.done ? 'DONE' : 'RUNNING',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -667,3 +667,12 @@ export async function updateNoteToERP(sn: string, note: string) {
|
|||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeInterface(name: string): string {
|
||||
return name
|
||||
.replace(/^Gi(?=\d)/, 'GigabitEthernet')
|
||||
.replace(/^Fa(?=\d)/, 'FastEthernet')
|
||||
.replace(/^Te(?=\d)/, 'TenGigabitEthernet')
|
||||
.replace(/^Hu(?=\d)/, 'HundredGigE')
|
||||
.replace(/^Eth(?=\d)/, 'Ethernet')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,3 +55,17 @@ export interface ErrorRow {
|
|||
log: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface PortState {
|
||||
name: string
|
||||
tested: boolean
|
||||
lastState?: 'up' | 'down'
|
||||
lastSeen?: Date
|
||||
}
|
||||
|
||||
export interface PhysicalTestResult {
|
||||
expected: number
|
||||
tested: number
|
||||
missingPorts: string[]
|
||||
status: 'RUNNING' | 'DONE' | 'WARNING'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,7 +133,14 @@ export class WebSocketIo {
|
|||
setTimeout(() => {
|
||||
io.to(socket.id).emit(
|
||||
'init',
|
||||
Array.from(this.lineMap.values()).map((el) => el?.config || {})
|
||||
Array.from(this.lineMap.values()).map((el) => {
|
||||
const config = el?.config || {}
|
||||
if (config.status !== 'connected') {
|
||||
config.runningScenario = ''
|
||||
config.runningPhysical = false
|
||||
}
|
||||
return config
|
||||
})
|
||||
)
|
||||
}, 500)
|
||||
|
||||
|
|
@ -606,6 +613,28 @@ export class WebSocketIo {
|
|||
{}
|
||||
)
|
||||
})
|
||||
|
||||
socket.on('run_physical_test', async (data) => {
|
||||
const { stationId, lineId } = data
|
||||
await this.handleLineOperation(
|
||||
io,
|
||||
stationId,
|
||||
[lineId],
|
||||
async (lineCon) => lineCon.runPhysicalTest(),
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
socket.on('end_run_physical_test', async (data) => {
|
||||
const { stationId, lineId } = data
|
||||
await this.handleLineOperation(
|
||||
io,
|
||||
stationId,
|
||||
[lineId],
|
||||
async (lineCon) => lineCon.endTesting(),
|
||||
{}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
socketServer.listen(SOCKET_IO_PORT, () => {
|
||||
|
|
@ -645,8 +674,10 @@ export class WebSocketIo {
|
|||
userEmailOpenCLI: '',
|
||||
userOpenCLI: '',
|
||||
data: [],
|
||||
commands: [],
|
||||
ports: [],
|
||||
inventory: inventory,
|
||||
runningPhysical: false,
|
||||
runningScenario: '',
|
||||
},
|
||||
socket,
|
||||
async () => {
|
||||
|
|
|
|||
|
|
@ -383,7 +383,11 @@ function App() {
|
|||
setTimeout(() => {
|
||||
updateValueLineStation(
|
||||
data?.lineId,
|
||||
{ runningScenario: data?.title || "" },
|
||||
{
|
||||
runningScenario: data?.title || "",
|
||||
runningPhysical: data?.physical || false,
|
||||
ports: data?.ports || [],
|
||||
},
|
||||
data?.stationId
|
||||
);
|
||||
}, 100);
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ const ModalTerminal = ({
|
|||
const [isDisable, setIsDisable] = useState<boolean>(false);
|
||||
const [isDisableTicket, setIsDisableTicket] = useState<boolean>(false);
|
||||
const [listPorts, setListPorts] = useState<SwitchPortsProps[]>([]);
|
||||
const [listPortsPhysical, setListPortsPhysical] = useState<string[]>([]);
|
||||
const [latestTicket, setLatestTicket] = useState<TDataTicket>(INIT_TICKET);
|
||||
const [dataTicket, setDataTicket] = useState<TDataTicket>(INIT_TICKET);
|
||||
const [valueBaud, setValueBaud] = useState<string>("");
|
||||
|
|
@ -130,8 +131,14 @@ const ModalTerminal = ({
|
|||
if (data?.ports && data?.ports.length > 0)
|
||||
setListPorts(data?.ports || []);
|
||||
});
|
||||
socket?.on("test_port_physical", (data) => {
|
||||
if (data.stationId !== stationItem?.id) return;
|
||||
if (data?.data && data?.data.length > 0)
|
||||
setListPortsPhysical(data?.data || []);
|
||||
});
|
||||
return () => {
|
||||
socket?.off("switch_ports_status");
|
||||
socket?.off("test_port_physical");
|
||||
};
|
||||
}, [socket, stationItem]);
|
||||
|
||||
|
|
@ -696,7 +703,7 @@ const ModalTerminal = ({
|
|||
copiedColor="violet"
|
||||
/>
|
||||
</Flex>
|
||||
<Flex mt="4px">
|
||||
<Flex mt="4px" display={line?.runningPhysical ? "none" : ""}>
|
||||
<Text size="md" mr={"6px"} fw={"bold"}>
|
||||
IOS:
|
||||
</Text>
|
||||
|
|
@ -711,7 +718,7 @@ const ModalTerminal = ({
|
|||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Flex>
|
||||
<Flex display={line?.runningPhysical ? "none" : ""}>
|
||||
<Text size="md" mr={"sm"} fw={"bold"}>
|
||||
License:
|
||||
</Text>
|
||||
|
|
@ -727,13 +734,13 @@ const ModalTerminal = ({
|
|||
: ""}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Flex display={line?.runningPhysical ? "none" : ""}>
|
||||
<Text size="md" mr={"sm"} fw={"bold"}>
|
||||
Sh env/module:
|
||||
</Text>
|
||||
<Text size="md">{""}</Text>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Flex display={line?.runningPhysical ? "none" : ""}>
|
||||
<Text size="md" mr={"sm"} fw={"bold"}>
|
||||
Mem/Flash:
|
||||
</Text>
|
||||
|
|
@ -746,7 +753,7 @@ const ModalTerminal = ({
|
|||
: ""}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Box>
|
||||
<Box display={line?.runningPhysical ? "none" : ""}>
|
||||
<Text size="md" mr={"sm"} fw={"bold"}>
|
||||
Warning from test report: AI
|
||||
</Text>
|
||||
|
|
@ -762,6 +769,65 @@ const ModalTerminal = ({
|
|||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box display={line?.runningPhysical ? "" : "none"}>
|
||||
<fieldset
|
||||
style={
|
||||
{
|
||||
// width: "300px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<legend>
|
||||
<Flex>
|
||||
<Text>
|
||||
List ports{" "}
|
||||
{line?.ports?.length
|
||||
? `(${listPortsPhysical.length}/${line?.ports?.length})`
|
||||
: ""}
|
||||
</Text>
|
||||
<Button
|
||||
fw={400}
|
||||
disabled={isDisable}
|
||||
variant="outline"
|
||||
color="green"
|
||||
miw={"80px"}
|
||||
h={"24px"}
|
||||
size="xs"
|
||||
fz={"xs"}
|
||||
ms={"8px"}
|
||||
onClick={() => {
|
||||
socket?.emit("end_run_physical_test", {
|
||||
lineId: line?.id,
|
||||
stationId: Number(stationItem?.id),
|
||||
});
|
||||
setIsDisable(true);
|
||||
setTimeout(() => {
|
||||
setIsDisable(false);
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
Done/End
|
||||
</Button>
|
||||
</Flex>
|
||||
</legend>
|
||||
<ScrollArea h={"300px"} p={"4px"}>
|
||||
<Flex wrap={"wrap"} gap={"xs"}>
|
||||
{line?.ports?.map((port, i) => (
|
||||
<Text
|
||||
key={i}
|
||||
c={
|
||||
listPortsPhysical?.includes(port)
|
||||
? "#19bc4f"
|
||||
: "#dedede"
|
||||
}
|
||||
>
|
||||
{port}
|
||||
</Text>
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollArea>
|
||||
</fieldset>
|
||||
</Box>
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
|
|
@ -902,10 +968,11 @@ const ModalTerminal = ({
|
|||
line_id={Number(line?.id)}
|
||||
line={line}
|
||||
station_id={Number(stationItem?.id)}
|
||||
isDisabled={
|
||||
typeof line?.userOpenCLI !== "undefined" &&
|
||||
line?.userOpenCLI !== user?.userName
|
||||
}
|
||||
// isDisabled={
|
||||
// typeof line?.userOpenCLI !== "undefined" &&
|
||||
// line?.userOpenCLI !== user?.userName
|
||||
// }
|
||||
isDisabled={line?.runningScenario ? true : false}
|
||||
line_status={line?.status || ""}
|
||||
loadingClearTerminal={line?.loadingClearTerminal}
|
||||
isClearKeepScrollBack={isClearKeepScrollBack}
|
||||
|
|
@ -1040,6 +1107,25 @@ const ModalTerminal = ({
|
|||
</Box>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
<Button
|
||||
disabled={isDisable || line?.runningPhysical}
|
||||
fw={400}
|
||||
variant="filled"
|
||||
color="green"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
socket?.emit("run_physical_test", {
|
||||
lineId: line?.id,
|
||||
stationId: Number(stationItem?.id),
|
||||
});
|
||||
setIsDisable(true);
|
||||
setTimeout(() => {
|
||||
setIsDisable(false);
|
||||
}, 5000);
|
||||
}}
|
||||
>
|
||||
Physical Test
|
||||
</Button>
|
||||
<Button
|
||||
disabled={true}
|
||||
fw={400}
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ export type TLine = {
|
|||
issue: string[];
|
||||
};
|
||||
};
|
||||
commands?: string[];
|
||||
ports?: string[];
|
||||
interface?: string;
|
||||
baud?: number;
|
||||
tickets?: TDataTicket[];
|
||||
|
|
@ -105,6 +105,7 @@ export type TLine = {
|
|||
runningScenario?: string;
|
||||
scenario?: IScenario;
|
||||
loadingClearTerminal?: boolean;
|
||||
runningPhysical?: boolean;
|
||||
};
|
||||
|
||||
export type TUser = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue