Updates to howe the file tree udpates
This commit is contained in:
parent
4370cbacf4
commit
d3287a0fa4
@ -48,6 +48,7 @@ import {
|
|||||||
FontSizeOutlined,
|
FontSizeOutlined,
|
||||||
BuildOutlined,
|
BuildOutlined,
|
||||||
HolderOutlined,
|
HolderOutlined,
|
||||||
|
QuestionCircleOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import Editor from '@monaco-editor/react';
|
import Editor from '@monaco-editor/react';
|
||||||
import type { OnMount } from '@monaco-editor/react';
|
import type { OnMount } from '@monaco-editor/react';
|
||||||
@ -402,11 +403,12 @@ export default function DocsPage() {
|
|||||||
const [messageApi, contextHolder] = message.useMessage();
|
const [messageApi, contextHolder] = message.useMessage();
|
||||||
|
|
||||||
// Fetch file tree
|
// Fetch file tree
|
||||||
const fetchTree = useCallback(async (showLoading = true) => {
|
const fetchTree = useCallback(async (showLoading = true, force = false) => {
|
||||||
try {
|
try {
|
||||||
if (showLoading) setLoading(true);
|
if (showLoading) setLoading(true);
|
||||||
setFetchError(false);
|
setFetchError(false);
|
||||||
const res = await api.get<FileNode[]>('/docs/files');
|
const url = force ? '/docs/files?force=true' : '/docs/files';
|
||||||
|
const res = await api.get<FileNode[]>(url);
|
||||||
setFileTree(res.data);
|
setFileTree(res.data);
|
||||||
setCachedTree(res.data);
|
setCachedTree(res.data);
|
||||||
} catch {
|
} catch {
|
||||||
@ -690,6 +692,11 @@ export default function DocsPage() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const refreshTree = useCallback(() => {
|
||||||
|
invalidateTreeCache();
|
||||||
|
fetchTree(false, true);
|
||||||
|
}, [fetchTree]);
|
||||||
|
|
||||||
// File tree context menu
|
// File tree context menu
|
||||||
const getContextMenuItems = useCallback((nodePath: string, isDirectory: boolean): MenuProps['items'] => {
|
const getContextMenuItems = useCallback((nodePath: string, isDirectory: boolean): MenuProps['items'] => {
|
||||||
const items: MenuProps['items'] = [];
|
const items: MenuProps['items'] = [];
|
||||||
@ -848,6 +855,10 @@ export default function DocsPage() {
|
|||||||
// Header actions
|
// Header actions
|
||||||
const headerActions = useMemo(() => (
|
const headerActions = useMemo(() => (
|
||||||
<Space size={8}>
|
<Space size={8}>
|
||||||
|
<Tooltip title="MkDocs Reference">
|
||||||
|
<Button type="text" icon={<QuestionCircleOutlined />} onClick={() => window.open('https://squidfunk.github.io/mkdocs-material/reference/', '_blank', 'noopener,noreferrer')} size="middle" />
|
||||||
|
</Tooltip>
|
||||||
|
<div style={{ width: 1, height: 24, background: token.colorBorderSecondary, margin: '0 4px' }} />
|
||||||
<Tooltip title="Editor + Preview">
|
<Tooltip title="Editor + Preview">
|
||||||
<Button type={layout === 'split' ? 'primary' : 'text'} icon={<ColumnWidthOutlined />} onClick={() => setLayout('split')} size="middle" />
|
<Button type={layout === 'split' ? 'primary' : 'text'} icon={<ColumnWidthOutlined />} onClick={() => setLayout('split')} size="middle" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -1056,6 +1067,9 @@ export default function DocsPage() {
|
|||||||
style={{ width: 28, height: 28, color: filterVisible ? token.colorPrimary : token.colorTextSecondary }}
|
style={{ width: 28, height: 28, color: filterVisible ? token.colorPrimary : token.colorTextSecondary }}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip title="Refresh" mouseEnterDelay={0.4}>
|
||||||
|
<Button type="text" size="small" icon={<ReloadOutlined />} onClick={refreshTree} aria-label="Refresh file tree" style={{ width: 28, height: 28, color: token.colorTextSecondary }} />
|
||||||
|
</Tooltip>
|
||||||
<Tooltip title="Expand All" mouseEnterDelay={0.4}>
|
<Tooltip title="Expand All" mouseEnterDelay={0.4}>
|
||||||
<Button type="text" size="small" icon={<NodeExpandOutlined />} onClick={expandAll} aria-label="Expand all folders" style={{ width: 28, height: 28, color: token.colorTextSecondary }} />
|
<Button type="text" size="small" icon={<NodeExpandOutlined />} onClick={expandAll} aria-label="Expand all folders" style={{ width: 28, height: 28, color: token.colorTextSecondary }} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -18,7 +18,8 @@ const DOCS_ROOT = pathResolve(env.MKDOCS_DOCS_PATH);
|
|||||||
// Redis cache configuration
|
// Redis cache configuration
|
||||||
const CACHE_KEY_PREFIX = 'DOCS_CACHE:';
|
const CACHE_KEY_PREFIX = 'DOCS_CACHE:';
|
||||||
const TREE_CACHE_KEY = `${CACHE_KEY_PREFIX}tree`;
|
const TREE_CACHE_KEY = `${CACHE_KEY_PREFIX}tree`;
|
||||||
const FILE_CACHE_TTL = 60 * 60; // 1 hour
|
const TREE_CACHE_TTL = 30; // 30 seconds — short so external changes show quickly
|
||||||
|
const FILE_CONTENT_CACHE_TTL = 60 * 60; // 1 hour for file content
|
||||||
|
|
||||||
function hashFilePath(path: string): string {
|
function hashFilePath(path: string): string {
|
||||||
return crypto.createHash('sha256').update(path).digest('hex').substring(0, 16);
|
return crypto.createHash('sha256').update(path).digest('hex').substring(0, 16);
|
||||||
@ -97,7 +98,7 @@ async function listTree(dir: string = DOCS_ROOT, relBase: string = ''): Promise<
|
|||||||
// Cache root result
|
// Cache root result
|
||||||
if (dir === DOCS_ROOT && !relBase) {
|
if (dir === DOCS_ROOT && !relBase) {
|
||||||
try {
|
try {
|
||||||
await redis.setex(TREE_CACHE_KEY, FILE_CACHE_TTL, JSON.stringify(nodes));
|
await redis.setex(TREE_CACHE_KEY, TREE_CACHE_TTL, JSON.stringify(nodes));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn('Failed to cache docs tree:', err);
|
logger.warn('Failed to cache docs tree:', err);
|
||||||
}
|
}
|
||||||
@ -129,7 +130,7 @@ async function readFileContent(relativePath: string): Promise<string> {
|
|||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
try {
|
try {
|
||||||
await redis.setex(cacheKey, FILE_CACHE_TTL, content);
|
await redis.setex(cacheKey, FILE_CONTENT_CACHE_TTL, content);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn('Failed to cache file content:', err);
|
logger.warn('Failed to cache file content:', err);
|
||||||
}
|
}
|
||||||
@ -249,6 +250,14 @@ function isEditableFile(relativePath: string): boolean {
|
|||||||
return ['.md', '.txt', '.yml', '.yaml', '.json', '.css', '.html', '.js'].includes(ext);
|
return ['.md', '.txt', '.yml', '.yaml', '.json', '.css', '.html', '.js'].includes(ext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function invalidateTreeCache(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await redis.del(TREE_CACHE_KEY);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('Failed to invalidate tree cache:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const docsFilesService = {
|
export const docsFilesService = {
|
||||||
listTree,
|
listTree,
|
||||||
readFileContent,
|
readFileContent,
|
||||||
@ -258,4 +267,5 @@ export const docsFilesService = {
|
|||||||
renameFile,
|
renameFile,
|
||||||
safeResolve,
|
safeResolve,
|
||||||
isEditableFile,
|
isEditableFile,
|
||||||
|
invalidateTreeCache,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -109,9 +109,12 @@ router.post(
|
|||||||
// GET /api/docs/files — list file tree
|
// GET /api/docs/files — list file tree
|
||||||
router.get(
|
router.get(
|
||||||
'/files',
|
'/files',
|
||||||
async (_req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
cm_docs_operations.inc({ operation: 'list' });
|
cm_docs_operations.inc({ operation: 'list' });
|
||||||
|
if (req.query['force'] === 'true') {
|
||||||
|
await docsFilesService.invalidateTreeCache();
|
||||||
|
}
|
||||||
const tree = await docsFilesService.listTree();
|
const tree = await docsFilesService.listTree();
|
||||||
res.json(tree);
|
res.json(tree);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user