Compare commits
9 Commits
a309baa841
...
5180c44ac2
Author | SHA1 | Date | |
---|---|---|---|
5180c44ac2 | |||
a2845ddafe | |||
c968b64b51 | |||
12833dc6da | |||
8c4f2a9f2d | |||
9a6b6cfb2d | |||
b28ca5f27d | |||
92f187bf91 | |||
9f1f4b44ca |
55
virtweb_frontend/package-lock.json
generated
55
virtweb_frontend/package-lock.json
generated
@ -16,7 +16,7 @@
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@mui/icons-material": "^7.1.1",
|
||||
"@mui/material": "^7.1.1",
|
||||
"@mui/x-charts": "^8.3.1",
|
||||
"@mui/x-charts": "^8.5.2",
|
||||
"@mui/x-data-grid": "^8.3.1",
|
||||
"date-and-time": "^3.6.0",
|
||||
"filesize": "^10.1.6",
|
||||
@ -29,7 +29,8 @@
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-vnc": "^3.1.0",
|
||||
"uuid": "^11.1.0",
|
||||
"xml-formatter": "^3.6.6"
|
||||
"xml-formatter": "^3.6.6",
|
||||
"yaml": "^2.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.27.0",
|
||||
@ -291,9 +292,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
|
||||
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
|
||||
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@ -1218,15 +1219,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-charts": {
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.3.1.tgz",
|
||||
"integrity": "sha512-jZClK40++ftcMwCeHKudGKmazd0MsgnrIP6RhYi2lH1kg0jK2upueokyxVIIxqquwWsQYE3WsflJBP61DvYXOQ==",
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.5.2.tgz",
|
||||
"integrity": "sha512-JLPTtd9m8CWMoIxwHFM9QpPDpfdsetfkCErJUvsyQnj/rC8sBMmQqk0c1olusA+OqTyVT3gGmiqXXFar/0cvkw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.1",
|
||||
"@mui/utils": "^7.0.2",
|
||||
"@mui/x-charts-vendor": "8.3.1",
|
||||
"@mui/x-internals": "8.3.1",
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"@mui/utils": "^7.1.1",
|
||||
"@mui/x-charts-vendor": "8.5.2",
|
||||
"@mui/x-internals": "8.5.2",
|
||||
"bezier-easing": "^2.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
@ -1254,12 +1255,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-charts-vendor": {
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.3.1.tgz",
|
||||
"integrity": "sha512-UcUa7HDIpSfeVBYgeHewWoVALcB4Gg9we53l78j2cyadYBZOWdtLj8fezo9zAhxfZ5s9T+1yIyuD+CCnYJnUpQ==",
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.5.2.tgz",
|
||||
"integrity": "sha512-93KFrEpo3Xhr0g2TQsbtPVqGAsbkKBN5J57ykrCM5GxFmq3kDGFU4k9+FpKiaIYYL8ijzgHGNh+jNVbP0pq3rQ==",
|
||||
"license": "MIT AND ISC",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.1",
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"@types/d3-color": "^3.1.3",
|
||||
"@types/d3-delaunay": "^6.0.4",
|
||||
"@types/d3-interpolate": "^3.0.4",
|
||||
@ -1278,6 +1279,28 @@
|
||||
"robust-predicates": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-charts/node_modules/@mui/x-internals": {
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.5.2.tgz",
|
||||
"integrity": "sha512-5YhB2AekK7G8d0YrAjg3WNf0uy3V73JD98WNxJhbIlCraQgl8QOQzr2zNO7MAf/X7mZQtjpjuAsiG3+gI2NVyg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"@mui/utils": "^7.1.1",
|
||||
"reselect": "^5.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-data-grid": {
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.3.1.tgz",
|
||||
|
@ -18,7 +18,7 @@
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@mui/icons-material": "^7.1.1",
|
||||
"@mui/material": "^7.1.1",
|
||||
"@mui/x-charts": "^8.3.1",
|
||||
"@mui/x-charts": "^8.5.2",
|
||||
"@mui/x-data-grid": "^8.3.1",
|
||||
"date-and-time": "^3.6.0",
|
||||
"filesize": "^10.1.6",
|
||||
@ -31,7 +31,8 @@
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-vnc": "^3.1.0",
|
||||
"uuid": "^11.1.0",
|
||||
"xml-formatter": "^3.6.6"
|
||||
"xml-formatter": "^3.6.6",
|
||||
"yaml": "^2.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.27.0",
|
||||
|
@ -17,7 +17,9 @@ export function CheckboxInput(p: {
|
||||
<Checkbox
|
||||
disabled={!p.editable}
|
||||
checked={p.checked}
|
||||
onChange={(e) => { p.onValueChange(e.target.checked); }}
|
||||
onChange={(e) => {
|
||||
p.onValueChange(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={p.label}
|
||||
|
@ -1,8 +1,14 @@
|
||||
/* eslint-disable @typescript-eslint/no-base-to-string */
|
||||
|
||||
import Editor from "@monaco-editor/react";
|
||||
import BookIcon from "@mui/icons-material/Book";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import { Grid, IconButton, InputAdornment, Tooltip } from "@mui/material";
|
||||
import React from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import YAML from "yaml";
|
||||
import { VMInfo } from "../../api/VMApi";
|
||||
import { RouterLink } from "../RouterLink";
|
||||
import { CheckboxInput } from "./CheckboxInput";
|
||||
import { EditSection } from "./EditSection";
|
||||
import { SelectInput } from "./SelectInput";
|
||||
@ -38,6 +44,14 @@ export function CloudInitEditor(p: CloudInitProps): React.ReactElement {
|
||||
{...p}
|
||||
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>
|
||||
</>
|
||||
);
|
||||
@ -108,12 +122,27 @@ function CloudInitMetadata(p: CloudInitProps): React.ReactElement {
|
||||
|
||||
function CloudInitRawUserData(p: CloudInitProps): React.ReactElement {
|
||||
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
|
||||
theme="vs-dark"
|
||||
options={{
|
||||
readOnly: !p.editable,
|
||||
quickSuggestions: { other: true, comments: true, strings: true },
|
||||
wordWrap: "on",
|
||||
}}
|
||||
language="yaml"
|
||||
height={"30vh"}
|
||||
@ -126,3 +155,188 @@ function CloudInitRawUserData(p: CloudInitProps): React.ReactElement {
|
||||
</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?.();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -19,13 +19,10 @@ export function EditSection(
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "15px",
|
||||
}}
|
||||
>
|
||||
{p.title && (
|
||||
<Typography variant="h5" style={{ marginBottom: "15px" }}>
|
||||
{p.title}
|
||||
</Typography>
|
||||
)}
|
||||
{p.title && <Typography variant="h5">{p.title}</Typography>}
|
||||
{p.actions}
|
||||
</span>
|
||||
)}
|
||||
|
Reference in New Issue
Block a user