Merge branch 'develop' into feature/react-admin
This commit is contained in:
commit
a87fef37ec
77 changed files with 1082 additions and 839 deletions
|
@ -13,6 +13,7 @@ import 'xterm/css/xterm.css';
|
|||
import useEventListener from '@/plugins/useEventListener';
|
||||
import { debounce } from 'debounce';
|
||||
import { usePersistedState } from '@/plugins/usePersistedState';
|
||||
import { SocketEvent, SocketRequest } from '@/components/server/events';
|
||||
|
||||
const theme = {
|
||||
background: th`colors.black`.toString(),
|
||||
|
@ -173,32 +174,35 @@ export default () => {
|
|||
useEventListener('resize', () => fit());
|
||||
|
||||
useEffect(() => {
|
||||
const listeners: Record<string, (s: string) => void> = {
|
||||
[SocketEvent.STATUS]: handlePowerChangeEvent,
|
||||
[SocketEvent.CONSOLE_OUTPUT]: handleConsoleOutput,
|
||||
[SocketEvent.INSTALL_OUTPUT]: handleConsoleOutput,
|
||||
[SocketEvent.TRANSFER_LOGS]: handleConsoleOutput,
|
||||
[SocketEvent.TRANSFER_STATUS]: handleTransferStatus,
|
||||
[SocketEvent.DAEMON_MESSAGE]: line => handleConsoleOutput(line, true),
|
||||
[SocketEvent.DAEMON_ERROR]: handleDaemonErrorOutput,
|
||||
};
|
||||
|
||||
if (connected && instance) {
|
||||
// Do not clear the console if the server is being transferred.
|
||||
if (!isTransferring) {
|
||||
terminal.clear();
|
||||
}
|
||||
|
||||
instance.addListener('status', handlePowerChangeEvent);
|
||||
instance.addListener('console output', handleConsoleOutput);
|
||||
instance.addListener('install output', handleConsoleOutput);
|
||||
instance.addListener('transfer logs', handleConsoleOutput);
|
||||
instance.addListener('transfer status', handleTransferStatus);
|
||||
instance.addListener('daemon message', line => handleConsoleOutput(line, true));
|
||||
instance.addListener('daemon error', handleDaemonErrorOutput);
|
||||
instance.send('send logs');
|
||||
Object.keys(listeners).forEach((key: string) => {
|
||||
instance.addListener(key, listeners[key]);
|
||||
});
|
||||
instance.send(SocketRequest.SEND_LOGS);
|
||||
}
|
||||
|
||||
return () => {
|
||||
instance && instance.removeListener('status', handlePowerChangeEvent)
|
||||
.removeListener('console output', handleConsoleOutput)
|
||||
.removeListener('install output', handleConsoleOutput)
|
||||
.removeListener('transfer logs', handleConsoleOutput)
|
||||
.removeListener('transfer status', handleTransferStatus)
|
||||
.removeListener('daemon message', line => handleConsoleOutput(line, true))
|
||||
.removeListener('daemon error', handleDaemonErrorOutput);
|
||||
if (instance) {
|
||||
Object.keys(listeners).forEach((key: string) => {
|
||||
instance.removeListener(key, listeners[key]);
|
||||
});
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ connected, instance ]);
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,22 +1,30 @@
|
|||
import useWebsocketEvent from '@/plugins/useWebsocketEvent';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { SocketEvent } from '@/components/server/events';
|
||||
import useFileManagerSwr from '@/plugins/useFileManagerSwr';
|
||||
|
||||
const InstallListener = () => {
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
|
||||
const { mutate } = useFileManagerSwr();
|
||||
const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState);
|
||||
|
||||
useWebsocketEvent(SocketEvent.BACKUP_RESTORE_COMPLETED, () => {
|
||||
mutate(undefined);
|
||||
setServerFromState(s => ({ ...s, status: null }));
|
||||
});
|
||||
|
||||
// Listen for the installation completion event and then fire off a request to fetch the updated
|
||||
// server information. This allows the server to automatically become available to the user if they
|
||||
// just sit on the page.
|
||||
useWebsocketEvent('install completed', () => {
|
||||
useWebsocketEvent(SocketEvent.INSTALL_COMPLETED, () => {
|
||||
getServer(uuid).catch(error => console.error(error));
|
||||
});
|
||||
|
||||
// When we see the install started event immediately update the state to indicate such so that the
|
||||
// screens automatically update.
|
||||
useWebsocketEvent('install started', () => {
|
||||
setServerFromState(s => ({ ...s, isInstalling: true }));
|
||||
useWebsocketEvent(SocketEvent.INSTALL_STARTED, () => {
|
||||
setServerFromState(s => ({ ...s, status: 'installing' }));
|
||||
});
|
||||
|
||||
return null;
|
||||
|
|
|
@ -6,6 +6,7 @@ import { bytesToHuman, megabytesToHuman } from '@/helpers';
|
|||
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||
import { SocketEvent, SocketRequest } from '@/components/server/events';
|
||||
|
||||
interface Stats {
|
||||
memory: number;
|
||||
|
@ -55,11 +56,11 @@ const ServerDetailsBlock = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
instance.addListener('stats', statsListener);
|
||||
instance.send('send stats');
|
||||
instance.addListener(SocketEvent.STATS, statsListener);
|
||||
instance.send(SocketRequest.SEND_STATS);
|
||||
|
||||
return () => {
|
||||
instance.removeListener('stats', statsListener);
|
||||
instance.removeListener(SocketEvent.STATS, statsListener);
|
||||
};
|
||||
}, [ instance, connected ]);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import Chart, { ChartConfiguration } from 'chart.js';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { bytesToMegabytes } from '@/helpers';
|
||||
|
@ -6,6 +6,8 @@ import merge from 'deepmerge';
|
|||
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
||||
import { faMemory, faMicrochip } from '@fortawesome/free-solid-svg-icons';
|
||||
import tw from 'twin.macro';
|
||||
import { SocketEvent } from '@/components/server/events';
|
||||
import useWebsocketEvent from '@/plugins/useWebsocketEvent';
|
||||
|
||||
const chartDefaults = (ticks?: Chart.TickOptions | undefined): ChartConfiguration => ({
|
||||
type: 'line',
|
||||
|
@ -70,7 +72,6 @@ const chartDefaults = (ticks?: Chart.TickOptions | undefined): ChartConfiguratio
|
|||
export default () => {
|
||||
const status = ServerContext.useStoreState(state => state.status.value);
|
||||
const limits = ServerContext.useStoreState(state => state.server.data!.limits);
|
||||
const { connected, instance } = ServerContext.useStoreState(state => state.socket);
|
||||
|
||||
const [ memory, setMemory ] = useState<Chart>();
|
||||
const [ cpu, setCpu ] = useState<Chart>();
|
||||
|
@ -84,7 +85,7 @@ export default () => {
|
|||
new Chart(node.getContext('2d')!, chartDefaults({
|
||||
callback: (value) => `${value}Mb `,
|
||||
suggestedMax: limits.memory,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
}, []);
|
||||
|
||||
|
@ -100,7 +101,7 @@ export default () => {
|
|||
);
|
||||
}, []);
|
||||
|
||||
const statsListener = (data: string) => {
|
||||
useWebsocketEvent(SocketEvent.STATS, (data: string) => {
|
||||
let stats: any = {};
|
||||
try {
|
||||
stats = JSON.parse(data);
|
||||
|
@ -125,27 +126,19 @@ export default () => {
|
|||
|
||||
cpu.update({ lazy: true });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!connected || !instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
instance.addListener('stats', statsListener);
|
||||
|
||||
return () => {
|
||||
instance.removeListener('stats', statsListener);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ instance, connected, memory, cpu ]);
|
||||
});
|
||||
|
||||
return (
|
||||
<div css={tw`flex flex-wrap mt-4`}>
|
||||
<div css={tw`w-full sm:w-1/2`}>
|
||||
<TitledGreyBox title={'Memory usage'} icon={faMemory} css={tw`mr-0 sm:mr-4`}>
|
||||
{status !== 'offline' ?
|
||||
<canvas id={'memory_chart'} ref={memoryRef} aria-label={'Server Memory Usage Graph'} role={'img'}/>
|
||||
<canvas
|
||||
id={'memory_chart'}
|
||||
ref={memoryRef}
|
||||
aria-label={'Server Memory Usage Graph'}
|
||||
role={'img'}
|
||||
/>
|
||||
:
|
||||
<p css={tw`text-xs text-neutral-400 text-center p-3`}>
|
||||
Server is offline.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import useWebsocketEvent from '@/plugins/useWebsocketEvent';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { SocketEvent } from '@/components/server/events';
|
||||
|
||||
const TransferListener = () => {
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
|
@ -7,7 +8,7 @@ const TransferListener = () => {
|
|||
const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState);
|
||||
|
||||
// Listen for the transfer status event so we can update the state of the server.
|
||||
useWebsocketEvent('transfer status', (status: string) => {
|
||||
useWebsocketEvent(SocketEvent.TRANSFER_STATUS, (status: string) => {
|
||||
if (status === 'starting') {
|
||||
setServerFromState(s => ({ ...s, isTransferring: true }));
|
||||
return;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import { faCloudDownloadAlt, faEllipsisH, faLock, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faBoxOpen, faCloudDownloadAlt, faEllipsisH, faLock, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import DropdownMenu, { DropdownButtonRow } from '@/components/elements/DropdownMenu';
|
||||
import getBackupDownloadUrl from '@/api/server/backups/getBackupDownloadUrl';
|
||||
|
@ -13,6 +13,8 @@ import tw from 'twin.macro';
|
|||
import getServerBackups from '@/api/swr/getServerBackups';
|
||||
import { ServerBackup } from '@/api/server/types';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import Input from '@/components/elements/Input';
|
||||
import { restoreServerBackup } from '@/api/server/backups';
|
||||
|
||||
interface Props {
|
||||
backup: ServerBackup;
|
||||
|
@ -20,9 +22,9 @@ interface Props {
|
|||
|
||||
export default ({ backup }: Props) => {
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState);
|
||||
const [ modal, setModal ] = useState('');
|
||||
const [ loading, setLoading ] = useState(false);
|
||||
const [ visible, setVisible ] = useState(false);
|
||||
const [ deleteVisible, setDeleteVisible ] = useState(false);
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const { mutate } = getServerBackups();
|
||||
|
||||
|
@ -45,36 +47,78 @@ export default ({ backup }: Props) => {
|
|||
setLoading(true);
|
||||
clearFlashes('backups');
|
||||
deleteBackup(uuid, backup.uuid)
|
||||
.then(() => {
|
||||
mutate(data => ({
|
||||
...data,
|
||||
items: data.items.filter(b => b.uuid !== backup.uuid),
|
||||
}), false);
|
||||
})
|
||||
.then(() => mutate(data => ({
|
||||
...data,
|
||||
items: data.items.filter(b => b.uuid !== backup.uuid),
|
||||
}), false))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
clearAndAddHttpError({ key: 'backups', error });
|
||||
setLoading(false);
|
||||
setDeleteVisible(false);
|
||||
setModal('');
|
||||
});
|
||||
};
|
||||
|
||||
const doRestorationAction = () => {
|
||||
setLoading(true);
|
||||
clearFlashes('backups');
|
||||
restoreServerBackup(uuid, backup.uuid)
|
||||
.then(() => setServerFromState(s => ({
|
||||
...s,
|
||||
status: 'restoring_backup',
|
||||
})))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
clearAndAddHttpError({ key: 'backups', error });
|
||||
})
|
||||
.then(() => setLoading(false))
|
||||
.then(() => setModal(''));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{visible &&
|
||||
<ChecksumModal
|
||||
appear
|
||||
visible={visible}
|
||||
onDismissed={() => setVisible(false)}
|
||||
visible={modal === 'checksum'}
|
||||
onDismissed={() => setModal('')}
|
||||
checksum={backup.checksum}
|
||||
/>
|
||||
}
|
||||
<ConfirmationModal
|
||||
visible={deleteVisible}
|
||||
visible={modal === 'restore'}
|
||||
title={'Restore this backup?'}
|
||||
buttonText={'Restore backup'}
|
||||
onConfirmed={() => doRestorationAction()}
|
||||
onModalDismissed={() => setModal('')}
|
||||
>
|
||||
<p css={tw`text-neutral-300`}>
|
||||
This server will be stopped in order to restore the backup. Once the backup has started you will
|
||||
not be able to control the server power state, access the file manager, or create additional backups
|
||||
until it has completed.
|
||||
</p>
|
||||
<p css={tw`text-neutral-300 mt-4`}>
|
||||
Are you sure you want to continue?
|
||||
</p>
|
||||
<p css={tw`mt-4 -mb-2 bg-neutral-900 p-3 rounded`}>
|
||||
<label
|
||||
htmlFor={'restore_truncate'}
|
||||
css={tw`text-base text-neutral-200 flex items-center cursor-pointer`}
|
||||
>
|
||||
<Input
|
||||
type={'checkbox'}
|
||||
css={tw`text-red-500! w-5! h-5! mr-2`}
|
||||
id={'restore_truncate'}
|
||||
value={'true'}
|
||||
/>
|
||||
Remove all files and folders before restoring this backup.
|
||||
</label>
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
<ConfirmationModal
|
||||
visible={modal === 'delete'}
|
||||
title={'Delete this backup?'}
|
||||
buttonText={'Yes, delete backup'}
|
||||
onConfirmed={() => doDeletion()}
|
||||
onModalDismissed={() => setDeleteVisible(false)}
|
||||
onModalDismissed={() => setModal('')}
|
||||
>
|
||||
Are you sure you wish to delete this backup? This is a permanent operation and the backup cannot
|
||||
be recovered once deleted.
|
||||
|
@ -93,17 +137,23 @@ export default ({ backup }: Props) => {
|
|||
>
|
||||
<div css={tw`text-sm`}>
|
||||
<Can action={'backup.download'}>
|
||||
<DropdownButtonRow onClick={() => doDownload()}>
|
||||
<DropdownButtonRow onClick={doDownload}>
|
||||
<FontAwesomeIcon fixedWidth icon={faCloudDownloadAlt} css={tw`text-xs`}/>
|
||||
<span css={tw`ml-2`}>Download</span>
|
||||
</DropdownButtonRow>
|
||||
</Can>
|
||||
<DropdownButtonRow onClick={() => setVisible(true)}>
|
||||
<Can action={'backup.restore'}>
|
||||
<DropdownButtonRow onClick={() => setModal('restore')}>
|
||||
<FontAwesomeIcon fixedWidth icon={faBoxOpen} css={tw`text-xs`}/>
|
||||
<span css={tw`ml-2`}>Restore</span>
|
||||
</DropdownButtonRow>
|
||||
</Can>
|
||||
<DropdownButtonRow onClick={() => setModal('checksum')}>
|
||||
<FontAwesomeIcon fixedWidth icon={faLock} css={tw`text-xs`}/>
|
||||
<span css={tw`ml-2`}>Checksum</span>
|
||||
</DropdownButtonRow>
|
||||
<Can action={'backup.delete'}>
|
||||
<DropdownButtonRow danger onClick={() => setDeleteVisible(true)}>
|
||||
<DropdownButtonRow danger onClick={() => setModal('delete')}>
|
||||
<FontAwesomeIcon fixedWidth icon={faTrashAlt} css={tw`text-xs`}/>
|
||||
<span css={tw`ml-2`}>Delete</span>
|
||||
</DropdownButtonRow>
|
||||
|
@ -112,7 +162,7 @@ export default ({ backup }: Props) => {
|
|||
</DropdownMenu>
|
||||
:
|
||||
<button
|
||||
onClick={() => setDeleteVisible(true)}
|
||||
onClick={() => setModal('delete')}
|
||||
css={tw`text-neutral-200 transition-colors duration-150 hover:text-neutral-100 p-2`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashAlt}/>
|
||||
|
|
|
@ -11,6 +11,7 @@ import tw from 'twin.macro';
|
|||
import GreyRowBox from '@/components/elements/GreyRowBox';
|
||||
import getServerBackups from '@/api/swr/getServerBackups';
|
||||
import { ServerBackup } from '@/api/server/types';
|
||||
import { SocketEvent } from '@/components/server/events';
|
||||
|
||||
interface Props {
|
||||
backup: ServerBackup;
|
||||
|
@ -20,7 +21,7 @@ interface Props {
|
|||
export default ({ backup, className }: Props) => {
|
||||
const { mutate } = getServerBackups();
|
||||
|
||||
useWebsocketEvent(`backup completed:${backup.uuid}`, data => {
|
||||
useWebsocketEvent(`${SocketEvent.BACKUP_COMPLETED}:${backup.uuid}` as SocketEvent, data => {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
export enum SocketEvent {
|
||||
DAEMON_MESSAGE = 'daemon message',
|
||||
DAEMON_ERROR = 'daemon error',
|
||||
INSTALL_OUTPUT = 'install output',
|
||||
INSTALL_STARTED = 'install started',
|
||||
INSTALL_COMPLETED = 'install completed',
|
||||
CONSOLE_OUTPUT = 'console output',
|
||||
STATUS = 'status',
|
||||
STATS = 'stats',
|
||||
TRANSFER_LOGS = 'transfer logs',
|
||||
TRANSFER_STATUS = 'transfer status',
|
||||
BACKUP_COMPLETED = 'backup completed',
|
||||
BACKUP_RESTORE_COMPLETED = 'backup restore completed',
|
||||
}
|
||||
|
||||
export enum SocketRequest {
|
||||
SEND_LOGS = 'send logs',
|
||||
SEND_STATS = 'send stats',
|
||||
SET_STATE = 'set state'
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import Button from '@/components/elements/Button';
|
|||
import saveFileContents from '@/api/server/files/saveFileContents';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import { SocketEvent, SocketRequest } from '@/components/server/events';
|
||||
|
||||
const EulaModalFeature = () => {
|
||||
const [ visible, setVisible ] = useState(false);
|
||||
|
@ -25,10 +26,10 @@ const EulaModalFeature = () => {
|
|||
}
|
||||
};
|
||||
|
||||
instance.addListener('console output', listener);
|
||||
instance.addListener(SocketEvent.CONSOLE_OUTPUT, listener);
|
||||
|
||||
return () => {
|
||||
instance.removeListener('console output', listener);
|
||||
instance.removeListener(SocketEvent.CONSOLE_OUTPUT, listener);
|
||||
};
|
||||
}, [ connected, instance, status ]);
|
||||
|
||||
|
@ -39,7 +40,7 @@ const EulaModalFeature = () => {
|
|||
saveFileContents(uuid, 'eula.txt', 'eula=true')
|
||||
.then(() => {
|
||||
if (status === 'offline' && instance) {
|
||||
instance.send('set state', 'restart');
|
||||
instance.send(SocketRequest.SET_STATE, 'restart');
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
|
|
|
@ -9,7 +9,7 @@ import FileNameModal from '@/components/server/files/FileNameModal';
|
|||
import Can from '@/components/elements/Can';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||
import ServerError from '@/components/screens/ServerError';
|
||||
import { ServerError } from '@/components/elements/ScreenBlock';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import Select from '@/components/elements/Select';
|
||||
|
|
|
@ -8,7 +8,7 @@ import { FileObject } from '@/api/server/files/loadDirectory';
|
|||
import NewDirectoryButton from '@/components/server/files/NewDirectoryButton';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import Can from '@/components/elements/Can';
|
||||
import ServerError from '@/components/screens/ServerError';
|
||||
import { ServerError } from '@/components/elements/ScreenBlock';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
|
|
@ -5,7 +5,7 @@ import VariableBox from '@/components/server/startup/VariableBox';
|
|||
import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
||||
import getServerStartup from '@/api/swr/getServerStartup';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import ServerError from '@/components/screens/ServerError';
|
||||
import { ServerError } from '@/components/elements/ScreenBlock';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue