фронт

This commit is contained in:
test 2025-06-09 10:31:14 +05:00
parent eb1f18f53e
commit d67ea17fd7
160 changed files with 2611 additions and 5680 deletions

View file

@ -1,10 +1,8 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { HashRouter } from 'react-router-dom';
import { NetworkService } from './src/api/request.ts';
import { useIpStore } from './src/store';
import { useEffect } from 'react';
import { useAdminStore } from './src/store/adminStore.ts';
import {useCheckAdmin} from './src/utils/checkAdmin.ts'
import {

View file

@ -21,7 +21,7 @@ function createWindow() {
win.show();
if (process.env.NODE_ENV === 'development') {
win.loadURL('http://localhost:3000');
win.loadURL('http://192.168.1.152:8001');
win.webContents.openDevTools();
} else {
win.loadFile(path.join(__dirname, 'dist/index.html'));
@ -35,7 +35,7 @@ app.whenReady().then(() => {
// 2. Всех IP в локальной сети (192.168.x.x, 10.x.x.x и т.д.)
// 3. Конкретного домена (если есть)
if (
request.hostname === 'localhost' ||
request.hostname === '192.168.1.152' ||
request.hostname === '127.0.0.1' ||
request.hostname === '0.0.0.0' ||
/^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/.test(request.hostname)
@ -44,6 +44,7 @@ app.whenReady().then(() => {
}
callback(-2);
});
Menu.setApplicationMenu(null);
createWindow();
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View file

@ -1,13 +1,13 @@
import axios, { AxiosResponse } from "axios";
const axiosWithoutCredentials = axios.create({
baseURL: `https://localhost:8001`,
baseURL: `http://localhost:8001`,
})
export class NetworkService {
static createAxiosInstance(ipAddress: string) {
return axios.create({
baseURL: `https://${ipAddress}:8001`,
baseURL: `http://${ipAddress}:8001`,
});
}
@ -82,4 +82,22 @@ export class NetworkService {
return error.response;
}
}
static async scanUser(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get('/subnet/scan');
} catch (error: any) {
return error.response;
}
}
static async scanWifi(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get('/subnet/user');
} catch (error: any) {
return error.response;
}
}
}

View file

@ -0,0 +1,115 @@
import React from 'react';
import styled, { css } from 'styled-components';
import { DeviceUser } from "../../../store/useDeviceStore";
interface DeviceCardProps {
device: DeviceUser;
status: 'online' | 'offline';
}
const CardWrapper = styled.div<{ status: 'online' | 'offline' }>`
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: all 0.3s ease;
cursor: pointer;
margin-bottom: 16px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
}
`;
const CardHeader = styled.div<{ status: 'online' | 'offline' }>`
background: ${({ status, theme }) =>
status === 'online' ? theme.successColor : theme.errorColor};
color: white;
padding: 16px;
font-size: 18px;
font-weight: 500;
display: flex;
justify-content: space-between;
align-items: center;
`;
const CardContent = styled.div<{ isExpanded: boolean }>`
max-height: 0;
opacity: 0;
padding: 0 16px;
transition: all 0.3s ease;
color: #333;
font-size: 14px;
display: grid;
gap: 12px;
${({ isExpanded }) => isExpanded && css`
max-height: 500px;
opacity: 1;
padding: 16px;
`}
`;
const InfoRow = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;
const Label = styled.span`
font-weight: 500;
color: #666;
`;
const Value = styled.span`
color: #333;
`;
const DeviceCard: React.FC<DeviceCardProps> = ({ device, status }) => {
const [isExpanded, setIsExpanded] = React.useState(false);
const toggleExpanded = () => setIsExpanded(!isExpanded);
return (
<CardWrapper status={status} onClick={toggleExpanded}>
<CardHeader status={status}>
<p>
MAC: {device.mac}
</p>
</CardHeader>
<CardContent isExpanded={isExpanded}>
<InfoRow>
<Label>Имя:</Label>
<Value>{device.hostname || 'Неизвестно'}</Value>
</InfoRow>
<InfoRow>
<Label>Производитель:</Label>
<Value>{device.vendor || 'Неизвестно'}</Value>
</InfoRow>
<InfoRow>
<Label>IP адрес:</Label>
<Value>{device.ip}</Value>
</InfoRow>
{device.open_ports?.length > 0 && (
<InfoRow>
<Label>Открытые порты:</Label>
<Value>{device.open_ports.join(', ')}</Value>
</InfoRow>
)}
<InfoRow>
<Label>Последняя активность:</Label>
<Value>{device.last_seen}</Value>
</InfoRow>
</CardContent>
</CardWrapper>
);
};
export default DeviceCard;

View file

@ -0,0 +1,129 @@
import React, {useEffect, useRef, useState} from 'react';
import styled from "styled-components";
import {buttonNames} from "../../../const/buttons/names";
import {IconComponent} from "../../../const/buttons/icons";
type Option = {
value: number,
label: string,
}
interface DropdownTypeProps {
options: Option[];
label?: string;
onChange?: (selectedItem: Option) => void;
value: Option | null;
}
const DropdownWrapper = styled.div`
position: absolute;
top: 18px;
right:20px;
display: inline-block;
background-color: ${({theme}) => theme.background};
padding: 12px 16px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 10;
`
const Label = styled.p`
color: ${({theme}) => theme.textColor};
font-size: 16px;
`
const Dropdown = styled.div`
background-color: ${({theme}) => theme.primary};
border-radius: 15px;
overflow: hidden;
transition: all .3s ease;
cursor: pointer;
z-index: 10;
margin-top: 6px;
`
const DropdownHeader = styled.div`
display: flex;
justify-content: space-between;
gap: 14px;
padding: 12px 24px;
font-size: 16px;
align-items: center;
height: 100%;
min-width: 200px;
`
const DropdownContent = styled.div<{$isOpen: boolean}>`
max-height: ${({$isOpen}) => $isOpen ? '500px' : '0'};
opacity: ${({$isOpen}) => $isOpen ? 1 : 0};
transition: all .3s ease;
display: flex;
flex-direction: column;
overflow: hidden;
`
const DropdownRow = styled.button`
padding: 12px 24px;
background-color: transparent;
text-align: start;
font-size: 16px;
border: none;
width: 100%;
transition: all .3s ease;
cursor: pointer;
&:hover {
background-color: ${({theme}) => theme.background};
}
`
const DropdownArrow = styled.div<{ $isOpen: boolean }>`
transition: transform 0.3s ease;
transform: rotate(${({ $isOpen }) => ($isOpen ? "90deg" : "0deg")});
`;
const DropdownType = ({options, value, onChange, label}: DropdownTypeProps) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
function toggleOpen() {
setIsOpen((prev) => !prev);
}
function handleSelect(option: Option) {
onChange?.(option);
setIsOpen(false);
}
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<DropdownWrapper ref={dropdownRef}>
<Label>{label}</Label>
<Dropdown>
<DropdownHeader onClick={toggleOpen}>
<p>{value?.label || 'Select an option'}</p>
<DropdownArrow $isOpen={isOpen}>
<IconComponent name={buttonNames[6]} />
</DropdownArrow>
</DropdownHeader>
<DropdownContent $isOpen={isOpen}>
{options.map((option, i) => (
<DropdownRow key={i} onClick={() => handleSelect(option)}>
{option.label}
</DropdownRow>
))}
</DropdownContent>
</Dropdown>
</DropdownWrapper>
);
};
export default DropdownType;

View file

@ -0,0 +1,88 @@
import React from 'react';
import { DeviceUser } from '../../../store/useDeviceStore';
interface TableScannerProps {
devices: DeviceUser[];
}
import styled from 'styled-components';
const TableContainer = styled.div`
width: 100%;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
background: ${({ theme }) => theme.primary};
margin: 24px 0;
`;
const StyledTable = styled.table`
width: 100%;
border-collapse: separate;
border-spacing: 0;
`;
const TableHeaderCell = styled.th<{ sortable?: boolean }>`
padding: 16px 24px;
text-align: left;
font-weight: 600;
font-size: 14px;
color: ${({ theme }) => theme.textSecondary};
background: ${({ theme }) => theme.primary};
border-bottom: 1px solid ${({ theme }) => theme.borderColor};
transition: all 0.2s ease;
`;
const TableRow = styled.tr<{ clickable?: boolean }>`
transition: all 0.2s ease;
&:hover {
background-color: ${({ theme }) => theme.background};
}
`;
const TableCell = styled.td`
padding: 16px 24px;
font-size: 14px;
color: ${({ theme }) => theme.text};
border-bottom: 1px solid ${({ theme }) => theme.borderColor};
transition: all 0.2s ease;
`;
const StatusBadge = styled.span<{ status:string }>`
width: 10px;
height: 30px;
background-color: ${({theme, status}) => (status==='online' ? theme.successColor : theme.errorColor)};
display: inline-block;
border-radius:4px;
`
const TableScanner = ({devices}:TableScannerProps) => {
return (
<TableContainer>
<StyledTable>
<thead>
<tr>
<TableHeaderCell></TableHeaderCell>
<TableHeaderCell>Имя</TableHeaderCell>
<TableHeaderCell>Производитель</TableHeaderCell>
<TableHeaderCell>IP адрес</TableHeaderCell>
<TableHeaderCell>Открытые порты</TableHeaderCell>
<TableHeaderCell>Последняя активность</TableHeaderCell>
</tr>
</thead>
<tbody>
{devices.map((item, i) => (
<TableRow key={i}>
<TableCell><StatusBadge status={item.status}></StatusBadge></TableCell>
<TableCell>{item.hostname || 'Неизвестен'}</TableCell>
<TableCell>{item.vendor}</TableCell>
<TableCell>{item.ip}</TableCell>
<TableCell>{item.open_ports.join(', ') || '-'}</TableCell>
<TableCell>{item.last_seen}</TableCell>
</TableRow>
))}
</tbody>
</StyledTable>
</TableContainer>
);
};
export default TableScanner;

View file

@ -110,15 +110,17 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
try {
const result = await NetworkService.saveIp(ipAddress, data);
if (result.status === 200) {
setIpAddress(ipData.newIp)
const updatedDevices = devices.map(device =>
device.address === ipData.currentIp
? { ...device, address: ipData.newIp }
: device
);
useDevicesStore.setState({ devices: updatedDevices });
if(ipAddress!=='localhost'){
setIpAddress(ipData.newIp)
const updatedDevices = devices.map(device =>
device.address === ipData.currentIp
? { ...device, address: ipData.newIp }
: device
);
useDevicesStore.setState({ devices: updatedDevices });
}
onClose()
}
} catch (err) {

View file

@ -23,7 +23,7 @@ export const ModalContainer = styled.div<{height?: string}>`
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 15px;
gap: 15px;
height: ${(props) => props.height ? props.height : '200px'};
width: 520px;
`

View file

@ -7,13 +7,16 @@ import { useIpStore } from "../store"
import { NetworkService } from "../api/request"
import { IconComponent } from "../const/buttons/icons"
import { buttonNames } from "../const/buttons/names"
import ThemeButton from "./UI/buttons/toggletheme"
import About from "./workSpace/modules/appModules/about"
const Wrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
background: ${(props)=> props.theme.background};
background: ${(props) => props.theme.background};
`
const Container = styled.form`
@ -33,7 +36,7 @@ const Title = styled.div`
gap: 15px;
align-items: center;
text-align: center;
font-size: 25px;
font-size: 20px;
`
const Content = styled.div`
@ -49,11 +52,27 @@ const ButtonContainer = styled.div`
justify-content: center;
`
const Menu = styled.div`
display: flex;
align-items: center;
`
const Button = styled.div`
width: 100%;
cursor: pointer;
`
const IconContainer = styled.div`
padding: 10px;
color: ${props => props.theme.text};
`
export const CreateUser = () => {
const { ipAddress } = useIpStore();
const { ipAddress, setIpAddress } = useIpStore();
const [repeatPassword, setRepeatPassword] = useState('');
const [passwordsMatch, setPasswordsMatch] = useState(true);
const [about, setAbout] = useState(false)
const [user, setUser] = useState({
username: 'operator',
@ -92,51 +111,85 @@ export const CreateUser = () => {
}
};
const back = ()=>{
setIpAddress('localhost')
}
const toAbout = ()=>{
setAbout(true)
}
return (
<Wrapper>
<Container onSubmit={handleSubmit}>
<Title>
<IconComponent name={buttonNames[3]} width="52" height="52"></IconComponent>
<h3>Первоначальная настройка приложения</h3>
</Title>
<>
{!about ? (
<Wrapper>
<Content>
<div>
<p>Перед началом работы необходимо создать пароль оператора приложения</p>
</div>
<div>
<Input
type="password"
placeholderText='Введите пароль...'
id="password"
value={user.password}
onChange={(id, value) => handleChange(id, value)}
/>
<Input
type="password"
placeholderText='Повторите пароль...'
id="repeatPassword"
value={repeatPassword}
onChange={(id, value) => handleChange(id, value)}
style={{ marginTop: '10px' }}
/>
{!passwordsMatch && (
<p style={{ color: 'red', marginTop: '5px' }}>
Пароли не совпадают!
</p>
)}
</div>
</Content>
<ButtonContainer>
<MainButton style={{ width: '100%' }} title="Отправить" type="submit" />
</ButtonContainer>
</Container>
</Wrapper>
<Container onSubmit={handleSubmit}>
<Title>
<IconComponent name={buttonNames[3]} width="52" height="52"></IconComponent>
<h3>Настройка оператора узла</h3>
{ipAddress != 'localhost' &&
<p>{ipAddress}</p>
}
</Title>
<Content>
<div>
<p>Перед началом работы необходимо создать пароль оператора приложения</p>
</div>
<div>
<Input
type="password"
placeholderText='Введите пароль...'
id="password"
value={user.password}
onChange={(id, value) => handleChange(id, value)}
/>
<Input
type="password"
placeholderText='Повторите пароль...'
id="repeatPassword"
value={repeatPassword}
onChange={(id, value) => handleChange(id, value)}
style={{ marginTop: '10px' }}
/>
{!passwordsMatch && (
<p style={{ color: 'red', marginTop: '5px' }}>
Пароли не совпадают!
</p>
)}
</div>
</Content>
<ButtonContainer>
<MainButton style={{ width: '100%' }} title="Отправить" type="submit" />
</ButtonContainer>
</Container>
<Menu>
{ipAddress != 'localhost' &&
<MainButton title="Назад" onClick={back} style={{marginRight: '10px'}}/>
}
<ThemeButton />
<Button>
<IconContainer>
<IconComponent name={buttonNames[4]} width="30" height="30" onClick={toAbout}/>
</IconContainer>
</Button>
</Menu>
</Wrapper>
): (
<About from="reg" onBack={() => setAbout(false)} />
)}
</>
)
}

View file

@ -1,69 +1,213 @@
import styled from "styled-components"
import { useNavigate } from "react-router-dom"
const Container = styled.div`
height: 100%;
import { MainButton } from "../../../UI/buttons/mainButton"
import { workSpaceNames } from "../../../../const/workspace/names"
import { useWorkSpaceStore } from "../../../../store"
import styled from "styled-components"
import ThemeButton from "../../../UI/buttons/toggletheme"
type Props = {
from: 'reg' | 'helloWindow' | 'workSpace'
onBack?: () => void
}
const Wrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 25px;
text-align: center;
h1, h2, h3, p{
margin: 0;
}
height: 100%;
width: 100%;
background: ${(props => props.theme.background)};
padding: 50px;
box-sizing: border-box;
`
const Content = styled.div`
max-width: 450px;
max-width: 1200px;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
box-sizing: border-box;
padding: 25px 50px;
background: ${(props => props.theme.secondary)};
border-radius: 18px;
`
const Listing = styled.div`
display: flex;
flex-direction: column;
gap: 15px;
`
const List = styled.div`
display: flex;
flex-direction: column;
gap: 15px;
margin-left: 10px;
`
const Title = styled.div`
font-size: 20px;
`
const SubTitle = styled.div`
display: flex;
flex-direction: column;
gap: 10px;
p{
font-size: 18px;
}
`
const Links = styled.div`
display: flex;
flex-direction: column;
gap: 20px;
`
const LinkColumn = styled.div`
display: flex;
flex-direction: column;
gap: 10px;
`
const LinkBlock = styled.div`
display: flex;
gap: 5px;
`
const Copyright = styled.div`
width: 100%;
text-align: center;
margin-top: 50px;
`
const ButtonContainer = styled.div`
width: 100%;
display: flex;
justifi-content: flex-start;
align-items: center;
gap: 100px;
button{
width: 250px;
`
const MainTitle = styled.div`
width: 100%;
display: flex;
gap: 15px;
align-items: center;
justify-content: center;
padding-top: 50px;
box-sizing: border-box;
img{
width: 40px;
height: 40px;
}
`
const Row = styled.div`
display: flex;
gap: 10px;
align-items: flex-end;
justify-content: center;
margin-left: 50px;
`
const Column = styled.div`
const Text = styled.div`
display: flex;
flex-direction: column;
gap: 10px;
gap: 30px;
`
export default function About() {
const navigate = useNavigate()
const redirect = (path: string) => {
navigate(path)
export default function About({ from, onBack }: Props) {
const { setWorkModule } = useWorkSpaceStore()
const getBackHandler = () => {
switch (from) {
case 'reg':
return onBack
case 'helloWindow':
return () => setWorkModule(workSpaceNames[1])
case 'workSpace':
return () => setWorkModule(workSpaceNames[3])
default:
return undefined
}
}
const backHandler = getBackHandler()
const showBackButton = from !== 'reg' || (from === 'reg' && onBack)
return (
<Container>
<Wrapper>
<Content>
<Row>
<h1>GoNetMan</h1>
<p>v 0.0.1</p>
</Row>
<Column>
<h3>О программе</h3>
GoNetMan - Это приложение представляющее собой панель управления сетевыми настройками в удобном и понятном графическом виде.
</Column>
<MainTitle>
<img src="./assets/images/gossys.png" alt="" />
<div style={{width: '100%', display: "flex", justifyContent: 'center'}}><h1>О приложении</h1></div>
</MainTitle>
<p>Copyright © ООО "Гибридные офисные системы"</p>
<button onClick={()=>redirect('/')}>В меню</button>
<Text>
<Title>
<p>Приложение ГоNets является минимально работоспособным кроссплатформенным продуктом (MVP) для демонстрации нашего подхода к управлению настройками ПК в корпоративной среде.</p>
</Title>
<Listing>
<p>Данное приложение умеет:</p>
<List>
<p>- Отображать в графическом виде активные сетевые ресурсы</p>
<p>- Настраивать сетевые параметры локального и удаленных сетевых ресурсов</p>
<p>- Отображать и изменять взаимодействие сетевых ресурсов с установленным Клиентом ГоNets</p>
</List>
</Listing>
<SubTitle>
<p>Программное обеспечение исключительно для ознакомления и домашнего использования.</p>
<p>Для коммерческого использования приобретите версию ГоNets-PRO (Подробная информация на странице продукта)</p>
</SubTitle>
<Links>
<LinkBlock>
<p>Ссылка на домашнюю страницу:</p>
<a href="https://gossys.dev/pages/projects/gonets">https://gossys.dev/pages/projects/gonets</a>
</LinkBlock>
<LinkBlock>
<p>Инструкции и информация:</p>
<a href="https://wiki.gossys.dev/ru/gonets">https://wiki.gossys.dev/ru/gonets</a>
</LinkBlock>
<LinkColumn>
<p>Распространяется под лицензией GosysPUB текущей версии.</p>
<LinkBlock>
<p>Полный текст актуальной лицензии:</p>
<a href="https://wiki.gossys.dev/ru/lic/GosysPUB">https://wiki.gossys.dev/ru/lic/GosysPUB</a>
</LinkBlock>
</LinkColumn>
</Links>
<Copyright>
<p>Разработано и поддерживается ООО «Гибридные Офисные Системы» 2024-2025 г. Все права защищены.</p>
</Copyright>
</Text>
<ButtonContainer>
{showBackButton && backHandler && (
<MainButton
title="Назад"
onClick={backHandler}
style={{ marginRight: '10px' }}
/>
)}
{from == 'reg' &&
<ThemeButton />
}
</ButtonContainer>
</Content>
</Container>
</Wrapper>
)
}

View file

@ -1,81 +1,82 @@
import styled from "styled-components"
import { MainButton } from "../../../UI/buttons/mainButton"
import { useWorkSpaceStore } from "../../../../store"
import { workSpaceNames } from "../../../../const/workspace/names"
import { buttonNames } from "../../../../const/buttons/names"
import styled from "styled-components"
import { MainButton } from "../../../UI/buttons/mainButton"
import { useWorkSpaceStore } from "../../../../store"
import { workSpaceNames } from "../../../../const/workspace/names"
import { buttonNames } from "../../../../const/buttons/names"
const Wrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: ${(props => props.theme.background)};
background-image: url('./assets/images/back1.svg');
background-size: 100%;
background-repeat: no-repeat;
background-position: bottom;
`
const Wrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: ${(props => props.theme.background)};
background-image: url('./assets/images/back1.svg');
background-size: 100%;
background-repeat: no-repeat;
background-position: bottom;
`
const Container = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 52px;
margin-bottom: 100px;
`
const Container = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 52px;
margin-bottom: 100px;
`
const ButtonWrapper = styled.div`
display: flex;
gap: 75px;
flex-direction: column;
`
const ButtonWrapper = styled.div`
display: flex;
gap: 75px;
flex-direction: column;
`
const ButtonContainer = styled.div`
display: flex;
gap: 15px;
flex-direction: column;
`
const ButtonContainer = styled.div`
display: flex;
gap: 15px;
flex-direction: column;
`
const Title = styled.div`
display: flex;
align-items: flex-end;
gap: 10px;
const Title = styled.div`
display: flex;
align-items: flex-end;
gap: 10px;
h1{
font-size: 52px;
}
`
h1{
font-size: 52px;
}
`
export const HelloWindow = () => {
export const HelloWindow = () => {
const { setWorkModule } = useWorkSpaceStore()
const { setWorkModule } = useWorkSpaceStore()
return (
<Wrapper>
<Container className="unselectable">
<Title>
<img src="./assets/images/logo.svg" alt="" style={{width: '152px'}}/>
</Title>
return (
<Wrapper>
<Container className="unselectable">
<Title>
<img src="./assets/images/logo.svg" alt="" style={{width: '152px'}}/>
</Title>
<ButtonWrapper>
<ButtonContainer>
<MainButton
title="Настройки сети"
icon={buttonNames[3]}
iconSize={{ width: '32', height: '32' }}
onClick={() => setWorkModule(workSpaceNames[3])}
/>
<MainButton
title="О программе"
icon={buttonNames[4]}
iconSize={{ width: '32', height: '32' }}
/>
</ButtonContainer>
</ButtonWrapper>
</Container>
</Wrapper>
)
}
<ButtonWrapper>
<ButtonContainer>
<MainButton
title="Настройки сети"
icon={buttonNames[3]}
iconSize={{ width: '32', height: '32' }}
onClick={() => setWorkModule(workSpaceNames[3])}
/>
<MainButton
title="О программе"
icon={buttonNames[4]}
iconSize={{ width: '32', height: '32' }}
onClick={()=>setWorkModule(workSpaceNames[4])}
/>
</ButtonContainer>
</ButtonWrapper>
</Container>
</Wrapper>
)
}

View file

@ -0,0 +1,75 @@
import React from 'react';
import styled from 'styled-components';
import DeviceCard from "../../../../../UI/Dropdown/DeviceCard";
import {DeviceUser} from "../../../../../../store/useDeviceStore";
const ScanBtn = styled.button`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
transition: .4s ease;
&:hover{
transform: scale(1.03);
}
`
interface ListUsersProps {
loading: boolean;
handleScan: ()=>void;
devices: DeviceUser[];
scanInitiated: boolean;
}
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
// Фильтр по статусу
const activeDevices = devices.filter(device => device.status === 'online');
const inactiveDevices = devices.filter(device => device.status !== 'online');
return (
<>
<ScanBtn onClick={handleScan} disabled={loading}>
{devices.length > 0 ? 'Обновить список' : 'Сканировать сеть'}
</ScanBtn>
{devices.length > 0 ? (
<div style={{
display: 'flex',
width: '70%',
gap: '20px',
}}>
{/* Колонка активных устройств */}
<div style={{ flex: 1 }}>
<h3 style={{ color: '#4CAF50', marginBottom:'12px' }}>Активные устройства ({activeDevices.length})</h3>
{activeDevices.map((device, index) => (
<DeviceCard
key={index}
status="online"
device={device}
/>
))}
</div>
{/* Колонка неактивных устройств */}
<div style={{ flex: 1 }}>
<h3 style={{ color: '#F44336', marginBottom:'12px' }}>Неактивные устройства ({inactiveDevices.length})</h3>
{inactiveDevices.map((device, index) => (
<DeviceCard
key={index}
status="offline"
device={device}
/>
))}
</div>
</div>
) : scanInitiated ? (
<p style={{ marginTop: '20px' }}>Устройства не найдены</p>
) : null}
</>
);
};
export default ListUsers;

View file

@ -0,0 +1,45 @@
import React from 'react';
import styled from 'styled-components';
import DeviceCard from "../../../../../UI/Dropdown/DeviceCard";
import {DeviceUser} from "../../../../../../store/useDeviceStore";
import TableScanner from "../../../../../UI/tables/TableScanner";
const ScanBtn = styled.button`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
transition: .4s ease;
&:hover{
transform: scale(1.03);
}
`
interface ListUsersProps {
loading: boolean;
handleScan: ()=>void;
devices: DeviceUser[];
scanInitiated: boolean;
}
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
return (
<>
<ScanBtn onClick={handleScan} disabled={loading}>
{devices.length > 0 ? 'Обновить список' : 'Сканировать сеть'}
</ScanBtn>
{devices.length > 0 ? (
<TableScanner devices={devices} />
) : scanInitiated ? (
<p style={{ marginTop: '20px' }}>Устройства не найдены</p>
) : null}
</>
);
};
export default ListUsers;

View file

@ -0,0 +1,185 @@
import { useIpStore } from "../../../../../store";
import styled, { keyframes } from "styled-components";
import { useState } from "react";
import { NetworkService } from "../../../../../api/request";
import ListUsers from "./modules/ListUsers";
import TableUsers from "./modules/TableUsers";
import DropdownType from "../../../../UI/Dropdown/DropdownType";
import useNetworkStore from "../../../../../store/useDeviceStore";
type Option ={
value: number,
label: string,
}
// Анимации
const ring1 = keyframes`
0% { width: 250px; height: 250px; }
50% { width: 280px; height: 280px; }
100% { width: 250px; height: 250px; }
`;
const ring2 = keyframes`
0% { width: 400px; height: 400px; }
50% { width: 430px; height: 430px; }
100% { width: 400px; height: 400px; }
`;
const ring3 = keyframes`
0% { width: 550px; height: 550px; }
50% { width: 580px; height: 580px; }
100% { width: 550px; height: 550px; }
`;
const dotAnimation = keyframes`
0% { content: '.'; }
33% { content: '..'; }
66% { content: '...'; }
100% { content: '.'; }
`;
// Стили
const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
display: flex;
flex-direction: column;
align-items: center;
justify-content:${({scanInitiated, loading})=>(scanInitiated && !loading ? 'start' : 'center')};
height: 100%;
width: 100%;
background: ${(props) => props.theme.background};
padding: 50px;
box-sizing: border-box;
gap: 24px;
transition: justify-content .4s ease;
overflow-y: auto;
overflow-x: hidden;
position: relative;
`;
const TitleWrapper = styled.h2`
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
font-size: 32px;
`
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
width: 60px;
height: 60px;
border-radius: 50%;
position: relative;
transition: .4s ease;
transform: ${({scanInitiated,loading})=>(scanInitiated && !loading ? 'scale(0)' : 'scale(1)')};
span {
position: absolute;
display: inline-block;
background: ${(props) => props.theme.accent};
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
&.center{
width: 60px;
height: 60px;
}
&.ring1 {
opacity: 0.12;
width: 250px;
height: 250px;
animation: ${({ loading }) => (loading ? ring1 : "none")} 4s linear infinite;
}
&.ring2 {
opacity: 0.08;
width: 400px;
height: 400px;
animation: ${({ loading }) => (loading ? ring2 : "none")} 4s linear infinite;
}
&.ring3 {
opacity: 0.04;
width: 550px;
height: 550px;
animation: ${({ loading }) => (loading ? ring3 : "none")} 4s linear infinite;
}
}
`;
const LoadingText = styled.div`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
`;
const ScanningText = styled.span`
&::after {
content: '.';
animation: ${dotAnimation} 1.5s infinite;
}
`;
export default function UserScanner() {
const { ipAddress } = useIpStore();
const [loading, setLoading] = useState(false);
const { devices, setDevices } = useNetworkStore();
const [scanInitiated, setScanInitiated] = useState(devices.length > 0);
// Сканирование
const handleScan = async () => {
try {
setLoading(true);
setScanInitiated(true);
const res = await NetworkService.scanUser(ipAddress);
setDevices(res.data.devices || []);
console.log(res.data.devices);
} catch (err) {
console.error("Ошибка сканирования сети:", err);
setDevices([])
} finally {
setLoading(false);
}
};
// Способ отображения
const viewOptions: Option[] = [
{ value: 1, label: 'Таблица' },
{ value: 2, label: 'Список' },
];
const [selectedView, setSelectedView] = useState<Option>(viewOptions[0]);
const handleViewChange = (option: Option) => {
setSelectedView(option);
};
return (
<Wrapper scanInitiated={scanInitiated} loading={loading}>
{(!scanInitiated || loading) && (<TitleWrapper>Сканирование пользователей в сети</TitleWrapper>)}
<UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center"></span>
<span className="ring1"></span>
<span className="ring2"></span>
<span className="ring3"></span>
</UserLocate>
{scanInitiated && !loading && (
<DropdownType options={viewOptions} value={selectedView} onChange={handleViewChange} label="Способ отображения" />
)}
{loading ? (
<LoadingText>
Сканирование сети<ScanningText />
</LoadingText>
) : selectedView.value == 1 ? (
<TableUsers loading={loading} handleScan={handleScan} devices={devices} scanInitiated={scanInitiated} />
):(
<ListUsers loading={loading} handleScan={handleScan} devices={devices} scanInitiated={scanInitiated} />
)}
</Wrapper>
);
}

View file

@ -0,0 +1,234 @@
import { useIpStore } from "../../../../../store";
import styled, { keyframes } from "styled-components";
import React, { useState } from "react";
import { NetworkService } from "../../../../../api/request";
import useDeviceStore from "../../../../../store/useDeviceStore";
import WifiDot, {WifiNetworkScanner} from "./modules/WifiDot";
import ModalWifiScaner from "./modules/ModalWifiScaner";
import useNetworkStore from "../../../../../store/useDeviceStore";
type Option ={
value: number,
label: string,
}
// Анимации
const ring1 = keyframes`
0% { width: 250px; height: 250px; }
50% { width: 280px; height: 280px; }
100% { width: 250px; height: 250px; }
`;
const ring2 = keyframes`
0% { width: 400px; height: 400px; }
50% { width: 430px; height: 430px; }
100% { width: 400px; height: 400px; }
`;
const ring3 = keyframes`
0% { width: 550px; height: 550px; }
50% { width: 580px; height: 580px; }
100% { width: 550px; height: 550px; }
`;
const dotAnimation = keyframes`
0% { content: '.'; }
33% { content: '..'; }
66% { content: '...'; }
100% { content: '.'; }
`;
// Стили
const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
display: flex;
flex-direction: column;
align-items: center;
justify-content:${({scanInitiated, loading})=>(scanInitiated && !loading ? 'start' : 'center')};
height: 100%;
width: 100%;
position: relative;
background: ${(props) => props.theme.background};
padding: 50px;
box-sizing: border-box;
gap: 24px;
transition: justify-content .4s ease;
overflow-y: auto;
overflow-x: hidden;
`;
const TitleWrapper = styled.h2`
position: absolute;
top: 24px;
left: 50%;
transform: translateX(-50%);
font-size: 32px;
`
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
width: 60px;
height: 60px;
border-radius: 50%;
position: relative;
transition: .4s ease;
transform: ${({scanInitiated,loading})=>(scanInitiated && !loading ? 'scale(0)' : 'scale(1)')};
span {
position: absolute;
display: inline-block;
background: ${(props) => props.theme.scannerColor};
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
&.center{
width: 60px;
height: 60px;
}
&.ring1 {
opacity: 0.12;
width: 250px;
height: 250px;
animation: ${({ loading }) => (loading ? ring1 : "none")} 4s linear infinite;
}
&.ring2 {
opacity: 0.08;
width: 400px;
height: 400px;
animation: ${({ loading }) => (loading ? ring2 : "none")} 4s linear infinite;
}
&.ring3 {
opacity: 0.04;
width: 550px;
height: 550px;
animation: ${({ loading }) => (loading ? ring3 : "none")} 4s linear infinite;
}
}
`;
const LoadingText = styled.div`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
`;
const ScanningText = styled.span`
&::after {
content: '.';
animation: ${dotAnimation} 1.5s infinite;
}
`;
const ScanBtn = styled.button<{loading: boolean}>`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
transition: .4s ease;
transform: ${({loading}) => (!loading ? "translateY(240px)" : "translateY(0)")};
&:hover{
transform: ${({loading}) => (!loading ? "translateY(240px)" : "translateY(0)")} scale(1.03);
}
`
const data = [
{
ssid: "Huawei",
bssid: "wef",
channel: 0,
signal:2,
encryption: "wef",
frequency: "ewf",
vendor: "striwefwng",
hidden: false
},
{
ssid: "Gu-2",
bssid: "grgerger",
channel: 0,
signal: 2,
encryption: "weergref",
frequency: "eergewf",
vendor: "grgregerge",
hidden: false
},
{
ssid: "Huawei_wifi2",
bssid: "wqdqw",
channel: 2,
signal: 3,
encryption: "wefwe",
frequency: "wef",
vendor: "fwefw",
hidden: false
},
];
export function WifiScanner() {
const {ipAddress} = useIpStore();
const [loading, setLoading] = useState(false);
const [scanInitiated, setScanInitiated] = useState(false);
const {wifiNetworks, setWifiNetworks} = useNetworkStore();
const [currentNetwork, setCurrentNetwork] = useState<WifiNetworkScanner | null>(null);
const handleScan = async () => {
try {
setLoading(true);
setScanInitiated(true);
const res = await NetworkService.scanWifi(ipAddress);
const networks = res.data.access_points || [];
setWifiNetworks(networks);
} catch (err) {
console.error("Ошибка сканирования сети:", err);
setWifiNetworks([]);
} finally {
setLoading(false);
}
};
return (
<Wrapper scanInitiated={scanInitiated} loading={loading}>
{(!scanInitiated || loading) && (
<TitleWrapper>Сканирование Wi-Fi сети</TitleWrapper>
)}
<UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center"></span>
<span className="ring1"></span>
<span className="ring2"></span>
<span className="ring3"></span>
{data.map((network) => (
<WifiDot
key={network.bssid}
network={network}
onSelect={setCurrentNetwork}
/>
))}
</UserLocate>
{currentNetwork && (
<ModalWifiScaner
onClose={() => setCurrentNetwork(null)}
network={currentNetwork}
/>
)}
{loading ? (
<LoadingText>
Сканирование сети <ScanningText/>
</LoadingText>
) : (
<ScanBtn onClick={handleScan} disabled={loading} loading={loading}>
{wifiNetworks.length > 0 ? 'Обновить список' : 'Сканировать сеть'}
</ScanBtn>
)}
</Wrapper>
);
}

View file

@ -0,0 +1,142 @@
import React from 'react';
import styled from 'styled-components';
import {WifiNetworkScanner} from "./WifiDot";
import {IconComponent} from "../../../../../../const/buttons/icons";
import {buttonNames} from "../../../../../../const/buttons/names";
const Modal = styled.div`
display: flex;
justify-content: center;
flex-direction: column;
background-color: ${(props) => props.theme.secondary};
border-radius: 24px;
padding: 20px 32px;
top: 14%;
right: 18%;
position: absolute;
z-index: 20;
min-width: 210px;
&:hover .close-btn{
opacity: 1;
visibility: visible;
}
`
const CloseBtn = styled.div`
position: absolute;
top: -20px;
right: -20px;
cursor: pointer;
background-color: white;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
transition: all .4s ease;
opacity: 0;
visibility: visible;
&:hover {
background-color: #d6d6d6;
}
`
const ModalTitle = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
font-size: 20px;
line-height: 1;
margin-bottom: 32px;
h4{
font-weight: normal;
}
`
const ModalContent = styled.div`
display: flex;
flex-direction: column;
gap: 10px;
justify-content: space-between;
align-items: center;
width: 100%;
`
const ModalRow = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
width: 100%;
font-size: 14px;
h6{
margin: 0;
padding: 0;
font-size: 1em;
}
`
const SignalBarsWrapper = styled.div`
display: flex;
align-items: flex-end;
gap: 2px;
height: 26px;
margin-left: 8px;
`;
const SignalBar = styled.div<{ active: boolean }>`
width: 9px;
background: ${({ active, theme }) => active ? theme.accent : '#ccc'};
border-radius: 2px;
&:nth-child(1) { height: 8px; }
&:nth-child(2) { height: 14px; }
&:nth-child(3) { height: 22px; }
`;
const ModalWifiScaner = ({ network,onClose }: { network: WifiNetworkScanner, onClose: ()=>void }) => {
return (
<Modal>
<CloseBtn className={'close-btn'} onClick={onClose}>
<IconComponent name={buttonNames[12]} width="30" height="30" />
</CloseBtn>
<ModalTitle>
<h4>{network.ssid}</h4>
<SignalBarsWrapper>
<SignalBar active={network.signal >= 1} />
<SignalBar active={network.signal >= 2} />
<SignalBar active={network.signal >= 3} />
</SignalBarsWrapper>
</ModalTitle>
<ModalContent>
<ModalRow>
<h6>BSSID:</h6>
<p>{network.bssid}</p>
</ModalRow>
<ModalRow>
<h6>Производитель:</h6>
<p>{network.vendor}</p>
</ModalRow>
<ModalRow>
<h6>Сигнал:</h6>
<p>{network.signal} dBm</p>
</ModalRow>
<ModalRow>
<h6>Канал:</h6>
<p>{network.channel}</p>
</ModalRow>
<ModalRow>
<h6>Шифрование:</h6>
<p>{network.encryption}</p>
</ModalRow>
<ModalRow>
<h6>Частота:</h6>
<p>{network.frequency}</p>
</ModalRow>
<ModalRow>
<h6>Скрытая сеть:</h6>
<p>{network.hidden ? 'Да' : 'Нет'}</p>
</ModalRow>
</ModalContent>
</Modal>
);
};
export default ModalWifiScaner;

View file

@ -0,0 +1,69 @@
import React from 'react';
import styled from "styled-components";
export interface WifiNetworkScanner {
ssid: string;
bssid: string;
channel: number;
signal: number;
encryption: string;
frequency: string;
vendor: string;
hidden: boolean;
}
//Точка вайфайл
const Dot = styled.div<{ top: string; left: string }>`
position: absolute;
top: ${props => props.top};
left: ${props => props.left};
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
transform: translate(-50%, -50%);
user-select: none;
transition: .4s;
text-wrap: nowrap;
&:hover {
transform: translate(-50%, -50%) scale(1.1);
}
`;
const WifiDot = ({ network, onSelect }: { network: WifiNetworkScanner, onSelect: (n: WifiNetworkScanner) => void }) => {
const getPositionInRing = (bssid: string, ringSize: number) => {
const hash = bssid.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
const randomAngle = (hash % 360) * (Math.PI / 180);
const innerRadius = ringSize * 0.4;
const outerRadius = ringSize * 0.5;
const randomValue = (hash % 1000) / 1000;
const randomRadius = Math.sqrt(innerRadius * innerRadius + randomValue * (outerRadius * outerRadius - innerRadius * innerRadius));
return {
x: Math.cos(randomAngle) * randomRadius,
y: Math.sin(randomAngle) * randomRadius
};
};
const getPosition = (signal: number, bssid: string) => {
const strength = Math.min(3, Math.max(1, signal));
const ringSizes = { 1: 550, 2: 400, 3: 250 };
const ringSize = ringSizes[strength as 1 | 2 | 3];
const offset = getPositionInRing(bssid, ringSize);
return {
top: `calc(50% - ${offset.y}px)`,
left: `calc(50% - ${offset.x}px)`
}
};
const position = getPosition(network.signal, network.bssid);
return (
<Dot top={position.top} left={position.left} onClick={() => onSelect(network)}>
{network.ssid}
</Dot>
);
};
export default WifiDot;

View file

@ -76,7 +76,7 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
setHasInited(false)
initializationAttempt.current = 0;
const newSocket = new WebSocket(`wss://${ws}:8001/api/v1`);
const newSocket = new WebSocket(`ws://${ws}:8001/api/v1`);
let connectionTimeout;
setSocket(newSocket);
@ -93,7 +93,7 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
newSocket.onmessage = (event) => {
const newData = JSON.parse(event.data);
setData(() => [
newData.activation,
newData.deactivation,
@ -126,7 +126,7 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
const activationData = data[0].interfaces;
const wifi = activationData.find((el: any) => el.type === 'WIFI');
setInterfaces(newInterfaces);
setEdgeData(newEdgeData);
if (wifi) { setWifi(wifi); }
@ -192,13 +192,13 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
}, [nodes, loader])
const onEdgesChange = useCallback(
(changes: any) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[setEdges],
(changes: any) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[setEdges],
);
const onConnect = useCallback(
(connection: any) => setEdges((eds) => addEdge(connection, eds)),
[setEdges],
(connection: any) => setEdges((eds) => addEdge(connection, eds)),
[setEdges],
);
const handleNodesChange = (changes: any) => {
@ -254,29 +254,29 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
}, [isNullPos]);
return (
<>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={handleNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
style={{ background: '#F7F9FB' }}
minZoom={0}
maxZoom={4}
fitView
fitViewOptions={{ padding: 0.1 }}
attributionPosition="bottom-left"
snapToGrid
snapGrid={[20, 20]}
panOnDrag={!isPanDisabled}
nodeTypes={nodeTypes}
proOptions={proOptions}
>
<Background gap={20} size={1} bgColor={theme.background} />
<Controls fitViewOptions={{ padding: 0.1 }} />
</ReactFlow>
</>
<>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={handleNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
style={{ background: '#F7F9FB' }}
minZoom={0}
maxZoom={4}
fitView
fitViewOptions={{ padding: 0.1 }}
attributionPosition="bottom-left"
snapToGrid
snapGrid={[20, 20]}
panOnDrag={!isPanDisabled}
nodeTypes={nodeTypes}
proOptions={proOptions}
>
<Background gap={20} size={1} bgColor={theme.background} />
<Controls fitViewOptions={{ padding: 0.1 }} />
</ReactFlow>
</>
);
};

View file

@ -41,8 +41,8 @@ export const ToolBar = () => {
const toolBarMenu = toolBar?.toolSpace
const { openModal } = useModalContext()
const {isAdmin} = useAdminStore()
const {setWorkModule} = useWorkSpaceStore()
const { isAdmin } = useAdminStore()
const { setWorkModule, workModule } = useWorkSpaceStore()
return (
<Container>
@ -50,13 +50,22 @@ export const ToolBar = () => {
{toolBarMenu && toolBarMenu.map((menu) => (
<ToolBarButton menuId={menu.id} key={menu.id} />
))}
<Button>
<IconContainer>
<IconComponent name={buttonNames[14]} width="30" height="30" onClick={() => setWorkModule(workSpaceNames[5])} />
</IconContainer>
</Button>
<Button>
<IconContainer>
<IconComponent name={buttonNames[14]} width="30" height="30" onClick={() => setWorkModule(workSpaceNames[6])} />
</IconContainer>
</Button>
</ButtonContainer>
<ButtonContainer>
<Button>
<IconContainer>
<IconComponent name={isAdmin ? buttonNames[10] : buttonNames[13]} width="30" height="30" onClick={() => openModal(ModalMap['5'])}/>
<IconComponent name={isAdmin ? buttonNames[10] : buttonNames[13]} width="30" height="30" onClick={() => openModal(ModalMap['5'])} />
</IconContainer>
</Button>
@ -64,7 +73,12 @@ export const ToolBar = () => {
<Button>
<IconContainer>
<IconComponent name={buttonNames[4]} width="30" height="30" onClick={()=>setWorkModule(workSpaceNames[2])}/>
<IconComponent name={buttonNames[4]} width="30" height="30"
onClick={() => workModule === workSpaceNames[3]
? setWorkModule(workSpaceNames[2])
: setWorkModule(workSpaceNames[4])
}
/>
</IconContainer>
</Button>
</ButtonContainer>

View file

@ -18,7 +18,7 @@ const Container = styled.div`
align-items: center;
gap: 15px;
padding: 20px;
width: 210px;
p{
margin: 0;
}
@ -115,12 +115,12 @@ export const DevicesBar = () => {
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
return new Promise((resolve) => {
const testSocket = new WebSocket(`wss://${ip}:8001/api/v1`);
const testSocket = new WebSocket(`ws://${ip}:8001/api/v1`);
const timeout = setTimeout(() => {
testSocket.close();
resolve(false);
}, 500
);
testSocket.close();
resolve(false);
}, 500
);
testSocket.onopen = () => {
clearTimeout(timeout);
@ -156,8 +156,8 @@ export const DevicesBar = () => {
};
const handleAddDeviceComplete = (data: string) => {
addDevice(data)
setIpAddress(data)
addDevice(data);
switchWs(data);
};
const handleDelete = (number: number, adress: string) => {
@ -170,7 +170,6 @@ export const DevicesBar = () => {
};
const switchWs = async (ip: string) => {
// Проверяем доступность WebSocket
const isAvailable = await checkWebSocketAvailability(ip);
if (!isAvailable) {
openModal(ModalMap['3'], {
@ -179,7 +178,6 @@ export const DevicesBar = () => {
return;
}
// Если WebSocket доступен, обновляем IP и переключаем модуль
setIpAddress(ip);
setWorkModule(workSpaceNames[3]);
};
@ -198,12 +196,12 @@ export const DevicesBar = () => {
<DeviceBlock>
<DeviceControl className="unselectable" onClick={() => switchWs(currentDevice.address)}>
<p>Текущее устройство</p>
<p>Корневой адрес</p>
</DeviceControl>
{currentDevice.hostname == '' ? (
<Text className="paddingP">Адрес: Загрузка<AnimDot /></Text>
<Text className="paddingP">Название: Загрузка<AnimDot /></Text>
) : (
<p className="paddingP">Адрес: {currentDevice.hostname}</p>
<p className="paddingP">Название: {currentDevice.hostname}</p>
)}
</DeviceBlock>

View file

@ -3,11 +3,15 @@ import { useToolBar } from "../../../../hooks/useToolBar"
import { Space } from "../../../../const/menus/sideBar/components"
import styled from "styled-components"
const Container = styled.div`
const Container = styled.div<{isOpen: boolean}>`
background-color: ${(props) => props.theme.secondary};
width: 250px;
font-size: 14px;
flex: 3;
overflow: hidden;
max-width: ${({isOpen})=>(isOpen ? '250px' : '0')};
transition: max-width .4s ease;
`
export const ToolSpace = () => {
@ -15,12 +19,12 @@ export const ToolSpace = () => {
const name = toolBar?.current || ''
const { isActive, isOpen } = useToolBar(name)
if (!isActive || !isOpen) {
if (!isActive) {
return null
}
return (
<Container>
<Container isOpen={isOpen}>
<Space name={name} />
</Container>
)

View file

@ -8,6 +8,9 @@ import { useWorkSpaceStore } from "../../store";
import { workSpaceNames } from "../../const/workspace/names";
import { Header } from "./modules/appModules/header";
import { ReactFlowProvider } from "@xyflow/react";
import About from "./modules/appModules/about";
import UserScanner from "./modules/appModules/usersScanner/userScanner";
import {WifiScanner} from "./modules/appModules/wifiScanner/WifiScanner";
const Section = styled.div`
display: flex;
@ -54,16 +57,32 @@ export default function WorkSpace() {
setIpAddress(currentWs);
}, [currentWs]);
console.log('aboba');
localStorage.getItem('workSpace-storage')
return (
<Section>
<FlowContainer>
<SideBar />
{workModule === workSpaceNames[1] && <HelloWindow /> }
{workModule === workSpaceNames[1] && <HelloWindow />}
{workModule === workSpaceNames[2] && (
<About
from={"workSpace"}
/>
)}
{workModule === workSpaceNames[4] && (
<About
from={"helloWindow"}
/>
)}
{workModule === workSpaceNames[5] && (
<UserScanner />
)}
{workModule === workSpaceNames[6] && (
<WifiScanner />
)}
{workModule === workSpaceNames[3] &&
<WorkSection>
<Header />

View file

@ -12,6 +12,7 @@ import arrowRight from '@iconify/icons-ic/baseline-keyboard-arrow-right';
import edit from '@iconify/icons-ic/baseline-edit';
import checkCircle from '@iconify/icons-ic/round-check-circle';
import cancel from '@iconify/icons-ic/baseline-cancel';
import userSearch from '@iconify/icons-ic/baseline-person-search';
import admin from '@iconify/icons-ic/baseline-admin-panel-settings';
import noAdmin from '@iconify/icons-ic/baseline-remove-moderator'
import refresh from '@iconify/icons-ic/baseline-refresh';
@ -30,8 +31,8 @@ export const ICONS = {
[buttonNames[10]]: admin,
[buttonNames[11]]: refresh,
[buttonNames[12]]: close,
[buttonNames[13]]: noAdmin
[buttonNames[13]]: noAdmin,
[buttonNames[14]]: userSearch,
} as const;
type IconName = keyof typeof ICONS;

View file

@ -13,5 +13,6 @@ export const buttonNames: names ={
10: 'admin',
11: 'reset',
12: 'close',
13: 'noAdmin'
13: 'noAdmin',
14: 'userSearch'
}

View file

@ -1,5 +1,7 @@
const baseTheme = {
accent: '#007ACC',
successColor: "#4CAf50",
errorColor: "#F44336",
accentHover: '#4A90E2',
}
@ -9,6 +11,7 @@ export const light = {
secondary: '#EDEDED',
background: '#E8E8E8',
textColor: '#000000',
scannerColor: '#007ACC',
};
export const dark = {
@ -17,4 +20,5 @@ export const dark = {
secondary: '#252525',
background: '#3A3A3A',
textColor: '#FFFFFF',
scannerColor: '#0E9C9D',
};

View file

@ -2,6 +2,9 @@ import { names } from "../types/types"
export const workSpaceNames: names ={
1: 'helloWindow',
2: 'aboutApp',
3: 'networkInterfaces'
2: 'aboutFromWork',
3: 'networkInterfaces',
4: 'aboutFromHello',
5: 'userScanner',
6: 'wifiScanner',
}

View file

@ -16,7 +16,6 @@ export default function Router() {
try {
let response = await NetworkService.checkServerConf(ipAddress);
if(response.status === 200) {
console.log(response.data.message);
setConfigExists(response.data.has_users);
}
} catch (error) {

View file

@ -9,7 +9,7 @@ interface IpAddressState {
export const useIpStore = create<IpAddressState>()(
persist(
(set)=>({
ipAddress: 'localhost',
ipAddress: '192:168.1.152',
setIpAddress: (newIp: string) => set({ ipAddress: newIp }),
}),
{

View file

@ -0,0 +1,49 @@
import { create } from 'zustand';
export interface DeviceUser {
ip: string;
mac: string;
hostname: string;
vendor: string;
open_ports: string[];
status: string;
last_seen: string;
}
export interface WifiNetworkScanner {
ssid: string;
bssid: string;
channel: number;
signal: number;
encryption: string;
frequency: string;
vendor: string;
hidden: boolean;
}
interface NetworkStore {
devices: DeviceUser[];
setDevices: (devices: DeviceUser[]) => void;
addDevice: (device: DeviceUser) => void;
wifiNetworks: WifiNetworkScanner[];
setWifiNetworks: (networks: WifiNetworkScanner[]) => void;
addWifiNetwork: (network: WifiNetworkScanner) => void;
clearAll: () => void;
}
const useNetworkStore = create<NetworkStore>((set) => ({
devices: [],
setDevices: (devices) => set({ devices }),
addDevice: (device) =>
set((state) => ({ devices: [...state.devices, device] })),
wifiNetworks: [],
setWifiNetworks: (networks) => set({ wifiNetworks: networks }),
addWifiNetwork: (network) =>
set((state) => ({ wifiNetworks: [...state.wifiNetworks, network] })),
clearAll: () => set({ devices: [], wifiNetworks: [] })
}));
export default useNetworkStore;

View file

@ -1,6 +1,5 @@
import { useIpStore } from "../store";
import { useAdminStore } from "../store/adminStore";
import { useEffect } from "react";
import { NetworkService } from "../api/request";
export const useCheckAdmin = () => {