Can upload disk images on the server
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
6a7af7e6c4
commit
b55880b43c
@ -250,6 +250,11 @@ impl AppConfig {
|
|||||||
self.storage_path().join("iso")
|
self.storage_path().join("iso")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get disk images storage directory
|
||||||
|
pub fn disk_images_storage_path(&self) -> PathBuf {
|
||||||
|
self.storage_path().join("disk_images")
|
||||||
|
}
|
||||||
|
|
||||||
/// Get VM vnc sockets directory
|
/// Get VM vnc sockets directory
|
||||||
pub fn vnc_sockets_path(&self) -> PathBuf {
|
pub fn vnc_sockets_path(&self) -> PathBuf {
|
||||||
self.storage_path().join("vnc")
|
self.storage_path().join("vnc")
|
||||||
|
@ -27,6 +27,13 @@ pub const ALLOWED_ISO_MIME_TYPES: [&str; 4] = [
|
|||||||
/// ISO max size
|
/// ISO max size
|
||||||
pub const ISO_MAX_SIZE: usize = 10 * 1000 * 1000 * 1000;
|
pub const ISO_MAX_SIZE: usize = 10 * 1000 * 1000 * 1000;
|
||||||
|
|
||||||
|
/// Allowed uploaded disk images formats
|
||||||
|
pub const ALLOWED_DISK_IMAGES_MIME_TYPES: [&str; 2] =
|
||||||
|
["application/x-qemu-disk", "application/gzip"];
|
||||||
|
|
||||||
|
/// Disk image max size
|
||||||
|
pub const DISK_IMAGE_MAX_SIZE: usize = 10 * 1000 * 1000 * 1000 * 1000;
|
||||||
|
|
||||||
/// Min VM memory size (MB)
|
/// Min VM memory size (MB)
|
||||||
pub const MIN_VM_MEMORY: usize = 100;
|
pub const MIN_VM_MEMORY: usize = 100;
|
||||||
|
|
||||||
|
57
virtweb_backend/src/controllers/disk_images_controller.rs
Normal file
57
virtweb_backend/src/controllers/disk_images_controller.rs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
use crate::app_config::AppConfig;
|
||||||
|
use crate::constants;
|
||||||
|
use crate::controllers::HttpResult;
|
||||||
|
use crate::utils::files_utils;
|
||||||
|
use actix_multipart::form::MultipartForm;
|
||||||
|
use actix_multipart::form::tempfile::TempFile;
|
||||||
|
use actix_web::HttpResponse;
|
||||||
|
|
||||||
|
#[derive(Debug, MultipartForm)]
|
||||||
|
pub struct UploadDiskImageForm {
|
||||||
|
#[multipart(rename = "file")]
|
||||||
|
files: Vec<TempFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload disk image file
|
||||||
|
pub async fn upload(MultipartForm(mut form): MultipartForm<UploadDiskImageForm>) -> HttpResult {
|
||||||
|
if form.files.is_empty() {
|
||||||
|
log::error!("Missing uploaded disk file!");
|
||||||
|
return Ok(HttpResponse::BadRequest().json("Missing file!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = form.files.remove(0);
|
||||||
|
|
||||||
|
// Check uploaded file size
|
||||||
|
if file.size > constants::DISK_IMAGE_MAX_SIZE {
|
||||||
|
return Ok(HttpResponse::BadRequest().json("Disk image max size exceeded!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file mime type
|
||||||
|
if let Some(mime_type) = file.content_type {
|
||||||
|
if !constants::ALLOWED_DISK_IMAGES_MIME_TYPES.contains(&mime_type.as_ref()) {
|
||||||
|
return Ok(HttpResponse::BadRequest().json(format!(
|
||||||
|
"Unsupported file type for disk upload: {}",
|
||||||
|
mime_type
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract and check file name
|
||||||
|
let Some(file_name) = file.file_name else {
|
||||||
|
return Ok(HttpResponse::BadRequest().json("Missing file name of uploaded file!"));
|
||||||
|
};
|
||||||
|
if !files_utils::check_file_name(&file_name) {
|
||||||
|
return Ok(HttpResponse::BadRequest().json("Invalid uploaded file name!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a file with the same name already exists
|
||||||
|
let dest_path = AppConfig::get().disk_images_storage_path().join(file_name);
|
||||||
|
if dest_path.is_file() {
|
||||||
|
return Ok(HttpResponse::Conflict().json("A file with the same name already exists!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the file to the destination
|
||||||
|
file.file.persist(dest_path)?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json("Successfully uploaded disk image!"))
|
||||||
|
}
|
@ -7,6 +7,7 @@ use std::fmt::{Display, Formatter};
|
|||||||
|
|
||||||
pub mod api_tokens_controller;
|
pub mod api_tokens_controller;
|
||||||
pub mod auth_controller;
|
pub mod auth_controller;
|
||||||
|
pub mod disk_images_controller;
|
||||||
pub mod groups_controller;
|
pub mod groups_controller;
|
||||||
pub mod iso_controller;
|
pub mod iso_controller;
|
||||||
pub mod network_controller;
|
pub mod network_controller;
|
||||||
|
@ -16,6 +16,7 @@ struct StaticConfig {
|
|||||||
local_auth_enabled: bool,
|
local_auth_enabled: bool,
|
||||||
oidc_auth_enabled: bool,
|
oidc_auth_enabled: bool,
|
||||||
iso_mimetypes: &'static [&'static str],
|
iso_mimetypes: &'static [&'static str],
|
||||||
|
disk_images_mimetypes: &'static [&'static str],
|
||||||
net_mac_prefix: &'static str,
|
net_mac_prefix: &'static str,
|
||||||
builtin_nwfilter_rules: &'static [&'static str],
|
builtin_nwfilter_rules: &'static [&'static str],
|
||||||
nwfilter_chains: &'static [&'static str],
|
nwfilter_chains: &'static [&'static str],
|
||||||
@ -37,6 +38,7 @@ struct SLenConstraints {
|
|||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
struct ServerConstraints {
|
struct ServerConstraints {
|
||||||
iso_max_size: usize,
|
iso_max_size: usize,
|
||||||
|
disk_image_max_size: usize,
|
||||||
vnc_token_duration: u64,
|
vnc_token_duration: u64,
|
||||||
vm_name_size: LenConstraints,
|
vm_name_size: LenConstraints,
|
||||||
vm_title_size: LenConstraints,
|
vm_title_size: LenConstraints,
|
||||||
@ -63,11 +65,13 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
|
|||||||
local_auth_enabled: *local_auth,
|
local_auth_enabled: *local_auth,
|
||||||
oidc_auth_enabled: !AppConfig::get().disable_oidc,
|
oidc_auth_enabled: !AppConfig::get().disable_oidc,
|
||||||
iso_mimetypes: &constants::ALLOWED_ISO_MIME_TYPES,
|
iso_mimetypes: &constants::ALLOWED_ISO_MIME_TYPES,
|
||||||
|
disk_images_mimetypes: &constants::ALLOWED_DISK_IMAGES_MIME_TYPES,
|
||||||
net_mac_prefix: constants::NET_MAC_ADDR_PREFIX,
|
net_mac_prefix: constants::NET_MAC_ADDR_PREFIX,
|
||||||
builtin_nwfilter_rules: &constants::BUILTIN_NETWORK_FILTER_RULES,
|
builtin_nwfilter_rules: &constants::BUILTIN_NETWORK_FILTER_RULES,
|
||||||
nwfilter_chains: &constants::NETWORK_CHAINS,
|
nwfilter_chains: &constants::NETWORK_CHAINS,
|
||||||
constraints: ServerConstraints {
|
constraints: ServerConstraints {
|
||||||
iso_max_size: constants::ISO_MAX_SIZE,
|
iso_max_size: constants::ISO_MAX_SIZE,
|
||||||
|
disk_image_max_size: constants::DISK_IMAGE_MAX_SIZE,
|
||||||
|
|
||||||
vnc_token_duration: VNC_TOKEN_LIFETIME,
|
vnc_token_duration: VNC_TOKEN_LIFETIME,
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use std::cmp::max;
|
||||||
use actix::Actor;
|
use actix::Actor;
|
||||||
use actix_cors::Cors;
|
use actix_cors::Cors;
|
||||||
use actix_identity::IdentityMiddleware;
|
use actix_identity::IdentityMiddleware;
|
||||||
@ -22,8 +23,9 @@ use virtweb_backend::constants::{
|
|||||||
MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME,
|
MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME,
|
||||||
};
|
};
|
||||||
use virtweb_backend::controllers::{
|
use virtweb_backend::controllers::{
|
||||||
api_tokens_controller, auth_controller, groups_controller, iso_controller, network_controller,
|
api_tokens_controller, auth_controller, disk_images_controller, groups_controller,
|
||||||
nwfilter_controller, server_controller, static_controller, vm_controller,
|
iso_controller, network_controller, nwfilter_controller, server_controller, static_controller,
|
||||||
|
vm_controller,
|
||||||
};
|
};
|
||||||
use virtweb_backend::libvirt_client::LibVirtClient;
|
use virtweb_backend::libvirt_client::LibVirtClient;
|
||||||
use virtweb_backend::middlewares::auth_middleware::AuthChecker;
|
use virtweb_backend::middlewares::auth_middleware::AuthChecker;
|
||||||
@ -55,6 +57,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
|
|
||||||
log::debug!("Create required directory, if missing");
|
log::debug!("Create required directory, if missing");
|
||||||
files_utils::create_directory_if_missing(AppConfig::get().iso_storage_path()).unwrap();
|
files_utils::create_directory_if_missing(AppConfig::get().iso_storage_path()).unwrap();
|
||||||
|
files_utils::create_directory_if_missing(AppConfig::get().disk_images_storage_path()).unwrap();
|
||||||
files_utils::create_directory_if_missing(AppConfig::get().vnc_sockets_path()).unwrap();
|
files_utils::create_directory_if_missing(AppConfig::get().vnc_sockets_path()).unwrap();
|
||||||
files_utils::set_file_permission(AppConfig::get().vnc_sockets_path(), 0o777).unwrap();
|
files_utils::set_file_permission(AppConfig::get().vnc_sockets_path(), 0o777).unwrap();
|
||||||
files_utils::create_directory_if_missing(AppConfig::get().disks_storage_path()).unwrap();
|
files_utils::create_directory_if_missing(AppConfig::get().disks_storage_path()).unwrap();
|
||||||
@ -118,7 +121,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
}))
|
}))
|
||||||
.app_data(conn.clone())
|
.app_data(conn.clone())
|
||||||
// Uploaded files
|
// Uploaded files
|
||||||
.app_data(MultipartFormConfig::default().total_limit(constants::ISO_MAX_SIZE))
|
.app_data(MultipartFormConfig::default().total_limit(max(constants::DISK_IMAGE_MAX_SIZE,constants::ISO_MAX_SIZE)))
|
||||||
.app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir))
|
.app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir))
|
||||||
// Server controller
|
// Server controller
|
||||||
.route(
|
.route(
|
||||||
@ -329,6 +332,11 @@ async fn main() -> std::io::Result<()> {
|
|||||||
"/api/nwfilter/{uid}",
|
"/api/nwfilter/{uid}",
|
||||||
web::delete().to(nwfilter_controller::delete),
|
web::delete().to(nwfilter_controller::delete),
|
||||||
)
|
)
|
||||||
|
// Disk images library
|
||||||
|
.route(
|
||||||
|
"/api/disk_images/upload",
|
||||||
|
web::post().to(disk_images_controller::upload),
|
||||||
|
)
|
||||||
// API tokens controller
|
// API tokens controller
|
||||||
.route(
|
.route(
|
||||||
"/api/token/create",
|
"/api/token/create",
|
||||||
|
@ -38,6 +38,7 @@ import { LoginRoute } from "./routes/auth/LoginRoute";
|
|||||||
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
|
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
|
||||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
||||||
import { BaseLoginPage } from "./widgets/BaseLoginPage";
|
import { BaseLoginPage } from "./widgets/BaseLoginPage";
|
||||||
|
import { DiskImagesRoute } from "./routes/DiskImagesRoute";
|
||||||
|
|
||||||
interface AuthContext {
|
interface AuthContext {
|
||||||
signedIn: boolean;
|
signedIn: boolean;
|
||||||
@ -63,6 +64,8 @@ export function App() {
|
|||||||
<Route path="*" element={<BaseAuthenticatedPage />}>
|
<Route path="*" element={<BaseAuthenticatedPage />}>
|
||||||
<Route path="" element={<HomeRoute />} />
|
<Route path="" element={<HomeRoute />} />
|
||||||
|
|
||||||
|
<Route path="disk_images" element={<DiskImagesRoute />} />
|
||||||
|
|
||||||
<Route path="iso" element={<IsoFilesRoute />} />
|
<Route path="iso" element={<IsoFilesRoute />} />
|
||||||
|
|
||||||
<Route path="vms" element={<VMListRoute />} />
|
<Route path="vms" element={<VMListRoute />} />
|
||||||
|
31
virtweb_frontend/src/api/DiskImageApi.ts
Normal file
31
virtweb_frontend/src/api/DiskImageApi.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { APIClient } from "./ApiClient";
|
||||||
|
|
||||||
|
export interface DiskImage {}
|
||||||
|
|
||||||
|
export class DiskImageApi {
|
||||||
|
/**
|
||||||
|
* Upload a new disk image file to the server
|
||||||
|
*/
|
||||||
|
static async Upload(
|
||||||
|
file: File,
|
||||||
|
progress: (progress: number) => void
|
||||||
|
): Promise<void> {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("file", file);
|
||||||
|
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "POST",
|
||||||
|
uri: "/disk_images/upload",
|
||||||
|
formData: fd,
|
||||||
|
upProgress: progress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of disk images
|
||||||
|
*/
|
||||||
|
static async GetList(): Promise<DiskImage[]> {
|
||||||
|
// TODO
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ export interface ServerConfig {
|
|||||||
local_auth_enabled: boolean;
|
local_auth_enabled: boolean;
|
||||||
oidc_auth_enabled: boolean;
|
oidc_auth_enabled: boolean;
|
||||||
iso_mimetypes: string[];
|
iso_mimetypes: string[];
|
||||||
|
disk_images_mimetypes: string[];
|
||||||
net_mac_prefix: string;
|
net_mac_prefix: string;
|
||||||
builtin_nwfilter_rules: string[];
|
builtin_nwfilter_rules: string[];
|
||||||
nwfilter_chains: string[];
|
nwfilter_chains: string[];
|
||||||
@ -13,6 +14,7 @@ export interface ServerConfig {
|
|||||||
|
|
||||||
export interface ServerConstraints {
|
export interface ServerConstraints {
|
||||||
iso_max_size: number;
|
iso_max_size: number;
|
||||||
|
disk_image_max_size: number;
|
||||||
vnc_token_duration: number;
|
vnc_token_duration: number;
|
||||||
vm_name_size: LenConstraint;
|
vm_name_size: LenConstraint;
|
||||||
vm_title_size: LenConstraint;
|
vm_title_size: LenConstraint;
|
||||||
|
151
virtweb_frontend/src/routes/DiskImagesRoute.tsx
Normal file
151
virtweb_frontend/src/routes/DiskImagesRoute.tsx
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
LinearProgress,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { filesize } from "filesize";
|
||||||
|
import React from "react";
|
||||||
|
import { DiskImage, DiskImageApi } from "../api/DiskImageApi";
|
||||||
|
import { ServerApi } from "../api/ServerApi";
|
||||||
|
import { useAlert } from "../hooks/providers/AlertDialogProvider";
|
||||||
|
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
|
||||||
|
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||||
|
import { FileInput } from "../widgets/forms/FileInput";
|
||||||
|
import { VirtWebPaper } from "../widgets/VirtWebPaper";
|
||||||
|
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
||||||
|
|
||||||
|
export function DiskImagesRoute(): React.ReactElement {
|
||||||
|
const [list, setList] = React.useState<DiskImage[] | undefined>();
|
||||||
|
|
||||||
|
const loadKey = React.useRef(1);
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
setList(await DiskImageApi.GetList());
|
||||||
|
};
|
||||||
|
|
||||||
|
const reload = () => {
|
||||||
|
loadKey.current += 1;
|
||||||
|
setList(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtWebRouteContainer label="Disk images">
|
||||||
|
<AsyncWidget
|
||||||
|
loadKey={loadKey.current}
|
||||||
|
errMsg="Failed to load disk images list!"
|
||||||
|
load={load}
|
||||||
|
ready={list !== undefined}
|
||||||
|
build={() => (
|
||||||
|
<VirtWebRouteContainer
|
||||||
|
label="Disk images management"
|
||||||
|
actions={
|
||||||
|
<span>
|
||||||
|
<Tooltip title="Refresh Disk images list">
|
||||||
|
<IconButton onClick={reload}>
|
||||||
|
<RefreshIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<UploadDiskImageCard onFileUploaded={reload} />
|
||||||
|
<DiskImageList list={list!} onReload={reload} />
|
||||||
|
</VirtWebRouteContainer>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</VirtWebRouteContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UploadDiskImageCard(p: {
|
||||||
|
onFileUploaded: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const alert = useAlert();
|
||||||
|
const snackbar = useSnackbar();
|
||||||
|
|
||||||
|
const [value, setValue] = React.useState<File | null>(null);
|
||||||
|
const [uploadProgress, setUploadProgress] = React.useState<number | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = (newValue: File | null) => {
|
||||||
|
if (
|
||||||
|
newValue &&
|
||||||
|
newValue.size > ServerApi.Config.constraints.disk_image_max_size
|
||||||
|
) {
|
||||||
|
alert(
|
||||||
|
`The file is too big (max size allowed: ${filesize(
|
||||||
|
ServerApi.Config.constraints.disk_image_max_size
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
newValue &&
|
||||||
|
!ServerApi.Config.disk_images_mimetypes.includes(newValue.type)
|
||||||
|
) {
|
||||||
|
alert(`Selected file mimetype is not allowed! (${newValue.type})`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const upload = async () => {
|
||||||
|
try {
|
||||||
|
setUploadProgress(0);
|
||||||
|
await DiskImageApi.Upload(value!, setUploadProgress);
|
||||||
|
|
||||||
|
setValue(null);
|
||||||
|
snackbar("The file was successfully uploaded!");
|
||||||
|
|
||||||
|
p.onFileUploaded();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
await alert(`Failed to perform file upload! ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadProgress(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (uploadProgress !== null) {
|
||||||
|
return (
|
||||||
|
<VirtWebPaper label="File upload" noHorizontalMargin>
|
||||||
|
<Typography variant="body1">
|
||||||
|
Upload in progress ({Math.floor(uploadProgress * 100)}%)...
|
||||||
|
</Typography>
|
||||||
|
<LinearProgress variant="determinate" value={uploadProgress * 100} />
|
||||||
|
</VirtWebPaper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtWebPaper label="Disk image upload" noHorizontalMargin>
|
||||||
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<FileInput
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
accept: ServerApi.Config.disk_images_mimetypes.join(","),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{value && <Button onClick={upload}>Upload</Button>}
|
||||||
|
</div>
|
||||||
|
</VirtWebPaper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DiskImageList(p: {
|
||||||
|
list: DiskImage[];
|
||||||
|
onReload: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
return <>todo</>;
|
||||||
|
}
|
@ -3,7 +3,15 @@ import { RouterLink } from "../widgets/RouterLink";
|
|||||||
|
|
||||||
export function NotFoundRoute(): React.ReactElement {
|
export function NotFoundRoute(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<div style={{ textAlign: "center" }}>
|
<div
|
||||||
|
style={{
|
||||||
|
textAlign: "center",
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<h1>404 Not found</h1>
|
<h1>404 Not found</h1>
|
||||||
<p>The page you requested was not found!</p>
|
<p>The page you requested was not found!</p>
|
||||||
<RouterLink to="/">
|
<RouterLink to="/">
|
||||||
|
@ -2,6 +2,7 @@ import {
|
|||||||
mdiApi,
|
mdiApi,
|
||||||
mdiBoxShadow,
|
mdiBoxShadow,
|
||||||
mdiDisc,
|
mdiDisc,
|
||||||
|
mdiHarddisk,
|
||||||
mdiHome,
|
mdiHome,
|
||||||
mdiInformation,
|
mdiInformation,
|
||||||
mdiLan,
|
mdiLan,
|
||||||
@ -66,6 +67,11 @@ export function BaseAuthenticatedPage(): React.ReactElement {
|
|||||||
uri="/nwfilter"
|
uri="/nwfilter"
|
||||||
icon={<Icon path={mdiSecurityNetwork} size={1} />}
|
icon={<Icon path={mdiSecurityNetwork} size={1} />}
|
||||||
/>
|
/>
|
||||||
|
<NavLink
|
||||||
|
label="Disk images"
|
||||||
|
uri="/disk_images"
|
||||||
|
icon={<Icon path={mdiHarddisk} size={1} />}
|
||||||
|
/>
|
||||||
<NavLink
|
<NavLink
|
||||||
label="ISO files"
|
label="ISO files"
|
||||||
uri="/iso"
|
uri="/iso"
|
||||||
|
@ -35,7 +35,7 @@ export function FileInput(
|
|||||||
<InputAdornment position="start">
|
<InputAdornment position="start">
|
||||||
<AttachFileIcon />
|
<AttachFileIcon />
|
||||||
|
|
||||||
{p.value ? p.value.name : "Insert a file"}
|
{p.value ? p.value.name : "Select a file"}
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user