This commit is contained in:
parent
6f7b821c8b
commit
3a5b0bdcbc
162 changed files with 15801 additions and 224 deletions
137
FRONT/DEV/src/components/UI/Dropdown/DeviceCard.tsx
Normal file
137
FRONT/DEV/src/components/UI/Dropdown/DeviceCard.tsx
Normal 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;
|
||||
130
FRONT/DEV/src/components/UI/Dropdown/DropdownType.tsx
Normal file
130
FRONT/DEV/src/components/UI/Dropdown/DropdownType.tsx
Normal 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;
|
||||
65
FRONT/DEV/src/components/UI/Pagination.tsx
Normal file
65
FRONT/DEV/src/components/UI/Pagination.tsx
Normal 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;
|
||||
29
FRONT/DEV/src/components/UI/animDot.tsx
Normal file
29
FRONT/DEV/src/components/UI/animDot.tsx
Normal 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>
|
||||
);
|
||||
75
FRONT/DEV/src/components/UI/buttons/mainButton.tsx
Normal file
75
FRONT/DEV/src/components/UI/buttons/mainButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
45
FRONT/DEV/src/components/UI/buttons/quitButton.tsx
Normal file
45
FRONT/DEV/src/components/UI/buttons/quitButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
26
FRONT/DEV/src/components/UI/buttons/toggletheme.tsx
Normal file
26
FRONT/DEV/src/components/UI/buttons/toggletheme.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
65
FRONT/DEV/src/components/UI/inputs/input.tsx
Normal file
65
FRONT/DEV/src/components/UI/inputs/input.tsx
Normal 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;
|
||||
101
FRONT/DEV/src/components/UI/loader.tsx
Normal file
101
FRONT/DEV/src/components/UI/loader.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
103
FRONT/DEV/src/components/UI/routerLoader.tsx
Normal file
103
FRONT/DEV/src/components/UI/routerLoader.tsx
Normal 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>
|
||||
|
||||
)
|
||||
}
|
||||
63
FRONT/DEV/src/components/UI/signalSvg.tsx
Normal file
63
FRONT/DEV/src/components/UI/signalSvg.tsx
Normal 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;
|
||||
137
FRONT/DEV/src/components/UI/tables/TableScanner.tsx
Normal file
137
FRONT/DEV/src/components/UI/tables/TableScanner.tsx
Normal 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue