-
-
Notifications
You must be signed in to change notification settings - Fork 11.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
π style: Add Cache, Metadata, FeatureFlag Viewer to DevPanel (#5764)
* β¨ feat: Add Chache DevTool * π style: Update Dev Panel style * β¨ feat: Add seo debug * β¨ feat: Add Feature Flag * π style: Update DevTool * π style: Update style * π style: Update style
- Loading branch information
1 parent
80fd5a8
commit db4e9c7
Showing
24 changed files
with
1,072 additions
and
299 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
'use client'; | ||
|
||
import { RefreshCw } from 'lucide-react'; | ||
import { memo } from 'react'; | ||
|
||
import Header from '../../features/Header'; | ||
import Table from '../../features/Table'; | ||
import { useCachePanelContext } from '../cacheProvider'; | ||
|
||
const DataTable = memo(() => { | ||
const { entries, isLoading, refreshData } = useCachePanelContext(); | ||
return ( | ||
<> | ||
<Header | ||
actions={[ | ||
{ | ||
icon: RefreshCw, | ||
onClick: () => refreshData(), | ||
title: 'Refresh', | ||
}, | ||
]} | ||
title="Cache Entries" | ||
/> | ||
<Table | ||
columns={['url', 'headers.content-type', 'body', 'kind', 'tags', 'revalidate', 'timestamp']} | ||
dataSource={entries} | ||
loading={isLoading} | ||
/> | ||
</> | ||
); | ||
}); | ||
|
||
export default DataTable; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
'use client'; | ||
|
||
import { usePathname } from 'next/navigation'; | ||
import { | ||
PropsWithChildren, | ||
createContext, | ||
useContext, | ||
useEffect, | ||
useState, | ||
useTransition, | ||
} from 'react'; | ||
|
||
import { getCacheFiles } from './getCacheEntries'; | ||
import type { NextCacheFileData } from './schema'; | ||
|
||
interface CachePanelContextProps { | ||
entries: NextCacheFileData[]; | ||
isLoading: boolean; | ||
refreshData: () => void; | ||
setEntries: (value: NextCacheFileData[]) => void; | ||
} | ||
|
||
const CachePanelContext = createContext<CachePanelContextProps>({ | ||
entries: [], | ||
isLoading: false, | ||
refreshData: () => {}, | ||
setEntries: () => {}, | ||
}); | ||
|
||
export const useCachePanelContext = () => useContext(CachePanelContext); | ||
|
||
export const CachePanelContextProvider = ( | ||
props: PropsWithChildren<{ | ||
entries: NextCacheFileData[]; | ||
}>, | ||
) => { | ||
const [isLoading, startTransition] = useTransition(); | ||
const [entries, setEntries] = useState(props.entries); | ||
const pathname = usePathname(); | ||
|
||
const refreshData = () => { | ||
startTransition(async () => { | ||
const files = await getCacheFiles(); | ||
setEntries(files ?? []); | ||
}); | ||
}; | ||
|
||
useEffect(() => { | ||
refreshData(); | ||
}, [pathname]); | ||
|
||
return ( | ||
<CachePanelContext.Provider | ||
value={{ | ||
entries, | ||
isLoading, | ||
refreshData, | ||
setEntries, | ||
}} | ||
> | ||
{props.children} | ||
</CachePanelContext.Provider> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
'use server'; | ||
|
||
import { existsSync, promises } from 'node:fs'; | ||
import pMap from 'p-map'; | ||
import { ZodError } from 'zod'; | ||
|
||
import { type NextCacheFileData, nextCacheFileSchema } from './schema'; | ||
|
||
const cachePath = '.next/cache/fetch-cache'; | ||
|
||
export const getCacheFiles = async (): Promise<NextCacheFileData[]> => { | ||
if (!existsSync(cachePath)) { | ||
return []; | ||
} | ||
const files = await promises.readdir(cachePath); | ||
let result: NextCacheFileData[] = (await pMap(files, async (file) => { | ||
// ignore tags-manifest file | ||
if (/manifest/.test(file)) return false; | ||
try { | ||
const fileContent = await promises.readFile(`${cachePath}/${file}`).catch((err) => { | ||
throw new Error(`Error reading file ${file}`, { | ||
cause: err, | ||
}); | ||
}); | ||
|
||
const fileStats = await promises.stat(`${cachePath}/${file}`).catch((err) => { | ||
throw new Error(`Error reading file ${file}`, { | ||
cause: err, | ||
}); | ||
}); | ||
|
||
const jsonData = JSON.parse(fileContent.toString()); | ||
|
||
return nextCacheFileSchema.parse({ | ||
...jsonData, | ||
id: file, | ||
timestamp: new Date(fileStats.birthtime), | ||
}); | ||
} catch (error) { | ||
if (error instanceof ZodError) { | ||
const issues = error.issues; | ||
console.error(`File ${file} do not match the schema`, issues); | ||
} | ||
console.error(`Error parsing ${file}`); | ||
return false; | ||
} | ||
})) as NextCacheFileData[]; | ||
|
||
result = result.filter(Boolean) as NextCacheFileData[]; | ||
|
||
return result.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { Empty } from 'antd'; | ||
import { Center } from 'react-layout-kit'; | ||
|
||
import DataTable from './DataTable'; | ||
import { CachePanelContextProvider } from './cacheProvider'; | ||
import { getCacheFiles } from './getCacheEntries'; | ||
|
||
const CacheViewer = async () => { | ||
const files = await getCacheFiles(); | ||
|
||
if (!files || files.length === 0) | ||
return ( | ||
<Center height={'80%'}> | ||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /> | ||
</Center> | ||
); | ||
|
||
return ( | ||
<CachePanelContextProvider entries={files}> | ||
<DataTable /> | ||
</CachePanelContextProvider> | ||
); | ||
}; | ||
|
||
export default CacheViewer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { z } from 'zod'; | ||
|
||
const unstableCacheFileSchema = z.object({ | ||
data: z.object({ | ||
body: z.string(), | ||
headers: z.object({}).transform(() => null), | ||
status: z.number(), | ||
url: z.literal(''), | ||
}), | ||
kind: z.union([z.literal('FETCH'), z.unknown()]), | ||
revalidate: z.number().optional(), | ||
tags: z.array(z.string()).optional().default([]), | ||
}); | ||
|
||
const fetchCacheFileSchema = z.object({ | ||
data: z.object({ | ||
body: z.string(), | ||
headers: z.record(z.string(), z.string()), | ||
status: z.number(), | ||
url: z.string().url(), | ||
}), | ||
id: z.string(), | ||
kind: z.union([z.literal('FETCH'), z.unknown()]), | ||
revalidate: z.number().optional(), | ||
tags: z.array(z.string()).optional().default([]), | ||
}); | ||
|
||
const atou = (str: string, type: string) => { | ||
if (type.startsWith('image/')) return `data:${type};base64,${str}`; | ||
return Buffer.from(str, 'base64').toString(); | ||
}; | ||
export const nextCacheFileSchema = z | ||
.union([unstableCacheFileSchema, fetchCacheFileSchema]) | ||
.transform((item) => { | ||
const { data, ...cacheEntry } = item; | ||
const body = | ||
data.url !== '' | ||
? atou(data.body, data.headers ? data.headers['content-type'] : '') | ||
: data.body; | ||
return { | ||
...cacheEntry, | ||
...data, | ||
body, | ||
timestamp: data.headers?.date ? new Date(data.headers?.date) : new Date(), | ||
url: data.url === '' ? 'unstable_cache' : data.url, | ||
}; | ||
}); | ||
|
||
export type NextCacheFileData = z.infer<typeof nextCacheFileSchema>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
'use client'; | ||
|
||
import { Form, Highlighter } from '@lobehub/ui'; | ||
import { Switch } from 'antd'; | ||
import { createStyles } from 'antd-style'; | ||
import { snakeCase } from 'lodash-es'; | ||
import { ListRestartIcon } from 'lucide-react'; | ||
import { memo, useMemo, useState } from 'react'; | ||
import { Flexbox } from 'react-layout-kit'; | ||
|
||
import { DEFAULT_FEATURE_FLAGS } from '@/config/featureFlags'; | ||
|
||
import Header from '../features/Header'; | ||
|
||
const useStyles = createStyles(({ css, token, prefixCls }) => ({ | ||
container: css` | ||
* { | ||
font-family: ${token.fontFamilyCode}; | ||
font-size: 12px; | ||
} | ||
.${prefixCls}-form-item { | ||
padding-block: 4px !important; | ||
} | ||
`, | ||
})); | ||
|
||
const FeatureFlagForm = memo<{ flags: any }>(({ flags }) => { | ||
const { styles } = useStyles(); | ||
const [data, setData] = useState(flags); | ||
const [form] = Form.useForm(); | ||
|
||
const output = useMemo( | ||
() => | ||
Object.entries(data).map(([key, value]) => { | ||
const flag = snakeCase(key); | ||
// @ts-ignore | ||
if (DEFAULT_FEATURE_FLAGS[flag] === value) return false; | ||
if (value === true) return `+${flag}`; | ||
return `-${flag}`; | ||
}), | ||
[data], | ||
); | ||
|
||
return ( | ||
<> | ||
<Header | ||
actions={[ | ||
{ | ||
icon: ListRestartIcon, | ||
onClick: () => { | ||
form.resetFields(); | ||
setData(flags); | ||
}, | ||
title: 'Reset', | ||
}, | ||
]} | ||
title={'Feature Flag Env'} | ||
/> | ||
<Flexbox | ||
className={styles.container} | ||
height={'100%'} | ||
paddingInline={16} | ||
style={{ overflow: 'auto', position: 'relative' }} | ||
width={'100%'} | ||
> | ||
<Form | ||
form={form} | ||
initialValues={flags} | ||
itemMinWidth={'max(75%,240px)'} | ||
items={Object.keys(flags).map((key) => { | ||
return { | ||
children: <Switch size={'small'} />, | ||
label: snakeCase(key), | ||
minWidth: undefined, | ||
name: key, | ||
valuePropName: 'checked', | ||
}; | ||
})} | ||
itemsType={'flat'} | ||
onValuesChange={(_, v) => setData(v)} | ||
variant={'pure'} | ||
/> | ||
</Flexbox> | ||
<Highlighter | ||
language={'env'} | ||
style={{ flex: 'none', fontSize: 12 }} | ||
wrap | ||
>{`FEATURE_FLAGS="${output.filter(Boolean).join(',')}"`}</Highlighter> | ||
</> | ||
); | ||
}); | ||
|
||
export default FeatureFlagForm; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { getServerFeatureFlagsValue } from '@/config/featureFlags'; | ||
|
||
import FeatureFlagForm from './Form'; | ||
|
||
const FeatureFlagViewer = () => { | ||
const serverFeatureFlags = getServerFeatureFlagsValue(); | ||
|
||
return <FeatureFlagForm flags={serverFeatureFlags} />; | ||
}; | ||
|
||
export default FeatureFlagViewer; |
Oops, something went wrong.