mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
Add UI for backend selection
This commit is contained in:
parent
93c3f9e84b
commit
fb704cc901
275
frontend/src/desktop/components/ConnectionSettings.tsx
Normal file
275
frontend/src/desktop/components/ConnectionSettings.tsx
Normal file
@ -0,0 +1,275 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Stack, Card, Badge, Button, Text, Group, Modal, TextInput, Radio } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
connectionModeService,
|
||||
ConnectionConfig,
|
||||
ServerConfig,
|
||||
} from '@app/services/connectionModeService';
|
||||
import { authService, UserInfo } from '@app/services/authService';
|
||||
import { LoginForm } from '@app/components/SetupWizard/LoginForm';
|
||||
import { STIRLING_SAAS_URL } from '@app/constants/connection';
|
||||
|
||||
export const ConnectionSettings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [config, setConfig] = useState<ConnectionConfig | null>(null);
|
||||
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showServerModal, setShowServerModal] = useState(false);
|
||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||
const [newServerConfig, setNewServerConfig] = useState<ServerConfig | null>(null);
|
||||
|
||||
// Load current config on mount
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
const currentConfig = await connectionModeService.getCurrentConfig();
|
||||
setConfig(currentConfig);
|
||||
|
||||
if (currentConfig.mode === 'server') {
|
||||
const user = await authService.getUserInfo();
|
||||
setUserInfo(user);
|
||||
}
|
||||
};
|
||||
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
const handleSwitchToOffline = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await connectionModeService.switchToOffline();
|
||||
|
||||
// Reload config
|
||||
const newConfig = await connectionModeService.getCurrentConfig();
|
||||
setConfig(newConfig);
|
||||
setUserInfo(null);
|
||||
} catch (error) {
|
||||
console.error('Failed to switch to offline:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSwitchToServer = () => {
|
||||
setShowServerModal(true);
|
||||
};
|
||||
|
||||
const handleServerConfigSubmit = (serverConfig: ServerConfig) => {
|
||||
setNewServerConfig(serverConfig);
|
||||
setShowServerModal(false);
|
||||
setShowLoginModal(true);
|
||||
};
|
||||
|
||||
const handleLogin = async (username: string, password: string) => {
|
||||
if (!newServerConfig) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Login
|
||||
await authService.login(newServerConfig.url, username, password);
|
||||
|
||||
// Switch to server mode
|
||||
await connectionModeService.switchToServer(newServerConfig);
|
||||
|
||||
// Reload config and user info
|
||||
const newConfig = await connectionModeService.getCurrentConfig();
|
||||
setConfig(newConfig);
|
||||
const user = await authService.getUserInfo();
|
||||
setUserInfo(user);
|
||||
|
||||
setShowLoginModal(false);
|
||||
setNewServerConfig(null);
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
throw error; // Let LoginForm handle the error
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await authService.logout();
|
||||
|
||||
// Switch to offline mode
|
||||
await connectionModeService.switchToOffline();
|
||||
|
||||
// Reload config
|
||||
const newConfig = await connectionModeService.getCurrentConfig();
|
||||
setConfig(newConfig);
|
||||
setUserInfo(null);
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!config) {
|
||||
return <Text>{t('common.loading', 'Loading...')}</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card shadow="sm" padding="lg" radius="md" withBorder>
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between">
|
||||
<Text fw={600}>{t('settings.connection.title', 'Connection Mode')}</Text>
|
||||
<Badge color={config.mode === 'offline' ? 'blue' : 'green'} variant="light">
|
||||
{config.mode === 'offline'
|
||||
? t('settings.connection.mode.offline', 'Offline')
|
||||
: t('settings.connection.mode.server', 'Server')}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
{config.mode === 'server' && config.server_config && (
|
||||
<>
|
||||
<div>
|
||||
<Text size="sm" fw={500}>
|
||||
{t('settings.connection.server', 'Server')}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{config.server_config.url}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{userInfo && (
|
||||
<div>
|
||||
<Text size="sm" fw={500}>
|
||||
{t('settings.connection.user', 'Logged in as')}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{userInfo.username}
|
||||
{userInfo.email && ` (${userInfo.email})`}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Group mt="md">
|
||||
{config.mode === 'offline' ? (
|
||||
<Button onClick={handleSwitchToServer} disabled={loading}>
|
||||
{t('settings.connection.switchToServer', 'Connect to Server')}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button onClick={handleSwitchToOffline} variant="default" disabled={loading}>
|
||||
{t('settings.connection.switchToOffline', 'Switch to Offline')}
|
||||
</Button>
|
||||
<Button onClick={handleLogout} color="red" variant="light" disabled={loading}>
|
||||
{t('settings.connection.logout', 'Logout')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Server selection modal */}
|
||||
<Modal
|
||||
opened={showServerModal}
|
||||
onClose={() => setShowServerModal(false)}
|
||||
title={t('settings.connection.selectServer', 'Select Server')}
|
||||
>
|
||||
<ServerSelectionInSettings onSubmit={handleServerConfigSubmit} />
|
||||
</Modal>
|
||||
|
||||
{/* Login modal */}
|
||||
<Modal
|
||||
opened={showLoginModal}
|
||||
onClose={() => {
|
||||
setShowLoginModal(false);
|
||||
setNewServerConfig(null);
|
||||
}}
|
||||
title={t('settings.connection.login', 'Login')}
|
||||
>
|
||||
{newServerConfig && (
|
||||
<LoginForm
|
||||
serverUrl={newServerConfig.url}
|
||||
onLogin={handleLogin}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// Mini server selection component for settings
|
||||
const ServerSelectionInSettings: React.FC<{ onSubmit: (config: ServerConfig) => void }> = ({
|
||||
onSubmit,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [serverType, setServerType] = useState<'saas' | 'selfhosted'>('saas');
|
||||
const [customUrl, setCustomUrl] = useState('');
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const url = serverType === 'saas' ? STIRLING_SAAS_URL : customUrl.trim();
|
||||
|
||||
if (!url) {
|
||||
setError(t('setup.server.error.emptyUrl', 'Please enter a server URL'));
|
||||
return;
|
||||
}
|
||||
|
||||
setTesting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const isReachable = await connectionModeService.testConnection(url);
|
||||
|
||||
if (!isReachable) {
|
||||
setError(t('setup.server.error.unreachable', 'Could not connect to server'));
|
||||
setTesting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit({
|
||||
url,
|
||||
server_type: serverType,
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t('setup.server.error.testFailed', 'Connection test failed'));
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Radio.Group value={serverType} onChange={(value) => setServerType(value as 'saas' | 'selfhosted')}>
|
||||
<Stack gap="xs">
|
||||
<Radio value="saas" label={t('setup.server.type.saas', 'Stirling PDF SaaS')} />
|
||||
<Radio value="selfhosted" label={t('setup.server.type.selfhosted', 'Self-hosted server')} />
|
||||
</Stack>
|
||||
</Radio.Group>
|
||||
|
||||
{serverType === 'selfhosted' && (
|
||||
<TextInput
|
||||
label={t('setup.server.url.label', 'Server URL')}
|
||||
placeholder="https://your-server.com"
|
||||
value={customUrl}
|
||||
onChange={(e) => {
|
||||
setCustomUrl(e.target.value);
|
||||
setError(null);
|
||||
}}
|
||||
disabled={testing}
|
||||
error={error}
|
||||
/>
|
||||
)}
|
||||
|
||||
{error && !customUrl && (
|
||||
<Text c="red" size="sm">
|
||||
{error}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Button onClick={handleSubmit} loading={testing} fullWidth>
|
||||
{testing ? t('setup.server.testing', 'Testing...') : t('common.continue', 'Continue')}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
78
frontend/src/desktop/components/SetupWizard/LoginForm.tsx
Normal file
78
frontend/src/desktop/components/SetupWizard/LoginForm.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Stack, TextInput, PasswordInput, Button, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface LoginFormProps {
|
||||
serverUrl: string;
|
||||
onLogin: (username: string, password: string) => Promise<void>;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export const LoginForm: React.FC<LoginFormProps> = ({ serverUrl, onLogin, loading }) => {
|
||||
const { t } = useTranslation();
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Validation
|
||||
if (!username.trim()) {
|
||||
setValidationError(t('setup.login.error.emptyUsername', 'Please enter your username'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
setValidationError(t('setup.login.error.emptyPassword', 'Please enter your password'));
|
||||
return;
|
||||
}
|
||||
|
||||
setValidationError(null);
|
||||
await onLogin(username.trim(), password);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack gap="md" mt="lg">
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('setup.login.connectingTo', 'Connecting to:')} <strong>{serverUrl}</strong>
|
||||
</Text>
|
||||
|
||||
<TextInput
|
||||
label={t('setup.login.username.label', 'Username')}
|
||||
placeholder={t('setup.login.username.placeholder', 'Enter your username')}
|
||||
value={username}
|
||||
onChange={(e) => {
|
||||
setUsername(e.target.value);
|
||||
setValidationError(null);
|
||||
}}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label={t('setup.login.password.label', 'Password')}
|
||||
placeholder={t('setup.login.password.placeholder', 'Enter your password')}
|
||||
value={password}
|
||||
onChange={(e) => {
|
||||
setPassword(e.target.value);
|
||||
setValidationError(null);
|
||||
}}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
|
||||
{validationError && (
|
||||
<Text c="red" size="sm">
|
||||
{validationError}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Button type="submit" loading={loading} disabled={loading} mt="md">
|
||||
{t('setup.login.submit', 'Login')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { Stack, Button, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IconCloud, IconDeviceDesktop } from '@tabler/icons-react';
|
||||
|
||||
interface ModeSelectionProps {
|
||||
onSelect: (mode: 'offline' | 'server') => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export const ModeSelection: React.FC<ModeSelectionProps> = ({ onSelect, loading }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="md" mt="lg">
|
||||
<Button
|
||||
size="lg"
|
||||
variant="light"
|
||||
onClick={() => onSelect('offline')}
|
||||
disabled={loading}
|
||||
leftSection={<IconDeviceDesktop size={24} />}
|
||||
>
|
||||
<div style={{ textAlign: 'left', flex: 1 }}>
|
||||
<Text fw={600}>{t('setup.mode.offline.title', 'Use Offline')}</Text>
|
||||
<Text size="sm" c="dimmed" fw={400}>
|
||||
{t('setup.mode.offline.description', 'Run locally without an internet connection')}
|
||||
</Text>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
variant="light"
|
||||
onClick={() => onSelect('server')}
|
||||
disabled={loading}
|
||||
leftSection={<IconCloud size={24} />}
|
||||
>
|
||||
<div style={{ textAlign: 'left', flex: 1 }}>
|
||||
<Text fw={600}>{t('setup.mode.server.title', 'Connect to Server')}</Text>
|
||||
<Text size="sm" c="dimmed" fw={400}>
|
||||
{t('setup.mode.server.description', 'Connect to a remote Stirling PDF server')}
|
||||
</Text>
|
||||
</div>
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,98 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Stack, Button, TextInput, Radio, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ServerConfig } from '@app/services/connectionModeService';
|
||||
import { connectionModeService } from '@app/services/connectionModeService';
|
||||
import { STIRLING_SAAS_URL } from '@app/constants/connection';
|
||||
|
||||
interface ServerSelectionProps {
|
||||
onSelect: (config: ServerConfig) => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export const ServerSelection: React.FC<ServerSelectionProps> = ({ onSelect, loading }) => {
|
||||
const { t } = useTranslation();
|
||||
const [serverType, setServerType] = useState<'saas' | 'selfhosted'>('saas');
|
||||
const [customUrl, setCustomUrl] = useState('');
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testError, setTestError] = useState<string | null>(null);
|
||||
|
||||
const handleContinue = async () => {
|
||||
const url = serverType === 'saas' ? STIRLING_SAAS_URL : customUrl.trim();
|
||||
|
||||
if (!url) {
|
||||
setTestError(t('setup.server.error.emptyUrl', 'Please enter a server URL'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Test connection before proceeding
|
||||
setTesting(true);
|
||||
setTestError(null);
|
||||
|
||||
try {
|
||||
const isReachable = await connectionModeService.testConnection(url);
|
||||
|
||||
if (!isReachable) {
|
||||
setTestError(t('setup.server.error.unreachable', 'Could not connect to server'));
|
||||
setTesting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Connection successful
|
||||
onSelect({
|
||||
url,
|
||||
server_type: serverType,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Connection test failed:', error);
|
||||
setTestError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t('setup.server.error.testFailed', 'Connection test failed')
|
||||
);
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="md" mt="lg">
|
||||
<Radio.Group value={serverType} onChange={(value) => setServerType(value as 'saas' | 'selfhosted')}>
|
||||
<Stack gap="xs">
|
||||
<Radio value="saas" label={t('setup.server.type.saas', 'Stirling PDF SaaS (stirling.com/app)')} />
|
||||
<Radio value="selfhosted" label={t('setup.server.type.selfhosted', 'Self-hosted server')} />
|
||||
</Stack>
|
||||
</Radio.Group>
|
||||
|
||||
{serverType === 'selfhosted' && (
|
||||
<TextInput
|
||||
label={t('setup.server.url.label', 'Server URL')}
|
||||
placeholder="https://your-server.com"
|
||||
value={customUrl}
|
||||
onChange={(e) => {
|
||||
setCustomUrl(e.target.value);
|
||||
setTestError(null);
|
||||
}}
|
||||
disabled={loading || testing}
|
||||
error={testError}
|
||||
description={t(
|
||||
'setup.server.url.description',
|
||||
'Enter the full URL of your self-hosted Stirling PDF server'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{testError && (
|
||||
<Text c="red" size="sm">
|
||||
{testError}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Button onClick={handleContinue} loading={testing || loading} disabled={loading} mt="md">
|
||||
{testing
|
||||
? t('setup.server.testing', 'Testing connection...')
|
||||
: t('common.continue', 'Continue')}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
163
frontend/src/desktop/components/SetupWizard/index.tsx
Normal file
163
frontend/src/desktop/components/SetupWizard/index.tsx
Normal file
@ -0,0 +1,163 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Container, Paper, Stepper, Button, Group, Title, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ModeSelection } from '@app/components/SetupWizard/ModeSelection';
|
||||
import { ServerSelection } from '@app/components/SetupWizard/ServerSelection';
|
||||
import { LoginForm } from '@app/components/SetupWizard/LoginForm';
|
||||
import { connectionModeService, ServerConfig } from '@app/services/connectionModeService';
|
||||
import { authService } from '@app/services/authService';
|
||||
import { tauriBackendService } from '@app/services/tauriBackendService';
|
||||
|
||||
enum SetupStep {
|
||||
ModeSelection,
|
||||
ServerSelection,
|
||||
Login,
|
||||
}
|
||||
|
||||
interface SetupWizardProps {
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
|
||||
const { t } = useTranslation();
|
||||
const [activeStep, setActiveStep] = useState<SetupStep>(SetupStep.ModeSelection);
|
||||
const [_selectedMode, setSelectedMode] = useState<'offline' | 'server' | null>(null);
|
||||
const [serverConfig, setServerConfig] = useState<ServerConfig | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleModeSelection = (mode: 'offline' | 'server') => {
|
||||
setSelectedMode(mode);
|
||||
setError(null);
|
||||
|
||||
if (mode === 'offline') {
|
||||
// Skip directly to completion for offline mode
|
||||
handleOfflineSetup();
|
||||
} else {
|
||||
// Go to server selection
|
||||
setActiveStep(SetupStep.ServerSelection);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOfflineSetup = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Set connection mode to offline
|
||||
await connectionModeService.switchToOffline();
|
||||
|
||||
// Start the local backend
|
||||
await tauriBackendService.startBackend();
|
||||
|
||||
// Complete setup
|
||||
onComplete();
|
||||
} catch (err) {
|
||||
console.error('Failed to set up offline mode:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to set up offline mode');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleServerSelection = (config: ServerConfig) => {
|
||||
setServerConfig(config);
|
||||
setError(null);
|
||||
setActiveStep(SetupStep.Login);
|
||||
};
|
||||
|
||||
const handleLogin = async (username: string, password: string) => {
|
||||
if (!serverConfig) {
|
||||
setError('No server configured');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Attempt login
|
||||
await authService.login(serverConfig.url, username, password);
|
||||
|
||||
// Set connection mode to server
|
||||
await connectionModeService.switchToServer(serverConfig);
|
||||
|
||||
// Start local backend (for hybrid execution support)
|
||||
await tauriBackendService.startBackend();
|
||||
|
||||
// Complete setup
|
||||
onComplete();
|
||||
} catch (err) {
|
||||
console.error('Login failed:', err);
|
||||
setError(err instanceof Error ? err.message : 'Login failed');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setError(null);
|
||||
if (activeStep === SetupStep.Login) {
|
||||
// From login back to server selection
|
||||
setActiveStep(SetupStep.ServerSelection);
|
||||
} else if (activeStep === SetupStep.ServerSelection) {
|
||||
// From server selection back to mode selection
|
||||
setActiveStep(SetupStep.ModeSelection);
|
||||
setSelectedMode(null);
|
||||
setServerConfig(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container size="sm" style={{ marginTop: '4rem' }}>
|
||||
<Paper shadow="md" p="xl" radius="md">
|
||||
<Title order={1} mb="md">
|
||||
{t('setup.welcome', 'Welcome to Stirling PDF')}
|
||||
</Title>
|
||||
|
||||
<Text size="sm" c="dimmed" mb="xl">
|
||||
{t('setup.description', 'Get started by choosing how you want to use Stirling PDF')}
|
||||
</Text>
|
||||
|
||||
<Stepper active={activeStep} breakpoint="sm">
|
||||
<Stepper.Step
|
||||
label={t('setup.step1.label', 'Choose Mode')}
|
||||
description={t('setup.step1.description', 'Offline or Server')}
|
||||
>
|
||||
<ModeSelection onSelect={handleModeSelection} loading={loading} />
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step
|
||||
label={t('setup.step2.label', 'Select Server')}
|
||||
description={t('setup.step2.description', 'SaaS or Self-hosted')}
|
||||
>
|
||||
<ServerSelection onSelect={handleServerSelection} loading={loading} />
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step
|
||||
label={t('setup.step3.label', 'Login')}
|
||||
description={t('setup.step3.description', 'Enter credentials')}
|
||||
>
|
||||
<LoginForm
|
||||
serverUrl={serverConfig?.url || ''}
|
||||
onLogin={handleLogin}
|
||||
loading={loading}
|
||||
/>
|
||||
</Stepper.Step>
|
||||
</Stepper>
|
||||
|
||||
{error && (
|
||||
<Text c="red" size="sm" mt="md">
|
||||
{error}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{activeStep > SetupStep.ModeSelection && (
|
||||
<Group justify="center" mt="xl">
|
||||
<Button variant="default" onClick={handleBack} disabled={loading}>
|
||||
{t('common.back', 'Back')}
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
5
frontend/src/desktop/constants/connection.ts
Normal file
5
frontend/src/desktop/constants/connection.ts
Normal file
@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Connection-related constants for desktop app
|
||||
*/
|
||||
|
||||
export const STIRLING_SAAS_URL = 'https://stirling.com/app';
|
||||
@ -1,18 +1,55 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useBackendInitializer } from '@app/hooks/useBackendInitializer';
|
||||
import { useOpenedFile } from '@app/hooks/useOpenedFile';
|
||||
import { fileOpenService } from '@app/services/fileOpenService';
|
||||
import { useFileManagement } from '@app/contexts/file/fileHooks';
|
||||
import { connectionModeService } from '@app/services/connectionModeService';
|
||||
import { authService } from '@app/services/authService';
|
||||
|
||||
/**
|
||||
* App initialization hook
|
||||
* Desktop version: Handles Tauri-specific initialization
|
||||
* - Starts the backend on app startup
|
||||
* - Checks for first launch and shows setup wizard if needed
|
||||
* - Starts the backend on app startup (after setup)
|
||||
* - Initializes auth state for server mode
|
||||
* - Handles files opened with the app (adds directly to FileContext)
|
||||
*/
|
||||
export function useAppInitialization(): void {
|
||||
// Initialize backend on app startup
|
||||
useBackendInitializer();
|
||||
export function useAppInitialization(): { isFirstLaunch: boolean; setupComplete: boolean } {
|
||||
const [isFirstLaunch, setIsFirstLaunch] = useState(false);
|
||||
const [setupComplete, setSetupComplete] = useState(false);
|
||||
const setupCheckCompleteRef = useRef(false);
|
||||
|
||||
// Check if this is first launch
|
||||
useEffect(() => {
|
||||
const checkFirstLaunch = async () => {
|
||||
try {
|
||||
const firstLaunch = await connectionModeService.isFirstLaunch();
|
||||
setIsFirstLaunch(firstLaunch);
|
||||
|
||||
if (!firstLaunch) {
|
||||
// Not first launch - initialize normally
|
||||
await authService.initializeAuthState();
|
||||
setSetupComplete(true);
|
||||
}
|
||||
|
||||
setupCheckCompleteRef.current = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to check first launch:', error);
|
||||
// On error, assume not first launch and proceed
|
||||
setIsFirstLaunch(false);
|
||||
setSetupComplete(true);
|
||||
setupCheckCompleteRef.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
if (!setupCheckCompleteRef.current) {
|
||||
checkFirstLaunch();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Initialize backend on app startup (only if setup is complete)
|
||||
const shouldStartBackend = setupComplete && !isFirstLaunch;
|
||||
useBackendInitializer(shouldStartBackend);
|
||||
|
||||
// Get file management actions
|
||||
const { addFiles } = useFileManagement();
|
||||
@ -91,4 +128,14 @@ export function useAppInitialization(): void {
|
||||
// Clean up listener on unmount
|
||||
return unlisten;
|
||||
}, [addFiles]);
|
||||
|
||||
return { isFirstLaunch, setupComplete };
|
||||
}
|
||||
|
||||
export function useSetupCompletion(): (completed: boolean) => void {
|
||||
const [, setSetupComplete] = useState(false);
|
||||
|
||||
return (completed: boolean) => {
|
||||
setSetupComplete(completed);
|
||||
};
|
||||
}
|
||||
|
||||
@ -5,12 +5,18 @@ import { tauriBackendService } from '@app/services/tauriBackendService';
|
||||
|
||||
/**
|
||||
* Hook to initialize backend and monitor health
|
||||
* @param enabled - Whether to initialize the backend (default: true)
|
||||
*/
|
||||
export function useBackendInitializer() {
|
||||
export function useBackendInitializer(enabled = true) {
|
||||
const { status, checkHealth } = useBackendHealth();
|
||||
const { backendUrl } = useEndpointConfig();
|
||||
|
||||
useEffect(() => {
|
||||
// Skip if disabled
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if backend already running
|
||||
if (tauriBackendService.isBackendRunning()) {
|
||||
void checkHealth();
|
||||
@ -36,5 +42,5 @@ export function useBackendInitializer() {
|
||||
if (status !== 'healthy' && status !== 'starting') {
|
||||
void initializeBackend();
|
||||
}
|
||||
}, [status, backendUrl, checkHealth]);
|
||||
}, [enabled, status, backendUrl, checkHealth]);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { connectionModeService, ConnectionMode } from './connectionModeService';
|
||||
import { connectionModeService } from '@app/services/connectionModeService';
|
||||
|
||||
export type ExecutionTarget = 'local' | 'remote';
|
||||
|
||||
@ -14,10 +14,10 @@ export class OperationRouter {
|
||||
|
||||
/**
|
||||
* Determines where an operation should execute
|
||||
* @param operation - The operation name (for future operation classification)
|
||||
* @param _operation - The operation name (for future operation classification)
|
||||
* @returns 'local' or 'remote'
|
||||
*/
|
||||
async getExecutionTarget(operation?: string): Promise<ExecutionTarget> {
|
||||
async getExecutionTarget(_operation?: string): Promise<ExecutionTarget> {
|
||||
const mode = await connectionModeService.getCurrentMode();
|
||||
|
||||
// Current implementation: simple mode-based routing
|
||||
@ -37,11 +37,11 @@ export class OperationRouter {
|
||||
|
||||
/**
|
||||
* Gets the base URL for an operation based on execution target
|
||||
* @param operation - The operation name (for future operation classification)
|
||||
* @param _operation - The operation name (for future operation classification)
|
||||
* @returns Base URL for API calls
|
||||
*/
|
||||
async getBaseUrl(operation?: string): Promise<string> {
|
||||
const target = await this.getExecutionTarget(operation);
|
||||
async getBaseUrl(_operation?: string): Promise<string> {
|
||||
const target = await this.getExecutionTarget(_operation);
|
||||
|
||||
if (target === 'local') {
|
||||
return 'http://localhost:8080';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user