diff --git a/.prettierrc.mjs b/.prettierrc.mjs
new file mode 100644
index 0000000..2cb1ba9
--- /dev/null
+++ b/.prettierrc.mjs
@@ -0,0 +1,7 @@
+export default {
+ semi: false,
+ singleQuote: true,
+ jsxSingleQuote: true,
+ arrowParens: 'avoid',
+ printWidth: 100,
+}
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 8e8adf9..daaa5ee 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,6 +1,7 @@
{
"recommendations": [
"arcanis.vscode-zipfs",
- "dbaeumer.vscode-eslint"
+ "dbaeumer.vscode-eslint",
+ "esbenp.prettier-vscode"
]
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b6d4f3d..20c8c73 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -8,5 +8,6 @@
"**/.pnp.*": true
},
"eslint.nodePath": ".yarn/sdks",
- "typescript.enablePromptUseWorkspaceTsdk": true
+ "typescript.enablePromptUseWorkspaceTsdk": true,
+ "prettier.prettierPath": ".yarn/sdks/prettier/index.cjs"
}
diff --git a/.yarn/sdks/prettier/bin/prettier.cjs b/.yarn/sdks/prettier/bin/prettier.cjs
new file mode 100755
index 0000000..9a4098f
--- /dev/null
+++ b/.yarn/sdks/prettier/bin/prettier.cjs
@@ -0,0 +1,32 @@
+#!/usr/bin/env node
+
+const {existsSync} = require(`fs`);
+const {createRequire, register} = require(`module`);
+const {resolve} = require(`path`);
+const {pathToFileURL} = require(`url`);
+
+const relPnpApiPath = "../../../../.pnp.cjs";
+
+const absPnpApiPath = resolve(__dirname, relPnpApiPath);
+const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
+const absRequire = createRequire(absPnpApiPath);
+
+const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
+const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
+
+if (existsSync(absPnpApiPath)) {
+ if (!process.versions.pnp) {
+ // Setup the environment to be able to require prettier/bin/prettier.cjs
+ require(absPnpApiPath).setup();
+ if (isPnpLoaderEnabled && register) {
+ register(pathToFileURL(absPnpLoaderPath));
+ }
+ }
+}
+
+const wrapWithUserWrapper = existsSync(absUserWrapperPath)
+ ? exports => absRequire(absUserWrapperPath)(exports)
+ : exports => exports;
+
+// Defer to the real prettier/bin/prettier.cjs your application uses
+module.exports = wrapWithUserWrapper(absRequire(`prettier/bin/prettier.cjs`));
diff --git a/.yarn/sdks/prettier/index.cjs b/.yarn/sdks/prettier/index.cjs
new file mode 100644
index 0000000..57cb2ab
--- /dev/null
+++ b/.yarn/sdks/prettier/index.cjs
@@ -0,0 +1,32 @@
+#!/usr/bin/env node
+
+const {existsSync} = require(`fs`);
+const {createRequire, register} = require(`module`);
+const {resolve} = require(`path`);
+const {pathToFileURL} = require(`url`);
+
+const relPnpApiPath = "../../../.pnp.cjs";
+
+const absPnpApiPath = resolve(__dirname, relPnpApiPath);
+const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
+const absRequire = createRequire(absPnpApiPath);
+
+const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
+const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
+
+if (existsSync(absPnpApiPath)) {
+ if (!process.versions.pnp) {
+ // Setup the environment to be able to require prettier
+ require(absPnpApiPath).setup();
+ if (isPnpLoaderEnabled && register) {
+ register(pathToFileURL(absPnpLoaderPath));
+ }
+ }
+}
+
+const wrapWithUserWrapper = existsSync(absUserWrapperPath)
+ ? exports => absRequire(absUserWrapperPath)(exports)
+ : exports => exports;
+
+// Defer to the real prettier your application uses
+module.exports = wrapWithUserWrapper(absRequire(`prettier`));
diff --git a/.yarn/sdks/prettier/package.json b/.yarn/sdks/prettier/package.json
new file mode 100644
index 0000000..b9ab57f
--- /dev/null
+++ b/.yarn/sdks/prettier/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "prettier",
+ "version": "3.4.2-sdk",
+ "main": "./index.cjs",
+ "type": "commonjs",
+ "bin": "./bin/prettier.cjs"
+}
diff --git a/__tests__/index.test.tsx b/__tests__/index.test.tsx
index b108223..0b318d3 100644
--- a/__tests__/index.test.tsx
+++ b/__tests__/index.test.tsx
@@ -5,8 +5,6 @@ import Home from '../pages/index'
test('renders a heading', () => {
render()
- const heading = screen.getByRole('heading', {
- name: /Octyne/i
- })
+ const heading = screen.getByRole('heading', { name: /Octyne/i })
expect(heading).toBeDefined()
})
diff --git a/eslint.config.mjs b/eslint.config.mjs
index c478c75..ced1aee 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,54 +1,81 @@
-import love from 'eslint-config-love'
+import js from '@eslint/js'
+import tseslint from 'typescript-eslint'
+import nextPlugin from '@next/eslint-plugin-next'
import standardJsx from 'eslint-config-standard-jsx'
import standardReact from 'eslint-config-standard-react'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
+import importPlugin from 'eslint-plugin-import'
+import pluginPromise from 'eslint-plugin-promise'
+import nodePlugin from 'eslint-plugin-n'
+import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
-export default [
+export default tseslint.config(
{
- ignores: ['.pnp.cjs', '.pnp.loader.mjs', '.yarn', '.next'],
+ ignores: [
+ '.pnp.cjs',
+ '.pnp.loader.mjs',
+ '.yarn',
+ '.next',
+ '.prettierrc.mjs',
+ '*.config.{mjs,js}',
+ ],
},
+ js.configs.recommended,
+ tseslint.configs.strictTypeChecked,
+ tseslint.configs.stylisticTypeChecked,
+ react.configs.flat.recommended,
+ pluginPromise.configs['flat/recommended'],
+ importPlugin.flatConfigs.recommended, // Could use TypeScript resolver
+ nodePlugin.configs['flat/recommended-module'],
{
- ...love,
- files: [
- '__tests__/**/*.{js,ts,tsx}',
- 'imports/**/*.{js,ts,tsx}',
- 'pages/**/*.{js,ts,tsx}',
- ],
+ plugins: { '@next/next': nextPlugin },
+ rules: nextPlugin.configs.recommended.rules,
+ },
+ {
+ plugins: { 'react-hooks': reactHooks },
+ rules: reactHooks.configs.recommended.rules,
},
- { ...react.configs.flat.recommended, settings: { react: { version: 'detect' } } },
- { plugins: { 'react-hooks': reactHooks }, rules: reactHooks.configs.recommended.rules },
{ rules: standardJsx.rules },
{ rules: standardReact.rules },
{
+ settings: { react: { version: 'detect' } },
+ languageOptions: {
+ parserOptions: {
+ projectService: true,
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
rules: {
- // Make TypeScript ESLint less strict.
- '@typescript-eslint/strict-boolean-expressions': 'off',
- '@typescript-eslint/triple-slash-reference': 'off',
- '@typescript-eslint/restrict-template-expressions': 'off',
'@typescript-eslint/no-confusing-void-expression': 'off',
- 'multiline-ternary': 'off', // Temporary.
- // Allow no-multi-str.
- 'no-multi-str': 'off',
- // Make ESLint Config Love less strict. Perhaps move to ESLint+TS-ESLint+import+promise+n?
- '@typescript-eslint/max-params': 'off',
- '@typescript-eslint/no-require-imports': 'off',
+ /* '@typescript-eslint/restrict-template-expressions': [
+ 'error',
+ {
+ allowAny: false,
+ allowBoolean: false,
+ allowNullish: false,
+ allowNumber: true,
+ allowRegExp: false,
+ allowNever: false,
+ },
+ ], */
+ 'n/no-missing-import': 'off',
+ 'n/no-unsupported-features/node-builtins': 'off',
+ 'n/no-unsupported-features/es-syntax': 'off',
+ 'import/no-unresolved': 'off',
+ // TODO: Re-enable these!
+ '@typescript-eslint/use-unknown-in-catch-callback-variable': 'off',
+ '@typescript-eslint/restrict-template-expressions': 'off',
+ '@typescript-eslint/no-unnecessary-condition': 'off',
+ '@typescript-eslint/no-unsafe-member-access': 'off',
+ '@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'off',
- '@typescript-eslint/no-magic-numbers': 'off',
- '@typescript-eslint/no-unsafe-argument': 'off',
- '@typescript-eslint/no-unsafe-assignment': 'off',
- '@typescript-eslint/no-unsafe-member-access': 'off',
- '@typescript-eslint/no-unnecessary-condition': 'off',
- '@typescript-eslint/no-unsafe-type-assertion': 'off',
- '@typescript-eslint/class-methods-use-this': 'off',
- '@typescript-eslint/prefer-destructuring': 'off',
- '@typescript-eslint/use-unknown-in-catch-callback-variable': 'off',
- 'complexity': 'off',
- 'promise/avoid-new': 'off',
- '@typescript-eslint/init-declarations': 'off',
- '@typescript-eslint/no-loop-func': 'off',
+ 'promise/catch-or-return': 'off',
+ 'promise/always-return': 'off',
+ 'promise/no-nesting': 'off',
},
},
-]
+ eslintPluginPrettierRecommended,
+)
diff --git a/imports/dashboard/console/consoleButtons.tsx b/imports/dashboard/console/consoleButtons.tsx
index 68968f2..4ad13df 100644
--- a/imports/dashboard/console/consoleButtons.tsx
+++ b/imports/dashboard/console/consoleButtons.tsx
@@ -4,7 +4,9 @@ import Stop from '@mui/icons-material/Stop'
import Close from '@mui/icons-material/Close'
import PlayArrow from '@mui/icons-material/PlayArrow'
-const ConsoleButtons = ({ stopStartServer }: {
+const ConsoleButtons = ({
+ stopStartServer,
+}: {
stopStartServer: (operation: 'START' | 'TERM' | 'KILL') => void
}): React.JSX.Element => {
const smallScreen = useMediaQuery(useTheme().breakpoints.only('xs'))
@@ -56,26 +58,24 @@ const ConsoleButtons = ({ stopStartServer }: {
>
)
- return smallScreen
- ? (
-
- {Buttons}
-
- )
- : (
-
- {Buttons}
-
- )
+ return smallScreen ? (
+
+ {Buttons}
+
+ ) : (
+
+ {Buttons}
+
+ )
}
export default ConsoleButtons
diff --git a/imports/dashboard/console/consoleView.tsx b/imports/dashboard/console/consoleView.tsx
index 44e8dd5..f017cf3 100644
--- a/imports/dashboard/console/consoleView.tsx
+++ b/imports/dashboard/console/consoleView.tsx
@@ -2,39 +2,43 @@ import React, { useRef, useLayoutEffect } from 'react'
import Typography from '@mui/material/Typography'
import styled from '@emotion/styled'
-let chrome = false
-try {
- if (
- Object.hasOwnProperty.call(window, 'chrome') &&
- !navigator.userAgent.includes('Trident') &&
- !navigator.userAgent.includes('Edge') // Chromium Edge uses Edg *sad noises*
- ) chrome = true
-} catch (e) {}
+const chrome =
+ Object.hasOwnProperty.call(window, 'chrome') &&
+ typeof navigator === 'object' &&
+ typeof navigator.userAgent === 'string' &&
+ !navigator.userAgent.includes('Trident') &&
+ !navigator.userAgent.includes('Edge') // Chromium Edge uses Edg *sad noises*
const ChromeConsoleViewContainer = styled.div({
height: '100%',
width: '100%',
overflow: 'auto',
display: 'flex',
- flexDirection: 'column-reverse'
+ flexDirection: 'column-reverse',
})
-const ChromeConsoleView = (props: { console: Array<{ id: number, text: string }> }): React.JSX.Element => (
+const ChromeConsoleView = (props: {
+ console: { id: number; text: string }[]
+}): React.JSX.Element => (
- {props.console.map((i) => (
- {i.text}
- )) /* Truncate to 650 lines due to performance issues afterwards. */}
+ {props.console.map(i => (
+
+ {i.text}
+
+
+ ))}
)
-const ConsoleView = (props: { console: Array<{ id: number, text: string }> }): React.JSX.Element => {
+const ConsoleView = (props: { console: { id: number; text: string }[] }): React.JSX.Element => {
const ref = useRef(null)
- const isScrolledToBottom = ref.current !== null
- ? ref.current.scrollHeight - ref.current.clientHeight <= ref.current.scrollTop + 1
- : false
+ const isScrolledToBottom =
+ ref.current !== null
+ ? ref.current.scrollHeight - ref.current.clientHeight <= ref.current.scrollTop + 1
+ : false
useLayoutEffect(() => {
if (ref.current) ref.current.scrollTop = ref.current.scrollHeight - ref.current.clientHeight
@@ -47,10 +51,17 @@ const ConsoleView = (props: { console: Array<{ id: number, text: string }> }): R
return (
-
- {props.console.map((i) => (
- {i.text}
- )) /* Truncate to 650 lines due to performance issues afterwards. */}
+
+ {props.console.map(i => (
+
+ {i.text}
+
+
+ ))}
diff --git a/imports/dashboard/dashboardLayout.tsx b/imports/dashboard/dashboardLayout.tsx
index 6b16ba9..6c3a4b8 100644
--- a/imports/dashboard/dashboardLayout.tsx
+++ b/imports/dashboard/dashboardLayout.tsx
@@ -1,9 +1,18 @@
import React, { useState } from 'react'
import styled from '@emotion/styled'
import {
- Typography, IconButton, Drawer,
- List, ListItemButton, ListItemIcon, ListItemText,
- Divider, useMediaQuery, useTheme, Toolbar, Tooltip
+ Typography,
+ IconButton,
+ Drawer,
+ List,
+ ListItemButton,
+ ListItemIcon,
+ ListItemText,
+ Divider,
+ useMediaQuery,
+ useTheme,
+ Toolbar,
+ Tooltip,
} from '@mui/material'
import Apps from '@mui/icons-material/Apps'
import Login from '@mui/icons-material/Login'
@@ -14,20 +23,24 @@ import CallToAction from '@mui/icons-material/CallToAction'
import Settings from '@mui/icons-material/Settings'
import Storage from '@mui/icons-material/Storage'
-import { useRouter } from 'next/router'
import Layout from '../layout'
import config from '../config'
import UnstyledLink from '../helpers/unstyledLink'
+import useOctyneData from './useOctyneData'
const DashboardContainer = styled.div({
padding: 20,
flexDirection: 'column',
display: 'flex',
- flex: 1
+ flex: 1,
})
-const DrawerItem = (props: { icon: React.ReactElement, name: string, subUrl: string }): React.JSX.Element => {
- const { server, node } = useRouter().query
+const DrawerItem = (props: {
+ icon: React.ReactElement
+ name: string
+ subUrl: string
+}): React.JSX.Element => {
+ const { server, node } = useOctyneData()
const nodeUri = typeof node === 'string' ? `?node=${encodeURIComponent(node)}` : ''
return (
@@ -46,12 +59,14 @@ const onLogout = (): void => {
fetch(`${config.ip}/logout`, { headers: { Authorization: token ?? '' } }).catch(console.error)
}
-const DashboardLayout = (props: React.PropsWithChildren<{ loggedIn: boolean }>): React.JSX.Element => {
+const DashboardLayout = (
+ props: React.PropsWithChildren<{ loggedIn: boolean }>,
+): React.JSX.Element => {
const [openDrawer, setOpenDrawer] = useState(false)
const drawerVariant = useMediaQuery(useTheme().breakpoints.only('xs')) ? 'temporary' : 'permanent'
const appBarContent = (
<>
- {(props.loggedIn && drawerVariant === 'temporary') && (
+ {props.loggedIn && drawerVariant === 'temporary' && (
<>
):
>
)}
- Octyne
+
+ Octyne
+
{/* These are displayed unconditionally in case of individual node authentication failure. */}
-
+
+
+
-
+
+
+
diff --git a/imports/dashboard/files/editor.tsx b/imports/dashboard/files/editor.tsx
index 657961b..34ee8a9 100644
--- a/imports/dashboard/files/editor.tsx
+++ b/imports/dashboard/files/editor.tsx
@@ -19,27 +19,33 @@ const Editor = (props: {
const saveFile = (): void => {
setSaving(true)
- Promise.resolve(props.onSave(name, content)).then(() => setSaving(false), console.error)
+ Promise.resolve(props.onSave(name, content))
+ .then(() => setSaving(false))
+ .catch(console.error)
}
return (
<>
- {props.name
- ?
{name}
- : (
-
setName(e.target.value)}
- helperText={error
+ {props.name ? (
+
+ {name}
+
+ ) : (
+ setName(e.target.value)}
+ helperText={
+ error
? 'This file already exists! Go back and open the file directly or delete it.'
- : undefined}
- />
- )}
+ : undefined
+ }
+ />
+ )}
{props.name && (
@@ -69,7 +75,11 @@ const Editor = (props: {
Save
- {saving && (
)}
+ {saving && (
+
+
+
+ )}
>
)
}
diff --git a/imports/dashboard/files/fileList.tsx b/imports/dashboard/files/fileList.tsx
index 48128ba..7dd46f8 100644
--- a/imports/dashboard/files/fileList.tsx
+++ b/imports/dashboard/files/fileList.tsx
@@ -1,7 +1,14 @@
import React from 'react'
import {
- ListItem, ListItemButton, ListItemText, ListItemAvatar, Avatar, IconButton, Checkbox,
- useMediaQuery, type Theme
+ ListItem,
+ ListItemButton,
+ ListItemText,
+ ListItemAvatar,
+ Avatar,
+ IconButton,
+ Checkbox,
+ useMediaQuery,
+ type Theme,
} from '@mui/material'
import AutoSizer from 'react-virtualized-auto-sizer'
import { FixedSizeList, type ListChildComponentProps } from 'react-window'
@@ -15,12 +22,13 @@ import { joinPath } from './fileUtils'
const rtd = (num: number): number => Math.round(num * 100) / 100
const bytesToGb = (bytes: number): string => {
if (bytes < 1024) return `${bytes} bytes`
- else if (bytes < (1024 * 1024)) return `${rtd(bytes / 1024)} KB`
- else if (bytes < (1024 * 1024 * 1024)) return `${rtd(bytes / (1024 * 1024))} MB`
- else if (bytes < (1024 * 1024 * 1024 * 1024)) return `${rtd(bytes / (1024 * 1024 * 1024))} GB`
+ else if (bytes < 1024 * 1024) return `${rtd(bytes / 1024)} KB`
+ else if (bytes < 1024 * 1024 * 1024) return `${rtd(bytes / (1024 * 1024))} MB`
+ else if (bytes < 1024 * 1024 * 1024 * 1024) return `${rtd(bytes / (1024 * 1024 * 1024))} GB`
else return `${rtd(bytes / (1024 * 1024 * 1024 * 1024))} TB`
}
-const tsts = (ts: number): string => new Date(ts * 1000).toISOString().substring(0, 19).replace('T', ' ')
+const tsts = (ts: number): string =>
+ new Date(ts * 1000).toISOString().substring(0, 19).replace('T', ' ')
export interface File {
name: string
@@ -30,7 +38,16 @@ export interface File {
mimeType: string
}
-const FileListItem = ({ file, style, disabled, filesSelected, onItemClick, onCheck, openMenu, url }: {
+const FileListItem = ({
+ file,
+ style,
+ disabled,
+ filesSelected,
+ onItemClick,
+ onCheck,
+ openMenu,
+ url,
+}: {
file: File
url: string
disabled: boolean
@@ -48,7 +65,9 @@ const FileListItem = ({ file, style, disabled, filesSelected, onItemClick, onChe
secondaryAction={
openMenu(file.name, e.currentTarget)} size='large'
+ disabled={disabled}
+ onClick={e => openMenu(file.name, e.currentTarget)}
+ size='large'
>
@@ -61,21 +80,27 @@ const FileListItem = ({ file, style, disabled, filesSelected, onItemClick, onChe
}
>
-
+ {/* style={{ paddingRight: 96 }} */}
+
{file.folder ? : }
)
-const FileListItemRenderer = ({ index, data, style }: ListChildComponentProps): React.JSX.Element => {
- const { files, path, disabled, filesSelected, setFilesSelected, openMenu, onClick } = data as FileItemData
+const FileListItemRenderer = ({
+ index,
+ data,
+ style,
+}: ListChildComponentProps): React.JSX.Element => {
+ const { files, path, disabled, filesSelected, setFilesSelected, openMenu, onClick } =
+ data as FileItemData
const router = useRouter()
const file = files[index]
const selectItem = (): void => {
@@ -87,10 +112,10 @@ const FileListItemRenderer = ({ index, data, style }: ListChildComponentProps):
let lastSelectedFileIdx = files.findLastIndex(e => filesSelected.includes(e.name))
if (lastSelectedFileIdx === -1) lastSelectedFileIdx = 0 // If none found, select first item.
// Select all items between the current item and found item. If they're already selected, skip.
- const filesToSelect =
- files.slice(Math.min(lastSelectedFileIdx, index), Math.max(lastSelectedFileIdx, index) + 1)
- .map(e => e.name)
- .filter(e => !filesSelected.includes(e))
+ const filesToSelect = files
+ .slice(Math.min(lastSelectedFileIdx, index), Math.max(lastSelectedFileIdx, index) + 1)
+ .map(e => e.name)
+ .filter(e => !filesSelected.includes(e))
setFilesSelected([...filesSelected, ...filesToSelect])
}
const subpath = file.folder ? joinPath(path, file.name) : path
@@ -105,17 +130,21 @@ const FileListItemRenderer = ({ index, data, style }: ListChildComponentProps):
disabled={disabled}
openMenu={openMenu}
filesSelected={filesSelected}
- onItemClick={(e) => e.ctrlKey && !e.shiftKey && !e.metaKey && !e.altKey
- ? selectItem()
- : !e.ctrlKey && e.shiftKey && !e.metaKey && !e.altKey ? shiftClickItem() : onClick(file)}
- onCheck={(e) => e.shiftKey ? shiftClickItem() : selectItem()}
+ onItemClick={e =>
+ e.ctrlKey && !e.shiftKey && !e.metaKey && !e.altKey
+ ? selectItem()
+ : !e.ctrlKey && e.shiftKey && !e.metaKey && !e.altKey
+ ? shiftClickItem()
+ : onClick(file)
+ }
+ onCheck={e => (e.shiftKey ? shiftClickItem() : selectItem())}
url={`/dashboard/${router.query.server}/files${subpath}${params.size ? '?' : ''}${params}`}
/>
)
}
-interface FileItemData { /* eslint-disable react/no-unused-prop-types -- false positive */
- files: File[]
+interface FileItemData {
+ /* eslint-disable react/no-unused-prop-types -- false positive */ files: File[]
path: string
disabled: boolean
filesSelected: string[]
@@ -132,17 +161,27 @@ const FileList = (props: FileItemData): React.JSX.Element => {
const px100sm = useMediaQuery('(min-width:328px)')
const px120sm = useMediaQuery('(min-width:288px)')
const px140sm = useMediaQuery('(min-width:280px)')
- const itemSize = px60 ? 60 : (
- !smDisplay ? 80 : ( // Sidebar is hidden when smDisplay is true, use 60/80/100/120/140/160px.
- px60sm ? 60 : (px80sm ? 80 : (px100sm ? 100 : (px120sm ? 120 : px140sm ? 140 : 160)))
- )
- )
+ const itemSize = px60
+ ? 60
+ : !smDisplay
+ ? 80 // Sidebar is hidden when smDisplay is true, use 60/80/100/120/140/160px.
+ : px60sm
+ ? 60
+ : px80sm
+ ? 80
+ : px100sm
+ ? 100
+ : px120sm
+ ? 120
+ : px140sm
+ ? 140
+ : 160
const sortedList = props.files.sort((a, b) => {
const aName = a.name.toLowerCase()
const bName = b.name.toLowerCase()
if (a.folder && !b.folder) return -1
else if (!a.folder && b.folder) return 1
- else return aName === bName ? 0 : (aName > bName ? 1 : -1)
+ else return aName === bName ? 0 : aName > bName ? 1 : -1
})
return (
@@ -159,9 +198,13 @@ const FileList = (props: FileItemData): React.JSX.Element => {
>
{FileListItemRenderer}
- )}
+ )}
- ) : }
+ ) : (
+
+
+
+ )}
)
}
diff --git a/imports/dashboard/files/fileManager.tsx b/imports/dashboard/files/fileManager.tsx
index 750caa1..e53caaf 100644
--- a/imports/dashboard/files/fileManager.tsx
+++ b/imports/dashboard/files/fileManager.tsx
@@ -2,8 +2,18 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'
import { useRouter } from 'next/router'
import {
- Paper, Typography, CircularProgress, IconButton, Divider, Tooltip, Menu, MenuItem, Slide,
- Snackbar, Button, TextField
+ Paper,
+ Typography,
+ CircularProgress,
+ IconButton,
+ Divider,
+ Tooltip,
+ Menu,
+ MenuItem,
+ Slide,
+ Snackbar,
+ Button,
+ TextField,
} from '@mui/material'
import Add from '@mui/icons-material/Add'
import Replay from '@mui/icons-material/Replay'
@@ -28,8 +38,8 @@ import MassActionDialog from './massActionDialog'
import ModifyFileDialog from './modifyFileDialog'
import FolderCreationDialog from './folderCreationDialog'
-let euc: (uriComponent: string | number | boolean) => string
-try { euc = encodeURIComponent } catch (e) { euc = e => e.toString() }
+const euc: (uriComponent: string | number | boolean) => string =
+ typeof encodeURIComponent === 'function' ? encodeURIComponent : e => e.toString()
const editorExts = ['properties', 'json', 'yaml', 'yml', 'xml', 'js', 'log', 'sh', 'txt']
const FileManager = (props: {
@@ -49,48 +59,54 @@ const FileManager = (props: {
const [search, setSearch] = useState(null)
const [searchApplies, setSearchApplies] = useState(true)
- const [overlay, setOverlay] = useState('')
+ const [overlay, setOverlay] = useState('')
const [message, setMessage] = useState('')
const [fetching, setFetching] = useState(true)
- const [error, setError] = useState(null)
+ const [error, setError] = // prettier-ignore
+ useState(null)
const [files, setFiles] = useState(null)
const [filesSelected, setFilesSelected] = useState([])
- const [file, setFile] = useState<{ name: string, content: string } | null>(null)
+ const [file, setFile] = useState<{ name: string; content: string } | null>(null)
const [download, setDownload] = useState('')
const [folderPromptOpen, setFolderPromptOpen] = useState(false)
const [massActionMenuOpen, setMassActionMenuOpen] = useState(null)
- const [modifyFileDialogOpen, setModifyFileDialogOpen] = useState<'' | 'move' | 'copy' | 'rename'>('')
- const [massActionDialogOpen, setMassActionDialogOpen] = useState<'move' | 'copy' | 'compress' | false>(false)
+ const [modifyFileDialogOpen, setModifyFileDialogOpen] = // prettier-ignore
+ useState<'' | 'move' | 'copy' | 'rename'>('')
+ const [massActionDialogOpen, setMassActionDialogOpen] = // prettier-ignore
+ useState<'move' | 'copy' | 'compress' | false>(false)
const searchRef = useRef(null)
// Update path when URL changes. Requires normalised path.
- const updatePath = useCallback((newPath: string, file?: string, replace?: boolean) => {
- const route = {
- pathname: '/dashboard/[server]/files/[[...path]]',
- query: { ...router.query }
- }
- const as = {
- pathname: `/dashboard/${server}/files${newPath}`,
- query: { ...router.query }
- }
- delete route.query.server
- delete route.query.path
- delete route.query.file
- delete as.query.server
- delete as.query.path
- delete as.query.file
- if (file !== undefined) {
- route.query.file = file
- as.query.file = file
- }
- (replace ? router.replace : router.push)(route, as, { shallow: true })
- // Apply search only when search has been focused once or if you are just downloading files.
- .then(() => setSearchApplies(file !== router.query.file))
- .catch(console.error)
- }, [router, server])
+ const updatePath = useCallback(
+ (newPath: string, file?: string, replace?: boolean) => {
+ const route = {
+ pathname: '/dashboard/[server]/files/[[...path]]',
+ query: { ...router.query },
+ }
+ const as = {
+ pathname: `/dashboard/${server}/files${newPath}`,
+ query: { ...router.query },
+ }
+ delete route.query.server
+ delete route.query.path
+ delete route.query.file
+ delete as.query.server
+ delete as.query.path
+ delete as.query.file
+ if (file !== undefined) {
+ route.query.file = file
+ as.query.file = file
+ }
+ ;(replace ? router.replace : router.push)(route, as, { shallow: true })
+ // Apply search only when search has been focused once or if you are just downloading files.
+ .then(() => setSearchApplies(file !== router.query.file))
+ .catch(console.error)
+ },
+ [router, server],
+ )
// Used to fetch files.
const { setAuthenticated, setServerExists } = props
@@ -98,24 +114,39 @@ const FileManager = (props: {
;(async () => {
setFetching(true) // TODO: Make it show up after 1.0 seconds.
setError(null)
- const files = await ky.get(`server/${server}/files?path=${euc(path)}`)
- .json<{ error?: string, contents: File[] }>()
+ const files = await ky
+ .get(`server/${server}/files?path=${euc(path)}`)
+ .json<{ error?: string; contents: File[] }>()
if (files.error === 'This server does not exist!') setServerExists(false)
- else if (files.error === 'You are not authenticated to access this resource!') setAuthenticated(false)
- else if (files.error === 'The folder requested is outside the server!') setError('outsideServerDir')
+ else if (files.error === 'You are not authenticated to access this resource!')
+ setAuthenticated(false)
+ else if (files.error === 'The folder requested is outside the server!')
+ setError('outsideServerDir')
else if (files.error === 'This folder does not exist!') setError('folderNotExist')
else if (files.error === 'This is not a folder!') {
- return updatePath(parentPath(path), path.substring(0, path.length - 1).split('/').pop(), true)
+ return updatePath(
+ parentPath(path),
+ path
+ .substring(0, path.length - 1)
+ .split('/')
+ .pop(),
+ true,
+ )
} else if (files) {
setFiles(files.contents)
setFilesSelected([])
}
setFetching(false)
- })().catch(e => { console.error(e); setMessage(`Failed to fetch files: ${e.message}`); setFetching(false) })
+ })().catch(e => {
+ console.error(e)
+ setMessage(`Failed to fetch files: ${e.message}`)
+ setFetching(false)
+ })
}, [path, ky, server, updatePath, setAuthenticated, setServerExists])
const prevPath = useRef(path)
- useEffect(() => { // Fetch files.
+ // Fetch files.
+ useEffect(() => {
if (server && (path !== prevPath.current || files === null)) {
fetchFiles()
}
@@ -129,7 +160,7 @@ const FileManager = (props: {
e.preventDefault()
if (searchRef.current !== document.activeElement) searchRef.current?.focus()
else searchRef.current?.setSelectionRange(0, searchRef.current.value.length)
- setSearch(search => typeof search === 'string' ? search : '')
+ setSearch(search => (typeof search === 'string' ? search : ''))
} else if (e.code === 'Escape') {
e.preventDefault()
setSearch(null)
@@ -139,24 +170,27 @@ const FileManager = (props: {
return () => window.removeEventListener('keydown', eventListener)
}, [file])
- const loadFileInEditor = useCallback(async (filename: string) => {
- setFetching(true)
- const req = await ky.get(`server/${server}/file?path=${euc(joinPath(path, filename))}`)
- if (req.status !== 200) {
- setMessage((await req.json<{ error: string }>()).error)
+ const loadFileInEditor = useCallback(
+ async (filename: string) => {
+ setFetching(true)
+ const req = await ky.get(`server/${server}/file?path=${euc(joinPath(path, filename))}`)
+ if (req.status !== 200) {
+ setMessage((await req.json<{ error: string }>()).error)
+ setFetching(false)
+ updatePath(path) // Remove file from path.
+ return
+ }
+ const content = await req.text()
+ setFile({ name: filename, content })
setFetching(false)
- updatePath(path) // Remove file from path.
- return
- }
- const content = await req.text()
- setFile({ name: filename, content })
- setFetching(false)
- }, [ky, path, server, updatePath])
+ },
+ [ky, path, server, updatePath],
+ )
// Load any file in path.
useEffect(() => {
// Remove any file if the path loses ?file, so going back closes the editor.
- if (file?.name !== undefined && filename !== file?.name) setFile(null)
+ if (file?.name !== undefined && filename !== file.name) setFile(null)
if (download && filename !== download) setDownload('')
if (filename === '') return setFile({ name: '', content: '' }) // Create a new empty file.
if (filename === undefined) return // No file defined, do nothing.
@@ -186,7 +220,7 @@ const FileManager = (props: {
setFetching(true)
try {
const endpoint = `server/${server}/folder?path=/${euc(joinPath(path, name))}`
- const createFolder = await ky.post(endpoint).json<{ success: boolean, error: string }>()
+ const createFolder = await ky.post(endpoint).json<{ success: boolean; error: string }>()
if (createFolder.success) fetchFiles()
else setMessage(createFolder.error)
setFetching(false)
@@ -195,7 +229,10 @@ const FileManager = (props: {
setFetching(false)
}
}
- const handleModifyFile = async (newPath: string, action: 'move' | 'copy' | 'rename'): Promise => {
+ const handleModifyFile = async (
+ newPath: string,
+ action: 'move' | 'copy' | 'rename',
+ ): Promise => {
setModifyFileDialogOpen('')
setMenuOpen('')
setAnchorEl(null)
@@ -207,9 +244,11 @@ const FileManager = (props: {
}
const target = action === 'rename' ? path + newPath : newPath
try {
- const editFile = await ky.patch(`server/${server}/file`, {
- body: `${action === 'copy' ? 'cp' : 'mv'}\n${path}${menuOpen}\n${target}`
- }).json<{ success: boolean, error: string }>()
+ const editFile = await ky
+ .patch(`server/${server}/file`, {
+ body: `${action === 'copy' ? 'cp' : 'mv'}\n${path}${menuOpen}\n${target}`,
+ })
+ .json<{ success: boolean; error: string }>()
if (editFile.success) fetchFiles()
else setMessage(editFile.error)
setFetching(false)
@@ -230,7 +269,7 @@ const FileManager = (props: {
fetchFiles()
return
} else if (res.status !== 404) {
- const errors = await res.json<{ errors?: Array<{ index: number, message: string }> }>()
+ const errors = await res.json<{ errors?: { index: number; message: string }[] }>()
if (errors.errors?.length === 1) {
setMessage(`Error deleting files: ${errors.errors[0].message}`)
setOverlay('')
@@ -256,21 +295,32 @@ const FileManager = (props: {
for (const file of filesSelected) {
// setOverlay('Deleting ' + file)
// Save the file.
- ops.push(ky.delete(`server/${server}/file?path=${euc(path + file)}`).then(async r => {
- if (r.status !== 200) {
- setMessage(`Error deleting ${file}\n${(await r.json<{ error: string }>()).error}`)
- } else {
- const progress = (filesSelected.length - total) * 100 / filesSelected.length
- setOverlay({ text: `Deleting ${--total} out of ${filesSelected.length} files.`, progress })
- }
- if (localStorage.getItem('ecthelion:logAsyncMassActions')) console.log('Deleted ' + file)
- }).catch(e => setMessage(`Error deleting ${file}\n${e}`)))
+ ops.push(
+ ky
+ .delete(`server/${server}/file?path=${euc(path + file)}`)
+ .then(async r => {
+ if (r.status !== 200) {
+ setMessage(`Error deleting ${file}\n${(await r.json<{ error: string }>()).error}`)
+ } else {
+ const progress = ((filesSelected.length - total) * 100) / filesSelected.length
+ setOverlay({
+ text: `Deleting ${--total} out of ${filesSelected.length} files.`,
+ progress,
+ })
+ }
+ if (localStorage.getItem('ecthelion:logAsyncMassActions'))
+ console.log('Deleted ' + file)
+ })
+ .catch(e => setMessage(`Error deleting ${file}\n${e}`)),
+ )
}
- Promise.allSettled(ops).then(() => {
- setMessage('Deleted all files successfully!')
- setOverlay('')
- fetchFiles()
- }).catch(console.error) // Should not be called, ideally.
+ Promise.allSettled(ops)
+ .then(() => {
+ setMessage('Deleted all files successfully!')
+ setOverlay('')
+ fetchFiles()
+ })
+ .catch(console.error) // Should not be called, ideally.
}
const handleFilesUpload = (files: FileList): void => {
if (overlay) return // TODO: Allow multiple file uploads/mass actions simultaneously in future.
@@ -280,9 +330,13 @@ const FileManager = (props: {
// Save the file.
const formData = new FormData()
formData.append('upload', file, file.name)
- const r = await uploadFormData(`${ip}/server/${server}/file?path=${euc(path)}`, formData, progress => {
- setOverlay({ text: `Uploading ${file.name} to ${path}`, progress: progress * 100 })
- })
+ const r = await uploadFormData(
+ `${ip}/server/${server}/file?path=${euc(path)}`,
+ formData,
+ progress => {
+ setOverlay({ text: `Uploading ${file.name} to ${path}`, progress: progress * 100 })
+ },
+ )
if (r.status !== 200) {
setMessage(`Error uploading ${file.name}\n${JSON.parse(r.body).error}`)
}
@@ -290,45 +344,62 @@ const FileManager = (props: {
}
setMessage('Uploaded all files successfully!')
if (path === prevPath.current) fetchFiles() // prevPath is current path after useEffect call.
- })().catch(e => { console.error(e); setOverlay(''); setMessage(`Failed to upload files: ${e.message}`) })
+ })().catch(e => {
+ console.error(e)
+ setOverlay('')
+ setMessage(`Failed to upload files: ${e.message}`)
+ })
}
// Single file logic.
const handleDeleteMenuButton = (): void => {
;(async () => {
setMenuOpen('')
setFetching(true)
- const a = await ky.delete(`server/${server}/file?path=${euc(path + menuOpen)}`)
+ const a = await ky
+ .delete(`server/${server}/file?path=${euc(path + menuOpen)}`)
.json<{ error: string }>()
if (a.error) setMessage(a.error)
setFetching(false)
setMenuOpen('')
fetchFiles()
- })().catch(e => { console.error(e); setMessage(`Failed to delete file: ${e.message}`) })
+ })().catch(e => {
+ console.error(e)
+ setMessage(`Failed to delete file: ${e.message}`)
+ })
}
const handleDownloadMenuButton = (): void => {
;(async () => {
setMenuOpen('')
const ticket = encodeURIComponent((await ky.get('ott').json<{ ticket: string }>()).ticket)
window.location.href = `${ip}/server/${server}/file?ticket=${ticket}&path=${path}${menuOpen}`
- })().catch(e => { console.error(e); setMessage(`Failed to download file: ${e.message}`) })
+ })().catch(e => {
+ console.error(e)
+ setMessage(`Failed to download file: ${e.message}`)
+ })
}
const handleDecompressMenuButton = (): void => {
;(async () => {
setMenuOpen('')
setFetching(true)
- const a = await ky.post(`server/${server}/decompress?path=${euc(path + menuOpen)}`, {
- body: path + menuOpen.replace(archiveRegex, '')
- })
+ const a = await ky
+ .post(`server/${server}/decompress?path=${euc(path + menuOpen)}`, {
+ body: path + menuOpen.replace(archiveRegex, ''),
+ })
.json<{ error: string }>()
- if (a.error?.includes('ZIP file') && !menuOpen.endsWith('.zip')) {
+ if (a.error.includes('ZIP file') && !menuOpen.endsWith('.zip')) {
setMessage('Archive failed to decompress! Update Octyne to v1.2+ to decompress this file.')
} else if (a.error) setMessage(a.error)
setFetching(false)
setMenuOpen('')
fetchFiles()
- })().catch(e => { console.error(e); setMessage(`Failed to decompress file: ${e.message}`) })
+ })().catch(e => {
+ console.error(e)
+ setMessage(`Failed to decompress file: ${e.message}`)
+ })
+ }
+ const handleCloseDownload = (): void => {
+ updatePath(path)
}
- const handleCloseDownload = (): void => { updatePath(path) }
const handleDownloadButton = (): void => {
;(async () => {
handleCloseDownload()
@@ -336,7 +407,10 @@ const FileManager = (props: {
const ticket = encodeURIComponent((await ky.get('ott').json<{ ticket: string }>()).ticket)
const loc = `${ip}/server/${server}/file?ticket=${ticket}&path=${euc(joinPath(path, download))}`
window.location.href = loc
- })().catch((e: any) => { console.error(e); setMessage(`Failed to download file: ${e.message}`) })
+ })().catch((e: any) => {
+ console.error(e)
+ setMessage(`Failed to download file: ${e.message}`)
+ })
}
const handleSaveFile = async (name: string, content: string): Promise => {
try {
@@ -346,28 +420,34 @@ const FileManager = (props: {
const r = await ky.post(`server/${server}/file?path=${encodedPath}`, { body: formData })
if (r.status !== 200) setMessage((await r.json<{ error: string }>()).error)
else setMessage('Saved successfully!')
- } catch (e: any) { setMessage(`Error saving file! ${e}`); console.error(e) }
+ } catch (e: any) {
+ setMessage(`Error saving file! ${e}`)
+ console.error(e)
+ }
}
const selectedFile = menuOpen && files?.find(e => e.name === menuOpen)
- const titleName = file?.name ? file.name + ' - ' : (path ? path + ' - ' : '')
+ const titleName = file?.name ? file.name + ' - ' : path ? path + ' - ' : ''
const alternativeDisplay = !error ? (
- !files || !server ? : null
+ !files || !server ? (
+
+ ) : null
) : (
- {error === 'folderNotExist'
- ? `The folder you are trying to access (${path}) does not exist.`
- : error === 'outsideServerDir'
- ? `The path you are trying to access (${path}) is outside the server folder!`
- : `The path you are trying to access (${path}) is a file!`}
+
+ {error === 'folderNotExist'
+ ? `The folder you are trying to access (${path}) does not exist.`
+ : error === 'outsideServerDir'
+ ? `The path you are trying to access (${path}) is outside the server folder!`
+ : `The path you are trying to access (${path}) is a file!`}
{path !== '/' && (
(
+ onClick={() =>
!fetching &&
updatePath(error === 'outsideServerDir' ? '/' : parentPath(path), undefined, true)
- )}
+ }
>
{error === 'outsideServerDir' ? 'Go to root folder?' : 'Try going up one path?'}
@@ -381,148 +461,168 @@ const FileManager = (props: {
description='The files of a process running on Octyne.'
url={`/dashboard/${server}/files`}
/>
- {!files || alternativeDisplay ? alternativeDisplay : (
- file !== null ? (
-
- e.name)}
- onSave={handleSaveFile}
- onClose={() => { updatePath(path); fetchFiles() }}
- onDownload={() => {
- ;(async () => {
- const ott = encodeURIComponent((await ky.get('ott').json<{ ticket: string }>()).ticket)
- window.location.href = `${ip}/server/${server}/file?path=${path}${file.name}&ticket=${ott}`
- })().catch(e => { console.error(e); setMessage(`Failed to download file: ${e.message}`) })
- }}
- />
-
- ) : (
- {
- e.stopPropagation()
- e.preventDefault()
- e.dataTransfer.dropEffect = 'copy'
- }} onDrop={e => {
- e.stopPropagation()
- e.preventDefault()
- handleFilesUpload(e.dataTransfer.files)
+ {!files || alternativeDisplay ? (
+ alternativeDisplay
+ ) : file !== null ? (
+
+ e.name)}
+ onSave={handleSaveFile}
+ onClose={() => {
+ updatePath(path)
+ fetchFiles()
}}
- >
- Files - {server}
-
- {path !== '/' && (
-
updatePath(parentPath(path))}>
-
+ onDownload={() => {
+ ;(async () => {
+ const ott = encodeURIComponent(
+ (await ky.get('ott').json<{ ticket: string }>()).ticket,
+ )
+ window.location.href = `${ip}/server/${server}/file?path=${path}${file.name}&ticket=${ott}`
+ })().catch(e => {
+ console.error(e)
+ setMessage(`Failed to download file: ${e.message}`)
+ })
+ }}
+ />
+
+ ) : (
+ {
+ e.stopPropagation()
+ e.preventDefault()
+ e.dataTransfer.dropEffect = 'copy'
+ }}
+ onDrop={e => {
+ e.stopPropagation()
+ e.preventDefault()
+ handleFilesUpload(e.dataTransfer.files)
+ }}
+ >
+
+ Files - {server}
+
+
+ {path !== '/' && (
+
updatePath(parentPath(path))}>
+
+
+ )}
+
+
{path}
+
+ {filesSelected.length > 0 && (
+ <>
+
+
+ setMassActionMenuOpen(e.currentTarget)}
+ >
+
+
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+ setSearch(s => (typeof s === 'string' ? null : ''))}>
+
+
+
+
+
+
+
+ setFolderPromptOpen(true)}>
+
+
+
+
+
+
+
+ updatePath(path, '')}>
+
- )}
-
- {path}
-
- {filesSelected.length > 0 && (
- <>
-
-
- setMassActionMenuOpen(e.currentTarget)}>
-
-
-
-
-
- >
- )}
-
-
-
-
-
-
-
-
-
- setSearch(s => typeof s === 'string' ? null : '')}>
-
-
-
-
-
-
-
- setFolderPromptOpen(true)}>
-
-
-
-
-
-
-
- updatePath(path, '')}>
-
-
-
-
-
-
- {fetching && (
- <>>
- )}
-
-
- {typeof search === 'string' && (
- setSearch(e.target.value)}
- onFocus={() => setSearchApplies(true)}
- />
+
+
+
+
+ {fetching && (
+ <>
+
+
+ >
)}
- {search === null && }
-
- {/* List of files and folders. */}
- (
- typeof search === 'string'
- ? e.name.toLowerCase().includes(search.toLowerCase()) || !searchApplies
- : true
- ))}
- disabled={fetching}
- filesSelected={filesSelected}
- setFilesSelected={setFilesSelected}
- onClick={(file) => {
- if (file.folder) updatePath(joinPath(path, file.name))
- else updatePath(path, file.name)
- }}
- openMenu={(fn, anchor) => {
- setMenuOpen(fn)
- setAnchorEl(anchor)
- }}
+
+
+ {typeof search === 'string' && (
+ setSearch(e.target.value)}
+ onFocus={() => setSearchApplies(true)}
/>
-
- )
+ )}
+ {search === null && }
+
+ {/* List of files and folders. */}
+
+ typeof search === 'string'
+ ? e.name.toLowerCase().includes(search.toLowerCase()) || !searchApplies
+ : true,
+ )}
+ disabled={fetching}
+ filesSelected={filesSelected}
+ setFilesSelected={setFilesSelected}
+ onClick={file => {
+ if (file.folder) updatePath(joinPath(path, file.name))
+ else updatePath(path, file.name)
+ }}
+ openMenu={(fn, anchor) => {
+ setMenuOpen(fn)
+ setAnchorEl(anchor)
+ }}
+ />
+
)}
{download && (
}
+ TransitionComponent={props => }
onClose={handleCloseDownload}
message={`Do you want to download '${download}'?`}
action={[
,
- ,
]}
/>
)}
@@ -537,7 +637,7 @@ const FileManager = (props: {
filename={menuOpen}
operation={modifyFileDialogOpen}
handleClose={() => setModifyFileDialogOpen('')}
- handleEdit={async (path) => await handleModifyFile(path, modifyFileDialogOpen)}
+ handleEdit={async path => await handleModifyFile(path, modifyFileDialogOpen)}
/>
)}
{massActionDialogOpen && (
@@ -557,11 +657,29 @@ const FileManager = (props: {
/>
)}
{massActionMenuOpen && (
-
diff --git a/imports/errors/connectionFailure.tsx b/imports/errors/connectionFailure.tsx
index 09538bc..2773469 100644
--- a/imports/errors/connectionFailure.tsx
+++ b/imports/errors/connectionFailure.tsx
@@ -1,19 +1,32 @@
import React from 'react'
import { Paper, Typography, LinearProgress } from '@mui/material'
-export const ConnectionFailure = (props: { loading: boolean, title?: string }): React.JSX.Element => (
+export const ConnectionFailure = (props: {
+ loading: boolean
+ title?: string
+}): React.JSX.Element => (
{props.loading ? (
- {props.title && {props.title}}
+ {props.title && (
+
+ {props.title}
+
+ )}
Loading...
) : (
<>
- {props.title && {props.title}}
+ {props.title && (
+
+ {props.title}
+
+ )}
Looks like we can't connect to the server. Oops!
- Check if the server is online and the dashboard configured correctly.
+
+ Check if the server is online and the dashboard configured correctly.
+
>
)}
diff --git a/imports/helpers/message.tsx b/imports/helpers/message.tsx
index 3a12ddf..4fa3cc7 100644
--- a/imports/helpers/message.tsx
+++ b/imports/helpers/message.tsx
@@ -2,7 +2,10 @@ import React from 'react'
import { Snackbar, Button, IconButton } from '@mui/material'
import Close from '@mui/icons-material/Close'
-const Message = ({ message, setMessage }: {
+const Message = ({
+ message,
+ setMessage,
+}: {
message: string
setMessage: (a: string) => void
}): React.JSX.Element => (
@@ -19,7 +22,7 @@ const Message = ({ message, setMessage }: {
,
setMessage('')}>
-
+ ,
]}
/>
)
diff --git a/imports/helpers/title.tsx b/imports/helpers/title.tsx
index d5f3ab3..828e1fe 100644
--- a/imports/helpers/title.tsx
+++ b/imports/helpers/title.tsx
@@ -1,7 +1,12 @@
import React from 'react'
import Head from 'next/head'
-const Title = ({ title, description, url, index }: {
+const Title = ({
+ title,
+ description,
+ url,
+ index,
+}: {
title: string
description: string
url: string
diff --git a/imports/helpers/unstyledLink.tsx b/imports/helpers/unstyledLink.tsx
index 59efade..449ce31 100644
--- a/imports/helpers/unstyledLink.tsx
+++ b/imports/helpers/unstyledLink.tsx
@@ -1,13 +1,13 @@
import React from 'react'
import Link, { type LinkProps } from 'next/link'
-const UnstyledLink = (props: Omit, keyof LinkProps> & LinkProps & {
- children?: React.ReactNode
-} & React.RefAttributes): React.JSX.Element => (
-
+const UnstyledLink = (
+ props: Omit, keyof LinkProps> &
+ LinkProps &
+ React.PropsWithChildren &
+ React.RefAttributes,
+): React.JSX.Element => (
+
)
export default UnstyledLink
diff --git a/imports/helpers/useInterval.ts b/imports/helpers/useInterval.ts
index c64329a..06f3320 100644
--- a/imports/helpers/useInterval.ts
+++ b/imports/helpers/useInterval.ts
@@ -1,17 +1,17 @@
import { useEffect, useRef } from 'react'
// https://overreacted.io/making-setinterval-declarative-with-react-hooks/
-const useInterval = (callback: (...args: any[]) => void, delay: number): void => {
+const useInterval = (callback: () => void, delay: number): void => {
const savedCallback = useRef<() => void>(null)
// Remember the latest callback.
- useEffect(() => { savedCallback.current = callback }, [callback])
+ useEffect(() => {
+ savedCallback.current = callback
+ }, [callback])
// Set up the interval.
useEffect(() => {
const tick = (): void => savedCallback.current?.()
- if (delay !== null) {
- const id = setInterval(tick, delay)
- return () => clearInterval(id)
- }
+ const id = setInterval(tick, delay)
+ return () => clearInterval(id)
}, [delay])
}
diff --git a/imports/helpers/useKy.ts b/imports/helpers/useKy.ts
index 83e8b3b..d518c8f 100644
--- a/imports/helpers/useKy.ts
+++ b/imports/helpers/useKy.ts
@@ -10,9 +10,9 @@ const defaultKy = ky.create({
prefixUrl: config.ip,
hooks: {
beforeRequest: [
- request => request.headers.set('Authorization', localStorage.getItem('ecthelion:token') ?? '')
- ]
- }
+ req => req.headers.set('Authorization', localStorage.getItem('ecthelion:token') ?? ''),
+ ],
+ },
})
const KyContext = React.createContext({
@@ -20,10 +20,10 @@ const KyContext = React.createContext({
nodes: Object.keys(nodes).reduce>((obj, node) => {
obj[node] = defaultKy.extend({ prefixUrl: nodes[node] })
return obj
- }, {})
+ }, {}),
})
-export default function useKy (node?: string): KyInstance {
+export default function useKy(node?: string): KyInstance {
const kyContext = React.useContext(KyContext)
return node ? kyContext.nodes[node] : kyContext.default
}
diff --git a/imports/layout.tsx b/imports/layout.tsx
index b196dd8..5a9f816 100644
--- a/imports/layout.tsx
+++ b/imports/layout.tsx
@@ -8,14 +8,16 @@ const LayoutContainer = styled.div({
width: '100vw',
maxWidth: '100%',
display: 'flex',
- flexDirection: 'column'
+ flexDirection: 'column',
// minWidth: '100%'
})
-const Layout = (props: React.PropsWithChildren<{
- appBar?: React.ReactNode
- removeToolbar?: boolean
-}>): React.JSX.Element => (
+const Layout = (
+ props: React.PropsWithChildren<{
+ appBar?: React.ReactNode
+ removeToolbar?: boolean
+ }>,
+): React.JSX.Element => (
{/* */}
theme.zIndex.drawer + 1 */ } }}>
-
- {props.appBar}
-
+ {props.appBar}
{props.removeToolbar ? '' : }
{props.children}
diff --git a/imports/servers/commandDialog.tsx b/imports/servers/commandDialog.tsx
index ffe9b9a..33eddf1 100644
--- a/imports/servers/commandDialog.tsx
+++ b/imports/servers/commandDialog.tsx
@@ -1,9 +1,19 @@
import React, { useState } from 'react'
import {
- Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button, TextField
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogContentText,
+ DialogActions,
+ Button,
+ TextField,
} from '@mui/material'
-const CommandDialog = ({ server, handleClose, runCommand }: {
+const CommandDialog = ({
+ server,
+ handleClose,
+ runCommand,
+}: {
server: string
handleClose: () => void
runCommand: (command: string) => void
@@ -28,7 +38,9 @@ const CommandDialog = ({ server, handleClose, runCommand }: {
Cancel
- runCommand(command)} color='primary'>Run
+ runCommand(command)} color='primary'>
+ Run
+
)
diff --git a/imports/servers/serverList.tsx b/imports/servers/serverList.tsx
index 25c1dc5..d0436cd 100644
--- a/imports/servers/serverList.tsx
+++ b/imports/servers/serverList.tsx
@@ -12,7 +12,12 @@ export interface ExtraServerInfo {
toDelete: boolean
}
-const ServerList = ({ ip, node, setMessage, setFailure }: {
+const ServerList = ({
+ ip,
+ node,
+ setMessage,
+ setFailure,
+}: {
ip: string
node?: string
setMessage: React.Dispatch>
@@ -27,21 +32,27 @@ const ServerList = ({ ip, node, setMessage, setFailure }: {
// null - not yet fetched.
const [loggedIn, setLoggedInDirect] = useState(null)
- const setLoggedIn = useCallback((newLoggedIn: typeof loggedIn) => {
- if (setFailure && newLoggedIn === 'failed') setFailure('failed')
- else if (setFailure && newLoggedIn === false) setFailure('logged out')
- setLoggedInDirect(newLoggedIn)
- }, [setLoggedInDirect, setFailure])
+ const setLoggedIn = useCallback(
+ (newLoggedIn: typeof loggedIn) => {
+ if (setFailure && newLoggedIn === 'failed') setFailure('failed')
+ else if (setFailure && newLoggedIn === false) setFailure('logged out')
+ setLoggedInDirect(newLoggedIn)
+ },
+ [setLoggedInDirect, setFailure],
+ )
const refetch = useCallback(() => {
- (async () => {
+ ;(async () => {
const req = await ky.get('servers?extrainfo=true')
if (req.ok) {
setServers((await req.json<{ servers: typeof servers }>()).servers)
setLoggedIn(true)
} else if (req.status === 401) setLoggedIn(false)
else setLoggedIn('failed')
- })().catch(e => { console.error(e); setLoggedIn('failed') })
+ })().catch((e: unknown) => {
+ console.error(e)
+ setLoggedIn('failed')
+ })
}, [ky, setLoggedIn, setServers])
useEffect(refetch, [refetch])
@@ -52,53 +63,63 @@ const ServerList = ({ ip, node, setMessage, setFailure }: {
;(async () => {
const ott = encodeURIComponent((await ky.get('ott').json<{ ticket: string }>()).ticket)
// document.cookie = `X-Authentication=${localStorage.getItem('ecthelion:token')}`
- const ws = new WebSocket(`${ip.split('http').join('ws')}/server/${server}/console?ticket=${ott}`)
+ const ws = new WebSocket(`${ip.replace('http', 'ws')}/server/${server}/console?ticket=${ott}`)
ws.onopen = () => {
ws.send(command)
ws.close()
handleClose()
}
ws.onerror = () => setMessage('Failed to send command!')
- })().catch((e: any) => { console.error(e); setMessage('Failed to send command!') })
+ })().catch((e: unknown) => {
+ console.error(e)
+ setMessage('Failed to send command!')
+ })
}
const stopStartServer = (operation: 'START' | 'KILL' | 'TERM', server: string): void => {
;(async () => {
// Send the request to stop or start the server.
const res = await ky.post('server/' + server, {
- body: operation === 'KILL' ? 'STOP' : operation // Octyne 1.0 compatibility.
+ body: operation === 'KILL' ? 'STOP' : operation, // Octyne 1.0 compatibility.
})
if (res.status === 400) {
const json = await res.json<{ error: string }>()
- setMessage(json.error === 'Invalid operation requested!' && operation === 'TERM'
- ? 'Gracefully stopping apps requires Octyne 1.1 or newer!'
- : json.error)
+ setMessage(
+ json.error === 'Invalid operation requested!' && operation === 'TERM'
+ ? 'Gracefully stopping apps requires Octyne 1.1 or newer!'
+ : json.error,
+ )
}
- })().catch((e: any) => { console.error(e); setMessage(e as string) })
+ })().catch((e: unknown) => {
+ console.error(e)
+ setMessage('Failed to stop/start/kill server!')
+ })
}
if (loggedIn === null || loggedIn === 'failed') {
return (
-
+
)
} else if (!loggedIn) {
return (
Unable to authenticate with Octyne node: {node}!
- Make sure your nodes are pointed to the same Redis server for authentication!
+
+ Make sure your nodes are pointed to the same Redis server for authentication!
+
)
} else {
return servers ? (
{/* Dialog box to show. */}
- {server &&
- }
+ {server && (
+
+ )}
-
Servers{node ? (' - ' + node) : ''}
+
+ Servers{node ? ' - ' + node : ''}
+
@@ -124,7 +145,9 @@ const ServerList = ({ ip, node, setMessage, setFailure }: {
))}
- ) :
+ ) : (
+
+ )
}
}
diff --git a/imports/servers/serverListItem.tsx b/imports/servers/serverListItem.tsx
index a0bfbe9..4eb9733 100644
--- a/imports/servers/serverListItem.tsx
+++ b/imports/servers/serverListItem.tsx
@@ -1,7 +1,13 @@
import React from 'react'
import { useRouter } from 'next/router'
import {
- ListItem, ListItemAvatar, ListItemButton, ListItemText, Avatar, Tooltip, IconButton
+ ListItem,
+ ListItemAvatar,
+ ListItemButton,
+ ListItemText,
+ Avatar,
+ Tooltip,
+ IconButton,
} from '@mui/material'
import Storage from '@mui/icons-material/Storage'
@@ -12,7 +18,13 @@ import Comment from '@mui/icons-material/Comment'
import UnstyledLink from '../helpers/unstyledLink'
import type { ExtraServerInfo } from './serverList'
-export const ServerListItem = ({ server, node, serverInfo, openDialog, stopStartServer }: {
+export const ServerListItem = ({
+ server,
+ node,
+ serverInfo,
+ openDialog,
+ stopStartServer,
+}: {
node?: string
server: string
serverInfo: number | ExtraServerInfo
@@ -20,13 +32,19 @@ export const ServerListItem = ({ server, node, serverInfo, openDialog, stopStart
stopStartServer: (operation: 'START' | 'TERM' | 'KILL', server: string) => void
}): React.JSX.Element => {
const router = useRouter()
- const href = { pathname: '/dashboard/[server]/console', query: node ? { server, node } : { server } }
+ const href = {
+ pathname: '/dashboard/[server]/console',
+ query: node ? { server, node } : { server },
+ }
const status = typeof serverInfo === 'number' ? serverInfo : serverInfo.status
const toDelete = typeof serverInfo === 'number' ? false : serverInfo.toDelete
-
- let statusText = status === 0 ? 'Offline' : (status === 1 ? 'Online' : 'Crashed')
+ let statusText = status === 0 ? 'Offline' : status === 1 ? 'Online' : 'Crashed'
if (toDelete) statusText += ' (marked for deletion)'
+ const handleClick = () => {
+ router.push(href).catch(console.error)
+ }
+
return (
e.preventDefault()}>
(stopStartServer(status !== 1 ? 'START' : 'TERM', server))}
+ onClick={() => stopStartServer(status !== 1 ? 'START' : 'TERM', server)}
color='default'
>
{status !== 1 ? : }
@@ -47,7 +65,7 @@ export const ServerListItem = ({ server, node, serverInfo, openDialog, stopStart
(stopStartServer('KILL', server))}
+ onClick={() => stopStartServer('KILL', server)}
color='primary'
>
@@ -64,16 +82,13 @@ export const ServerListItem = ({ server, node, serverInfo, openDialog, stopStart
>
}
>
- { router.push(href).catch(console.error) }}>
+
-
+
diff --git a/imports/settings/accountDialog.tsx b/imports/settings/accountDialog.tsx
index 2658aa9..a6abc32 100644
--- a/imports/settings/accountDialog.tsx
+++ b/imports/settings/accountDialog.tsx
@@ -1,7 +1,13 @@
import React, { useState } from 'react'
import {
- Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button, TextField
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogContentText,
+ DialogActions,
+ Button,
+ TextField,
} from '@mui/material'
const AccountDialog = (props: {
@@ -47,13 +53,17 @@ const AccountDialog = (props: {
{changePassword
? 'Change Password'
- : props.rename ? `Rename Account: ${props.username}` : 'Create Account'}
+ : props.rename
+ ? `Rename Account: ${props.username}`
+ : 'Create Account'}
{changePassword
? `Enter new password for ${props.username}:`
- : props.rename ? 'Enter new username:' : 'Enter username and password:'}
+ : props.rename
+ ? 'Enter new username:'
+ : 'Enter username and password:'}
{!changePassword && (
setPassword(e.target.value)}
- onKeyDown={e => { if (e.key === 'Enter') confirmRef.current?.focus() }}
+ onKeyDown={e => {
+ if (e.key === 'Enter') confirmRef.current?.focus()
+ }}
/>
setConfirmPassword(e.target.value)}
- onKeyDown={e => { if (e.key === 'Enter') handleSubmit() }}
+ onKeyDown={e => {
+ if (e.key === 'Enter') handleSubmit()
+ }}
/>
>
)}
{error && {error}}
- Cancel
- Done
+
+ Cancel
+
+
+ Done
+
>
diff --git a/imports/settings/confirmDialog.tsx b/imports/settings/confirmDialog.tsx
index ce48a79..83cdcd6 100644
--- a/imports/settings/confirmDialog.tsx
+++ b/imports/settings/confirmDialog.tsx
@@ -1,6 +1,11 @@
import React from 'react'
import {
- Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogContentText,
+ DialogActions,
+ Button,
} from '@mui/material'
const ConfirmDialog = (props: {
@@ -17,8 +22,12 @@ const ConfirmDialog = (props: {
{props.prompt}
- props.onCancel()} color='secondary'>Cancel
- props.onConfirm()} color='primary'>Confirm
+ props.onCancel()} color='secondary'>
+ Cancel
+
+ props.onConfirm()} color='primary'>
+ Confirm
+
>
diff --git a/imports/settings/settingsLayout.tsx b/imports/settings/settingsLayout.tsx
index 82f568b..cc74b3b 100644
--- a/imports/settings/settingsLayout.tsx
+++ b/imports/settings/settingsLayout.tsx
@@ -1,9 +1,18 @@
import React, { useState } from 'react'
import styled from '@emotion/styled'
import {
- Typography, IconButton, Drawer,
- List, ListItemButton, ListItemIcon, ListItemText,
- Divider, useMediaQuery, useTheme, Toolbar, Tooltip
+ Typography,
+ IconButton,
+ Drawer,
+ List,
+ ListItemButton,
+ ListItemIcon,
+ ListItemText,
+ Divider,
+ useMediaQuery,
+ useTheme,
+ Toolbar,
+ Tooltip,
} from '@mui/material'
import MenuIcon from '@mui/icons-material/Menu'
import Apps from '@mui/icons-material/Apps'
@@ -21,10 +30,14 @@ const SettingsContainer = styled.div({
padding: 20,
flexDirection: 'column',
display: 'flex',
- flex: 1
+ flex: 1,
})
-const DrawerItem = (props: { icon: React.ReactElement, name: string, subUrl: string }): React.JSX.Element => (
+const DrawerItem = (props: {
+ icon: React.ReactElement
+ name: string
+ subUrl: string
+}): React.JSX.Element => (
{props.icon}
@@ -40,12 +53,14 @@ const onLogout = (): void => {
fetch(`${config.ip}/logout`, { headers: { Authorization: token ?? '' } }).catch(console.error)
}
-const SettingsLayout = (props: React.PropsWithChildren<{ loggedIn: boolean }>): React.JSX.Element => {
+const SettingsLayout = (
+ props: React.PropsWithChildren<{ loggedIn: boolean }>,
+): React.JSX.Element => {
const [openDrawer, setOpenDrawer] = useState(false)
const drawerVariant = useMediaQuery(useTheme().breakpoints.only('xs')) ? 'temporary' : 'permanent'
const appBarContent = (
<>
- {(props.loggedIn && drawerVariant === 'temporary') && (
+ {props.loggedIn && drawerVariant === 'temporary' && (
<>
):
>
)}
- Octyne
+
+ Octyne
+
{props.loggedIn && (
-
+
+
+
)}
diff --git a/imports/theme.ts b/imports/theme.ts
index 5765fe5..6b05ec0 100644
--- a/imports/theme.ts
+++ b/imports/theme.ts
@@ -8,13 +8,13 @@ export const defaultThemeOptions: ThemeOptions = {
palette: {
primary: black,
secondary: white,
- mode: 'dark'
+ mode: 'dark',
},
components: {
MuiTextField: { defaultProps: { color: 'secondary' } },
MuiCheckbox: { defaultProps: { color: 'secondary' } },
- MuiButton: { defaultProps: { color: 'secondary' } }
- }
+ MuiButton: { defaultProps: { color: 'secondary' } },
+ },
}
const theme = createTheme(defaultThemeOptions)
diff --git a/package.json b/package.json
index cb459e2..842dad1 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
},
"devDependencies": {
"@babel/core": "^7.26.0",
+ "@eslint/js": "^9.17.0",
"@next/eslint-plugin-next": "^15.1.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.1.0",
@@ -41,22 +42,23 @@
"@types/react": "^19.0.2",
"@types/react-virtualized-auto-sizer": "^1.0.4",
"@types/react-window": "^1.8.8",
- "@typescript-eslint/eslint-plugin": "^8.18.1",
- "@typescript-eslint/parser": "^8.18.1",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.17.0",
- "eslint-config-love": "^112.0.0",
+ "eslint-config-prettier": "^9.1.0",
"eslint-config-standard-jsx": "^11.0.0",
"eslint-config-standard-react": "^13.0.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-n": "^17.15.0",
+ "eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-promise": "^7.2.1",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.1.0",
"husky": "^9.1.7",
"jsdom": "^25.0.1",
"next-router-mock": "^0.9.13",
+ "prettier": "^3.4.2",
"typescript": "^5.7.2",
+ "typescript-eslint": "^8.18.1",
"vite": "^6.0.4",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^2.1.8"
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 0030785..7d61f99 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -12,7 +12,9 @@ const clientSideEmotionCache = createCache({ key: 'css' })
export const UpdateThemeContext = React.createContext(() => {})
-export default function MyApp (props: AppProps & { emotionCache?: EmotionCache }): React.JSX.Element {
+export default function MyApp(
+ props: AppProps & { emotionCache?: EmotionCache },
+): React.JSX.Element {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props
// Customisable theming options.
diff --git a/pages/_document.tsx b/pages/_document.tsx
index 10d449a..b42fcc0 100644
--- a/pages/_document.tsx
+++ b/pages/_document.tsx
@@ -8,7 +8,7 @@ import config from '../imports/config'
const ico = `${config.basePath ?? ''}/favicon.png`
class MyDocument extends Document {
- render (): React.JSX.Element {
+ render(): React.JSX.Element {
return (
@@ -66,16 +66,18 @@ MyDocument.getInitialProps = async ctx => {
ctx.renderPage = async () =>
await originalRenderPage({
enhanceApp: (App: any) => {
- const EnhancedApp = (props: any): React.JSX.Element =>
+ const EnhancedApp = (props: any): React.JSX.Element => (
+
+ )
return EnhancedApp
- }
+ },
})
const initialProps = await Document.getInitialProps(ctx)
// This is important. It prevents emotion to render invalid HTML.
// See https://github.com/mui-org/material-ui/issues/26561#issuecomment-855286153
const emotionStyles = emotionServer.extractCriticalToChunks(initialProps.html)
- const emotionStyleTags = emotionStyles.styles.map((style) => (
+ const emotionStyleTags = emotionStyles.styles.map(style => (