Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/features/settings/components/LocalModeSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ExternalLink, Server } from 'lucide-react';

interface LocalModeSettingsProps {
faasUrl: string;
}

/** Settings for a Launchpad served against a local FaaS, where there is no account. */
export function LocalModeSettings({ faasUrl }: LocalModeSettingsProps) {
return (
<div className="grow flex flex-col items-center justify-start p-4 bg-white animate-in fade-in duration-500">
<div className="w-full max-w-350 flex flex-col mt-4">
<div className="flex justify-between items-center mb-10 pb-6 border-b-2 border-slate-200">
<div>
<h2 className="text-3xl font-medium text-slate-800 tracking-tight">Settings</h2>
<p className="text-gray-500 mt-1 text-sm font-medium">
Launchpad is connected to the MetaCall FaaS running on this machine.
</p>
</div>
</div>

<div className="border border-slate-300 bg-white flex flex-col max-w-2xl">
<div className="bg-slate-50 text-slate-800 text-[11px] uppercase tracking-widest font-bold px-6 py-4 border-b border-slate-300 flex items-center gap-2">
<Server size={14} className="text-slate-600" />
Local Mode
</div>
<div className="p-6 flex flex-col gap-6">
<div>
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest mb-1.5">
FaaS URL
</p>
<p className="border-b border-slate-300 text-slate-800 py-2.5 text-sm font-mono break-all">
{faasUrl}
</p>
</div>
<p className="text-[12px] text-gray-600 leading-relaxed">
There is no account in local mode, so profile, password, CLI token and billing
settings are not available here. They are on the hosted dashboard.
</p>
<a
href="https://dashboard.metacall.io"
target="_blank"
rel="noreferrer"
className="w-full flex justify-between items-center px-4 py-3 border border-slate-300 text-slate-700 hover:bg-slate-50 transition-all text-xs uppercase tracking-wider font-bold"
>
dashboard.metacall.io
<ExternalLink size={14} className="text-gray-400" />
</a>
</div>
</div>
</div>
</div>
);
}
11 changes: 9 additions & 2 deletions src/features/settings/pages/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import {
Download,
} from 'lucide-react';
import { CopyButton } from '@/shared/ui/CopyButton';
import { api } from '@/lib/api-client';
import { api, getBaseUrl } from '@/lib/api-client';
import { isLocalMode } from '@/app/config/runtime';
import { useAuth } from '@/features/auth/hooks/useAuth';
import { LS_TOKEN_KEY, LS_EMAIL_KEY } from '@/shared/constants';
import { normalizePlan } from '@/shared/lib/plan';
import { downloadInvoicePDF, formatInvoiceNumber } from '../utils/invoice';
import { LocalModeSettings } from '../components/LocalModeSettings';
import type { Deployment } from '@/shared/types';
import type { SubscriptionDeploy } from '@metacall/protocol';

Expand Down Expand Up @@ -71,6 +73,7 @@ const PdfIcon = ({ className = '', size = 16 }: PdfIconProps) => (
export default function SettingsPage() {
const navigate = useNavigate();
const { logout } = useAuth();
const localMode = isLocalMode();
const [email] = useState(() => localStorage.getItem(LS_EMAIL_KEY) ?? 'example@gmail.com');
const [vatId, setVatId] = useState(() => localStorage.getItem('faas_vat_id') ?? '');
const [currentPassword, setCurrentPassword] = useState('');
Expand Down Expand Up @@ -103,8 +106,10 @@ export default function SettingsPage() {
}, []);

useEffect(() => {
// Local FaaS has no billing, so there is nothing to load
if (localMode) return;
fetchSettingsData();
}, [fetchSettingsData]);
}, [fetchSettingsData, localMode]);

const billingHistory = useMemo(() => {
if (subDeploys.length > 0) {
Expand Down Expand Up @@ -236,6 +241,8 @@ export default function SettingsPage() {
logout();
};

if (localMode) return <LocalModeSettings faasUrl={getBaseUrl()} />;

return (
<div className="grow flex flex-col items-center justify-start p-4 bg-white animate-in fade-in duration-500">
<div className="w-full max-w-350 flex flex-col mt-4">
Expand Down
66 changes: 66 additions & 0 deletions src/tests/unit/settings_page_test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MemoryRouter } from 'react-router-dom';
import { AuthProvider, useAuth } from '@/features/auth/hooks/useAuth';
import SettingsPage from '@/features/settings/pages/SettingsPage';
import { api } from '@/lib/api-client';
import { applyRuntimeConfig } from '@/app/config/runtime';

vi.mock('@/lib/api-client', () => ({
api: {
Expand All @@ -12,6 +13,7 @@ vi.mock('@/lib/api-client', () => ({
listSubscriptionsDeploys: vi.fn().mockResolvedValue([]),
changePassword: vi.fn(),
},
getBaseUrl: vi.fn(() => 'http://localhost:9000'),
}));

// Exposes the auth state that ProtectedRoute relies on
Expand Down Expand Up @@ -139,3 +141,67 @@ describe('SettingsPage password update', () => {
expect(fields.confirmation).toHaveValue('');
});
});

describe('SettingsPage in local mode', () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
applyRuntimeConfig({ mode: 'local', faasUrl: 'http://localhost:9000' });
});

afterEach(() => {
applyRuntimeConfig({});
});

const renderPage = () =>
render(
<MemoryRouter initialEntries={['/settings']}>
<AuthProvider>
<SettingsPage />
</AuthProvider>
</MemoryRouter>,
);

it('shows the local mode notice and the FaaS URL', () => {
renderPage();

expect(screen.getByText(/no account in local mode/i)).toBeInTheDocument();
expect(screen.getByText('http://localhost:9000')).toBeInTheDocument();
expect(screen.getByRole('link', { name: /dashboard\.metacall\.io/i })).toHaveAttribute(
'href',
'https://dashboard.metacall.io',
);
});

it('hides the account, security and billing panels', () => {
renderPage();

expect(screen.queryByText('CLI Token')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /refresh token/i })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /update password/i })).not.toBeInTheDocument();
expect(screen.queryByText('Company VAT')).not.toBeInTheDocument();
expect(screen.queryByText('Billing History')).not.toBeInTheDocument();
expect(screen.queryByText('Active Subscriptions')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /upgrade plan/i })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /delete account/i })).not.toBeInTheDocument();
});

it('does not load billing data', () => {
renderPage();

expect(api.listSubscriptions).not.toHaveBeenCalled();
expect(api.inspect).not.toHaveBeenCalled();
expect(api.listSubscriptionsDeploys).not.toHaveBeenCalled();
});

it('keeps the account panels outside local mode', async () => {
applyRuntimeConfig({});
localStorage.setItem('faas_token', 'token-1');
localStorage.setItem('faas_user_email', 'alice@example.com');
renderPage();

await waitFor(() => expect(api.listSubscriptionsDeploys).toHaveBeenCalled());
expect(screen.getByText('CLI Token')).toBeInTheDocument();
expect(screen.queryByText(/no account in local mode/i)).not.toBeInTheDocument();
});
});
Loading