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 compatibilidademaster
parent
a69aca5dc8
commit
4efdfc9970
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ spring:
|
|||
password: ${DB_PASSWD}
|
||||
|
||||
jpa:
|
||||
defer-datasource-initialization: true
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: true
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue