From e0fa98f21abb3b971f666ad9660f85caa8fd2e2f Mon Sep 17 00:00:00 2001 From: Denis Shatskiy Date: Wed, 9 Jul 2025 10:36:44 +0700 Subject: [PATCH] feat: Add StatCard component to Dashboard and implement sorting, filtering, and pagination in DataManager --- frontend/src/App.css | 6 - frontend/src/App.jsx | 50 +++++++- frontend/src/DataManager.jsx | 229 +++++++++++++++++++++-------------- 3 files changed, 187 insertions(+), 98 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index 5ceeec8..86c8979 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -23,12 +23,6 @@ } /* Custom hover effects */ -.card:hover { - transform: translateY(-2px); - box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); - transition: all 0.3s ease; -} - .btn:hover { transform: translateY(-1px); transition: all 0.2s ease; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7d79e04..ebb3c2e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -12,7 +12,10 @@ import { IconActivity, IconBell, IconChevronDown, - IconUser + IconUser, + IconDatabase, + IconAlertTriangle, + IconAlertCircle } from '@tabler/icons-react'; import DataManager from './DataManager'; import './App.css'; @@ -151,9 +154,54 @@ function App() { ); } +function StatCard({ icon: Icon, color, value, title, subtitle }) { + return ( +
+
+ + + +
+
{value} {title}
+
{subtitle}
+
+
+
+ ); +} + function Dashboard() { return (
+
+
+ +
+
+ +
+
+ +
+
diff --git a/frontend/src/DataManager.jsx b/frontend/src/DataManager.jsx index 10b0558..e45d134 100644 --- a/frontend/src/DataManager.jsx +++ b/frontend/src/DataManager.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import axios from 'axios'; import { IconPlus, @@ -30,6 +30,12 @@ function DataManager({ entityName, entityKey, placeholder }) { const [loading, setLoading] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [itemToDelete, setItemToDelete] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + const [sortField, setSortField] = useState('domain'); + const [sortOrder, setSortOrder] = useState('asc'); + const [filterType, setFilterType] = useState(''); + const editInputRef = useRef(null); + const pageSize = 10; useEffect(() => { fetchItems(); @@ -122,10 +128,56 @@ function DataManager({ entityName, entityKey, placeholder }) { } }; - const filteredItems = items.filter(i => + // Сортировка + const sortedItems = [...items].sort((a, b) => { + let valA = a[sortField] || ''; + let valB = b[sortField] || ''; + if (typeof valA === 'string') valA = valA.toLowerCase(); + if (typeof valB === 'string') valB = valB.toLowerCase(); + if (valA < valB) return sortOrder === 'asc' ? -1 : 1; + if (valA > valB) return sortOrder === 'asc' ? 1 : -1; + return 0; + }); + + // Фильтрация по шлюзу + const filteredByType = filterType + ? sortedItems.filter(i => i.type === filterType) + : sortedItems; + + // Поиск + const filteredItems = filteredByType.filter(i => i.domain.toLowerCase().includes(searchTerm.toLowerCase()) ); + // Пагинация + const totalPages = Math.ceil(filteredItems.length / pageSize); + const paginatedItems = filteredItems.slice((currentPage - 1) * pageSize, currentPage * pageSize); + + // Для фильтра - список всех уникальных шлюзов + const allTypes = Array.from(new Set(items.map(i => i.type))); + + // Сортировка по клику + const handleSort = (field) => { + if (sortField === field) { + setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); + } else { + setSortField(field); + setSortOrder('asc'); + } + }; + + // Улучшенный инлайн-редакт + useEffect(() => { + if (editingDomain && editInputRef.current) { + editInputRef.current.focus(); + } + }, [editingDomain]); + + const handleEditKeyDown = (e, domainName) => { + if (e.key === 'Enter') handleSaveEdit(domainName); + if (e.key === 'Escape') handleCancelEdit(); + }; + return ( <> {error && ( @@ -278,110 +330,105 @@ function DataManager({ entityName, entityKey, placeholder }) {
- {/* Search and Table Card */}
-
-

Список {entityName}

-
-
- +
+

Список {entityName === 'домен' ? 'доменов' : entityName}

+
+ {/* Фильтр по шлюзу */} + + {/* Поиск */} +
+ + + setSearchTerm(e.target.value)} + onChange={e => { setSearchTerm(e.target.value); setCurrentPage(1); }} />
-
-
- - - - - - - - - - {filteredItems.map((item, index) => ( - - + + ))} + +
{entityName}Шлюз
- {editingDomain === item.domain ? ( +
+ + + + + + + + + + {paginatedItems.map((item) => ( + + + + - - - - ))} - -
handleSort('domain')}> + Домен + {sortField === 'domain' && ( + + {sortOrder === 'asc' ? '▲' : '▼'} + + )} + handleSort('type')}> + Шлюз + {sortField === 'type' && ( + + {sortOrder === 'asc' ? '▲' : '▼'} + + )} +
{item.domain}{item.type} + {editingDomain === item.domain ? ( + <> setEditingDomain(e.target.value)} - /> - ) : ( - item.domain - )} - - {editingDomain === item.domain ? ( - setEditingValue(e.target.value)} + ref={editInputRef} + onChange={e => setEditingValue(e.target.value)} + onKeyDown={e => handleEditKeyDown(e, item.domain)} + style={{maxWidth: 120}} /> - ) : ( - {item.type} - )} - - {editingDomain === item.domain ? ( -
- - -
- ) : ( -
- - -
- )} -
-
- {filteredItems.length === 0 && ( -
-
- -
-

Нет {entityName} для отображения

-

- Добавьте первый {entityName} используя форму слева. -

-
- )} + + + + ) : ( + <> + + + + )} +
+ {/* Пагинация */} + {totalPages > 1 && ( +
+
    +
  • + +
  • + {Array.from({length: totalPages}, (_, i) => i + 1).map(page => ( +
  • + +
  • + ))} +
  • + +
  • +
+
+ )}