- add Card header prop (primary/secondary variants) and footer prop (sticks to bottom) - add Breadcrumbs to all 5 pages; remove duplicate h1 headings - add pageToolbar layout for breadcrumbs + danger button aligned right - add DescriptionList component replacing raw dl/dt/dd markup; supports truncate+tooltip - rewrite ContextMenu to render dropdown via React portal to avoid clip/overflow issues - update EnvironmentCard to use Card header (primary), footer (Timestamp), and truncated DescriptionList - fix cog button contrast, square sizing, and right-edge alignment on colored header - add border to Timestamp pill for dark-mode visibility - add id badge pill styling to environment card header
96 lines
2.9 KiB
TypeScript
96 lines
2.9 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Trash2 } from 'lucide-react';
|
|
import { environments } from '../api';
|
|
import type { Environment } from '../api';
|
|
import { Breadcrumbs, Button, Card, DescriptionList, Timestamp } from '../ui';
|
|
import styles from './Page.module.css';
|
|
|
|
export function EnvironmentDetailPage() {
|
|
const { t } = useTranslation();
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const [env, setEnv] = useState<Environment | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!id) return;
|
|
environments
|
|
.get(Number(id))
|
|
.then(setEnv)
|
|
.catch((err: Error) => setError(err.message))
|
|
.finally(() => setLoading(false));
|
|
}, [id]);
|
|
|
|
const handleDelete = async () => {
|
|
if (!env) return;
|
|
await environments.remove(env.id);
|
|
navigate('/environments');
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div className={styles.pageToolbar}>
|
|
<Breadcrumbs
|
|
items={[
|
|
{ label: t('environments.title'), onClick: () => navigate('/environments') },
|
|
{ label: env?.name ?? `#${id}` },
|
|
]}
|
|
/>
|
|
{env && (
|
|
<Button variant="danger" size="sm" onClick={handleDelete}>
|
|
<Trash2 size={14} />
|
|
{t('environments.action_delete')}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{error && <p className={styles.error}>{error}</p>}
|
|
|
|
{loading && <p className={styles.muted}>{t('environments.loading')}</p>}
|
|
|
|
{env && (
|
|
<>
|
|
<div className={styles.detailMeta}>
|
|
<DescriptionList
|
|
items={[
|
|
{ term: t('environments.field_id'), detail: env.id },
|
|
{
|
|
term: t('environments.field_created'),
|
|
detail: <Timestamp value={env.createdAt} />,
|
|
},
|
|
{
|
|
term: t('environments.field_updated'),
|
|
detail: <Timestamp value={env.updatedAt} />,
|
|
},
|
|
]}
|
|
/>
|
|
</div>
|
|
|
|
<h2 className={styles.sectionHeading}>{t('environments.section_urls')}</h2>
|
|
<Card>
|
|
{Object.entries(env.urls).filter(([, v]) => v).length === 0 ? (
|
|
<p className={styles.muted}>{t('environments.no_urls')}</p>
|
|
) : (
|
|
<DescriptionList
|
|
items={Object.entries(env.urls)
|
|
.filter(([, v]) => v)
|
|
.map(([key, value]) => ({
|
|
term: key,
|
|
detail: (
|
|
<a href={value} target="_blank" rel="noopener noreferrer">
|
|
{value}
|
|
</a>
|
|
),
|
|
}))}
|
|
/>
|
|
)}
|
|
</Card>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|