mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-12 11:47:11 +00:00
feat(stacks): add Schedule task shortcut to stack context and kebab menus (#772)
Right-clicking a stack or opening its 3-dot menu now shows a 'Schedule task' entry in the lifecycle group (visible to paid tiers). Clicking it navigates to Scheduled Operations and opens the New Schedule dialog pre-filled with the stack name and active node, removing the need to navigate there manually and re-enter the target. - Added openScheduleTask to StackMenuCtx; wired in buildMenuCtx using the active node from NodeContext - Extended ScheduledOperationsView with optional prefill/onPrefillConsumed props; a ref-guarded effect calls openCreate() with the prefill data - openCreate refactored to accept an optional prefill arg, removing the duplication between the effect and the existing 'New Schedule' button
This commit is contained in:
@@ -47,7 +47,7 @@ import { Sparkline } from './ui/sparkline';
|
||||
import { GlobalObservabilityView } from './GlobalObservabilityView';
|
||||
import { FleetView } from './FleetView';
|
||||
import { AuditLogView } from './AuditLogView';
|
||||
import ScheduledOperationsView from './ScheduledOperationsView';
|
||||
import ScheduledOperationsView, { type ScheduleTaskPrefill } from './ScheduledOperationsView';
|
||||
import AutoUpdateReadinessView from './AutoUpdateReadinessView';
|
||||
import { SecurityHistoryView } from './SecurityHistoryView';
|
||||
import { SENCHO_NAVIGATE_EVENT } from './NodeManager';
|
||||
@@ -322,6 +322,8 @@ export default function EditorLayout() {
|
||||
const [activeView, setActiveView] = useState<'dashboard' | 'editor' | 'host-console' | 'resources' | 'templates' | 'global-observability' | 'fleet' | 'audit-log' | 'scheduled-ops' | 'auto-updates'>('dashboard');
|
||||
const [securityHistoryOpen, setSecurityHistoryOpen] = useState(false);
|
||||
const [filterNodeId, setFilterNodeId] = useState<number | null>(null);
|
||||
const [schedulePrefill, setSchedulePrefill] = useState<ScheduleTaskPrefill | null>(null);
|
||||
const handlePrefillConsumed = useCallback(() => setSchedulePrefill(null), []);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editingCompose, setEditingCompose] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -2004,6 +2006,11 @@ export default function EditorLayout() {
|
||||
}
|
||||
},
|
||||
openLabelManager: () => { setSettingsInitialSection('labels'); setSettingsModalOpen(true); },
|
||||
openScheduleTask: () => {
|
||||
const stackName = file.replace(/\.(yml|yaml)$/, '');
|
||||
setSchedulePrefill({ stackName, nodeId: activeNode?.id ?? null });
|
||||
setActiveView('scheduled-ops');
|
||||
},
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
@@ -2811,7 +2818,12 @@ export default function EditorLayout() {
|
||||
</CapabilityGate>
|
||||
) : activeView === 'scheduled-ops' ? (
|
||||
<CapabilityGate capability="scheduled-ops" featureName="Scheduled Operations">
|
||||
<ScheduledOperationsView filterNodeId={filterNodeId} onClearFilter={() => setFilterNodeId(null)} />
|
||||
<ScheduledOperationsView
|
||||
filterNodeId={filterNodeId}
|
||||
onClearFilter={() => setFilterNodeId(null)}
|
||||
prefill={schedulePrefill}
|
||||
onPrefillConsumed={handlePrefillConsumed}
|
||||
/>
|
||||
</CapabilityGate>
|
||||
) : (
|
||||
<HomeDashboard
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -51,12 +51,19 @@ function formatRelative(ts: number, now: number): string {
|
||||
return remMins === 0 ? `in ${hours}h` : `in ${hours}h ${remMins}m`;
|
||||
}
|
||||
|
||||
export interface ScheduleTaskPrefill {
|
||||
stackName: string;
|
||||
nodeId: number | null;
|
||||
}
|
||||
|
||||
interface ScheduledOperationsViewProps {
|
||||
filterNodeId?: number | null;
|
||||
onClearFilter?: () => void;
|
||||
prefill?: ScheduleTaskPrefill | null;
|
||||
onPrefillConsumed?: () => void;
|
||||
}
|
||||
|
||||
export default function ScheduledOperationsView({ filterNodeId, onClearFilter }: ScheduledOperationsViewProps) {
|
||||
export default function ScheduledOperationsView({ filterNodeId, onClearFilter, prefill, onPrefillConsumed }: ScheduledOperationsViewProps) {
|
||||
const [tasks, setTasks] = useState<ScheduledTask[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [view, setView] = useState<'timeline' | 'table'>('timeline');
|
||||
@@ -96,6 +103,8 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter }:
|
||||
? nodes.find(n => n.id === filterNodeId)?.name
|
||||
: null;
|
||||
|
||||
const consumedPrefillRef = useRef<ScheduleTaskPrefill | null>(null);
|
||||
|
||||
const fetchTasks = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -141,6 +150,13 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter }:
|
||||
fetchNodes();
|
||||
}, [fetchTasks, fetchStacks, fetchNodes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!prefill || prefill === consumedPrefillRef.current) return;
|
||||
consumedPrefillRef.current = prefill;
|
||||
openCreate({ stackName: prefill.stackName, nodeId: prefill.nodeId != null ? String(prefill.nodeId) : '' });
|
||||
onPrefillConsumed?.();
|
||||
}, [prefill, onPrefillConsumed]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setNow(Date.now()), 60_000);
|
||||
return () => clearInterval(id);
|
||||
@@ -177,21 +193,20 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter }:
|
||||
}
|
||||
}, [formNodeId, dialogOpen, fetchStacks]);
|
||||
|
||||
const openCreate = () => {
|
||||
const openCreate = (prefillData?: { stackName: string; nodeId: string }) => {
|
||||
const nodeId = prefillData?.nodeId ?? (filterNodeId != null ? String(filterNodeId) : '');
|
||||
setEditingTask(null);
|
||||
setFormName('');
|
||||
setFormAction('restart');
|
||||
setFormTargetId('');
|
||||
setFormNodeId(filterNodeId != null ? String(filterNodeId) : '');
|
||||
setFormTargetId(prefillData?.stackName ?? '');
|
||||
setFormNodeId(nodeId);
|
||||
setFormCron('0 3 * * *');
|
||||
setFormEnabled(true);
|
||||
setFormPruneTargets(['containers', 'images', 'networks', 'volumes']);
|
||||
setFormTargetServices([]);
|
||||
setFormPruneLabelFilter('');
|
||||
setDialogOpen(true);
|
||||
if (filterNodeId != null) {
|
||||
fetchStacks(String(filterNodeId));
|
||||
}
|
||||
if (nodeId) fetchStacks(nodeId);
|
||||
};
|
||||
|
||||
const openEdit = (task: ScheduledTask) => {
|
||||
@@ -380,7 +395,7 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter }:
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} strokeWidth={1.5} />
|
||||
Refresh
|
||||
</Button>
|
||||
<Button size="sm" onClick={openCreate}>
|
||||
<Button size="sm" onClick={() => openCreate()}>
|
||||
<Plus className="w-4 h-4 mr-2" strokeWidth={1.5} />
|
||||
New Schedule
|
||||
</Button>
|
||||
|
||||
@@ -26,9 +26,6 @@ export interface StackMenuCtx {
|
||||
hasPort: boolean;
|
||||
isBusy: boolean;
|
||||
isPaid: boolean;
|
||||
// isAdmiral: plumbed now so Admiral-specific menu items can be added in a
|
||||
// follow-up PR without changing this interface. No Admiral-only items ship
|
||||
// in the current PR; the auto-update toggle is Skipper+, gated on isPaid.
|
||||
isAdmiral: boolean;
|
||||
canDelete: boolean;
|
||||
isPinned: boolean;
|
||||
@@ -51,6 +48,7 @@ export interface StackMenuCtx {
|
||||
createAndAssignLabel: (name: string, color: LabelColor) => Promise<void>;
|
||||
openLabelManager: () => void;
|
||||
setAutoUpdateEnabled: (enabled: boolean) => void;
|
||||
openScheduleTask: () => void;
|
||||
}
|
||||
|
||||
export type StackGroupKind = 'pinned' | 'labeled' | 'unlabeled';
|
||||
|
||||
Reference in New Issue
Block a user