Skip to content
Open
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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ocotillo-ui",
"version": "1.1.1",
"version": "1.1.2",
"private": true,
"type": "module",
"scripts": {
Expand Down
28 changes: 28 additions & 0 deletions public/content/analytics-disclosure.md
Original file line number Diff line number Diff line change
@@ -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 geologic 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.
41 changes: 20 additions & 21 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
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 { 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 { settings } from '@/settings'
import { AppProviders } from '@/AppProviders'

const App: React.FC = () => (
<BrowserRouter basename={settings.urlprefix}>
<AppProviders>
<Routes>
<Route
path="/analytics-disclosure"
element={
<Authenticated
key="analytics-disclosure"
fallback={<ContentPage src="/content/analytics-disclosure.md" />}
>
<AppShell />
</Authenticated>
}
>
<Route
index
element={<ContentPage src="/content/analytics-disclosure.md" />}
/>
</Route>
<Route
element={
<Authenticated key={'auth-pages'} fallback={<Outlet />}>
Expand All @@ -24,23 +39,7 @@ const App: React.FC = () => (
}
>
<Route path="/callback" element={<Callback />} />
<Route
path="/login"
element={
<AuthPage
title={<ThemedTitleV2 collapsed={false} />}
hideForm={true}
type="login"
registerLink={false}
providers={[
{
name: 'authentik',
label: 'Sign in with Authentik',
},
]}
/>
}
/>
<Route path="/login" element={<Login />} />
</Route>
<Route
element={
Expand Down
1 change: 1 addition & 0 deletions src/components/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
55 changes: 46 additions & 9 deletions src/components/Auth.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
import { useEffect, useRef, useState } from 'react'
import {
clearPkceFallbacks,
consumePkceFallbackByState,
tokenStore,
transientStore,
} from '@/providers/authentik-provider'
import { useNavigate } from 'react-router'
import { useQueryClient } from '@tanstack/react-query'
import {
Alert,
Box,
Expand All @@ -15,17 +6,63 @@ import {
CardContent,
CircularProgress,
Container,
Link,
Stack,
Typography,
} from '@mui/material'
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 { AUTHENTIK_URL, CLIENT_ID, REDIRECT_URI } from '@/config'
import {
clearPkceFallbacks,
consumePkceFallbackByState,
tokenStore,
transientStore,
} from '@/providers/authentik-provider'

type TokenResponse = {
access_token: string
id_token?: string
refresh_token?: string
}

export const Login = () => (
<AuthPage
title={<ThemedTitleV2 collapsed={false} />}
hideForm={true}
type="login"
registerLink={false}
providers={[
{
name: 'authentik',
label: 'Sign in with Authentik',
},
]}
renderContent={(content, title) => (
<>
{title}
{content}
<Box sx={{ mt: 2, px: 1, textAlign: 'center' }}>
<Typography variant="caption" color="text.secondary">
Ocotillo uses PostHog analytics and records production sessions to
improve reliability and support users.{' '}
<Link
component={RouterLink}
to="/analytics-disclosure"
underline="hover"
>
Learn why.
</Link>
</Typography>
</Box>
</>
)}
/>
)

export const Callback = () => {
const queryClient = useQueryClient()
const navigate = useNavigate()
Expand Down
73 changes: 60 additions & 13 deletions src/components/ListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import {
useGridSelector,
GridColDef,
GridRowParams,
MuiEvent,
} from '@mui/x-data-grid'
import { settings } from '@/settings'
import React, { useMemo, useState } from 'react'
import { useNavigate } from 'react-router'
import {
CanAccess,
useExport,
useGetToPath,
useNavigation,
useResourceParams,
} from '@refinedev/core'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -284,21 +306,46 @@ export const ListPage: React.FC<ListPageProps> = ({
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<React.MouseEvent>) => {
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'
Expand Down
1 change: 1 addition & 0 deletions src/components/layout/sider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ export const ThemedSiderV2: React.FC<RefineThemedLayoutSiderProps> = ({
{[
{ 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 }) => (
<RouterLink key={to} to={to} style={{ textDecoration: 'none' }}>
Expand Down
Loading
Loading