Files
sgse-app/apps/web/src/lib/components/ti/AlertConfigModal.svelte
killer-cf 11eef4aa2a refactor: improve Svelte components and enhance user experience
- Updated various Svelte components to improve code readability and maintainability.
- Standardized button classes across components for a consistent user interface.
- Enhanced error handling and user feedback in modals and forms.
- Cleaned up unnecessary imports and optimized component structure for better performance.
2025-11-12 16:36:29 -03:00

480 lines
13 KiB
Svelte

<script lang="ts">
import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
let { onClose }: { onClose: () => void } = $props();
const client = useConvexClient();
const alertasQuery = useQuery(api.monitoramento.listarAlertas, {});
const alertas = $derived.by(() => {
if (!alertasQuery) return [];
// O useQuery pode retornar o array diretamente ou em .data
if (Array.isArray(alertasQuery)) return alertasQuery;
return alertasQuery.data ?? [];
});
// Estado para novo alerta
let editingAlertId = $state<Id<'alertConfigurations'> | null>(null);
let metricName = $state('cpuUsage');
let threshold = $state(80);
let operator = $state<'>' | '<' | '>=' | '<=' | '=='>('>');
let enabled = $state(true);
let notifyByEmail = $state(false);
let notifyByChat = $state(true);
let saving = $state(false);
let showForm = $state(false);
const metricOptions = [
{ value: 'cpuUsage', label: 'Uso de CPU (%)' },
{ value: 'memoryUsage', label: 'Uso de Memória (%)' },
{ value: 'networkLatency', label: 'Latência de Rede (ms)' },
{ value: 'storageUsed', label: 'Armazenamento Usado (%)' },
{ value: 'usuariosOnline', label: 'Usuários Online' },
{ value: 'mensagensPorMinuto', label: 'Mensagens por Minuto' },
{ value: 'tempoRespostaMedio', label: 'Tempo de Resposta (ms)' },
{ value: 'errosCount', label: 'Contagem de Erros' }
];
const operatorOptions = [
{ value: '>', label: 'Maior que (>)' },
{ value: '>=', label: 'Maior ou igual (≥)' },
{ value: '<', label: 'Menor que (<)' },
{ value: '<=', label: 'Menor ou igual (≤)' },
{ value: '==', label: 'Igual a (=)' }
];
function resetForm() {
editingAlertId = null;
metricName = 'cpuUsage';
threshold = 80;
operator = '>';
enabled = true;
notifyByEmail = false;
notifyByChat = true;
showForm = false;
}
function editAlert(alert: any) {
editingAlertId = alert._id;
metricName = alert.metricName;
threshold = alert.threshold;
operator = alert.operator;
enabled = alert.enabled;
notifyByEmail = alert.notifyByEmail;
notifyByChat = alert.notifyByChat;
showForm = true;
}
async function saveAlert() {
saving = true;
try {
await client.mutation(api.monitoramento.configurarAlerta, {
alertId: editingAlertId || undefined,
metricName,
threshold,
operator,
enabled,
notifyByEmail,
notifyByChat
});
resetForm();
} catch (error) {
console.error('Erro ao salvar alerta:', error);
alert('Erro ao salvar alerta. Tente novamente.');
} finally {
saving = false;
}
}
async function deleteAlert(alertId: Id<'alertConfigurations'>) {
if (!confirm('Tem certeza que deseja deletar este alerta?')) return;
try {
await client.mutation(api.monitoramento.deletarAlerta, { alertId });
} catch (error) {
console.error('Erro ao deletar alerta:', error);
alert('Erro ao deletar alerta. Tente novamente.');
}
}
function getMetricLabel(metricName: string): string {
return metricOptions.find((m) => m.value === metricName)?.label || metricName;
}
function getOperatorLabel(op: string): string {
return operatorOptions.find((o) => o.value === op)?.label || op;
}
</script>
<dialog class="modal modal-open">
<div class="modal-box from-base-100 to-base-200 max-w-4xl bg-linear-to-br">
<button
type="button"
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
onclick={onClose}
>
</button>
<h3 class="text-primary mb-2 text-3xl font-bold">⚙️ Configuração de Alertas</h3>
<p class="text-base-content/60 mb-6">
Configure alertas personalizados para monitoramento do sistema
</p>
<!-- Botão Novo Alerta -->
{#if !showForm}
<button type="button" class="btn btn-primary mb-6" onclick={() => (showForm = true)}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg>
Novo Alerta
</button>
{/if}
<!-- Formulário de Alerta -->
{#if showForm}
<div class="card bg-base-100 border-primary/20 mb-6 border-2 shadow-xl">
<div class="card-body">
<h4 class="card-title text-xl">
{editingAlertId ? 'Editar Alerta' : 'Novo Alerta'}
</h4>
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<!-- Métrica -->
<div class="form-control">
<label class="label" for="metric">
<span class="label-text font-semibold">Métrica</span>
</label>
<select
id="metric"
class="select select-bordered select-primary"
bind:value={metricName}
>
{#each metricOptions as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</div>
<!-- Operador -->
<div class="form-control">
<label class="label" for="operator">
<span class="label-text font-semibold">Condição</span>
</label>
<select
id="operator"
class="select select-bordered select-primary"
bind:value={operator}
>
{#each operatorOptions as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</div>
<!-- Threshold -->
<div class="form-control">
<label class="label" for="threshold">
<span class="label-text font-semibold">Valor Limite</span>
</label>
<input
id="threshold"
type="number"
class="input input-bordered input-primary"
bind:value={threshold}
min="0"
step="1"
/>
</div>
<!-- Ativo -->
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<span class="label-text font-semibold">Alerta Ativo</span>
<input type="checkbox" class="toggle toggle-primary" bind:checked={enabled} />
</label>
</div>
</div>
<!-- Notificações -->
<div class="divider">Método de Notificação</div>
<div class="flex gap-6">
<label class="label cursor-pointer gap-3">
<input
type="checkbox"
class="checkbox checkbox-primary"
bind:checked={notifyByChat}
/>
<span class="label-text">
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 inline h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
/>
</svg>
Notificar por Chat
</span>
</label>
<label class="label cursor-pointer gap-3">
<input
type="checkbox"
class="checkbox checkbox-secondary"
bind:checked={notifyByEmail}
/>
<span class="label-text">
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 inline h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
Notificar por E-mail
</span>
</label>
</div>
<!-- Preview -->
<div class="alert alert-info mt-4">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<div>
<h4 class="font-bold">Preview do Alerta:</h4>
<p class="text-sm">
Alertar quando <strong>{getMetricLabel(metricName)}</strong> for
<strong>{getOperatorLabel(operator)}</strong> a
<strong>{threshold}</strong>
</p>
</div>
</div>
<!-- Botões -->
<div class="card-actions mt-4 justify-end">
<button type="button" class="btn" onclick={resetForm} disabled={saving}>
Cancelar
</button>
<button
type="button"
class="btn btn-primary"
onclick={saveAlert}
disabled={saving || (!notifyByChat && !notifyByEmail)}
>
{#if saving}
<span class="loading loading-spinner"></span>
Salvando...
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
Salvar Alerta
{/if}
</button>
</div>
</div>
</div>
{/if}
<!-- Lista de Alertas -->
<div class="divider">Alertas Configurados</div>
{#if alertas.length > 0}
<div class="overflow-x-auto">
<table class="table-zebra table">
<thead>
<tr>
<th>Métrica</th>
<th>Condição</th>
<th>Status</th>
<th>Notificações</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{#each alertas as alerta}
<tr class={!alerta.enabled ? 'opacity-50' : ''}>
<td>
<div class="font-semibold">
{getMetricLabel(alerta.metricName)}
</div>
</td>
<td>
<div class="badge badge-outline">
{getOperatorLabel(alerta.operator)}
{alerta.threshold}
</div>
</td>
<td>
{#if alerta.enabled}
<div class="badge badge-success gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3 w-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
Ativo
</div>
{:else}
<div class="badge badge-ghost gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3 w-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
Inativo
</div>
{/if}
</td>
<td>
<div class="flex gap-1">
{#if alerta.notifyByChat}
<div class="badge badge-primary badge-sm">Chat</div>
{/if}
{#if alerta.notifyByEmail}
<div class="badge badge-secondary badge-sm">Email</div>
{/if}
</div>
</td>
<td>
<div class="flex gap-2">
<button type="button" class="btn btn-xs" onclick={() => editAlert(alerta)}>
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
</button>
<button
type="button"
class="btn btn-xs text-error"
onclick={() => deleteAlert(alerta._id)}
>
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{:else}
<div class="alert">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-info h-6 w-6 shrink-0"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>Nenhum alerta configurado. Clique em "Novo Alerta" para criar um.</span>
</div>
{/if}
<div class="modal-action">
<button type="button" class="btn btn-lg" onclick={onClose}>Fechar</button>
</div>
</div>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<form method="dialog" class="modal-backdrop" onclick={onClose}>
<button type="button">close</button>
</form>
</dialog>