core/dashboard/src/pages/Cloudflare.tsx
2025-12-11 19:03:48 -03:00

305 lines
12 KiB
TypeScript

import { Execution, ID, Query } from 'appwrite'
import { FormEvent, useEffect, useState } from 'react'
import {
appwriteCollectionCloudflareAccountsId,
appwriteDatabaseId,
appwriteCollectionServersId,
databases,
functions,
} from '../lib/appwrite'
const badgeClass = (status: string) =>
status === 'active' ? 'rounded-full bg-emerald-500/20 px-2 py-1 text-xs text-emerald-200' : 'rounded-full bg-amber-500/20 px-2 py-1 text-xs text-amber-200'
type Zone = {
id: string
name: string
status: string
paused?: boolean
}
type Worker = {
name: string
modifiedOn?: string
active?: boolean
}
export default function Cloudflare() {
const [label, setLabel] = useState('')
const [apiKey, setApiKey] = useState('')
const [accountId, setAccountId] = useState('')
const [cloudflareAccountId, setCloudflareAccountId] = useState('')
const [credentials, setCredentials] = useState<{ $id: string; label: string }[]>([])
const [zones, setZones] = useState<Zone[]>([])
const [workers, setWorkers] = useState<Worker[]>([])
const [execution, setExecution] = useState<Execution | null>(null)
const [error, setError] = useState<string | null>(null)
const [modalOpen, setModalOpen] = useState(false)
const [newZone, setNewZone] = useState('')
const [onlineCount, setOnlineCount] = useState<number | null>(null)
useEffect(() => {
const loadCredentials = async () => {
if (!appwriteDatabaseId || !appwriteCollectionCloudflareAccountsId) return
try {
const response = await databases.listDocuments(appwriteDatabaseId, appwriteCollectionCloudflareAccountsId, [
Query.equal('provider', 'cloudflare'),
])
const docs = response.documents.map((doc) => ({ $id: doc.$id, label: (doc as { label?: string }).label || doc.$id }))
setCredentials(docs)
} catch (err) {
console.error(err)
setError('Falha ao carregar credenciais Cloudflare.')
}
}
const loadWorkers = async () => {
if (!appwriteDatabaseId || !appwriteCollectionServersId) return
try {
const servers = await databases.listDocuments<{ status?: string }>(appwriteDatabaseId, appwriteCollectionServersId, [
Query.equal('status', 'online'),
])
setOnlineCount(servers.total)
} catch (err) {
console.error(err)
}
}
loadCredentials()
loadWorkers()
}, [])
const handleSaveKey = async (event: FormEvent) => {
event.preventDefault()
setError(null)
if (!appwriteDatabaseId || !appwriteCollectionCloudflareAccountsId) {
setError('Configure VITE_APPWRITE_DATABASE_ID e VITE_APPWRITE_COLLECTION_CLOUDFLARE_ACCOUNTS_ID para salvar a chave de API.')
return
}
try {
const document = await databases.createDocument(appwriteDatabaseId, appwriteCollectionCloudflareAccountsId, ID.unique(), {
provider: 'cloudflare',
apiKey,
label,
})
setCredentials((prev) => [...prev, { $id: document.$id, label: label || document.$id }])
setLabel('')
setApiKey('')
} catch (err) {
console.error(err)
setError('Não foi possível salvar a API Key.')
}
}
const handleStatus = async () => {
setError(null)
const selectedAccount = accountId || credentials[0]?.$id
if (!selectedAccount) {
setError('Selecione ou cadastre uma credencial Cloudflare antes de consultar o status.')
return
}
try {
const executionResult = await functions.createExecution(
'check-cloudflare-status',
JSON.stringify({ accountId: selectedAccount, cloudflareAccountId }),
)
setExecution(executionResult)
const payload = executionResult.responseBody ? JSON.parse(executionResult.responseBody) : {}
setZones((payload.zones as Zone[]) || [])
setWorkers((payload.workers as Worker[]) || [])
} catch (err) {
console.error(err)
setError('Não foi possível checar o status do Cloudflare.')
}
}
const handleAddZone = () => {
if (!newZone.trim()) return
setZones((prev) => [{ id: crypto.randomUUID(), name: newZone, status: 'pending' }, ...prev])
setNewZone('')
setModalOpen(false)
}
const online = zones.some((zone) => zone.status === 'active' && !zone.paused)
return (
<div className="space-y-6">
<header className="rounded-xl border border-slate-800/70 bg-slate-900/60 p-5 shadow-lg shadow-slate-950/50">
<p className="text-xs uppercase tracking-[0.3em] text-cyan-300">Cloudflare</p>
<h1 className="mt-2 text-2xl font-semibold text-slate-50">Integração e Status</h1>
<p className="mt-2 text-sm text-slate-400">
Salve a API Key criptografada no Appwrite e consulte a função <span className="text-cyan-300">check-cloudflare-status</span>.
</p>
</header>
<div className="grid gap-4 lg:grid-cols-2">
<form className="rounded-xl border border-slate-800/70 bg-slate-900/70 p-5 shadow-inner shadow-slate-950/60" onSubmit={handleSaveKey}>
<h2 className="text-lg font-semibold text-slate-100">Salvar credencial</h2>
<p className="text-sm text-slate-400">Armazene a chave com criptografia gerenciada pelo Appwrite.</p>
<div className="mt-4 space-y-3">
<div>
<label className="text-sm text-slate-200">Label</label>
<input
type="text"
value={label}
onChange={(e) => setLabel(e.target.value)}
className="mt-1 w-full rounded-lg border border-slate-800 bg-slate-950 px-3 py-2 text-sm text-slate-100 outline-none focus:border-cyan-400"
/>
</div>
<div>
<label className="text-sm text-slate-200">API Key</label>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
required
className="mt-1 w-full rounded-lg border border-slate-800 bg-slate-950 px-3 py-2 text-sm text-slate-100 outline-none focus:border-cyan-400"
/>
</div>
<button
type="submit"
className="rounded-lg bg-cyan-500 px-4 py-2 text-sm font-semibold text-slate-900 transition hover:bg-cyan-400"
>
Salvar API Key
</button>
</div>
{error ? <p className="mt-3 text-sm text-red-300">{error}</p> : null}
</form>
<div className="rounded-xl border border-slate-800/70 bg-slate-900/70 p-5 shadow-inner shadow-slate-950/60">
<h2 className="text-lg font-semibold text-slate-100">Status das zonas</h2>
<p className="text-sm text-slate-400">Selecione a credencial e consulte o status em tempo real.</p>
<div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
<div>
<label className="text-sm text-slate-200">Credencial</label>
<select
value={accountId}
onChange={(e) => setAccountId(e.target.value)}
className="mt-1 w-full rounded-lg border border-slate-800 bg-slate-950 px-3 py-2 text-sm text-slate-100 outline-none focus:border-cyan-400"
>
<option value="">Selecione um documento</option>
{credentials.map((cred) => (
<option key={cred.$id} value={cred.$id}>
{cred.label}
</option>
))}
</select>
</div>
<div>
<label className="text-sm text-slate-200">Cloudflare Account ID</label>
<input
type="text"
value={cloudflareAccountId}
onChange={(e) => setCloudflareAccountId(e.target.value)}
className="mt-1 w-full rounded-lg border border-slate-800 bg-slate-950 px-3 py-2 text-sm text-slate-100 outline-none focus:border-cyan-400"
/>
</div>
</div>
<div className="mt-4 flex items-center gap-3">
<button
type="button"
onClick={handleStatus}
className="rounded-lg bg-cyan-500 px-4 py-2 text-sm font-semibold text-slate-900 transition hover:bg-cyan-400"
>
Consultar status
</button>
<button
type="button"
onClick={() => setModalOpen(true)}
className="rounded-lg border border-cyan-500/60 px-4 py-2 text-sm text-cyan-200 transition hover:bg-cyan-500/10"
>
Adicionar Nova Zona
</button>
</div>
{execution ? (
<div className="mt-3 rounded-lg border border-slate-800 bg-slate-950/60 p-3 text-xs text-slate-300">
Execução #{execution.$id} Status {execution.status}
</div>
) : null}
</div>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<div className="rounded-xl border border-slate-800/70 bg-slate-900/70 p-5">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-slate-100">Zonas DNS</h3>
<span className={online ? 'text-xs text-emerald-300' : 'text-xs text-amber-300'}>
{online ? 'Online' : 'Aguardando verificação'}
</span>
</div>
<ul className="mt-3 divide-y divide-slate-800 text-sm text-slate-200">
{zones.length === 0 ? <li className="py-3 text-slate-500">Nenhuma zona carregada.</li> : null}
{zones.map((zone) => (
<li key={zone.id} className="flex items-center justify-between py-3">
<div>
<p className="font-semibold">{zone.name}</p>
<p className="text-xs text-slate-400">ID: {zone.id}</p>
</div>
<span className={badgeClass(zone.status)}>{zone.status}</span>
</li>
))}
</ul>
</div>
<div className="rounded-xl border border-slate-800/70 bg-slate-900/70 p-5">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-slate-100">Workers</h3>
<span className="text-xs text-slate-400">Ativos no Appwrite: {onlineCount ?? '---'}</span>
</div>
<ul className="mt-3 divide-y divide-slate-800 text-sm text-slate-200">
{workers.length === 0 ? <li className="py-3 text-slate-500">Nenhum worker listado.</li> : null}
{workers.map((worker) => (
<li key={worker.name} className="flex items-center justify-between py-3">
<div>
<p className="font-semibold">{worker.name}</p>
<p className="text-xs text-slate-400">{worker.modifiedOn}</p>
</div>
<span className={worker.active ? 'text-xs text-emerald-300' : 'text-xs text-slate-400'}>
{worker.active ? 'Ativo' : 'Inativo'}
</span>
</li>
))}
</ul>
</div>
</div>
{modalOpen ? (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4">
<div className="w-full max-w-md rounded-xl border border-slate-800 bg-slate-950 p-6 shadow-2xl shadow-slate-900">
<h4 className="text-lg font-semibold text-slate-100">Adicionar Nova Zona</h4>
<p className="mt-1 text-sm text-slate-400">Inclua uma entrada manual enquanto a função sincroniza zonas.</p>
<input
type="text"
value={newZone}
onChange={(e) => setNewZone(e.target.value)}
placeholder="example.com"
className="mt-3 w-full rounded-lg border border-slate-800 bg-slate-900 px-3 py-2 text-sm text-slate-100 outline-none focus:border-cyan-400"
/>
<div className="mt-4 flex items-center justify-end gap-3">
<button
type="button"
onClick={() => setModalOpen(false)}
className="rounded-lg px-4 py-2 text-sm text-slate-300 hover:text-slate-50"
>
Cancelar
</button>
<button
type="button"
onClick={handleAddZone}
className="rounded-lg bg-cyan-500 px-4 py-2 text-sm font-semibold text-slate-900 hover:bg-cyan-400"
>
Adicionar
</button>
</div>
</div>
</div>
) : null}
</div>
)
}