:electron: Server config ui (#4870)

* server config ui 

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Michael Clark
2025-05-15 18:52:46 +01:00
committed by GitHub
parent d225e5d5e1
commit 70362f6801
4 changed files with 398 additions and 121 deletions

View File

@@ -4,10 +4,12 @@ import { Trans, useTranslation } from 'react-i18next';
import { Button, ButtonWithLoading } from '@actual-app/components/button';
import { BigInput } from '@actual-app/components/input';
import { Label } from '@actual-app/components/label';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import { saveGlobalPrefs } from 'loot-core/client/prefs/prefsSlice';
import { loggedIn, signOut } from 'loot-core/client/users/usersSlice';
import {
isNonProductionEnvironment,
@@ -24,6 +26,241 @@ import { Title } from './subscribe/common';
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
export function ElectronServerConfig({
onDoNotUseServer,
onSetServerConfigView,
}: {
onDoNotUseServer: () => void;
onSetServerConfigView: (view: 'internal' | 'external') => void;
}) {
const { t } = useTranslation();
const navigate = useNavigate();
const setServerUrl = useSetServerURL();
const currentUrl = useServerURL();
const dispatch = useDispatch();
const [syncServerConfig, setSyncServerConfig] =
useGlobalPref('syncServerConfig');
const [electronServerPort, setElectronServerPort] = useState(
syncServerConfig?.port || 5007,
);
const [configError, setConfigError] = useState<string | null>(null);
const canShowExternalServerConfig = !syncServerConfig?.port && !currentUrl;
const hasInternalServerConfig = syncServerConfig?.port;
const [startingSyncServer, setStartingSyncServer] = useState(false);
const onConfigureSyncServer = async () => {
if (
isNaN(electronServerPort) ||
electronServerPort <= 0 ||
electronServerPort > 65535
) {
setConfigError('Ports must be within range 1 - 65535');
return;
}
try {
setConfigError(null);
setStartingSyncServer(true);
// Ensure config is saved before starting the server
await dispatch(
saveGlobalPrefs({
prefs: {
syncServerConfig: {
...syncServerConfig,
port: electronServerPort,
autoStart: true,
},
},
}),
).unwrap();
await window.globalThis.Actual.stopSyncServer();
await window.globalThis.Actual.startSyncServer();
setStartingSyncServer(false);
initElectronSyncServerRunningStatus();
await setServerUrl(`http://localhost:${electronServerPort}`);
navigate('/');
} catch (error) {
setStartingSyncServer(false);
setConfigError('Failed to configure sync server');
console.error('Failed to configure sync server:', error);
}
};
const [electronSyncServerRunning, setElectronSyncServerRunning] =
useState(false);
const initElectronSyncServerRunningStatus = async () => {
setElectronSyncServerRunning(
await window.globalThis.Actual.isSyncServerRunning(),
);
};
useEffect(() => {
initElectronSyncServerRunningStatus();
}, []);
async function dontUseSyncServer() {
setSyncServerConfig(null);
if (electronSyncServerRunning) {
await window.globalThis.Actual.stopSyncServer();
}
onDoNotUseServer();
}
return (
<>
<Title text={t('Configure your server')} />
<View
style={{
display: 'flex',
flexDirection: 'column',
gap: 20,
}}
>
<Text
style={{
fontSize: 16,
color: theme.pageText,
lineHeight: 1.5,
}}
>
<Trans>
Configure your local server below to allow seamless data
synchronization across your devices, bank sync and more...
</Trans>
</Text>
{configError && (
<Text style={{ color: theme.errorText, marginTop: 10 }}>
{configError}
</Text>
)}
<View
style={{
display: 'flex',
flexDirection: 'row',
gap: 10,
}}
>
<View style={{ flexDirection: 'column', gap: 5, flex: 1 }}>
<Label title={t('Domain')} style={{ textAlign: 'left' }} />
<BigInput
value="localhost"
disabled
type="text"
style={{
'&::-webkit-outer-spin-button, &::-webkit-inner-spin-button': {
WebkitAppearance: 'none',
margin: 0,
},
}}
/>
</View>
<View style={{ flexDirection: 'column', gap: 5 }}>
<Label
title={t('Port')}
style={{ textAlign: 'left', width: '7ch' }}
/>
<BigInput
name="port"
value={String(electronServerPort)}
aria-label={t('Port')}
type="number"
style={{
'&::-webkit-outer-spin-button, &::-webkit-inner-spin-button': {
WebkitAppearance: 'none',
margin: 0,
},
width: '7ch',
textAlign: 'center',
}}
autoFocus={true}
maxLength={5}
onChange={event =>
setElectronServerPort(Number(event.target.value))
}
/>
</View>
<View
style={{
flexDirection: 'column',
gap: 5,
justifyContent: 'end',
}}
>
<Label title={t('')} style={{ textAlign: 'left', width: '7ch' }} />
{!electronSyncServerRunning ? (
<Button
variant="primary"
style={{ padding: 10, width: '8ch' }}
onPress={onConfigureSyncServer}
isPending={startingSyncServer}
>
<Trans>Start</Trans>
</Button>
) : (
<Button
variant="primary"
style={{ padding: 10, width: '8ch' }}
onPress={onConfigureSyncServer}
isPending={startingSyncServer}
>
<Trans>Save</Trans>
</Button>
)}
</View>
</View>
</View>
<View
style={{
flexDirection: 'row',
marginTop: 20,
gap: 15,
flexFlow: 'row wrap',
justifyContent: 'center',
}}
>
{hasInternalServerConfig && (
<Button
variant="bare"
style={{ color: theme.pageTextLight, margin: 5 }}
onPress={() => navigate(-1)}
>
<Trans>Cancel</Trans>
</Button>
)}
<Button
variant="bare"
style={{ color: theme.pageTextLight, margin: 5 }}
onPress={dontUseSyncServer}
>
<Trans>Dont use a server</Trans>
</Button>
{canShowExternalServerConfig && (
<Button
variant="bare"
style={{ color: theme.pageTextLight, margin: 5 }}
onPress={() => onSetServerConfigView('external')}
>
<Trans>Use an external server</Trans>
</Button>
)}
</View>
</>
);
}
export function ConfigServer() {
const { t } = useTranslation();
const dispatch = useDispatch();
@@ -61,7 +298,7 @@ export function ConfigServer() {
}
async function onSubmit() {
if (url === '' || loading) {
if (url === null || url === '' || loading) {
return;
}
@@ -118,153 +355,179 @@ export function ConfigServer() {
navigate('/');
}
const [syncServerConfig] = useGlobalPref('syncServerConfig');
const hasExternalServerConfig = !syncServerConfig?.port && !!currentUrl;
const [serverConfigView, onSetServerConfigView] = useState<
'internal' | 'external'
>(() => {
if (isElectron() && !hasExternalServerConfig) {
return 'internal';
}
return 'external';
});
return (
<View style={{ maxWidth: 500, marginTop: -30 }}>
<Title text={t('Wheres the server?')} />
<Text
style={{
fontSize: 16,
color: theme.tableRowHeaderText,
lineHeight: 1.5,
}}
>
{currentUrl ? (
<Trans>
Existing sessions will be logged out and you will log in to this
server. We will validate that Actual is running at this URL.
</Trans>
) : (
<Trans>
There is no server configured. After running the server, specify the
URL here to use the app. You can always change this later. We will
validate that Actual is running at this URL.
</Trans>
)}
</Text>
{error && (
{serverConfigView === 'internal' && (
<ElectronServerConfig
onDoNotUseServer={onSkip}
onSetServerConfigView={onSetServerConfigView}
/>
)}
{serverConfigView === 'external' && (
<>
<Title text={t('Wheres the server?')} />
<Text
style={{
marginTop: 20,
color: theme.errorText,
borderRadius: 4,
fontSize: 15,
fontSize: 16,
color: theme.tableRowHeaderText,
lineHeight: 1.5,
}}
>
{getErrorMessage(error)}
{currentUrl ? (
<Trans>
Existing sessions will be logged out and you will log in to this
server. We will validate that Actual is running at this URL.
</Trans>
) : (
<Trans>
There is no server configured. After running the server, specify
the URL here to use the app. You can always change this later.
We will validate that Actual is running at this URL.
</Trans>
)}
</Text>
{isElectron() && (
<View
style={{ display: 'flex', flexDirection: 'row', marginTop: 20 }}
>
{error && (
<>
<Text
style={{
marginTop: 20,
color: theme.errorText,
borderRadius: 4,
fontSize: 15,
}}
>
<Trans>
If the server is using a self-signed certificate{' '}
<Link
variant="text"
style={{ fontSize: 15 }}
onClick={onSelectSelfSignedCertificate}
>
select it here
</Link>
.
</Trans>
{getErrorMessage(error)}
</Text>
</View>
{isElectron() && (
<View
style={{
display: 'flex',
flexDirection: 'row',
marginTop: 20,
}}
>
<Text
style={{
color: theme.errorText,
borderRadius: 4,
fontSize: 15,
}}
>
<Trans>
If the server is using a self-signed certificate{' '}
<Link
variant="text"
style={{ fontSize: 15 }}
onClick={onSelectSelfSignedCertificate}
>
select it here
</Link>
.
</Trans>
</Text>
</View>
)}
</>
)}
</>
)}
<View style={{ display: 'flex', flexDirection: 'row', marginTop: 30 }}>
<BigInput
autoFocus={true}
placeholder={t('https://example.com')}
value={url || ''}
onChangeValue={setUrl}
style={{ flex: 1, marginRight: 10 }}
onEnter={onSubmit}
/>
<ButtonWithLoading
variant="primary"
isLoading={loading}
style={{ fontSize: 15 }}
onPress={onSubmit}
>
{t('OK')}
</ButtonWithLoading>
{currentUrl && (
<Button
variant="bare"
style={{ fontSize: 15, marginLeft: 10 }}
onPress={() => navigate(-1)}
<View
style={{ display: 'flex', flexDirection: 'row', marginTop: 30 }}
>
{t('Cancel')}
</Button>
)}
</View>
<View
style={{
flexDirection: 'row',
flexFlow: 'row wrap',
justifyContent: 'center',
marginTop: 15,
}}
>
{currentUrl ? (
<Button
variant="bare"
style={{ color: theme.pageTextLight }}
onPress={onSkip}
>
{t('Stop using a server')}
</Button>
) : (
<>
{!isElectron() && (
<BigInput
autoFocus={true}
placeholder={t('https://example.com')}
value={url || ''}
onChangeValue={setUrl}
style={{ flex: 1, marginRight: 10 }}
onEnter={onSubmit}
/>
<ButtonWithLoading
variant="primary"
isLoading={loading}
style={{ fontSize: 15 }}
onPress={onSubmit}
>
{t('OK')}
</ButtonWithLoading>
{currentUrl && (
<Button
variant="bare"
style={{
color: theme.pageTextLight,
margin: 5,
marginRight: 15,
}}
onPress={onSameDomain}
style={{ fontSize: 15, marginLeft: 10 }}
onPress={() => navigate(-1)}
>
{t('Use current domain')}
{t('Cancel')}
</Button>
)}
<Button
variant="bare"
style={{ color: theme.pageTextLight, margin: 5 }}
onPress={onSkip}
>
{t('Dont use a server')}
</Button>
{isNonProductionEnvironment() && (
</View>
<View
style={{
flexDirection: 'row',
flexFlow: 'row wrap',
justifyContent: 'center',
marginTop: 15,
}}
>
{currentUrl ? (
<Button
variant="primary"
style={{ marginLeft: 15 }}
onPress={async () => {
await onCreateTestFile();
navigate('/');
}}
variant="bare"
style={{ color: theme.pageTextLight }}
onPress={onSkip}
>
{t('Create test file')}
{t('Stop using a server')}
</Button>
) : (
<>
{!isElectron() && (
<Button
variant="bare"
style={{
color: theme.pageTextLight,
margin: 5,
marginRight: 15,
}}
onPress={onSameDomain}
>
{t('Use current domain')}
</Button>
)}
<Button
variant="bare"
style={{ color: theme.pageTextLight, margin: 5 }}
onPress={onSkip}
>
{t('Dont use a server')}
</Button>
{isNonProductionEnvironment() && (
<Button
variant="primary"
style={{ marginLeft: 15 }}
onPress={async () => {
await onCreateTestFile();
navigate('/');
}}
>
{t('Create test file')}
</Button>
)}
</>
)}
</>
)}
</View>
</View>
</>
)}
</View>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 534 KiB

After

Width:  |  Height:  |  Size: 531 KiB

View File

@@ -207,6 +207,14 @@ async function createBackgroundProcess() {
async function startSyncServer() {
try {
if (syncServerProcess) {
logMessage(
'info',
'Sync-Server: Already started! Ignoring request to start.',
);
return;
}
const globalPrefs = await loadGlobalPrefs();
const syncServerConfig = {

View File

@@ -0,0 +1,6 @@
---
category: Features
authors: [MikesGlitch]
---
Desktop app Sync Server server configuration screen