feat: improve alert configuration form and user experience

- Added a new input field for alert configuration names, enhancing clarity for users creating or editing configurations.
- Implemented a function to clear the alert configuration form, allowing users to start fresh when creating new settings.
- Updated feedback messages to provide clearer guidance on saving and updating configurations.
- Enhanced the layout of the alert settings section for better usability and organization.
- Introduced smooth scrolling to the alert form when editing configurations, improving navigation experience.
This commit is contained in:
2025-11-17 11:19:51 -03:00
parent 7e3c100fb9
commit d173e2a255

View File

@@ -25,40 +25,45 @@
const alertConfigs = useQuery(api.security.listarAlertConfigs, { limit: 100 });
// Estado local para preferências de alertas
let alertNomeConfig = $state('');
let alertEmails = $state('');
let alertSeveridadeMin: SeveridadeSeguranca = 'alto';
let alertTiposAtaque: string[] = [];
let alertSeveridadeMin = $state<SeveridadeSeguranca>('alto');
let alertTiposAtaque = $state<string[]>([]);
let alertReenvioMin = $state(15);
let alertTemplate = $state('incidente_critico');
let alertUsersChat = $state('');
let chatSeveridadeMin: SeveridadeSeguranca = 'alto';
let chatTiposAtaque: string[] = [];
let chatSeveridadeMin = $state<SeveridadeSeguranca>('alto');
let chatTiposAtaque = $state<string[]>([]);
let chatReenvioMin = $state(10);
let enviarPorEmail = $state(true);
let enviarPorChat = $state(false);
let editarAlertConfigId: Id<'alertConfigs'> | null = null;
let editarAlertConfigId = $state<Id<'alertConfigs'> | null>(null);
// Sugestões a partir de configs salvas
const sugestoesEmails = $derived.by(() => {
const set = new Set<string>();
for (const cfg of alertConfigs?.data ?? []) {
for (const e of cfg.emails ?? []) set.add(e);
}
for (const u of usuariosQuery?.data ?? []) {
if (u?.email) set.add(u.email);
}
return Array.from(set).slice(0, 16);
});
const sugestoesChatUsers = $derived.by(() => {
const set = new Set<string>();
for (const cfg of alertConfigs?.data ?? []) {
for (const u of cfg.chatUsers ?? []) set.add(u);
}
for (const u of usuariosQuery?.data ?? []) {
if (u?.username) set.add(u.username);
}
return Array.from(set).slice(0, 16);
});
// Sugestões a partir de configs salvas (podem ser usadas no futuro para autocomplete)
// const sugestoesEmails = $derived.by(() => {
// const emails: string[] = [];
// for (const cfg of alertConfigs?.data ?? []) {
// for (const e of cfg.emails ?? []) {
// if (!emails.includes(e)) emails.push(e);
// }
// }
// for (const u of usuariosQuery?.data ?? []) {
// if (u?.email && !emails.includes(u.email)) emails.push(u.email);
// }
// return emails.slice(0, 16);
// });
// const sugestoesChatUsers = $derived.by(() => {
// const emails: string[] = [];
// for (const cfg of alertConfigs?.data ?? []) {
// for (const u of cfg.chatUsers ?? []) {
// if (!emails.includes(u)) emails.push(u);
// }
// }
// for (const u of usuariosQuery?.data ?? []) {
// if (u?.email && !emails.includes(u.email)) emails.push(u.email);
// }
// return emails.slice(0, 16);
// });
function adicionarEmailSugestao(email: string) {
const linhas = alertEmails
.split('\n')
@@ -86,12 +91,31 @@
.map((s) => s.trim())
.filter((s) => s.length > 0);
}
function limparFormularioAlertas() {
alertNomeConfig = '';
alertEmails = '';
alertSeveridadeMin = 'alto';
alertTiposAtaque = [];
alertReenvioMin = 15;
alertTemplate = 'incidente_critico';
alertUsersChat = '';
chatSeveridadeMin = 'alto';
chatTiposAtaque = [];
chatReenvioMin = 10;
enviarPorEmail = true;
enviarPorChat = false;
editarAlertConfigId = null;
}
async function salvarPreferenciasAlertas(configId?: string) {
try {
if (!alertNomeConfig.trim()) {
feedback = { tipo: 'error', mensagem: 'Por favor, informe um nome para a configuração.' };
return;
}
const canais = { email: enviarPorEmail, chat: enviarPorChat };
const resp = await client.mutation(api.security.salvarAlertConfig, {
configId: configId as Id<'alertConfigs'> | undefined,
nome: alertTemplate || 'Notificação',
nome: alertNomeConfig.trim(),
canais,
emails: parseLinhasParaArray(alertEmails),
chatUsers: parseLinhasParaArray(alertUsersChat),
@@ -100,8 +124,12 @@
reenvioMin: alertReenvioMin,
criadoPor: obterUsuarioId()
});
feedback = { tipo: 'success', mensagem: 'Preferências salvas.' };
feedback = {
tipo: 'success',
mensagem: editarAlertConfigId ? 'Configuração atualizada.' : 'Nova configuração salva.'
};
editarAlertConfigId = resp._id as Id<'alertConfigs'>;
// Não limpar o formulário após salvar, permitindo edição contínua
return resp._id;
} catch (erro: unknown) {
feedback = { tipo: 'error', mensagem: mensagemErro(erro) };
@@ -125,7 +153,8 @@
tiposAtaque?: AtaqueCiberneticoTipo[];
reenvioMin: number;
}) {
alertTemplate = config.nome ?? 'incidente_critico';
alertNomeConfig = config.nome ?? '';
alertTemplate = 'incidente_critico'; // Mantém o template padrão
enviarPorEmail = config.canais?.email ?? true;
enviarPorChat = config.canais?.chat ?? false;
alertEmails = (config.emails ?? []).join('\n');
@@ -133,7 +162,15 @@
alertSeveridadeMin = config.severidadeMin ?? 'alto';
alertTiposAtaque = (config.tiposAtaque ?? []) as string[];
alertReenvioMin = config.reenvioMin ?? 15;
chatSeveridadeMin = config.severidadeMin ?? 'alto';
chatTiposAtaque = (config.tiposAtaque ?? []) as string[];
chatReenvioMin = config.reenvioMin ?? 10;
editarAlertConfigId = config._id;
// Scroll suave até o formulário
setTimeout(() => {
const formSection = document.querySelector('[data-alert-form]');
formSection?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 100);
}
const severidadesDisponiveis: SeveridadeSeguranca[] = [
'informativo',
@@ -323,9 +360,9 @@
}
return buckets;
});
const maxBucketTotal = $derived.by(() =>
Math.max(1, ...timelineBuckets.map((b) => Object.values(b.counts).reduce((a, n) => a + n, 0)))
);
// const maxBucketTotal = $derived.by(() =>
// Math.max(1, ...timelineBuckets.map((b) => Object.values(b.counts).reduce((a, n) => a + n, 0)))
// );
function maxSeriesValue(dataset: Array<Array<number>>): number {
let max = 1;
@@ -1150,10 +1187,10 @@
<div class="bg-base-200/40 mt-6 rounded-2xl p-4">
<h3 class="text-base-content text-lg font-semibold">Realtime por tipo (60 min)</h3>
<div class="mt-3 grid grid-cols-12 gap-2">
{#each timelineBuckets as b}
{#each timelineBuckets as b (b.inicio)}
<div class="col-span-1">
<div class="bg-base-300 h-24 w-full overflow-hidden rounded">
{#each tiposParaChart as t}
{#each tiposParaChart as t (t)}
{#if b.counts[t] > 0}
<div
class="w-full"
@@ -1181,7 +1218,7 @@
{/each}
</div>
<div class="mt-3 flex flex-wrap gap-2 text-xs">
{#each tiposParaChart as t}
{#each tiposParaChart as t (t)}
<span class="badge" style={`background:${coresTipo[t]}`}>{t.replace('_', ' ')}</span>
{/each}
</div>
@@ -1200,15 +1237,13 @@
{#each [...new Map(timelineBuckets
.map((b) => Object.entries(b.topDestinos))
.flat()
.reduce((acc, [k, v]) => acc.set(k, (acc.get(k) ?? 0) + (v as number)), new Map()))].slice(0, 8) as item}
{#key item[0]}
{@const partes = item[0].split('|')}
<tr>
<td>{partes[0]}</td>
<td>{partes[1]}</td>
<td>{item[1] as number}</td>
</tr>
{/key}
.reduce((acc, [k, v]) => acc.set(k, (acc.get(k) ?? 0) + (v as number)), new Map()))].slice(0, 8) as item (item[0])}
{@const partes = item[0].split('|')}
<tr>
<td>{partes[0]}</td>
<td>{partes[1]}</td>
<td>{item[1] as number}</td>
</tr>
{/each}
</tbody>
</table>
@@ -1387,7 +1422,6 @@
mensagem: `🧹 ${resultado.removidos} eventos de teste removidos`
};
novosEventos = 0;
ultimoTotalEventos = (eventos?.data ?? []).length - (resultado.removidos ?? 0);
} catch (error) {
feedback = {
tipo: 'error',
@@ -1972,15 +2006,59 @@
</section>
<!-- Alertas e Notificações -->
<section class="border-info/20 bg-base-100/80 mt-6 rounded-3xl border p-6 shadow-2xl">
<section
class="border-info/20 bg-base-100/80 mt-6 rounded-3xl border p-6 shadow-2xl"
data-alert-form
>
<div class="mb-6 flex items-center justify-between">
<div>
<h3 class="text-info text-2xl font-bold">Alertas e Notificações</h3>
<p class="text-base-content/70 mt-1 text-sm">
Configure destinatários, níveis e tipos de alarme e reenvio para monitoramento de
segurança.
Configure múltiplas configurações de alertas. Cada configuração pode ter diferentes
destinatários, níveis e tipos de alarme.
</p>
</div>
<button
type="button"
class="btn btn-primary btn-sm gap-2"
onclick={() => limparFormularioAlertas()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg>
Nova Configuração
</button>
</div>
<!-- Nome da Configuração -->
<div class="border-base-300 bg-base-100/50 mb-6 rounded-xl border p-4">
<div class="form-control">
<label class="label py-1">
<span class="label-text text-sm font-semibold">Nome da Configuração</span>
<span class="label-text-alt text-base-content/50 text-xs"
>{editarAlertConfigId
? 'Editando configuração existente'
: 'Criando nova configuração'}</span
>
</label>
<input
type="text"
class="input input-bordered input-primary w-full"
bind:value={alertNomeConfig}
placeholder="Ex: Alertas Críticos - Equipe TI"
/>
</div>
</div>
<!-- Configurações de Notificação -->
@@ -2299,9 +2377,7 @@
}}
>
{#each usuariosParaChat?.data ?? [] as u (u._id)}
<option value={u.username ?? u.email}
>{u.nome} ({u.username ?? u.email})</option
>
<option value={u.email}>{u.nome} ({u.email})</option>
{/each}
</select>
</div>
@@ -2461,8 +2537,17 @@
</div>
</div>
<!-- Botão de Salvar -->
<div class="mb-6 flex justify-end">
<!-- Botões de Ação -->
<div class="mb-6 flex justify-end gap-3">
{#if editarAlertConfigId}
<button
type="button"
class="btn btn-ghost btn-lg"
onclick={() => limparFormularioAlertas()}
>
Cancelar Edição
</button>
{/if}
<button
type="button"
class="btn btn-info btn-lg gap-2"
@@ -2482,7 +2567,7 @@
d="M5 13l4 4L19 7"
/>
</svg>
{editarAlertConfigId ? 'Atualizar Configuração' : 'Salvar Configuração'}
{editarAlertConfigId ? 'Atualizar Configuração' : 'Salvar Nova Configuração'}
</button>
</div>