diff --git a/Data/Engine/web-interface/src/App.jsx b/Data/Engine/web-interface/src/App.jsx
index 4305e53f..b2caeeca 100644
--- a/Data/Engine/web-interface/src/App.jsx
+++ b/Data/Engine/web-interface/src/App.jsx
@@ -115,10 +115,12 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
const [userDisplayName, setUserDisplayName] = useState(null);
const [editingJob, setEditingJob] = useState(null);
const [jobsRefreshToken, setJobsRefreshToken] = useState(0);
+ const [quickJobDraft, setQuickJobDraft] = useState(null);
const [assemblyEditorState, setAssemblyEditorState] = useState(null); // { mode: 'script'|'ansible', row, nonce }
const [sessionResolved, setSessionResolved] = useState(false);
const initialPathRef = useRef(window.location.pathname + window.location.search);
const pendingPathRef = useRef(null);
+ const quickJobSeedRef = useRef(0);
const [notAuthorizedOpen, setNotAuthorizedOpen] = useState(false);
// Top-bar search state
@@ -380,6 +382,45 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
navigateByPathRef.current = navigateByPath;
}, [navigateTo, navigateByPath]);
+ const handleQuickJobLaunch = useCallback(
+ (hostnames) => {
+ const list = Array.isArray(hostnames) ? hostnames : [hostnames];
+ const normalized = Array.from(
+ new Set(
+ list
+ .map((host) => (typeof host === "string" ? host.trim() : ""))
+ .filter((host) => Boolean(host))
+ )
+ );
+ if (!normalized.length) {
+ return;
+ }
+ quickJobSeedRef.current += 1;
+ const primary = normalized[0];
+ const extraCount = normalized.length - 1;
+ const deviceLabel = extraCount > 0 ? `${primary} +${extraCount} more` : primary;
+ setEditingJob(null);
+ setQuickJobDraft({
+ id: `${Date.now()}_${quickJobSeedRef.current}`,
+ hostnames: normalized,
+ deviceLabel,
+ initialTabKey: "components",
+ scheduleType: "immediately",
+ placeholderAssemblyLabel: "Choose Assembly",
+ });
+ navigateTo("create_job");
+ },
+ [navigateTo]
+ );
+
+ const handleConsumeQuickJobDraft = useCallback((draftId) => {
+ setQuickJobDraft((prev) => {
+ if (!prev) return prev;
+ if (draftId && prev.id !== draftId) return prev;
+ return null;
+ });
+ }, []);
+
// Build breadcrumb items for current view
const breadcrumbs = React.useMemo(() => {
const items = [];
@@ -1039,6 +1080,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
onSelectDevice={(d) => {
navigateTo("device_details", { device: d });
}}
+ onQuickJobLaunch={handleQuickJobLaunch}
/>
);
case "agent_devices":
@@ -1047,17 +1089,19 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
onSelectDevice={(d) => {
navigateTo("device_details", { device: d });
}}
+ onQuickJobLaunch={handleQuickJobLaunch}
/>
);
case "ssh_devices":
- return ;
+ return ;
case "winrm_devices":
- return ;
+ return ;
case "device_details":
return (
{
navigateTo("devices");
setSelectedDevice(null);
@@ -1078,8 +1122,19 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
return (
{ navigateTo("jobs"); setEditingJob(null); }}
- onCreated={() => { navigateTo("jobs"); setEditingJob(null); setJobsRefreshToken(Date.now()); }}
+ quickJobDraft={quickJobDraft}
+ onConsumeQuickJobDraft={handleConsumeQuickJobDraft}
+ onCancel={() => {
+ navigateTo("jobs");
+ setEditingJob(null);
+ setQuickJobDraft(null);
+ }}
+ onCreated={() => {
+ navigateTo("jobs");
+ setEditingJob(null);
+ setJobsRefreshToken(Date.now());
+ setQuickJobDraft(null);
+ }}
/>
);
diff --git a/Data/Engine/web-interface/src/Devices/Device_Details.jsx b/Data/Engine/web-interface/src/Devices/Device_Details.jsx
index b2cc4b5b..1e0c38e7 100644
--- a/Data/Engine/web-interface/src/Devices/Device_Details.jsx
+++ b/Data/Engine/web-interface/src/Devices/Device_Details.jsx
@@ -29,7 +29,6 @@ import "prismjs/components/prism-powershell";
import "prismjs/components/prism-batch";
import "prismjs/themes/prism-okaidia.css";
import Editor from "react-simple-code-editor";
-import QuickJob from "../Scheduling/Quick_Job.jsx";
import { AgGridReact } from "ag-grid-react";
import { ModuleRegistry, AllCommunityModule, themeQuartz } from "ag-grid-community";
@@ -248,7 +247,7 @@ const GRID_COMPONENTS = {
HistoryActionsCell,
};
-export default function DeviceDetails({ device, onBack }) {
+export default function DeviceDetails({ device, onBack, onQuickJobLaunch }) {
const [tab, setTab] = useState(0);
const [agent, setAgent] = useState(device || {});
const [details, setDetails] = useState({});
@@ -266,7 +265,6 @@ export default function DeviceDetails({ device, onBack }) {
const [outputTitle, setOutputTitle] = useState("");
const [outputContent, setOutputContent] = useState("");
const [outputLang, setOutputLang] = useState("powershell");
- const [quickJobOpen, setQuickJobOpen] = useState(false);
const [menuAnchor, setMenuAnchor] = useState(null);
const [clearDialogOpen, setClearDialogOpen] = useState(false);
const [assemblyNameMap, setAssemblyNameMap] = useState({});
@@ -281,6 +279,18 @@ export default function DeviceDetails({ device, onBack }) {
const now = Date.now() / 1000;
return now - tsSec <= 300 ? "Online" : "Offline";
});
+ const quickJobTargets = useMemo(() => {
+ const values = [];
+ const push = (value) => {
+ const normalized = typeof value === "string" ? value.trim() : "";
+ if (!normalized) return;
+ if (!values.includes(normalized)) values.push(normalized);
+ };
+ push(agent?.hostname);
+ push(device?.hostname);
+ return values;
+ }, [agent, device]);
+ const canLaunchQuickJob = quickJobTargets.length > 0 && typeof onQuickJobLaunch === "function";
useEffect(() => {
setConnectionError("");
@@ -1626,11 +1636,11 @@ export default function DeviceDetails({ device, onBack }) {
>
-