[release] v0.15.0-unstable21

This commit is contained in:
Yann Stepienik
2024-03-26 18:35:48 +00:00
parent 670df638b0
commit b2e93257dd
14 changed files with 325 additions and 112 deletions

View File

@@ -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>
);
};

View File

@@ -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

View File

@@ -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 };

View File

@@ -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>
</>
},
]}
/>

View File

@@ -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 };

View 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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -207,4 +207,5 @@ const SnapRAIDDialog = ({ refresh, data }) => {
</>
}
export default SnapRAIDDialog;
export default SnapRAIDDialog;
export { SnapRAIDDialog, SnapRAIDDialogInternal };

19
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
},

View File

@@ -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 {

View File

@@ -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))

View File

@@ -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