@@ -63,6 +63,7 @@ export type DemoStatus =
|
|||||||
|
|
||||||
export interface Demo {
|
export interface Demo {
|
||||||
id: string
|
id: string
|
||||||
|
username: string
|
||||||
lead_id?: string
|
lead_id?: string
|
||||||
status: DemoStatus
|
status: DemoStatus
|
||||||
namespace: string
|
namespace: string
|
||||||
@@ -87,6 +88,20 @@ export interface Contact {
|
|||||||
created_at: string
|
created_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DemoState {
|
||||||
|
APIState: string
|
||||||
|
WebState: string
|
||||||
|
DBState: string
|
||||||
|
DBMemoryState: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DemoDetails {
|
||||||
|
username: string
|
||||||
|
namespace: string
|
||||||
|
state: DemoState
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Endpoints ---
|
// --- Endpoints ---
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
@@ -130,4 +145,6 @@ export const api = {
|
|||||||
sendMessage: (username: string, telegram: string, sujet: string, message: string) =>
|
sendMessage: (username: string, telegram: string, sujet: string, message: string) =>
|
||||||
request<{ success: string }>('POST', '/send/message', { username, telegram, sujet, message }),
|
request<{ success: string }>('POST', '/send/message', { username, telegram, sujet, message }),
|
||||||
getMessage: () => request<{ messages: Contact[] }>('GET', '/messages'),
|
getMessage: () => request<{ messages: Contact[] }>('GET', '/messages'),
|
||||||
|
getDemoDetails: (username: string) =>
|
||||||
|
request<DemoDetails>('POST', '/demos/details', { username }),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,19 @@ import {
|
|||||||
Heading,
|
Heading,
|
||||||
HStack,
|
HStack,
|
||||||
Link,
|
Link,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
SimpleGrid,
|
||||||
Spacer,
|
Spacer,
|
||||||
Spinner,
|
Spinner,
|
||||||
|
Stat,
|
||||||
|
StatLabel,
|
||||||
|
StatNumber,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
Tbody,
|
Tbody,
|
||||||
@@ -19,7 +30,7 @@ import {
|
|||||||
useToast,
|
useToast,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { api, ApiError, type Demo } from '../../lib/api'
|
import { api, ApiError, type Demo, type DemoDetails } from '../../lib/api'
|
||||||
import { statusColor, statusLabel, timeRemaining } from '../../lib/format'
|
import { statusColor, statusLabel, timeRemaining } from '../../lib/format'
|
||||||
import { ConfirmDialog } from '../../components/ConfirmDialog'
|
import { ConfirmDialog } from '../../components/ConfirmDialog'
|
||||||
|
|
||||||
@@ -34,6 +45,11 @@ export function Demos() {
|
|||||||
const [busy, setBusy] = useState<string | null>(null)
|
const [busy, setBusy] = useState<string | null>(null)
|
||||||
const [toDelete, setToDelete] = useState<Demo | null>(null)
|
const [toDelete, setToDelete] = useState<Demo | null>(null)
|
||||||
|
|
||||||
|
// --- Détails d'une démo (modale) ---
|
||||||
|
const [detailsFor, setDetailsFor] = useState<Demo | null>(null)
|
||||||
|
const [details, setDetails] = useState<DemoDetails | null>(null)
|
||||||
|
const [detailsLoading, setDetailsLoading] = useState(false)
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.listDemos()
|
const res = await api.listDemos()
|
||||||
@@ -97,6 +113,27 @@ export function Demos() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openDetails = async (d: Demo) => {
|
||||||
|
setDetailsFor(d)
|
||||||
|
setDetails(null)
|
||||||
|
setDetailsLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await api.getDemoDetails(d.username)
|
||||||
|
setDetails(res)
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof ApiError ? err.message : 'Erreur'
|
||||||
|
toast({ status: 'error', title: 'Détails indisponibles', description: msg })
|
||||||
|
setDetailsFor(null)
|
||||||
|
} finally {
|
||||||
|
setDetailsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDetails = () => {
|
||||||
|
setDetailsFor(null)
|
||||||
|
setDetails(null)
|
||||||
|
}
|
||||||
|
|
||||||
const isAlive = (s: Demo['status']) => s !== 'expired' && s !== 'failed'
|
const isAlive = (s: Demo['status']) => s !== 'expired' && s !== 'failed'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -127,14 +164,24 @@ export function Demos() {
|
|||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
{demos.map((d) => (
|
{demos.map((d) => (
|
||||||
<Tr key={d.id}>
|
<Tr
|
||||||
|
key={d.id}
|
||||||
|
cursor="pointer"
|
||||||
|
_hover={{ bg: 'gray.50' }}
|
||||||
|
onClick={() => openDetails(d)}
|
||||||
|
>
|
||||||
<Td fontFamily="mono">{d.namespace}</Td>
|
<Td fontFamily="mono">{d.namespace}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Badge colorScheme={statusColor(d.status)}>{statusLabel(d.status)}</Badge>
|
<Badge colorScheme={statusColor(d.status)}>{statusLabel(d.status)}</Badge>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
{d.status === 'ready' ? (
|
{d.status === 'ready' ? (
|
||||||
<Link href={d.url} color="primary.500" isExternal>
|
<Link
|
||||||
|
href={d.url}
|
||||||
|
color="primary.500"
|
||||||
|
isExternal
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
{d.url}
|
{d.url}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
@@ -148,7 +195,10 @@ export function Demos() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
isDisabled={!isAlive(d.status) || busy === d.id}
|
isDisabled={!isAlive(d.status) || busy === d.id}
|
||||||
onClick={() => extend(d)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
extend(d)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
+30 j
|
+30 j
|
||||||
</Button>
|
</Button>
|
||||||
@@ -157,7 +207,10 @@ export function Demos() {
|
|||||||
colorScheme="red"
|
colorScheme="red"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
isDisabled={!isAlive(d.status)}
|
isDisabled={!isAlive(d.status)}
|
||||||
onClick={() => setToDelete(d)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setToDelete(d)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Détruire
|
Détruire
|
||||||
</Button>
|
</Button>
|
||||||
@@ -185,6 +238,50 @@ export function Demos() {
|
|||||||
et toutes ses données seront supprimées définitivement. Les ressources du pool seront
|
et toutes ses données seront supprimées définitivement. Les ressources du pool seront
|
||||||
libérées. Cette action est irréversible.
|
libérées. Cette action est irréversible.
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<Modal isOpen={!!detailsFor} onClose={closeDetails} size="lg">
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>
|
||||||
|
Détails de{' '}
|
||||||
|
<Text as="span" fontFamily="mono">
|
||||||
|
{detailsFor?.namespace}
|
||||||
|
</Text>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
{detailsLoading ? (
|
||||||
|
<Flex justify="center" py={8}>
|
||||||
|
<Spinner />
|
||||||
|
</Flex>
|
||||||
|
) : details ? (
|
||||||
|
<SimpleGrid columns={2} spacing={4}>
|
||||||
|
<Stat>
|
||||||
|
<StatLabel>Backend</StatLabel>
|
||||||
|
<StatNumber fontSize="md">{details.state.APIState || '—'}</StatNumber>
|
||||||
|
</Stat>
|
||||||
|
<Stat>
|
||||||
|
<StatLabel>Frontend</StatLabel>
|
||||||
|
<StatNumber fontSize="md">{details.state.WebState || '—'}</StatNumber>
|
||||||
|
</Stat>
|
||||||
|
<Stat>
|
||||||
|
<StatLabel>PostgreSQL</StatLabel>
|
||||||
|
<StatNumber fontSize="md">{details.state.DBState || '—'}</StatNumber>
|
||||||
|
</Stat>
|
||||||
|
<Stat>
|
||||||
|
<StatLabel>Redis</StatLabel>
|
||||||
|
<StatNumber fontSize="md">{details.state.DBMemoryState || '—'}</StatNumber>
|
||||||
|
</Stat>
|
||||||
|
</SimpleGrid>
|
||||||
|
) : (
|
||||||
|
<Text color="gray.500">Aucune donnée.</Text>
|
||||||
|
)}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onClick={closeDetails}>Fermer</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user