import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { Settings, ExternalLink, Pencil, Trash2, Plus, Globe, Download } from 'lucide-react';
import { parse as yamlParse } from 'yaml';
import { stringify as yamlStringify } from 'yaml';
import { environments } from '../../api';
import type { Environment } from '../../api';
import {
Breadcrumbs,
Button,
Card,
ContextMenu,
DescriptionList,
Timestamp,
UuidBadge,
} from '../../ui';
import { DeleteConfirmationModal } from '../../components/modals';
import styles from '../Page.module.css';
function EnvironmentCard({ env, onDelete }: { env: Environment; onDelete: (id: string) => void }) {
const { t } = useTranslation();
const navigate = useNavigate();
const dataEntries = Object.entries(env.data ?? {}).filter(([, v]) => v);
const handleExport = async () => {
const data = await environments.exportEnvironment(env.id);
const blob = new Blob([yamlStringify(data)], { type: 'application/yaml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `environment-${env.name}.yaml`;
a.click();
URL.revokeObjectURL(url);
};
const header = (
{env.name}
e.stopPropagation()}>
}
items={[
{
label: t('environments.action_view'),
icon: ,
onClick: () => navigate(`/environments/${env.id}`),
},
{
label: t('environments.action_edit'),
icon: ,
onClick: () => navigate(`/environments/${env.id}/edit`),
},
{
label: t('environments.action_export'),
icon: ,
onClick: () => void handleExport(),
},
{
label: t('environments.action_delete'),
icon: ,
variant: 'danger',
onClick: () => onDelete(env.id),
},
]}
/>
);
const footer = (
);
return (
navigate(`/environments/${env.id}`)}
>
{dataEntries.length > 0 && (
({ term: key, detail: value }))}
/>
)}
{dataEntries.length === 0 && (
{t('environments.no_data')}
)}
);
}
function AddEnvironmentCard() {
const { t } = useTranslation();
const navigate = useNavigate();
return (
);
}
export function EnvironmentsPage() {
const { t } = useTranslation();
usePageTitle(t('environments.title'));
const navigate = useNavigate();
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [deleteId, setDeleteId] = useState(null);
const [deleting, setDeleting] = useState(false);
const fileInputRef = useRef(null);
const load = () => {
environments
.list()
.then((res) => setItems(res.data))
.finally(() => setLoading(false));
};
useEffect(() => {
load();
}, []);
const requestDelete = (id: string) => {
setDeleteId(id);
};
const handleDelete = async () => {
if (!deleteId) return;
setDeleting(true);
try {
await environments.remove(deleteId);
setItems((prev) => prev.filter((e) => e.id !== deleteId));
setDeleteId(null);
} finally {
setDeleting(false);
}
};
const handleImport = async (e: React.ChangeEvent) => {
const file = e.target.files?.[0];
if (!file) return;
e.target.value = '';
try {
const text = await file.text();
const payload = yamlParse(text) as unknown;
const imported = await environments.importEnvironment(payload);
navigate(`/environments/${imported.id}`);
} catch {
// Import errors are silently ignored
}
};
return (
{loading ? (
{t('environments.loading')}
) : (
{items.map((env) => (
))}
)}
setDeleteId(null)}
onConfirm={handleDelete}
isDeleting={deleting}
/>
);
}