Skip to content

Commit 8eaf71e

Browse files
feat: implement runtime base url configuration (#310)
1 parent 3aa3b2a commit 8eaf71e

17 files changed

+1270
-229
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/map/constants.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { VALHALLA_OSM_URL } from '@/utils/valhalla';
2-
31
const centerCoords = import.meta.env.VITE_CENTER_COORDS?.split(',') || [];
42

53
export const DEFAULT_CENTER: [number, number] = [
@@ -13,11 +11,8 @@ export const maxBounds: [[number, number], [number, number]] | undefined =
1311
undefined;
1412

1513
export const routeObjects = {
16-
[VALHALLA_OSM_URL!]: {
17-
color: '#0066ff',
18-
alternativeColor: '#66a3ff',
19-
name: 'OSM',
20-
},
14+
color: '#0066ff',
15+
alternativeColor: '#66a3ff',
2116
};
2217

2318
export const MAP_STYLE_STORAGE_KEY = 'selectedMapStyle';

src/components/map/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import 'maplibre-gl/dist/maplibre-gl.css';
1616
import axios from 'axios';
1717
import { throttle } from 'throttle-debounce';
1818
import {
19-
VALHALLA_OSM_URL,
19+
getValhallaUrl,
2020
buildHeightRequest,
2121
buildLocateRequest,
2222
} from '@/utils/valhalla';
@@ -262,7 +262,7 @@ export const MapComponent = () => {
262262
const getHeight = useCallback((lng: number, lat: number) => {
263263
setIsHeightLoading(true);
264264
axios
265-
.post(VALHALLA_OSM_URL + '/height', buildHeightRequest([[lat, lng]]), {
265+
.post(getValhallaUrl() + '/height', buildHeightRequest([[lat, lng]]), {
266266
headers: {
267267
'Content-Type': 'application/json',
268268
},
@@ -285,7 +285,7 @@ export const MapComponent = () => {
285285
setIsLocateLoading(true);
286286
axios
287287
.post(
288-
VALHALLA_OSM_URL + '/locate',
288+
getValhallaUrl() + '/locate',
289289
buildLocateRequest({ lng, lat }, profile || 'bicycle'),
290290
{
291291
headers: {
@@ -336,7 +336,7 @@ export const MapComponent = () => {
336336
setIsHeightLoading(true);
337337
setHeightPayload(heightPayloadNew);
338338
axios
339-
.post(VALHALLA_OSM_URL + '/height', heightPayloadNew, {
339+
.post(getValhallaUrl() + '/height', heightPayloadNew, {
340340
headers: {
341341
'Content-Type': 'application/json',
342342
},

src/components/map/parts/route-lines.spec.tsx

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,6 @@ vi.mock('@/stores/directions-store', () => ({
2323
mockUseDirectionsStore(selector),
2424
}));
2525

26-
vi.mock('@/utils/valhalla', () => ({
27-
VALHALLA_OSM_URL: 'https://valhalla.example.com',
28-
}));
29-
30-
vi.mock('../constants', () => ({
31-
routeObjects: {
32-
'https://valhalla.example.com': {
33-
color: '#0066ff',
34-
alternativeColor: '#999999',
35-
},
36-
},
37-
}));
38-
3926
const createMockState = (overrides = {}) => ({
4027
results: {
4128
data: {

src/components/map/parts/route-lines.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { useMemo } from 'react';
22
import { Source, Layer } from 'react-map-gl/maplibre';
33
import { useDirectionsStore } from '@/stores/directions-store';
4-
import { VALHALLA_OSM_URL } from '@/utils/valhalla';
54
import { routeObjects } from '../constants';
65
import type { Feature, FeatureCollection, LineString } from 'geojson';
76
import type { ParsedDirectionsGeometry } from '@/components/types';
@@ -34,7 +33,7 @@ export function RouteLines() {
3433
coordinates: coords.map((c) => [c[1] ?? 0, c[0] ?? 0]),
3534
},
3635
properties: {
37-
color: routeObjects[VALHALLA_OSM_URL!]!.alternativeColor,
36+
color: routeObjects.alternativeColor,
3837
type: 'alternate',
3938
summary,
4039
},
@@ -53,7 +52,7 @@ export function RouteLines() {
5352
coordinates: coords.map((c) => [c[1] ?? 0, c[0] ?? 0]),
5453
},
5554
properties: {
56-
color: routeObjects[VALHALLA_OSM_URL!]!.color,
55+
color: routeObjects.color,
5756
type: 'main',
5857
summary,
5958
},

src/components/route-planner.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { format } from 'date-fns';
33
import { DirectionsControl } from './directions/directions';
44
import { IsochronesControl } from './isochrones/isochrones';
55
import { useCommonStore } from '@/stores/common-store';
6-
import { VALHALLA_OSM_URL } from '@/utils/valhalla';
6+
import { getValhallaUrl } from '@/utils/valhalla';
77
import {
88
Sheet,
99
SheetContent,
@@ -40,7 +40,7 @@ export const RoutePlanner = () => {
4040
} = useQuery({
4141
queryKey: ['lastUpdate'],
4242
queryFn: async () => {
43-
const response = await fetch(`${VALHALLA_OSM_URL}/status`);
43+
const response = await fetch(`${getValhallaUrl()}/status`);
4444
const data = await response.json();
4545
return new Date(data.tileset_last_modified * 1000);
4646
},
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { useState } from 'react';
2+
import { useMutation } from '@tanstack/react-query';
3+
import { Button } from '@/components/ui/button';
4+
import { Input } from '@/components/ui/input';
5+
import {
6+
getBaseUrl,
7+
setBaseUrl,
8+
getDefaultBaseUrl,
9+
validateBaseUrl,
10+
normalizeBaseUrl,
11+
testConnection,
12+
} from '@/utils/base-url';
13+
import { CollapsibleSection } from '@/components/ui/collapsible-section';
14+
import { Server, RotateCcw, Loader2 } from 'lucide-react';
15+
import {
16+
Field,
17+
FieldLabel,
18+
FieldDescription,
19+
FieldError,
20+
} from '@/components/ui/field';
21+
22+
export const ServerSettings = () => {
23+
const [baseUrl, setBaseUrlState] = useState<string>(() => getBaseUrl());
24+
const [isOpen, setIsOpen] = useState(false);
25+
26+
const connectionMutation = useMutation({
27+
mutationFn: testConnection,
28+
onSuccess: (result, url) => {
29+
if (result.reachable) {
30+
const normalizedUrl = normalizeBaseUrl(url);
31+
setBaseUrl(normalizedUrl);
32+
setBaseUrlState(normalizedUrl);
33+
}
34+
},
35+
});
36+
37+
const getErrorMessage = (): string | null => {
38+
if (connectionMutation.error) {
39+
return connectionMutation.error.message || 'Connection failed';
40+
}
41+
if (connectionMutation.data && !connectionMutation.data.reachable) {
42+
return connectionMutation.data.error || 'Server unreachable';
43+
}
44+
return null;
45+
};
46+
47+
const errorMessage = getErrorMessage();
48+
49+
const handleBaseUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
50+
setBaseUrlState(e.target.value);
51+
connectionMutation.reset();
52+
};
53+
54+
const handleBaseUrlBlur = () => {
55+
const currentStoredUrl = getBaseUrl();
56+
const trimmedUrl = baseUrl.trim();
57+
58+
if (trimmedUrl === currentStoredUrl) {
59+
return;
60+
}
61+
62+
const lastTestedUrl = connectionMutation.variables;
63+
if (lastTestedUrl === trimmedUrl && errorMessage) {
64+
return;
65+
}
66+
67+
if (trimmedUrl === '' || trimmedUrl === getDefaultBaseUrl()) {
68+
setBaseUrl(trimmedUrl);
69+
setBaseUrlState(trimmedUrl || getDefaultBaseUrl());
70+
connectionMutation.reset();
71+
return;
72+
}
73+
74+
const validation = validateBaseUrl(trimmedUrl);
75+
if (!validation.valid) {
76+
connectionMutation.mutate(trimmedUrl);
77+
return;
78+
}
79+
80+
connectionMutation.mutate(trimmedUrl);
81+
};
82+
83+
const handleResetBaseUrl = () => {
84+
const defaultUrl = getDefaultBaseUrl();
85+
setBaseUrlState(defaultUrl);
86+
setBaseUrl(defaultUrl);
87+
connectionMutation.reset();
88+
};
89+
90+
return (
91+
<CollapsibleSection
92+
title="Server Settings"
93+
icon={Server}
94+
open={isOpen}
95+
onOpenChange={setIsOpen}
96+
>
97+
<div className="space-y-2">
98+
<Field data-invalid={!!errorMessage}>
99+
<FieldLabel htmlFor="base-url-input">Base URL</FieldLabel>
100+
<FieldDescription>
101+
The Valhalla server URL for routing and isochrone requests
102+
</FieldDescription>
103+
<div className="flex gap-2">
104+
<div className="flex-1 relative">
105+
<Input
106+
id="base-url-input"
107+
type="url"
108+
placeholder="https://valhalla.example.com"
109+
value={baseUrl}
110+
onChange={handleBaseUrlChange}
111+
onBlur={handleBaseUrlBlur}
112+
disabled={connectionMutation.isPending}
113+
aria-invalid={!!errorMessage}
114+
className={
115+
errorMessage
116+
? 'border-destructive focus-visible:ring-destructive/50'
117+
: ''
118+
}
119+
/>
120+
{connectionMutation.isPending && (
121+
<div className="absolute right-3 top-1/2 -translate-y-1/2">
122+
<Loader2 className="size-4 animate-spin text-muted-foreground" />
123+
</div>
124+
)}
125+
</div>
126+
</div>
127+
<FieldError>{errorMessage}</FieldError>
128+
</Field>
129+
<Button
130+
variant="outline"
131+
size="sm"
132+
onClick={handleResetBaseUrl}
133+
disabled={
134+
connectionMutation.isPending ||
135+
normalizeBaseUrl(baseUrl) === getDefaultBaseUrl()
136+
}
137+
className="w-full"
138+
>
139+
<RotateCcw className="size-3.5" />
140+
Reset Base URL
141+
</Button>
142+
</div>
143+
</CollapsibleSection>
144+
);
145+
};

0 commit comments

Comments
 (0)