diff --git a/src/apiRoutes.tsx b/src/apiRoutes.tsx index 0b396cce..2327da70 100644 --- a/src/apiRoutes.tsx +++ b/src/apiRoutes.tsx @@ -54,9 +54,6 @@ export const apiRoutes = { plugins: '/api/plugins', atakQrString: '/api/atak_qr_string', pluginRepo: '/api/plugins/repo', - ldapLogin: '/api/ldap_login', - allGroups: '/api/groups/all', - allUsers: '/api/users/all', - groupMembers: '/api/groups/members', - userGroups: '/api/users/groups' + federationServers: '/api/federation/servers', + federationHealth: '/api/federation/health', }; diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx index e8cea5e4..98104e29 100644 --- a/src/components/Navbar/Navbar.tsx +++ b/src/components/Navbar/Navbar.tsx @@ -26,6 +26,7 @@ import { IconPlugConnected, IconPlug, IconCircleMinus, + IconShare, IconUsersGroup } from '@tabler/icons-react'; import { @@ -72,6 +73,7 @@ const adminLinks = [ { link: '/plugin_updates', label: 'Plugin Updates', icon: IconPuzzle }, { link: '/device_profiles', label: 'Device Profiles', icon: IconDeviceMobile }, { link: '/server_plugin_manager', label: 'Server Plugin Manager', icon: IconPlugConnected }, + { link: '/federation', label: 'Federation', icon: IconShare }, ]; interface ATAKQrCode { diff --git a/src/pages/Federation.tsx b/src/pages/Federation.tsx new file mode 100644 index 00000000..c7a7c908 --- /dev/null +++ b/src/pages/Federation.tsx @@ -0,0 +1,677 @@ +import { + Badge, + Button, + Checkbox, + FileButton, + Group, + Modal, + NumberInput, + Select, + Stack, + Table, + TableData, + Text, + Textarea, + TextInput, + Title, + Tooltip, +} from '@mantine/core'; +import React, { useEffect, useState } from 'react'; +import { + IconCheck, + IconCircleMinus, + IconEdit, + IconPlus, + IconRefresh, + IconToggleLeft, + IconToggleRight, + IconUpload, + IconX, +} from '@tabler/icons-react'; +import { notifications } from '@mantine/notifications'; +import axios from '../axios_config'; +import { apiRoutes } from '../apiRoutes'; + +interface FederationServer { + id: number; + name: string; + description?: string; + address: string; + port: number; + connection_type: string; + protocol_version: string; + transport_protocol: string; + use_tls: boolean; + verify_ssl: boolean; + ca_certificate?: string; + client_certificate?: string; + client_key?: string; + sync_missions: boolean; + sync_cot: boolean; + mission_filter?: any; + enabled: boolean; + status?: string; +} + +interface FederationStatus { + total_changes?: number; + sent_changes?: number; + pending_changes?: number; +} + +export default function FederationPage() { + const [federations, setFederations] = useState({ + caption: '', + head: ['Name', 'Address', 'Connection', 'Protocol', 'Transport', 'Status', 'Enabled', 'Actions'], + body: [], + }); + + // Modals + const [addModalOpened, setAddModalOpened] = useState(false); + const [editModalOpened, setEditModalOpened] = useState(false); + const [deleteModalOpened, setDeleteModalOpened] = useState(false); + const [statsModalOpened, setStatsModalOpened] = useState(false); + + // Form state + const [currentFederation, setCurrentFederation] = useState(null); + const [currentStats, setCurrentStats] = useState(null); + const [formData, setFormData] = useState({ + name: '', + description: '', + address: '', + port: 9000, + connection_type: 'OUTBOUND', + protocol_version: 'FEDERATION_V2', + transport_protocol: 'tcp', + use_tls: true, + verify_ssl: true, + ca_certificate: '', + client_certificate: '', + client_key: '', + sync_missions: true, + sync_cot: true, + mission_filter: '', + enabled: true, + }); + + useEffect(() => { + getFederations(); + }, []); + + function getFederations() { + axios + .get(apiRoutes.federationServers) + .then((r) => { + if (r.status === 200) { + const tableData: TableData = { + caption: '', + head: ['Name', 'Address', 'Connection', 'Protocol', 'Transport', 'Status', 'Enabled', 'Actions'], + body: [], + }; + + const servers = r.data.servers || []; + servers.forEach((fed: FederationServer) => { + const statusColor = + fed.status === 'connected' + ? 'green' + : fed.status === 'error' + ? 'red' + : 'gray'; + + const row = [ +
+ {fed.name} + {fed.description && ( + + {fed.description} + + )} +
, + + {fed.address}:{fed.port} + , + + {fed.connection_type.toUpperCase()} + , +
+ + {fed.protocol_version === 'v2' ? 'V2' : 'V1'} + +
, +
+ + {(fed.transport_protocol || 'tcp').toUpperCase()} + + {fed.use_tls && ( + + {fed.transport_protocol === 'tcp' ? 'TLS' : 'DTLS'} + + )} + {(fed.transport_protocol === 'udp' || fed.transport_protocol === 'multicast') && ( + + Config Only + + )} +
, + + {fed.status || 'UNKNOWN'} + , + + {fed.enabled ? 'Enabled' : 'Disabled'} + , + + + + + + + + + + + + + + , + ]; + + if (tableData.body !== undefined) { + tableData.body.push(row); + } + }); + + setFederations(tableData); + } + }) + .catch((err) => { + console.error(err); + notifications.show({ + title: 'Error', + message: err.response?.data?.error || 'Failed to load federation servers', + color: 'red', + }); + }); + } + + function handleViewStats(federation: FederationServer) { + axios + .get(`${apiRoutes.federationServers}/${federation.id}/status`) + .then((r) => { + if (r.status === 200) { + setCurrentFederation(federation); + setCurrentStats({ + total_changes: r.data.total_changes, + sent_changes: r.data.sent_changes, + pending_changes: r.data.pending_changes, + }); + setStatsModalOpened(true); + } + }) + .catch((err) => { + notifications.show({ + title: 'Error', + message: err.response?.data?.error || 'Failed to load statistics', + color: 'red', + }); + }); + } + + function handleAdd() { + const payload = { + ...formData, + mission_filter: formData.mission_filter ? JSON.parse(formData.mission_filter) : null, + }; + + axios + .post(apiRoutes.federationServers, payload) + .then((r) => { + if (r.status === 201) { + notifications.show({ + message: 'Federation server created successfully', + color: 'green', + }); + setAddModalOpened(false); + resetForm(); + getFederations(); + } + }) + .catch((err) => { + notifications.show({ + title: 'Error', + message: err.response?.data?.error || 'Failed to create federation server', + color: 'red', + }); + }); + } + + function handleEdit(federation: FederationServer) { + setCurrentFederation(federation); + setFormData({ + name: federation.name, + description: federation.description || '', + address: federation.address, + port: federation.port, + connection_type: federation.connection_type, + protocol_version: federation.protocol_version, + transport_protocol: federation.transport_protocol || 'tcp', + use_tls: federation.use_tls, + verify_ssl: federation.verify_ssl, + ca_certificate: federation.ca_certificate || '', + client_certificate: federation.client_certificate || '', + client_key: federation.client_key || '', + sync_missions: federation.sync_missions, + sync_cot: federation.sync_cot, + mission_filter: federation.mission_filter ? JSON.stringify(federation.mission_filter) : '', + enabled: federation.enabled, + }); + setEditModalOpened(true); + } + + function handleUpdate() { + if (!currentFederation) return; + + const payload = { + ...formData, + mission_filter: formData.mission_filter ? JSON.parse(formData.mission_filter) : null, + }; + + axios + .put(`${apiRoutes.federationServers}/${currentFederation.id}`, payload) + .then((r) => { + if (r.status === 200) { + notifications.show({ + message: 'Federation server updated successfully', + color: 'green', + }); + setEditModalOpened(false); + setCurrentFederation(null); + resetForm(); + getFederations(); + } + }) + .catch((err) => { + notifications.show({ + title: 'Error', + message: err.response?.data?.error || 'Failed to update federation server', + color: 'red', + }); + }); + } + + function handleDelete() { + if (!currentFederation) return; + + axios + .delete(`${apiRoutes.federationServers}/${currentFederation.id}`) + .then((r) => { + if (r.status === 200) { + notifications.show({ + message: 'Federation server deleted successfully', + color: 'green', + }); + setDeleteModalOpened(false); + setCurrentFederation(null); + getFederations(); + } + }) + .catch((err) => { + notifications.show({ + title: 'Error', + message: err.response?.data?.error || 'Failed to delete federation server', + color: 'red', + }); + }); + } + + function handleToggle(federation: FederationServer) { + axios + .put(`${apiRoutes.federationServers}/${federation.id}`, { + enabled: !federation.enabled, + }) + .then((r) => { + if (r.status === 200) { + notifications.show({ + message: r.data.enabled ? 'Federation enabled' : 'Federation disabled', + color: 'green', + }); + getFederations(); + } + }) + .catch((err) => { + notifications.show({ + title: 'Error', + message: err.response?.data?.error || 'Failed to toggle federation', + color: 'red', + }); + }); + } + + function resetForm() { + setFormData({ + name: '', + description: '', + address: '', + port: 9000, + connection_type: 'OUTBOUND', + protocol_version: 'FEDERATION_V2', + transport_protocol: 'tcp', + use_tls: true, + verify_ssl: true, + ca_certificate: '', + client_certificate: '', + client_key: '', + sync_missions: true, + sync_cot: true, + mission_filter: '', + enabled: true, + }); + } + + function handleFileUpload(file: File | null, fieldName: 'ca_certificate' | 'client_certificate' | 'client_key') { + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + const content = e.target?.result as string; + if (content) { + setFormData({ ...formData, [fieldName]: content }); + notifications.show({ + message: `${file.name} uploaded successfully`, + color: 'green', + }); + } + }; + reader.onerror = () => { + notifications.show({ + title: 'Error', + message: `Failed to read ${file.name}`, + color: 'red', + }); + }; + reader.readAsText(file); + } + + function renderFormFields() { + return ( + + setFormData({ ...formData, name: e.currentTarget.value })} + /> +