diff --git a/CHANGELOG.md b/CHANGELOG.md index 576b6cad..f3430794 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Config validation. When loading config files we now check that specified options will work. If not, the frontend will show an error message with details on what's wrong. This applies to `gui` settings (i.e. our own ones, `beets-flask/config.yaml`) and very select ones from native beets (only those which we use directly). Hopefully, this will eventually cover all config options of beets native, but this is more of an upsream task. [#224](https://github.com/pSpitzner/beets-flask/pull/224). - Upload Files via the WebUI. You can now drag-and-drop single files into an inbox. To upload whole albums, zip them on your host first (uploading of folders directly is not implemented, as it would require a secure context). +- New config option `gui.terminal.enabled` (default: true) [#254](https://github.com/pSpitzner/beets-flask/pull/224) ### Other (dev) diff --git a/backend/beets_flask/config/beets_config.py b/backend/beets_flask/config/beets_config.py index 706565f6..76e150b2 100644 --- a/backend/beets_flask/config/beets_config.py +++ b/backend/beets_flask/config/beets_config.py @@ -52,7 +52,7 @@ def reload(self, extra_yaml_path: str | Path | None = None) -> Self: This loads the user config from yaml files after resetting to defaults. - The `extra_yaml_path` argument is mainly for testing puproses, to add a last + The `extra_yaml_path` argument is mainly for testing purposes, to add a last yaml layer with high priority. """ log.debug("Resetting/Reloading config") diff --git a/backend/beets_flask/config/schema.py b/backend/beets_flask/config/schema.py index 045035b7..8093645b 100644 --- a/backend/beets_flask/config/schema.py +++ b/backend/beets_flask/config/schema.py @@ -124,4 +124,5 @@ class LibrarySectionSchema: @dataclass class TerminalSectionSchema: + enabled: bool = True start_path: str = "/repo" diff --git a/backend/beets_flask/server/websocket/__init__.py b/backend/beets_flask/server/websocket/__init__.py index fcc63027..d458ba18 100644 --- a/backend/beets_flask/server/websocket/__init__.py +++ b/backend/beets_flask/server/websocket/__init__.py @@ -3,6 +3,10 @@ from typing import cast import socketio +from eyconf.validation import ConfigurationError, MultiConfigurationError + +from beets_flask.config import get_config +from beets_flask.logger import log old_on = socketio.AsyncServer.on @@ -34,7 +38,20 @@ def register_socketio(app): # Register all socketio namespaces from .status import register_status - from .terminal import register_tmux - register_tmux() register_status() + + terminal_enabled = True + try: + terminal_enabled = get_config().data.gui.terminal.enabled + except (MultiConfigurationError, ConfigurationError): + # We don't want to let the exception propagate here as it won't reach the frontend. + log.debug("Encountered config error. Will raise on next call to get_config()") + + if terminal_enabled: + log.info("Setting up Web-Terminal") + from .terminal import register_tmux + + register_tmux() + else: + log.info("Web-Terminal is disabled, skipping setup") diff --git a/docker/entrypoints/entrypoint_dev.sh b/docker/entrypoints/entrypoint_dev.sh index cb1ffea5..8e0f8d77 100755 --- a/docker/entrypoints/entrypoint_dev.sh +++ b/docker/entrypoints/entrypoint_dev.sh @@ -62,7 +62,7 @@ redis-cli FLUSHALL python ./generate_types.py # we need to run with one worker for socketio to work (but need at least threads for SSEs) -# sufficient timout for the interactive import sessions, which may take a couple of minutes +# sufficient timeout for the interactive import sessions, which may take a couple of minutes # gunicorn --worker-class eventlet -w 1 --threads 32 --timeout 300 --bind 0.0.0.0:5001 --reload 'main:create_app()' @@ -77,5 +77,5 @@ uvicorn beets_flask.server.app:create_app --port 5001 \ -# if we need to debug the continaer without running the webserver: +# if we need to debug the container without running the webserver: # tail -f /dev/null diff --git a/docs/configuration.md b/docs/configuration.md index 8c6e304e..5324fc1d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -134,6 +134,11 @@ The default is `[";", ",", "&"]`. ## Terminal +### `gui.terminal.enabled` + +A boolean to enable or disable the terminal in the web interface. +By default, the terminal is enabled. + ### `gui.terminal.start_path` Specifies the path that is used when starting the terminal in the web interface. @@ -155,8 +160,8 @@ However, the import itself is always done sequentially. This is to ensure that the import process is not interrupted by other operations. ``` -### `gui.inbox.temp_dir` -Specifies the temporary directory that is used to store files during the upload process. +### `gui.inbox.temp_dir` +Specifies the temporary directory that is used to store files during the upload process. This is useful to ensure that files are uploaded to a filesystem that is fast and has enough space. The default value is `/tmp/beets-flask/upload`. diff --git a/frontend/src/components/frontpage/navbar.tsx b/frontend/src/components/frontpage/navbar.tsx index 04b3ffdb..582dab04 100644 --- a/frontend/src/components/frontpage/navbar.tsx +++ b/frontend/src/components/frontpage/navbar.tsx @@ -6,6 +6,8 @@ import Tab, { tabClasses, TabProps } from '@mui/material/Tab'; import Tabs, { tabsClasses } from '@mui/material/Tabs'; import { createLink, LinkProps, useRouterState } from '@tanstack/react-router'; +import { useConfig } from '@/api/config.ts'; + export const NAVBAR_HEIGHT = { desktop: '48px', mobile: '74px', @@ -142,6 +144,8 @@ function NavItem({ label, ...props }: StyledTabProps) { function NavTabs() { const theme = useTheme(); + const config = useConfig(); + const location = useRouterState({ select: (s) => s.location }); let basePath = location.pathname.split('/')[1]; @@ -150,18 +154,21 @@ function NavTabs() { basePath += '/' + location.pathname.split('/')[2]; } - const navItems = [ - { label: 'Home', icon: , to: '/' as const }, - { label: 'Inbox', icon: , to: '/inbox' as const }, - //{ label: "Session", icon: , to: "/sessiondraft" as const }, - { label: 'Library', icon: , to: '/library/browse' as const }, - { label: 'Search', icon: , to: '/library/search' as const }, - { + const navItems: StyledTabProps[] = [ + { label: 'Home', icon: , to: '/' }, + { label: 'Inbox', icon: , to: '/inbox' }, + //{ label: "Session", icon: , to: '/sessiondraft'}, + { label: 'Library', icon: , to: '/library/browse' }, + { label: 'Search', icon: , to: '/library/search' }, + ]; + + if (config.gui.terminal.enabled) { + navItems.push({ label: '', icon: , - to: '/terminal' as const, - }, - ]; + to: '/terminal', + }); + } const currentIdx = navItems.findIndex((item) => item.to === '/' + basePath); const ref = useRef(null); diff --git a/frontend/src/components/frontpage/terminal.tsx b/frontend/src/components/frontpage/terminal.tsx index 9ac8d8d9..bfa90004 100644 --- a/frontend/src/components/frontpage/terminal.tsx +++ b/frontend/src/components/frontpage/terminal.tsx @@ -14,6 +14,7 @@ import { Terminal as xTerminal } from '@xterm/xterm'; import useSocket from '@/components/common/websocket/useSocket'; import 'node_modules/@xterm/xterm/css/xterm.css'; +import { useConfig } from '@/api/config.ts'; import { Socket } from 'socket.io-client'; // match our style - this is somewhat redundant with main.css @@ -139,8 +140,37 @@ export function TerminalContextProvider({ }: { children: React.ReactNode; }) { - const { socket, isConnected } = useSocket('terminal'); + const config = useConfig(); + + if (!config.gui.terminal.enabled) { + const noop = () => { + console.warn( + 'Terminal is not available (disabled in server config).' + ); + }; + + return ( + + {children} + + ); + } + return {children}; +} + +function InitTerminalContext({ children }: { children: React.ReactNode }) { + const { socket, isConnected } = useSocket('terminal'); const [open, setOpen] = useState(false); const [term, setTerm] = useState(); diff --git a/frontend/src/components/inbox/settings/actionButtonSettings.tsx b/frontend/src/components/inbox/settings/actionButtonSettings.tsx index ddddc0a6..4771d775 100644 --- a/frontend/src/components/inbox/settings/actionButtonSettings.tsx +++ b/frontend/src/components/inbox/settings/actionButtonSettings.tsx @@ -48,6 +48,7 @@ import { ACTIONS, DEFAULT_INBOX_FOLDER_FRONTEND_CONFIG, InboxFolderFrontendConfig, + useConfig, } from '@/api/config'; import { Dialog } from '@/components/common/dialogs'; @@ -775,11 +776,22 @@ function AddActionButton({ onAdd: (action: Action) => void; } & ButtonProps) { const theme = useTheme(); + const config = useConfig(); const [open, setOpen] = useState(false); - const defaultActions: Array = Object.entries(ACTIONS).map( - ([_, a]) => a - ); + function isEnabled([actionName, _action]: [string, Action]) { + switch (actionName) { + case 'import_terminal': + return config.gui.terminal.enabled; + default: + return true; + } + } + + const defaultActions: Array = Object.entries(ACTIONS) + .filter(isEnabled) + .map(([_, a]) => a); + const [action, setAction] = useState(defaultActions[0]); return ( diff --git a/frontend/src/pythonTypes.ts b/frontend/src/pythonTypes.ts index af88c66b..ca67838a 100644 --- a/frontend/src/pythonTypes.ts +++ b/frontend/src/pythonTypes.ts @@ -103,6 +103,7 @@ export interface BeetsSchema { } export interface TerminalSectionSchema { + enabled: boolean; start_path: string; } diff --git a/frontend/src/routes/inbox/index.tsx b/frontend/src/routes/inbox/index.tsx index 3e243f8f..080710a8 100644 --- a/frontend/src/routes/inbox/index.tsx +++ b/frontend/src/routes/inbox/index.tsx @@ -11,7 +11,7 @@ import { import { useSuspenseQuery } from '@tanstack/react-query'; import { createFileRoute } from '@tanstack/react-router'; -import { Action } from '@/api/config'; +import { Action, useConfig } from '@/api/config'; import { inboxQueryOptions } from '@/api/inbox'; import { MatchChip, StyledChip } from '@/components/common/chips'; import { Dialog } from '@/components/common/dialogs'; @@ -158,6 +158,8 @@ function PageHeader({ inboxes, ...props }: { inboxes: Folder[] } & BoxProps) { /** Description of the inbox page, shown as modal on click */ function InfoDescription() { const theme = useTheme(); + const config = useConfig(); + const [open, setOpen] = useState(false); const { data } = useSuspenseQuery(inboxQueryOptions()); @@ -257,7 +259,9 @@ function InfoDescription() { - + {config.gui.terminal.enabled ?? ( + + )} diff --git a/frontend/src/routes/terminal/index.tsx b/frontend/src/routes/terminal/index.tsx index 4e434820..346da474 100644 --- a/frontend/src/routes/terminal/index.tsx +++ b/frontend/src/routes/terminal/index.tsx @@ -1,5 +1,7 @@ +import { Alert, AlertTitle, Box } from '@mui/material'; import { createFileRoute } from '@tanstack/react-router'; +import { useConfig } from '@/api/config'; import { PageWrapper } from '@/components/common/page'; import { Terminal } from '@/components/frontpage/terminal'; @@ -8,6 +10,25 @@ export const Route = createFileRoute('/terminal/')({ }); function TerminalPage() { + const config = useConfig(); + if (!config.gui.terminal.enabled) { + return ( + + + Terminal Disabled + + The terminal is not enabled in the server configuration. + + + + ); + } + return (