diff --git a/eslint.config.mjs b/eslint.config.mjs index ced1aee..f54d3e6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -48,33 +48,32 @@ export default tseslint.config( }, rules: { '@typescript-eslint/no-confusing-void-expression': 'off', - /* '@typescript-eslint/restrict-template-expressions': [ + '@typescript-eslint/no-import-type-side-effects': ['error'], + '@typescript-eslint/consistent-type-imports': [ + 'error', + { + prefer: 'type-imports', + disallowTypeAnnotations: true, + fixStyle: 'inline-type-imports', + }, + ], + '@typescript-eslint/restrict-template-expressions': [ 'error', { allowAny: false, allowBoolean: false, - allowNullish: false, + allowNullish: true, // TODO: Turn this off! allowNumber: true, allowRegExp: false, allowNever: false, }, - ], */ + ], + 'promise/no-nesting': 'off', // TODO: Re-enable! + 'promise/always-return': ['error', { ignoreLastCallback: true }], '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', - 'promise/catch-or-return': 'off', - 'promise/always-return': 'off', - 'promise/no-nesting': 'off', }, }, eslintPluginPrettierRecommended, diff --git a/imports/dashboard/files/fileList.tsx b/imports/dashboard/files/fileList.tsx index 7dd46f8..03dd3a2 100644 --- a/imports/dashboard/files/fileList.tsx +++ b/imports/dashboard/files/fileList.tsx @@ -12,12 +12,12 @@ import { } from '@mui/material' import AutoSizer from 'react-virtualized-auto-sizer' import { FixedSizeList, type ListChildComponentProps } from 'react-window' -import { useRouter } from 'next/router' import Folder from '@mui/icons-material/Folder' import MoreVert from '@mui/icons-material/MoreVert' import InsertDriveFile from '@mui/icons-material/InsertDriveFile' import UnstyledLink from '../../helpers/unstyledLink' import { joinPath } from './fileUtils' +import useOctyneData from '../useOctyneData' const rtd = (num: number): number => Math.round(num * 100) / 100 const bytesToGb = (bytes: number): string => { @@ -101,7 +101,7 @@ const FileListItemRenderer = ({ }: ListChildComponentProps): React.JSX.Element => { const { files, path, disabled, filesSelected, setFilesSelected, openMenu, onClick } = data as FileItemData - const router = useRouter() + const { node, server } = useOctyneData() const file = files[index] const selectItem = (): void => { if (!filesSelected.includes(file.name)) setFilesSelected([...filesSelected, file.name]) @@ -120,7 +120,7 @@ const FileListItemRenderer = ({ } const subpath = file.folder ? joinPath(path, file.name) : path const params = new URLSearchParams() - if (router.query.node) params.append('node', router.query.node as string) + if (node) params.append('node', node) if (!file.folder) params.append('file', file.name) return ( (e.shiftKey ? shiftClickItem() : selectItem())} - url={`/dashboard/${router.query.server}/files${subpath}${params.size ? '?' : ''}${params}`} + url={`/dashboard/${server}/files${subpath}${params.size ? '?' : ''}${params}`} /> ) } diff --git a/imports/dashboard/files/fileManager.tsx b/imports/dashboard/files/fileManager.tsx index e53caaf..8645d3a 100644 --- a/imports/dashboard/files/fileManager.tsx +++ b/imports/dashboard/files/fileManager.tsx @@ -40,6 +40,8 @@ import FolderCreationDialog from './folderCreationDialog' const euc: (uriComponent: string | number | boolean) => string = typeof encodeURIComponent === 'function' ? encodeURIComponent : e => e.toString() +const errorMessage = (err: unknown) => + err instanceof Error ? err.message : typeof err === 'string' ? err : JSON.stringify(err) const editorExts = ['properties', 'json', 'yaml', 'yml', 'xml', 'js', 'log', 'sh', 'txt'] const FileManager = (props: { @@ -116,7 +118,7 @@ const FileManager = (props: { setError(null) const files = await ky .get(`server/${server}/files?path=${euc(path)}`) - .json<{ error?: string; contents: File[] }>() + .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) @@ -132,14 +134,14 @@ const FileManager = (props: { .pop(), true, ) - } else if (files) { + } else if (files.contents) { setFiles(files.contents) setFilesSelected([]) } setFetching(false) - })().catch(e => { + })().catch((e: unknown) => { console.error(e) - setMessage(`Failed to fetch files: ${e.message}`) + setMessage(`Failed to fetch files: ${errorMessage(e)}`) setFetching(false) }) }, [path, ky, server, updatePath, setAuthenticated, setServerExists]) @@ -207,7 +209,7 @@ const FileManager = (props: { fileInf.size < 2 * 1024 * 1024 && (editorExts.includes(filename.split('.').pop() ?? '') || fileInf.mimeType.startsWith('text/')) ) { - loadFileInEditor(filename).catch(err => { + loadFileInEditor(filename).catch((err: unknown) => { console.error(err) setMessage('An error occurred while loading file!') }) @@ -224,8 +226,8 @@ const FileManager = (props: { if (createFolder.success) fetchFiles() else setMessage(createFolder.error) setFetching(false) - } catch (e: any) { - setMessage(e.message as string) + } catch (e: unknown) { + setMessage(errorMessage(e)) setFetching(false) } } @@ -252,8 +254,8 @@ const FileManager = (props: { if (editFile.success) fetchFiles() else setMessage(editFile.error) setFetching(false) - } catch (e: any) { - setMessage(e.message as string) + } catch (e: unknown) { + setMessage(errorMessage(e)) setFetching(false) } } @@ -282,7 +284,7 @@ const FileManager = (props: { return } } catch (e) { - setMessage(`Error deleting files: ${e}`) + setMessage(`Error deleting files: ${errorMessage(e)}`) setOverlay('') fetchFiles() return @@ -308,10 +310,11 @@ const FileManager = (props: { progress, }) } + // eslint-disable-next-line promise/always-return -- false positive if (localStorage.getItem('ecthelion:logAsyncMassActions')) console.log('Deleted ' + file) }) - .catch(e => setMessage(`Error deleting ${file}\n${e}`)), + .catch((e: unknown) => setMessage(`Error deleting ${file}\n${errorMessage(e)}`)), ) } Promise.allSettled(ops) @@ -338,16 +341,17 @@ const FileManager = (props: { }, ) if (r.status !== 200) { - setMessage(`Error uploading ${file.name}\n${JSON.parse(r.body).error}`) + const { error } = JSON.parse(r.body) as { error?: string } + setMessage(`Error uploading ${file.name}: ${error ?? 'Unknown Error'}`) } setOverlay('') } setMessage('Uploaded all files successfully!') if (path === prevPath.current) fetchFiles() // prevPath is current path after useEffect call. - })().catch(e => { + })().catch((e: unknown) => { console.error(e) setOverlay('') - setMessage(`Failed to upload files: ${e.message}`) + setMessage(`Failed to upload files: ${errorMessage(e)}`) }) } // Single file logic. @@ -362,9 +366,9 @@ const FileManager = (props: { setFetching(false) setMenuOpen('') fetchFiles() - })().catch(e => { + })().catch((e: unknown) => { console.error(e) - setMessage(`Failed to delete file: ${e.message}`) + setMessage(`Failed to delete file: ${errorMessage(e)}`) }) } const handleDownloadMenuButton = (): void => { @@ -372,9 +376,9 @@ const FileManager = (props: { 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 => { + })().catch((e: unknown) => { console.error(e) - setMessage(`Failed to download file: ${e.message}`) + setMessage(`Failed to download file: ${errorMessage(e)}`) }) } const handleDecompressMenuButton = (): void => { @@ -392,9 +396,9 @@ const FileManager = (props: { setFetching(false) setMenuOpen('') fetchFiles() - })().catch(e => { + })().catch((e: unknown) => { console.error(e) - setMessage(`Failed to decompress file: ${e.message}`) + setMessage(`Failed to decompress file: ${errorMessage(e)}`) }) } const handleCloseDownload = (): void => { @@ -407,9 +411,9 @@ 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) => { + })().catch((e: unknown) => { console.error(e) - setMessage(`Failed to download file: ${e.message}`) + setMessage(`Failed to download file: ${errorMessage(e)}`) }) } const handleSaveFile = async (name: string, content: string): Promise => { @@ -420,8 +424,8 @@ 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}`) + } catch (e: unknown) { + setMessage(`Error saving file! ${errorMessage(e)}`) console.error(e) } } @@ -479,9 +483,9 @@ const FileManager = (props: { (await ky.get('ott').json<{ ticket: string }>()).ticket, ) window.location.href = `${ip}/server/${server}/file?path=${path}${file.name}&ticket=${ott}` - })().catch(e => { + })().catch((e: unknown) => { console.error(e) - setMessage(`Failed to download file: ${e.message}`) + setMessage(`Failed to download file: ${errorMessage(e)}`) }) }} /> @@ -629,7 +633,9 @@ const FileManager = (props: { {folderPromptOpen && ( setFolderPromptOpen(false)} - handleCreateFolder={async (name: string) => await handleCreateFolder(name)} + handleCreateFolder={(name: string) => { + handleCreateFolder(name).catch(console.error) + }} /> )} {modifyFileDialogOpen && ( @@ -637,7 +643,9 @@ const FileManager = (props: { filename={menuOpen} operation={modifyFileDialogOpen} handleClose={() => setModifyFileDialogOpen('')} - handleEdit={async path => await handleModifyFile(path, modifyFileDialogOpen)} + handleEdit={path => { + handleModifyFile(path, modifyFileDialogOpen).catch(console.error) + }} /> )} {massActionDialogOpen && ( @@ -671,7 +679,7 @@ const FileManager = (props: { { - handleFilesDelete().catch(() => {}) + handleFilesDelete().catch(console.error) }} disabled={!!overlay} > diff --git a/imports/dashboard/files/folderCreationDialog.tsx b/imports/dashboard/files/folderCreationDialog.tsx index 1390d55..e05f4cf 100644 --- a/imports/dashboard/files/folderCreationDialog.tsx +++ b/imports/dashboard/files/folderCreationDialog.tsx @@ -14,7 +14,7 @@ const FolderCreationDialog = ({ handleCreateFolder, handleClose, }: { - handleCreateFolder: (name: string) => any + handleCreateFolder: (name: string) => void handleClose: () => void }): React.JSX.Element => { const [name, setName] = useState('') diff --git a/imports/dashboard/files/massActionDialog.tsx b/imports/dashboard/files/massActionDialog.tsx index 5c8247c..386b458 100644 --- a/imports/dashboard/files/massActionDialog.tsx +++ b/imports/dashboard/files/massActionDialog.tsx @@ -70,18 +70,18 @@ const MassActionDialog = ({ res .json<{ token: string }>() .then(async ({ token }) => { - while (true) { + let finished: boolean | string = false + while (!finished) { + await new Promise(resolve => setTimeout(resolve, 1000)) const res = await ky .get(`server/${server}/compress/v2?token=${token}`) - .json<{ finished: boolean; error: string }>() - if (res.finished || res.error) { - reload() - setOverlay('') - setMessage(res.error ?? 'Compressed all files successfully!') - break - } - await new Promise(resolve => setTimeout(resolve, 1000)) + .json<{ finished: boolean; error?: string }>() + + finished = res.finished || !!res.error + if (finished) setMessage(res.error ?? 'Compressed all files successfully!') } + reload() + setOverlay('') }) .catch(() => setMessage('Failed to compress the files!')) } else if (res.status === 404 && archiveType !== 'zip') { @@ -101,7 +101,7 @@ const MassActionDialog = ({ } else { res .json<{ error: string }>() - .then(({ error }) => setMessage(error ?? 'Failed to compress the files!')) + .then(({ error }) => setMessage(error)) .catch(() => setMessage('Failed to compress the files!')) } }) @@ -113,7 +113,7 @@ const MassActionDialog = ({ setOverlay('') res .json<{ error: string }>() - .then(({ error }) => setMessage(error ?? 'Failed to compress the files!')) + .then(({ error }) => setMessage(error)) .catch(() => setMessage('Failed to compress the files!')) } }) @@ -152,7 +152,8 @@ const MassActionDialog = ({ } } catch (e) { reload() - setMessage(`Error ${movingl} files: ${e}`) + console.error(e) + setMessage(`Error ${movingl} files: ${e instanceof Error ? e.message : 'Unknown Error!'}`) setOverlay('') return } @@ -174,10 +175,13 @@ const MassActionDialog = ({ } const progress = ((files.length - left) * 100) / files.length setOverlay({ text: `${moving} ${--left} out of ${files.length} files.`, progress }) + // eslint-disable-next-line promise/always-return -- false positive if (localStorage.getItem('ecthelion:logAsyncMassActions')) console.log(moved + ' ' + file) }) - .catch(e => setMessage(`Error ${movingl} ${file}\n${e}`)), + .catch((e: unknown) => + setMessage(`Error ${movingl} ${file}: ${e instanceof Error ? e.message : 'Unknown'}`), + ), ) } Promise.allSettled(requests) diff --git a/imports/dashboard/files/modifyFileDialog.tsx b/imports/dashboard/files/modifyFileDialog.tsx index d98776f..3e63925 100644 --- a/imports/dashboard/files/modifyFileDialog.tsx +++ b/imports/dashboard/files/modifyFileDialog.tsx @@ -16,7 +16,7 @@ const ModifyFileDialog = ({ operation, filename, }: { - handleEdit: (path: string) => any + handleEdit: (path: string) => void handleClose: () => void operation: 'move' | 'copy' | 'rename' filename: string diff --git a/imports/dashboard/useOctyneData.tsx b/imports/dashboard/useOctyneData.tsx index 5db293f..11de67b 100644 --- a/imports/dashboard/useOctyneData.tsx +++ b/imports/dashboard/useOctyneData.tsx @@ -33,7 +33,7 @@ export const useOctyneAuth = (): OctyneDataWithAuth => { if (servers.ok) setServerExists(!!resp[server]) if (servers.ok || servers.status === 401 || servers.status === 403) setAuth(servers.ok) else setConnectionFailure(true) - })().catch(err => { + })().catch((err: unknown) => { console.error(err) setConnectionFailure(true) }) diff --git a/pages/_app.tsx b/pages/_app.tsx index 7d61f99..9a70142 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -10,11 +10,14 @@ import defaultTheme, { defaultThemeOptions, white, black } from '../imports/them // Client-side cache, shared for the whole session of the user in the browser. const clientSideEmotionCache = createCache({ key: 'css' }) -export const UpdateThemeContext = React.createContext(() => {}) +export const UpdateThemeContext = React.createContext(() => { + /* no-op */ +}) export default function MyApp( props: AppProps & { emotionCache?: EmotionCache }, ): React.JSX.Element { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { Component, emotionCache = clientSideEmotionCache, pageProps } = props // Customisable theming options. diff --git a/pages/_document.tsx b/pages/_document.tsx index b42fcc0..6572385 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -1,4 +1,5 @@ import React from 'react' +import type { AppProps } from 'next/app' import Document, { Html, Head, Main, NextScript } from 'next/document' import createEmotionServer from '@emotion/server/create-instance' import createCache from '@emotion/cache' @@ -65,8 +66,9 @@ MyDocument.getInitialProps = async ctx => { ctx.renderPage = async () => await originalRenderPage({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any enhanceApp: (App: any) => { - const EnhancedApp = (props: any): React.JSX.Element => ( + const EnhancedApp = (props: AppProps): React.JSX.Element => ( ) return EnhancedApp diff --git a/pages/dashboard/[server]/console.tsx b/pages/dashboard/[server]/console.tsx index 79d032d..a28872a 100644 --- a/pages/dashboard/[server]/console.tsx +++ b/pages/dashboard/[server]/console.tsx @@ -14,6 +14,24 @@ import ConsoleView from '../../../imports/dashboard/console/consoleView' import ConsoleButtons from '../../../imports/dashboard/console/consoleButtons' import ConnectionFailure from '../../../imports/errors/connectionFailure' +interface ConsoleDataMessage { + type: 'output' + data: string +} + +interface ConsoleErrorMessage { + type: 'error' + message: string +} + +interface ConsolePingMessage { + type: 'pong' +} + +interface ConsoleSettingsMessage { + type: 'settings' +} + const CommandTextField = ({ ws, id, @@ -142,9 +160,13 @@ const Console = ({ } newWS.onmessage = (event: MessageEvent): void => { if (newWS.protocol === 'console-v2') { - const data = JSON.parse(event.data) // For now, ignore settings and pong. + const data = JSON.parse(event.data) as + | ConsoleDataMessage + | ConsoleErrorMessage + | ConsolePingMessage + | ConsoleSettingsMessage if (data.type === 'output') { - handleOutputData(data.data as string) + handleOutputData(data.data) } else if (data.type === 'error') { buffer.current.push({ id: ++id.current, text: `[Ecthelion] Error: ${data.message}` }) } else if (data.type === 'pong') console.log('Pong!') @@ -180,7 +202,7 @@ const Console = ({ setWs(newWS) } catch (e) { setListening(false) - console.error(`An error occurred while connecting to console.\n${e}`) + console.error('An error occurred while connecting to console.', e) } }, [ip, ky, server, setAuthenticated, setServerExists], @@ -188,7 +210,7 @@ const Console = ({ useEffect(() => { if (!ws) { const ignore = { current: false } // Required to handle React.StrictMode correctly. - connectToServer(ignore).catch(() => {}) + connectToServer(ignore).catch(console.error) return () => { ignore.current = true } diff --git a/pages/index.tsx b/pages/index.tsx index 00053a2..97c3a0b 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -87,7 +87,7 @@ const Index = (): React.JSX.Element => { } const handleLogin = (): void => { - login().catch(() => {}) + login().catch(console.error) } return ( diff --git a/pages/settings/accounts.tsx b/pages/settings/accounts.tsx index f8cf789..20e7443 100644 --- a/pages/settings/accounts.tsx +++ b/pages/settings/accounts.tsx @@ -39,15 +39,14 @@ const AccountsPage = (): React.JSX.Element => { const refetch = (): void => { ky.get('accounts') .then(async res => { - if (res.ok) return await res.json() - else if (res.status === 401) setStatus('not logged in') + if (res.ok) { + const data = await res.json() + if (Array.isArray(data)) + setAccounts(data.sort((a: string, b: string) => a.localeCompare(b))) + } else if (res.status === 401) setStatus('not logged in') else if (res.status === 404) setStatus('unsupported') else setStatus('failure') }) - .then(data => { - if (Array.isArray(data)) - setAccounts(data.sort((a: string, b: string) => a.localeCompare(b))) - }) .catch(() => setStatus('failure')) } @@ -64,7 +63,7 @@ const AccountsPage = (): React.JSX.Element => { setMessage(typeof json.error === 'string' ? json.error : 'Failed to create account!') } setCreateAccount(false) - })().catch(e => { + })().catch((e: unknown) => { console.error(e) setMessage('Failed to create account!') setCreateAccount(false) @@ -86,7 +85,7 @@ const AccountsPage = (): React.JSX.Element => { } else setMessage(typeof json.error === 'string' ? json.error : 'Failed to rename account!') } setRenameAccount('') - })().catch(e => { + })().catch((e: unknown) => { console.error(e) setMessage('Failed to rename account!') setRenameAccount('') @@ -104,7 +103,7 @@ const AccountsPage = (): React.JSX.Element => { setMessage(typeof json.error === 'string' ? json.error : 'Failed to change password!') } setChangePassword('') - })().catch(e => { + })().catch((e: unknown) => { console.error(e) setMessage('Failed to change password!') setChangePassword('') @@ -122,7 +121,7 @@ const AccountsPage = (): React.JSX.Element => { setMessage(typeof json.error === 'string' ? json.error : 'Failed to delete account!') } setDeleteAccount('') - })().catch(e => { + })().catch((e: unknown) => { console.error(e) setMessage('Failed to delete account!') setDeleteAccount('') diff --git a/pages/settings/config.tsx b/pages/settings/config.tsx index 7f3a1ef..b8686aa 100644 --- a/pages/settings/config.tsx +++ b/pages/settings/config.tsx @@ -69,7 +69,7 @@ const ConfigPage = (): React.JSX.Element => { .get('config/reload', { throwHttpErrors: true }) .then(async () => await loadConfig()) .then(() => setMessage('Successfully reloaded config!')) - .catch(err => { + .catch((err: unknown) => { console.error(err) setMessage('An error occurred reloading Octyne!') }) @@ -173,7 +173,7 @@ const ConfigPage = (): React.JSX.Element => { element.click() document.body.removeChild(element) }) - .catch(e => { + .catch((e: unknown) => { setMessage('Failed to download config!') console.error(e) }) @@ -191,10 +191,9 @@ const ConfigPage = (): React.JSX.Element => { title='Reload config from disk?' prompt={confirmDialogWarning} onConfirm={() => { - reloadFromDisk().then( - () => setConfirmDialog(false), - () => {}, - ) + reloadFromDisk() + .then(() => setConfirmDialog(false)) + .catch(console.error) }} onCancel={() => setConfirmDialog(false)} /> @@ -203,10 +202,9 @@ const ConfigPage = (): React.JSX.Element => { title='Save config?' prompt={confirmDialogWarning} onConfirm={() => { - saveConfig(confirmDialog as string).then( - () => setConfirmDialog(false), - () => {}, - ) + saveConfig(confirmDialog as string) + .then(() => setConfirmDialog(false)) + .catch(console.error) }} onCancel={() => setConfirmDialog(false)} />