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:
@@ -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]}
|
||||
.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>
|
||||
{/key}
|
||||
{/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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user