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
38 changes: 36 additions & 2 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.3.0",
"@tanstack/react-query": "^5.67.3",
"@tanstack/react-table": "^8.21.3",
"@tiptap/extension-color": "^2.9.1",
"@tiptap/pm": "^2.9.1",
"@tiptap/react": "^2.9.1",
Expand Down Expand Up @@ -145,4 +146,4 @@
"refine": {
"projectId": "wCqQ1f-agx0FN-70pXIr"
}
}
}
9 changes: 9 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ 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 { AccessConsentPage } from '@/pages/access/consent'
import { AccessDestinationsPage } from '@/pages/access/destinations'
import { AccessGrantsPage } from '@/pages/access/grants'
import { ContentPage } from '@/pages/content'
import { TypographyPage } from '@/pages/example/TypographyPage'
import { Home } from '@/pages/home'
Expand Down Expand Up @@ -61,6 +64,12 @@ const App: React.FC = () => (
path="/ogcapi"
element={<ContentPage src="/content/ogcapi.md" />}
/>
<Route path="/access/grants" element={<AccessGrantsPage />} />
<Route
path="/access/destinations"
element={<AccessDestinationsPage />}
/>
<Route path="/access/consent" element={<AccessConsentPage />} />
{/* TEMPORARY: example specimen pages */}
<Route path="/example/typography" element={<TypographyPage />} />
<Route path="/ocotillo/*" element={<OcotilloRoutes />} />
Expand Down
114 changes: 114 additions & 0 deletions src/components/ui/table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import * as React from 'react'

import { cn } from '@/lib/utils'

function Table({ className, ...props }: React.ComponentProps<'table'>) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn('w-full caption-bottom text-sm', className)}
{...props}
/>
</div>
)
}

function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
return (
<thead
data-slot="table-header"
className={cn('[&_tr]:border-b', className)}
{...props}
/>
)
}

function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
return (
<tbody
data-slot="table-body"
className={cn('[&_tr:last-child]:border-0', className)}
{...props}
/>
)
}

function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
return (
<tfoot
data-slot="table-footer"
className={cn(
'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
className
)}
{...props}
/>
)
}

function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
return (
<tr
data-slot="table-row"
className={cn(
'border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted',
className
)}
{...props}
/>
)
}

function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
return (
<th
data-slot="table-head"
className={cn(
'h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className
)}
{...props}
/>
)
}

function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
return (
<td
data-slot="table-cell"
className={cn(
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className
)}
{...props}
/>
)
}

function TableCaption({
className,
...props
}: React.ComponentProps<'caption'>) {
return (
<caption
data-slot="table-caption"
className={cn('mt-4 text-sm text-muted-foreground', className)}
{...props}
/>
)
}

export {
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
}
10 changes: 9 additions & 1 deletion src/config/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { LucideIcon } from 'lucide-react'
import {
BookOpen,
Database,
Expand All @@ -10,9 +11,9 @@ import {
Map as MapIcon,
MapPin,
Search,
ShieldCheck,
Users,
} from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import type { PortalRole } from '@/utils/accessControl'

/**
Expand Down Expand Up @@ -149,6 +150,13 @@ export const RESOURCE_NAV: NavItem[] = [
resource: 'ocotillo.lexicon',
roles: adminOnly,
},
{
label: 'Access Control',
href: '/access/grants',
icon: ShieldCheck,
resource: 'ocotillo.access-grants',
roles: adminOnly,
},
{
label: 'Hydrograph Correction',
href: '/ocotillo/hydrograph-correction',
Expand Down
19 changes: 12 additions & 7 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
export * from './useListPageDataGridAnalytics'
export * from './useAbortableList'
export * from './useAccessCapabilities'
export * from './useSearchHistory'
export * from './useAccessConsent'
export * from './useAccessDestinations'
export * from './useAccessGrants'
export * from './useAll'
export * from './useAllNotes'
export * from './useContainerMinWidth'
export * from './useDebounce'
export * from './useElevation'
export * from './useGisArtifacts'
export * from './useGroups'
export * from './useLayer'
export * from './useLexicon'
export * from './useMostRecentObservation'
export * from './useListPageDataGridAnalytics'
export * from './useMeasuredHeight'
export * from './useMostRecentObservation'
export * from './useOSEPODInfo'
export * from './usePrimaryAndSecondaryContact'
export * from './useWellPdfData'
export * from './useSearchHistory'
export * from './useSearchModalState'
export * from './useSensor'
export * from './useSensorDeploymentRows'
export * from './useSidebarPanelSync'
export * from './useThingLayers'
export * from './useThingSearch'
export * from './useUSGSSiteInfo'
export * from './useViewportBbox'
export * from './useSearchModalState'
export * from './useSidebarPanelSync'
export * from './useWellDetails'
export * from './useContainerMinWidth'
export * from './useWellPdfData'
66 changes: 66 additions & 0 deletions src/hooks/useAccessConsent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { axiosCall, fetcher } from '@/providers/ocotillo-data-provider'
import {
type CreateConsentInput,
type PublicationConsent,
zPublicationConsent,
zPublicationConsentList,
} from '@/utils/accessConsent'

/**
* Publication consent (`/access/consent`).
*
* Unlike grants, this route still requires `thing_id` — there is no
* consent-wide audit view — so the tab stays thing-scoped and does not fetch
* until one is supplied.
*/
export const useAccessConsent = (
thingId: string,
options?: { includeRevoked?: boolean; enabled?: boolean }
) =>
useQuery<PublicationConsent[]>({
queryKey: ['access-consent', thingId, options?.includeRevoked ?? false],
enabled: (options?.enabled ?? true) && /^\d+$/.test(thingId.trim()),
queryFn: async () => {
const response = await fetcher('access/consent', {
params: {
thing_id: Number(thingId.trim()),
include_revoked: options?.includeRevoked ?? false,
},
})
return zPublicationConsentList.parse(response.data)
},
})

const useConsentMutation = <TVariables>(
mutationFn: (variables: TVariables) => Promise<PublicationConsent>
) => {
const queryClient = useQueryClient()

return useMutation({
mutationFn,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['access-consent'] })
// A consent change moves what a destination may read, and that view is
// computed server-side from these rows.
queryClient.invalidateQueries({ queryKey: ['access-published-things'] })
},
})
}

export const useCreateConsent = () =>
useConsentMutation(async (input: CreateConsentInput) => {
const response = await axiosCall('access/consent', {
method: 'POST',
data: input,
})
return zPublicationConsent.parse(response.data)
})

export const useRevokeConsent = () =>
useConsentMutation(async (consentId: number) => {
const response = await axiosCall(`access/consent/${consentId}/revocation`, {
method: 'POST',
})
return zPublicationConsent.parse(response.data)
})
Loading
Loading