chore: build
This commit is contained in:
Vendored
-1510
File diff suppressed because one or more lines are too long
Vendored
+1510
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Omnex — Plateforme de gestion de commandes & livraison</title>
|
||||
<meta name="description" content="Déployez en un clic une démo complète de la plateforme de gestion de commandes et de livraison." />
|
||||
<script type="module" crossorigin src="/assets/index-DkbJvUQ3.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-rDhSK02l.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -248,6 +248,8 @@ export const api = {
|
||||
transferDemoToPremium: (id: string) => request<Demo>('POST', `/demos/${id}/premium`),
|
||||
|
||||
listProjects: () => request<{ items: Project[] }>('GET', '/projects'),
|
||||
// État live des pods d'un projet (même format que getDemoDetails).
|
||||
getProjectDetails: (id: string) => request<DemoDetails>('GET', `/projects/${id}/details`),
|
||||
createProject: (params: CreateProjectParams) =>
|
||||
request<Project>('POST', '/projects', {
|
||||
client_name: params.clientName,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Fragment, useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Collapse,
|
||||
Flex,
|
||||
Heading,
|
||||
HStack,
|
||||
Icon,
|
||||
Link,
|
||||
Spacer,
|
||||
Spinner,
|
||||
@@ -20,15 +23,19 @@ import {
|
||||
Wrap,
|
||||
} from '@chakra-ui/react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { api, ApiError, type Project } from '../../lib/api'
|
||||
import { api, ApiError, type DemoDetails, type Project } from '../../lib/api'
|
||||
import { formatDate, monthsLabel, projectStatusColor, projectStatusLabel, timeRemaining } from '../../lib/format'
|
||||
import { ConfirmDialog } from '../../components/ConfirmDialog'
|
||||
import { CreateProjectModal } from '../../components/CreateProjectModal'
|
||||
import { ExtendProjectModal } from '../../components/ExtendProjectModal'
|
||||
import { ChevronIcon, PodStatusPanel } from '../../components/PodStatusPanel'
|
||||
|
||||
// Un déploiement en cours => on rafraîchit régulièrement.
|
||||
const POLL_MS = 5000
|
||||
|
||||
// Rafraîchissement de l'état des pods pendant que le détail d'une ligne est déplié.
|
||||
const DETAILS_POLL_MS = 5000
|
||||
|
||||
// Projets vitrine déployés pour des clients sur abonnement de 1 à 12 mois.
|
||||
export function Projects() {
|
||||
const toast = useToast()
|
||||
@@ -40,6 +47,11 @@ export function Projects() {
|
||||
const [toExtend, setToExtend] = useState<Project | null>(null)
|
||||
const [toDelete, setToDelete] = useState<Project | null>(null)
|
||||
|
||||
// --- Ligne dépliée (état live des pods) ---
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null)
|
||||
const [details, setDetails] = useState<DemoDetails | null>(null)
|
||||
const [detailsLoading, setDetailsLoading] = useState(false)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const res = await api.listProjects()
|
||||
@@ -75,6 +87,55 @@ export function Projects() {
|
||||
}
|
||||
}
|
||||
|
||||
// Seul un projet déployé (ou en cours de déploiement) a des pods à afficher.
|
||||
const hasPods = (p: Project) => p.status === 'ready' || p.status === 'provisioning'
|
||||
|
||||
const toggleRow = async (p: Project) => {
|
||||
if (!hasPods(p)) return
|
||||
if (expandedId === p.id) {
|
||||
setExpandedId(null)
|
||||
setDetails(null)
|
||||
return
|
||||
}
|
||||
setExpandedId(p.id)
|
||||
setDetails(null)
|
||||
setDetailsLoading(true)
|
||||
try {
|
||||
setDetails(await api.getProjectDetails(p.id))
|
||||
} catch (err) {
|
||||
const msg = err instanceof ApiError ? err.message : 'Erreur'
|
||||
toast({ status: 'error', title: 'État des pods indisponible', description: msg })
|
||||
setExpandedId(null)
|
||||
} finally {
|
||||
setDetailsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Tant qu'une ligne est dépliée, on rafraîchit l'état des pods en direct
|
||||
// (silencieux : pas de spinner, juste la mise à jour des badges/jauges).
|
||||
useEffect(() => {
|
||||
if (!expandedId) return
|
||||
const id = setInterval(() => {
|
||||
api
|
||||
.getProjectDetails(expandedId)
|
||||
.then(setDetails)
|
||||
.catch(() => {
|
||||
/* échec silencieux : on garde le dernier état connu affiché */
|
||||
})
|
||||
}, DETAILS_POLL_MS)
|
||||
return () => clearInterval(id)
|
||||
}, [expandedId])
|
||||
|
||||
// Le projet déplié a été supprimé ou n'a plus de pods : on referme la ligne.
|
||||
useEffect(() => {
|
||||
if (!expandedId) return
|
||||
const current = projects.find((p) => p.id === expandedId)
|
||||
if (!current || !hasPods(current)) {
|
||||
setExpandedId(null)
|
||||
setDetails(null)
|
||||
}
|
||||
}, [projects, expandedId])
|
||||
|
||||
// Échéance affichée : date de suppression et temps restant.
|
||||
const deadline = (p: Project) => (
|
||||
<Text fontSize="sm">
|
||||
@@ -113,40 +174,96 @@ export function Projects() {
|
||||
<Tbody>
|
||||
{projects.map((p) => {
|
||||
const locked = busy === p.id
|
||||
const canExtend = p.status === 'ready' || p.status === 'provisioning'
|
||||
const canExtend = hasPods(p)
|
||||
const isOpen = expandedId === p.id
|
||||
return (
|
||||
<Tr key={p.id}>
|
||||
<Td>{p.client_name}</Td>
|
||||
<Td>
|
||||
<Badge colorScheme={projectStatusColor(p.status)}>{projectStatusLabel(p.status)}</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<Link href={p.url} isExternal fontFamily="mono" fontSize="sm">
|
||||
{p.host}
|
||||
</Link>
|
||||
</Td>
|
||||
<Td>{monthsLabel(p.months_purchased)} au total</Td>
|
||||
<Td>{deadline(p)}</Td>
|
||||
<Td>
|
||||
{p.admin_username} · max {p.admin_number}
|
||||
</Td>
|
||||
<Td>
|
||||
<Wrap justify="flex-end" spacing={2}>
|
||||
<Button size="xs" colorScheme="primary" isDisabled={!canExtend || locked} onClick={() => setToExtend(p)}>
|
||||
Renouveler
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline"
|
||||
colorScheme="red"
|
||||
isDisabled={p.status === 'deleting' || locked}
|
||||
onClick={() => setToDelete(p)}
|
||||
<Fragment key={p.id}>
|
||||
<Tr
|
||||
cursor={hasPods(p) ? 'pointer' : undefined}
|
||||
_hover={hasPods(p) ? { bg: 'chakra-subtle-bg' } : undefined}
|
||||
onClick={() => void toggleRow(p)}
|
||||
>
|
||||
<Td>
|
||||
<HStack spacing={2}>
|
||||
{hasPods(p) && (
|
||||
<Icon
|
||||
as={ChevronIcon}
|
||||
boxSize={3}
|
||||
color="gray.400"
|
||||
transform={isOpen ? 'rotate(90deg)' : undefined}
|
||||
transition="transform 0.15s"
|
||||
/>
|
||||
)}
|
||||
<Text>{p.client_name}</Text>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge colorScheme={projectStatusColor(p.status)}>{projectStatusLabel(p.status)}</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<Link
|
||||
href={p.url}
|
||||
isExternal
|
||||
fontFamily="mono"
|
||||
fontSize="sm"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
</Wrap>
|
||||
</Td>
|
||||
</Tr>
|
||||
{p.host}
|
||||
</Link>
|
||||
</Td>
|
||||
<Td>{monthsLabel(p.months_purchased)} au total</Td>
|
||||
<Td>{deadline(p)}</Td>
|
||||
<Td>
|
||||
{p.admin_username} · max {p.admin_number}
|
||||
</Td>
|
||||
<Td>
|
||||
<Wrap justify="flex-end" spacing={2}>
|
||||
<Button
|
||||
size="xs"
|
||||
colorScheme="primary"
|
||||
isDisabled={!canExtend || locked}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setToExtend(p)
|
||||
}}
|
||||
>
|
||||
Renouveler
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline"
|
||||
colorScheme="red"
|
||||
isDisabled={p.status === 'deleting' || locked}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setToDelete(p)
|
||||
}}
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
</Wrap>
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td p={0} border={isOpen ? undefined : 'none'} colSpan={7}>
|
||||
<Collapse in={isOpen} unmountOnExit animateOpacity>
|
||||
<Box p={4} bg="chakra-subtle-bg" borderTopWidth="1px">
|
||||
{detailsLoading && !details ? (
|
||||
<Flex justify="center" py={4}>
|
||||
<Spinner size="sm" />
|
||||
</Flex>
|
||||
) : details ? (
|
||||
<PodStatusPanel state={details.state} />
|
||||
) : (
|
||||
<Text color="gray.500" fontSize="sm">
|
||||
Aucune donnée.
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</Tbody>
|
||||
|
||||
Reference in New Issue
Block a user