-
Notifications
You must be signed in to change notification settings - Fork 0
feat: admin ability feed detail page #88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
77fdc3b
f508b98
39d6908
7a1a1ce
2eb6040
8dfa848
e611722
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,10 +24,12 @@ import { | |
| type GTFSRTFeedType, | ||
| } from '../../services/feeds/utils'; | ||
| import ClientDownloadButton from './components/ClientDownloadButton'; | ||
| import RevalidateCacheButton from './components/RevalidateCacheButton'; | ||
| import { type components } from '../../services/feeds/types'; | ||
| import ClientQualityReportButton from './components/ClientQualityReportButton'; | ||
| import { getBoundingBox } from './Feed.functions'; | ||
| import dynamic from 'next/dynamic'; | ||
| import { ContentBox } from '../../components/ContentBox'; | ||
|
|
||
| const CoveredAreaMap = dynamic( | ||
| async () => | ||
|
|
@@ -68,6 +70,7 @@ interface Props { | |
| relatedGtfsRtFeeds?: GTFSRTFeedType[]; | ||
| totalRoutes?: number; | ||
| routeTypes?: string[]; | ||
| isMobilityDatabaseAdmin?: boolean; | ||
| } | ||
|
|
||
| type LatestDatasetFull = components['schemas']['GtfsDataset'] | undefined; | ||
|
|
@@ -79,6 +82,7 @@ export default async function FeedView({ | |
| relatedGtfsRtFeeds = [], | ||
| totalRoutes, | ||
| routeTypes, | ||
| isMobilityDatabaseAdmin = false, | ||
| }: Props): Promise<React.ReactElement> { | ||
| if (feed == undefined) notFound(); | ||
|
|
||
|
|
@@ -418,6 +422,23 @@ export default async function FeedView({ | |
| </Box> | ||
| </Box> | ||
| </Box> | ||
| {isMobilityDatabaseAdmin && ( | ||
| <ContentBox | ||
| title={'MobilityDatabase Admin Tools'} | ||
| subtitle={ | ||
| <> | ||
| This section is only visible to Mobility Data employees with an{' '} | ||
| <code>@mobilitydata.org</code> email address. It contains tools | ||
| for debugging and managing feed data | ||
| </> | ||
| } | ||
| sx={{ mt: 4, backgroundColor: 'background.paper' }} | ||
| > | ||
| {feed?.id != null && feed?.id !== '' && ( | ||
| <RevalidateCacheButton feedId={feed.id} /> | ||
| )} | ||
| </ContentBox> | ||
|
Comment on lines
+425
to
+440
|
||
| )} | ||
| </Container> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,18 @@ | ||||||||||||||||||||
| 'use server'; | ||||||||||||||||||||
| import { | ||||||||||||||||||||
| getCurrentUserFromCookie, | ||||||||||||||||||||
| isMobilityDatabaseAdmin, | ||||||||||||||||||||
| } from '../../utils/auth-server'; | ||||||||||||||||||||
| import { revalidateSpecificFeeds } from '../../utils/revalidate-feeds'; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export async function revalidateFeedCache( | ||||||||||||||||||||
| feedId: string, | ||||||||||||||||||||
| ): Promise<{ ok: boolean; message: string }> { | ||||||||||||||||||||
| const user = await getCurrentUserFromCookie(); | ||||||||||||||||||||
| if (!isMobilityDatabaseAdmin(user?.email)) { | ||||||||||||||||||||
| return { ok: false, message: 'Unauthorized: admin access required' }; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| revalidateSpecificFeeds([feedId]); | ||||||||||||||||||||
| return { ok: true, message: `Cache revalidated for feed ${feedId}` }; | ||||||||||||||||||||
|
Comment on lines
+16
to
+17
|
||||||||||||||||||||
| revalidateSpecificFeeds([feedId]); | |
| return { ok: true, message: `Cache revalidated for feed ${feedId}` }; | |
| const normalizedFeedId = feedId.trim(); | |
| if (normalizedFeedId.length === 0) { | |
| return { ok: false, message: 'Invalid feed id: value is empty or whitespace' }; | |
| } | |
| revalidateSpecificFeeds([normalizedFeedId]); | |
| return { ok: true, message: `Cache revalidated for feed ${normalizedFeedId}` }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| 'use client'; | ||
| import { useState, useTransition } from 'react'; | ||
| import Box from '@mui/material/Box'; | ||
| import Button from '@mui/material/Button'; | ||
| import Typography from '@mui/material/Typography'; | ||
| import { revalidateFeedCache } from '../actions'; | ||
|
|
||
| interface Props { | ||
| feedId: string; | ||
| } | ||
|
|
||
| export default function RevalidateCacheButton({ | ||
| feedId, | ||
| }: Props): React.ReactElement { | ||
| const [isPending, startTransition] = useTransition(); | ||
| const [result, setResult] = useState<{ | ||
| ok: boolean; | ||
| message: string; | ||
| } | null>(null); | ||
|
|
||
| const handleClick = (): void => { | ||
| setResult(null); | ||
|
|
||
| startTransition(async () => { | ||
| try { | ||
| const res = await revalidateFeedCache(feedId); | ||
| setResult(res); | ||
| } catch { | ||
| setResult({ | ||
| ok: false, | ||
| message: 'Failed to revalidate the cache. Please try again.', | ||
| }); | ||
| } | ||
| }); | ||
| }; | ||
|
|
||
| return ( | ||
| <Box | ||
| sx={{ | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| gap: 1, | ||
| width: 'fit-content', | ||
| }} | ||
| > | ||
| <Button variant='contained' onClick={handleClick} disabled={isPending}> | ||
| {isPending ? 'Revalidating…' : 'Revalidate The Cache of This Page'} | ||
| </Button> | ||
|
Comment on lines
+46
to
+48
|
||
| {result != null && !isPending && ( | ||
| <Typography | ||
| variant='caption' | ||
| color={result.ok ? 'success.main' : 'error.main'} | ||
| > | ||
| {result.message} | ||
| </Typography> | ||
| )} | ||
| </Box> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import 'server-only'; | ||
| import { revalidatePath, revalidateTag } from 'next/cache'; | ||
| import { AVAILABLE_LOCALES } from '../../i18n/routing'; | ||
|
|
||
| /** | ||
| * Revalidates the ISR cache for specific feed pages. | ||
| * Applies to all feed types (gtfs, gtfs_rt, gbfs) since we don't know the type from the id alone. | ||
| * Also revalidates localized paths and /map sub-routes. | ||
| */ | ||
| export function revalidateSpecificFeeds(feedIds: string[]): void { | ||
| const localPaths = AVAILABLE_LOCALES.filter((loc) => loc !== 'en'); | ||
| const pathsToRevalidate: string[] = []; | ||
|
|
||
| feedIds.forEach((id) => { | ||
| revalidateTag(`feed-${id}`, 'max'); | ||
| // The id will try to revalidate all feed types with that id, but that's necessary since we don't know the feed type here and it's not a big deal if we revalidate some non-existent pages | ||
| pathsToRevalidate.push(`/feeds/gtfs/${id}`); | ||
| pathsToRevalidate.push(`/feeds/gtfs_rt/${id}`); | ||
| pathsToRevalidate.push(`/feeds/gbfs/${id}`); | ||
| }); | ||
|
|
||
| console.log('Revalidating paths:', pathsToRevalidate); | ||
|
|
||
| pathsToRevalidate.forEach((path) => { | ||
| revalidatePath(path); | ||
| revalidatePath(path + '/map'); | ||
| localPaths.forEach((loc) => { | ||
| revalidatePath(`/${loc}${path}`); | ||
| revalidatePath(`/${loc}${path}/map`); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| /** Clears cache for all feed pages (ISR-cached layout). */ | ||
| export function revalidateAllFeeds(): void { | ||
| revalidateTag('guest-feeds', 'max'); | ||
| revalidatePath('/[locale]/feeds/[feedDataType]/[feedId]', 'layout'); | ||
| } | ||
|
|
||
| /** Clears cache for all GBFS feed pages (ISR-cached layout). */ | ||
| export function revalidateAllGbfsFeeds(): void { | ||
| revalidateTag('feed-type-gbfs', 'max'); | ||
| revalidatePath('/[locale]/feeds/gbfs/[feedId]', 'layout'); | ||
| } | ||
|
|
||
| /** Clears cache for all GTFS feed pages (ISR-cached layout). */ | ||
| export function revalidateAllGtfsFeeds(): void { | ||
| revalidateTag('feed-type-gtfs', 'max'); | ||
| revalidatePath('/[locale]/feeds/gtfs/[feedId]', 'layout'); | ||
| } | ||
|
|
||
| /** Clears cache for all GTFS-RT feed pages (ISR-cached layout). */ | ||
| export function revalidateAllGtfsRtFeeds(): void { | ||
| revalidateTag('feed-type-gtfs_rt', 'max'); | ||
| revalidatePath('/[locale]/feeds/gtfs_rt/[feedId]', 'layout'); | ||
| } | ||
|
|
||
| /** Clears cache for the entire site. */ | ||
| export function revalidateFullSite(): void { | ||
| revalidateTag('guest-feeds', 'max'); | ||
| revalidatePath('/', 'layout'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new admin section strings are hardcoded English, but this page already uses
next-intltranslations (getTranslations(...)). To keep localization consistent (especially for/fr), move these new strings into the message catalogs and render them viat(...).