| 1 | // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt |
| 2 | |
| 3 | import { createElement as h, Fragment, useEffect, useState } from 'react'; |
| 4 | import { apiCall, useApiEx } from './api' |
| 5 | import { Alert, Box } from '@mui/material' |
| 6 | import { downloadFileWithContent, focusSelector, isCtrlKey, KeepInScreen, prefix } from './misc' |
| 7 | import { Btn, Flex, IconBtn, reloadBtn } from './mui'; |
| 8 | import { Save, Edit, Download } from '@mui/icons-material' |
| 9 | import { TextEditor } from './TextEditor'; |
| 10 | import { state } from './state'; |
| 11 | import { DisplayField } from '@hfs/mui-grid-form' |
| 12 | import { adminApis } from '../../src/adminApis' |
| 13 | |
| 14 | export default function ConfigFilePage() { |
| 15 | state.title = "Config file" |
| 16 | const { data, reload, element } = useApiEx<typeof adminApis.get_config_text>('get_config_text', {}) |
| 17 | const [text, setText] = useState<string | undefined>() |
| 18 | const [saved, setSaved] = useState<string | undefined>() |
| 19 | const [edit, setEdit] = useState(false) |
| 20 | useEffect(() => { setSaved(data?.text) }, [data]) |
| 21 | useEffect(() => { saved !== undefined && setText(saved || '') }, [saved]) |
| 22 | return element || h(Fragment, {}, |
| 23 | h(Flex, { flexWrap: 'wrap', justifyContent: 'space-between' }, |
| 24 | h(Btn, { icon: Download, onClick: exportConfig, disabled: !data }, "Export without passwords"), |
| 25 | edit ? h(Fragment, {}, |
| 26 | reloadBtn(reload), |
| 27 | h(IconBtn, { |
| 28 | icon: Save, |
| 29 | title: "Save\n(ctrl+enter)", |
| 30 | modified: text !== saved, |
| 31 | onClick: save, |
| 32 | }), |
| 33 | h(Alert, { severity: 'warning', sx: { minWidth: '10em' } }, "Be careful, you can easily break things here"), |
| 34 | ) : h(Btn, { |
| 35 | icon: Edit, |
| 36 | variant: 'outlined', |
| 37 | onClick() { |
| 38 | setEdit(true) |
| 39 | setTimeout(() => focusSelector('main textarea'), 500) |
| 40 | } |
| 41 | }, "Edit"), |
| 42 | h(Box, { sx: { flex: 1, minWidth: 'fit-content' } }, h(DisplayField, { label: "File path", value: data?.fullPath, size: 'small' })) |
| 43 | ), |
| 44 | element || text !== undefined && // avoids bad undo behavior on start |
| 45 | h(Box, { sx: { '& pre,& textarea': { wordBreak: 'break-all !important' } } }, // fixes long lines not wrapping at the right point when the side menu is visible |
| 46 | h(KeepInScreen, { margin: 10 }, h(TextEditor, { |
| 47 | value: text, |
| 48 | disabled: !edit, |
| 49 | style: { background: '#8881' }, |
| 50 | onValueChange: setText, |
| 51 | onKeyDown(ev) { |
| 52 | if (['s','Enter'].includes(isCtrlKey(ev) as any)) { |
| 53 | void save() |
| 54 | ev.preventDefault() |
| 55 | } |
| 56 | }, |
| 57 | }))), |
| 58 | ) |
| 59 | |
| 60 | function save() { |
| 61 | return apiCall('set_config_text', { text }).then(() => setSaved(text)) |
| 62 | } |
| 63 | |
| 64 | function exportConfig() { |
| 65 | const s = (text || '') |
| 66 | .replace(/^(\s*(\w*password|_\w*|token|srp|dynamic_dns_url):\s*).+\n/gm, '$1removed\n') |
| 67 | .replace(/(:\/\/)[^/@\s]+@/g, '$1removed@') // url credentials |
| 68 | + prefix('custom_html: | # this is currently ignored by hfs, just here for reference\n', data!.customHtml?.replace(/^/gm, ' ')) |
| 69 | if (!s) return |
| 70 | downloadFileWithContent('config_no_passwords.yaml', s) |
| 71 | } |
| 72 | } |