From 9c24c45dec22563f4cfcf2cece8934fd48741987 Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Fri, 14 Aug 2026 10:22:49 -0500 Subject: [PATCH 1/6] task(analytics-disclosure): add disclosure to the sign in to authentik screen --- public/content/analytics-disclosure.md | 28 +++++++++++++ src/App.tsx | 32 +++++---------- src/components/Auth.tsx | 56 +++++++++++++++++++++----- 3 files changed, 86 insertions(+), 30 deletions(-) create mode 100644 public/content/analytics-disclosure.md diff --git a/public/content/analytics-disclosure.md b/public/content/analytics-disclosure.md new file mode 100644 index 00000000..069c8c48 --- /dev/null +++ b/public/content/analytics-disclosure.md @@ -0,0 +1,28 @@ +--- +title: Analytics Disclosure +deck: Why Ocotillo records production sessions and uses site analytics. +date: 2026-08-14 +--- + +## What is recorded? + +Ocotillo uses PostHog to collect site analytics and, on the production site, session recordings for signed-in users. A session recording captures page views, clicks, navigation, and general interface behavior so the development team can understand where the application is confusing, slow, or broken. + +Form inputs are masked by default in session recordings. Ocotillo does not use session recordings to collect passwords or intentionally capture sensitive field values. + +## Why we need it + +Ocotillo supports Bureau staff working with water data across many workflows. Session analytics help the team: + +- Find broken or confusing screens +- Understand which workflows need performance or usability improvements +- Troubleshoot support requests with the page and browser context needed to reproduce problems +- Prioritize fixes based on real usage instead of guesses + +## When it starts + +Session recording starts after sign-in on the production Ocotillo site. Analytics events may also record basic application activity, such as page views and feature usage. + +## Questions + +Contact the Data Services team at [ocotillo-nmbg@nmt.edu](mailto:ocotillo-nmbg@nmt.edu) with questions about Ocotillo analytics or session recording. diff --git a/src/App.tsx b/src/App.tsx index 07496244..a01b8b46 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,21 +1,27 @@ import { Authenticated } from '@refinedev/core' -import { AuthPage, ErrorComponent } from '@refinedev/mui' +import { ErrorComponent } from '@refinedev/mui' import { BrowserRouter, Navigate, Outlet, Route, Routes } from 'react-router' +import { AppProviders } from '@/AppProviders' import { AppShell } from '@/components/AppShell' -import { ThemedTitleV2 } from '@/components/layout/title' -import { Callback } from '@/components/Auth' +import { Callback, Login } from '@/components/Auth' +import { ContentPage } from '@/pages/content' +import { TypographyPage } from '@/pages/example/TypographyPage' import { Home } from '@/pages/home' import { TypographyPage } from '@/pages/example/TypographyPage' import { DataGridPage } from '@/pages/example/DataGridPage' import { ContentPage } from '@/pages/content' import { OcotilloRoutes, ST2Routes } from '@/routes' +import { GeothermalRoutes, OcotilloRoutes, ST2Routes } from '@/routes' import { settings } from '@/settings' -import { AppProviders } from '@/AppProviders' const App: React.FC = () => ( + } + /> }> @@ -24,23 +30,7 @@ const App: React.FC = () => ( } > } /> - } - hideForm={true} - type="login" - registerLink={false} - providers={[ - { - name: 'authentik', - label: 'Sign in with Authentik', - }, - ]} - /> - } - /> + } /> ( + } + hideForm={true} + type="login" + registerLink={false} + providers={[ + { + name: 'authentik', + label: 'Sign in with Authentik', + }, + ]} + renderContent={(content, title) => ( + <> + {title} + {content} + + + Ocotillo uses PostHog analytics and records production sessions to + improve reliability and support users.{' '} + + Learn why. + + + + + )} + /> +) + export const Callback = () => { const queryClient = useQueryClient() const navigate = useNavigate() From d8e399bd46fbf1e4b084d9809a624a380bc15b68 Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Fri, 14 Aug 2026 10:29:25 -0500 Subject: [PATCH 2/6] fix(analytics-disclosure): add disclouse to the authentik sidebar --- src/App.tsx | 16 ++++++++++++++-- src/components/AppShell.tsx | 1 + src/components/layout/sider.tsx | 1 + 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index a01b8b46..09ca9b0f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,8 +20,20 @@ const App: React.FC = () => ( } - /> + element={ + } + > + + + } + > + } + /> + }> diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index 2c84b52b..8101a457 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -159,6 +159,7 @@ function ExpandButton() { const FOOTER_LINKS = [ { label: 'About', href: '/about' }, { label: 'Connect Desktop GIS', href: '/ogcapi' }, + { label: 'Analytics Disclosure', href: '/analytics-disclosure' }, { label: 'Report a Bug', href: '/report-a-bug' }, ] as const diff --git a/src/components/layout/sider.tsx b/src/components/layout/sider.tsx index ebe79b21..ce90bc38 100644 --- a/src/components/layout/sider.tsx +++ b/src/components/layout/sider.tsx @@ -417,6 +417,7 @@ export const ThemedSiderV2: React.FC = ({ {[ { to: '/about', label: 'About' }, { to: '/ogcapi', label: 'Connect Desktop GIS' }, + { to: '/analytics-disclosure', label: 'Analytics Disclosure' }, { to: '/report-a-bug', label: 'Report a Bug' }, ].map(({ to, label }) => ( From 16ba6a51651b06003431b24d134c72b6ae4714fb Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Fri, 14 Aug 2026 11:46:55 -0500 Subject: [PATCH 3/6] fix(analytics-disclosure): update verbage of disclosure statement --- public/content/analytics-disclosure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/content/analytics-disclosure.md b/public/content/analytics-disclosure.md index 069c8c48..396a837a 100644 --- a/public/content/analytics-disclosure.md +++ b/public/content/analytics-disclosure.md @@ -12,7 +12,7 @@ Form inputs are masked by default in session recordings. Ocotillo does not use s ## Why we need it -Ocotillo supports Bureau staff working with water data across many workflows. Session analytics help the team: +Ocotillo supports Bureau staff working with geologic data across many workflows. Session analytics help the team: - Find broken or confusing screens - Understand which workflows need performance or usability improvements From 2779826f6f3da1a05a2c771be4613baf90e1df79 Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Fri, 14 Aug 2026 16:18:41 -0500 Subject: [PATCH 4/6] chore(package): update frontend version number from v1.1.1 to v1.1.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b6cf175f..9787f307 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ocotillo-ui", - "version": "1.0.1", + "version": "1.1.2", "lockfileVersion": 3, "requires": true, "packages": { @@ -31117,4 +31117,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index c992f5b5..ec5dccf5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ocotillo-ui", - "version": "1.1.1", + "version": "1.1.2", "private": true, "type": "module", "scripts": { From 4dbb5abfb534e29a4674b592f4c7a5edcf4e435c Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Tue, 18 Aug 2026 09:13:27 -0500 Subject: [PATCH 5/6] chore(App): rm unused & duplicate imports --- src/App.tsx | 3 --- src/components/Auth.tsx | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 09ca9b0f..47449304 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,14 +4,11 @@ import { BrowserRouter, Navigate, Outlet, Route, Routes } from 'react-router' import { AppProviders } from '@/AppProviders' import { AppShell } from '@/components/AppShell' import { Callback, Login } from '@/components/Auth' -import { ContentPage } from '@/pages/content' -import { TypographyPage } from '@/pages/example/TypographyPage' import { Home } from '@/pages/home' import { TypographyPage } from '@/pages/example/TypographyPage' import { DataGridPage } from '@/pages/example/DataGridPage' import { ContentPage } from '@/pages/content' import { OcotilloRoutes, ST2Routes } from '@/routes' -import { GeothermalRoutes, OcotilloRoutes, ST2Routes } from '@/routes' import { settings } from '@/settings' const App: React.FC = () => ( diff --git a/src/components/Auth.tsx b/src/components/Auth.tsx index 879a8e7d..e234fecc 100644 --- a/src/components/Auth.tsx +++ b/src/components/Auth.tsx @@ -10,13 +10,12 @@ import { Stack, Typography, } from '@mui/material' -import { AUTHENTIK_URL, CLIENT_ID, REDIRECT_URI } from '@/config' import { AuthPage } from '@refinedev/mui' import { useQueryClient } from '@tanstack/react-query' import { useEffect, useRef, useState } from 'react' import { Link as RouterLink, useNavigate } from 'react-router' import { ThemedTitleV2 } from '@/components/layout/title' -import { buildAuthentikUrl, CLIENT_ID, REDIRECT_URI } from '@/config' +import { AUTHENTIK_URL, CLIENT_ID, REDIRECT_URI } from '@/config' import { clearPkceFallbacks, consumePkceFallbackByState, From 61e1175523ace994e87f82bab6c86c20cae34f7b Mon Sep 17 00:00:00 2001 From: jakeross Date: Wed, 5 Aug 2026 11:35:36 -0700 Subject: [PATCH 6/6] BDMS-903: open well show pages in new windows from the well list The well list is the entry point for comparing several wells, but every path to a detail page replaced the list in place, so users lost their scroll position and filters each time they checked one well. - Add a leading column on the well list whose cell is a link to the well show page with target="_blank". The link fills the cell so the whole column is the hit target, and being a real anchor it also supports middle click and the browser "open in new window" context menu. - Support Ctrl/Cmd+click and middle click on any ListPage row to open the record's show page in a new window. Shift is left alone because the DataGrid uses it for row range selection. - Extract buildWellShowPath so the in-app route has one definition. Co-Authored-By: Claude Opus 5 --- src/components/ListPage.tsx | 73 +++++++++++++++---- src/pages/ocotillo/thing/list.tsx | 70 ++++++++++++++---- src/test/components/listPageNewWindow.test.ts | 49 +++++++++++++ src/utils/wellPublicUrls.ts | 7 +- 4 files changed, 172 insertions(+), 27 deletions(-) create mode 100644 src/test/components/listPageNewWindow.test.ts diff --git a/src/components/ListPage.tsx b/src/components/ListPage.tsx index 3e7e3633..ab5e16f9 100644 --- a/src/components/ListPage.tsx +++ b/src/components/ListPage.tsx @@ -15,6 +15,7 @@ import { useGridSelector, GridColDef, GridRowParams, + MuiEvent, } from '@mui/x-data-grid' import { settings } from '@/settings' import React, { useMemo, useState } from 'react' @@ -22,6 +23,7 @@ import { useNavigate } from 'react-router' import { CanAccess, useExport, + useGetToPath, useNavigation, useResourceParams, } from '@refinedev/core' @@ -154,6 +156,26 @@ function ListPageToolbar({ ) } +/** + * DataGrid rows are not anchors, so modifier clicks would otherwise navigate in + * place. Treat the browser conventions for "open elsewhere" as new-window intent. + */ +export function isNewWindowClick(event: { + ctrlKey?: boolean + metaKey?: boolean + button?: number +}): boolean { + // Shift is left alone: the DataGrid uses it for row range selection. + return Boolean(event.ctrlKey || event.metaKey || event.button === 1) +} + +export function openInNewWindow(href: string) { + // Router paths are basename-relative; window.open is not. + const target = href.startsWith('/') ? `${settings.urlprefix}${href}` : href + const opened = window.open(target, '_blank', 'noopener,noreferrer') + if (opened) opened.opener = null +} + type ListPageProps = { title?: string description?: string @@ -284,21 +306,46 @@ export const ListPage: React.FC = ({ hideExport: restDataGridProps.paginationMode === 'server', } - const handleRowClick = getRowHref - ? (params: GridRowParams) => { - onRowClick?.(params) - navigate(getRowHref(params)) - } - : disableRowClick - ? onRowClick - ? (params: GridRowParams) => onRowClick(params) - : undefined - : resource - ? (params: GridRowParams) => { - onRowClick?.(params) + const getToPath = useGetToPath() + + // Href for the row's destination, used for modifier-click new-window opens. + const resolveRowHref = (params: GridRowParams): string | undefined => { + if (getRowHref) return getRowHref(params) + if (disableRowClick || !resource) return undefined + return getToPath({ + resource, + action: 'show', + meta: { id: params.id }, + }) + } + + const rowClickNavigates = Boolean( + getRowHref || (!disableRowClick && resource) + ) + + const handleRowClick = + rowClickNavigates || onRowClick + ? (params: GridRowParams, event: MuiEvent) => { + onRowClick?.(params) + + if (!rowClickNavigates) return + + const href = resolveRowHref(params) + if (href && isNewWindowClick(event)) { + openInNewWindow(href) + return + } + + if (getRowHref) { + navigate(getRowHref(params)) + return + } + + if (resource) { show(resource.name, params.id as string | number) } - : undefined + } + : undefined const rowCursor = getRowHref || (!disableRowClick && resource) ? 'pointer' : 'default' diff --git a/src/pages/ocotillo/thing/list.tsx b/src/pages/ocotillo/thing/list.tsx index 4383847f..da0f4614 100644 --- a/src/pages/ocotillo/thing/list.tsx +++ b/src/pages/ocotillo/thing/list.tsx @@ -1,12 +1,20 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { Link as RouterLink, useNavigate, useSearchParams } from 'react-router' -import { useExport, useGo, useLink, useOne, type CrudFilter } from '@refinedev/core' +import { + useExport, + useGo, + useLink, + useOne, + type CrudFilter, +} from '@refinedev/core' import { useDataGrid } from '@refinedev/mui' +import { GridColDef } from '@mui/x-data-grid' import { - GridColDef, -} from '@mui/x-data-grid' -import { captureEvent, consumeWellsProjectFilterSource, setWellsProjectFilterSource } from '@/analytics/posthog' -import { Download, FileText, Loader2, X } from 'lucide-react' + captureEvent, + consumeWellsProjectFilterSource, + setWellsProjectFilterSource, +} from '@/analytics/posthog' +import { Download, ExternalLink, FileText, Loader2, X } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { ListPage } from '@/components/ListPage' @@ -15,6 +23,7 @@ import { ISpring, IWell } from '@/interfaces/ocotillo' import { IGroup } from '@/interfaces/ocotillo/IGroup' import { displayWellSiteName, formatAppDate, formatAppDateTime } from '@/utils' import { getContactDisplayName } from '@/utils/contactDisplayName' +import { buildWellShowPath } from '@/utils/wellPublicUrls' import { WellListColumnLabels } from '@/well-list/wellListColumnLabels' export const SpringList: React.FC = () => { @@ -84,9 +93,7 @@ export const WellList: React.FC = () => { const projectFilters = useMemo( () => - projectId - ? [{ field: 'groups', operator: 'eq', value: projectId }] - : [], + projectId ? [{ field: 'groups', operator: 'eq', value: projectId }] : [], [projectId] ) @@ -179,6 +186,42 @@ export const WellList: React.FC = () => { const columns = useMemo[]>( () => [ + { + field: 'open_in_new_window', + headerName: '', + description: + 'Open this well detail page in a new browser window so several wells can stay open at once.', + width: 52, + sortable: false, + filterable: false, + hideable: false, + disableColumnMenu: true, + disableExport: true, + align: 'center', + headerAlign: 'center', + // Drop the default cell padding so the link can cover the full cell. + cellClassName: 'relative !p-0', + // The link fills the whole cell so the entire column is the hit target, + // not just the icon glyph. + renderCell: (params) => ( + ) => { + e.stopPropagation() + captureEvent('wells_opened_new_window', { + well_id: params.row.id, + }) + }} + className="absolute inset-0 flex items-center justify-center text-muted-foreground hover:bg-primary/5 hover:text-primary" + > + + + ), + }, { field: 'name', headerName: WellListColumnLabels.name, @@ -480,11 +523,12 @@ export const WellList: React.FC = () => { > {projectId ? (
- -
+ +
Project: {projectName ?? projectId}