feat(pagination): paginar listagem de servidores
- adiciona DTO de página e paginação no endpoint GET /api/servers - aplica busca paginada no service/repositório com limites seguros - atualiza dashboard e tabela React com controles e requisições paginadasmaster
parent
d4d65ad0f9
commit
7460577423
|
|
@ -3,6 +3,7 @@ package com.hitcommunications.servermanager.controllers;
|
||||||
import com.hitcommunications.servermanager.model.dtos.BulkServerImportResponse;
|
import com.hitcommunications.servermanager.model.dtos.BulkServerImportResponse;
|
||||||
import com.hitcommunications.servermanager.model.dtos.NewServerDTO;
|
import com.hitcommunications.servermanager.model.dtos.NewServerDTO;
|
||||||
import com.hitcommunications.servermanager.model.dtos.ServerDTO;
|
import com.hitcommunications.servermanager.model.dtos.ServerDTO;
|
||||||
|
import com.hitcommunications.servermanager.model.dtos.PagedResponse;
|
||||||
import com.hitcommunications.servermanager.services.ServersService;
|
import com.hitcommunications.servermanager.services.ServersService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
|
@ -100,13 +101,15 @@ public class ServersController {
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "Pesquisa servidores com filtros combinados.")
|
@Operation(summary = "Pesquisa servidores com filtros combinados.")
|
||||||
@ApiResponse(responseCode = "200", description = "Resultados retornados.")
|
@ApiResponse(responseCode = "200", description = "Resultados retornados.")
|
||||||
public ResponseEntity<List<ServerDTO>> getAll(
|
public ResponseEntity<PagedResponse<ServerDTO>> getAll(
|
||||||
@RequestParam(value = "query", required = false) String query,
|
@RequestParam(value = "query", required = false) String query,
|
||||||
@RequestParam(value = "type", required = false) String type,
|
@RequestParam(value = "type", required = false) String type,
|
||||||
@RequestParam(value = "application", required = false) String application,
|
@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}")
|
@PutMapping("/{id}")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.hitcommunications.servermanager.model.dtos;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record PagedResponse<T>(
|
||||||
|
List<T> content,
|
||||||
|
long totalItems,
|
||||||
|
int totalPages,
|
||||||
|
int page,
|
||||||
|
int size
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
package com.hitcommunications.servermanager.repositories;
|
package com.hitcommunications.servermanager.repositories;
|
||||||
|
|
||||||
|
import com.hitcommunications.servermanager.model.Servers;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
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.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
import com.hitcommunications.servermanager.model.Servers;
|
|
||||||
public interface ServersRepository extends JpaRepository<Servers, String> {
|
public interface ServersRepository extends JpaRepository<Servers, String> {
|
||||||
Optional<Servers> findByName(String name);
|
Optional<Servers> findByName(String name);
|
||||||
List<Servers> findByType(String type);
|
List<Servers> findByType(String type);
|
||||||
|
|
@ -15,25 +16,44 @@ public interface ServersRepository extends JpaRepository<Servers, String> {
|
||||||
Optional<Servers> findByIpAndPort(String ip, Integer port);
|
Optional<Servers> findByIpAndPort(String ip, Integer port);
|
||||||
Integer countAllByType(String type);
|
Integer countAllByType(String type);
|
||||||
|
|
||||||
@Query(value = """
|
@Query(
|
||||||
select s.* from "server-manager".tab_servers s
|
value = """
|
||||||
where
|
select s.* from "server-manager".tab_servers s
|
||||||
(case when :query is null or length(:query) = 0
|
where
|
||||||
then true
|
(case when :query is null or length(:query) = 0
|
||||||
else (
|
then true
|
||||||
lower(s.name) like lower(concat('%', cast(:query as text), '%'))
|
else (
|
||||||
or lower(s.username) like lower(concat('%', cast(:query as text), '%'))
|
lower(s.name) like lower(concat('%', cast(:query as text), '%'))
|
||||||
or lower(s.ip) 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)
|
end)
|
||||||
and (:application is null or s.application = :application)
|
and (:type is null or s.type = :type)
|
||||||
and (:dbType is null or s.db_type = :dbType)
|
and (:application is null or s.application = :application)
|
||||||
""", nativeQuery = true)
|
and (:dbType is null or s.db_type = :dbType)
|
||||||
List<Servers> search(
|
""",
|
||||||
|
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<Servers> search(
|
||||||
@Param("query") String query,
|
@Param("query") String query,
|
||||||
@Param("type") String type,
|
@Param("type") String type,
|
||||||
@Param("application") String application,
|
@Param("application") String application,
|
||||||
@Param("dbType") String dbType
|
@Param("dbType") String dbType,
|
||||||
|
Pageable pageable
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.BulkServerImportResponse.FailedRow;
|
||||||
import com.hitcommunications.servermanager.model.dtos.NewServerDTO;
|
import com.hitcommunications.servermanager.model.dtos.NewServerDTO;
|
||||||
import com.hitcommunications.servermanager.model.dtos.ServerDTO;
|
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.model.enums.TypeCategory;
|
||||||
import com.hitcommunications.servermanager.repositories.ServersRepository;
|
import com.hitcommunications.servermanager.repositories.ServersRepository;
|
||||||
import com.hitcommunications.servermanager.services.TypeOptionService;
|
import com.hitcommunications.servermanager.services.TypeOptionService;
|
||||||
import com.hitcommunications.servermanager.utils.TypeNormalizer;
|
import com.hitcommunications.servermanager.utils.TypeNormalizer;
|
||||||
import lombok.RequiredArgsConstructor;
|
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.stereotype.Service;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
|
@ -25,6 +29,9 @@ import java.util.LinkedHashMap;
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ServersService {
|
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 ServersMapper mapper;
|
||||||
private final ServersRepository repo;
|
private final ServersRepository repo;
|
||||||
private final TypeOptionService typeOptionService;
|
private final TypeOptionService typeOptionService;
|
||||||
|
|
@ -176,16 +183,29 @@ public class ServersService {
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ServerDTO> search(String query, String type, String application, String dbType) {
|
public PagedResponse<ServerDTO> search(String query, String type, String application, String dbType, Integer page, Integer size) {
|
||||||
String normalizedQuery = (query == null || query.isBlank()) ? null : query.trim();
|
String normalizedQuery = (query == null || query.isBlank()) ? null : query.trim();
|
||||||
String typeFilter = normalizeFilter(type, TypeCategory.SERVER_TYPE);
|
String typeFilter = normalizeFilter(type, TypeCategory.SERVER_TYPE);
|
||||||
String applicationFilter = normalizeFilter(application, TypeCategory.APPLICATION);
|
String applicationFilter = normalizeFilter(application, TypeCategory.APPLICATION);
|
||||||
String dbTypeFilter = normalizeFilter(dbType, TypeCategory.DATABASE);
|
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<Servers> result = repo.search(normalizedQuery, typeFilter, applicationFilter, dbTypeFilter, pageable);
|
||||||
|
List<ServerDTO> content = result.getContent()
|
||||||
.stream()
|
.stream()
|
||||||
.map(mapper::toDTO)
|
.map(mapper::toDTO)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
return new PagedResponse<>(
|
||||||
|
content,
|
||||||
|
result.getTotalElements(),
|
||||||
|
result.getTotalPages(),
|
||||||
|
result.getNumber(),
|
||||||
|
result.getSize()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerDTO update(String id, NewServerDTO updateDTO) {
|
public ServerDTO update(String id, NewServerDTO updateDTO) {
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,31 @@ interface Props {
|
||||||
servers: Server[];
|
servers: Server[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
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 (
|
return (
|
||||||
<div className={Styles.card}>
|
<div className={Styles.card}>
|
||||||
{loading && <div className={Styles.status}>Carregando servidores...</div>}
|
{loading && <div className={Styles.status}>Carregando servidores...</div>}
|
||||||
|
|
@ -14,42 +36,86 @@ export const ServersTable = ({ servers, loading, error }: Props) => {
|
||||||
{!loading && !error && servers.length === 0 && (
|
{!loading && !error && servers.length === 0 && (
|
||||||
<div className={Styles.status}>Nenhum servidor encontrado.</div>
|
<div className={Styles.status}>Nenhum servidor encontrado.</div>
|
||||||
)}
|
)}
|
||||||
{!loading && !error && servers.length > 0 && (
|
{!loading && !error && hasResults && (
|
||||||
<div className={Styles.tableWrapper}>
|
<>
|
||||||
<table className={Styles.table}>
|
<div className={Styles.tableWrapper}>
|
||||||
<thead className={Styles.tableHead}>
|
<table className={Styles.table}>
|
||||||
<tr className="text-left">
|
<thead className={Styles.tableHead}>
|
||||||
<th className={Styles.tableHeadCell}>Nome</th>
|
<tr className="text-left">
|
||||||
<th className={Styles.tableHeadCell}>IP</th>
|
<th className={Styles.tableHeadCell}>Nome</th>
|
||||||
<th className={Styles.tableHeadCell}>Porta</th>
|
<th className={Styles.tableHeadCell}>IP</th>
|
||||||
<th className={Styles.tableHeadCell}>Usuário</th>
|
<th className={Styles.tableHeadCell}>Porta</th>
|
||||||
<th className={Styles.tableHeadCell}>Senha</th>
|
<th className={Styles.tableHeadCell}>Usuário</th>
|
||||||
<th className={Styles.tableHeadCell}>Tipo</th>
|
<th className={Styles.tableHeadCell}>Senha</th>
|
||||||
<th className={Styles.tableHeadCell}>Aplicação</th>
|
<th className={Styles.tableHeadCell}>Tipo</th>
|
||||||
<th className={Styles.tableHeadCell}>Banco</th>
|
<th className={Styles.tableHeadCell}>Aplicação</th>
|
||||||
</tr>
|
<th className={Styles.tableHeadCell}>Banco</th>
|
||||||
</thead>
|
|
||||||
<tbody className={Styles.tableBody}>
|
|
||||||
{servers.map((server) => (
|
|
||||||
<tr
|
|
||||||
key={server.id}
|
|
||||||
className="hover:bg-gray-50 transition-colors even:bg-gray-50/50"
|
|
||||||
>
|
|
||||||
<td className={Styles.rowCell}>{server.name}</td>
|
|
||||||
<td className={Styles.rowCell}>{server.ip}</td>
|
|
||||||
<td className={Styles.rowCell}>{server.port}</td>
|
|
||||||
<td className={Styles.rowCell}>{server.user}</td>
|
|
||||||
<td className={Styles.rowCell}>
|
|
||||||
<code className="text-xs bg-gray-100 px-2 py-1 rounded">{server.password}</code>
|
|
||||||
</td>
|
|
||||||
<td className={`${Styles.rowCell} capitalize`}>{server.type.toLowerCase()}</td>
|
|
||||||
<td className={`${Styles.rowCell} capitalize`}>{server.application.toLowerCase()}</td>
|
|
||||||
<td className={`${Styles.rowCell} capitalize`}>{server.dbType.toLowerCase()}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody className={Styles.tableBody}>
|
||||||
</table>
|
{servers.map((server) => (
|
||||||
</div>
|
<tr
|
||||||
|
key={server.id}
|
||||||
|
className="hover:bg-gray-50 transition-colors even:bg-gray-50/50"
|
||||||
|
>
|
||||||
|
<td className={Styles.rowCell}>{server.name}</td>
|
||||||
|
<td className={Styles.rowCell}>{server.ip}</td>
|
||||||
|
<td className={Styles.rowCell}>{server.port}</td>
|
||||||
|
<td className={Styles.rowCell}>{server.user}</td>
|
||||||
|
<td className={Styles.rowCell}>
|
||||||
|
<code className="text-xs bg-gray-100 px-2 py-1 rounded">{server.password}</code>
|
||||||
|
</td>
|
||||||
|
<td className={`${Styles.rowCell} capitalize`}>{server.type.toLowerCase()}</td>
|
||||||
|
<td className={`${Styles.rowCell} capitalize`}>{server.application.toLowerCase()}</td>
|
||||||
|
<td className={`${Styles.rowCell} capitalize`}>{server.dbType.toLowerCase()}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className={Styles.pagination}>
|
||||||
|
<div className={Styles.pageInfo}>
|
||||||
|
Mostrando {showingFrom} - {showingTo} de {totalItems}
|
||||||
|
</div>
|
||||||
|
<div className={Styles.paginationControls}>
|
||||||
|
<label className={Styles.pageSizeLabel}>
|
||||||
|
Linhas por página
|
||||||
|
<select
|
||||||
|
className={Styles.pageSizeSelect}
|
||||||
|
value={pageSize}
|
||||||
|
onChange={(event) => onPageSizeChange(Number(event.target.value))}
|
||||||
|
>
|
||||||
|
{PAGE_SIZE_OPTIONS.map((option) => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div className={Styles.pageButtons}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={Styles.pageButton}
|
||||||
|
onClick={() => onPageChange(page - 1)}
|
||||||
|
disabled={page <= 0}
|
||||||
|
>
|
||||||
|
Anterior
|
||||||
|
</button>
|
||||||
|
<span className={Styles.pageIndicator}>
|
||||||
|
Página {page + 1} de {Math.max(totalPages, 1)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={Styles.pageButton}
|
||||||
|
onClick={() => onPageChange(page + 1)}
|
||||||
|
disabled={totalPages === 0 || page >= totalPages - 1}
|
||||||
|
>
|
||||||
|
Próxima
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -65,4 +131,12 @@ const Styles = {
|
||||||
tableHeadCell: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-text-secondary",
|
tableHeadCell: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-text-secondary",
|
||||||
tableBody: "bg-white divide-y divide-cardBorder",
|
tableBody: "bg-white divide-y divide-cardBorder",
|
||||||
rowCell: "px-4 py-3 text-sm text-text",
|
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",
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { ServersFilterBar } from "../components/ServersFilterBar";
|
||||||
import type { Server } from "../types/Server";
|
import type { Server } from "../types/Server";
|
||||||
import type { User } from "../types/User";
|
import type { User } from "../types/User";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import type { PaginatedResponse } from "../types/Pagination";
|
||||||
|
|
||||||
export const Dashboard = () => {
|
export const Dashboard = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -21,6 +22,10 @@ export const Dashboard = () => {
|
||||||
const [typeFilter, setTypeFilter] = useState<string>("ALL");
|
const [typeFilter, setTypeFilter] = useState<string>("ALL");
|
||||||
const [applicationFilter, setApplicationFilter] = useState<string>("ALL");
|
const [applicationFilter, setApplicationFilter] = useState<string>("ALL");
|
||||||
const [dbFilter, setDbFilter] = useState<string>("ALL");
|
const [dbFilter, setDbFilter] = useState<string>("ALL");
|
||||||
|
const [page, setPage] = useState<number>(0);
|
||||||
|
const [pageSize, setPageSize] = useState<number>(10);
|
||||||
|
const [totalPages, setTotalPages] = useState<number>(0);
|
||||||
|
const [totalItems, setTotalItems] = useState<number>(0);
|
||||||
const [serverTypeOptions, setServerTypeOptions] = useState<string[]>([]);
|
const [serverTypeOptions, setServerTypeOptions] = useState<string[]>([]);
|
||||||
const [applicationOptions, setApplicationOptions] = useState<string[]>([]);
|
const [applicationOptions, setApplicationOptions] = useState<string[]>([]);
|
||||||
const [databaseOptions, setDatabaseOptions] = useState<string[]>([]);
|
const [databaseOptions, setDatabaseOptions] = useState<string[]>([]);
|
||||||
|
|
@ -42,9 +47,19 @@ export const Dashboard = () => {
|
||||||
if (dbFilter !== "ALL" && dbFilter.trim().length > 0) {
|
if (dbFilter !== "ALL" && dbFilter.trim().length > 0) {
|
||||||
params.set("dbType", dbFilter);
|
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 endpoint = params.toString() ? `/api/servers?${params.toString()}` : "/api/servers";
|
||||||
const { data } = await api.get<Server[]>(endpoint);
|
const { data } = await api.get<PaginatedResponse<Server>>(endpoint);
|
||||||
setServers(data);
|
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);
|
setError(null);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const message = err?.response?.data?.message || "Falha ao carregar servidores.";
|
const message = err?.response?.data?.message || "Falha ao carregar servidores.";
|
||||||
|
|
@ -52,7 +67,7 @@ export const Dashboard = () => {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [searchTerm, typeFilter, applicationFilter, dbFilter]);
|
}, [searchTerm, typeFilter, applicationFilter, dbFilter, page, pageSize]);
|
||||||
|
|
||||||
const fetchCurrentUser = useCallback(async () => {
|
const fetchCurrentUser = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -101,6 +116,40 @@ export const Dashboard = () => {
|
||||||
fetchServers();
|
fetchServers();
|
||||||
}, [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(() => {
|
useEffect(() => {
|
||||||
fetchTypeOptions();
|
fetchTypeOptions();
|
||||||
}, [fetchTypeOptions]);
|
}, [fetchTypeOptions]);
|
||||||
|
|
@ -123,15 +172,25 @@ export const Dashboard = () => {
|
||||||
type={typeFilter}
|
type={typeFilter}
|
||||||
application={applicationFilter}
|
application={applicationFilter}
|
||||||
dbType={dbFilter}
|
dbType={dbFilter}
|
||||||
onSearchChange={setSearchTerm}
|
onSearchChange={handleSearchChange}
|
||||||
onTypeChange={setTypeFilter}
|
onTypeChange={handleTypeChange}
|
||||||
onApplicationChange={setApplicationFilter}
|
onApplicationChange={handleApplicationChange}
|
||||||
onDbTypeChange={setDbFilter}
|
onDbTypeChange={handleDbTypeChange}
|
||||||
serverTypeOptions={serverTypeOptions}
|
serverTypeOptions={serverTypeOptions}
|
||||||
applicationOptions={applicationOptions}
|
applicationOptions={applicationOptions}
|
||||||
databaseOptions={databaseOptions}
|
databaseOptions={databaseOptions}
|
||||||
/>
|
/>
|
||||||
<ServersTable servers={servers} loading={loading} error={error} />
|
<ServersTable
|
||||||
|
servers={servers}
|
||||||
|
loading={loading}
|
||||||
|
error={error}
|
||||||
|
page={page}
|
||||||
|
pageSize={pageSize}
|
||||||
|
totalPages={totalPages}
|
||||||
|
totalItems={totalItems}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onPageSizeChange={handlePageSizeChange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
content: T[];
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue