feat(frontend): consumir tipos dinâmicos

- remove listas fixas em Dashboard, Header e FilterBar
- busca opções via API e reutiliza nos selects e métricas
- fallback dos labels agora trata tipos desconhecidos
master
Artur Oliveira 2025-12-16 18:09:54 -03:00
parent a43fc58ff7
commit d4d65ad0f9
6 changed files with 85 additions and 72 deletions

View File

@ -19,6 +19,9 @@ interface HeaderProps {
userError: string | null;
onServerCreated?: () => Promise<void> | void;
onProfileUpdated?: (user: User) => void;
serverTypeOptions?: ServersType[];
applicationOptions?: Applications[];
databaseOptions?: DatabaseType[];
}
const defaultServerForm: ServerFormState = {
@ -39,20 +42,15 @@ const defaultProfileForm: ProfileFormState = {
password: "",
};
const serverTypeOptions: ServersType[] = ["PRODUCTION", "HOMOLOGATION", "DATABASE"];
const applicationOptions: Applications[] = [
"ASTERISK",
"HITMANAGER",
"HITMANAGER_V2",
"OMNIHIT",
"HITPHONE",
"CDR",
"FUNCIONALIDADE",
"VOICEMAIL",
];
const databaseOptions: DatabaseType[] = ["MYSQL", "POSTGRESQL", "SQLSERVER", "ORACLE", "REDIS", "MONGODB", "MARIADB", "NONE"];
export const Header = ({ currentUser, userError, onServerCreated, onProfileUpdated }: HeaderProps) => {
export const Header = ({
currentUser,
userError,
onServerCreated,
onProfileUpdated,
serverTypeOptions = [],
applicationOptions = [],
databaseOptions = [],
}: HeaderProps) => {
const navigate = useNavigate();
const [isMenuOpen, setMenuOpen] = useState(false);
const [activeModal, setActiveModal] = useState<ModalType>(null);

View File

@ -1,10 +1,10 @@
import { useEffect, useState } from "react";
import api from "../Api";
import { ServerTypeLabels, type ServersType } from "../types/enums";
import { ServerTypeLabels } from "../types/enums";
import { Code, Database, FlaskConical, Server } from "lucide-icons-react";
interface ServerCount {
type: ServersType;
type: string;
total: number;
}
@ -15,9 +15,9 @@ export const ServerCardMetrics = () => {
useEffect(() => {
const fetchCounts = async () => {
try {
const { data } = await api.get<Record<ServersType, number>>("/api/servers/type");
const { data } = await api.get<Record<string, number>>("/api/servers/type");
const normalized = Object.entries(data).map(([type, total]) => ({
type: type as ServersType,
type,
total,
}));
setCounts(normalized);
@ -38,7 +38,7 @@ export const ServerCardMetrics = () => {
return <div className={Styles.placeholder}>Carregando métricas...</div>;
}
const handleCardIcon = (type: ServersType) => {
const handleCardIcon = (type: string) => {
const iconProps = { size: 32, strokeWidth: 1.5 };
switch (type) {
@ -64,7 +64,7 @@ export const ServerCardMetrics = () => {
</div>
<div className={Styles.textGroup}>
<p className={Styles.value}>{formatTotal(total)}</p>
<p className={Styles.label}>{ServerTypeLabels[type]}</p>
<p className={Styles.label}>{ServerTypeLabels[type] ?? type}</p>
</div>
</div>
))}

View File

@ -12,21 +12,18 @@ interface Props {
onTypeChange: (value: ServersType | OptionAll) => void;
onApplicationChange: (value: Applications | OptionAll) => void;
onDbTypeChange: (value: DatabaseType | OptionAll) => void;
serverTypeOptions?: ServersType[];
applicationOptions?: Applications[];
databaseOptions?: DatabaseType[];
}
const typeOptions: Array<ServersType | OptionAll> = ["ALL", "PRODUCTION", "HOMOLOGATION", "DATABASE"];
const applicationOptions: Array<Applications | OptionAll> = [
"ALL",
"ASTERISK",
"HITMANAGER",
"HITMANAGER_V2",
"OMNIHIT",
"HITPHONE",
"CDR",
"FUNCIONALIDADE",
"VOICEMAIL",
];
const databaseOptions: Array<DatabaseType | OptionAll> = ["ALL", "MYSQL", "POSTGRESQL", "SQLSERVER", "ORACLE", "REDIS", "MONGODB", "MARIADB", "NONE"];
const withAllOption = <T extends string>(options?: T[]): Array<T | OptionAll> => {
if (!options || options.length === 0) {
return ["ALL"];
}
const unique = Array.from(new Set(options));
return ["ALL", ...unique];
};
export const ServersFilterBar = ({
search,
@ -37,7 +34,14 @@ export const ServersFilterBar = ({
onTypeChange,
onApplicationChange,
onDbTypeChange,
serverTypeOptions,
applicationOptions,
databaseOptions,
}: Props) => {
const typeOptions = withAllOption(serverTypeOptions);
const applicationOptionsList = withAllOption(applicationOptions);
const databaseOptionsList = withAllOption(databaseOptions);
return (
<div className={Styles.wrapper}>
<div className={Styles.searchGroup}>
@ -61,13 +65,13 @@ export const ServersFilterBar = ({
label="Aplicação"
value={application}
onChange={(event) => onApplicationChange(event.target.value as Applications | OptionAll)}
options={applicationOptions}
options={applicationOptionsList}
/>
<Select
label="Banco"
value={dbType}
onChange={(event) => onDbTypeChange(event.target.value as DatabaseType | OptionAll)}
options={databaseOptions}
options={databaseOptionsList}
/>
</div>
);

View File

@ -1,14 +1,12 @@
import type { Applications, DatabaseType, ServersType } from "../../types/enums";
export type ServerFormState = {
name: string;
ip: string;
port: string;
user: string;
password: string;
type: ServersType;
application: Applications;
dbType: DatabaseType;
type: string;
application: string;
dbType: string;
};
export type ProfileFormState = {

View File

@ -8,7 +8,6 @@ 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 = () => {
@ -19,9 +18,12 @@ export const Dashboard = () => {
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 [typeFilter, setTypeFilter] = useState<string>("ALL");
const [applicationFilter, setApplicationFilter] = useState<string>("ALL");
const [dbFilter, setDbFilter] = useState<string>("ALL");
const [serverTypeOptions, setServerTypeOptions] = useState<string[]>([]);
const [applicationOptions, setApplicationOptions] = useState<string[]>([]);
const [databaseOptions, setDatabaseOptions] = useState<string[]>([]);
const fetchServers = useCallback(async () => {
setLoading(true);
@ -31,13 +33,13 @@ export const Dashboard = () => {
if (trimmedQuery.length > 0) {
params.set("query", trimmedQuery);
}
if (typeFilter !== "ALL") {
if (typeFilter !== "ALL" && typeFilter.trim().length > 0) {
params.set("type", typeFilter);
}
if (applicationFilter !== "ALL") {
if (applicationFilter !== "ALL" && applicationFilter.trim().length > 0) {
params.set("application", applicationFilter);
}
if (dbFilter !== "ALL") {
if (dbFilter !== "ALL" && dbFilter.trim().length > 0) {
params.set("dbType", dbFilter);
}
const endpoint = params.toString() ? `/api/servers?${params.toString()}` : "/api/servers";
@ -76,10 +78,33 @@ export const Dashboard = () => {
fetchCurrentUser();
}, [fetchCurrentUser]);
const fetchTypeOptions = useCallback(async () => {
try {
const [typesResponse, applicationsResponse, databasesResponse] = await Promise.all([
api.get<string[]>("/api/type-options/SERVER_TYPE"),
api.get<string[]>("/api/type-options/APPLICATION"),
api.get<string[]>("/api/type-options/DATABASE"),
]);
setServerTypeOptions(typesResponse.data ?? []);
setApplicationOptions(applicationsResponse.data ?? []);
setDatabaseOptions(databasesResponse.data ?? []);
} catch (err: any) {
const message = err?.response?.data?.message || "Falha ao carregar opções padrão.";
toast.error(message);
setServerTypeOptions([]);
setApplicationOptions([]);
setDatabaseOptions([]);
}
}, []);
useEffect(() => {
fetchServers();
}, [fetchServers]);
useEffect(() => {
fetchTypeOptions();
}, [fetchTypeOptions]);
return (
<Layout className="h-screen py-10">
<div className="space-y-6">
@ -88,6 +113,9 @@ export const Dashboard = () => {
userError={userError}
onServerCreated={fetchServers}
onProfileUpdated={setCurrentUser}
serverTypeOptions={serverTypeOptions}
applicationOptions={applicationOptions}
databaseOptions={databaseOptions}
/>
<ServerCardMetrics />
<ServersFilterBar
@ -99,6 +127,9 @@ export const Dashboard = () => {
onTypeChange={setTypeFilter}
onApplicationChange={setApplicationFilter}
onDbTypeChange={setDbFilter}
serverTypeOptions={serverTypeOptions}
applicationOptions={applicationOptions}
databaseOptions={databaseOptions}
/>
<ServersTable servers={servers} loading={loading} error={error} />
</div>

View File

@ -1,27 +1,9 @@
export type DatabaseType =
| 'MYSQL'
| 'POSTGRESQL'
| 'SQLSERVER'
| 'ORACLE'
| 'REDIS'
| 'MONGODB'
| 'MARIADB'
| 'NONE';
export type ServersType = string;
export type Applications = string;
export type DatabaseType = string;
export type Applications =
| 'ASTERISK'
| 'HITMANAGER'
| 'HITMANAGER_V2'
| 'OMNIHIT'
| 'HITPHONE'
| 'CDR'
| 'FUNCIONALIDADE'
| 'VOICEMAIL';
export type ServersType = 'PRODUCTION' | 'HOMOLOGATION' | 'DATABASE';
export const ServerTypeLabels: Record<ServersType, string> = {
PRODUCTION: "Produção",
HOMOLOGATION: "Homologação",
DATABASE: "Banco de Dados",
export const ServerTypeLabels: Record<string, string> = {
PRODUCTION: "Produção",
HOMOLOGATION: "Homologação",
DATABASE: "Banco de Dados",
};