import CheckBoxIcon from '@mui/icons-material/CheckBox'; import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { Accordion, AccordionDetails, AccordionSummary, Autocomplete, AutocompleteRenderInputParams, Button, Checkbox, createFilterOptions, Divider, Link, List, Switch, TextField, Tooltip, } from '@mui/material'; import { styled } from '@mui/material/styles'; import { useTranslation } from 'react-i18next'; import { ListItem, ListItemText } from '@/components/ListItem'; import { DEFAULT_USER_NAME, getTidGiAuthHeaderWithToken } from '@/constants/auth'; import { WikiChannel } from '@/constants/channels'; import { rootTiddlers } from '@/constants/defaultTiddlerNames'; import { tlsCertExtensions, tlsKeyExtensions } from '@/constants/fileNames'; import { getDefaultHTTPServerIP } from '@/constants/urls'; import { usePromiseValue } from '@/helpers/useServiceValue'; import { useActualIp } from '@services/native/hooks'; import { isWikiWorkspace, IWorkspace } from '@services/workspaces/interface'; const AServerOptionsAccordion = styled(Accordion)` box-shadow: unset; background-color: unset; `; const AServerOptionsAccordionSummary = styled(AccordionSummary)` padding: 0; flex-direction: row-reverse; `; const HttpsCertKeyListItem = styled(ListItem)` flex-direction: row; align-items: flex-start; justify-content: space-between; `; const AutocompleteWithMarginTop = styled(Autocomplete)` margin-top: 8px; `; const AuthTokenTextAndButtonContainer = styled('div')` display: flex; flex-direction: row; `; export interface IServerOptionsProps { workspace: IWorkspace; workspaceSetter: (newValue: IWorkspace, requestSaveAndRestart?: boolean) => void; } export function ServerOptions(props: IServerOptionsProps) { const { t } = useTranslation(); const { workspace, workspaceSetter } = props; const isWiki = isWikiWorkspace(workspace); const { https = { enabled: false }, port, rootTiddler, lastNodeJSArgv, enableHTTPAPI, readOnlyMode = false, tokenAuth, authToken, userName, id, } = isWiki ? workspace : { https: { enabled: false }, port: 0, rootTiddler: '', lastNodeJSArgv: [], enableHTTPAPI: false, readOnlyMode: false, tokenAuth: false, authToken: '', userName: '', id: workspace.id, }; const actualIP = useActualIp(getDefaultHTTPServerIP(port), id); // some feature need a username to work, so if userName is empty, assign a fallbackUserName DEFAULT_USER_NAME const fallbackUserName = usePromiseValue(async () => (await window.service.auth.get('userName'))!, ''); const userNameIsEmpty = !(userName || fallbackUserName); const alreadyEnableSomeServerOptions = readOnlyMode; return ( }> {t('EditWorkspace.ServerOptions')} ({t('EditWorkspace.EnableHTTPAPI')}) ) => { workspaceSetter({ ...workspace, enableHTTPAPI: event.target.checked }, true); }} /> } > {t('EditWorkspace.URL')}{' '} { if (actualIP) { await window.service.native.openURI(actualIP); } }} style={{ cursor: 'pointer' }} > {actualIP} } placeholder='Optional' value={port} onChange={async (event) => { if (!Number.isNaN(Number.parseInt(event.target.value))) { workspaceSetter({ ...workspace, port: Number(event.target.value), homeUrl: await window.service.native.getLocalHostUrlWithActualInfo(getDefaultHTTPServerIP(Number(event.target.value)), id), }, true); } }} /> { const nextTokenAuth = !tokenAuth; const newAuthToken = authToken || await (window.service.auth.generateOneTimeAdminAuthTokenForWorkspace(id)); workspaceSetter({ ...workspace, userName: userNameIsEmpty ? DEFAULT_USER_NAME : userName, tokenAuth: nextTokenAuth, readOnlyMode: nextTokenAuth ? false : readOnlyMode, authToken: newAuthToken, }, true); }} /> } >
{t('EditWorkspace.TokenAuthDescription')}
{(userNameIsEmpty || !fallbackUserName) &&
{t('EditWorkspace.TokenAuthAutoFillUserNameDescription')}
} } />
{tokenAuth && ( <>
{t('EditWorkspace.TokenAuthCurrentTokenDescription')}
{' '} } placeholder={t('EditWorkspace.TokenAuthCurrentTokenEmptyText')} fullWidth value={authToken ?? ''} onChange={(event: React.ChangeEvent) => { workspaceSetter({ ...workspace, authToken: event.target.value }, true); }} />
)} {Array.isArray(lastNodeJSArgv) && ( <> )} ) => { workspaceSetter({ ...workspace, readOnlyMode: event.target.checked, tokenAuth: event.target.checked ? false : tokenAuth }, true); }} /> } > {workspace !== undefined && readOnlyMode && } ) => { workspaceSetter({ ...workspace, https: { ...https, enabled: event.target.checked } }); }} /> } > {https.enabled && ( <> ) => { workspaceSetter({ ...workspace, https: { ...https, tlsCert: event.target.value } }); }} /> ) => { workspaceSetter({ ...workspace, https: { ...https, tlsKey: event.target.value } }); }} /> )}
{ void event; workspaceSetter({ ...workspace, rootTiddler: value }); // void requestSaveAndRestart(); }} renderInput={(parameters: AutocompleteRenderInputParams) => ( )} renderOption={(props, option) =>
  • {t(`EditWorkspace.WikiRootTiddlerItems.${String(option).replace('$:/core/save/', '')}`)} ({String(option)})
  • } />
    ); } const autocompleteExcludedPluginsFilter = createFilterOptions(); const uncheckedIcon = ; const checkedIcon = ; function ExcludedPluginsAutocomplete(props: { workspace: IWorkspace; workspaceSetter: (newValue: IWorkspace, requestSaveAndRestart?: boolean) => void }) { const { t } = useTranslation(); const { workspaceSetter, workspace } = props; if (!isWikiWorkspace(workspace)) { return null; } const { excludedPlugins, id, active, } = workspace; const pluginsInWiki = usePromiseValue( async () => (await window.service.wiki.wikiOperationInBrowser(WikiChannel.runFilter, id, ['[!has[draft.of]plugin-type[plugin]sort[]]'])), [], [id, active], ) ?? []; return ( <> (
  • ) => { if (event.target.checked) { workspaceSetter({ ...workspace, excludedPlugins: [...excludedPlugins.filter(item => item !== option), option] }, true); } else { workspaceSetter({ ...workspace, excludedPlugins: excludedPlugins.filter(item => item !== option) }, true); } }} /> {option}
  • )} slotProps={{ chip: { onDelete: (event: Event) => { // Be defensive: event.target can be null and EventTarget doesn't have DOM properties in TS. const target = event.target as HTMLElement | null; if (!target) return; let node = target.parentNode as HTMLElement | null; if (!node) return; if (node.tagName !== 'DIV') { node = node.parentNode as HTMLElement | null; if (!node) return; } const value = node.innerText; workspaceSetter({ ...workspace, excludedPlugins: excludedPlugins.filter(item => item !== value) }, true); }, }, }} filterOptions={(options, parameters) => { const filtered = autocompleteExcludedPluginsFilter(options, parameters); if (parameters.inputValue !== '') { filtered.push(parameters.inputValue); } return filtered; }} groupBy={(option) => option.split('/')[2]} renderInput={(parameters: AutocompleteRenderInputParams) => ( )} /> ); }