diff --git a/ui/src/components/Document/DocumentViewMode.jsx b/ui/src/components/Document/DocumentViewMode.jsx
index 9c5330313..111fa4e28 100644
--- a/ui/src/components/Document/DocumentViewMode.jsx
+++ b/ui/src/components/Document/DocumentViewMode.jsx
@@ -12,6 +12,7 @@ import FolderViewer from 'viewers/FolderViewer';
import EmailViewer from 'viewers/EmailViewer';
import VideoViewer from 'viewers/VideoViewer';
import ArticleViewer from 'viewers/ArticleViewer';
+import JsonViewer from 'viewers/JsonViewer';
import withRouter from 'app/withRouter';
import { SectionLoading } from 'components/common';
import { selectEntityDirectionality } from 'selectors';
@@ -77,6 +78,10 @@ export class DocumentViewMode extends React.Component {
if (document.schema.isA('Article')) {
return ;
}
+ // Check for application/json MIME type
+ if (document.getProperty('mimeType')?.[0] === 'application/json') {
+ return ;
+ }
return ;
}
@@ -92,4 +97,4 @@ const mapStateToProps = (state, ownProps) => {
};
};
-export default compose(withRouter, connect(mapStateToProps))(DocumentViewMode);
+export default compose(withRouter, connect(mapStateToProps))(DocumentViewMode);
\ No newline at end of file
diff --git a/ui/src/viewers/JsonViewer.jsx b/ui/src/viewers/JsonViewer.jsx
new file mode 100644
index 000000000..e6bb5db50
--- /dev/null
+++ b/ui/src/viewers/JsonViewer.jsx
@@ -0,0 +1,253 @@
+import { useEffect, useState, useMemo } from 'react';
+import PropTypes from 'prop-types';
+import { NonIdealState, Spinner, Icon } from '@blueprintjs/core';
+import { useIntl } from 'react-intl';
+import queryString from 'query-string';
+import classNames from 'classnames';
+
+import axios from 'axios';
+import './JsonViewer.scss';
+
+// Helper function to get highlighted parts of a string
+const getHighlightedParts = (text, searchTerms) => {
+ if (!searchTerms || searchTerms.length === 0 || typeof text !== 'string') {
+ return [text]; // Always return an array
+ }
+ const regex = new RegExp(`(${searchTerms.join('|')})`, 'gi');
+ const parts = [];
+ let lastIndex = 0;
+ let match;
+
+ while ((match = regex.exec(text)) !== null) {
+ if (match.index > lastIndex) {
+ parts.push(text.substring(lastIndex, match.index));
+ }
+ parts.push({match[0]});
+ lastIndex = regex.lastIndex;
+ }
+
+ if (lastIndex < text.length) {
+ parts.push(text.substring(lastIndex));
+ }
+ return parts.length > 0 ? parts : [text];
+};
+
+// Recursive rendering function for JSON nodes
+const RenderJsonNode = ({ node, level = 0, searchTerms, isLastInObject }) => {
+ const [isOpen, setIsOpen] = useState(level < 2); // Initially open up to level 2
+
+ const toggleOpen = () => setIsOpen(!isOpen);
+
+ if (typeof node === 'string') {
+ return "{getHighlightedParts(node, searchTerms)}";
+ }
+ if (typeof node === 'number') {
+ return {node};
+ }
+ if (typeof node === 'boolean') {
+ return {String(node)};
+ }
+ if (node === null) {
+ return null;
+ }
+
+ const padding = `json-level-${level}`;
+
+ if (Array.isArray(node)) {
+ const isEmpty = node.length === 0;
+ return (
+
+
+ {isOpen || isEmpty ? '[' : `[...${node.length} items...]`}
+ {!isEmpty && }
+
+ {isOpen && !isEmpty && (
+ <>
+ {node.map((item, index) => (
+
+
+ {index < node.length - 1 && ,}
+
+ ))}
+ >
+ )}
+ {isOpen || isEmpty ? ']' : ''}
+
+ );
+ }
+
+ if (typeof node === 'object' && node !== null) {
+ const keys = Object.keys(node);
+ const isEmpty = keys.length === 0;
+ return (
+
+
+ {isOpen || isEmpty ? '{' : `{...${keys.length} props...}`}
+ {!isEmpty && }
+
+ {isOpen && !isEmpty && (
+ <>
+ {keys.map((key, index) => (
+
+ "{key}":
+
+ {index < keys.length - 1 && ,}
+
+ ))}
+ >
+ )}
+ {isOpen || isEmpty ? '}' : ''}
+
+ );
+ }
+ return null; // Should not happen for valid JSON
+};
+
+RenderJsonNode.propTypes = {
+ node: PropTypes.any,
+ level: PropTypes.number,
+ searchTerms: PropTypes.arrayOf(PropTypes.string),
+ isLastInObject: PropTypes.bool, // To help with trailing commas if needed, though CSS handles it now.
+};
+
+
+const JsonViewer = ({ document, query: queryProp }) => {
+ const [jsonData, setJsonData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const intl = useIntl();
+
+ const searchTerms = useMemo(() => {
+ if (queryProp) {
+ const parsedQuery = queryString.parse(queryProp);
+ if (parsedQuery && parsedQuery.q && typeof parsedQuery.q === 'string') {
+ // Ensure terms are not empty and are lowercased for case-insensitive search
+ return parsedQuery.q.toLowerCase().split(' ').filter(term => term.length > 0);
+ }
+ }
+ return [];
+ }, [queryProp]);
+
+ useEffect(() => {
+ const fetchContent = async () => {
+ setLoading(true);
+ setError(null);
+
+ const currentFileSize = Number(document?.getProperty('fileSize')?.[0] ?? 0)
+
+ if (currentFileSize > 2500000) {
+ setError(intl.formatMessage({
+ id: 'jsonViewer.fileSizeLimit',
+ defaultMessage: 'JSON Viewer not available: File size limit exceeded.',
+ }));
+ setLoading(false);
+ }
+ if (document && document.links && document.links.file) {
+ try {
+ const response = await axios.get(document.links.file, {
+ responseType: 'text',
+ transformResponse: [(data) => data],
+ });
+ const content = response.data;
+ try {
+ const parsedData = JSON.parse(content);
+ setJsonData(parsedData);
+ } catch (e) {
+ setError(intl.formatMessage({
+ id: 'jsonViewer.parseError',
+ defaultMessage: 'Error parsing JSON content.',
+ }));
+ }
+ } catch (e) {
+ setError(intl.formatMessage({
+ id: 'jsonViewer.fetchError',
+ defaultMessage: 'Error fetching document content.',
+ }));
+ }
+ } else if (document && document.content) {
+ try {
+ const parsedData = JSON.parse(document.content);
+ setJsonData(parsedData);
+ } catch (e) {
+ setError(intl.formatMessage({
+ id: 'jsonViewer.parseError',
+ defaultMessage: 'Error parsing JSON content.',
+ }));
+ }
+ } else {
+ setError(intl.formatMessage({
+ id: 'jsonViewer.noContent',
+ defaultMessage: 'Document content or file link is not available.',
+ }));
+ }
+ setLoading(false);
+ };
+
+ fetchContent();
+ }, [document, intl]);
+
+ // Note: The getHighlightedJson and getHighlightedParts from previous step are not directly used.
+ // Highlighting is now integrated within RenderJsonNode's string rendering.
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (!jsonData) {
+ return (
+
+ );
+ }
+
+ return (
+
+
{/* Using for overall monospace font and pre-like layout */}
+
+
+
+ );
+};
+
+JsonViewer.propTypes = {
+ document: PropTypes.shape({
+ content: PropTypes.string,
+ links: PropTypes.shape({
+ file: PropTypes.string,
+ }),
+ }),
+ query: PropTypes.string,
+};
+
+JsonViewer.defaultProps = {
+ document: null,
+ query: '',
+};
+
+export default JsonViewer;
\ No newline at end of file
diff --git a/ui/src/viewers/JsonViewer.scss b/ui/src/viewers/JsonViewer.scss
new file mode 100644
index 000000000..80cbc85b5
--- /dev/null
+++ b/ui/src/viewers/JsonViewer.scss
@@ -0,0 +1,80 @@
+.json-viewer-error {
+ color: red;
+ padding: 10px;
+ border: 1px solid red;
+}
+
+.json-viewer-loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 200px; // Ensure spinner is visible
+ padding: 10px;
+}
+
+.json-viewer {
+ font-family: monospace;
+ padding: 10px;
+ overflow-x: auto; // Handle very long lines if any
+
+ pre {
+ white-space: pre; // Use pre for basic formatting, but custom components will handle wrap/indent
+ margin: 0; // Remove default pre margin
+ }
+
+ .json-string {
+ color: green;
+ }
+
+ .json-number {
+ color: darkorange;
+ }
+
+ .json-boolean {
+ color: blue;
+ }
+
+ .json-null {
+ color: magenta;
+ }
+
+ .json-key {
+ color: #333; // Dark grey for keys
+ margin-right: 0.5em;
+ }
+
+ .json-bracket,
+ .json-brace {
+ cursor: pointer;
+ user-select: none; // Prevent text selection on brackets/braces
+ .bp4-icon { // Blueprint icon styling
+ vertical-align: middle;
+ margin-left: 4px;
+ color: #5c7080; // Blueprint icon color
+ }
+ }
+
+ .json-comma {
+ margin-right: 0.5em;
+ }
+
+ // Indentation based on level
+ // Create padding classes for levels 0 through (say) 10
+ @for $i from 0 through 10 {
+ .json-level-#{$i} {
+ padding-left: calc(#{$i} * 20px); // 20px indent per level
+ }
+ }
+
+ .json-array-item, .json-key-value-pair {
+ display: block; // Each item/pair on a new line
+ position: relative; // For absolute positioning of commas if needed, though not used here
+ }
+
+ mark {
+ background-color: yellow;
+ color: black;
+ padding: 0.1em;
+ border-radius: 3px;
+ }
+}
\ No newline at end of file