/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useQuery, useQueryClient } from '@tanstack/react-query' import { getRouteApi } from '@tanstack/react-router' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { DataTablePage, useDataTable } from '@/components/data-table' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import { useMediaQuery } from '@/hooks' import { useTableUrlState } from '@/hooks/use-table-url-state' import { deleteDeployment, listDeployments, searchDeployments } from '../api' import { getDeploymentStatusOptions } from '../constants' import { deploymentsQueryKeys } from '../lib' import type { Deployment } from '../types' import { useDeploymentsColumns } from './deployments-columns' import { ExtendDeploymentDialog } from './dialogs/extend-deployment-dialog' import { RenameDeploymentDialog } from './dialogs/rename-deployment-dialog' import { UpdateConfigDialog } from './dialogs/update-config-dialog' import { ViewDetailsDialog } from './dialogs/view-details-dialog' import { ViewLogsDialog } from './dialogs/view-logs-dialog' const route = getRouteApi('/_authenticated/models/$section') export function DeploymentsTable() { const { t } = useTranslation() const queryClient = useQueryClient() const isMobile = useMediaQuery('(max-width: 640px)') // URL state (use dedicated keys so it won't collide with metadata table) const { globalFilter, onGlobalFilterChange, columnFilters, onColumnFiltersChange, pagination, onPaginationChange, ensurePageInRange, } = useTableUrlState({ search: route.useSearch(), navigate: route.useNavigate(), pagination: { pageKey: 'dPage', pageSizeKey: 'dPageSize', defaultPage: 1, defaultPageSize: isMobile ? 8 : 10, }, globalFilter: { enabled: true, key: 'dFilter' }, columnFilters: [ { columnId: 'status', searchKey: 'dStatus', type: 'array' }, ], }) const keyword = globalFilter ?? '' const statusFilter = (columnFilters.find((f) => f.id === 'status')?.value as string[]) || [] const activeStatus = statusFilter.length > 0 && !statusFilter.includes('all') ? statusFilter[0] : undefined // Dialog state const [logsOpen, setLogsOpen] = useState(false) const [logsDeploymentId, setLogsDeploymentId] = useState< string | number | null >(null) const [detailsOpen, setDetailsOpen] = useState(false) const [detailsDeploymentId, setDetailsDeploymentId] = useState< string | number | null >(null) const [updateOpen, setUpdateOpen] = useState(false) const [updateDeploymentId, setUpdateDeploymentId] = useState< string | number | null >(null) const [extendOpen, setExtendOpen] = useState(false) const [extendDeploymentId, setExtendDeploymentId] = useState< string | number | null >(null) const [renameOpen, setRenameOpen] = useState(false) const [renameDeploymentId, setRenameDeploymentId] = useState< string | number | null >(null) const [renameCurrentName, setRenameCurrentName] = useState('') // Delete confirm const [deleteOpen, setDeleteOpen] = useState(false) const [deleteTarget, setDeleteTarget] = useState(null) const [isDeleting, setIsDeleting] = useState(false) const { data, isLoading, isFetching } = useQuery({ queryKey: deploymentsQueryKeys.list({ keyword, status: activeStatus, p: pagination.pageIndex + 1, page_size: pagination.pageSize, }), queryFn: async () => { if (keyword.trim()) { return searchDeployments({ keyword, status: activeStatus, p: pagination.pageIndex + 1, page_size: pagination.pageSize, }) } return listDeployments({ status: activeStatus, p: pagination.pageIndex + 1, page_size: pagination.pageSize, }) }, placeholderData: (prev) => prev, }) const deployments = data?.data?.items || [] const totalCount = data?.data?.total || 0 const handleDelete = async () => { if (!deleteTarget) return setIsDeleting(true) try { const res = await deleteDeployment(deleteTarget.id) if (res?.success) { toast.success(t('Deleted successfully')) queryClient.invalidateQueries({ queryKey: deploymentsQueryKeys.lists(), }) } else { toast.error(res?.message || t('Delete failed')) } } catch (err) { toast.error(err instanceof Error ? err.message : t('Delete failed')) } finally { setIsDeleting(false) setDeleteOpen(false) setDeleteTarget(null) } } const columns = useDeploymentsColumns({ onViewLogs: (id) => { setLogsDeploymentId(id) setLogsOpen(true) }, onViewDetails: (id) => { setDetailsDeploymentId(id) setDetailsOpen(true) }, onUpdateConfig: (id) => { setUpdateDeploymentId(id) setUpdateOpen(true) }, onExtend: (id) => { setExtendDeploymentId(id) setExtendOpen(true) }, onRename: (id, currentName) => { setRenameCurrentName(currentName) setRenameDeploymentId(id) setRenameOpen(true) }, onDelete: (deployment) => { setDeleteTarget(deployment) setDeleteOpen(true) }, }) const { table } = useDataTable({ data: deployments, columns, totalCount, columnFilters, pagination, globalFilter, onColumnFiltersChange, onPaginationChange, onGlobalFilterChange, manualPagination: true, manualFiltering: true, withSortedRowModel: false, ensurePageInRange, }) const statusFilterOptions = useMemo(() => { return [...getDeploymentStatusOptions(t)].map((opt) => ({ label: opt.label, value: opt.value, })) }, [t]) return ( <> { setLogsOpen(open) if (!open) setLogsDeploymentId(null) }} deploymentId={logsDeploymentId} /> { setDetailsOpen(open) if (!open) setDetailsDeploymentId(null) }} deploymentId={detailsDeploymentId} /> { setUpdateOpen(open) if (!open) setUpdateDeploymentId(null) }} deploymentId={updateDeploymentId} /> { setExtendOpen(open) if (!open) setExtendDeploymentId(null) }} deploymentId={extendDeploymentId} /> { setRenameOpen(open) if (!open) setRenameDeploymentId(null) }} deploymentId={renameDeploymentId} currentName={renameCurrentName} /> {t('Confirm delete')} {t( 'Are you sure you want to delete deployment "{{name}}"? This action cannot be undone.', { name: deleteTarget?.container_name || deleteTarget?.deployment_name || deleteTarget?.id, } )} {t('Cancel')} {isDeleting ? t('Deleting...') : t('Delete')} ) }