9 Commits

Author SHA1 Message Date
a61a047db2 Update dependency @eslint/js to ^9.29.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-06-17 00:25:31 +00:00
a2845ddafe Enable word wrapping in Monaco editors
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-16 21:53:43 +02:00
c968b64b51 Fix ESLint issues
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-16 21:52:00 +02:00
12833dc6da Fix cloud init command
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-16 21:48:06 +02:00
8c4f2a9f2d Fix issue with Ubuntu cloud images
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-16 21:31:33 +02:00
9a6b6cfb2d Can change default username
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-16 19:56:04 +02:00
b28ca5f27d Add new options
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-16 19:42:57 +02:00
92f187bf91 Start to auto-fill cloudinit fields
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-16 19:31:13 +02:00
9f1f4b44ca Make network configuration editable 2025-06-16 18:51:33 +02:00
5 changed files with 242 additions and 14 deletions

View File

@ -29,10 +29,11 @@
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",
"react-vnc": "^3.1.0", "react-vnc": "^3.1.0",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"xml-formatter": "^3.6.6" "xml-formatter": "^3.6.6",
"yaml": "^2.8.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.27.0", "@eslint/js": "^9.29.0",
"@types/humanize-duration": "^3.27.4", "@types/humanize-duration": "^3.27.4",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
@ -743,9 +744,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.27.0", "version": "9.29.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz",
"integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -2788,6 +2789,19 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/eslint/node_modules/@eslint/js": {
"version": "9.27.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz",
"integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://eslint.org/donate"
}
},
"node_modules/espree": { "node_modules/espree": {
"version": "10.3.0", "version": "10.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",

View File

@ -31,10 +31,11 @@
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",
"react-vnc": "^3.1.0", "react-vnc": "^3.1.0",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"xml-formatter": "^3.6.6" "xml-formatter": "^3.6.6",
"yaml": "^2.8.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.27.0", "@eslint/js": "^9.29.0",
"@types/humanize-duration": "^3.27.4", "@types/humanize-duration": "^3.27.4",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",

View File

@ -17,7 +17,9 @@ export function CheckboxInput(p: {
<Checkbox <Checkbox
disabled={!p.editable} disabled={!p.editable}
checked={p.checked} checked={p.checked}
onChange={(e) => { p.onValueChange(e.target.checked); }} onChange={(e) => {
p.onValueChange(e.target.checked);
}}
/> />
} }
label={p.label} label={p.label}

View File

@ -1,8 +1,14 @@
/* eslint-disable @typescript-eslint/no-base-to-string */
import Editor from "@monaco-editor/react"; import Editor from "@monaco-editor/react";
import BookIcon from "@mui/icons-material/Book";
import RefreshIcon from "@mui/icons-material/Refresh"; import RefreshIcon from "@mui/icons-material/Refresh";
import { Grid, IconButton, InputAdornment, Tooltip } from "@mui/material"; import { Grid, IconButton, InputAdornment, Tooltip } from "@mui/material";
import React from "react";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import YAML from "yaml";
import { VMInfo } from "../../api/VMApi"; import { VMInfo } from "../../api/VMApi";
import { RouterLink } from "../RouterLink";
import { CheckboxInput } from "./CheckboxInput"; import { CheckboxInput } from "./CheckboxInput";
import { EditSection } from "./EditSection"; import { EditSection } from "./EditSection";
import { SelectInput } from "./SelectInput"; import { SelectInput } from "./SelectInput";
@ -38,6 +44,14 @@ export function CloudInitEditor(p: CloudInitProps): React.ReactElement {
{...p} {...p}
editable={p.editable && p.vm.cloud_init.attach_config} editable={p.editable && p.vm.cloud_init.attach_config}
/> />
<CloudInitNetworkConfig
{...p}
editable={p.editable && p.vm.cloud_init.attach_config}
/>
<CloudInitUserDataAssistant
{...p}
editable={p.editable && p.vm.cloud_init.attach_config}
/>
</Grid> </Grid>
</> </>
); );
@ -108,12 +122,27 @@ function CloudInitMetadata(p: CloudInitProps): React.ReactElement {
function CloudInitRawUserData(p: CloudInitProps): React.ReactElement { function CloudInitRawUserData(p: CloudInitProps): React.ReactElement {
return ( return (
<EditSection title="User data"> <EditSection
title="User data"
actions={
<RouterLink
target="_blank"
to="https://cloudinit.readthedocs.io/en/latest/reference/index.html"
>
<Tooltip title="Official reference">
<IconButton size="small">
<BookIcon />
</IconButton>
</Tooltip>
</RouterLink>
}
>
<Editor <Editor
theme="vs-dark" theme="vs-dark"
options={{ options={{
readOnly: !p.editable, readOnly: !p.editable,
quickSuggestions: { other: true, comments: true, strings: true }, quickSuggestions: { other: true, comments: true, strings: true },
wordWrap: "on",
}} }}
language="yaml" language="yaml"
height={"30vh"} height={"30vh"}
@ -126,3 +155,188 @@ function CloudInitRawUserData(p: CloudInitProps): React.ReactElement {
</EditSection> </EditSection>
); );
} }
function CloudInitNetworkConfig(p: CloudInitProps): React.ReactElement {
if (!p.editable && !p.vm.cloud_init.network_configuration) return <></>;
return (
<EditSection
title="Network configuration"
actions={
<RouterLink
target="_blank"
to="https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v2.html"
>
<Tooltip title="Official network configuration reference">
<IconButton size="small">
<BookIcon />
</IconButton>
</Tooltip>
</RouterLink>
}
>
<Editor
theme="vs-dark"
options={{
readOnly: !p.editable,
quickSuggestions: { other: true, comments: true, strings: true },
wordWrap: "on",
}}
language="yaml"
height={"30vh"}
value={p.vm.cloud_init.network_configuration ?? ""}
onChange={(v) => {
if (v && v !== "") p.vm.cloud_init.network_configuration = v;
else p.vm.cloud_init.network_configuration = undefined;
p.onChange?.();
}}
/>
</EditSection>
);
}
function CloudInitUserDataAssistant(p: CloudInitProps): React.ReactElement {
const user_data = React.useMemo(() => {
return YAML.parseDocument(p.vm.cloud_init.user_data);
}, [p.vm.cloud_init.user_data]);
const onChange = () => {
p.vm.cloud_init.user_data = user_data.toString();
if (!p.vm.cloud_init.user_data.startsWith("#cloud-config"))
p.vm.cloud_init.user_data = `#cloud-config\n${p.vm.cloud_init.user_data}`;
p.onChange?.();
};
const SYSTEMD_NOT_SERIAL = `/bin/sh -c "rm -f /etc/default/grub.d/50-cloudimg-settings.cfg && sed -i 's/quiet splash//g' /etc/default/grub && update-grub"`;
return (
<EditSection title="User data assistant">
<CloudInitTextInput
editable={p.editable}
name="Default user name"
refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#set-passwords"
attrPath={["user", "name"]}
onChange={onChange}
yaml={user_data}
/>
<CloudInitTextInput
editable={p.editable}
name="Default user password"
refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#set-passwords"
attrPath={["password"]}
onChange={onChange}
yaml={user_data}
/>
<CloudInitBooleanInput
editable={p.editable}
name="Expire password to require new password on next login"
yaml={user_data}
attrPath={["chpasswd", "expire"]}
onChange={onChange}
refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#set-passwords"
/>
<br />
<CloudInitBooleanInput
editable={p.editable}
name="Enable SSH password auth"
yaml={user_data}
attrPath={["ssh_pwauth"]}
onChange={onChange}
refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#set-passwords"
/>
<CloudInitTextInput
editable={p.editable}
name="Keyboard layout"
refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#keyboard"
attrPath={["keyboard", "layout"]}
onChange={onChange}
yaml={user_data}
/>
<CloudInitTextInput
editable={p.editable}
name="Final message"
refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#final-message"
attrPath={["final_message"]}
onChange={onChange}
yaml={user_data}
/>
{/* /bin/sh -c "rm -f /etc/default/grub.d/50-cloudimg-settings.cfg && update-grub" */}
<CheckboxInput
editable={p.editable}
label="Show all startup messages on tty1, not serial"
checked={
!!(user_data.get("runcmd") as any)?.items.find(
(a: any) => a.value === SYSTEMD_NOT_SERIAL
)
}
onValueChange={(c) => {
if (!user_data.getIn(["runcmd"])) user_data.addIn(["runcmd"], []);
const runcmd = user_data.getIn(["runcmd"]) as any;
if (c) {
runcmd.addIn([], SYSTEMD_NOT_SERIAL);
} else {
const idx = runcmd.items.findIndex(
(o: any) => o.value === SYSTEMD_NOT_SERIAL
);
runcmd.items.splice(idx, 1);
}
onChange();
}}
/>
</EditSection>
);
}
function CloudInitTextInput(p: {
editable: boolean;
name: string;
refUrl: string;
attrPath: Iterable<unknown>;
yaml: YAML.Document;
onChange?: () => void;
}): React.ReactElement {
return (
<TextInput
editable={p.editable}
label={p.name}
value={String(p.yaml.getIn(p.attrPath) ?? "")}
onValueChange={(v) => {
if (v !== undefined) p.yaml.setIn(p.attrPath, v);
else p.yaml.deleteIn(p.attrPath);
p.onChange?.();
}}
endAdornment={
<RouterLink to={p.refUrl} target="_blank">
<IconButton size="small">
<BookIcon />
</IconButton>
</RouterLink>
}
/>
);
}
function CloudInitBooleanInput(p: {
editable: boolean;
name: string;
refUrl: string;
attrPath: Iterable<unknown>;
yaml: YAML.Document;
onChange?: () => void;
}): React.ReactElement {
return (
<CheckboxInput
editable={p.editable}
label={p.name}
checked={p.yaml.getIn(p.attrPath) === true}
onValueChange={(v) => {
if (v) p.yaml.setIn(p.attrPath, v);
else p.yaml.deleteIn(p.attrPath);
p.onChange?.();
}}
/>
);
}

View File

@ -19,13 +19,10 @@ export function EditSection(
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
marginBottom: "15px",
}} }}
> >
{p.title && ( {p.title && <Typography variant="h5">{p.title}</Typography>}
<Typography variant="h5" style={{ marginBottom: "15px" }}>
{p.title}
</Typography>
)}
{p.actions} {p.actions}
</span> </span>
)} )}