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
master
Artur Oliveira 2025-12-16 15:10:31 -03:00
parent a69aca5dc8
commit 4efdfc9970
7 changed files with 202 additions and 19 deletions

View File

@ -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<List<ServerDTO>> getAll() {
return ResponseEntity.ok().body(serversService.getAll());
public ResponseEntity<List<ServerDTO>> 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}")

View File

@ -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)

View File

@ -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<Servers, String> {
Optional<Servers> findByName(String name);
List<Servers> findByType(ServersType type);
List<Servers> findByApplication(Applications application);
Optional<Servers> 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<Servers> search(
@Param("query") String query,
@Param("type") String type,
@Param("application") String application,
@Param("dbType") String dbType
);
}

View File

@ -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<ServerDTO> 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));

View File

@ -13,6 +13,7 @@ spring:
password: ${DB_PASSWD}
jpa:
defer-datasource-initialization: true
hibernate:
ddl-auto: update
show-sql: true

View File

@ -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<ServersType | OptionAll> = ["ALL", "PRODUCTION", "HOMOLOGATION", "DATABASE"];
const applicationOptions: Array<Applications | OptionAll> = ["ALL", "ASTERISK", "HITMANAGER", "HITMANAGER_V2", "OMNIHIT", "HITPHONE"];
const databaseOptions: Array<DatabaseType | OptionAll> = ["ALL", "MYSQL", "POSTGRESQL", "SQLSERVER", "ORACLE", "REDIS", "MONGODB", "MARIADB", "NONE"];
export const ServersFilterBar = ({
search,
type,
application,
dbType,
onSearchChange,
onTypeChange,
onApplicationChange,
onDbTypeChange,
}: Props) => {
return (
<div className={Styles.wrapper}>
<div className={Styles.searchGroup}>
<label htmlFor="server-search" className={Styles.label}>Buscar</label>
<input
id="server-search"
type="text"
placeholder="Buscar por nome, IP ou usuário..."
value={search}
onChange={(event) => onSearchChange(event.target.value)}
className={Styles.input}
/>
</div>
<Select
label="Tipo"
value={type}
onChange={(event) => onTypeChange(event.target.value as ServersType | OptionAll)}
options={typeOptions}
/>
<Select
label="Aplicação"
value={application}
onChange={(event) => onApplicationChange(event.target.value as Applications | OptionAll)}
options={applicationOptions}
/>
<Select
label="Banco"
value={dbType}
onChange={(event) => onDbTypeChange(event.target.value as DatabaseType | OptionAll)}
options={databaseOptions}
/>
</div>
);
};
interface SelectProps<T extends string> {
label: string;
value: T;
onChange: (event: ChangeEvent<HTMLSelectElement>) => void;
options: T[];
}
const Select = <T extends string>({ label, value, onChange, options }: SelectProps<T>) => {
return (
<div className={Styles.selectGroup}>
<label className={Styles.label}>{label}</label>
<select value={value} onChange={onChange} className={Styles.select}>
{options.map((option) => (
<option key={option} value={option}>
{option === "ALL" ? "Todos" : option.toLowerCase()}
</option>
))}
</select>
</div>
);
};
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",
};

View File

@ -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<string | null>(null);
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [userError, setUserError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState("");
const [typeFilter, setTypeFilter] = useState<ServersType | "ALL">("ALL");
const [applicationFilter, setApplicationFilter] = useState<Applications | "ALL">("ALL");
const [dbFilter, setDbFilter] = useState<DatabaseType | "ALL">("ALL");
const fetchServers = useCallback(async () => {
setLoading(true);
try {
const { data } = await api.get<Server[]>("/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<Server[]>(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 (
<Layout className="h-screen py-10">
@ -61,6 +85,16 @@ export const Dashboard = () => {
onProfileUpdated={setCurrentUser}
/>
<ServerCardMetrics />
<ServersFilterBar
search={searchTerm}
type={typeFilter}
application={applicationFilter}
dbType={dbFilter}
onSearchChange={setSearchTerm}
onTypeChange={setTypeFilter}
onApplicationChange={setApplicationFilter}
onDbTypeChange={setDbFilter}
/>
<ServersTable servers={servers} loading={loading} error={error} />
</div>
</Layout>