feat: add new page and update
ci-web / test (push) Canceled after 4m40s

This commit is contained in:
Nuxgrid
2026-08-01 23:27:22 +02:00
parent 5d2a132c3c
commit cb966fcaf0
7 changed files with 319 additions and 71 deletions
+10 -51
View File
@@ -5,12 +5,9 @@ import {
Button,
Collapse,
Flex,
FormControl,
FormLabel,
Heading,
HStack,
Icon,
Input,
Link,
Progress,
SimpleGrid,
@@ -31,6 +28,7 @@ import { useNavigate } from 'react-router-dom'
import { api, ApiError, type ComponentState, type Demo, type DemoDetails } from '../../lib/api'
import { podStatusColor, podStatusLabel, statusColor, statusLabel, timeRemaining } from '../../lib/format'
import { ConfirmDialog } from '../../components/ConfirmDialog'
import { CreateDemoModal } from '../../components/CreateDemoModal'
// Un provisioning en cours => on rafraîchit régulièrement.
const POLL_MS = 5000
@@ -51,10 +49,7 @@ export function Demos() {
const [loading, setLoading] = useState(true)
const [busy, setBusy] = useState<string | null>(null)
const [toDelete, setToDelete] = useState<Demo | null>(null)
// --- Formulaire de création ---
const [newUsername, setNewUsername] = useState('')
const [newLeadId, setNewLeadId] = useState('')
const [createModalOpen, setCreateModalOpen] = useState(false)
// --- Ligne dépliée (état live des pods) ---
const [expandedId, setExpandedId] = useState<string | null>(null)
@@ -79,26 +74,6 @@ export function Demos() {
return () => clearInterval(id)
}, [load])
const create = async () => {
if (!newUsername.trim()) {
toast({ status: 'warning', title: 'Username requis' })
return
}
setBusy('new')
try {
await api.createDemo(newUsername.trim(), newLeadId.trim() || undefined)
toast({ status: 'success', title: 'Démo lancée' })
setNewUsername('')
setNewLeadId('')
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 {
@@ -173,38 +148,22 @@ export function Demos() {
return (
<>
<Flex mb={6} align="flex-end" gap={4} wrap="wrap">
<Flex mb={6} align="center" gap={4} wrap="wrap">
<Heading size="md" mr={4}>
Démos
</Heading>
<Spacer />
<FormControl maxW="220px">
<FormLabel fontSize="sm" mb={1}>
Username
</FormLabel>
<Input
size="sm"
placeholder="ex: acme-corp"
value={newUsername}
onChange={(e) => setNewUsername(e.target.value)}
/>
</FormControl>
<FormControl maxW="220px">
<FormLabel fontSize="sm" mb={1}>
Lead ID (optionnel)
</FormLabel>
<Input
size="sm"
placeholder="uuid du lead"
value={newLeadId}
onChange={(e) => setNewLeadId(e.target.value)}
/>
</FormControl>
<Button colorScheme="primary" isLoading={busy === 'new'} onClick={create}>
<Button colorScheme="primary" onClick={() => setCreateModalOpen(true)}>
Nouvelle démo
</Button>
</Flex>
<CreateDemoModal
isOpen={createModalOpen}
onClose={() => setCreateModalOpen(false)}
onCreated={() => void load()}
/>
{loading ? (
<Spinner />
) : demos.length === 0 ? (
+13 -17
View File
@@ -19,6 +19,7 @@ import { useNavigate } from 'react-router-dom'
import { api, ApiError, type Lead } from '../../lib/api'
import { formatDate } from '../../lib/format'
import { useAuth } from '../../lib/auth'
import { CreateDemoModal } from '../../components/CreateDemoModal'
export function Leads() {
const toast = useToast()
@@ -26,7 +27,7 @@ export function Leads() {
const { isAdmin } = useAuth()
const [leads, setLeads] = useState<Lead[]>([])
const [loading, setLoading] = useState(true)
const [launching, setLaunching] = useState<string | null>(null)
const [launchingFor, setLaunchingFor] = useState<Lead | null>(null)
const load = useCallback(async () => {
try {
@@ -44,20 +45,6 @@ export function Leads() {
void load()
}, [load])
const launchDemo = async (lead: Lead) => {
setLaunching(lead.id)
try {
await api.createDemo(lead.id)
toast({ status: 'success', title: 'Démo lancée', description: 'Provisioning en cours.' })
navigate('/app/demos')
} catch (err) {
const msg = err instanceof ApiError ? err.message : 'Erreur'
toast({ status: 'error', title: 'Lancement impossible', description: msg })
} finally {
setLaunching(null)
}
}
if (loading) return <Spinner />
return (
@@ -94,8 +81,7 @@ export function Leads() {
<Button
size="sm"
colorScheme="primary"
isLoading={launching === l.id}
onClick={() => launchDemo(l)}
onClick={() => setLaunchingFor(l)}
>
Lancer une démo
</Button>
@@ -108,6 +94,16 @@ export function Leads() {
</Table>
</TableContainer>
)}
<CreateDemoModal
isOpen={!!launchingFor}
onClose={() => setLaunchingFor(null)}
onCreated={() => {
void load()
navigate('/app/demos')
}}
leadId={launchingFor?.id}
/>
</>
)
}
+97
View File
@@ -0,0 +1,97 @@
import { useCallback, useEffect, useState } from 'react'
import {
Badge,
Heading,
Link,
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 } from '../../lib/format'
// Un provisioning en cours => on rafraîchit régulièrement.
const POLL_MS = 5000
export function PremiumDemos() {
const toast = useToast()
const navigate = useNavigate()
const [demos, setDemos] = useState<Demo[]>([])
const [loading, setLoading] = useState(true)
const load = useCallback(async () => {
try {
const res = await api.listDemos()
setDemos((res.items ?? []).filter((d) => d.type_abonnement === 'premium'))
} 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])
return (
<>
<Heading size="md" mb={2}>
Démos Premium
</Heading>
<Text color="gray.500" mb={6} fontSize="sm">
Démos rattachées à un client passé en abonnement payant stockage persistant, n'expirent plus.
</Text>
{loading ? (
<Spinner />
) : demos.length === 0 ? (
<Text color="gray.500">Aucune démo premium pour le moment.</Text>
) : (
<TableContainer borderWidth="1px" borderRadius="lg">
<Table>
<Thead>
<Tr>
<Th>Namespace</Th>
<Th>Client</Th>
<Th>Statut</Th>
<Th>URL</Th>
</Tr>
</Thead>
<Tbody>
{demos.map((d) => (
<Tr key={d.id}>
<Td fontFamily="mono">{d.namespace}</Td>
<Td>{d.username || <Text color="gray.400">—</Text>}</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>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
)}
</>
)
}