Skip to content
Draft
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ etl.env
cwms-data-api/features.properties
cda-etl/logs
cda-etl/cache
**/.venv
**/.venv
cda-gui/.env.dev-cda-compose.local
4 changes: 4 additions & 0 deletions cda-gui/.env.dev-cda-compose
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
VITE_CDA_API_ROOT=/cwms-data
CDA_DEV_PROXY_ROOT=http://localhost:8081
VITE_AUTH_HOST=http://localhost:8081/auth
VITE_AUTH_REALM=cwms
4 changes: 3 additions & 1 deletion cda-gui/.env.development
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
VITE_CDA_API_ROOT=https://water.dev.cwbi.us/cwms-data
VITE_CDA_API_ROOT=https://water.dev.cwbi.us/cwms-data
VITE_AUTH_HOST=https://identity-test.cwbi.us/auth
VITE_AUTH_REALM=cwbi
4 changes: 3 additions & 1 deletion cda-gui/.env.test
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
VITE_CDA_API_ROOT=https://cwms-data-test.cwbi.us/cwms-data
VITE_CDA_API_ROOT=https://cwms-data-test.cwbi.us/cwms-data
VITE_AUTH_HOST=https://identity-test.cwbi.us/auth
VITE_AUTH_REALM=cwbi
14 changes: 14 additions & 0 deletions cda-gui/src/components/AuthButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useAuth } from "@usace-watermanagement/groundwork-water";
import { LoginButton } from "@usace/groundwork";

export default function AuthButton() {
const auth = useAuth();

return auth.isAuth ? (
<button className="text-white underline" type="button" onClick={auth.logout}>
Log out
</button>
) : (
<LoginButton onClick={auth.login} />
);
}
68 changes: 68 additions & 0 deletions cda-gui/src/components/HelpTip.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useId, useState } from "react";
import PropTypes from "prop-types";
import { FaCircleQuestion, FaXmark } from "react-icons/fa6";

export function HelpTip({ title, children, className = "" }) {
const [open, setOpen] = useState(false);
const titleId = useId();

return (
<div className={`inline-block ${className}`}>
<button
type="button"
aria-expanded={open}
aria-haspopup="dialog"
aria-label={`Help: ${title}`}
title={title}
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full text-blue-700 hover:bg-blue-50 hover:text-blue-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-600"
onClick={() => setOpen(true)}
>
<FaCircleQuestion aria-hidden="true" className="h-4 w-4" />
</button>
{open && (
<div
className="fixed inset-0 z-[100] flex items-center justify-center p-4"
role="presentation"
onKeyDown={(event) => {
if (event.key === "Escape") setOpen(false);
}}
>
<button
type="button"
className="absolute inset-0 cursor-default bg-zinc-950/35"
aria-label="Close help"
onClick={() => setOpen(false)}
/>
<div
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
className="relative z-10 block max-h-[calc(100vh-2rem)] w-full max-w-md overflow-y-auto rounded-lg border border-zinc-200 bg-white p-5 text-left text-sm font-normal leading-5 text-zinc-700 shadow-2xl"
>
<div className="mb-3 flex items-start justify-between gap-4">
<strong id={titleId} className="text-zinc-950">
{title}
</strong>
<button
type="button"
autoFocus
aria-label="Close help"
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-zinc-600 hover:bg-zinc-100 hover:text-zinc-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-600"
onClick={() => setOpen(false)}
>
<FaXmark aria-hidden="true" />
</button>
</div>
{children}
</div>
</div>
)}
</div>
);
}

HelpTip.propTypes = {
title: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
className: PropTypes.string,
};
24 changes: 14 additions & 10 deletions cda-gui/src/components/Layout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import footerLinks from "../links/footer-links";
import externalLinks from "../links/external-links";
import Breadcrumbs from "./Breadcrumbs";
import { FaGithub } from "react-icons/fa";
import AuthButton from "./AuthButton";

export default function Layout() {
return (
Expand All @@ -15,16 +16,19 @@ export default function Layout() {
subtitle="CWMS Restful API for Data Retrieval"
aboutText="Deliver vital engineering solutions, in collaboration with our partners, to secure our Nation, energize our economy, and reduce disaster risk. The official public website of the U.S. Army Corps of Engineers Hydrologic Engineering Center (HEC)."
navRight={
<Button
missiontext="Corps Water Management System API"
style="plain"
color="white"
size="lg"
href="https://github.com/USACE/cwms-data-api"
title="View on GitHub"
>
GitHub <FaGithub />
</Button>
<div className="flex items-center gap-2">
<AuthButton />
<Button
missiontext="Corps Water Management System API"
style="plain"
color="white"
size="lg"
href="https://github.com/USACE/cwms-data-api"
title="View on GitHub"
>
GitHub <FaGithub />
</Button>
</div>
}
usaceLinks={footerLinks}
externalLinks={externalLinks}
Expand Down
5 changes: 5 additions & 0 deletions cda-gui/src/links/header-links.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ export default [
},
],
},
{
id: "user-lists",
text: "User Lists",
href: "/user-lists",
},
{
id: "help",
text: "Help",
Expand Down
60 changes: 57 additions & 3 deletions cda-gui/src/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { Link, createBrowserRouter, RouterProvider } from "react-router-dom";

import { LinkProvider } from "@usace/groundwork";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
AuthProvider,
createKeycloakAuthMethod,
} from "@usace-watermanagement/groundwork-water";

// Pages
import Home from "./pages/Home";
Expand All @@ -22,9 +26,56 @@ import ErrorFallback from "./pages/ErrorFallback";
import FilterExpressions from "./pages/rsql";
import Timestamps from "./pages/timestamps";
import LegacyFormat from "./pages/legacy-format/index.jsx";
import UserLists from "./pages/user-lists/index.jsx";
import { routePaths } from "./route-paths";

const queryClient = new QueryClient();

function createLocalAuthMethod() {
let token;
return {
async login() {
const response = await fetch(
`${import.meta.env.VITE_AUTH_HOST}/realms/${import.meta.env.VITE_AUTH_REALM}/protocol/openid-connect/token`,
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "password",
client_id: "cwms",
username: import.meta.env.VITE_AUTH_USER,
password: import.meta.env.VITE_AUTH_PASSWORD,
}),
},
);
if (!response.ok) {
throw new Error(`Local Keycloak login failed (${response.status})`);
}
token = (await response.json()).access_token;
},
async logout() {
token = undefined;
},
async isAuth() {
return !!token;
},
get token() {
return token;
},
};
}

const authMethod =
import.meta.env.MODE === "dev-cda-compose"
? createLocalAuthMethod()
: createKeycloakAuthMethod({
host: import.meta.env.VITE_AUTH_HOST,
realm: import.meta.env.VITE_AUTH_REALM,
client: "cwms",
flow: "authorization-code-pkce",
redirectUri: window.location.href,
providerHint: "federation-eams",
});
const routeComponents = {
home: Home,
"swagger-ui": SwaggerUI,
Expand All @@ -34,6 +85,7 @@ const routeComponents = {
timestamps: Timestamps,
"legacy-format": LegacyFormat,
"location-search": LocationSearch,
"user-lists": UserLists,
};

const router = createBrowserRouter(
Expand All @@ -59,9 +111,11 @@ const router = createBrowserRouter(
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<LinkProvider component={Link} hrefMap="to">
<RouterProvider router={router} />
</LinkProvider>
<AuthProvider method={authMethod}>
<LinkProvider component={Link} hrefMap="to">
<RouterProvider router={router} />
</LinkProvider>
</AuthProvider>
</QueryClientProvider>
</React.StrictMode>,
);
Loading
Loading