From 7460577423b1a5e14d258f3f3eadee354e011179 Mon Sep 17 00:00:00 2001 From: Artur Oliveira Date: Tue, 16 Dec 2025 18:16:22 -0300 Subject: [PATCH] =?UTF-8?q?feat(pagination):=20paginar=20listagem=20de=20s?= =?UTF-8?q?ervidores=20-=20adiciona=20DTO=20de=20p=C3=A1gina=20e=20pagina?= =?UTF-8?q?=C3=A7=C3=A3o=20no=20endpoint=20GET=20/api/servers=20-=20aplica?= =?UTF-8?q?=20busca=20paginada=20no=20service/reposit=C3=B3rio=20com=20lim?= =?UTF-8?q?ites=20seguros=20-=20atualiza=20dashboard=20e=20tabela=20React?= =?UTF-8?q?=20com=20controles=20e=20requisi=C3=A7=C3=B5es=20paginadas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/ServersController.java | 9 +- .../model/dtos/PagedResponse.java | 12 ++ .../repositories/ServersRepository.java | 58 ++++--- .../services/ServersService.java | 24 ++- frontend/src/components/ServersTable.tsx | 146 +++++++++++++----- frontend/src/pages/Dashboard.tsx | 75 ++++++++- frontend/src/types/Pagination.ts | 7 + 7 files changed, 263 insertions(+), 68 deletions(-) create mode 100644 backend/src/main/java/com/hitcommunications/servermanager/model/dtos/PagedResponse.java create mode 100644 frontend/src/types/Pagination.ts diff --git a/backend/src/main/java/com/hitcommunications/servermanager/controllers/ServersController.java b/backend/src/main/java/com/hitcommunications/servermanager/controllers/ServersController.java index 56b2a59..8de5717 100644 --- a/backend/src/main/java/com/hitcommunications/servermanager/controllers/ServersController.java +++ b/backend/src/main/java/com/hitcommunications/servermanager/controllers/ServersController.java @@ -3,6 +3,7 @@ package com.hitcommunications.servermanager.controllers; import com.hitcommunications.servermanager.model.dtos.BulkServerImportResponse; import com.hitcommunications.servermanager.model.dtos.NewServerDTO; import com.hitcommunications.servermanager.model.dtos.ServerDTO; +import com.hitcommunications.servermanager.model.dtos.PagedResponse; import com.hitcommunications.servermanager.services.ServersService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -100,13 +101,15 @@ public class ServersController { @GetMapping @Operation(summary = "Pesquisa servidores com filtros combinados.") @ApiResponse(responseCode = "200", description = "Resultados retornados.") - public ResponseEntity> getAll( + public ResponseEntity> getAll( @RequestParam(value = "query", required = false) String query, @RequestParam(value = "type", required = false) String type, @RequestParam(value = "application", required = false) String application, - @RequestParam(value = "dbType", required = false) String dbType + @RequestParam(value = "dbType", required = false) String dbType, + @RequestParam(value = "page", required = false, defaultValue = "0") Integer page, + @RequestParam(value = "size", required = false, defaultValue = "10") Integer size ) { - return ResponseEntity.ok().body(serversService.search(query, type, application, dbType)); + return ResponseEntity.ok().body(serversService.search(query, type, application, dbType, page, size)); } @PutMapping("/{id}") diff --git a/backend/src/main/java/com/hitcommunications/servermanager/model/dtos/PagedResponse.java b/backend/src/main/java/com/hitcommunications/servermanager/model/dtos/PagedResponse.java new file mode 100644 index 0000000..e3db73a --- /dev/null +++ b/backend/src/main/java/com/hitcommunications/servermanager/model/dtos/PagedResponse.java @@ -0,0 +1,12 @@ +package com.hitcommunications.servermanager.model.dtos; + +import java.util.List; + +public record PagedResponse( + List content, + long totalItems, + int totalPages, + int page, + int size +) { +} diff --git a/backend/src/main/java/com/hitcommunications/servermanager/repositories/ServersRepository.java b/backend/src/main/java/com/hitcommunications/servermanager/repositories/ServersRepository.java index 4b566c8..b876209 100644 --- a/backend/src/main/java/com/hitcommunications/servermanager/repositories/ServersRepository.java +++ b/backend/src/main/java/com/hitcommunications/servermanager/repositories/ServersRepository.java @@ -1,13 +1,14 @@ package com.hitcommunications.servermanager.repositories; +import com.hitcommunications.servermanager.model.Servers; import java.util.List; import java.util.Optional; - +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import com.hitcommunications.servermanager.model.Servers; public interface ServersRepository extends JpaRepository { Optional findByName(String name); List findByType(String type); @@ -15,25 +16,44 @@ public interface ServersRepository extends JpaRepository { Optional findByIpAndPort(String ip, Integer port); Integer countAllByType(String type); - @Query(value = """ - select s.* from "server-manager".tab_servers s - where - (case when :query is null or length(:query) = 0 - then true - else ( - lower(s.name) like lower(concat('%', cast(:query as text), '%')) - or lower(s.username) like lower(concat('%', cast(:query as text), '%')) - or lower(s.ip) like lower(concat('%', cast(:query as text), '%')) - ) - end) - and (:type is null or s.type = :type) - and (:application is null or s.application = :application) - and (:dbType is null or s.db_type = :dbType) - """, nativeQuery = true) - List search( + @Query( + value = """ + select s.* from "server-manager".tab_servers s + where + (case when :query is null or length(:query) = 0 + then true + else ( + lower(s.name) like lower(concat('%', cast(:query as text), '%')) + or lower(s.username) like lower(concat('%', cast(:query as text), '%')) + or lower(s.ip) like lower(concat('%', cast(:query as text), '%')) + ) + end) + and (:type is null or s.type = :type) + and (:application is null or s.application = :application) + and (:dbType is null or s.db_type = :dbType) + """, + countQuery = """ + select count(*) from "server-manager".tab_servers s + where + (case when :query is null or length(:query) = 0 + then true + else ( + lower(s.name) like lower(concat('%', cast(:query as text), '%')) + or lower(s.username) like lower(concat('%', cast(:query as text), '%')) + or lower(s.ip) like lower(concat('%', cast(:query as text), '%')) + ) + end) + and (:type is null or s.type = :type) + and (:application is null or s.application = :application) + and (:dbType is null or s.db_type = :dbType) + """, + nativeQuery = true + ) + Page search( @Param("query") String query, @Param("type") String type, @Param("application") String application, - @Param("dbType") String dbType + @Param("dbType") String dbType, + Pageable pageable ); } diff --git a/backend/src/main/java/com/hitcommunications/servermanager/services/ServersService.java b/backend/src/main/java/com/hitcommunications/servermanager/services/ServersService.java index 32bcad4..085db90 100644 --- a/backend/src/main/java/com/hitcommunications/servermanager/services/ServersService.java +++ b/backend/src/main/java/com/hitcommunications/servermanager/services/ServersService.java @@ -6,11 +6,15 @@ import com.hitcommunications.servermanager.model.dtos.BulkServerImportResponse; import com.hitcommunications.servermanager.model.dtos.BulkServerImportResponse.FailedRow; import com.hitcommunications.servermanager.model.dtos.NewServerDTO; import com.hitcommunications.servermanager.model.dtos.ServerDTO; +import com.hitcommunications.servermanager.model.dtos.PagedResponse; import com.hitcommunications.servermanager.model.enums.TypeCategory; import com.hitcommunications.servermanager.repositories.ServersRepository; import com.hitcommunications.servermanager.services.TypeOptionService; import com.hitcommunications.servermanager.utils.TypeNormalizer; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -25,6 +29,9 @@ import java.util.LinkedHashMap; @Service @RequiredArgsConstructor public class ServersService { + private static final int DEFAULT_PAGE_SIZE = 10; + private static final int MAX_PAGE_SIZE = 50; + private final ServersMapper mapper; private final ServersRepository repo; private final TypeOptionService typeOptionService; @@ -176,16 +183,29 @@ public class ServersService { .toList(); } - public List search(String query, String type, String application, String dbType) { + public PagedResponse search(String query, String type, String application, String dbType, Integer page, Integer size) { String normalizedQuery = (query == null || query.isBlank()) ? null : query.trim(); String typeFilter = normalizeFilter(type, TypeCategory.SERVER_TYPE); String applicationFilter = normalizeFilter(application, TypeCategory.APPLICATION); String dbTypeFilter = normalizeFilter(dbType, TypeCategory.DATABASE); + int safePage = page != null && page >= 0 ? page : 0; + int requestedSize = (size != null && size > 0) ? size : DEFAULT_PAGE_SIZE; + int safeSize = Math.min(requestedSize, MAX_PAGE_SIZE); + Pageable pageable = PageRequest.of(safePage, safeSize); - return repo.search(normalizedQuery, typeFilter, applicationFilter, dbTypeFilter) + Page result = repo.search(normalizedQuery, typeFilter, applicationFilter, dbTypeFilter, pageable); + List content = result.getContent() .stream() .map(mapper::toDTO) .toList(); + + return new PagedResponse<>( + content, + result.getTotalElements(), + result.getTotalPages(), + result.getNumber(), + result.getSize() + ); } public ServerDTO update(String id, NewServerDTO updateDTO) { diff --git a/frontend/src/components/ServersTable.tsx b/frontend/src/components/ServersTable.tsx index 5c09ab6..fa782e9 100644 --- a/frontend/src/components/ServersTable.tsx +++ b/frontend/src/components/ServersTable.tsx @@ -4,9 +4,31 @@ interface Props { servers: Server[]; loading: boolean; error: string | null; + page: number; + pageSize: number; + totalPages: number; + totalItems: number; + onPageChange: (page: number) => void; + onPageSizeChange: (size: number) => void; } -export const ServersTable = ({ servers, loading, error }: Props) => { +const PAGE_SIZE_OPTIONS = [5, 10, 20, 50]; + +export const ServersTable = ({ + servers, + loading, + error, + page, + pageSize, + totalPages, + totalItems, + onPageChange, + onPageSizeChange, +}: Props) => { + const showingFrom = totalItems === 0 ? 0 : page * pageSize + 1; + const showingTo = totalItems === 0 ? 0 : Math.min((page + 1) * pageSize, totalItems); + const hasResults = servers.length > 0; + return (
{loading &&
Carregando servidores...
} @@ -14,42 +36,86 @@ export const ServersTable = ({ servers, loading, error }: Props) => { {!loading && !error && servers.length === 0 && (
Nenhum servidor encontrado.
)} - {!loading && !error && servers.length > 0 && ( -
- - - - - - - - - - - - - - - {servers.map((server) => ( - - - - - - - - - + {!loading && !error && hasResults && ( + <> +
+
NomeIPPortaUsuárioSenhaTipoAplicaçãoBanco
{server.name}{server.ip}{server.port}{server.user} - {server.password} - {server.type.toLowerCase()}{server.application.toLowerCase()}{server.dbType.toLowerCase()}
+ + + + + + + + + + - ))} - -
NomeIPPortaUsuárioSenhaTipoAplicaçãoBanco
-
+ + + {servers.map((server) => ( + + {server.name} + {server.ip} + {server.port} + {server.user} + + {server.password} + + {server.type.toLowerCase()} + {server.application.toLowerCase()} + {server.dbType.toLowerCase()} + + ))} + + +
+
+
+ Mostrando {showingFrom} - {showingTo} de {totalItems} +
+
+ +
+ + + Página {page + 1} de {Math.max(totalPages, 1)} + + +
+
+
+ )} ); @@ -65,4 +131,12 @@ const Styles = { tableHeadCell: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-text-secondary", tableBody: "bg-white divide-y divide-cardBorder", rowCell: "px-4 py-3 text-sm text-text", + pagination: "flex flex-col gap-2 border-t border-cardBorder bg-white px-4 py-3 sm:flex-row sm:items-center sm:justify-between", + pageInfo: "text-sm text-text-secondary", + paginationControls: "flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4", + pageSizeLabel: "text-sm text-text flex items-center gap-2", + pageSizeSelect: "rounded-md border border-cardBorder bg-white px-2 py-1 text-sm text-text outline-none focus:border-accent focus:ring-1 focus:ring-accent", + pageButtons: "flex items-center gap-3", + pageButton: "rounded-md border border-cardBorder px-3 py-1.5 text-sm font-medium text-text hover:bg-bg disabled:opacity-50 disabled:hover:bg-transparent", + pageIndicator: "text-sm text-text-secondary", }; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 4b3488c..4675fdf 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -9,6 +9,7 @@ import { ServersFilterBar } from "../components/ServersFilterBar"; import type { Server } from "../types/Server"; import type { User } from "../types/User"; import toast from "react-hot-toast"; +import type { PaginatedResponse } from "../types/Pagination"; export const Dashboard = () => { const navigate = useNavigate(); @@ -21,6 +22,10 @@ export const Dashboard = () => { const [typeFilter, setTypeFilter] = useState("ALL"); const [applicationFilter, setApplicationFilter] = useState("ALL"); const [dbFilter, setDbFilter] = useState("ALL"); + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(10); + const [totalPages, setTotalPages] = useState(0); + const [totalItems, setTotalItems] = useState(0); const [serverTypeOptions, setServerTypeOptions] = useState([]); const [applicationOptions, setApplicationOptions] = useState([]); const [databaseOptions, setDatabaseOptions] = useState([]); @@ -42,9 +47,19 @@ export const Dashboard = () => { if (dbFilter !== "ALL" && dbFilter.trim().length > 0) { params.set("dbType", dbFilter); } + params.set("page", String(page)); + params.set("size", String(pageSize)); const endpoint = params.toString() ? `/api/servers?${params.toString()}` : "/api/servers"; - const { data } = await api.get(endpoint); - setServers(data); + const { data } = await api.get>(endpoint); + setServers(data.content ?? []); + setTotalPages(data.totalPages ?? 0); + setTotalItems(data.totalItems ?? 0); + if (typeof data.page === "number") { + setPage(data.page); + } + if (typeof data.size === "number") { + setPageSize(data.size); + } setError(null); } catch (err: any) { const message = err?.response?.data?.message || "Falha ao carregar servidores."; @@ -52,7 +67,7 @@ export const Dashboard = () => { } finally { setLoading(false); } - }, [searchTerm, typeFilter, applicationFilter, dbFilter]); + }, [searchTerm, typeFilter, applicationFilter, dbFilter, page, pageSize]); const fetchCurrentUser = useCallback(async () => { try { @@ -101,6 +116,40 @@ export const Dashboard = () => { fetchServers(); }, [fetchServers]); + const handleSearchChange = (value: string) => { + setPage(0); + setSearchTerm(value); + }; + + const handleTypeChange = (value: string) => { + setPage(0); + setTypeFilter(value); + }; + + const handleApplicationChange = (value: string) => { + setPage(0); + setApplicationFilter(value); + }; + + const handleDbTypeChange = (value: string) => { + setPage(0); + setDbFilter(value); + }; + + const handlePageChange = (nextPage: number) => { + const maxPage = totalPages > 0 ? totalPages - 1 : 0; + const safePage = Math.max(0, Math.min(nextPage, maxPage)); + setPage(safePage); + }; + + const handlePageSizeChange = (nextSize: number) => { + if (nextSize <= 0) { + return; + } + setPage(0); + setPageSize(nextSize); + }; + useEffect(() => { fetchTypeOptions(); }, [fetchTypeOptions]); @@ -123,15 +172,25 @@ export const Dashboard = () => { type={typeFilter} application={applicationFilter} dbType={dbFilter} - onSearchChange={setSearchTerm} - onTypeChange={setTypeFilter} - onApplicationChange={setApplicationFilter} - onDbTypeChange={setDbFilter} + onSearchChange={handleSearchChange} + onTypeChange={handleTypeChange} + onApplicationChange={handleApplicationChange} + onDbTypeChange={handleDbTypeChange} serverTypeOptions={serverTypeOptions} applicationOptions={applicationOptions} databaseOptions={databaseOptions} /> - + ); diff --git a/frontend/src/types/Pagination.ts b/frontend/src/types/Pagination.ts new file mode 100644 index 0000000..bcf5a9a --- /dev/null +++ b/frontend/src/types/Pagination.ts @@ -0,0 +1,7 @@ +export interface PaginatedResponse { + content: T[]; + totalItems: number; + totalPages: number; + page: number; + size: number; +}