Update
This commit is contained in:
parent
fb1554d857
commit
1682a28029
|
|
@ -5,39 +5,39 @@ export default class AuthController {
|
|||
// Đăng ký
|
||||
async register({ request, response }: HttpContext) {
|
||||
try {
|
||||
const data = request.only(['email', 'password', 'full_name'])
|
||||
const data = request.only(['email', 'password', 'user_name'])
|
||||
|
||||
const user = await User.query().where('email', data.email).first()
|
||||
const user = await User.query().where('user_name', data.user_name).first()
|
||||
|
||||
if (user) {
|
||||
return response.status(401).json({ status: false, message: 'Email is exist' })
|
||||
return response.status(401).json({ status: false, message: 'Username is exist' })
|
||||
}
|
||||
|
||||
const newUser = await User.create(data)
|
||||
return response.json({ status: true, message: 'User created', user: newUser })
|
||||
} catch (error) {
|
||||
return response.status(401).json({ status: false, message: 'Invalid credentials' })
|
||||
return response.status(401).json({ error, status: false, message: 'Invalid credentials' })
|
||||
}
|
||||
}
|
||||
|
||||
// Đăng nhập
|
||||
async login({ request, auth, response }: HttpContext) {
|
||||
const { email, password } = request.only(['email', 'password'])
|
||||
const user = await User.query().where('email', email).first()
|
||||
const { user_name: userName, password } = request.only(['user_name', 'password'])
|
||||
const user = await User.query().where('user_name', userName).first()
|
||||
|
||||
if (!user) {
|
||||
return response.status(401).json({ message: 'Invalid email or password' })
|
||||
return response.status(401).json({ message: 'Invalid Username or password' })
|
||||
}
|
||||
|
||||
try {
|
||||
// So sánh password
|
||||
if (user.password !== password) {
|
||||
return response.status(401).json({ message: 'Invalid email or password' })
|
||||
return response.status(401).json({ message: 'Invalid username or password' })
|
||||
}
|
||||
|
||||
return response.json({
|
||||
message: 'Login successful',
|
||||
user: { id: user.id, email: user.email, fullName: user.fullName },
|
||||
user: { id: user.id, email: user.email, userName: user.userName },
|
||||
})
|
||||
} catch {
|
||||
return response.status(401).json({ message: 'Invalid credentials' })
|
||||
|
|
|
|||
|
|
@ -71,13 +71,6 @@ export default class StationsController {
|
|||
let lines: Line[] = request.body().lines || []
|
||||
|
||||
try {
|
||||
// Kiểm tra trùng name hoặc ip
|
||||
if (await Station.findBy('name', payload.name))
|
||||
return response.status(400).json({ message: 'Station name exist!' })
|
||||
|
||||
if (await Station.findBy('ip', payload.ip))
|
||||
return response.status(400).json({ message: 'Ip of station is exist!' })
|
||||
|
||||
const station = await Station.find(request.body().id)
|
||||
|
||||
// If the station does not exist, return a 404 response
|
||||
|
|
|
|||
|
|
@ -25,19 +25,19 @@ export default class UsersController {
|
|||
|
||||
async store({ request, response }: HttpContext) {
|
||||
try {
|
||||
const data = request.only(['full_name', 'email', 'password'])
|
||||
const data = request.only(['user_name', 'email', 'password'])
|
||||
|
||||
// Check if email already exists
|
||||
const existingUser = await User.findBy('email', data.email)
|
||||
const existingUser = await User.findBy('user_name', data.user_name)
|
||||
if (existingUser) {
|
||||
return response.conflict({
|
||||
status: false,
|
||||
message: 'Email already exists',
|
||||
message: 'Username already exists',
|
||||
})
|
||||
}
|
||||
|
||||
const user = await User.create({
|
||||
fullName: data.full_name,
|
||||
userName: data.user_name,
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
})
|
||||
|
|
@ -55,16 +55,16 @@ export default class UsersController {
|
|||
async update({ params, request, response }: HttpContext) {
|
||||
try {
|
||||
const user = await User.findOrFail(params.id)
|
||||
const data = request.only(['full_name', 'email', 'password']) // Include password
|
||||
const data = request.only(['user_name', 'email', 'password']) // Include password
|
||||
|
||||
// Check if email already exists for another user
|
||||
if (data.email)
|
||||
if (data.email !== user.email) {
|
||||
const existingUser = await User.findBy('email', data.email)
|
||||
const existingUser = await User.findBy('user_name', data.user_name)
|
||||
if (existingUser) {
|
||||
return response.conflict({
|
||||
status: false,
|
||||
message: 'Email already exists',
|
||||
message: 'Username already exists',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export default class User extends BaseModel {
|
|||
declare id: number
|
||||
|
||||
@column()
|
||||
declare fullName: string | null
|
||||
declare userName: string
|
||||
|
||||
@column()
|
||||
declare email: string
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ interface LineConfig {
|
|||
openCLI: boolean
|
||||
userEmailOpenCLI: string
|
||||
userOpenCLI: string
|
||||
inventory?: string
|
||||
latestScenario?: {
|
||||
name: string
|
||||
time: number
|
||||
}
|
||||
data: {
|
||||
command: string
|
||||
output: string
|
||||
|
|
@ -184,6 +189,10 @@ export default class LineConnection {
|
|||
this.config.lineNumber,
|
||||
this.config.port
|
||||
)
|
||||
this.config.latestScenario = {
|
||||
name: script?.title,
|
||||
time: now,
|
||||
}
|
||||
const steps = typeof script?.body === 'string' ? JSON.parse(script?.body) : []
|
||||
let stepIndex = 0
|
||||
|
||||
|
|
@ -228,10 +237,17 @@ export default class LineConnection {
|
|||
if (err) return
|
||||
|
||||
const logScenarios = getLogWithTimeScenario(content, now) || ''
|
||||
const data = await textfsmResults(logScenarios, '')
|
||||
const data = textfsmResults(logScenarios, '')
|
||||
try {
|
||||
data.forEach((item) => {
|
||||
if (item?.textfsm && isValidJson(item?.textfsm)) {
|
||||
if (
|
||||
['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(
|
||||
item.command
|
||||
)
|
||||
) {
|
||||
this.config.inventory = JSON.parse(item.textfsm)[0]
|
||||
}
|
||||
item.textfsm = JSON.parse(item.textfsm)
|
||||
}
|
||||
})
|
||||
|
|
@ -240,6 +256,8 @@ export default class LineConnection {
|
|||
stationId: this.config.stationId,
|
||||
lineId: this.config.id,
|
||||
data,
|
||||
inventory: this.config.inventory || null,
|
||||
latestScenario: this.config.latestScenario || null,
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||
|
||||
export default class extends BaseSchema {
|
||||
protected tableName = 'users'
|
||||
|
||||
public async up() {
|
||||
this.schema.alterTable(this.tableName, (table) => {
|
||||
table.renameColumn('full_name', 'user_name')
|
||||
})
|
||||
this.schema.alterTable(this.tableName, (table) => {
|
||||
table.string('user_name').notNullable().unique().alter()
|
||||
table.dropUnique(['email'])
|
||||
})
|
||||
}
|
||||
|
||||
public async down() {
|
||||
this.schema.alterTable(this.tableName, (table) => {
|
||||
table.unique(['email'])
|
||||
table.string('user_name').nullable().alter()
|
||||
table.renameColumn('user_name', 'full_name')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -78,15 +78,19 @@ export class WebSocketIo {
|
|||
this.userConnecting.set(userId, { userId, userName })
|
||||
|
||||
setTimeout(() => {
|
||||
io.emit('user_connecting', Array.from(this.userConnecting.values()))
|
||||
}, 200)
|
||||
const listUser = Array.from(this.userConnecting.values())
|
||||
if (!listUser.find((el) => el.userId === userId)) {
|
||||
listUser.push({ userId, userName })
|
||||
}
|
||||
io.emit('user_connecting', listUser)
|
||||
}, 500)
|
||||
|
||||
setTimeout(() => {
|
||||
io.to(socket.id).emit(
|
||||
'init',
|
||||
Array.from(this.lineMap.values()).map((el) => el.config)
|
||||
)
|
||||
}, 200)
|
||||
}, 500)
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log(`FE disconnected: ${socket.id}`)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
VITE_BACKEND_URL=http://localhost:3333/
|
||||
VITE_SOCKET_SERVER=http://localhost:8989/
|
||||
VITE_LOCALSTORAGE_VARIABLE=au_ma_te_da
|
||||
VITE_DOMAIN=http://localhost:5173/
|
||||
|
|
@ -13,7 +13,6 @@ import {
|
|||
MantineProvider,
|
||||
Grid,
|
||||
ScrollArea,
|
||||
Button,
|
||||
LoadingOverlay,
|
||||
} from "@mantine/core";
|
||||
import type {
|
||||
|
|
@ -22,7 +21,6 @@ import type {
|
|||
LineConfig,
|
||||
ReceivedFile,
|
||||
ResponseData,
|
||||
TextFSM,
|
||||
TLine,
|
||||
TStation,
|
||||
TUser,
|
||||
|
|
@ -30,7 +28,13 @@ import type {
|
|||
import axios from "axios";
|
||||
import CardLine from "./components/CardLine";
|
||||
import { SocketProvider, useSocket } from "./context/SocketContext";
|
||||
import { ButtonDPELP, ButtonScenario } from "./components/ButtonAction";
|
||||
import {
|
||||
ButtonConnect,
|
||||
ButtonCopy,
|
||||
ButtonDPELP,
|
||||
ButtonScenario,
|
||||
ButtonSelect,
|
||||
} from "./components/ButtonAction";
|
||||
import StationSetting from "./components/FormAddEdit";
|
||||
import DrawerScenario from "./components/DrawerScenario";
|
||||
import { Notifications } from "@mantine/notifications";
|
||||
|
|
@ -230,6 +234,8 @@ function App() {
|
|||
setTimeout(() => {
|
||||
updateValueLineStation(data.lineId, {
|
||||
data: data.data,
|
||||
inventory: data.inventory,
|
||||
latestScenario: data.latestScenario,
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
|
|
@ -319,19 +325,19 @@ function App() {
|
|||
if (!line.userEmailOpenCLI) {
|
||||
data.cliOpened = true;
|
||||
data.userEmailOpenCLI = user?.email;
|
||||
data.userOpenCLI = user?.fullName;
|
||||
data.userOpenCLI = user?.userName;
|
||||
socket?.emit("open_cli", {
|
||||
lineId: line.id,
|
||||
stationId: line.station_id,
|
||||
userEmail: user?.email,
|
||||
userName: user?.fullName,
|
||||
userName: user?.userName,
|
||||
});
|
||||
}
|
||||
setSelectedLine(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container py="md" w={"100%"} style={{ maxWidth: "100%" }}>
|
||||
<Container w={"100%"} style={{ maxWidth: "100%" }}>
|
||||
<DraggableTabs
|
||||
socket={socket}
|
||||
usersConnecting={usersConnecting}
|
||||
|
|
@ -405,100 +411,36 @@ function App() {
|
|||
<Flex
|
||||
direction={"column"}
|
||||
align={"center"}
|
||||
gap={"xs"}
|
||||
gap={"6px"}
|
||||
wrap={"wrap"}
|
||||
>
|
||||
<Button
|
||||
variant="filled"
|
||||
style={{ height: "30px", width: "100px" }}
|
||||
onClick={() => {
|
||||
if (selectedLines.length !== station.lines.length)
|
||||
setSelectedLines(station.lines);
|
||||
else setSelectedLines([]);
|
||||
}}
|
||||
>
|
||||
{selectedLines.length !== station.lines.length
|
||||
? "Select All"
|
||||
: "Deselect All"}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={
|
||||
selectedLines.filter((el) => el.status !== "connected")
|
||||
.length === 0
|
||||
}
|
||||
variant="outline"
|
||||
style={{ height: "30px", width: "100px" }}
|
||||
onClick={() => {
|
||||
const lines = selectedLines.filter(
|
||||
(el) => el.status !== "connected"
|
||||
);
|
||||
socket?.emit("connect_lines", {
|
||||
stationData: station,
|
||||
linesData: lines,
|
||||
});
|
||||
setSelectedLines([]);
|
||||
}}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
<Button
|
||||
disabled={selectedLines.length === 0}
|
||||
variant="outline"
|
||||
style={{ height: "30px", width: "100px" }}
|
||||
onClick={() => {
|
||||
if (selectedLines?.length > 0) {
|
||||
const value = selectedLines
|
||||
?.map((el) => {
|
||||
// Get data platform
|
||||
const dataPlatform = el.data?.find(
|
||||
(comm: TextFSM) =>
|
||||
comm.command?.trim() === "show platform"
|
||||
);
|
||||
const DPELP =
|
||||
dataPlatform &&
|
||||
!dataPlatform?.output?.includes("Incomplete")
|
||||
? true
|
||||
: false;
|
||||
|
||||
// Get data license
|
||||
const dataLicense = el.data?.find(
|
||||
(comm: TextFSM) =>
|
||||
comm.command?.trim() === "show license" ||
|
||||
comm.command?.trim() === "sh license"
|
||||
);
|
||||
const listLicense =
|
||||
dataLicense?.textfsm &&
|
||||
Array.isArray(dataLicense?.textfsm)
|
||||
? dataLicense?.textfsm
|
||||
?.map(
|
||||
(val: { FEATURE: string }) =>
|
||||
val.FEATURE
|
||||
)
|
||||
.join(", ")
|
||||
: "";
|
||||
|
||||
return `Line ${el.line_number ?? ""}: PID: ${
|
||||
el.inventory?.pid ?? ""
|
||||
}, SN: ${el.inventory?.sn ?? ""}, VID: ${
|
||||
el.inventory?.vid ?? ""
|
||||
}, Tested mode: ${
|
||||
DPELP ? "DPELP" : "DPEL"
|
||||
}, License: ${listLicense}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
navigator.clipboard.writeText(value);
|
||||
setSelectedLines([]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
<hr style={{ width: "100%" }} />
|
||||
<DrawerScenario
|
||||
scenarios={scenarios}
|
||||
setScenarios={setScenarios}
|
||||
<ButtonSelect
|
||||
selectedLines={selectedLines}
|
||||
setSelectedLines={setSelectedLines}
|
||||
station={station}
|
||||
/>
|
||||
<ButtonConnect
|
||||
selectedLines={selectedLines}
|
||||
setSelectedLines={setSelectedLines}
|
||||
station={station}
|
||||
socket={socket}
|
||||
/>
|
||||
<ButtonCopy
|
||||
selectedLines={selectedLines}
|
||||
setSelectedLines={setSelectedLines}
|
||||
/>
|
||||
<Flex
|
||||
w={"100%"}
|
||||
direction={"column"}
|
||||
align={"center"}
|
||||
wrap={"wrap"}
|
||||
>
|
||||
<hr style={{ width: "100%" }} />
|
||||
<DrawerScenario
|
||||
scenarios={scenarios}
|
||||
setScenarios={setScenarios}
|
||||
/>
|
||||
</Flex>
|
||||
<ButtonDPELP
|
||||
socket={socket}
|
||||
selectedLines={selectedLines}
|
||||
|
|
@ -511,26 +453,35 @@ function App() {
|
|||
}, 10000);
|
||||
}}
|
||||
/>
|
||||
{scenarios.map((el, i) => (
|
||||
<ButtonScenario
|
||||
key={i}
|
||||
socket={socket}
|
||||
selectedLines={selectedLines.filter(
|
||||
(el) =>
|
||||
typeof el?.userEmailOpenCLI === "undefined" ||
|
||||
el?.userEmailOpenCLI === user?.email
|
||||
)}
|
||||
isDisable={isDisable || selectedLines.length === 0}
|
||||
onClick={() => {
|
||||
setSelectedLines([]);
|
||||
setIsDisable(true);
|
||||
setTimeout(() => {
|
||||
setIsDisable(false);
|
||||
}, 10000);
|
||||
}}
|
||||
scenario={el}
|
||||
/>
|
||||
))}
|
||||
<ScrollArea h={"60vh"} style={{ paddingBottom: "12px" }}>
|
||||
<Flex
|
||||
w={"100%"}
|
||||
direction={"column"}
|
||||
wrap={"wrap"}
|
||||
gap={"6px"}
|
||||
>
|
||||
{scenarios.map((el, i) => (
|
||||
<ButtonScenario
|
||||
key={i}
|
||||
socket={socket}
|
||||
selectedLines={selectedLines.filter(
|
||||
(el) =>
|
||||
typeof el?.userEmailOpenCLI === "undefined" ||
|
||||
el?.userEmailOpenCLI === user?.email
|
||||
)}
|
||||
isDisable={isDisable || selectedLines.length === 0}
|
||||
onClick={() => {
|
||||
setSelectedLines([]);
|
||||
setIsDisable(true);
|
||||
setTimeout(() => {
|
||||
setIsDisable(false);
|
||||
}, 10000);
|
||||
}}
|
||||
scenario={el}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollArea>
|
||||
</Flex>
|
||||
<DrawerLogs
|
||||
socket={socket}
|
||||
|
|
@ -554,6 +505,25 @@ function App() {
|
|||
}}
|
||||
setActive={setActiveTab}
|
||||
active={activeTab}
|
||||
onSendCommand={(value) => {
|
||||
const listLine = selectedLines.length
|
||||
? selectedLines
|
||||
: stations.find((el) => el.id === Number(activeTab))?.lines;
|
||||
if (listLine?.length) {
|
||||
socket?.emit("write_command_line_from_web", {
|
||||
lineIds: listLine.map((line) => line.id),
|
||||
stationId: Number(activeTab),
|
||||
command: value + "\n",
|
||||
});
|
||||
setTimeout(() => {
|
||||
socket?.emit("write_command_line_from_web", {
|
||||
lineIds: listLine.map((line) => line.id),
|
||||
stationId: Number(activeTab),
|
||||
command: " \n",
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<StationSetting
|
||||
|
|
|
|||
|
|
@ -6,18 +6,18 @@ import axios from "axios";
|
|||
const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
||||
|
||||
type TLogin = {
|
||||
email: string;
|
||||
user_name: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
const Login = () => {
|
||||
const formLogin = useForm<TLogin>({
|
||||
initialValues: {
|
||||
email: "",
|
||||
user_name: "",
|
||||
password: "",
|
||||
},
|
||||
validate: (values) => ({
|
||||
email: values.email === "" ? "Email is required" : null,
|
||||
user_name: values.user_name === "" ? "Email is required" : null,
|
||||
|
||||
password: values.password === "" ? "Password is required" : null,
|
||||
}),
|
||||
|
|
@ -25,10 +25,10 @@ const Login = () => {
|
|||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
if (!formLogin.values.email) {
|
||||
if (!formLogin.values.user_name) {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Email is required",
|
||||
message: "Username is required",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
|
|
@ -42,7 +42,7 @@ const Login = () => {
|
|||
return;
|
||||
}
|
||||
const payload = {
|
||||
email: formLogin.values.email,
|
||||
user_name: formLogin.values.user_name,
|
||||
password: formLogin.values.password,
|
||||
};
|
||||
const response = await axios.post(apiUrl + "api/auth/login", payload);
|
||||
|
|
@ -69,12 +69,12 @@ const Login = () => {
|
|||
onSubmit={formLogin.onSubmit(handleLogin)}
|
||||
>
|
||||
<TextInput
|
||||
label={"Email address"}
|
||||
placeholder="hello@gmail.com"
|
||||
value={formLogin.values.email}
|
||||
error={formLogin.errors.email}
|
||||
label={"Username"}
|
||||
placeholder="Your username"
|
||||
value={formLogin.values.user_name}
|
||||
error={formLogin.errors.user_name}
|
||||
onChange={(e) => {
|
||||
formLogin.setFieldValue("email", e.target.value!);
|
||||
formLogin.setFieldValue("user_name", e.target.value!);
|
||||
}}
|
||||
required
|
||||
size="md"
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ type TRegister = {
|
|||
email: string;
|
||||
password: string;
|
||||
confirm_password: string;
|
||||
full_name: string;
|
||||
user_name: string;
|
||||
};
|
||||
|
||||
function Register() {
|
||||
const [formRegister, setFormRegister] = useState<TRegister>({
|
||||
email: "",
|
||||
full_name: "",
|
||||
user_name: "",
|
||||
password: "",
|
||||
confirm_password: "",
|
||||
});
|
||||
|
|
@ -42,12 +42,12 @@ function Register() {
|
|||
const payload = {
|
||||
email: formRegister.email,
|
||||
password: formRegister.password,
|
||||
full_name: formRegister.full_name,
|
||||
user_name: formRegister.user_name,
|
||||
};
|
||||
const response = await axios.post(apiUrl + "api/auth/register", payload);
|
||||
if (response.data.user) {
|
||||
const user = response.data.user;
|
||||
user.fullName = user.full_name;
|
||||
user.userName = user.user_name;
|
||||
localStorage.setItem("user", JSON.stringify(user));
|
||||
window.location.href = "/";
|
||||
} else {
|
||||
|
|
@ -86,18 +86,17 @@ function Register() {
|
|||
onChange={(e) => {
|
||||
setFormRegister({ ...formRegister, email: e.target.value });
|
||||
}}
|
||||
required
|
||||
size="md"
|
||||
mb="md"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
mb="md"
|
||||
label="Full name"
|
||||
label="Username"
|
||||
placeholder="Bill Gates"
|
||||
value={formRegister.full_name}
|
||||
value={formRegister.user_name}
|
||||
onChange={(e) => {
|
||||
setFormRegister({ ...formRegister, full_name: e.target.value });
|
||||
setFormRegister({ ...formRegister, user_name: e.target.value });
|
||||
}}
|
||||
required
|
||||
size="md"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Socket } from "socket.io-client";
|
||||
import type { IScenario, TLine } from "../untils/types";
|
||||
import type { IScenario, TextFSM, TLine, TStation } from "../untils/types";
|
||||
import { Button } from "@mantine/core";
|
||||
import classes from "./Component.module.css";
|
||||
|
||||
|
|
@ -27,6 +27,13 @@ export const ButtonDPELP = ({
|
|||
onClick();
|
||||
selectedLines?.forEach((el) => {
|
||||
const body = [
|
||||
{
|
||||
expect: "",
|
||||
send: " show inventory",
|
||||
delay: "1000",
|
||||
repeat: "1",
|
||||
note: "",
|
||||
},
|
||||
{
|
||||
expect: "",
|
||||
send: " show diag",
|
||||
|
|
@ -149,7 +156,7 @@ export const ButtonScenario = ({
|
|||
<Button
|
||||
disabled={isDisable}
|
||||
miw={"100px"}
|
||||
style={{ minHeight: "24px", height: "auto" }}
|
||||
style={{ minHeight: "28px", height: "auto" }}
|
||||
mr={"5px"}
|
||||
variant="outline"
|
||||
color="#00a164"
|
||||
|
|
@ -170,3 +177,119 @@ export const ButtonScenario = ({
|
|||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const ButtonCopy = ({
|
||||
selectedLines,
|
||||
setSelectedLines,
|
||||
}: {
|
||||
setSelectedLines: (value: React.SetStateAction<TLine[]>) => void;
|
||||
selectedLines: TLine[];
|
||||
}) => {
|
||||
return (
|
||||
<Button
|
||||
disabled={selectedLines.length === 0}
|
||||
variant="outline"
|
||||
style={{ height: "30px", width: "100px" }}
|
||||
onClick={() => {
|
||||
if (selectedLines?.length > 0) {
|
||||
const value = selectedLines
|
||||
?.map((el) => {
|
||||
// Get data platform
|
||||
const dataPlatform = el.data?.find(
|
||||
(comm: TextFSM) => comm.command?.trim() === "show platform"
|
||||
);
|
||||
const DPELP =
|
||||
dataPlatform && !dataPlatform?.output?.includes("Incomplete")
|
||||
? true
|
||||
: false;
|
||||
|
||||
// Get data license
|
||||
const dataLicense = el.data?.find(
|
||||
(comm: TextFSM) =>
|
||||
comm.command?.trim() === "show license" ||
|
||||
comm.command?.trim() === "sh license"
|
||||
);
|
||||
const listLicense =
|
||||
dataLicense?.textfsm && Array.isArray(dataLicense?.textfsm)
|
||||
? dataLicense?.textfsm
|
||||
?.map((val: { FEATURE: string }) => val.FEATURE)
|
||||
.join(", ")
|
||||
: "";
|
||||
|
||||
return `Line ${el.line_number ?? ""}: PID: ${
|
||||
el.inventory?.pid ?? ""
|
||||
}, SN: ${el.inventory?.sn ?? ""}, VID: ${
|
||||
el.inventory?.vid ?? ""
|
||||
}, Tested mode: ${
|
||||
DPELP ? "DPELP" : "DPEL"
|
||||
}, License: ${listLicense}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
navigator.clipboard.writeText(value);
|
||||
setSelectedLines([]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const ButtonSelect = ({
|
||||
selectedLines,
|
||||
setSelectedLines,
|
||||
station,
|
||||
}: {
|
||||
setSelectedLines: (value: React.SetStateAction<TLine[]>) => void;
|
||||
selectedLines: TLine[];
|
||||
station: TStation;
|
||||
}) => {
|
||||
return (
|
||||
<Button
|
||||
variant="filled"
|
||||
style={{ height: "30px", width: "100px" }}
|
||||
onClick={() => {
|
||||
if (selectedLines.length !== station.lines.length)
|
||||
setSelectedLines(station.lines);
|
||||
else setSelectedLines([]);
|
||||
}}
|
||||
>
|
||||
{selectedLines.length !== station.lines.length
|
||||
? "Select All"
|
||||
: "Deselect"}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const ButtonConnect = ({
|
||||
selectedLines,
|
||||
setSelectedLines,
|
||||
station,
|
||||
socket,
|
||||
}: {
|
||||
setSelectedLines: (value: React.SetStateAction<TLine[]>) => void;
|
||||
selectedLines: TLine[];
|
||||
station: TStation;
|
||||
socket: Socket | null;
|
||||
}) => {
|
||||
return (
|
||||
<Button
|
||||
disabled={
|
||||
selectedLines.filter((el) => el.status !== "connected").length === 0
|
||||
}
|
||||
variant="outline"
|
||||
style={{ height: "30px", width: "100px" }}
|
||||
onClick={() => {
|
||||
const lines = selectedLines.filter((el) => el.status !== "connected");
|
||||
socket?.emit("connect_lines", {
|
||||
stationData: station,
|
||||
linesData: lines,
|
||||
});
|
||||
setSelectedLines([]);
|
||||
}}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import TerminalCLI from "./TerminalXTerm";
|
|||
import type { Socket } from "socket.io-client";
|
||||
import { IconCircleCheckFilled } from "@tabler/icons-react";
|
||||
import { memo, useMemo } from "react";
|
||||
import { convertTimestampToDate } from "../untils/helper";
|
||||
|
||||
const CardLine = ({
|
||||
line,
|
||||
|
|
@ -62,7 +63,10 @@ const CardLine = ({
|
|||
// align={"center"}
|
||||
>
|
||||
<Flex justify={"space-between"}>
|
||||
<Text fw={600} style={{ display: "flex", gap: "4px" }}>
|
||||
<Text
|
||||
fw={600}
|
||||
style={{ display: "flex", gap: "4px", fontSize: "15px" }}
|
||||
>
|
||||
Line: {line.lineNumber || line.line_number} - {line.port}{" "}
|
||||
{line.status === "connected" && (
|
||||
<IconCircleCheckFilled color="green" />
|
||||
|
|
@ -80,9 +84,17 @@ const CardLine = ({
|
|||
{line?.userOpenCLI ? line?.userOpenCLI + " is using" : ""}
|
||||
</div>
|
||||
</Flex>
|
||||
{/* <Text className={classes.info_line}>PID: WS-C3560CG-8PC-S</Text>
|
||||
<div className={classes.info_line}>SN: FGL2240307M</div>
|
||||
<div className={classes.info_line}>VID: V01</div> */}
|
||||
<Flex justify={"space-between"}>
|
||||
<div className={classes.info_line}>
|
||||
PID: {line?.inventory?.pid || ""}
|
||||
</div>
|
||||
<div className={classes.info_line}>
|
||||
SN: {line?.inventory?.sn || ""}
|
||||
</div>
|
||||
<div className={classes.info_line} style={{ minWidth: "50px" }}>
|
||||
VID: {line?.inventory?.vid || ""}
|
||||
</div>
|
||||
</Flex>
|
||||
<Box
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -118,6 +130,16 @@ const CardLine = ({
|
|||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<div className={classes.info_line}>
|
||||
Latest: {line?.latestScenario?.name || ""}
|
||||
<Text style={{ fontStyle: "italic", fontSize: "11px" }}>
|
||||
{line?.latestScenario?.time
|
||||
? "(" + convertTimestampToDate(line?.latestScenario?.time) + ")"
|
||||
: ""}
|
||||
</Text>
|
||||
</div>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
.card_line {
|
||||
width: 320px;
|
||||
height: 220px;
|
||||
height: 250px;
|
||||
padding: 8px;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
|
|
@ -8,10 +8,10 @@
|
|||
|
||||
.info_line {
|
||||
color: dimgrey;
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
/* margin-top: 4px; */
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,15 @@ import {
|
|||
ActionIcon,
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
CloseButton,
|
||||
Flex,
|
||||
Group,
|
||||
Input,
|
||||
Menu,
|
||||
Tabs,
|
||||
Text,
|
||||
Tooltip,
|
||||
UnstyledButton,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
DndContext,
|
||||
|
|
@ -24,7 +28,13 @@ import {
|
|||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { useEffect, useMemo, useState, type JSX } from "react";
|
||||
import { IconEdit, IconSettingsPlus } from "@tabler/icons-react";
|
||||
import {
|
||||
IconChevronRight,
|
||||
IconEdit,
|
||||
IconLogout,
|
||||
IconSettingsPlus,
|
||||
IconUsersGroup,
|
||||
} from "@tabler/icons-react";
|
||||
import classes from "./Component.module.css";
|
||||
import type { TStation, TUser } from "../untils/types";
|
||||
import type { Socket } from "socket.io-client";
|
||||
|
|
@ -43,6 +53,7 @@ interface DraggableTabsProps {
|
|||
setStationEdit: (value: React.SetStateAction<TStation | undefined>) => void;
|
||||
active: string;
|
||||
setActive: (value: React.SetStateAction<string>) => void;
|
||||
onSendCommand: (value: string) => void;
|
||||
}
|
||||
|
||||
function SortableTab({
|
||||
|
|
@ -103,6 +114,7 @@ export default function DraggableTabs({
|
|||
setStationEdit,
|
||||
active,
|
||||
setActive,
|
||||
onSendCommand,
|
||||
}: DraggableTabsProps) {
|
||||
const user = useMemo(() => {
|
||||
return localStorage.getItem("user") &&
|
||||
|
|
@ -113,6 +125,7 @@ export default function DraggableTabs({
|
|||
const [tabs, setTabs] = useState<TStation[]>(tabsData);
|
||||
const [isChangeTab, setIsChangeTab] = useState<boolean>(false);
|
||||
const [isSetActive, setIsSetActive] = useState<boolean>(false);
|
||||
const [valueInput, setValueInput] = useState<string>("");
|
||||
// const [active, setActive] = useState<string | null>(
|
||||
// tabsData?.length > 0 ? tabsData[0]?.id.toString() : null
|
||||
// );
|
||||
|
|
@ -219,14 +232,33 @@ export default function DraggableTabs({
|
|||
w={w}
|
||||
>
|
||||
<Flex justify={"space-between"}>
|
||||
<Flex wrap={"wrap"} style={{ maxWidth: "250px" }} gap={"4px"}>
|
||||
{usersConnecting.map((el) => (
|
||||
<Tooltip label={el.userName} key={el.userId}>
|
||||
<Avatar color="cyan" radius="xl" size={"md"}>
|
||||
{el.userName.slice(0, 2)}
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
))}
|
||||
<Flex style={{ width: "300px" }} align={"center"}>
|
||||
<Input
|
||||
style={{
|
||||
width: "300px",
|
||||
boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)",
|
||||
}}
|
||||
placeholder={"Chat to Port/All"}
|
||||
value={valueInput}
|
||||
onChange={(event) => {
|
||||
const newValue = event.currentTarget.value;
|
||||
setValueInput(newValue);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
onSendCommand(valueInput);
|
||||
setValueInput("");
|
||||
}
|
||||
}}
|
||||
rightSectionPointerEvents="all"
|
||||
rightSection={
|
||||
<CloseButton
|
||||
aria-label="Clear input"
|
||||
onClick={() => setValueInput("")}
|
||||
style={{ display: valueInput ? undefined : "none" }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
<Tabs.List className={classes.list}>
|
||||
<SortableContext
|
||||
|
|
@ -280,20 +312,56 @@ export default function DraggableTabs({
|
|||
</ActionIcon>
|
||||
</Flex>
|
||||
</Tabs.List>
|
||||
<Flex gap={"sm"} align={"baseline"}>
|
||||
<Text className={classes.userName}>{user?.fullName}</Text>
|
||||
<Button
|
||||
variant="outline"
|
||||
color="red"
|
||||
style={{ height: "30px", width: "100px" }}
|
||||
onClick={() => {
|
||||
localStorage.removeItem("user");
|
||||
window.location.href = "/";
|
||||
socket?.disconnect();
|
||||
}}
|
||||
<Flex align={"center"}>
|
||||
<Tooltip
|
||||
withArrow
|
||||
label={usersConnecting.map((el) => (
|
||||
<Text key={el.userId}>{el.userName}</Text>
|
||||
))}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
<Avatar radius="xl" me={"sm"}>
|
||||
<IconUsersGroup color="green" />
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
<Menu withArrow>
|
||||
<Menu.Target>
|
||||
<UnstyledButton
|
||||
style={{
|
||||
padding: "var(--mantine-spacing-md)",
|
||||
color: "var(--mantine-color-text)",
|
||||
borderRadius: "var(--mantine-radius-sm)",
|
||||
}}
|
||||
>
|
||||
<Group>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size="sm" fw={500}>
|
||||
{user?.userName || user?.user_name || ""}
|
||||
</Text>
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{user?.email}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<IconChevronRight size={16} />
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
style={{ width: "150px" }}
|
||||
onClick={() => {
|
||||
localStorage.removeItem("user");
|
||||
window.location.href = "/";
|
||||
socket?.disconnect();
|
||||
}}
|
||||
color="red"
|
||||
leftSection={<IconLogout size={16} stroke={1.5} />}
|
||||
>
|
||||
Logout
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { useDisclosure } from "@mantine/hooks";
|
||||
import {
|
||||
Drawer,
|
||||
ActionIcon,
|
||||
Box,
|
||||
ScrollArea,
|
||||
Table,
|
||||
|
|
@ -9,6 +8,7 @@ import {
|
|||
TextInput,
|
||||
Button,
|
||||
Checkbox,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { IconSettingsPlus } from "@tabler/icons-react";
|
||||
import TableRows from "./Scenario/TableRows";
|
||||
|
|
@ -361,17 +361,25 @@ function DrawerScenario({
|
|||
</Grid.Col>
|
||||
</Grid>
|
||||
</Drawer>
|
||||
|
||||
<ActionIcon
|
||||
title="Add Scenario"
|
||||
variant="outline"
|
||||
color="green"
|
||||
onClick={() => {
|
||||
open();
|
||||
<Text
|
||||
fw={700}
|
||||
c={"#747474"}
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
}}
|
||||
>
|
||||
<IconSettingsPlus />
|
||||
</ActionIcon>
|
||||
Scenarios
|
||||
<IconSettingsPlus
|
||||
color="green"
|
||||
style={{ cursor: "pointer", width: "18px", height: "18px" }}
|
||||
onClick={() => {
|
||||
open();
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<DialogConfirm
|
||||
opened={openConfirm}
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ const StationSetting = ({
|
|||
)}
|
||||
</div>
|
||||
}
|
||||
size={"60%"}
|
||||
size={"80%"}
|
||||
style={{ position: "absolute", left: 0 }}
|
||||
centered
|
||||
opened={isOpen}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Modal, Text } from "@mantine/core";
|
||||
import classes from "./Component.module.css";
|
||||
import { convertTimestampToDate } from "../untils/helper";
|
||||
|
||||
const ModalLog = ({
|
||||
opened,
|
||||
|
|
@ -25,8 +26,8 @@ const ModalLog = ({
|
|||
const colorPhysicalStart = "#7fffd4";
|
||||
const colorPhysicalEnd = "#ffa589";
|
||||
return logText
|
||||
.replace(/^---split-point-scenario---.*$/gm, "") // Remove split-point lines
|
||||
.replace(/^---split-point---.*$/gm, "") // Remove split-point lines
|
||||
.replace(/^---scenario---.*$/gm, "") // Remove split-point lines
|
||||
.replace(/^---send-command---.*$/gm, "") // Remove split-point lines
|
||||
.replace(
|
||||
/^(---start-testing---|---end-testing---|---start-scenarios---|---end-scenarios---)(\d+)(---.*)?$/gm,
|
||||
(_, prefix, timestamp, suffix = "") => {
|
||||
|
|
@ -48,12 +49,6 @@ const ModalLog = ({
|
|||
);
|
||||
};
|
||||
|
||||
// Function to convert timestamp to date
|
||||
const convertTimestampToDate = (timestamp: number) => {
|
||||
const date = new Date(Number(timestamp));
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
style={{ position: "absolute", left: 0 }}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ const ModalTerminal = ({
|
|||
lineId: line?.id,
|
||||
stationId: line?.station_id,
|
||||
userEmail: user?.email,
|
||||
userName: user?.fullName,
|
||||
userName: user?.userName,
|
||||
});
|
||||
socket?.emit("request_take_over", {
|
||||
station_id: line?.station_id,
|
||||
|
|
@ -202,7 +202,7 @@ const ModalTerminal = ({
|
|||
socket?.emit("request_take_over", {
|
||||
line_id: line?.id,
|
||||
station_id: Number(line?.station_id),
|
||||
userName: user?.fullName?.trim() || "",
|
||||
userName: user?.userName?.trim() || "",
|
||||
userEmail: user?.email || "",
|
||||
});
|
||||
setDisableRequestTakeOver(true);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { useEffect, useRef, useState } from "react";
|
|||
import { Terminal } from "xterm";
|
||||
import "xterm/css/xterm.css";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { SOCKET_EVENTS } from "../untils/constanst";
|
||||
import type { Socket } from "socket.io-client";
|
||||
|
||||
interface TerminalCLIProps {
|
||||
|
|
@ -76,7 +75,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
|||
|
||||
// Gửi input từ người dùng lên server
|
||||
terminal.current.onData((data) => {
|
||||
socket?.emit(SOCKET_EVENTS.CLI.WRITE_COMMAND_FROM_WEB, {
|
||||
socket?.emit("write_command_line_from_web", {
|
||||
lineIds: [line_id],
|
||||
stationId: station_id,
|
||||
command: data,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||
const newSocket = io(SOCKET_URL, {
|
||||
auth: {
|
||||
userId: user?.id,
|
||||
userName: user?.fullName,
|
||||
userName: user?.userName,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -26,3 +26,9 @@ export function mergeArray(array: any[], key: string) {
|
|||
.flat()
|
||||
.filter((el) => Object.keys(el).length > 0);
|
||||
}
|
||||
|
||||
// Function to convert timestamp to date
|
||||
export const convertTimestampToDate = (timestamp: number) => {
|
||||
const date = new Date(Number(timestamp));
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -83,6 +83,10 @@ export type TLine = {
|
|||
userOpenCLI?: string;
|
||||
userEmailOpenCLI?: string;
|
||||
statusTicket?: string;
|
||||
latestScenario?: {
|
||||
name: string;
|
||||
time: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type TUser = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue