From 5b4b78afd5b6334f1169db30ea667837cc0ad1c3 Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Tue, 28 Apr 2026 14:57:01 -0500 Subject: [PATCH 1/6] feat(pages/content): Update content manager to accept ENVs & code element --- src/App.tsx | 4 ++ src/components/layout/sider.tsx | 1 + src/pages/content/index.tsx | 116 ++++++++++++++++++++++++++++++-- 3 files changed, 114 insertions(+), 7 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 6673c1f7..d2d4a7c3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -67,6 +67,10 @@ const App: React.FC = () => ( path="/report-a-bug" element={} /> + } + /> } /> } /> } /> diff --git a/src/components/layout/sider.tsx b/src/components/layout/sider.tsx index 039669d2..85160060 100644 --- a/src/components/layout/sider.tsx +++ b/src/components/layout/sider.tsx @@ -418,6 +418,7 @@ export const ThemedSiderV2: React.FC = ({ { to: '/about', label: 'About' }, { to: '/ocotillo/help', label: 'Connect Desktop GIS' }, { to: '/report-a-bug', label: 'Report a Bug' }, + { to: '/ogcapi', label: 'Connect Desktop GIS (md)' }, ].map(({ to, label }) => ( ), a: ({ href, children }) => ( - + {children} - + ), + blockquote: ({ children }) => ( + + {children} + + ), + code: ({ children, className }) => { + const value = String(children).replace(/\n$/, '') + + if (className) { + return + } + + return ( + + {children} + + ) + }, ul: ({ children }) => ( {children} @@ -69,7 +101,11 @@ export const markdownComponents: Components = { ), li: ({ children }) => ( - + {children} ), @@ -128,7 +164,10 @@ export const MarkdownPage: React.FC = ({ )} {frontmatter.date && ( - + {new Date(frontmatter.date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', @@ -159,7 +198,28 @@ export const ContentPage: React.FC = ({ src }) => { return res.text() }) .then((text) => { - const parsed = parseFrontmatter(text) + // Replace template placeholders like {{ key }} in the markdown text + // with corresponding values from the `settings` object. + // + // Example: + // "https://{{ ocotillo_api_url }}/ogcapi" + // → "https://actual-value/ogcapi" + // + const hydratedText = text.replace( + /{{\s*([\w]+)\s*}}/g, + (_, key: string) => { + const value = (settings as Record)[key] + + if (typeof value === 'string') { + return value.replace(/\/+$/, '') + } + + // if key not found or not string → reinsert key name + return `{{ ${key} }}` + } + ) + + const parsed = parseFrontmatter(hydratedText) setFrontmatter(parsed.data) setBody(parsed.content) }) @@ -194,3 +254,45 @@ export const ContentPage: React.FC = ({ src }) => { return } + +const CopyCodeBlock = ({ value }: { value: string }) => { + const handleCopy = async () => { + await navigator.clipboard.writeText(value) + } + + return ( + + + {value} + + + + + + + + + ) +} From cfe6b7ac44b0e6952dbd4f1b626f685ea116c5bc Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Tue, 28 Apr 2026 15:50:02 -0500 Subject: [PATCH 2/6] feat(public/content/ogcapi): Recreate the ocotillo/help pg in markdown --- public/content/ogcapi.md | 68 ++++++++++++++++++++++ src/pages/content/index.tsx | 109 ++++++++++++++++++++++++++++++++---- src/routes/ocotillo.tsx | 2 - 3 files changed, 167 insertions(+), 12 deletions(-) create mode 100644 public/content/ogcapi.md diff --git a/public/content/ogcapi.md b/public/content/ogcapi.md new file mode 100644 index 00000000..8ecc64aa --- /dev/null +++ b/public/content/ogcapi.md @@ -0,0 +1,68 @@ +--- +title: Connect the Ocotillo OGC API to desktop GIS +deck: Use the Ocotillo OGC API Features endpoint to browse collections in ArcGIS Desktop and in QGIS. +--- + +> [!WARNING] +> OGC API layers are read-only in desktop GIS. Use them for discovery, map display, querying, and export. + +##### Ocotillo OGC landing page URL + +```text +{{ ocotillo_api_url }}/ogcapi +``` + +## ArcGIS Pro / Desktop + +1. Open the **Catalog pane** and create a new OGC API Server connection. +2. Paste the Ocotillo landing page URL. +3. Expand the server connection, choose the collection you want, and add it to the current map. +4. If ArcGIS prompts for layer options, use extent or maximum-feature limits for large collections. + +**Official documentation:** +[https://pro.arcgis.com/en/pro-app/latest/help/data/services/use-ogc-api-services.htm](https://pro.arcgis.com/en/pro-app/latest/help/data/services/use-ogc-api-services.htm) + +--- + +## QGIS + +1. Open **Data Source Manager**. +2. Choose the WFS / OGC API - Features connection tab. +3. Create a new connection using the Ocotillo landing page URL. +4. Connect to the server, select one or more collections, and add them to the map. +5. For large layers, set paging or feature limits in the connection and layer options. + +> [!INFO] +> QGIS expects the OGC API landing page, not a single collection items URL, when you create the server connection. + +**Official documentation:** +[https://docs.qgis.org/latest/en/docs/user_manual/working_with_ogc/ogc_client_support.html](https://docs.qgis.org/latest/en/docs/user_manual/working_with_ogc/ogc_client_support.html) + +--- + +## Useful Ocotillo endpoints + +### Landing page + +[{{ ocotillo_api_url }}/ogcapi]({{ ocotillo_api_url }}/ogcapi) + +Use this as the server URL when creating the connection. + +### Collections + +[{{ ocotillo_api_url }}/ogcapi/collections]({{ ocotillo_api_url }}/ogcapi/collections) + +Review available collections before connecting from desktop GIS. + +--- + +## Common collections to look for + +- [!CHIPS] +- Water Wells +- Springs +- Latest Depth to Water +- Average TDS +- Latest TDS + +Collection names can change by deployment. If you do not see one of these, open the [collections endpoint]({{ ocotillo_api_url }}/ogcapi/collections) and use the names published there. diff --git a/src/pages/content/index.tsx b/src/pages/content/index.tsx index 7ef8e30c..91c99300 100644 --- a/src/pages/content/index.tsx +++ b/src/pages/content/index.tsx @@ -9,6 +9,8 @@ import { Typography, IconButton, Tooltip, + Stack, + Chip, } from '@mui/material' import { ContentCopy } from '@mui/icons-material' import { Components } from 'react-markdown' @@ -72,11 +74,56 @@ export const markdownComponents: Components = { {children} ), - blockquote: ({ children }) => ( - - {children} - - ), + blockquote: ({ children }) => { + const text = React.Children.toArray(children) + .map((child) => { + if (React.isValidElement(child)) { + return React.Children.toArray(child.props.children).join('') + } + + return String(child) + }) + .join('') + .trim() + + const alertMatch = text.match( + /^\[!(WARNING|INFO|ERROR|SUCCESS)\]\s*([\s\S]*)$/i + ) + + if (alertMatch) { + const severityMap = { + WARNING: 'warning', + INFO: 'info', + ERROR: 'error', + SUCCESS: 'success', + } as const + + const alertType = alertMatch[1].toUpperCase() as keyof typeof severityMap + const alertBody = alertMatch[2].trim() + + return ( + + {alertBody} + + ) + } + + return ( + + {children} + + ) + }, code: ({ children, className }) => { const value = String(children).replace(/\n$/, '') @@ -90,11 +137,53 @@ export const markdownComponents: Components = { ) }, - ul: ({ children }) => ( - - {children} - - ), + ul: ({ children, node }) => { + const getListItemText = (listItem: any): string => { + return ( + listItem?.children + ?.map((child: any) => child.value ?? '') + ?.join('') + ?.trim() ?? '' + ) + } + + const listItems = + node?.children?.filter( + (child: any) => child.type === 'element' && child.tagName === 'li' + ) ?? [] + const firstItemText = getListItemText(listItems[0]) + + if (firstItemText === '[!CHIPS]') { + return ( + + {listItems.slice(1).map((item: any, index: number) => { + const label = getListItemText(item) + + return ( + + ) + })} + + ) + } + + return ( + + {children} + + ) + }, ol: ({ children }) => ( {children} diff --git a/src/routes/ocotillo.tsx b/src/routes/ocotillo.tsx index f21cd232..378309df 100644 --- a/src/routes/ocotillo.tsx +++ b/src/routes/ocotillo.tsx @@ -18,7 +18,6 @@ import { SpringShow, } from '@/pages/ocotillo/thing' import { MapView } from '@/pages/ocotillo/map' -import { HelpPage } from '@/pages/ocotillo/help' import { CollectionsPage } from '@/pages/ocotillo/collections' import { LocationList, @@ -132,7 +131,6 @@ export const OcotilloRoutes = () => { } /> } /> - } /> } /> } /> From 87c2cd46bac2f5ec0e7b9d79dcb39c1eecef95ee Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Tue, 28 Apr 2026 15:51:51 -0500 Subject: [PATCH 3/6] chore(ocotillo/help): Rm old help page --- src/pages/ocotillo/help/index.tsx | 1 - src/pages/ocotillo/help/list.tsx | 253 ------------------------------ 2 files changed, 254 deletions(-) delete mode 100644 src/pages/ocotillo/help/index.tsx delete mode 100644 src/pages/ocotillo/help/list.tsx diff --git a/src/pages/ocotillo/help/index.tsx b/src/pages/ocotillo/help/index.tsx deleted file mode 100644 index c1edf1e2..00000000 --- a/src/pages/ocotillo/help/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './list' diff --git a/src/pages/ocotillo/help/list.tsx b/src/pages/ocotillo/help/list.tsx deleted file mode 100644 index 32b62adb..00000000 --- a/src/pages/ocotillo/help/list.tsx +++ /dev/null @@ -1,253 +0,0 @@ -import { - Alert, - Box, - Card, - CardContent, - Chip, - Container, - Divider, - IconButton, - Link, - Stack, - Tooltip, - Typography, -} from '@mui/material' -import { ContentCopy } from '@mui/icons-material' -import Grid from '@mui/material/Grid2' -import { settings } from '@/settings' - -const trimTrailingSlash = (value: string) => value.replace(/\/+$/, '') - -const baseApiUrl = trimTrailingSlash(settings.ocotillo_api_url) -const ogcLandingPageUrl = `${baseApiUrl}/ogcapi` -const ogcCollectionsUrl = `${ogcLandingPageUrl}/collections` - -const commonCollections = [ - 'Water Wells', - 'Springs', - 'Latest Depth to Water', - 'Average TDS', - 'Latest TDS', -] - -const docs = { - arcgis: - 'https://pro.arcgis.com/en/pro-app/latest/help/data/services/use-ogc-api-services.htm', - qgis: 'https://docs.qgis.org/latest/en/docs/user_manual/working_with_ogc/ogc_client_support.html', -} - -export const HelpPage = () => { - const handleCopy = async (value: string) => { - try { - await navigator.clipboard.writeText(value) - } catch (error) { - console.error('Failed to copy OGC URL', error) - } - } - - return ( - - - - - Connect the Ocotillo OGC API to desktop GIS - - - Use the Ocotillo OGC API Features endpoint to browse collections in - ArcGIS Desktop and in QGIS. - - - - - OGC API layers are read-only in desktop GIS. Use them for discovery, - map display, querying, and export. - - - - - Ocotillo OGC landing page URL - - - - - - - - - - - - - - - - - Useful Ocotillo endpoints - - - - - - - - - - - Common collections to look for - - - {commonCollections.map((collection) => ( - - ))} - - - Collection names can change by deployment. If you do not see one - of these, open the{' '} - - collections endpoint - {' '} - and use the names published there. - - - - - - - ) -} - -const InstructionCard = ({ - title, - steps, - note, - href, -}: { - title: string - steps: string[] - note?: string - href: string -}) => ( - - - - {title} - - {steps.map((step) => ( - - {step} - - ))} - - - {note ? ( - - {note} - - ) : null} - - Official documentation - - {href} - - - - - -) - -const EndpointRow = ({ - label, - href, - description, -}: { - label: string - href: string - description: string -}) => ( - - {label} - - {href} - - - {description} - - -) - -const CopyUrlBox = ({ - value, - onCopy, -}: { - value: string - onCopy: (value: string) => void -}) => ( - - - {value} - - - onCopy(value)} - sx={{ - position: 'absolute', - top: 6, - right: 6, - }} - aria-label="Copy OGC landing page URL" - > - - - - -) From 5893f56bd29b44a8ea5fd626a4980983d7d9bdad Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Tue, 28 Apr 2026 15:56:53 -0500 Subject: [PATCH 4/6] chore(layout/sider): Update sider for update pg links --- src/components/layout/sider.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/layout/sider.tsx b/src/components/layout/sider.tsx index 85160060..47eb7295 100644 --- a/src/components/layout/sider.tsx +++ b/src/components/layout/sider.tsx @@ -416,9 +416,8 @@ export const ThemedSiderV2: React.FC = ({ > {[ { to: '/about', label: 'About' }, - { to: '/ocotillo/help', label: 'Connect Desktop GIS' }, + { to: '/ogcapi', label: 'Connect Desktop GIS' }, { to: '/report-a-bug', label: 'Report a Bug' }, - { to: '/ogcapi', label: 'Connect Desktop GIS (md)' }, ].map(({ to, label }) => ( Date: Tue, 28 Apr 2026 16:13:29 -0500 Subject: [PATCH 5/6] doc(ogcapi): Update content based on https://nmbgmr.atlassian.net/browse/BDMS-769 ticket --- public/content/ogcapi.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/public/content/ogcapi.md b/public/content/ogcapi.md index 8ecc64aa..5d14294b 100644 --- a/public/content/ogcapi.md +++ b/public/content/ogcapi.md @@ -14,12 +14,14 @@ deck: Use the Ocotillo OGC API Features endpoint to browse collections in ArcGIS ## ArcGIS Pro / Desktop -1. Open the **Catalog pane** and create a new OGC API Server connection. -2. Paste the Ocotillo landing page URL. -3. Expand the server connection, choose the collection you want, and add it to the current map. -4. If ArcGIS prompts for layer options, use extent or maximum-feature limits for large collections. +1. On the **Insert** tab, in the **Project** group, click **Connections > Server > New OGC API Server**. The **Add OGC API Server Connection** dialog box appears. +2. Enter this URL ([{{ ocotillo_api_url }}/ogcapi]({{ ocotillo_api_url }}/ogcapi)) in the **Server URL** text box. +3. Leave the rest of the options as-is and click OK. +4. In the Catalog pane, expand the “Servers” folder. You should see the Ocotillo OGC API connection. Expand the connection, then expand “Features”. Drag the datasets you want into your map area. +5. When adding a layer, a dialog box will appear with spatial extent options. Click OK to add the entire contents. You can also check the “Use Spatial Extent” box and spatially filter via options in the “Get extent from:” box - e.g. spatially filter by existing layers, selected polygon extents, visible extent, etc. **Official documentation:** +[How to add OGC API datasets to ArcGIS Pro](https://pro.arcgis.com/en/pro-app/latest/help/data/services/add-ogc-api-services.htm) [https://pro.arcgis.com/en/pro-app/latest/help/data/services/use-ogc-api-services.htm](https://pro.arcgis.com/en/pro-app/latest/help/data/services/use-ogc-api-services.htm) --- From a1491bb864005305f86877e17877a801892e62ad Mon Sep 17 00:00:00 2001 From: Jeremy Zilar Date: Tue, 28 Apr 2026 17:44:43 -0400 Subject: [PATCH 6/6] Break authentik-provider utils barrel cycle that hung Vitest access-control tests. Import PKCE and JWT helpers from Auth, Http, and Jwt modules instead of the utils index so ApiFetch no longer pulls authentik-provider during module init. Partial-mock @/config with importOriginal instead of vi.importActual inside the mock factory. --- src/providers/authentik-provider.ts | 9 ++++----- .../authentik-provider.access-control.test.ts | 10 ++++------ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/providers/authentik-provider.ts b/src/providers/authentik-provider.ts index 15a93b13..3a81e412 100644 --- a/src/providers/authentik-provider.ts +++ b/src/providers/authentik-provider.ts @@ -10,10 +10,9 @@ import { generateCodeChallenge, generateCodeVerifier, generateOAuthState, - getStatusCode, - hasError, - isJwtExpired, -} from '@/utils' +} from '@/utils/Auth' +import { getStatusCode, hasError } from '@/utils/Http' +import { isJwtExpired } from '@/utils/Jwt' import { HttpStatus } from '@/enums' import { AUTHENTIK_URL, @@ -22,7 +21,7 @@ import { STORAGE_KEYS, IS_TESTING_AUTH, } from '@/config' -import { normalizeAccessControlGroups } from '@/utils' +import { normalizeAccessControlGroups } from '@/utils/accessControl' const gravatarUrl = (email: string) => { let hash = email.trim().toLowerCase() diff --git a/src/test/providers/authentik-provider.access-control.test.ts b/src/test/providers/authentik-provider.access-control.test.ts index d406b1e4..6c5eab8a 100644 --- a/src/test/providers/authentik-provider.access-control.test.ts +++ b/src/test/providers/authentik-provider.access-control.test.ts @@ -50,9 +50,8 @@ describe('authentik provider access-control normalization', () => { }) it('normalizes token groups from the ID token when testing auth is disabled', async () => { - vi.doMock('@/config', async () => { - const actual = - await vi.importActual('@/config') + vi.doMock('@/config', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, IS_TESTING_AUTH: false } }) vi.doMock('jwt-decode', () => ({ @@ -79,9 +78,8 @@ describe('authentik provider access-control normalization', () => { }) it('returns null when there is no ID token', async () => { - vi.doMock('@/config', async () => { - const actual = - await vi.importActual('@/config') + vi.doMock('@/config', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, IS_TESTING_AUTH: false } }) vi.doMock('jwt-decode', () => ({