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
2 changes: 1 addition & 1 deletion frontend/src/app/routes/profile/datasets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const UserProfileDatasetsPage = () => (
<ProfileSectionHeader
title={USER_PROFILE_PAGE_CONTENT.datasets.sectionTitle}
/>
<div className="w-full flex flex-col justify-center items-center min-h-[360px] py-12">
<div className="flex w-full flex-1 flex-col items-center justify-center py-12">
<PageEmptyState
heading="Coming soon"
subHeading="Creating and managing your own datasets is coming soon."
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/routes/profile/models.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const UserModelsPage = () => (
<ProfileSectionHeader
title={USER_PROFILE_PAGE_CONTENT.models.sectionTitle}
/>
<div className="w-full flex flex-col justify-center items-center min-h-[360px] py-12">
<div className="flex w-full flex-1 flex-col items-center justify-center py-12">
<PageEmptyState
heading="Coming soon"
subHeading="Creating and managing your own models is coming soon."
Expand Down
42 changes: 37 additions & 5 deletions frontend/src/components/layouts/navbar/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { DashboardLink } from "./dashboard-link";
import { NavLogo } from "@/components/layouts";
import { APPLICATION_ROUTES, SHARED_CONTENT } from "@/constants";
import { useAuth } from "@/app/providers/auth-provider";
import { useLocation, useNavigate } from "react-router-dom";
import { useLocation, useMatch, useNavigate } from "react-router-dom";
import { useState } from "react";
// import { UserNotifications } from "@/features/user-profile/components/notifications/user-notifications";
import { AUTH_PROVIDER, FRONTEND_URL } from "@/config";
Expand All @@ -23,6 +23,8 @@ import {
import { BackButton } from "@/components/ui/button";
import { useNavbarState } from "@/components/layouts/navbar/hooks/use-navbar-state";
import { CloudSaveStatus } from "@/components/layouts/navbar/cloud-save-status";
import { useGetSinglePrediction } from "@/features/user-profile/hooks/use-predictions";
import type { TOfflinePrediction } from "@/types";

const IS_HANKO_AUTH = AUTH_PROVIDER === "hanko";

Expand All @@ -41,6 +43,15 @@ export const NavBar = () => {
showDashboardLink,
isMapRequestResult,
} = useNavbarState();
const resultMatch = useMatch(APPLICATION_ROUTES.MAP_REQUEST_RESULT);
const initialPrediction = (
location.state as { prediction?: TOfflinePrediction } | null
)?.prediction;
const { data: prediction } = useGetSinglePrediction(
resultMatch?.params.id,
isMapRequestResult ? initialPrediction : undefined,
);
const resultTitle = prediction?.description || "Map Request Result";

const isTryFairPage = location.pathname.includes(APPLICATION_ROUTES.TRY_FAIR);
const isProfilePage = location.pathname.includes(
Expand Down Expand Up @@ -100,19 +111,40 @@ export const NavBar = () => {
<nav
className={`${styles.nav} app-padding z-20 py-1 border-b border-gray-border`}
>
<div className="flex-1 flex gap-4 items-center justify-start">
{showBackButton ? <BackButton /> : <NavLogo />}
<div className="min-w-0 flex-1 flex gap-4 items-center justify-start">
{showBackButton ? (
<BackButton showText={false} className="shrink-0" />
) : (
<NavLogo />
)}

{isMapRequestResult && (
<h1
className="truncate text-base font-medium text-ink capitalize"
title={resultTitle}
>
{resultTitle}
</h1>
)}

<CloudSaveStatus />
</div>

<div className="flex-1 hidden sm:flex items-center justify-center">
<div
className={
isMapRequestResult
? "hidden"
: "flex-1 hidden sm:flex items-center justify-center"
}
>
{!isTryFairPage && !isProfilePage && !isMapRequestResult && (
<NavBarLinks className={styles.webNavLinks} />
)}
</div>

<div className="hidden flex-1 items-center justify-end gap-x-6 sm:flex">
<div
className={`hidden ${isMapRequestResult ? "shrink-0" : "flex-1"} items-center justify-end gap-x-6 sm:flex`}
>
{showMappingMode && <MappingMode />}
<DesktopAuthSection
isAuthenticated={isAuthenticated}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/layouts/root-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const RootLayout = () => {

<div
// Disable global padding on landing page.
className={`flex-1 mx-auto w-full ${pathname === APPLICATION_ROUTES.HOMEPAGE ? "" : "app-padding"} ${pathname.includes(MODELS_ROUTES.CREATE_MODEL_BASE) || modelId ? "bg-frosted-blue" : ""}`}
className={`flex-1 mx-auto w-full ${pathname.includes(APPLICATION_ROUTES.PROFILE_BASE) ? "flex flex-col" : ""} ${pathname === APPLICATION_ROUTES.HOMEPAGE ? "" : "app-padding"} ${pathname.includes(MODELS_ROUTES.CREATE_MODEL_BASE) || modelId ? "bg-frosted-blue" : ""}`}
>
<Outlet />
</div>
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/components/layouts/user-profile-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ export const UserProfileLayout = () => {

if (mappingMode === "advanced") {
return (
<main className="my-6 min-w-0">
<div className="flex min-w-0 flex-col items-start gap-4 lg:flex-row lg:gap-6">
<div className="w-full min-w-0 lg:w-60 lg:shrink-0 xl:w-64">
<main className="my-6 flex min-w-0 flex-1 flex-col">
<div className="flex min-w-0 flex-1 flex-col items-stretch gap-4 lg:flex-row lg:gap-6">
<div className="w-full min-w-0 lg:sticky lg:top-6 lg:max-h-[calc(100dvh-3rem)] lg:w-60 lg:shrink-0 lg:self-start lg:overflow-y-auto xl:w-60">
<UserSidebar />
</div>
<div className="w-full min-w-0 flex-1">
<div className="flex w-full min-w-0 flex-1 flex-col">
<Outlet />
</div>
</div>
Expand All @@ -21,7 +21,7 @@ export const UserProfileLayout = () => {
}

return (
<main className="min-w-0 mt-6 mb-10">
<main className="mt-6 mb-10 flex min-w-0 flex-1 flex-col">
<Outlet />
</main>
);
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/components/ui/button/back-button.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { ArrowBackIcon } from "@/components/ui/icons";
import { useHistory } from "@/hooks/use-history";

const BackButton = ({ className }: { className?: string }) => {
const BackButton = ({
className,
showText,
}: {
className?: string;
showText?: boolean;
}) => {
const { goBack } = useHistory();
return (
<button
Expand All @@ -10,7 +16,7 @@ const BackButton = ({ className }: { className?: string }) => {
title="Go back"
>
<ArrowBackIcon className="icon md:icon-lg" />
<span className={`text-dark text-body-3`}>Back</span>
{showText && <span className={`text-dark text-body-3`}>Back</span>}
</button>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { InferenceParam } from "@/features/try-fair/api/stac";
import { CloseIcon, InfoIcon, RefreshIcon } from "@/components/ui/icons";
import { RedoIcon, UndoIcon } from "@/components/ui/icons/undo-icon";
// import { RedoIcon, UndoIcon } from "@/components/ui/icons/undo-icon";
import { ToolTip } from "@/components/ui/tooltip";

type AdvancedSettingsPanelProps = {
Expand Down Expand Up @@ -185,7 +185,7 @@ export const AdvancedSettingsPanel = ({
</div>

<div className="-mx-4 flex items-center justify-between border-t border-gray-border px-4 pt-3">
<div className="flex items-center gap-2">
{/* <div className="flex items-center gap-2">
<button
type="button"
disabled
Expand All @@ -200,7 +200,7 @@ export const AdvancedSettingsPanel = ({
>
<UndoIcon />
</button>
</div>
</div> */}
<button
type="button"
onClick={onReset}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { act, cleanup, render, screen } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Map as MapLibreMap, MapMouseEvent } from "maplibre-gl";
import { TryFairPredictionsLayer } from "@/features/try-fair/components/map/try-fair-prediction-results";
import { TryFairMapOutputType } from "@/enums";

const params = vi.hoisted(() => ({
mappingMode: "advanced",
isAuthenticated: true,
}));
vi.mock("@/app/providers/auth-provider", () => ({ useAuth: () => params }));
vi.mock("@/features/try-fair/hooks/use-try-fair-params", () => ({
useTryFairParams: () => params,
}));

beforeEach(() => {
params.mappingMode = "advanced";
params.isAuthenticated = true;
});
afterEach(cleanup);

function setup(outputType: TryFairMapOutputType, score: number | null = 0.87) {
const handlers = new Map<string, (event: MapMouseEvent) => void>();
const canvas = { style: { cursor: "" } };
const map = {
getStyle: () => null,
getCanvas: () => canvas,
queryRenderedFeatures: () => [{ properties: { score, count: 12 } }],
on: (
event: string,
layer: string,
handler: (event: MapMouseEvent) => void,
) => {
handlers.set(`${event}:${layer}`, handler);
},
off: (event: string, layer: string) => {
handlers.delete(`${event}:${layer}`);
},
};
const props = {
map: map as unknown as MapLibreMap,
predictions: null,
predictionBBox: null,
outputType,
};
const view = render(<TryFairPredictionsLayer {...props} />);
return {
...view,
signOut: () => {
params.isAuthenticated = false;
view.rerender(<TryFairPredictionsLayer {...props} />);
},
changeMode: (mode: string) => {
params.mappingMode = mode;
view.rerender(<TryFairPredictionsLayer {...props} />);
},
hover: () =>
act(() => {
for (const [key, handler] of handlers) {
if (key.startsWith("mousemove:"))
handler({ point: { x: 10, y: 20 } } as MapMouseEvent);
}
}),
leave: () =>
act(() => {
for (const [key, handler] of handlers) {
if (key.startsWith("mouseleave:")) handler({} as MapMouseEvent);
}
}),
};
}

describe("prediction confidence tooltip", () => {
it.each([TryFairMapOutputType.POLYGON, TryFairMapOutputType.POINTS])(
"shows confidence in Advanced mode for %s and clears it on mouse leave",
(outputType) => {
const view = setup(outputType);
view.hover();
expect(screen.getByText("Confidence")).toBeInTheDocument();
expect(screen.getByText("87.0%")).toBeInTheDocument();
view.leave();
expect(screen.queryByText("Confidence")).not.toBeInTheDocument();
},
);
it("updates visibility when switching between Basic and Advanced", () => {
params.mappingMode = "basic";
const view = setup(TryFairMapOutputType.POLYGON);
view.hover();
expect(screen.queryByText("Confidence")).not.toBeInTheDocument();
view.changeMode("advanced");
view.hover();
expect(screen.getByText("Confidence")).toBeInTheDocument();
view.changeMode("basic");
expect(screen.queryByText("Confidence")).not.toBeInTheDocument();
});
it("hides confidence for unauthenticated users even in Advanced mode", () => {
params.isAuthenticated = false;
setup(TryFairMapOutputType.POLYGON).hover();
expect(screen.queryByText("Confidence")).not.toBeInTheDocument();
});

it("clears the confidence tooltip when the user signs out", () => {
const view = setup(TryFairMapOutputType.POINTS);
view.hover();
expect(screen.getByText("Confidence")).toBeInTheDocument();
view.signOut();
expect(screen.queryByText("Confidence")).not.toBeInTheDocument();
view.hover();
expect(screen.queryByText("Confidence")).not.toBeInTheDocument();
});

it("shows zero confidence without inventing a score for missing values", () => {
const view = setup(TryFairMapOutputType.POLYGON, 0);
view.hover();
expect(screen.getByText("0.0%")).toBeInTheDocument();
view.unmount();
setup(TryFairMapOutputType.POLYGON, null).hover();
expect(screen.queryByText("Confidence")).not.toBeInTheDocument();
});
it("keeps cluster counts available in Basic mode", () => {
params.mappingMode = "basic";
setup(TryFairMapOutputType.CLUSTER).hover();
expect(screen.getByText("Features detected")).toBeInTheDocument();
expect(screen.getByText("12")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import {
type PredictionClassStyle,
} from "@/features/try-fair/utils/prediction-classes";

vi.mock("@/features/try-fair/hooks/use-try-fair-params", () => ({
useTryFairParams: () => ({ mappingMode: "basic" }),
}));
vi.mock("@/app/providers/auth-provider", () => ({
useAuth: () => ({ isAuthenticated: true }),
}));

const predictions: GeoJSON.FeatureCollection = {
type: "FeatureCollection",
features: [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useAuth } from "@/app/providers/auth-provider";
import { RefObject, useEffect, useMemo, useState } from "react";
import { Map } from "maplibre-gl";
import useScreenSize from "@/hooks/use-screen-size";
Expand Down Expand Up @@ -62,6 +63,8 @@ export const TryFairDraggableGrid = ({

const { isSmallViewport } = useScreenSize();
const { mappingMode } = useTryFairParams();
const { isAuthenticated } = useAuth();
const showConfidence = isAuthenticated && mappingMode === "advanced";
const [isNoResultsDismissed, setIsNoResultsDismissed] = useState(false);

const { anchor, setAnchor, tileZoom } = useTileGrid({
Expand Down Expand Up @@ -112,6 +115,10 @@ export const TryFairDraggableGrid = ({

const [hoverTooltip, setHoverTooltip] = useState<HoverTooltip>(null);

useEffect(() => {
setHoverTooltip(null);
}, [showConfidence, outputType]);

// Screen projection

const screenGeometry = useGridScreenGeometry({
Expand Down Expand Up @@ -235,7 +242,7 @@ export const TryFairDraggableGrid = ({

const score = feature[0].properties?.score;
// The per-feature confidence tooltip is an advanced-mode only affordance.
if (typeof score !== "number" || mappingMode !== "advanced") {
if (typeof score !== "number" || !showConfidence) {
setHoverTooltip(null);
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useTryFairParams } from "@/features/try-fair/hooks/use-try-fair-params";
import { useAuth } from "@/app/providers/auth-provider";
import { TryFairMapOutputType } from "@/enums/try-fair";
import { BBOX } from "@/types";
import { ExpressionSpecification, Map, MapMouseEvent } from "maplibre-gl";
Expand Down Expand Up @@ -71,6 +73,9 @@ export const TryFairPredictionsLayer = ({
onChoroplethBucketsChange,
classStyle,
}: Props) => {
const { mappingMode } = useTryFairParams();
const { isAuthenticated } = useAuth();
const showConfidence = isAuthenticated && mappingMode === "advanced";
const { choropleth, buckets } = useMemo(() => {
if (
outputType !== TryFairMapOutputType.CLUSTER ||
Expand Down Expand Up @@ -287,7 +292,7 @@ export const TryFairPredictionsLayer = ({
: outputType === TryFairMapOutputType.POLYGON
? FILL_LAYER
: null;
if (!map || !predictionLayer) {
if (!map || !predictionLayer || !showConfidence) {
setTooltip(null);
return;
}
Expand Down Expand Up @@ -326,7 +331,7 @@ export const TryFairPredictionsLayer = ({
map.getCanvas().style.cursor = "";
setTooltip(null);
};
}, [map, outputType]);
}, [map, outputType, showConfidence]);

if (!tooltip) return null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function PredictionHistoryButton() {
: "flex items-center p-1 text-dark hover:text-primary"
}
>
<HistoryIcon />
<HistoryIcon className="size-6" />
</button>
</ToolTip>
);
Expand Down
Loading
Loading