M2: frontend walkthrough fixes + explicit dev compose stack
Post-M2 self-walkthrough polish, batched into one commit. Map / heat: - fix heat-layer white-screen crash after login (add layer to map before setLatLngs; an off-map leaflet.heat layer has a null _map and throws) - normalize each heat layer to the densest pixel cell visible in the CURRENT viewport (maxZoom:0 so intensity factor f=1) and recompute on moveend/zoomend, so sparse poo data reaches red and stays normalized at any zoom level - dark CARTO basemap tiles when the color scheme is dark UI: - dark-mode toggle in the top-right, beside the settings gear - switch top-right nav (records / theme / settings / logout) to Feather icons with hover tooltips - home: Grafana-style quick time-range presets + back/forward shift buttons, placed between the From/To pickers and Apply; fix Select/tooltip z-index (Leaflet stacking) and the shift-button height alignment API client: - stop flooding GET /api/session with 401s: the session probe and the login endpoint own their 401s (no global redirect), which fixes the logout hang and the spinning login page Compose: - rename docker-compose.override.yml -> docker-compose.dev.yml as an explicit, non-auto-layered dev stack (8001, -dev container names, prod-copy ./data DB); update tests/test_deployment.py (read dev.yml, tolerate the !override tag) and the README "Docker Compose" section Tests: - pixel-grid peak counter, time-range presets, heat-layer ordering regression, and 401-redirect regression
This commit is contained in:
@@ -17,13 +17,18 @@ import {
|
||||
Switch,
|
||||
TextInput,
|
||||
Button,
|
||||
Select,
|
||||
ActionIcon,
|
||||
Tooltip,
|
||||
Paper,
|
||||
Text,
|
||||
Box,
|
||||
Loader,
|
||||
Alert,
|
||||
Badge,
|
||||
useComputedColorScheme,
|
||||
} from '@mantine/core'
|
||||
import { ChevronLeft, ChevronRight } from 'react-feather'
|
||||
|
||||
import apiClient from '../api/client'
|
||||
import {
|
||||
@@ -34,6 +39,9 @@ import {
|
||||
filterPooByTimeWindow,
|
||||
daysAgoISO,
|
||||
nowISO,
|
||||
TIME_PRESETS,
|
||||
presetRange,
|
||||
shiftRange,
|
||||
} from '../map'
|
||||
import { RecordsMap } from '../map'
|
||||
import {
|
||||
@@ -108,9 +116,37 @@ export function HomePage() {
|
||||
return d.toISOString().slice(0, 16) // "YYYY-MM-DDTHH:MM"
|
||||
})
|
||||
const [endInput, setEndInput] = useState(() => nowISO().slice(0, 16))
|
||||
// Applied (committed) window — updated on button click
|
||||
// Applied (committed) window — updated on Apply / preset / shift
|
||||
const [appliedStart, setAppliedStart] = useState<string | null>(() => daysAgoISO(30))
|
||||
const [appliedEnd, setAppliedEnd] = useState<string | null>(() => nowISO())
|
||||
// Which quick-range preset is currently active (null = custom / shifted range)
|
||||
const [activePreset, setActivePreset] = useState<string | null>(null)
|
||||
|
||||
// Set both the committed window and the editable inputs from an ISO [start, end].
|
||||
function setWindow(startISO: string, endISO: string) {
|
||||
setAppliedStart(startISO)
|
||||
setAppliedEnd(endISO)
|
||||
setStartInput(startISO.slice(0, 16))
|
||||
setEndInput(endISO.slice(0, 16))
|
||||
}
|
||||
|
||||
// Pick a quick range: fill from-to ending at now, apply immediately (Grafana-style).
|
||||
function applyPreset(value: string | null) {
|
||||
const preset = TIME_PRESETS.find((p) => p.value === value)
|
||||
if (!preset) return
|
||||
const { start, end } = presetRange(preset.spanMs)
|
||||
setWindow(start, end)
|
||||
setActivePreset(value)
|
||||
}
|
||||
|
||||
// Shift the committed window by its own span. -1 = earlier, +1 = later.
|
||||
function shiftWindow(direction: -1 | 1) {
|
||||
if (!appliedStart || !appliedEnd) return
|
||||
const { start, end } = shiftRange(appliedStart, appliedEnd, direction)
|
||||
setWindow(start, end)
|
||||
// A shifted window is an absolute range, no longer "now - X".
|
||||
setActivePreset(null)
|
||||
}
|
||||
|
||||
// ------ Layer toggle state -----------------------------------------------
|
||||
const [showLocationHeat, setShowLocationHeat] = useState(true)
|
||||
@@ -165,11 +201,14 @@ export function HomePage() {
|
||||
const toISO = (s: string) => (s ? s + ':00Z' : null)
|
||||
setAppliedStart(toISO(startInput))
|
||||
setAppliedEnd(toISO(endInput))
|
||||
// Manually-applied range is custom, not a preset.
|
||||
setActivePreset(null)
|
||||
}
|
||||
|
||||
// ------ Render -----------------------------------------------------------
|
||||
const isLoading = locationsQuery.isLoading || pooQuery.isLoading
|
||||
const isError = locationsQuery.isError || pooQuery.isError
|
||||
const colorScheme = useComputedColorScheme('light', { getInitialValueInEffect: true })
|
||||
|
||||
return (
|
||||
<Box style={{ height: 'calc(100vh - 52px)', display: 'flex', flexDirection: 'column' }}>
|
||||
@@ -201,6 +240,46 @@ export function HomePage() {
|
||||
style={{ minWidth: 180 }}
|
||||
data-testid="time-end-input"
|
||||
/>
|
||||
{/* Quick range + shift buttons (Grafana-style) — between To and Apply.
|
||||
zIndex raised above Leaflet (~1000) so the dropdown/tooltips are
|
||||
not painted over by the map below. */}
|
||||
<Group gap={4} align="flex-end">
|
||||
<Select
|
||||
label="Quick range"
|
||||
placeholder="Pick a range"
|
||||
data={TIME_PRESETS.map((p) => ({ value: p.value, label: p.label }))}
|
||||
value={activePreset}
|
||||
onChange={applyPreset}
|
||||
size="xs"
|
||||
allowDeselect={false}
|
||||
style={{ width: 150 }}
|
||||
comboboxProps={{ zIndex: 3000 }}
|
||||
data-testid="quick-range-select"
|
||||
/>
|
||||
<Tooltip label="Shift earlier (one window back)" zIndex={3000}>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="input-xs"
|
||||
aria-label="Shift earlier"
|
||||
onClick={() => shiftWindow(-1)}
|
||||
data-testid="shift-earlier"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Shift later (one window forward)" zIndex={3000}>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="input-xs"
|
||||
aria-label="Shift later"
|
||||
onClick={() => shiftWindow(1)}
|
||||
data-testid="shift-later"
|
||||
>
|
||||
<ChevronRight size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
<Button size="xs" onClick={applyWindow} data-testid="apply-window-button">
|
||||
Apply
|
||||
</Button>
|
||||
@@ -255,8 +334,11 @@ export function HomePage() {
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Map fills remaining height */}
|
||||
<Box style={{ flex: 1, minHeight: 0 }}>
|
||||
{/* Map fills remaining height. `isolation: isolate` traps Leaflet's internal
|
||||
z-indexes (panes/controls up to ~1000) in their own stacking context so
|
||||
they can't paint over portaled popups (Quick-range dropdown, tooltips,
|
||||
and the point-select edit/delete modals). */}
|
||||
<Box style={{ flex: 1, minHeight: 0, isolation: 'isolate' }}>
|
||||
<RecordsMap
|
||||
locationHeatPoints={locationHeatPoints}
|
||||
pooHeatPoints={pooHeatPoints}
|
||||
@@ -268,6 +350,7 @@ export function HomePage() {
|
||||
onSelectLocation={handleSelectLocation}
|
||||
onSelectPoo={handleSelectPoo}
|
||||
height="100%"
|
||||
dark={colorScheme === 'dark'}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user