mirror of
https://github.com/azukaar/Cosmos-Server.git
synced 2026-01-04 19:30:41 -06:00
[release] v0.15.0-unstable21
This commit is contained in:
@@ -6,9 +6,9 @@ import { Alert, Input, Stack, useMediaQuery, useTheme } from '@mui/material';
|
||||
import { ApiOutlined, SendOutlined } from '@ant-design/icons';
|
||||
import ResponsiveButton from '../../../components/responseiveButton';
|
||||
|
||||
import { Terminal, ITerminalOptions, ITerminalAddon } from 'xterm'
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
import 'xterm/css/xterm.css'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
|
||||
const DockerTerminal = ({containerInfo, refresh}) => {
|
||||
const { Name, Config, NetworkSettings, State } = containerInfo;
|
||||
@@ -76,14 +76,14 @@ const DockerTerminal = ({containerInfo, refresh}) => {
|
||||
setIsConnected(false);
|
||||
let terminalBoldRed = '\x1b[1;31m';
|
||||
let terminalReset = '\x1b[0m';
|
||||
terminal.write(terminalBoldRed + 'Disconnected from ' + (newProc ? 'bash' : 'main process TTY') + '\r\n' + terminalReset);
|
||||
terminal.write(terminalBoldRed + 'Disconnected from ' + (newProc ? 'shell' : 'main process TTY') + '\r\n' + terminalReset);
|
||||
};
|
||||
|
||||
ws.current.onopen = () => {
|
||||
setIsConnected(true);
|
||||
let terminalBoldGreen = '\x1b[1;32m';
|
||||
let terminalReset = '\x1b[0m';
|
||||
terminal.write(terminalBoldGreen + 'Connected to ' + (newProc ? 'bash' : 'main process TTY') + '\r\n' + terminalReset);
|
||||
terminal.write(terminalBoldGreen + 'Connected to ' + (newProc ? 'shell' : 'main process TTY') + '\r\n' + terminalReset);
|
||||
// focus terminal
|
||||
terminal.focus();
|
||||
};
|
||||
@@ -97,44 +97,20 @@ const DockerTerminal = ({containerInfo, refresh}) => {
|
||||
const [SelectedText, setSelectedText] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
xtermRef.current.innerHTML = '';
|
||||
// xtermRef.current.innerHTML = '';
|
||||
terminal.open(xtermRef.current);
|
||||
|
||||
// const fitAddon = new FitAddon();
|
||||
// terminal.loadAddon(fitAddon);
|
||||
// fitAddon.fit();
|
||||
|
||||
if (navigator.clipboard) {
|
||||
navigator.permissions.query({ name: "clipboard-read" })
|
||||
navigator.permissions.query({ name: "clipboard-write" })
|
||||
} else {
|
||||
console.error('Clipboard API not available');
|
||||
return;
|
||||
}
|
||||
// const onFocus = () => {
|
||||
// terminal.focus();
|
||||
// }
|
||||
|
||||
// xtermRef.current.removeEventListener('touchstart', onFocus);
|
||||
|
||||
// remove all listener from xtermRef
|
||||
const contextMenuListener = async (e) => {
|
||||
e.preventDefault();
|
||||
console.log('context menu', SelectedText)
|
||||
if(SelectedText && SelectedText.length > 0 && SelectedText != "<empty string>") {
|
||||
await navigator.clipboard.writeText(SelectedText);
|
||||
} else {
|
||||
const toWrite = await navigator.clipboard.readText();
|
||||
ws.current.send(toWrite);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const onFocus = () => {
|
||||
terminal.focus();
|
||||
}
|
||||
|
||||
xtermRef.current.removeEventListener('contextmenu', contextMenuListener);
|
||||
xtermRef.current.removeEventListener('touchstart', onFocus);
|
||||
|
||||
xtermRef.current.addEventListener('contextmenu', contextMenuListener);
|
||||
xtermRef.current.addEventListener('touchstart', onFocus);
|
||||
// xtermRef.current.addEventListener('touchstart', onFocus);
|
||||
|
||||
terminal.onSelectionChange((e) => {
|
||||
let sel = terminal.getSelection();
|
||||
@@ -144,6 +120,12 @@ const DockerTerminal = ({containerInfo, refresh}) => {
|
||||
}
|
||||
});
|
||||
|
||||
terminal.onData((data) => {
|
||||
if (data.startsWith("\x1b[200~") && data.endsWith("\x1b[201~")) {
|
||||
ws.current.send(data);
|
||||
}
|
||||
});
|
||||
|
||||
terminal.attachCustomKeyEventHandler((e) => {
|
||||
const codes = {
|
||||
'Enter': '\r',
|
||||
@@ -193,13 +175,16 @@ const DockerTerminal = ({containerInfo, refresh}) => {
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="terminal-container">
|
||||
<div className="terminal-container" style={{
|
||||
background: '#000',
|
||||
width: '100%',
|
||||
maxWidth: '900px',
|
||||
position:'relative'
|
||||
}}>
|
||||
{(!isInteractive) && (
|
||||
<Alert severity="warning">
|
||||
This container is not interactive.
|
||||
@@ -207,25 +192,25 @@ const DockerTerminal = ({containerInfo, refresh}) => {
|
||||
<Button onClick={() => makeInteractive()}>Enable TTY</Button>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div style={{width: '100%', maxWidth: '750px', overflowX: 'auto', overflowY: 'hidden', background: '#000'}}>
|
||||
<div style={{
|
||||
overflowX: 'auto',
|
||||
width: '100%',
|
||||
maxWidth: '900px',
|
||||
}}>
|
||||
<div ref={xtermRef}></div>
|
||||
<br/>
|
||||
|
||||
<Stack
|
||||
direction="column"
|
||||
spacing={1}
|
||||
>
|
||||
</div>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
alignItems="center"
|
||||
sx={{
|
||||
background: '#272d36',
|
||||
color: '#fff',
|
||||
padding: '10px',
|
||||
width: '100%',
|
||||
}}
|
||||
alignItems="center"
|
||||
>
|
||||
<div>{
|
||||
isConnected ? (
|
||||
@@ -248,9 +233,6 @@ const DockerTerminal = ({containerInfo, refresh}) => {
|
||||
</>
|
||||
}
|
||||
</Stack>
|
||||
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
|
||||
import * as API from "../../api";
|
||||
import PrettyTableView from "../../components/tableView/prettyTableView";
|
||||
import { DeleteButton } from "../../components/delete";
|
||||
import { CloudOutlined, CompassOutlined, DesktopOutlined, ExpandOutlined, LaptopOutlined, MenuFoldOutlined, MenuOutlined, MinusCircleFilled, MobileOutlined, NodeCollapseOutlined, PlusCircleFilled, PlusCircleOutlined, SettingFilled, TabletOutlined, WarningFilled, WarningOutlined } from "@ant-design/icons";
|
||||
import { CloudOutlined, CompassOutlined, DesktopOutlined, ExpandOutlined, LaptopOutlined, MenuFoldOutlined, MenuOutlined, MinusCircleFilled, MobileOutlined, NodeCollapseOutlined, PlusCircleFilled, PlusCircleOutlined, ReloadOutlined, SettingFilled, TabletOutlined, WarningFilled, WarningOutlined } from "@ant-design/icons";
|
||||
import { Alert, Button, CircularProgress, InputLabel, LinearProgress, ListItemIcon, ListItemText, MenuItem, Stack, Tooltip } from "@mui/material";
|
||||
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText } from "../config/users/formShortcuts";
|
||||
import MainCard from "../../components/MainCard";
|
||||
@@ -21,10 +21,11 @@ import lockIcon from '../../assets/images/icons/lock.svg';
|
||||
import raidIcon from '../../assets/images/icons/database.svg';
|
||||
import { simplifyNumber } from "../dashboard/components/utils";
|
||||
import LogsInModal from "../../components/logsInModal";
|
||||
import MountDialog from "./mountDialog";
|
||||
import MountDiskDialog from "./mountDiskDialog";
|
||||
import PasswordModal from "../../components/passwordModal";
|
||||
import FormatModal from "./FormatModal";
|
||||
import MenuButton from "../../components/MenuButton";
|
||||
import ResponsiveButton from "../../components/responseiveButton";
|
||||
|
||||
const diskStyle = {
|
||||
width: "100%",
|
||||
@@ -173,14 +174,14 @@ const Disk = ({disk, refresh}) => {
|
||||
<Stack spacing={2} direction="column" justifyContent={"center"}>
|
||||
{(disk.type == "disk" || disk.type == "part") ? <FormatButton disk={disk} refresh={refresh}/> : ""}
|
||||
|
||||
{disk.mountpoint ? <MountDialog disk={disk} unmount={true} refresh={refresh} /> : ""}
|
||||
{disk.mountpoint ? <MountDiskDialog disk={disk} unmount={true} refresh={refresh} /> : ""}
|
||||
|
||||
{(
|
||||
(disk.type == "part" || (disk.type == "disk" && (!disk.children || !disk.children.length))) &&
|
||||
disk.fstype &&
|
||||
disk.fstype !== "swap" &&
|
||||
!disk.mountpoint
|
||||
) ? <MountDialog disk={disk} refresh={refresh} /> : ""}
|
||||
) ? <MountDiskDialog disk={disk} refresh={refresh} /> : ""}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -218,7 +219,9 @@ export const StorageDisks = () => {
|
||||
<Stack spacing={2} style={{maxWidth: "1000px"}}>
|
||||
{containerized && <Alert severity="warning">You are running Cosmos inside a Docker container. As such, it will only have limited access to your disks and their informations.</Alert>}
|
||||
<div>
|
||||
<Button variant="contained" color="primary" onClick={refresh}>Refresh</Button>
|
||||
<ResponsiveButton variant="outlined" startIcon={<ReloadOutlined />} onClick={() => {
|
||||
refresh();
|
||||
}}>Refresh</ResponsiveButton>
|
||||
</div>
|
||||
<div>
|
||||
<TreeView
|
||||
|
||||
@@ -7,15 +7,17 @@ import * as yup from "yup";
|
||||
|
||||
import * as API from '../../api';
|
||||
import { MountPicker } from "./mountPicker";
|
||||
import ResponsiveButton from "../../components/responseiveButton";
|
||||
import { PlusCircleOutlined } from "@ant-design/icons";
|
||||
|
||||
const MergerDialogInternal = ({ refresh, open, setOpen }) => {
|
||||
const MergerDialogInternal = ({ refresh, open, setOpen, data }) => {
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
path: '/mnt/storage',
|
||||
permanent: false,
|
||||
path: data ? data.path : '/mnt/storage',
|
||||
permanent: data ? data.permanent : false,
|
||||
branches: [],
|
||||
chown: '1000:1000',
|
||||
opts: '',
|
||||
chown: data ? data.chown : '1000:1000',
|
||||
opts: data ? data.opts.join(',') : '',
|
||||
},
|
||||
validateOnChange: false,
|
||||
validationSchema: yup.object({
|
||||
@@ -128,12 +130,14 @@ const MergerDialog = ({ refresh }) => {
|
||||
return <>
|
||||
{open && <MergerDialogInternal refresh={refresh} open={open} setOpen={setOpen}/>}
|
||||
|
||||
<Button
|
||||
<ResponsiveButton
|
||||
onClick={() => {setOpen(true);}}
|
||||
variant="outlined"
|
||||
variant="contained"
|
||||
startIcon={<PlusCircleOutlined />}
|
||||
size="small"
|
||||
>Create Merge</Button>
|
||||
>Create Merge</ResponsiveButton>
|
||||
</>
|
||||
}
|
||||
|
||||
export default MergerDialog;
|
||||
export default MergerDialog;
|
||||
export { MergerDialogInternal };
|
||||
@@ -3,8 +3,8 @@ import { useEffect, useState } from "react";
|
||||
import * as API from "../../api";
|
||||
import PrettyTableView from "../../components/tableView/prettyTableView";
|
||||
import { DeleteButton } from "../../components/delete";
|
||||
import { CloudOutlined, CloudServerOutlined, CompassOutlined, DesktopOutlined, FolderOutlined, LaptopOutlined, MobileOutlined, TabletOutlined } from "@ant-design/icons";
|
||||
import { Alert, Button, CircularProgress, InputLabel, Stack } from "@mui/material";
|
||||
import { CloudOutlined, CloudServerOutlined, CompassOutlined, DeleteOutlined, DesktopOutlined, EditOutlined, FolderOutlined, LaptopOutlined, MobileOutlined, ReloadOutlined, TabletOutlined } from "@ant-design/icons";
|
||||
import { Alert, Button, CircularProgress, InputLabel, ListItemIcon, ListItemText, MenuItem, Stack } from "@mui/material";
|
||||
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText } from "../config/users/formShortcuts";
|
||||
import MainCard from "../../components/MainCard";
|
||||
import { Formik } from "formik";
|
||||
@@ -13,19 +13,25 @@ import ApiModal from "../../components/apiModal";
|
||||
import ConfirmModal from "../../components/confirmModal";
|
||||
import { isDomain } from "../../utils/indexs";
|
||||
import UploadButtons from "../../components/fileUpload";
|
||||
import MergerDialog from "./mergerDialog";
|
||||
import MergerDialog, { MergerDialogInternal } from "./mergerDialog";
|
||||
import ResponsiveButton from "../../components/responseiveButton";
|
||||
import MenuButton from "../../components/MenuButton";
|
||||
|
||||
export const StorageMerges = () => {
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [config, setConfig] = useState(null);
|
||||
const [mounts, setMounts] = useState([]);
|
||||
const [mergeDialog, setMergeDialog] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const refresh = async () => {
|
||||
setLoading(true);
|
||||
let mountsData = await API.storage.mounts.list();
|
||||
let configAsync = await API.config.get();
|
||||
setConfig(configAsync.data);
|
||||
setIsAdmin(configAsync.isAdmin);
|
||||
setMounts(mountsData.data);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -34,15 +40,18 @@ export const StorageMerges = () => {
|
||||
|
||||
return <>
|
||||
{(config) ? <>
|
||||
<Stack spacing={2} style={{maxWidth: "1000px"}}>
|
||||
{mergeDialog && <MergerDialogInternal data={mergeDialog.data} refresh={refresh} unmount={mergeDialog.unmount} open={mergeDialog} setOpen={setMergeDialog} />}
|
||||
<Stack spacing={2}>
|
||||
<div>
|
||||
<MergerDialog disk={{name: '/dev/sda'}} refresh={refresh}/>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<PrettyTableView
|
||||
data={mounts.filter((mount) => mount.type === 'fuse.mergerfs')}
|
||||
getKey={(r) => `${r.device} - ${refresh.path}`}
|
||||
buttons={[
|
||||
<MergerDialog disk={{name: '/dev/sda'}} refresh={refresh}/>,
|
||||
<ResponsiveButton variant="outlined" startIcon={<ReloadOutlined />} onClick={() => {
|
||||
refresh();
|
||||
}}>Refresh</ResponsiveButton>
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
title: 'Device',
|
||||
@@ -58,7 +67,20 @@ export const StorageMerges = () => {
|
||||
},
|
||||
{
|
||||
title: 'Options',
|
||||
screenMin: 'md',
|
||||
field: (r) => JSON.stringify(r.opts),
|
||||
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
clickable:true,
|
||||
field: (r) => <>
|
||||
<ResponsiveButton color={'error'} variant="outlined" startIcon={<DeleteOutlined />} onClick={() => {
|
||||
API.storage.mounts.unmount({ mountPoint: r.path, permanent: true }).then(() => {
|
||||
refresh();
|
||||
});
|
||||
}}>Delete</ResponsiveButton>
|
||||
</>
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -7,25 +7,37 @@ import * as yup from "yup";
|
||||
|
||||
import * as API from '../../api';
|
||||
|
||||
const MountDialogInternal = ({disk, unmount, refresh, open, setOpen }) => {
|
||||
const MountDialogInternal = ({ unmount, refresh, open, setOpen, data }) => {
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
path: '/mnt/' + disk.name.replace('/dev/', ''),
|
||||
device: data ? data.device : '',
|
||||
path: data ? data.path : '',
|
||||
permanent: false,
|
||||
chown: '1000:1000',
|
||||
},
|
||||
validationSchema: yup.object({
|
||||
// should start with /mnt/ or /var/mnt
|
||||
device: yup.string().required('Required'),
|
||||
path: yup.string().required('Required').matches(/^\/(mnt|var\/mnt)\/.{1,}$/, 'Path should start with /mnt/ or /var/mnt'),
|
||||
}),
|
||||
onSubmit: (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
onSubmit: async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
setSubmitting(true);
|
||||
if(data && !unmount) {
|
||||
try {
|
||||
await API.storage.mounts.unmount({
|
||||
mountPoint: data.path,
|
||||
permanent: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
return (unmount ? API.storage.mounts.unmount({
|
||||
mountPoint: disk.mountpoint,
|
||||
mountPoint: values.path,
|
||||
chown: values.chown,
|
||||
permanent: values.permanent,
|
||||
}) : API.storage.mounts.mount({
|
||||
path: disk.name,
|
||||
path: values.device,
|
||||
mountPoint: values.path,
|
||||
chown: values.chown,
|
||||
permanent: values.permanent,
|
||||
@@ -52,11 +64,21 @@ const MountDialogInternal = ({disk, unmount, refresh, open, setOpen }) => {
|
||||
<Stack spacing={2} style={{ marginTop: '10px', width: '500px', maxWidth: '100%' }}>
|
||||
<div>
|
||||
<Alert severity="info">
|
||||
You are about to {unmount ? 'unmount' : 'mount'} the disk <strong>{disk.name}</strong>{disk.mountpoint && (<> mounted at <strong>{disk.mountpoint}</strong></>)}. This will make the content {unmount ? 'unavailable' : 'available'} to be viewed in the file explorer.
|
||||
You are about to {unmount ? 'unmount' : 'mount'} a folder {data && data.mountpoint && (<> mounted at <strong>{data && data.mountpoint}</strong></>)}. This will make the content {unmount ? 'unavailable' : 'available'} to be viewed in the file explorer.
|
||||
Permanent {unmount ? 'unmount' : 'mount'} will persist after reboot.
|
||||
</Alert>
|
||||
</div>
|
||||
{unmount ? '' : <>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="device"
|
||||
name="device"
|
||||
label="What to mount"
|
||||
value={formik.values.device}
|
||||
onChange={formik.handleChange}
|
||||
error={formik.touched.device && Boolean(formik.errors.device)}
|
||||
helperText={formik.touched.device && formik.errors.device}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="path"
|
||||
@@ -120,4 +142,5 @@ const MountDialog = ({ disk, unmount, refresh }) => {
|
||||
}
|
||||
|
||||
|
||||
export default MountDialog;
|
||||
export default MountDialog;
|
||||
export { MountDialogInternal };
|
||||
|
||||
123
client/src/pages/storage/mountDiskDialog.jsx
Normal file
123
client/src/pages/storage/mountDiskDialog.jsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import { Alert, Grid, Button, Checkbox, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormHelperText, Stack, TextField } from "@mui/material";
|
||||
import React, {useState} from "react";
|
||||
import { CosmosCheckbox } from "../config/users/formShortcuts";
|
||||
import { Formik, FormikProvider, useFormik } from "formik";
|
||||
import * as yup from "yup";
|
||||
|
||||
import * as API from '../../api';
|
||||
|
||||
const MountDiskDialogInternal = ({disk, unmount, refresh, open, setOpen }) => {
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
path: '/mnt/' + disk.name.replace('/dev/', ''),
|
||||
permanent: false,
|
||||
chown: '1000:1000',
|
||||
},
|
||||
validationSchema: yup.object({
|
||||
// should start with /mnt/ or /var/mnt
|
||||
path: yup.string().required('Required').matches(/^\/(mnt|var\/mnt)\/.{1,}$/, 'Path should start with /mnt/ or /var/mnt'),
|
||||
}),
|
||||
onSubmit: (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
setSubmitting(true);
|
||||
return (unmount ? API.storage.mounts.unmount({
|
||||
mountPoint: disk.mountpoint,
|
||||
chown: values.chown,
|
||||
permanent: values.permanent,
|
||||
}) : API.storage.mounts.mount({
|
||||
path: disk.name,
|
||||
mountPoint: values.path,
|
||||
chown: values.chown,
|
||||
permanent: values.permanent,
|
||||
})).then((res) => {
|
||||
setStatus({ success: true });
|
||||
setSubmitting(false);
|
||||
setOpen(false);
|
||||
refresh && refresh();
|
||||
}).catch((err) => {
|
||||
setStatus({ success: false });
|
||||
setErrors({ submit: err.message });
|
||||
setSubmitting(false);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return <>
|
||||
<Dialog open={open} onClose={() => setOpen(false)}>
|
||||
<FormikProvider value={formik}>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<DialogTitle>{unmount ? 'Unmount' : 'Mount'} Disk</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<Stack spacing={2} style={{ marginTop: '10px', width: '500px', maxWidth: '100%' }}>
|
||||
<div>
|
||||
<Alert severity="info">
|
||||
You are about to {unmount ? 'unmount' : 'mount'} the disk <strong>{disk.name}</strong>{disk.mountpoint && (<> mounted at <strong>{disk.mountpoint}</strong></>)}. This will make the content {unmount ? 'unavailable' : 'available'} to be viewed in the file explorer.
|
||||
Permanent {unmount ? 'unmount' : 'mount'} will persist after reboot.
|
||||
</Alert>
|
||||
</div>
|
||||
{unmount ? '' : <>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="path"
|
||||
name="path"
|
||||
label="Path to mount to"
|
||||
value={formik.values.path}
|
||||
onChange={formik.handleChange}
|
||||
error={formik.touched.path && Boolean(formik.errors.path)}
|
||||
helperText={formik.touched.path && formik.errors.path}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="chown"
|
||||
name="chown"
|
||||
label="Change mount folder owner (optional, ex. 1000:1000)"
|
||||
value={formik.values.chown}
|
||||
onChange={formik.handleChange}
|
||||
error={formik.touched.chown && Boolean(formik.errors.chown)}
|
||||
helperText={formik.touched.chown && formik.errors.chown}
|
||||
/>
|
||||
</>}
|
||||
<div>
|
||||
<Checkbox
|
||||
name="permanent"
|
||||
checked={formik.values.permanent}
|
||||
onChange={formik.handleChange}
|
||||
/> Permanent {unmount ? 'Unmount' : 'Mount'}
|
||||
</div>
|
||||
{formik.errors.submit && (
|
||||
<Grid item xs={12}>
|
||||
<FormHelperText error>{formik.errors.submit}</FormHelperText>
|
||||
</Grid>
|
||||
)}
|
||||
</Stack>
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpen(false)}>Cancel</Button>
|
||||
<LoadingButton color="primary" variant="contained" type="submit" onClick={() => {
|
||||
formik.handleSubmit();
|
||||
}}>{unmount ? 'Unmount' : 'Mount'}</LoadingButton>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</FormikProvider>
|
||||
</Dialog>
|
||||
</>
|
||||
}
|
||||
|
||||
const MountDiskDialog = ({ disk, unmount, refresh }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return <>
|
||||
{open && <MountDiskDialogInternal disk={disk} unmount={unmount} refresh={refresh} open={open} setOpen={setOpen}/>}
|
||||
|
||||
<Button
|
||||
onClick={() => {setOpen(true);}}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>{unmount ? 'Unmount' : 'Mount'}</Button>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
export default MountDiskDialog;
|
||||
@@ -3,30 +3,36 @@ import { useEffect, useState } from "react";
|
||||
import * as API from "../../api";
|
||||
import PrettyTableView from "../../components/tableView/prettyTableView";
|
||||
import { DeleteButton } from "../../components/delete";
|
||||
import { CloudOutlined, CloudServerOutlined, CompassOutlined, DeleteOutlined, DesktopOutlined, EditOutlined, FolderOutlined, LaptopOutlined, MobileOutlined, TabletOutlined } from "@ant-design/icons";
|
||||
import { CloudOutlined, CloudServerOutlined, CompassOutlined, DeleteOutlined, DesktopOutlined, EditOutlined, FolderOutlined, LaptopOutlined, MobileOutlined, PlusCircleOutlined, ReloadOutlined, TabletOutlined } from "@ant-design/icons";
|
||||
import { Alert, Button, CircularProgress, InputLabel, ListItemIcon, ListItemText, MenuItem, Stack } from "@mui/material";
|
||||
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText } from "../config/users/formShortcuts";
|
||||
import MainCard from "../../components/MainCard";
|
||||
import { Formik } from "formik";
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import ApiModal from "../../components/apiModal";
|
||||
import ConfirmModal from "../../components/confirmModal";
|
||||
import ConfirmModal, { ConfirmModalDirect } from "../../components/confirmModal";
|
||||
import { isDomain } from "../../utils/indexs";
|
||||
import UploadButtons from "../../components/fileUpload";
|
||||
import SnapRAIDDialog from "./snapRaidDialog";
|
||||
import MenuButton from "../../components/MenuButton";
|
||||
import MountDialog, { MountDialogInternal } from "./mountDialog";
|
||||
import ResponsiveButton from "../../components/responseiveButton";
|
||||
|
||||
export const StorageMounts = () => {
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [config, setConfig] = useState(null);
|
||||
const [mounts, setMounts] = useState([]);
|
||||
const [mountDialog, setMountDialog] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const refresh = async () => {
|
||||
setLoading(true);
|
||||
let mountsData = await API.storage.mounts.list();
|
||||
let configAsync = await API.config.get();
|
||||
setConfig(configAsync.data);
|
||||
setIsAdmin(configAsync.isAdmin);
|
||||
setMounts(mountsData.data);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -34,10 +40,17 @@ export const StorageMounts = () => {
|
||||
}, []);
|
||||
|
||||
return <>
|
||||
{mountDialog && <MountDialogInternal data={mountDialog.data} refresh={refresh} unmount={mountDialog.unmount} open={mountDialog} setOpen={setMountDialog} />}
|
||||
{(config) ? <>
|
||||
<PrettyTableView
|
||||
data={mounts}
|
||||
getKey={(r) => `${r.device} - ${refresh.path}`}
|
||||
buttons={[
|
||||
<ResponsiveButton startIcon={<PlusCircleOutlined />} variant="contained" onClick={() => setMountDialog({data: null, unmount: false})}>New Mount</ResponsiveButton>,
|
||||
<ResponsiveButton variant="outlined" startIcon={<ReloadOutlined />} onClick={() => {
|
||||
refresh();
|
||||
}}>Refresh</ResponsiveButton>
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
title: 'Device',
|
||||
@@ -60,17 +73,17 @@ export const StorageMounts = () => {
|
||||
field: (r) => <>
|
||||
<div style={{position: 'relative'}}>
|
||||
<MenuButton>
|
||||
<MenuItem>
|
||||
<MenuItem disabled={!r.device.startsWith('/dev/') || loading} onClick={() => setMountDialog({data: r, unmount: false})}>
|
||||
<ListItemIcon>
|
||||
<EditOutlined fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText disabled={false} onClick={() => tryDeleteRaid(r.Name)}>Edit</ListItemText>
|
||||
<ListItemText >Edit</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<MenuItem disabled={loading} onClick={() => setMountDialog({data: r, unmount: true})}>
|
||||
<ListItemIcon>
|
||||
<DeleteOutlined fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText disabled={false} onClick={() => tryDeleteRaid(r.Name)}>Delete</ListItemText>
|
||||
<ListItemText >unmount</ListItemText>
|
||||
</MenuItem>
|
||||
</MenuButton>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ import ApiModal from "../../components/apiModal";
|
||||
import ConfirmModal, { ConfirmModalDirect } from "../../components/confirmModal";
|
||||
import { crontabToText, isDomain } from "../../utils/indexs";
|
||||
import UploadButtons from "../../components/fileUpload";
|
||||
import SnapRAIDDialog from "./snapRaidDialog";
|
||||
import SnapRAIDDialog, { SnapRAIDDialogInternal } from "./snapRaidDialog";
|
||||
import MenuButton from "../../components/MenuButton";
|
||||
import diskIcon from '../../assets/images/icons/disk.svg';
|
||||
import ResponsiveButton from "../../components/responseiveButton";
|
||||
@@ -61,13 +61,16 @@ export const Parity = () => {
|
||||
const [parities, setParities] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [deleteRaid, setDeleteRaid] = useState(null);
|
||||
const [editOpened, setEditOpened] = useState(null);
|
||||
|
||||
const refresh = async () => {
|
||||
setLoading(true);
|
||||
let paritiesData = await API.storage.snapRAID.list();
|
||||
let configAsync = await API.config.get();
|
||||
setConfig(configAsync.data);
|
||||
setIsAdmin(configAsync.isAdmin);
|
||||
setParities(paritiesData.data);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const apiDeleteRaid = async (name) => {
|
||||
@@ -126,8 +129,8 @@ export const Parity = () => {
|
||||
refresh();
|
||||
}}>Refresh</ResponsiveButton>
|
||||
</Stack>
|
||||
|
||||
<div>
|
||||
{editOpened && <SnapRAIDDialogInternal refresh={refresh} open={editOpened} setOpen={setEditOpened} data={editOpened} />}
|
||||
{parities && <PrettyTableView
|
||||
data={parities}
|
||||
getKey={(r) => r.Name}
|
||||
@@ -195,37 +198,35 @@ export const Parity = () => {
|
||||
field: (r) => {
|
||||
return <div style={{position: 'relative'}}>
|
||||
<MenuButton>
|
||||
<MenuItem>
|
||||
<MenuItem disabled={loading} onClick={() => setEditOpened(r)}>
|
||||
<ListItemIcon>
|
||||
<EditOutlined fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText disabled={loading}>
|
||||
<SnapRAIDDialog refresh={refresh} data={r} />
|
||||
</ListItemText>
|
||||
<ListItemText>Edit</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<MenuItem disabled={loading} onClick={() => sync(r.Name)}>
|
||||
<ListItemIcon>
|
||||
<CloudOutlined fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText disabled={loading} onClick={() => sync(r.Name)}>Sync</ListItemText>
|
||||
<ListItemText>Sync</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<MenuItem disabled={loading} onClick={() => scrub(r.Name)}>
|
||||
<ListItemIcon>
|
||||
<CompassOutlined fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText disabled={loading} onClick={() => scrub(r.Name)}>Scrub</ListItemText>
|
||||
<ListItemText>Scrub</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<MenuItem disabled={loading} onClick={() => fix(r.Name)}>
|
||||
<ListItemIcon>
|
||||
<CloudOutlined fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText disabled={loading} onClick={() => fix(r.Name)}>Fix</ListItemText>
|
||||
<ListItemText>Fix</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<ListItemIcon>
|
||||
<ListItemIcon disabled={loading} onClick={() => tryDeleteRaid(r.Name)}>
|
||||
<DeleteOutlined fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText disabled={loading} onClick={() => tryDeleteRaid(r.Name)}>Delete</ListItemText>
|
||||
<ListItemText>Delete</ListItemText>
|
||||
</MenuItem>
|
||||
</MenuButton>
|
||||
</div>
|
||||
|
||||
@@ -207,4 +207,5 @@ const SnapRAIDDialog = ({ refresh, data }) => {
|
||||
</>
|
||||
}
|
||||
|
||||
export default SnapRAIDDialog;
|
||||
export default SnapRAIDDialog;
|
||||
export { SnapRAIDDialog, SnapRAIDDialogInternal };
|
||||
19
package-lock.json
generated
19
package-lock.json
generated
@@ -22,6 +22,8 @@
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"@xterm/addon-fit": "^0.9.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"apexcharts": "^3.35.5",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"browserslist": "^4.21.7",
|
||||
@@ -69,7 +71,6 @@
|
||||
"web-vitals": "^3.0.2",
|
||||
"whiskers": "^0.4.0",
|
||||
"whiskers.js": "^1.0.0",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
@@ -4416,6 +4417,19 @@
|
||||
"vite": "^4.1.0-beta.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/addon-fit": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.9.0.tgz",
|
||||
"integrity": "sha512-hDlPPbTVPYyvwXu/asW8HbJkI/2RMi0cMaJnBZYVeJB0SWP2NeESMCNr+I7CvBlyI0sAxpxOg8Wk4OMkxBz9WA==",
|
||||
"peerDependencies": {
|
||||
"@xterm/xterm": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/xterm": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.4.0.tgz",
|
||||
"integrity": "sha512-GlyzcZZ7LJjhFevthHtikhiDIl8lnTSgol6eTM4aoSNLcuXu3OEhnbqdCVIjtIil3jjabf3gDtb1S8FGahsuEw=="
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -10945,7 +10959,8 @@
|
||||
"node_modules/xterm": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
|
||||
"integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg=="
|
||||
"integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/xterm-addon-fit": {
|
||||
"version": "0.8.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.15.0-unstable19",
|
||||
"version": "0.15.0-unstable21",
|
||||
"description": "",
|
||||
"main": "test-server.js",
|
||||
"bugs": {
|
||||
@@ -22,6 +22,8 @@
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"@xterm/addon-fit": "^0.9.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"apexcharts": "^3.35.5",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"browserslist": "^4.21.7",
|
||||
@@ -69,7 +71,6 @@
|
||||
"web-vitals": "^3.0.2",
|
||||
"whiskers": "^0.4.0",
|
||||
"whiskers.js": "^1.0.0",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
|
||||
21
src/CRON.go
21
src/CRON.go
@@ -18,9 +18,7 @@ type Version struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func checkVersion() {
|
||||
utils.NewVersionAvailable = false
|
||||
|
||||
func GetCosmosVersion() string {
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -30,7 +28,7 @@ func checkVersion() {
|
||||
pjs, errPR := os.Open(exPath + "/meta.json")
|
||||
if errPR != nil {
|
||||
utils.Error("checkVersion", errPR)
|
||||
return
|
||||
return ""
|
||||
}
|
||||
|
||||
packageJson, _ := ioutil.ReadAll(pjs)
|
||||
@@ -41,10 +39,21 @@ func checkVersion() {
|
||||
errJ := json.Unmarshal(packageJson, &version)
|
||||
if errJ != nil {
|
||||
utils.Error("checkVersion", errJ)
|
||||
return
|
||||
return ""
|
||||
}
|
||||
|
||||
myVersion := version.Version
|
||||
return version.Version
|
||||
|
||||
}
|
||||
|
||||
func checkVersion() {
|
||||
utils.NewVersionAvailable = false
|
||||
|
||||
myVersion := GetCosmosVersion()
|
||||
if myVersion == "" {
|
||||
utils.Error("checkVersion - Could not get version", nil)
|
||||
return
|
||||
}
|
||||
|
||||
response, err := http.Get("https://cosmos-cloud.io/versions/" + myVersion)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,6 +2,8 @@ package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/gorilla/websocket"
|
||||
@@ -12,6 +14,8 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
const timeoutDuration = 2 * time.Minute
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
@@ -78,6 +82,9 @@ func TerminalRoute(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
ws.SetReadDeadline(time.Now().Add(timeoutDuration))
|
||||
ws.SetWriteDeadline(time.Now().Add(timeoutDuration))
|
||||
|
||||
ctx := context.Background()
|
||||
errD := Connect()
|
||||
if errD != nil {
|
||||
@@ -107,7 +114,7 @@ func TerminalRoute(w http.ResponseWriter, r *http.Request) {
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Cmd: []string{"bash"},
|
||||
Cmd: []string{"/bin/sh"},
|
||||
}
|
||||
|
||||
execStart := types.ExecStartCheck{
|
||||
@@ -127,6 +134,9 @@ func TerminalRoute(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "ContainerExecAttach failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Start bash if it exists
|
||||
resp.Conn.Write([]byte("bash\n"))
|
||||
|
||||
utils.Log("Created new shell and attached to it in container " + containerID)
|
||||
} else {
|
||||
@@ -153,6 +163,7 @@ func TerminalRoute(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var WSChan = make(chan []byte, 1024*1024*4)
|
||||
var DockerChan = make(chan []byte, 1024*1024*4)
|
||||
|
||||
|
||||
// Start a goroutine to read from our websocket and write to the container
|
||||
go func(ctx context.Context) {
|
||||
@@ -162,6 +173,7 @@ func TerminalRoute(w http.ResponseWriter, r *http.Request) {
|
||||
case <-ctx.Done(): // Context cancellation
|
||||
return
|
||||
default:
|
||||
ws.SetReadDeadline(time.Now().Add(timeoutDuration))
|
||||
_, message, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
|
||||
@@ -224,6 +236,8 @@ func TerminalRoute(w http.ResponseWriter, r *http.Request) {
|
||||
utils.Debug("Writing message to websocket " + string(message))
|
||||
|
||||
messages := splitIntoChunks(string(message))
|
||||
|
||||
ws.SetWriteDeadline(time.Now().Add(timeoutDuration))
|
||||
|
||||
for _, messageSplit := range messages {
|
||||
err = ws.WriteMessage(websocket.TextMessage, []byte(messageSplit))
|
||||
|
||||
@@ -16,7 +16,9 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
utils.Log("Starting...")
|
||||
utils.Log("------------------------------------------")
|
||||
utils.Log("Starting Cosmos-Server version " + GetCosmosVersion())
|
||||
utils.Log("------------------------------------------")
|
||||
|
||||
// utils.ReBootstrapContainer = docker.BootstrapContainerFromTags
|
||||
utils.PushShieldMetrics = metrics.PushShieldMetrics
|
||||
|
||||
Reference in New Issue
Block a user