src_pages_UserDocumentation.jsx
import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import "../../docs/styles/github-markdown-light.css";
const backendURL = __APP_CONFIG__.backendURL;
/**
* @module UserDocumentation
*/
/**
* A component for displaying user documentation from markdown files.
* @component
* @param {Object} [initialData] - Initial data. Contain path and content. If content is defined,
* this component will not fetch and display the passed content. Otherwise will fetch the path.
* @param {Function} onStateChange - Call back function to let parent component know about state change
* @param {Object} [savedState] - Saved state. Contain path and content. If content is defined,
* this component will not fetch and display the passed content. Otherwise will fetch the path.
* @returns {JSX.Element} Rendered documentation component
* @example
* // Basic usage
* <UserDocumentation />
*
* // To open this page as new tab, dispatch event as follows
* const event = new CustomEvent('addDocumentationTab', {
* detail: {
*
* // String
* path: 'stimulation',
*
* // Markdown String
* // If content is defined, the component will display that instead of fetching the path.
* // Otherwise it will fetch using the path
* content: "Content"
* }
* });
* window.dispatchEvent(event);
*/
const UserDocumentation = ({
initialData = {},
onStateChange,
savedState = {},
}) => {
const [markdownContent, setMarkdownContent] = useState(
savedState.content || initialData.content || null,
);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [path, _] = useState(
useParams().path || savedState.path || initialData.path,
);
/**
* Notify parent component of state changes
*/
useEffect(() => {
if (onStateChange) {
onStateChange({
content: markdownContent,
path: path,
});
}
}, [markdownContent, path]);
/**
* Fetch documentation from path if content is null
*/
useEffect(() => {
if (!markdownContent) {
fetchDocumentation(path);
}
}, []);
/**
* Fetches documentation content from the backend
* @async
* @param {string} path - The documentation path to fetch
*/
const fetchDocumentation = async (path) => {
try {
setIsLoading(true);
setError(null);
// Fetch the markdown through backend
const response = await fetch(
`${backendURL}/api/usage-docs/${path}`,
);
if (!response.ok) {
throw new Error("Document not found");
}
const data = await response.text();
setMarkdownContent(data);
} catch (err) {
setError(err.message);
setMarkdownContent(
`# **The requested document could not be loaded.**`,
);
} finally {
setIsLoading(false);
}
};
if (isLoading) {
return (
<div className="flex justify-center items-center h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500" data-testid="loading-spinner"></div>
</div>
);
}
return (
<div className="max-w-5xl mx-auto px-4 py-8">
{error && (
<div
className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6"
role="alert"
>
<p className="font-bold">Error</p>
<p>{error}</p>
</div>
)}
{/* Different style depending on how it was opened - onStateChange is set means it is opened as a tab */}
<div
className={
onStateChange
? "h-[82vh] overflow-scroll markdown-body"
: "overflow-scroll markdown-body"
}
>
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{markdownContent}
</ReactMarkdown>
</div>
</div>
);
};
export default UserDocumentation;