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 }); const alertConfigs = useQuery(api.security.listarAlertConfigs, { limit: 100 });
// Estado local para preferências de alertas // Estado local para preferências de alertas
let alertNomeConfig = $state('');
let alertEmails = $state(''); let alertEmails = $state('');
let alertSeveridadeMin: SeveridadeSeguranca = 'alto'; let alertSeveridadeMin = $state<SeveridadeSeguranca>('alto');
let alertTiposAtaque: string[] = []; let alertTiposAtaque = $state<string[]>([]);
let alertReenvioMin = $state(15); let alertReenvioMin = $state(15);
let alertTemplate = $state('incidente_critico'); let alertTemplate = $state('incidente_critico');
let alertUsersChat = $state(''); let alertUsersChat = $state('');
let chatSeveridadeMin: SeveridadeSeguranca = 'alto'; let chatSeveridadeMin = $state<SeveridadeSeguranca>('alto');
let chatTiposAtaque: string[] = []; let chatTiposAtaque = $state<string[]>([]);
let chatReenvioMin = $state(10); let chatReenvioMin = $state(10);
let enviarPorEmail = $state(true); let enviarPorEmail = $state(true);
let enviarPorChat = $state(false); let enviarPorChat = $state(false);
let editarAlertConfigId: Id<'alertConfigs'> | null = null; let editarAlertConfigId = $state<Id<'alertConfigs'> | null>(null);
// Sugestões a partir de configs salvas // Sugestões a partir de configs salvas (podem ser usadas no futuro para autocomplete)
const sugestoesEmails = $derived.by(() => { // const sugestoesEmails = $derived.by(() => {
const set = new Set<string>(); // const emails: string[] = [];
for (const cfg of alertConfigs?.data ?? []) { // for (const cfg of alertConfigs?.data ?? []) {
for (const e of cfg.emails ?? []) set.add(e); // for (const e of cfg.emails ?? []) {
} // if (!emails.includes(e)) emails.push(e);
for (const u of usuariosQuery?.data ?? []) { // }
if (u?.email) set.add(u.email); // }
} // for (const u of usuariosQuery?.data ?? []) {
return Array.from(set).slice(0, 16); // if (u?.email && !emails.includes(u.email)) emails.push(u.email);
}); // }
const sugestoesChatUsers = $derived.by(() => { // return emails.slice(0, 16);
const set = new Set<string>(); // });
for (const cfg of alertConfigs?.data ?? []) { // const sugestoesChatUsers = $derived.by(() => {
for (const u of cfg.chatUsers ?? []) set.add(u); // const emails: string[] = [];
} // for (const cfg of alertConfigs?.data ?? []) {
for (const u of usuariosQuery?.data ?? []) { // for (const u of cfg.chatUsers ?? []) {
if (u?.username) set.add(u.username); // if (!emails.includes(u)) emails.push(u);
} // }
return Array.from(set).slice(0, 16); // }
}); // 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) { function adicionarEmailSugestao(email: string) {
const linhas = alertEmails const linhas = alertEmails
.split('\n') .split('\n')
@@ -86,12 +91,31 @@
.map((s) => s.trim()) .map((s) => s.trim())
.filter((s) => s.length > 0); .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) { async function salvarPreferenciasAlertas(configId?: string) {
try { 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 canais = { email: enviarPorEmail, chat: enviarPorChat };
const resp = await client.mutation(api.security.salvarAlertConfig, { const resp = await client.mutation(api.security.salvarAlertConfig, {
configId: configId as Id<'alertConfigs'> | undefined, configId: configId as Id<'alertConfigs'> | undefined,
nome: alertTemplate || 'Notificação', nome: alertNomeConfig.trim(),
canais, canais,
emails: parseLinhasParaArray(alertEmails), emails: parseLinhasParaArray(alertEmails),
chatUsers: parseLinhasParaArray(alertUsersChat), chatUsers: parseLinhasParaArray(alertUsersChat),
@@ -100,8 +124,12 @@
reenvioMin: alertReenvioMin, reenvioMin: alertReenvioMin,
criadoPor: obterUsuarioId() 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'>; editarAlertConfigId = resp._id as Id<'alertConfigs'>;
// Não limpar o formulário após salvar, permitindo edição contínua
return resp._id; return resp._id;
} catch (erro: unknown) { } catch (erro: unknown) {
feedback = { tipo: 'error', mensagem: mensagemErro(erro) }; feedback = { tipo: 'error', mensagem: mensagemErro(erro) };
@@ -125,7 +153,8 @@
tiposAtaque?: AtaqueCiberneticoTipo[]; tiposAtaque?: AtaqueCiberneticoTipo[];
reenvioMin: number; reenvioMin: number;
}) { }) {
alertTemplate = config.nome ?? 'incidente_critico'; alertNomeConfig = config.nome ?? '';
alertTemplate = 'incidente_critico'; // Mantém o template padrão
enviarPorEmail = config.canais?.email ?? true; enviarPorEmail = config.canais?.email ?? true;
enviarPorChat = config.canais?.chat ?? false; enviarPorChat = config.canais?.chat ?? false;
alertEmails = (config.emails ?? []).join('\n'); alertEmails = (config.emails ?? []).join('\n');
@@ -133,7 +162,15 @@
alertSeveridadeMin = config.severidadeMin ?? 'alto'; alertSeveridadeMin = config.severidadeMin ?? 'alto';
alertTiposAtaque = (config.tiposAtaque ?? []) as string[]; alertTiposAtaque = (config.tiposAtaque ?? []) as string[];
alertReenvioMin = config.reenvioMin ?? 15; alertReenvioMin = config.reenvioMin ?? 15;
chatSeveridadeMin = config.severidadeMin ?? 'alto';
chatTiposAtaque = (config.tiposAtaque ?? []) as string[];
chatReenvioMin = config.reenvioMin ?? 10;
editarAlertConfigId = config._id; 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[] = [ const severidadesDisponiveis: SeveridadeSeguranca[] = [
'informativo', 'informativo',
@@ -323,9 +360,9 @@
} }
return buckets; return buckets;
}); });
const maxBucketTotal = $derived.by(() => // const maxBucketTotal = $derived.by(() =>
Math.max(1, ...timelineBuckets.map((b) => Object.values(b.counts).reduce((a, n) => a + n, 0))) // Math.max(1, ...timelineBuckets.map((b) => Object.values(b.counts).reduce((a, n) => a + n, 0)))
); // );
function maxSeriesValue(dataset: Array<Array<number>>): number { function maxSeriesValue(dataset: Array<Array<number>>): number {
let max = 1; let max = 1;
@@ -1150,10 +1187,10 @@
<div class="bg-base-200/40 mt-6 rounded-2xl p-4"> <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> <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"> <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="col-span-1">
<div class="bg-base-300 h-24 w-full overflow-hidden rounded"> <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} {#if b.counts[t] > 0}
<div <div
class="w-full" class="w-full"
@@ -1181,7 +1218,7 @@
{/each} {/each}
</div> </div>
<div class="mt-3 flex flex-wrap gap-2 text-xs"> <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> <span class="badge" style={`background:${coresTipo[t]}`}>{t.replace('_', ' ')}</span>
{/each} {/each}
</div> </div>
@@ -1200,15 +1237,13 @@
{#each [...new Map(timelineBuckets {#each [...new Map(timelineBuckets
.map((b) => Object.entries(b.topDestinos)) .map((b) => Object.entries(b.topDestinos))
.flat() .flat()
.reduce((acc, [k, v]) => acc.set(k, (acc.get(k) ?? 0) + (v as number)), new Map()))].slice(0, 8) as item} .reduce((acc, [k, v]) => acc.set(k, (acc.get(k) ?? 0) + (v as number)), new Map()))].slice(0, 8) as item (item[0])}
{#key item[0]}
{@const partes = item[0].split('|')} {@const partes = item[0].split('|')}
<tr> <tr>
<td>{partes[0]}</td> <td>{partes[0]}</td>
<td>{partes[1]}</td> <td>{partes[1]}</td>
<td>{item[1] as number}</td> <td>{item[1] as number}</td>
</tr> </tr>
{/key}
{/each} {/each}
</tbody> </tbody>
</table> </table>
@@ -1387,7 +1422,6 @@
mensagem: `🧹 ${resultado.removidos} eventos de teste removidos` mensagem: `🧹 ${resultado.removidos} eventos de teste removidos`
}; };
novosEventos = 0; novosEventos = 0;
ultimoTotalEventos = (eventos?.data ?? []).length - (resultado.removidos ?? 0);
} catch (error) { } catch (error) {
feedback = { feedback = {
tipo: 'error', tipo: 'error',
@@ -1972,15 +2006,59 @@
</section> </section>
<!-- Alertas e Notificações --> <!-- 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 class="mb-6 flex items-center justify-between">
<div> <div>
<h3 class="text-info text-2xl font-bold">Alertas e Notificações</h3> <h3 class="text-info text-2xl font-bold">Alertas e Notificações</h3>
<p class="text-base-content/70 mt-1 text-sm"> <p class="text-base-content/70 mt-1 text-sm">
Configure destinatários, níveis e tipos de alarme e reenvio para monitoramento de Configure múltiplas configurações de alertas. Cada configuração pode ter diferentes
segurança. destinatários, níveis e tipos de alarme.
</p> </p>
</div> </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> </div>
<!-- Configurações de Notificação --> <!-- Configurações de Notificação -->
@@ -2299,9 +2377,7 @@
}} }}
> >
{#each usuariosParaChat?.data ?? [] as u (u._id)} {#each usuariosParaChat?.data ?? [] as u (u._id)}
<option value={u.username ?? u.email} <option value={u.email}>{u.nome} ({u.email})</option>
>{u.nome} ({u.username ?? u.email})</option
>
{/each} {/each}
</select> </select>
</div> </div>
@@ -2461,8 +2537,17 @@
</div> </div>
</div> </div>
<!-- Botão de Salvar --> <!-- Botões de Ação -->
<div class="mb-6 flex justify-end"> <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 <button
type="button" type="button"
class="btn btn-info btn-lg gap-2" class="btn btn-info btn-lg gap-2"
@@ -2482,7 +2567,7 @@
d="M5 13l4 4L19 7" d="M5 13l4 4L19 7"
/> />
</svg> </svg>
{editarAlertConfigId ? 'Atualizar Configuração' : 'Salvar Configuração'} {editarAlertConfigId ? 'Atualizar Configuração' : 'Salvar Nova Configuração'}
</button> </button>
</div> </div>