mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-16 10:45:48 -07:00
refine assembly editor styling and metadata
This commit is contained in:
@@ -4,22 +4,18 @@ import {
|
|||||||
Paper,
|
Paper,
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
Button,
|
||||||
Select,
|
|
||||||
FormControl,
|
|
||||||
InputLabel,
|
|
||||||
TextField,
|
TextField,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Grid,
|
Grid,
|
||||||
RadioGroup,
|
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
Radio,
|
|
||||||
Checkbox,
|
Checkbox,
|
||||||
IconButton,
|
IconButton,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions
|
DialogActions,
|
||||||
|
ListItemText
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { Add as AddIcon, Delete as DeleteIcon, UploadFile as UploadFileIcon } from "@mui/icons-material";
|
import { Add as AddIcon, Delete as DeleteIcon, UploadFile as UploadFileIcon } from "@mui/icons-material";
|
||||||
import Prism from "prismjs";
|
import Prism from "prismjs";
|
||||||
@@ -50,6 +46,65 @@ const VARIABLE_TYPE_OPTIONS = [
|
|||||||
{ key: "credential", label: "Credential" }
|
{ key: "credential", label: "Credential" }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const INPUT_BASE_SX = {
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
bgcolor: "#1f2329",
|
||||||
|
color: "#e6edf3",
|
||||||
|
borderRadius: 1,
|
||||||
|
minHeight: 42,
|
||||||
|
"& fieldset": { borderColor: "#2d333b" },
|
||||||
|
"&:hover fieldset": { borderColor: "#3b4a5c" },
|
||||||
|
"&.Mui-focused fieldset": { borderColor: "#58a6ff" }
|
||||||
|
},
|
||||||
|
"& .MuiOutlinedInput-input": {
|
||||||
|
padding: "10px 12px",
|
||||||
|
fontSize: "0.95rem"
|
||||||
|
},
|
||||||
|
"& .MuiOutlinedInput-inputMultiline": {
|
||||||
|
padding: "10px 12px"
|
||||||
|
},
|
||||||
|
"& .MuiInputLabel-root": { color: "#9ba3b4" },
|
||||||
|
"& .MuiInputLabel-root.Mui-focused": { color: "#58a6ff" }
|
||||||
|
};
|
||||||
|
|
||||||
|
const SELECT_BASE_SX = {
|
||||||
|
...INPUT_BASE_SX,
|
||||||
|
"& .MuiSelect-select": {
|
||||||
|
padding: "10px 12px !important",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const SECTION_TITLE_SX = {
|
||||||
|
color: "#58a6ff",
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: "14px",
|
||||||
|
letterSpacing: 0.2
|
||||||
|
};
|
||||||
|
|
||||||
|
const SECTION_CARD_SX = {
|
||||||
|
bgcolor: "#161b22",
|
||||||
|
borderRadius: 2,
|
||||||
|
border: "1px solid #1f2a37"
|
||||||
|
};
|
||||||
|
|
||||||
|
const MENU_PROPS = {
|
||||||
|
PaperProps: {
|
||||||
|
sx: {
|
||||||
|
bgcolor: "#1f2329",
|
||||||
|
color: "#e6edf3",
|
||||||
|
border: "1px solid #2d333b",
|
||||||
|
"& .MuiMenuItem-root.Mui-selected": {
|
||||||
|
bgcolor: "rgba(88,166,255,0.16)"
|
||||||
|
},
|
||||||
|
"& .MuiMenuItem-root.Mui-selected:hover": {
|
||||||
|
bgcolor: "rgba(88,166,255,0.24)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function keyBy(arr) {
|
function keyBy(arr) {
|
||||||
return Object.fromEntries(arr.map((o) => [o.key, o]));
|
return Object.fromEntries(arr.map((o) => [o.key, o]));
|
||||||
}
|
}
|
||||||
@@ -98,7 +153,7 @@ function defaultAssembly(defaultType = "powershell") {
|
|||||||
category: defaultType === "ansible" ? "application" : "script",
|
category: defaultType === "ansible" ? "application" : "script",
|
||||||
type: defaultType,
|
type: defaultType,
|
||||||
script: "",
|
script: "",
|
||||||
timeoutSeconds: 0,
|
timeoutSeconds: 3600,
|
||||||
sites: { mode: "all", values: [] },
|
sites: { mode: "all", values: [] },
|
||||||
variables: [],
|
variables: [],
|
||||||
files: []
|
files: []
|
||||||
@@ -134,9 +189,17 @@ function fromServerDocument(doc = {}, defaultType = "powershell") {
|
|||||||
assembly.description = doc.description || "";
|
assembly.description = doc.description || "";
|
||||||
assembly.category = doc.category || assembly.category;
|
assembly.category = doc.category || assembly.category;
|
||||||
assembly.type = doc.type || assembly.type;
|
assembly.type = doc.type || assembly.type;
|
||||||
assembly.script = doc.script ?? doc.content ?? "";
|
if (Array.isArray(doc.script_lines)) {
|
||||||
const timeout = doc.timeout_seconds ?? doc.timeout ?? 0;
|
assembly.script = doc.script_lines
|
||||||
assembly.timeoutSeconds = Number.isFinite(Number(timeout)) ? Number(timeout) : 0;
|
.map((line) => (line == null ? "" : String(line)))
|
||||||
|
.join("\n");
|
||||||
|
} else {
|
||||||
|
assembly.script = doc.script ?? doc.content ?? "";
|
||||||
|
}
|
||||||
|
const timeout = doc.timeout_seconds ?? doc.timeout ?? assembly.timeoutSeconds;
|
||||||
|
assembly.timeoutSeconds = Number.isFinite(Number(timeout))
|
||||||
|
? Number(timeout)
|
||||||
|
: assembly.timeoutSeconds;
|
||||||
const sites = doc.sites || {};
|
const sites = doc.sites || {};
|
||||||
assembly.sites = {
|
assembly.sites = {
|
||||||
mode: sites.mode || (Array.isArray(sites.values) && sites.values.length ? "specific" : "all"),
|
mode: sites.mode || (Array.isArray(sites.values) && sites.values.length ? "specific" : "all"),
|
||||||
@@ -149,14 +212,21 @@ function fromServerDocument(doc = {}, defaultType = "powershell") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toServerDocument(assembly) {
|
function toServerDocument(assembly) {
|
||||||
|
const normalizedScript = typeof assembly.script === "string"
|
||||||
|
? assembly.script.replace(/\r\n/g, "\n")
|
||||||
|
: "";
|
||||||
|
const scriptLines = normalizedScript ? normalizedScript.split("\n") : [];
|
||||||
|
const timeoutNumeric = Number(assembly.timeoutSeconds);
|
||||||
|
const timeoutSeconds = Number.isFinite(timeoutNumeric) ? Math.max(0, Math.round(timeoutNumeric)) : 3600;
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
name: assembly.name?.trim() || "",
|
name: assembly.name?.trim() || "",
|
||||||
description: assembly.description || "",
|
description: assembly.description || "",
|
||||||
category: assembly.category || "script",
|
category: assembly.category || "script",
|
||||||
type: assembly.type || "powershell",
|
type: assembly.type || "powershell",
|
||||||
script: assembly.script ?? "",
|
script: normalizedScript,
|
||||||
timeout_seconds: Number.isFinite(Number(assembly.timeoutSeconds)) ? Number(assembly.timeoutSeconds) : 0,
|
script_lines: scriptLines,
|
||||||
|
timeout_seconds: timeoutSeconds,
|
||||||
sites: {
|
sites: {
|
||||||
mode: assembly.sites?.mode === "specific" ? "specific" : "all",
|
mode: assembly.sites?.mode === "specific" ? "specific" : "all",
|
||||||
values: Array.isArray(assembly.sites?.values)
|
values: Array.isArray(assembly.sites?.values)
|
||||||
@@ -182,7 +252,7 @@ function toServerDocument(assembly) {
|
|||||||
|
|
||||||
function RenameFileDialog({ open, value, onChange, onCancel, onSave }) {
|
function RenameFileDialog({ open, value, onChange, onCancel, onSave }) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onCancel} PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}>
|
<Dialog open={open} onClose={onCancel} PaperProps={{ sx: { bgcolor: "#1a1f27", color: "#fff" } }}>
|
||||||
<DialogTitle>Rename Assembly File</DialogTitle>
|
<DialogTitle>Rename Assembly File</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -193,15 +263,7 @@ function RenameFileDialog({ open, value, onChange, onCancel, onSave }) {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
sx={{
|
sx={INPUT_BASE_SX}
|
||||||
"& .MuiOutlinedInput-root": {
|
|
||||||
backgroundColor: "#1e1e1e",
|
|
||||||
color: "#e6edf3",
|
|
||||||
"& fieldset": { borderColor: "#333" },
|
|
||||||
"&:hover fieldset": { borderColor: "#555" }
|
|
||||||
},
|
|
||||||
"& .MuiInputLabel-root": { color: "#aaa" }
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
@@ -229,6 +291,8 @@ export default function AssemblyEditor({
|
|||||||
const [renameValue, setRenameValue] = useState("");
|
const [renameValue, setRenameValue] = useState("");
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [siteOptions, setSiteOptions] = useState([]);
|
||||||
|
const [siteLoading, setSiteLoading] = useState(false);
|
||||||
const contextNonceRef = useRef(null);
|
const contextNonceRef = useRef(null);
|
||||||
|
|
||||||
const TYPE_OPTIONS = useMemo(
|
const TYPE_OPTIONS = useMemo(
|
||||||
@@ -236,6 +300,17 @@ export default function AssemblyEditor({
|
|||||||
[isAnsible]
|
[isAnsible]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const siteOptionMap = useMemo(() => {
|
||||||
|
const map = new Map();
|
||||||
|
siteOptions.forEach((site) => {
|
||||||
|
if (!site) return;
|
||||||
|
const id = site.id != null ? String(site.id) : "";
|
||||||
|
if (!id) return;
|
||||||
|
map.set(id, site);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [siteOptions]);
|
||||||
|
|
||||||
const island = isAnsible ? "ansible" : "scripts";
|
const island = isAnsible ? "ansible" : "scripts";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -284,20 +359,59 @@ export default function AssemblyEditor({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [initialContext?.nonce]);
|
}, [initialContext?.nonce]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let canceled = false;
|
||||||
|
const loadSites = async () => {
|
||||||
|
try {
|
||||||
|
setSiteLoading(true);
|
||||||
|
const resp = await fetch("/api/sites");
|
||||||
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
if (canceled) return;
|
||||||
|
const items = Array.isArray(data?.sites) ? data.sites : [];
|
||||||
|
setSiteOptions(items.map((s) => ({ ...s, id: s?.id != null ? String(s.id) : "" })).filter((s) => s.id));
|
||||||
|
} catch (err) {
|
||||||
|
if (!canceled) {
|
||||||
|
console.error("Failed to load sites:", err);
|
||||||
|
setSiteOptions([]);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!canceled) setSiteLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadSites();
|
||||||
|
return () => {
|
||||||
|
canceled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const prismLanguage = TYPE_MAP[assembly.type]?.prism || "powershell";
|
const prismLanguage = TYPE_MAP[assembly.type]?.prism || "powershell";
|
||||||
|
|
||||||
const updateAssembly = (partial) => {
|
const updateAssembly = (partial) => {
|
||||||
setAssembly((prev) => ({ ...prev, ...partial }));
|
setAssembly((prev) => ({ ...prev, ...partial }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSitesChange = (modeValue, values) => {
|
const updateSitesMode = (modeValue) => {
|
||||||
setAssembly((prev) => ({
|
setAssembly((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
sites: {
|
sites: {
|
||||||
mode: modeValue,
|
mode: modeValue,
|
||||||
values: Array.isArray(values)
|
values: modeValue === "specific" ? prev.sites.values || [] : []
|
||||||
? values
|
}
|
||||||
: ((values || "").split(/\r?\n/).map((v) => v.trim()).filter(Boolean))
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSelectedSites = (values) => {
|
||||||
|
const arr = Array.isArray(values)
|
||||||
|
? values
|
||||||
|
: typeof values === "string"
|
||||||
|
? values.split(",").map((v) => v.trim()).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
setAssembly((prev) => ({
|
||||||
|
...prev,
|
||||||
|
sites: {
|
||||||
|
mode: "specific",
|
||||||
|
values: arr.map((v) => String(v))
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
@@ -475,23 +589,27 @@ export default function AssemblyEditor({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const siteValuesText = (assembly.sites?.values || []).join("\n");
|
const siteScopeValue = assembly.sites?.mode === "specific" ? "specific" : "all";
|
||||||
|
const selectedSiteValues = Array.isArray(assembly.sites?.values)
|
||||||
|
? assembly.sites.values.map((v) => String(v))
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", flex: 1, height: "100%", overflow: "hidden" }}>
|
<Box sx={{ display: "flex", flexDirection: "column", flex: 1, height: "100%", overflow: "hidden" }}>
|
||||||
<Box sx={{ px: 2, pt: 2 }}>
|
<Box sx={{ flex: 1, overflow: "auto", p: 2 }}>
|
||||||
<Typography variant="h5" sx={{ color: "#58a6ff", fontWeight: 500, mb: 0.5 }}>
|
<Paper sx={{ p: 3, ...SECTION_CARD_SX, minHeight: "100%" }} elevation={0}>
|
||||||
Assembly Editor
|
<Box sx={{ mb: 3 }}>
|
||||||
</Typography>
|
<Typography variant="h5" sx={{ color: "#58a6ff", fontWeight: 500, mb: 0.5 }}>
|
||||||
<Typography variant="body2" sx={{ color: "#9ba3b4", mb: 2 }}>
|
Assembly Editor
|
||||||
Create and edit variables, scripts, and other fields related to assemblies.
|
</Typography>
|
||||||
</Typography>
|
<Typography variant="body2" sx={{ color: "#9ba3b4" }}>
|
||||||
</Box>
|
Create and edit variables, scripts, and other fields related to assemblies.
|
||||||
<Box sx={{ flex: 1, overflow: "auto", p: 2, pt: 0 }}>
|
</Typography>
|
||||||
<Paper sx={{ p: 2, bgcolor: "#1e1e1e", borderRadius: 2, border: "1px solid #2a2a2a" }} elevation={2}>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2, mb: 3 }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 2, mb: 3 }}>
|
||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{ flex: 1 }}>
|
||||||
<Typography variant="subtitle2" sx={{ color: "#58a6ff" }}>
|
<Typography variant="caption" sx={SECTION_TITLE_SX}>
|
||||||
Assembly Details
|
Assembly Details
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -525,8 +643,15 @@ export default function AssemblyEditor({
|
|||||||
color: "#58a6ff",
|
color: "#58a6ff",
|
||||||
borderColor: "#58a6ff",
|
borderColor: "#58a6ff",
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
backgroundColor: saving ? "rgba(88,166,255,0.08)" : "#1e1e1e",
|
backgroundColor: saving ? "rgba(88,166,255,0.12)" : "#1f2329",
|
||||||
"&:hover": { borderColor: "#7db7ff" }
|
"&:hover": {
|
||||||
|
borderColor: "#7db7ff",
|
||||||
|
backgroundColor: "rgba(88,166,255,0.18)"
|
||||||
|
},
|
||||||
|
"&.Mui-disabled": {
|
||||||
|
color: "#3c4452",
|
||||||
|
borderColor: "#2d333b"
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{saving ? "Saving..." : "Save Assembly"}
|
{saving ? "Saving..." : "Save Assembly"}
|
||||||
@@ -541,16 +666,7 @@ export default function AssemblyEditor({
|
|||||||
onChange={(e) => updateAssembly({ name: e.target.value })}
|
onChange={(e) => updateAssembly({ name: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{
|
sx={{ ...INPUT_BASE_SX, mb: 2 }}
|
||||||
mb: 2,
|
|
||||||
"& .MuiOutlinedInput-root": {
|
|
||||||
bgcolor: "#121212",
|
|
||||||
color: "#e6edf3",
|
|
||||||
"& fieldset": { borderColor: "#333" },
|
|
||||||
"&:hover fieldset": { borderColor: "#555" }
|
|
||||||
},
|
|
||||||
"& .MuiInputLabel-root": { color: "#aaa" }
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="Description"
|
label="Description"
|
||||||
@@ -560,63 +676,45 @@ export default function AssemblyEditor({
|
|||||||
minRows={3}
|
minRows={3}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{
|
sx={INPUT_BASE_SX}
|
||||||
"& .MuiOutlinedInput-root": {
|
|
||||||
bgcolor: "#121212",
|
|
||||||
color: "#e6edf3",
|
|
||||||
"& fieldset": { borderColor: "#333" },
|
|
||||||
"&:hover fieldset": { borderColor: "#555" }
|
|
||||||
},
|
|
||||||
"& .MuiInputLabel-root": { color: "#aaa" }
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={6}>
|
||||||
<FormControl fullWidth sx={{ mb: 2 }}>
|
<TextField
|
||||||
<InputLabel sx={{ color: "#aaa" }}>Category</InputLabel>
|
select
|
||||||
<Select
|
fullWidth
|
||||||
value={assembly.category}
|
label="Category"
|
||||||
label="Category"
|
value={assembly.category}
|
||||||
onChange={(e) => updateAssembly({ category: e.target.value })}
|
onChange={(e) => updateAssembly({ category: e.target.value })}
|
||||||
sx={{
|
sx={{ ...SELECT_BASE_SX, mb: 2 }}
|
||||||
bgcolor: "#121212",
|
SelectProps={{ MenuProps: MENU_PROPS }}
|
||||||
color: "#e6edf3",
|
>
|
||||||
"& .MuiOutlinedInput-notchedOutline": { borderColor: "#333" },
|
{CATEGORY_OPTIONS.map((o) => (
|
||||||
"&:hover .MuiOutlinedInput-notchedOutline": { borderColor: "#555" }
|
<MenuItem key={o.key} value={o.key}>{o.label}</MenuItem>
|
||||||
}}
|
))}
|
||||||
>
|
</TextField>
|
||||||
{CATEGORY_OPTIONS.map((o) => (
|
|
||||||
<MenuItem key={o.key} value={o.key}>{o.label}</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl fullWidth>
|
<TextField
|
||||||
<InputLabel sx={{ color: "#aaa" }}>Type</InputLabel>
|
select
|
||||||
<Select
|
fullWidth
|
||||||
value={assembly.type}
|
label="Type"
|
||||||
label="Type"
|
value={assembly.type}
|
||||||
onChange={(e) => updateAssembly({ type: e.target.value })}
|
onChange={(e) => updateAssembly({ type: e.target.value })}
|
||||||
sx={{
|
sx={SELECT_BASE_SX}
|
||||||
bgcolor: "#121212",
|
SelectProps={{ MenuProps: MENU_PROPS }}
|
||||||
color: "#e6edf3",
|
>
|
||||||
"& .MuiOutlinedInput-notchedOutline": { borderColor: "#333" },
|
{TYPE_OPTIONS.map((o) => (
|
||||||
"&:hover .MuiOutlinedInput-notchedOutline": { borderColor: "#555" }
|
<MenuItem key={o.key} value={o.key}>{o.label}</MenuItem>
|
||||||
}}
|
))}
|
||||||
>
|
</TextField>
|
||||||
{TYPE_OPTIONS.map((o) => (
|
|
||||||
<MenuItem key={o.key} value={o.key}>{o.label}</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Box sx={{ mt: 3 }}>
|
<Box sx={{ mt: 3 }}>
|
||||||
<Typography variant="subtitle2" sx={{ color: "#58a6ff", mb: 1 }}>
|
<Typography variant="caption" sx={{ ...SECTION_TITLE_SX, mb: 1 }}>
|
||||||
Script Content
|
Script Content
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ border: "1px solid #333", borderRadius: 1, background: "#121212" }}>
|
<Box sx={{ border: "1px solid #2d333b", borderRadius: 1, background: "#1f2329" }}>
|
||||||
<Editor
|
<Editor
|
||||||
value={assembly.script}
|
value={assembly.script}
|
||||||
onValueChange={(value) => updateAssembly({ script: value })}
|
onValueChange={(value) => updateAssembly({ script: value })}
|
||||||
@@ -627,7 +725,7 @@ export default function AssemblyEditor({
|
|||||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: "#e6edf3",
|
color: "#e6edf3",
|
||||||
background: "#121212",
|
background: "#1f2329",
|
||||||
outline: "none",
|
outline: "none",
|
||||||
minHeight: 320,
|
minHeight: 320,
|
||||||
lineHeight: 1.45,
|
lineHeight: 1.45,
|
||||||
@@ -649,57 +747,78 @@ export default function AssemblyEditor({
|
|||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{
|
sx={INPUT_BASE_SX}
|
||||||
"& .MuiOutlinedInput-root": {
|
|
||||||
bgcolor: "#121212",
|
|
||||||
color: "#e6edf3",
|
|
||||||
"& fieldset": { borderColor: "#333" },
|
|
||||||
"&:hover fieldset": { borderColor: "#555" }
|
|
||||||
},
|
|
||||||
"& .MuiInputLabel-root": { color: "#aaa" }
|
|
||||||
}}
|
|
||||||
helperText="Timeout this script if not completed within X seconds"
|
helperText="Timeout this script if not completed within X seconds"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={6}>
|
||||||
<Typography variant="subtitle2" sx={{ color: "#58a6ff", mb: 1 }}>
|
<Typography variant="caption" sx={{ ...SECTION_TITLE_SX, mb: 1 }}>
|
||||||
Sites
|
Sites
|
||||||
</Typography>
|
</Typography>
|
||||||
<RadioGroup
|
<TextField
|
||||||
row
|
select
|
||||||
value={assembly.sites.mode === "specific" ? "specific" : "all"}
|
fullWidth
|
||||||
onChange={(e) => handleSitesChange(e.target.value, assembly.sites.values)}
|
label="Site Scope"
|
||||||
sx={{ color: "#e6edf3" }}
|
value={siteScopeValue}
|
||||||
|
onChange={(e) => updateSitesMode(e.target.value)}
|
||||||
|
sx={{ ...SELECT_BASE_SX, mb: siteScopeValue === "specific" ? 2 : 0 }}
|
||||||
|
SelectProps={{ MenuProps: MENU_PROPS }}
|
||||||
>
|
>
|
||||||
<FormControlLabel value="all" control={<Radio sx={{ color: "#58a6ff" }} />} label="All Sites" />
|
<MenuItem value="all">All Sites</MenuItem>
|
||||||
<FormControlLabel value="specific" control={<Radio sx={{ color: "#58a6ff" }} />} label="Specific Sites" />
|
<MenuItem value="specific">Specific Sites</MenuItem>
|
||||||
</RadioGroup>
|
</TextField>
|
||||||
{assembly.sites.mode === "specific" ? (
|
{siteScopeValue === "specific" ? (
|
||||||
<TextField
|
<TextField
|
||||||
label="Allowed Sites (one per line)"
|
select
|
||||||
value={siteValuesText}
|
|
||||||
onChange={(e) => handleSitesChange("specific", e.target.value)}
|
|
||||||
multiline
|
|
||||||
minRows={3}
|
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
label="Allowed Sites"
|
||||||
sx={{
|
value={selectedSiteValues}
|
||||||
mt: 1,
|
onChange={(e) => updateSelectedSites(Array.isArray(e.target.value) ? e.target.value : [])}
|
||||||
"& .MuiOutlinedInput-root": {
|
sx={SELECT_BASE_SX}
|
||||||
bgcolor: "#121212",
|
SelectProps={{
|
||||||
color: "#e6edf3",
|
multiple: true,
|
||||||
"& fieldset": { borderColor: "#333" },
|
renderValue: (selected) => {
|
||||||
"&:hover fieldset": { borderColor: "#555" }
|
if (!selected || selected.length === 0) {
|
||||||
|
return <Typography sx={{ color: "#6b7687" }}>Select sites</Typography>;
|
||||||
|
}
|
||||||
|
const names = selected.map((val) => siteOptionMap.get(String(val))?.name || String(val));
|
||||||
|
return names.join(", ");
|
||||||
},
|
},
|
||||||
"& .MuiInputLabel-root": { color: "#aaa" }
|
MenuProps: MENU_PROPS
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
{siteLoading ? (
|
||||||
|
<MenuItem disabled>
|
||||||
|
<ListItemText primary="Loading sites..." />
|
||||||
|
</MenuItem>
|
||||||
|
) : siteOptions.length ? (
|
||||||
|
siteOptions.map((site) => {
|
||||||
|
const value = String(site.id);
|
||||||
|
const checked = selectedSiteValues.includes(value);
|
||||||
|
return (
|
||||||
|
<MenuItem key={value} value={value} sx={{ display: "flex", alignItems: "flex-start", gap: 1 }}>
|
||||||
|
<Checkbox checked={checked} sx={{ color: "#58a6ff", mr: 1 }} />
|
||||||
|
<ListItemText
|
||||||
|
primary={site.name}
|
||||||
|
secondary={site.description ? site.description : undefined}
|
||||||
|
primaryTypographyProps={{ sx: { color: "#e6edf3" } }}
|
||||||
|
secondaryTypographyProps={{ sx: { color: "#7f8794" } }}
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<MenuItem disabled>
|
||||||
|
<ListItemText primary="No sites available" />
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</TextField>
|
||||||
) : null}
|
) : null}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Box sx={{ mt: 4 }}>
|
<Box sx={{ mt: 4 }}>
|
||||||
<Typography variant="subtitle2" sx={{ color: "#58a6ff", mb: 1 }}>
|
<Typography variant="caption" sx={{ ...SECTION_TITLE_SX, mb: 1 }}>
|
||||||
Variables
|
Variables
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" sx={{ color: "#9ba3b4", mb: 2 }}>
|
<Typography variant="body2" sx={{ color: "#9ba3b4", mb: 2 }}>
|
||||||
@@ -710,7 +829,7 @@ export default function AssemblyEditor({
|
|||||||
{assembly.variables.map((variable) => (
|
{assembly.variables.map((variable) => (
|
||||||
<Paper
|
<Paper
|
||||||
key={variable.id}
|
key={variable.id}
|
||||||
sx={{ p: 2, bgcolor: "#171717", border: "1px solid #2a2a2a", borderRadius: 1 }}
|
sx={{ p: 2, bgcolor: "#1f2329", border: "1px solid #2d333b", borderRadius: 1 }}
|
||||||
>
|
>
|
||||||
<Grid container spacing={2} alignItems="center">
|
<Grid container spacing={2} alignItems="center">
|
||||||
<Grid item xs={12} md={3}>
|
<Grid item xs={12} md={3}>
|
||||||
@@ -720,15 +839,7 @@ export default function AssemblyEditor({
|
|||||||
onChange={(e) => updateVariable(variable.id, { name: e.target.value })}
|
onChange={(e) => updateVariable(variable.id, { name: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{
|
sx={INPUT_BASE_SX}
|
||||||
"& .MuiOutlinedInput-root": {
|
|
||||||
bgcolor: "#121212",
|
|
||||||
color: "#e6edf3",
|
|
||||||
"& fieldset": { borderColor: "#333" },
|
|
||||||
"&:hover fieldset": { borderColor: "#555" }
|
|
||||||
},
|
|
||||||
"& .MuiInputLabel-root": { color: "#aaa" }
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} md={3}>
|
<Grid item xs={12} md={3}>
|
||||||
@@ -738,36 +849,23 @@ export default function AssemblyEditor({
|
|||||||
onChange={(e) => updateVariable(variable.id, { label: e.target.value })}
|
onChange={(e) => updateVariable(variable.id, { label: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{
|
sx={INPUT_BASE_SX}
|
||||||
"& .MuiOutlinedInput-root": {
|
|
||||||
bgcolor: "#121212",
|
|
||||||
color: "#e6edf3",
|
|
||||||
"& fieldset": { borderColor: "#333" },
|
|
||||||
"&:hover fieldset": { borderColor: "#555" }
|
|
||||||
},
|
|
||||||
"& .MuiInputLabel-root": { color: "#aaa" }
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} md={2}>
|
<Grid item xs={12} md={2}>
|
||||||
<FormControl fullWidth>
|
<TextField
|
||||||
<InputLabel sx={{ color: "#aaa" }}>Type</InputLabel>
|
select
|
||||||
<Select
|
fullWidth
|
||||||
value={variable.type}
|
label="Type"
|
||||||
label="Type"
|
value={variable.type}
|
||||||
onChange={(e) => updateVariable(variable.id, { type: e.target.value })}
|
onChange={(e) => updateVariable(variable.id, { type: e.target.value })}
|
||||||
sx={{
|
sx={SELECT_BASE_SX}
|
||||||
bgcolor: "#121212",
|
SelectProps={{ MenuProps: MENU_PROPS }}
|
||||||
color: "#e6edf3",
|
>
|
||||||
"& .MuiOutlinedInput-notchedOutline": { borderColor: "#333" },
|
{VARIABLE_TYPE_OPTIONS.map((opt) => (
|
||||||
"&:hover .MuiOutlinedInput-notchedOutline": { borderColor: "#555" }
|
<MenuItem key={opt.key} value={opt.key}>{opt.label}</MenuItem>
|
||||||
}}
|
))}
|
||||||
>
|
</TextField>
|
||||||
{VARIABLE_TYPE_OPTIONS.map((opt) => (
|
|
||||||
<MenuItem key={opt.key} value={opt.key}>{opt.label}</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} md={3}>
|
<Grid item xs={12} md={3}>
|
||||||
{variable.type === "boolean" ? (
|
{variable.type === "boolean" ? (
|
||||||
@@ -788,15 +886,7 @@ export default function AssemblyEditor({
|
|||||||
onChange={(e) => updateVariable(variable.id, { defaultValue: e.target.value })}
|
onChange={(e) => updateVariable(variable.id, { defaultValue: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{
|
sx={INPUT_BASE_SX}
|
||||||
"& .MuiOutlinedInput-root": {
|
|
||||||
bgcolor: "#121212",
|
|
||||||
color: "#e6edf3",
|
|
||||||
"& fieldset": { borderColor: "#333" },
|
|
||||||
"&:hover fieldset": { borderColor: "#555" }
|
|
||||||
},
|
|
||||||
"& .MuiInputLabel-root": { color: "#aaa" }
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -818,15 +908,7 @@ export default function AssemblyEditor({
|
|||||||
multiline
|
multiline
|
||||||
minRows={2}
|
minRows={2}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{
|
sx={INPUT_BASE_SX}
|
||||||
"& .MuiOutlinedInput-root": {
|
|
||||||
bgcolor: "#121212",
|
|
||||||
color: "#e6edf3",
|
|
||||||
"& fieldset": { borderColor: "#333" },
|
|
||||||
"&:hover fieldset": { borderColor: "#555" }
|
|
||||||
},
|
|
||||||
"& .MuiInputLabel-root": { color: "#aaa" }
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sx={{ display: "flex", justifyContent: "flex-end" }}>
|
<Grid item xs={12} sx={{ display: "flex", justifyContent: "flex-end" }}>
|
||||||
@@ -853,7 +935,7 @@ export default function AssemblyEditor({
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ mt: 4 }}>
|
<Box sx={{ mt: 4 }}>
|
||||||
<Typography variant="subtitle2" sx={{ color: "#58a6ff", mb: 1 }}>
|
<Typography variant="caption" sx={{ ...SECTION_TITLE_SX, mb: 1 }}>
|
||||||
Files
|
Files
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" sx={{ color: "#9ba3b4", mb: 2 }}>
|
<Typography variant="body2" sx={{ color: "#9ba3b4", mb: 2 }}>
|
||||||
@@ -862,10 +944,20 @@ export default function AssemblyEditor({
|
|||||||
{(assembly.files || []).length ? (
|
{(assembly.files || []).length ? (
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
|
||||||
{assembly.files.map((file) => (
|
{assembly.files.map((file) => (
|
||||||
<Paper key={file.id} sx={{ p: 1.5, bgcolor: "#171717", border: "1px solid #2a2a2a", display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
<Paper
|
||||||
|
key={file.id}
|
||||||
|
sx={{
|
||||||
|
p: 1.5,
|
||||||
|
bgcolor: "#1f2329",
|
||||||
|
border: "1px solid #2d333b",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between"
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="body2" sx={{ color: "#e6edf3" }}>{file.fileName}</Typography>
|
<Typography variant="body2" sx={{ color: "#e6edf3" }}>{file.fileName}</Typography>
|
||||||
<Typography variant="caption" sx={{ color: "#888" }}>{formatBytes(file.size)}{file.mimeType ? ` • ${file.mimeType}` : ""}</Typography>
|
<Typography variant="caption" sx={{ color: "#7f8794" }}>{formatBytes(file.size)}{file.mimeType ? ` • ${file.mimeType}` : ""}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton onClick={() => removeFile(file.id)} sx={{ color: "#ff6b6b" }}>
|
<IconButton onClick={() => removeFile(file.id)} sx={{ color: "#ff6b6b" }}>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
|
|||||||
@@ -182,9 +182,10 @@ class JobScheduler:
|
|||||||
"category": "application" if default_type == "ansible" else "script",
|
"category": "application" if default_type == "ansible" else "script",
|
||||||
"type": default_type,
|
"type": default_type,
|
||||||
"script": "",
|
"script": "",
|
||||||
|
"script_lines": [],
|
||||||
"variables": [],
|
"variables": [],
|
||||||
"files": [],
|
"files": [],
|
||||||
"timeout_seconds": 0,
|
"timeout_seconds": 3600,
|
||||||
}
|
}
|
||||||
if abs_path.lower().endswith(".json") and os.path.isfile(abs_path):
|
if abs_path.lower().endswith(".json") and os.path.isfile(abs_path):
|
||||||
try:
|
try:
|
||||||
@@ -202,16 +203,29 @@ class JobScheduler:
|
|||||||
if typ in ("powershell", "batch", "bash", "ansible"):
|
if typ in ("powershell", "batch", "bash", "ansible"):
|
||||||
doc["type"] = typ
|
doc["type"] = typ
|
||||||
script_val = data.get("script")
|
script_val = data.get("script")
|
||||||
if isinstance(script_val, str):
|
script_lines = data.get("script_lines")
|
||||||
|
if isinstance(script_lines, list):
|
||||||
|
try:
|
||||||
|
doc["script"] = "\n".join(str(line) for line in script_lines)
|
||||||
|
except Exception:
|
||||||
|
doc["script"] = ""
|
||||||
|
elif isinstance(script_val, str):
|
||||||
doc["script"] = script_val
|
doc["script"] = script_val
|
||||||
else:
|
else:
|
||||||
content_val = data.get("content")
|
content_val = data.get("content")
|
||||||
if isinstance(content_val, str):
|
if isinstance(content_val, str):
|
||||||
doc["script"] = content_val
|
doc["script"] = content_val
|
||||||
|
normalized_script = (doc["script"] or "").replace("\r\n", "\n")
|
||||||
|
doc["script"] = normalized_script
|
||||||
|
doc["script_lines"] = normalized_script.split("\n") if normalized_script else []
|
||||||
try:
|
try:
|
||||||
doc["timeout_seconds"] = max(0, int(data.get("timeout_seconds") or 0))
|
timeout_raw = data.get("timeout_seconds", data.get("timeout"))
|
||||||
|
if timeout_raw is None:
|
||||||
|
doc["timeout_seconds"] = 3600
|
||||||
|
else:
|
||||||
|
doc["timeout_seconds"] = max(0, int(timeout_raw))
|
||||||
except Exception:
|
except Exception:
|
||||||
doc["timeout_seconds"] = 0
|
doc["timeout_seconds"] = 3600
|
||||||
vars_in = data.get("variables") if isinstance(data.get("variables"), list) else []
|
vars_in = data.get("variables") if isinstance(data.get("variables"), list) else []
|
||||||
doc["variables"] = []
|
doc["variables"] = []
|
||||||
for v in vars_in:
|
for v in vars_in:
|
||||||
@@ -252,9 +266,12 @@ class JobScheduler:
|
|||||||
return doc
|
return doc
|
||||||
try:
|
try:
|
||||||
with open(abs_path, "r", encoding="utf-8", errors="replace") as fh:
|
with open(abs_path, "r", encoding="utf-8", errors="replace") as fh:
|
||||||
doc["script"] = fh.read()
|
content = fh.read()
|
||||||
except Exception:
|
except Exception:
|
||||||
doc["script"] = ""
|
content = ""
|
||||||
|
normalized_script = (content or "").replace("\r\n", "\n")
|
||||||
|
doc["script"] = normalized_script
|
||||||
|
doc["script_lines"] = normalized_script.split("\n") if normalized_script else []
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
def _ansible_root(self) -> str:
|
def _ansible_root(self) -> str:
|
||||||
|
|||||||
@@ -682,7 +682,8 @@ def _empty_assembly_document(default_type: str = "powershell") -> Dict[str, Any]
|
|||||||
"category": "application" if (default_type or "").lower() == "ansible" else "script",
|
"category": "application" if (default_type or "").lower() == "ansible" else "script",
|
||||||
"type": default_type or "powershell",
|
"type": default_type or "powershell",
|
||||||
"script": "",
|
"script": "",
|
||||||
"timeout_seconds": 0,
|
"script_lines": [],
|
||||||
|
"timeout_seconds": 3600,
|
||||||
"sites": {"mode": "all", "values": []},
|
"sites": {"mode": "all", "values": []},
|
||||||
"variables": [],
|
"variables": [],
|
||||||
"files": []
|
"files": []
|
||||||
@@ -703,12 +704,21 @@ def _normalize_assembly_document(obj: Any, default_type: str, base_name: str) ->
|
|||||||
if typ in ("powershell", "batch", "bash", "ansible"):
|
if typ in ("powershell", "batch", "bash", "ansible"):
|
||||||
doc["type"] = typ
|
doc["type"] = typ
|
||||||
script_val = obj.get("script")
|
script_val = obj.get("script")
|
||||||
if isinstance(script_val, str):
|
script_lines = obj.get("script_lines")
|
||||||
|
if isinstance(script_lines, list):
|
||||||
|
try:
|
||||||
|
doc["script"] = "\n".join(str(line) for line in script_lines)
|
||||||
|
except Exception:
|
||||||
|
doc["script"] = ""
|
||||||
|
elif isinstance(script_val, str):
|
||||||
doc["script"] = script_val
|
doc["script"] = script_val
|
||||||
else:
|
else:
|
||||||
content_val = obj.get("content")
|
content_val = obj.get("content")
|
||||||
if isinstance(content_val, str):
|
if isinstance(content_val, str):
|
||||||
doc["script"] = content_val
|
doc["script"] = content_val
|
||||||
|
normalized_script = (doc["script"] or "").replace("\r\n", "\n")
|
||||||
|
doc["script"] = normalized_script
|
||||||
|
doc["script_lines"] = normalized_script.split("\n") if normalized_script else []
|
||||||
timeout_val = obj.get("timeout_seconds", obj.get("timeout"))
|
timeout_val = obj.get("timeout_seconds", obj.get("timeout"))
|
||||||
if timeout_val is not None:
|
if timeout_val is not None:
|
||||||
try:
|
try:
|
||||||
@@ -786,7 +796,9 @@ def _load_assembly_document(abs_path: str, island: str, type_hint: str = "") ->
|
|||||||
content = ""
|
content = ""
|
||||||
doc = _empty_assembly_document(default_type)
|
doc = _empty_assembly_document(default_type)
|
||||||
doc["name"] = base_name
|
doc["name"] = base_name
|
||||||
doc["script"] = content
|
normalized_script = (content or "").replace("\r\n", "\n")
|
||||||
|
doc["script"] = normalized_script
|
||||||
|
doc["script_lines"] = normalized_script.split("\n") if normalized_script else []
|
||||||
if default_type == "ansible":
|
if default_type == "ansible":
|
||||||
doc["category"] = "application"
|
doc["category"] = "application"
|
||||||
return doc
|
return doc
|
||||||
|
|||||||
Reference in New Issue
Block a user