@@ -0,0 +1,190 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Flex,
|
||||
Heading,
|
||||
HStack,
|
||||
Link,
|
||||
Spacer,
|
||||
Spinner,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useToast,
|
||||
} from '@chakra-ui/react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { api, ApiError, type Demo } from '../../lib/api'
|
||||
import { statusColor, statusLabel, timeRemaining } from '../../lib/format'
|
||||
import { ConfirmDialog } from '../../components/ConfirmDialog'
|
||||
|
||||
// Un provisioning en cours => on rafraîchit régulièrement.
|
||||
const POLL_MS = 5000
|
||||
|
||||
export function Demos() {
|
||||
const toast = useToast()
|
||||
const navigate = useNavigate()
|
||||
const [demos, setDemos] = useState<Demo[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [busy, setBusy] = useState<string | null>(null)
|
||||
const [toDelete, setToDelete] = useState<Demo | null>(null)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const res = await api.listDemos()
|
||||
setDemos(res.items ?? [])
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 401) navigate('/login')
|
||||
else toast({ status: 'error', title: 'Chargement des démos impossible' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [navigate, toast])
|
||||
|
||||
useEffect(() => {
|
||||
void load()
|
||||
const id = setInterval(() => void load(), POLL_MS)
|
||||
return () => clearInterval(id)
|
||||
}, [load])
|
||||
|
||||
const create = async () => {
|
||||
setBusy('new')
|
||||
try {
|
||||
await api.createDemo()
|
||||
toast({ status: 'success', title: 'Démo lancée' })
|
||||
await load()
|
||||
} catch (err) {
|
||||
const msg = err instanceof ApiError ? err.message : 'Erreur'
|
||||
toast({ status: 'error', title: 'Lancement impossible', description: msg })
|
||||
} finally {
|
||||
setBusy(null)
|
||||
}
|
||||
}
|
||||
|
||||
const extend = async (d: Demo) => {
|
||||
setBusy(d.id)
|
||||
try {
|
||||
await api.extendDemo(d.id)
|
||||
toast({ status: 'success', title: 'Démo prolongée de 30 jours' })
|
||||
await load()
|
||||
} catch (err) {
|
||||
const msg = err instanceof ApiError ? err.message : 'Erreur'
|
||||
toast({ status: 'error', title: 'Prolongation impossible', description: msg })
|
||||
} finally {
|
||||
setBusy(null)
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDestroy = async () => {
|
||||
if (!toDelete) return
|
||||
const d = toDelete
|
||||
setBusy(d.id)
|
||||
try {
|
||||
await api.deleteDemo(d.id)
|
||||
toast({ status: 'success', title: 'Démo détruite' })
|
||||
setToDelete(null)
|
||||
await load()
|
||||
} catch (err) {
|
||||
const msg = err instanceof ApiError ? err.message : 'Erreur'
|
||||
toast({ status: 'error', title: 'Destruction impossible', description: msg })
|
||||
} finally {
|
||||
setBusy(null)
|
||||
}
|
||||
}
|
||||
|
||||
const isAlive = (s: Demo['status']) => s !== 'expired' && s !== 'failed'
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex mb={6} align="center">
|
||||
<Heading size="md">Démos</Heading>
|
||||
<Spacer />
|
||||
<Button colorScheme="primary" isLoading={busy === 'new'} onClick={create}>
|
||||
Nouvelle démo
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{loading ? (
|
||||
<Spinner />
|
||||
) : demos.length === 0 ? (
|
||||
<Text color="gray.500">Aucune démo active. Lancez-en une depuis un lead ou ci-dessus.</Text>
|
||||
) : (
|
||||
<TableContainer borderWidth="1px" borderRadius="lg">
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Namespace</Th>
|
||||
<Th>Statut</Th>
|
||||
<Th>URL</Th>
|
||||
<Th>Expire dans</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{demos.map((d) => (
|
||||
<Tr key={d.id}>
|
||||
<Td fontFamily="mono">{d.namespace}</Td>
|
||||
<Td>
|
||||
<Badge colorScheme={statusColor(d.status)}>{statusLabel(d.status)}</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
{d.status === 'ready' ? (
|
||||
<Link href={d.url} color="primary.500" isExternal>
|
||||
{d.url}
|
||||
</Link>
|
||||
) : (
|
||||
<Text color="gray.400">—</Text>
|
||||
)}
|
||||
</Td>
|
||||
<Td>{isAlive(d.status) ? timeRemaining(d.expires_at) : '—'}</Td>
|
||||
<Td textAlign="right">
|
||||
<HStack justify="flex-end">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
isDisabled={!isAlive(d.status) || busy === d.id}
|
||||
onClick={() => extend(d)}
|
||||
>
|
||||
+30 j
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="red"
|
||||
variant="outline"
|
||||
isDisabled={!isAlive(d.status)}
|
||||
onClick={() => setToDelete(d)}
|
||||
>
|
||||
Détruire
|
||||
</Button>
|
||||
</HStack>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={!!toDelete}
|
||||
title="Détruire la démo ?"
|
||||
confirmLabel="Détruire"
|
||||
isLoading={!!toDelete && busy === toDelete.id}
|
||||
onConfirm={confirmDestroy}
|
||||
onClose={() => setToDelete(null)}
|
||||
>
|
||||
La démo{' '}
|
||||
<Text as="span" fontFamily="mono" fontWeight="semibold">
|
||||
{toDelete?.namespace}
|
||||
</Text>{' '}
|
||||
et toutes ses données seront supprimées définitivement. Les ressources du pool seront
|
||||
libérées. Cette action est irréversible.
|
||||
</ConfirmDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user