import { Badge, Box, Button, chakra, CircularProgress, FormControl, FormLabel, HStack, IconButton, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Text, Tooltip, useToast, } from '@chakra-ui/react'; import { ArrowPathIcon, ArrowsPointingInIcon, ArrowsPointingOutIcon, Cog6ToothIcon, } from '@heroicons/react/24/outline'; import { joinPaths } from '@remix-run/router'; import classNames from 'classnames'; import { useNodes, useCoreSettings } from 'stores'; import debounce from 'lodash.debounce'; import { FC, useCallback, useEffect, useRef, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useMutation } from '@tanstack/react-query'; import { ReadyState } from 'react-use-websocket'; import { useWebSocket } from 'react-use-websocket/dist/lib/use-websocket'; import { getAuthToken } from 'service/auth-storage'; import { Icon } from 'components/icon'; import { JsonEditor } from 'components/json-editor'; import 'components/json-editor/themes.js'; export const MAX_NUMBER_OF_LOGS = 500; const UsageIcon = chakra(Cog6ToothIcon, { baseStyle: { w: 5, h: 5, }, }); export const ReloadIcon = chakra(ArrowPathIcon, { baseStyle: { w: 4, h: 4, }, }); export const FullScreenIcon = chakra(ArrowsPointingOutIcon, { baseStyle: { w: 4, h: 4, }, }); export const ExitFullScreenIcon = chakra(ArrowsPointingInIcon, { baseStyle: { w: 3, h: 3, }, }); const getStatus = (status: string) => { return { [ReadyState.CONNECTING]: 'connecting', [ReadyState.OPEN]: 'connected', [ReadyState.CLOSING]: 'closed', [ReadyState.CLOSED]: 'closed', [ReadyState.UNINSTANTIATED]: 'closed', }[status]; }; const getWebsocketUrl = () => { try { let baseURL = new URL( import.meta.env.VITE_BASE_API.startsWith('/') ? window.location.origin + import.meta.env.VITE_BASE_API : import.meta.env.VITE_BASE_API ); return ( (baseURL.protocol === 'https:' ? 'wss://' : 'ws://') + joinPaths([baseURL.host + baseURL.pathname, '/core/logs']) + '?interval=1&token=' + getAuthToken() ); } catch (e) { console.error('Unable to generate websocket url'); console.error(e); return null; } }; let logsTmp: string[] = []; const CoreSettingModalContent: FC = () => { const { isEditingCore } = useNodes(); const { fetchCoreSettings, updateConfig, isLoading, config, isPostLoading, version, restartCore, } = useCoreSettings(); const logsDiv = useRef(null); const [logs, setLogs] = useState([]); const { t } = useTranslation(); const toast = useToast(); const form = useForm({ defaultValues: { config: config || {} }, }); useEffect(() => { if (config) form.setValue('config', config); }, [config]); useEffect(() => { if (isEditingCore) fetchCoreSettings(); }, [isEditingCore]); ''.startsWith; const scrollShouldStayOnEnd = useRef(true); const updateLogs = useCallback( debounce((logs: string[]) => { const isScrollOnEnd = Math.abs( (logsDiv.current?.scrollTop || 0) - (logsDiv.current?.scrollHeight || 0) + (logsDiv.current?.offsetHeight || 0) ) < 10; if (logsDiv.current && isScrollOnEnd) scrollShouldStayOnEnd.current = true; else scrollShouldStayOnEnd.current = false; if (logs.length < 40) setLogs(logs); }, 300), [] ); const { readyState } = useWebSocket(getWebsocketUrl(), { onMessage: (e: any) => { logsTmp.push(e.data); if (logsTmp.length > MAX_NUMBER_OF_LOGS) logsTmp = logsTmp.splice(0, logsTmp.length - MAX_NUMBER_OF_LOGS); updateLogs([...logsTmp]); }, shouldReconnect: () => true, reconnectAttempts: 10, reconnectInterval: 1000, }); useEffect(() => { if (logsDiv.current && scrollShouldStayOnEnd.current) logsDiv.current.scrollTop = logsDiv.current?.scrollHeight; }, [logs]); useEffect(() => { return () => { logsTmp = []; }; }, []); const status = getStatus(readyState.toString()); const { mutate: handleRestartCore, isPending: isRestarting } = useMutation({ mutationFn: restartCore }); const handleOnSave = ({ config }: any) => { updateConfig(config) .then(() => { toast({ title: t('core.successMessage'), status: 'success', isClosable: true, position: 'top', duration: 3000, }); }) .catch((e) => { let message = t('core.generalErrorMessage'); if (typeof e.response._data.detail === 'object') message = e.response._data.detail[Object.keys(e.response._data.detail)[0]]; if (typeof e.response._data.detail === 'string') message = e.response._data.detail; toast({ title: message, status: 'error', isClosable: true, position: 'top', duration: 3000, }); }); }; const editorRef = useRef(null); const [isFullScreen, setFullScreen] = useState(false); const handleFullScreen = () => { if (document.fullscreenElement) { document.exitFullscreen(); setFullScreen(false); } else { editorRef.current?.requestFullscreen(); setFullScreen(true); } }; return (
{t('core.configuration')}{' '} {isLoading && } {version && `v${version}`} ( )} /> {!isFullScreen ? : } {t('core.logs')} {t(`core.socket.${status}`)} {logs.map((message, i) => ( {message} ))}
); }; export const CoreSettingsModal: FC = () => { const { isEditingCore } = useNodes(); const onClose = useNodes.setState.bind(null, { isEditingCore: false }); const { t } = useTranslation(); return ( {t('core.title')} ); };