Merge branch 'develop' into subusers
This commit is contained in:
commit
aad3019747
71 changed files with 1375 additions and 753 deletions
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ITerminalOptions, Terminal } from 'xterm';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
import { SearchAddon } from 'xterm-addon-search';
|
||||
|
@ -11,6 +11,7 @@ import tw, { theme as th } from 'twin.macro';
|
|||
import 'xterm/css/xterm.css';
|
||||
import useEventListener from '@/plugins/useEventListener';
|
||||
import { debounce } from 'debounce';
|
||||
import { usePersistedState } from '@/plugins/usePersistedState';
|
||||
|
||||
const theme = {
|
||||
background: th`colors.black`.toString(),
|
||||
|
@ -63,6 +64,9 @@ export default () => {
|
|||
const searchBar = new SearchBarAddon({ searchAddon });
|
||||
const { connected, instance } = ServerContext.useStoreState(state => state.socket);
|
||||
const [ canSendCommands ] = usePermissions([ 'control.console' ]);
|
||||
const serverId = ServerContext.useStoreState(state => state.server.data!.id);
|
||||
const [ history, setHistory ] = usePersistedState<string[]>(`${serverId}:command_history`, []);
|
||||
const [ historyIndex, setHistoryIndex ] = useState(-1);
|
||||
|
||||
const handleConsoleOutput = (line: string, prelude = false) => terminal.writeln(
|
||||
(prelude ? TERMINAL_PRELUDE : '') + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m',
|
||||
|
@ -77,12 +81,28 @@ export default () => {
|
|||
);
|
||||
|
||||
const handleCommandKeydown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key !== 'Enter' || (e.key === 'Enter' && e.currentTarget.value.length < 1)) {
|
||||
return;
|
||||
if (e.key === 'ArrowUp') {
|
||||
const newIndex = Math.min(historyIndex + 1, history!.length - 1);
|
||||
|
||||
setHistoryIndex(newIndex);
|
||||
e.currentTarget.value = history![newIndex] || '';
|
||||
}
|
||||
|
||||
instance && instance.send('send command', e.currentTarget.value);
|
||||
e.currentTarget.value = '';
|
||||
if (e.key === 'ArrowDown') {
|
||||
const newIndex = Math.max(historyIndex - 1, -1);
|
||||
|
||||
setHistoryIndex(newIndex);
|
||||
e.currentTarget.value = history![newIndex] || '';
|
||||
}
|
||||
|
||||
const command = e.currentTarget.value;
|
||||
if (e.key === 'Enter' && command.length > 0) {
|
||||
setHistory(prevHistory => [ command, ...prevHistory! ].slice(0, 32));
|
||||
setHistoryIndex(-1);
|
||||
|
||||
instance && instance.send('send command', command);
|
||||
e.currentTarget.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -16,6 +16,7 @@ import Select from '@/components/elements/Select';
|
|||
import modes from '@/modes';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import ErrorBoundary from '@/components/elements/ErrorBoundary';
|
||||
|
||||
const LazyCodemirrorEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/CodemirrorEditor'));
|
||||
|
||||
|
@ -60,9 +61,7 @@ export default () => {
|
|||
setLoading(true);
|
||||
clearFlashes('files:view');
|
||||
fetchFileContent()
|
||||
.then(content => {
|
||||
return saveFileContents(uuid, name || hash.replace(/^#/, ''), content);
|
||||
})
|
||||
.then(content => saveFileContents(uuid, encodeURIComponent(name || hash.replace(/^#/, '')), content))
|
||||
.then(() => {
|
||||
if (name) {
|
||||
history.push(`/server/${id}/files/edit#/${name}`);
|
||||
|
@ -87,7 +86,9 @@ export default () => {
|
|||
return (
|
||||
<PageContentBlock>
|
||||
<FlashMessageRender byKey={'files:view'} css={tw`mb-4`}/>
|
||||
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/>
|
||||
<ErrorBoundary>
|
||||
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/>
|
||||
</ErrorBoundary>
|
||||
{hash.replace(/^#/, '').endsWith('.pteroignore') &&
|
||||
<div css={tw`mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400`}>
|
||||
<p css={tw`text-neutral-300 text-sm`}>
|
||||
|
|
|
@ -33,10 +33,10 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
|
|||
.filter(directory => !!directory)
|
||||
.map((directory, index, dirs) => {
|
||||
if (!withinFileEditor && index === dirs.length - 1) {
|
||||
return { name: decodeURIComponent(directory) };
|
||||
return { name: decodeURIComponent(encodeURIComponent(directory)) };
|
||||
}
|
||||
|
||||
return { name: decodeURIComponent(directory), path: `/${dirs.slice(0, index + 1).join('/')}` };
|
||||
return { name: decodeURIComponent(encodeURIComponent(directory)), path: `/${dirs.slice(0, index + 1).join('/')}` };
|
||||
});
|
||||
|
||||
const onSelectAllClick = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
@ -79,7 +79,7 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
|
|||
}
|
||||
{file &&
|
||||
<React.Fragment>
|
||||
<span css={tw`px-1 text-neutral-300`}>{decodeURIComponent(file)}</span>
|
||||
<span css={tw`px-1 text-neutral-300`}>{decodeURIComponent(encodeURIComponent(file))}</span>
|
||||
</React.Fragment>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -17,6 +17,7 @@ import MassActionsBar from '@/components/server/files/MassActionsBar';
|
|||
import UploadButton from '@/components/server/files/UploadButton';
|
||||
import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
||||
import { useStoreActions } from '@/state/hooks';
|
||||
import ErrorBoundary from '@/components/elements/ErrorBoundary';
|
||||
|
||||
const sortFiles = (files: FileObject[]): FileObject[] => {
|
||||
return files.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
@ -50,7 +51,9 @@ export default () => {
|
|||
|
||||
return (
|
||||
<ServerContentBlock title={'File Manager'} showFlashKey={'files'}>
|
||||
<FileManagerBreadcrumbs/>
|
||||
<ErrorBoundary>
|
||||
<FileManagerBreadcrumbs/>
|
||||
</ErrorBoundary>
|
||||
{
|
||||
!files ?
|
||||
<Spinner size={'large'} centered/>
|
||||
|
@ -81,18 +84,20 @@ export default () => {
|
|||
</CSSTransition>
|
||||
}
|
||||
<Can action={'file.create'}>
|
||||
<div css={tw`flex flex-wrap-reverse justify-end mt-4`}>
|
||||
<NewDirectoryButton css={tw`w-full flex-none mt-4 sm:mt-0 sm:w-auto sm:mr-4`}/>
|
||||
<UploadButton css={tw`flex-1 mr-4 sm:flex-none sm:mt-0`}/>
|
||||
<NavLink
|
||||
to={`/server/${id}/files/new${window.location.hash}`}
|
||||
css={tw`flex-1 sm:flex-none sm:mt-0`}
|
||||
>
|
||||
<Button css={tw`w-full`}>
|
||||
New File
|
||||
</Button>
|
||||
</NavLink>
|
||||
</div>
|
||||
<ErrorBoundary>
|
||||
<div css={tw`flex flex-wrap-reverse justify-end mt-4`}>
|
||||
<NewDirectoryButton css={tw`w-full flex-none mt-4 sm:mt-0 sm:w-auto sm:mr-4`}/>
|
||||
<UploadButton css={tw`flex-1 mr-4 sm:flex-none sm:mt-0`}/>
|
||||
<NavLink
|
||||
to={`/server/${id}/files/new${window.location.hash}`}
|
||||
css={tw`flex-1 sm:flex-none sm:mt-0`}
|
||||
>
|
||||
<Button css={tw`w-full`}>
|
||||
New File
|
||||
</Button>
|
||||
</NavLink>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</Can>
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -92,9 +92,9 @@ export default ({ className }: WithClassname) => {
|
|||
<span css={tw`text-neutral-200`}>This directory will be created as</span>
|
||||
/home/container/
|
||||
<span css={tw`text-cyan-200`}>
|
||||
{decodeURIComponent(
|
||||
{decodeURIComponent(encodeURIComponent(
|
||||
join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''),
|
||||
)}
|
||||
))}
|
||||
</span>
|
||||
</p>
|
||||
<div css={tw`flex justify-end`}>
|
||||
|
|
|
@ -14,6 +14,7 @@ import { debounce } from 'debounce';
|
|||
import setServerAllocationNotes from '@/api/server/network/setServerAllocationNotes';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||
|
||||
const Code = styled.code`${tw`font-mono py-1 px-2 bg-neutral-900 rounded text-sm inline-block`}`;
|
||||
const Label = styled.label`${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}`;
|
||||
|
@ -43,11 +44,12 @@ const AllocationRow = ({ allocation, onSetPrimary, onNotesChanged }: Props) => {
|
|||
<GreyRowBox $hoverable={false} css={tw`flex-wrap md:flex-no-wrap mt-2`}>
|
||||
<div css={tw`flex items-center w-full md:w-auto`}>
|
||||
<div css={tw`pl-4 pr-6 text-neutral-400`}>
|
||||
<FontAwesomeIcon icon={faNetworkWired}/>
|
||||
<FontAwesomeIcon icon={faNetworkWired} />
|
||||
</div>
|
||||
<div css={tw`mr-4 flex-1 md:w-40`}>
|
||||
<Code>{allocation.alias || allocation.ip}</Code>
|
||||
<Label>IP Address</Label>
|
||||
{allocation.alias ? <CopyOnClick text={allocation.alias}><Code css={tw`w-40 truncate`}>{allocation.alias}</Code></CopyOnClick> :
|
||||
<CopyOnClick text={allocation.ip}><Code>{allocation.ip}</Code></CopyOnClick>}
|
||||
<Label>{allocation.alias ? 'Hostname' : 'IP Address'}</Label>
|
||||
</div>
|
||||
<div css={tw`w-16 md:w-24 overflow-hidden`}>
|
||||
<Code>{allocation.port}</Code>
|
||||
|
|
|
@ -10,7 +10,7 @@ export default () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{visible && <EditSubuserModal appear visible onDismissed={() => setVisible(false)}/>}
|
||||
<EditSubuserModal visible={visible} onModalDismissed={() => setVisible(false)}/>
|
||||
<Button onClick={() => setVisible(true)}>
|
||||
<FontAwesomeIcon icon={faUserPlus} css={tw`mr-1`}/> New User
|
||||
</Button>
|
||||
|
|
65
resources/scripts/components/server/users/PermissionRow.tsx
Normal file
65
resources/scripts/components/server/users/PermissionRow.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import styled from 'styled-components/macro';
|
||||
import tw from 'twin.macro';
|
||||
import Checkbox from '@/components/elements/Checkbox';
|
||||
import React from 'react';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
import Label from '@/components/elements/Label';
|
||||
|
||||
const Container = styled.label`
|
||||
${tw`flex items-center border border-transparent rounded md:p-2 transition-colors duration-75`};
|
||||
text-transform: none;
|
||||
|
||||
&:not(.disabled) {
|
||||
${tw`cursor-pointer`};
|
||||
|
||||
&:hover {
|
||||
${tw`border-neutral-500 bg-neutral-800`};
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
${tw`mt-4 sm:mt-2`};
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
${tw`opacity-50`};
|
||||
|
||||
& input[type="checkbox"]:not(:checked) {
|
||||
${tw`border-0`};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
permission: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const PermissionRow = ({ permission, disabled }: Props) => {
|
||||
const [ key, pkey ] = permission.split('.', 2);
|
||||
const permissions = useStoreState(state => state.permissions.data);
|
||||
|
||||
return (
|
||||
<Container htmlFor={`permission_${permission}`} className={disabled ? 'disabled' : undefined}>
|
||||
<div css={tw`p-2`}>
|
||||
<Checkbox
|
||||
id={`permission_${permission}`}
|
||||
name={'permissions'}
|
||||
value={permission}
|
||||
css={tw`w-5 h-5 mr-2`}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`flex-1`}>
|
||||
<Label as={'p'} css={tw`font-medium`}>{pkey}</Label>
|
||||
{permissions[key].keys[pkey].length > 0 &&
|
||||
<p css={tw`text-xs text-neutral-400 mt-1`}>
|
||||
{permissions[key].keys[pkey]}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermissionRow;
|
|
@ -0,0 +1,50 @@
|
|||
import React, { memo, useCallback } from 'react';
|
||||
import { useField } from 'formik';
|
||||
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
||||
import tw from 'twin.macro';
|
||||
import Input from '@/components/elements/Input';
|
||||
import isEqual from 'react-fast-compare';
|
||||
|
||||
interface Props {
|
||||
isEditable: boolean;
|
||||
title: string;
|
||||
permissions: string[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PermissionTitleBox: React.FC<Props> = memo(({ isEditable, title, permissions, className, children }) => {
|
||||
const [ { value }, , { setValue } ] = useField<string[]>('permissions');
|
||||
|
||||
const onCheckboxClicked = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.currentTarget.checked) {
|
||||
setValue([
|
||||
...value,
|
||||
...permissions.filter(p => !value.includes(p)),
|
||||
]);
|
||||
} else {
|
||||
setValue(value.filter(p => !permissions.includes(p)));
|
||||
}
|
||||
}, [ permissions, value ]);
|
||||
|
||||
return (
|
||||
<TitledGreyBox
|
||||
title={
|
||||
<div css={tw`flex items-center`}>
|
||||
<p css={tw`text-sm uppercase flex-1`}>{title}</p>
|
||||
{isEditable &&
|
||||
<Input
|
||||
type={'checkbox'}
|
||||
checked={permissions.every(p => value.includes(p))}
|
||||
onChange={onCheckboxClicked}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</TitledGreyBox>
|
||||
);
|
||||
}, isEqual);
|
||||
|
||||
export default PermissionTitleBox;
|
|
@ -19,14 +19,11 @@ export default ({ subuser }: Props) => {
|
|||
|
||||
return (
|
||||
<GreyRowBox css={tw`mb-2`}>
|
||||
{visible &&
|
||||
<EditSubuserModal
|
||||
appear
|
||||
visible
|
||||
subuser={subuser}
|
||||
onDismissed={() => setVisible(false)}
|
||||
visible={visible}
|
||||
onModalDismissed={() => setVisible(false)}
|
||||
/>
|
||||
}
|
||||
<div css={tw`w-10 h-10 rounded-full bg-white border-2 border-neutral-800 overflow-hidden hidden md:block`}>
|
||||
<img css={tw`w-full h-full`} src={`${subuser.image}?s=400`}/>
|
||||
</div>
|
||||
|
|
Reference in a new issue