mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
feat(deploy): lifecycle polish — canvas polling, log links, rollback confirm
Canvas nodes were static: servicesQuery had no refetchInterval, so build → deploy status never reached the node without a manual refresh. It now polls at 3s while any service is transitional and 15s otherwise. Deployment history rows link directly into that deployment's log tail (service detail logs section, with a "show latest" reset), and rollback now asks for confirmation before firing — it previously fired on a single click. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
1d5b99e1ba
commit
dfae6ef0bd
3 files changed
+55
-17
No files matched your search
@@ -96,6 +96,7 @@ export function ServiceDetailPage() {
|
||||
const [varDrafts, setVarDrafts] = useState<VariableDraft[] | null>(null);
|
||||
const [bulkOpen, setBulkOpen] = useState(false);
|
||||
const [bulkText, setBulkText] = useState('');
|
||||
const [logDeploymentId, setLogDeploymentId] = useState<string | null>(null);
|
||||
|
||||
const projectQuery = useQuery({
|
||||
queryKey: ['project', projectId],
|
||||
@@ -148,11 +149,14 @@ export function ServiceDetailPage() {
|
||||
return deployments[0] ?? null;
|
||||
}, [deploymentsQuery.data, isDemoMode]);
|
||||
|
||||
const logsDeployment =
|
||||
(deploymentsQuery.data ?? []).find((d) => d.id === logDeploymentId) ?? latestDeployment;
|
||||
|
||||
const deploymentLogsQuery = useQuery({
|
||||
queryKey: ['deployment-logs', latestDeployment?.id],
|
||||
queryFn: () => getDeploymentLogs(latestDeployment!.id, { type: 'all' }),
|
||||
queryKey: ['deployment-logs', logsDeployment?.id],
|
||||
queryFn: () => getDeploymentLogs(logsDeployment!.id, { type: 'all' }),
|
||||
enabled:
|
||||
Boolean(latestDeployment?.id) &&
|
||||
Boolean(logsDeployment?.id) &&
|
||||
!isDemoMode &&
|
||||
activeSection === 'logs',
|
||||
});
|
||||
@@ -775,7 +779,7 @@ export function ServiceDetailPage() {
|
||||
<button
|
||||
onClick={() => {
|
||||
serviceLogsQuery.refetch();
|
||||
if (latestDeployment?.id) deploymentLogsQuery.refetch();
|
||||
if (logsDeployment?.id) deploymentLogsQuery.refetch();
|
||||
}}
|
||||
className="flex items-center gap-2 h-9 px-4 rounded-[var(--radius-md)] border border-[var(--border-subtle)] text-sm font-medium hover:border-[var(--border-default)] transition-colors"
|
||||
>
|
||||
@@ -824,13 +828,25 @@ export function ServiceDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isDemoMode && latestDeployment && (
|
||||
{!isDemoMode && logsDeployment && (
|
||||
<div className="mt-6 panel-soft p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<p className="text-xs uppercase tracking-wider text-[var(--text-muted)]">Latest Deployment</p>
|
||||
<StatusBadge status={latestDeployment.status} />
|
||||
<p className="text-xs uppercase tracking-wider text-[var(--text-muted)]">
|
||||
{logsDeployment.id === latestDeployment?.id ? 'Latest Deployment' : 'Deployment'}
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
{logsDeployment.id !== latestDeployment?.id && (
|
||||
<button
|
||||
onClick={() => setLogDeploymentId(null)}
|
||||
className="text-xs text-[var(--accent-primary)] hover:underline"
|
||||
>
|
||||
Show latest
|
||||
</button>
|
||||
)}
|
||||
<StatusBadge status={logsDeployment.status} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-tertiary)] mono">{latestDeployment.id}</p>
|
||||
<p className="text-xs text-[var(--text-tertiary)] mono">{logsDeployment.id}</p>
|
||||
<pre className="mono mt-3 max-h-40 overflow-auto whitespace-pre-wrap break-all text-xs text-[var(--text-secondary)]">
|
||||
{deploymentLogsQuery.isLoading
|
||||
? 'Loading...'
|
||||
@@ -1109,13 +1125,28 @@ export function ServiceDetailPage() {
|
||||
{deployment.createdAt ? formatRelative(deployment.createdAt) : '—'}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<button
|
||||
onClick={() => rollbackMutation.mutate(deployment.id)}
|
||||
disabled={!canRollback(deployment.status) || rollbackMutation.isPending}
|
||||
className="px-3 py-1.5 rounded-[var(--radius-sm)] border border-[var(--border-subtle)] text-xs font-medium hover:border-[var(--border-default)] disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{rollbackMutation.isPending && rollbackMutation.variables === deployment.id ? '...' : 'Rollback'}
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setLogDeploymentId(deployment.id);
|
||||
setActiveSection('logs');
|
||||
}}
|
||||
className="px-3 py-1.5 rounded-[var(--radius-sm)] border border-[var(--border-subtle)] text-xs font-medium hover:border-[var(--border-default)] transition-colors"
|
||||
>
|
||||
Logs
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (window.confirm(`Rollback to deployment ${deployment.id.slice(0, 8)}?`)) {
|
||||
rollbackMutation.mutate(deployment.id);
|
||||
}
|
||||
}}
|
||||
disabled={!canRollback(deployment.status) || rollbackMutation.isPending}
|
||||
className="px-3 py-1.5 rounded-[var(--radius-sm)] border border-[var(--border-subtle)] text-xs font-medium hover:border-[var(--border-default)] disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{rollbackMutation.isPending && rollbackMutation.variables === deployment.id ? '...' : 'Rollback'}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
|
||||
@@ -491,6 +491,10 @@ export function ProjectWorkspacePage() {
|
||||
queryKey: ['project-services', projectId],
|
||||
queryFn: () => listServicesByProject(projectId),
|
||||
enabled: Boolean(projectId) && !isDemoMode,
|
||||
refetchInterval: (query) =>
|
||||
query.state.data?.some((s) => ['building', 'deploying', 'pending', 'rolling_back'].includes(s.status))
|
||||
? 3000
|
||||
: 15000,
|
||||
});
|
||||
|
||||
const createServiceMutation = useMutation({
|
||||
|
||||
Reference in new issue
Block a user