From 4efdfc997071b36e540aced038b8560fce58c025 Mon Sep 17 00:00:00 2001 From: Artur Oliveira Date: Tue, 16 Dec 2025 15:10:31 -0300 Subject: [PATCH] feat(servers): habilitar filtros com busca - Expor GET /api/servers com parametros query, type, application e dbType - Implementar metodo search com consulta nativa e normalizacao de filtros - Criar ServersFilterBar e integrar filtros ao Dashboard - Ajustar entidade Servers e configs JPA para compatibilidade --- .../controllers/ServersController.java | 10 +- .../servermanager/model/Servers.java | 23 +++-- .../repositories/ServersRepository.java | 37 ++++++-- .../services/ServersService.java | 13 ++- backend/src/main/resources/application.yaml | 1 + frontend/src/components/ServersFilterBar.tsx | 95 +++++++++++++++++++ frontend/src/pages/Dashboard.tsx | 42 +++++++- 7 files changed, 202 insertions(+), 19 deletions(-) create mode 100644 frontend/src/components/ServersFilterBar.tsx 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 32aed7d..6eb9435 100644 --- a/backend/src/main/java/com/hitcommunications/servermanager/controllers/ServersController.java +++ b/backend/src/main/java/com/hitcommunications/servermanager/controllers/ServersController.java @@ -4,6 +4,7 @@ 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.enums.Applications; +import com.hitcommunications.servermanager.model.enums.DatabaseType; import com.hitcommunications.servermanager.model.enums.ServersType; import com.hitcommunications.servermanager.services.ServersService; import jakarta.validation.Valid; @@ -58,8 +59,13 @@ public class ServersController { } @GetMapping - public ResponseEntity> getAll() { - return ResponseEntity.ok().body(serversService.getAll()); + public ResponseEntity> getAll( + @RequestParam(value = "query", required = false) String query, + @RequestParam(value = "type", required = false) ServersType type, + @RequestParam(value = "application", required = false) Applications application, + @RequestParam(value = "dbType", required = false) DatabaseType dbType + ) { + return ResponseEntity.ok().body(serversService.search(query, type, application, dbType)); } @PutMapping("/{id}") diff --git a/backend/src/main/java/com/hitcommunications/servermanager/model/Servers.java b/backend/src/main/java/com/hitcommunications/servermanager/model/Servers.java index 0889c56..457efe3 100644 --- a/backend/src/main/java/com/hitcommunications/servermanager/model/Servers.java +++ b/backend/src/main/java/com/hitcommunications/servermanager/model/Servers.java @@ -1,15 +1,26 @@ package com.hitcommunications.servermanager.model; +import java.sql.Timestamp; + +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + import com.hitcommunications.servermanager.model.enums.Applications; import com.hitcommunications.servermanager.model.enums.DatabaseType; import com.hitcommunications.servermanager.model.enums.ServersType; import com.hitcommunications.servermanager.utils.ServerIdGenerator; -import jakarta.persistence.*; -import lombok.*; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; -import java.sql.Timestamp; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; @Entity @Table(name = "tab_servers") @@ -28,7 +39,7 @@ public class Servers { @Column(nullable = false) private String name; - @Column(nullable = false) + @Column(nullable = false, columnDefinition = "VARCHAR(45)") private String ip; @Column(nullable = false) 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 baa0110..6b45be1 100644 --- a/backend/src/main/java/com/hitcommunications/servermanager/repositories/ServersRepository.java +++ b/backend/src/main/java/com/hitcommunications/servermanager/repositories/ServersRepository.java @@ -1,18 +1,43 @@ package com.hitcommunications.servermanager.repositories; -import com.hitcommunications.servermanager.model.Servers; -import com.hitcommunications.servermanager.model.enums.Applications; -import com.hitcommunications.servermanager.model.enums.ServersType; -import org.springframework.data.jpa.repository.JpaRepository; - import java.util.List; import java.util.Optional; +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; +import com.hitcommunications.servermanager.model.enums.Applications; +import com.hitcommunications.servermanager.model.enums.DatabaseType; +import com.hitcommunications.servermanager.model.enums.ServersType; + public interface ServersRepository extends JpaRepository { Optional findByName(String name); List findByType(ServersType type); List findByApplication(Applications application); Optional findByIpAndPort(String ip, Integer port); Integer countAllByType(ServersType 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( + @Param("query") String query, + @Param("type") String type, + @Param("application") String application, + @Param("dbType") String dbType + ); +} 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 0fad71c..3e6eb7c 100644 --- a/backend/src/main/java/com/hitcommunications/servermanager/services/ServersService.java +++ b/backend/src/main/java/com/hitcommunications/servermanager/services/ServersService.java @@ -22,7 +22,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.ArrayList; - @Service @RequiredArgsConstructor public class ServersService { @@ -154,6 +153,18 @@ public class ServersService { .toList(); } + public List search(String query, ServersType type, Applications application, DatabaseType dbType) { + String normalizedQuery = (query == null || query.isBlank()) ? null : query.trim(); + String typeFilter = type != null ? type.name() : null; + String applicationFilter = application != null ? application.name() : null; + String dbTypeFilter = dbType != null ? dbType.name() : null; + + return repo.search(normalizedQuery, typeFilter, applicationFilter, dbTypeFilter) + .stream() + .map(mapper::toDTO) + .toList(); + } + public ServerDTO update(String id, NewServerDTO updateDTO) { Servers entity = repo.findById(id) .orElseThrow(() -> new RuntimeException("Server not found with id: " + id)); diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index 9399a8d..475c075 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -13,6 +13,7 @@ spring: password: ${DB_PASSWD} jpa: + defer-datasource-initialization: true hibernate: ddl-auto: update show-sql: true diff --git a/frontend/src/components/ServersFilterBar.tsx b/frontend/src/components/ServersFilterBar.tsx new file mode 100644 index 0000000..73c9971 --- /dev/null +++ b/frontend/src/components/ServersFilterBar.tsx @@ -0,0 +1,95 @@ +import type { ChangeEvent } from "react"; +import type { Applications, DatabaseType, ServersType } from "../types/enums"; + +type OptionAll = "ALL"; + +interface Props { + search: string; + type: ServersType | OptionAll; + application: Applications | OptionAll; + dbType: DatabaseType | OptionAll; + onSearchChange: (value: string) => void; + onTypeChange: (value: ServersType | OptionAll) => void; + onApplicationChange: (value: Applications | OptionAll) => void; + onDbTypeChange: (value: DatabaseType | OptionAll) => void; +} + +const typeOptions: Array = ["ALL", "PRODUCTION", "HOMOLOGATION", "DATABASE"]; +const applicationOptions: Array = ["ALL", "ASTERISK", "HITMANAGER", "HITMANAGER_V2", "OMNIHIT", "HITPHONE"]; +const databaseOptions: Array = ["ALL", "MYSQL", "POSTGRESQL", "SQLSERVER", "ORACLE", "REDIS", "MONGODB", "MARIADB", "NONE"]; + +export const ServersFilterBar = ({ + search, + type, + application, + dbType, + onSearchChange, + onTypeChange, + onApplicationChange, + onDbTypeChange, +}: Props) => { + return ( +
+
+ + onSearchChange(event.target.value)} + className={Styles.input} + /> +
+ onApplicationChange(event.target.value as Applications | OptionAll)} + options={applicationOptions} + /> + + {options.map((option) => ( + + ))} + +
+ ); +}; + +const Styles = { + wrapper: "flex flex-wrap gap-4 rounded-lg border border-cardBorder bg-white/70 p-4 shadow-sm", + searchGroup: "flex-1 min-w-[220px]", + selectGroup: "flex min-w-[150px] flex-col gap-1", + label: "text-xs font-semibold uppercase tracking-wide text-text-secondary", + input: "w-full rounded-lg border border-cardBorder bg-white px-3 py-2 text-sm text-text outline-none focus:border-accent focus:ring-1 focus:ring-accent", + select: "w-full rounded-lg border border-cardBorder bg-white px-3 py-2 text-sm text-text outline-none focus:border-accent focus:ring-1 focus:ring-accent capitalize", +}; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 170e7c8..de89e4f 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -4,8 +4,10 @@ import { Layout } from "../components/Layout"; import { Header } from "../components/Header"; import { ServerCardMetrics } from "../components/ServerCardMetrics"; import { ServersTable } from "../components/ServersTable"; +import { ServersFilterBar } from "../components/ServersFilterBar"; import type { Server } from "../types/Server"; import type { User } from "../types/User"; +import type { Applications, DatabaseType, ServersType } from "../types/enums"; import toast from "react-hot-toast"; export const Dashboard = () => { @@ -14,11 +16,30 @@ export const Dashboard = () => { const [error, setError] = useState(null); const [currentUser, setCurrentUser] = useState(null); const [userError, setUserError] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + const [typeFilter, setTypeFilter] = useState("ALL"); + const [applicationFilter, setApplicationFilter] = useState("ALL"); + const [dbFilter, setDbFilter] = useState("ALL"); const fetchServers = useCallback(async () => { setLoading(true); try { - const { data } = await api.get("/api/servers"); + const params = new URLSearchParams(); + const trimmedQuery = searchTerm.trim(); + if (trimmedQuery.length > 0) { + params.set("query", trimmedQuery); + } + if (typeFilter !== "ALL") { + params.set("type", typeFilter); + } + if (applicationFilter !== "ALL") { + params.set("application", applicationFilter); + } + if (dbFilter !== "ALL") { + params.set("dbType", dbFilter); + } + const endpoint = params.toString() ? `/api/servers?${params.toString()}` : "/api/servers"; + const { data } = await api.get(endpoint); setServers(data); setError(null); } catch (err: any) { @@ -27,7 +48,7 @@ export const Dashboard = () => { } finally { setLoading(false); } - }, []); + }, [searchTerm, typeFilter, applicationFilter, dbFilter]); const fetchCurrentUser = useCallback(async () => { try { @@ -47,9 +68,12 @@ export const Dashboard = () => { }, []); useEffect(() => { - fetchServers(); fetchCurrentUser(); - }, [fetchServers, fetchCurrentUser]); + }, [fetchCurrentUser]); + + useEffect(() => { + fetchServers(); + }, [fetchServers]); return ( @@ -61,6 +85,16 @@ export const Dashboard = () => { onProfileUpdated={setCurrentUser} /> +