front/dev
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
parent 6f7b821c8b
commit 3a5b0bdcbc
162 changed files with 15801 additions and 224 deletions

View file

@ -0,0 +1,137 @@
import React, {useState} from 'react';
import styled, { css } from 'styled-components';
import { DeviceUser } from "../../../store/useDeviceStore";
import {buttonNames} from "../../../const/buttons/names";
import {ModalMap} from "../../modalwindows/windowsType";
import {IconComponent} from "../../../const/buttons/icons";
import {useModalContext} from "../../../store";
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.offlineColor};
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] = useState(false);
const toggleExpanded = () => setIsExpanded(!isExpanded);
const { openModal } = useModalContext();
const openModalNote = (e: React.MouseEvent) => {
e.stopPropagation();
openModal(ModalMap['6'], {
address: device.mac,
currentNote: device.note || ''
});
};
return (
<CardWrapper status={status} onClick={toggleExpanded}>
<CardHeader status={status}>
<p>
MAC: {device.mac}
</p>
<span onClick={openModalNote}>
<IconComponent name={buttonNames[16]} width="28" height="28" setColor={'white'} />
</span>
</CardHeader>
<CardContent isExpanded={isExpanded}>
<InfoRow>
<Label>Имя:</Label>
<Value>{device.hostname || device.ip}</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>
<InfoRow>
<Label>Заметка:</Label>
<Value>{device.note || '-'}</Value>
</InfoRow>
</CardContent>
</CardWrapper>
);
};
export default DeviceCard;

View file

@ -0,0 +1,130 @@
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%;
color: ${({theme}) => theme.textColor};
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,65 @@
import React from 'react';
import styled from "styled-components";
import {buttonNames} from "../../const/buttons/names";
import {IconComponent} from "../../const/buttons/icons";
const PaginateWrapper = styled.div`
width: 100%;
margin-top: 24px;
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
`
const Page = styled.button`
width: 36px;
height: 36px;
background: ${(props => props.theme.primary)};
color: ${(props => props.theme.textColor)};
border: 1px solid ${(props => props.theme.borderColor)};
font-size: 16px;
cursor: pointer;
border-radius: 4px;
transition: .4s ease;
&:hover {
background: ${(props => props.theme.background)};
}
`
interface PaginationProps {
currentPage: number;
totalPages: number;
onPrevPage: () => void;
onNextPage: () => void;
}
const Pagination = ({currentPage, totalPages, onPrevPage, onNextPage}: PaginationProps) => {
return (
<PaginateWrapper>
<Page
onClick={onPrevPage}
disabled={currentPage === 1}
>
<IconComponent
width="20"
height="20"
name={buttonNames[17]}
/>
</Page>
<Page
onClick={onNextPage}
disabled={currentPage === totalPages}
>
<IconComponent
width="20"
height="20"
name={buttonNames[6]}
/>
</Page>
</PaginateWrapper>
);
};
export default Pagination;

View file

@ -0,0 +1,29 @@
import styled, { keyframes } from "styled-components";
const blink = keyframes`
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
`;
interface DotProps {
$delay: number;
}
const Dot = styled.span<DotProps>`
animation: ${blink} 1s infinite;
animation-delay: ${({ $delay }) => $delay}s;
margin: 0;
`;
const Wrapper = styled.div`
display: flex;
gap: 4px;
`;
export const AnimDot = () => (
<Wrapper>
<Dot $delay={0}>.</Dot>
<Dot $delay={0.2}>.</Dot>
<Dot $delay={0.4}>.</Dot>
</Wrapper>
);

View file

@ -0,0 +1,75 @@
import styled from "styled-components";
import { useTheme } from "styled-components";
import { IconComponent } from "../../../const/buttons/icons";
const Button = styled.button.withConfig({
shouldForwardProp: (prop) => !['color', '$hoverсolor', 'size', '$iconpos'].includes(prop),
})<{ color: string; size?: string, $hoverсolor: string, $iconpos: string }>`
background-color: ${(props) => props.color};
border: none;
padding: ${(props) =>
props.size === "small"
? "10px 28px"
: props.size === "big"
? "10px 20px"
: "12px 20px"}; // По умолчанию
border-radius: 8px;
color: white;
font-size: ${(props) =>
props.size === "small"
? "14px"
: props.size === "big"
? "16px"
: "14px"}; // По умолчанию
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
flex-direction: ${(props) =>
props.$iconpos === 'right'
? 'row-reverse'
: 'row'};
gap: 10px;
transition: all .3s ease;
&:hover{
background-color: ${(props) => props.$hoverсolor};
}
`;
type Props = {
color?: string;
hoverColor?: string
size?: "small" | "big";
title: string
onClick?: () => void;
type?: "button" | "submit" | "reset" | undefined
icon?: string
iconPosition?: 'left' | 'right'
iconSize?: {
width: string,
height: string
}
style?: any
};
export const MainButton = ({ color, size, title, hoverColor, type, icon, iconPosition, iconSize, onClick, style }: Props) => {
const theme = useTheme();
const defaultColor: string = theme.accent;
const defaultHoverColor: string = theme.accentHover
return (
<Button
className="unselectable"
color={color || defaultColor} size={size}
$hoverсolor={hoverColor || defaultHoverColor}
onClick={onClick}
type={type}
$iconpos={iconPosition || 'left'}
style={style}
>
{icon && <IconComponent name={icon} width={iconSize?.width} height={iconSize?.height} setColor="white"/> }
{title}
</Button>
);
};

View file

@ -0,0 +1,45 @@
import { MainButton } from "./mainButton";
import styled from "styled-components";
import { buttonNames } from "../../../const/buttons/names";
import { IconComponent as Icon } from '../../../const/buttons/icons';
import { useWorkSpaceStore } from "../../../store";
type Props = {
size: "small" | "big";
};
const Container = styled.div<{ size: "small" | "big" }>`
${(props) => props.size === "small" && "width: 100%;"}
`;
const Button = styled.div`
width: 100%;
cursor: pointer;
color: ${(props) => props.theme.textColor};
`;
const IconContainer = styled.div`
padding: 10px;
`;
export const QuitButton = ({ size }: Props) => {
const handleExit = () => {
window.electron.quitApp();
};
const { setWorkModule } = useWorkSpaceStore()
return (
<Container size={size} onClick={()=>setWorkModule('helloWindow')}>
{size === "big" ? (
<MainButton title="Выйти" size="big" />
) : (
<Button>
<IconContainer>
<Icon name={buttonNames[12]} width="30" height="30"/>
</IconContainer>
</Button>
)}
</Container>
);
};

View file

@ -0,0 +1,26 @@
import styled, { useTheme } from "styled-components";
import { IconComponent as Icon } from '../../../const/theme/icons';
import { ThemName } from "../../../const/theme/names";
const Button = styled.div`
width: 100%;
cursor: pointer;
`
const IconContainer = styled.div`
padding: 10px;
color: ${props => props.theme.text};
`
export default function ThemeButton() {
const theme = useTheme();
const name = localStorage.getItem('theme') as ThemName
return (
<Button onClick={theme.toggleTheme}>
<IconContainer>
<Icon name={name} width="30" height="30"/>
</IconContainer>
</Button>
);
}

View file

@ -0,0 +1,65 @@
import React from 'react';
import styled from 'styled-components';
type Props = {
placeholderText: string;
id: string;
value?: string;
onChange?: (value: string) => void;
onFileChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
type?: string;
multiple?: boolean;
style?: React.CSSProperties;
search?: boolean;
onFocus?: any
onBlur?: any
$size?: string
}
const InputContainer = styled.div`
width: 100%;
display: flex;
flex: 3;
`;
const StyledInput = styled.input`
padding: 8px 10px;
color: black;
border-radius: 8px;
border: 1px solid #A1A1A1;
background-color: transparent;
&:focus{
border: 1px solid #adadad;
}
width: 100%;
font-size: 18px;
color: ${props=>props.theme.textColor}
`;
const Input = React.forwardRef<HTMLInputElement, Props>(({ placeholderText, id, value, onChange, onFileChange, type = "text", multiple, style, onFocus, onBlur }, ref) => {
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
if (onChange) {
onChange(value);
}
};
return (
<InputContainer>
<StyledInput
type={type}
placeholder={placeholderText}
value={value}
multiple={multiple}
onChange={type === "file" && onFileChange ? onFileChange : handleInputChange}
ref={ref}
style={style}
onFocus={onFocus}
onBlur={onBlur}
/>
</InputContainer>
);
});
export default Input;

View file

@ -0,0 +1,101 @@
type Props = {
loaderText: string; //Тип передаваемых данных loaderText - string
}
import styled, { keyframes } from 'styled-components'
import { AnimDot } from './animDot';
//Константа которая устанавливает кейфвреймы для вращения
const spinAnimation = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
//Стиль экрана загрузки
export const LoaderContainer = styled.div`
top: 0px;
z-index:998;
background-color: rgba(0,0,0, 0.7);
position: fixed;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
overflow-y: hidden;
`
//Стиль блока с элементами загрузки
export const Loaderblock = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
p{
font-size: 24px;
color: white;
}
`
//Стиль для блока с кольцом загрузки
export const LoadingSpinner = styled.div`
position: relative;
width: 65px;
height: 65px;
`
//Стиль для кольца загрузки
export const LoadingRing = styled.div`
position: absolute;
top: 0;
left: 0;
width: 65px;
height: 65px;
border-radius: 50%;
border: 12px solid white;
`
//Стиль для части кольца загрузки, которая будет вращаться
export const LoadingPart = styled.div`
position: absolute;
top: 0;
left: 0;
width: 65px;
height: 65px;
border-radius: 50%;
border: 12px solid transparent;
border-top: 12px solid ${props => props.theme.accent};
// Добавление аниманции и использование кейфремов
animation: ${spinAnimation} 1s linear infinite;
`
const Text = styled.div`
display: flex;
gap: 3px;
font-size: 24px;
color: white;
margin-top: 30px;
margin-left: 25px;
`
export default function Loader({ loaderText }: Props) {
return (
<Loaderblock>
<LoadingSpinner>
<LoadingRing />
<LoadingPart />
</LoadingSpinner>
{/* Текст экрана загрузки устанавливается с помощью передаваемых данных loaderText */}
<Text>{loaderText}<AnimDot/></Text>
</Loaderblock>
)
}

View file

@ -0,0 +1,103 @@
type Props = {
loaderText: string; //Тип передаваемых данных loaderText - string
}
import styled, { keyframes } from 'styled-components'
import { AnimDot } from './animDot';
import { ModalWindow } from '../modalwindows/style';
//Константа которая устанавливает кейфвреймы для вращения
const spinAnimation = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
//Стиль экрана загрузки
export const LoaderContainer = styled.div`
top: 0px;
z-index:998;
background-color: rgba(0,0,0, 0.7);
position: fixed;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
overflow-y: hidden;
`
//Стиль блока с элементами загрузки
export const Loaderblock = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
p{
font-size: 24px;
color: white;
}
`
//Стиль для блока с кольцом загрузки
export const LoadingSpinner = styled.div`
position: relative;
width: 65px;
height: 65px;
`
//Стиль для кольца загрузки
export const LoadingRing = styled.div`
position: absolute;
top: 0;
left: 0;
width: 65px;
height: 65px;
border-radius: 50%;
border: 12px solid white;
`
//Стиль для части кольца загрузки, которая будет вращаться
export const LoadingPart = styled.div`
position: absolute;
top: 0;
left: 0;
width: 65px;
height: 65px;
border-radius: 50%;
border: 12px solid transparent;
border-top: 12px solid ${props => props.theme.accent};
// Добавление аниманции и использование кейфремов
animation: ${spinAnimation} 1s linear infinite;
`
const Text = styled.div`
display: flex;
gap: 3px;
font-size: 24px;
color: white;
margin-top: 30px;
margin-left: 25px;
`
export default function RouterLoader({ loaderText }: Props) {
return (
<ModalWindow>
<Loaderblock>
<LoadingSpinner>
<LoadingRing />
<LoadingPart />
</LoadingSpinner>
{/* Текст экрана загрузки устанавливается с помощью передаваемых данных loaderText */}
<Text>{loaderText}<AnimDot/></Text>
</Loaderblock>
</ModalWindow>
)
}

View file

@ -0,0 +1,63 @@
import React, { useEffect, useRef } from 'react';
interface SignalSVGProps {
signal: number; // Принимаем signal как пропс
color: string;
size: string;
}
const SignalSVG: React.FC<SignalSVGProps> = ({ signal, color, size }) => {
const svgRef = useRef<SVGSVGElement | null>(null);
useEffect(() => {
// Устанавливаем цвета в зависимости от signal
const colors = {
'--st1-color': signal >= 1 ? color : 'black',
'--st2-color': signal >= 2 ? color : 'black',
'--st3-color': signal >= 3 ? color : 'black',
'--st4-color': signal >= 4 ? color : 'black',
'--st5-color': signal === 5 ? color : 'black',
};
// Применяем стили к SVG
if (svgRef.current) {
Object.entries(colors).forEach(([key, value]) => {
svgRef.current?.style.setProperty(key, value);
});
}
}, [signal]);
return (
<svg
ref={svgRef}
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 296 235"
xmlSpace="preserve"
width={size} // Фиксированная ширина
height={size}
>
<style type="text/css">
{`
.st1 { fill-rule: evenodd; clip-rule: evenodd; fill: var(--st1-color, white); }
.st2 { fill-rule: evenodd; clip-rule: evenodd; fill: var(--st2-color, white); }
.st3 { fill-rule: evenodd; clip-rule: evenodd; fill: var(--st3-color, white); }
.st4 { fill-rule: evenodd; clip-rule: evenodd; fill: var(--st4-color, white); }
.st5 { fill-rule: evenodd; clip-rule: evenodd; fill: var(--st5-color, white); }
`}
</style>
<g>
<polygon className="st1" points="47,138 47,188 0,188 0,164" />
<polygon className="st2" points="109,104 109,188 62,188 62,130" />
<polygon className="st3" points="171,69 171,188 124,188 124,95" />
<polygon className="st4" points="234,35 234,188 187,188 187,61" />
<polygon className="st5" points="296,0 296,188 249,188 249,26" />
</g>
</svg>
);
};
export default SignalSVG;

View file

@ -0,0 +1,137 @@
import React, {useState} from 'react';
import { DeviceUser } from '../../../store/useDeviceStore';
interface TableScannerProps {
devices: DeviceUser[];
}
import styled from 'styled-components';
import {IconComponent} from "../../../const/buttons/icons";
import {buttonNames} from "../../../const/buttons/names";
import {ModalMap} from "../../modalwindows/windowsType";
import {useModalContext} from "../../../store";
import Pagination from "../Pagination";
const TableContainer = styled.div`
width: 100%;
margin: 24px 0;
`;
const StyledTable = styled.table`
width: 100%;
border-collapse: separate;
border-spacing: 0;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
background: ${({ theme }) => theme.primary};
`;
const TableHeaderCell = styled.th<{ sortable?: boolean }>`
padding: 16px 24px;
text-align: left;
font-weight: 600;
font-size: 14px;
color: ${({ theme }) => theme.textColor};
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;
button{
background-color: transparent;
border: none;
cursor: pointer;
}
`;
const StatusBadge = styled.span<{ status:string }>`
width: 10px;
height: 30px;
background-color: ${({theme, status}) => (status==='online' ? theme.successColor : theme.offlineColor)};
display: inline-block;
border-radius:4px;
`
const TableScanner = ({devices}:TableScannerProps) => {
const { openModal } = useModalContext();
const openModalNote = (e: React.MouseEvent, device:DeviceUser ) => {
e.stopPropagation();
openModal(ModalMap['6'], {
address: device.mac,
currentNote: device.note || ''
});
};
// Пагинация
const [currentPage, setCurrentPage] = useState(1);
const [devicesPerPage] = useState(10);
const indexOfLastDevice = currentPage * devicesPerPage;
const indexOfFirstDevice = indexOfLastDevice - devicesPerPage;
const currentDevices = devices.slice(indexOfFirstDevice, indexOfLastDevice);
const totalPages = Math.ceil(devices.length / devicesPerPage);
const handlePrevPage = () => {
if(currentPage > 1) setCurrentPage(currentPage - 1);
}
const handleNextPage = () => {
if(currentPage < totalPages) setCurrentPage(currentPage + 1);
}
return (
<TableContainer>
<StyledTable>
<thead>
<tr>
<TableHeaderCell></TableHeaderCell>
<TableHeaderCell>Имя</TableHeaderCell>
<TableHeaderCell>Производитель</TableHeaderCell>
<TableHeaderCell>IP адрес</TableHeaderCell>
<TableHeaderCell>Открытые порты</TableHeaderCell>
<TableHeaderCell>Последняя активность</TableHeaderCell>
<TableHeaderCell>Заметка</TableHeaderCell>
<TableHeaderCell></TableHeaderCell>
</tr>
</thead>
<tbody>
{currentDevices.map((item, i) => (
<TableRow key={i}>
<TableCell><StatusBadge status={item.status}></StatusBadge></TableCell>
<TableCell>{item.hostname || item.ip}</TableCell>
<TableCell>{item.vendor}</TableCell>
<TableCell>{item.ip}</TableCell>
<TableCell>{item.open_ports.join(', ') || '-'}</TableCell>
<TableCell>{item.last_seen}</TableCell>
<TableCell>{item.note || '-'}</TableCell>
<TableCell>
<button onClick={(e)=>openModalNote(e, item)}>
<IconComponent name={buttonNames[16]} width="26" height="26" />
</button>
</TableCell>
</TableRow>
))}
</tbody>
</StyledTable>
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPrevPage={handlePrevPage}
onNextPage={handleNextPage}
/>
</TableContainer>
);
};
export default TableScanner;