Compare commits
	
		
			9 Commits
		
	
	
		
			092119f089
			...
			a61a047db2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a61a047db2 | |||
| a2845ddafe | |||
| c968b64b51 | |||
| 12833dc6da | |||
| 8c4f2a9f2d | |||
| 9a6b6cfb2d | |||
| b28ca5f27d | |||
| 92f187bf91 | |||
| 9f1f4b44ca | 
							
								
								
									
										3
									
								
								virtweb_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								virtweb_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -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.29.0", | ||||
|   | ||||
| @@ -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.29.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