Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ Thumbs.db
.env.*

# Logs
logs
*.log
npm-debug.log*

# TypeScript
*.tsbuildinfo

# Test
test-results/
test-results/
Original file line number Diff line number Diff line change
@@ -1,33 +1,39 @@
import React from 'react';
import ReactJson from 'react-json-view';
import { useDispatch, useSelector } from 'react-redux';
import find from 'lodash/find';
import ErrorBanner from 'ui/ErrorBanner';
import { updateResponseJsonPreviewMode } from 'providers/ReduxStore/slices/tabs';
import JsonTablePreview from './JsonTablePreview';
import ModeToggle, { type JsonPreviewMode } from './JsonTablePreview/ModeToggle';

interface JsonPreviewProps {
data?: unknown[];
displayedTheme?: boolean;
data?: unknown;
displayedTheme?: string;
}

const JsonPreview = ({
data,
displayedTheme
}: any) => {
const validateJsonData = (data: unknown): { data: unknown; error: string | null } => {
// If data is already an object or array, use it directly
if (typeof data === 'object' && data !== null) {
return { data, error: null };
}
const JsonPreview = ({ data, displayedTheme }: any) => {
const dispatch = useDispatch();
const tabs = useSelector((state: any) => state.tabs.tabs);
const activeTabUid = useSelector((state: any) => state.tabs.activeTabUid);
const focusedTab = find(tabs, (t: any) => t.uid === activeTabUid);

const mode: JsonPreviewMode = focusedTab?.responseJsonPreviewMode === 'table' ? 'table' : 'tree';

const handleModeChange = (next: JsonPreviewMode) => {
if (!focusedTab) return;
dispatch(updateResponseJsonPreviewMode({ uid: focusedTab.uid, mode: next }));
};

// If data is a string, try to parse it
if (typeof data === 'string') {
const validateJsonData = (input: unknown): { data: unknown; error: string | null } => {
if (typeof input === 'object' && input !== null) return { data: input, error: null };
if (typeof input === 'string') {
try {
const parsed = JSON.parse(data);
return { data: parsed, error: null };
return { data: JSON.parse(input), error: null };
} catch (e) {
return { data: null, error: `Invalid JSON format: ${(e as Error).message}` };
}
}

// For other types, return error
return { data: null, error: 'Invalid input. Expected a JSON object, array, or valid JSON string.' };
};

Expand All @@ -45,22 +51,36 @@ const JsonPreview = ({
return <ErrorBanner errors={[{ title: 'Cannot preview as JSON', message: 'Data cannot be rendered as a JSON tree. Expected a JSON object or array.' }]} />;
}

if (mode === 'table') {
return (
<JsonTablePreview
data={jsonData.data}
modeToggle={<ModeToggle mode="table" onChange={handleModeChange} />}
/>
);
}

return (
<ReactJson
src={jsonData.data}
theme={displayedTheme === 'light' ? 'rjv-default' : 'monokai'}
collapsed={1}
displayDataTypes={false}
displayObjectSize={true}
enableClipboard={true}
name={false}
style={{
backgroundColor: 'transparent',
fontSize: '12px',
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace',
padding: '16px'
}}
/>
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
<div style={{ position: 'absolute', top: 8, right: 8, zIndex: 2 }}>
<ModeToggle mode="tree" onChange={handleModeChange} />
</div>
<ReactJson
src={jsonData.data}
theme={displayedTheme === 'light' ? 'rjv-default' : 'monokai'}
collapsed={1}
displayDataTypes={false}
displayObjectSize={true}
enableClipboard={true}
name={false}
style={{
backgroundColor: 'transparent',
fontSize: '12px',
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace',
padding: '16px'
}}
/>
</div>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { useEffect, useMemo, useState } from 'react';
import debounce from 'lodash/debounce';

interface FilterInputProps {
onChange: (value: string) => void;
placeholder?: string;
resetToken?: number;
}

const FilterInput: React.FC<FilterInputProps> = ({ onChange, placeholder = 'Filter rows…', resetToken }) => {
const [value, setValue] = useState('');

const debounced = useMemo(
() => debounce((v: string) => onChange(v), 200),
[onChange]
);

useEffect(() => () => debounced.cancel(), [debounced]);

useEffect(() => {
if (resetToken === undefined) return;
setValue('');
debounced.cancel();
onChange('');
}, [resetToken]); // eslint-disable-line react-hooks/exhaustive-deps

return (
<div className="json-table-filter">
<input
type="text"
value={value}
placeholder={placeholder}
onChange={(e) => {
setValue(e.target.value);
debounced(e.target.value);
}}
aria-label="Filter table rows"
/>
</div>
);
};

export default FilterInput;
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';
import styled from 'styled-components';

export type JsonPreviewMode = 'tree' | 'table';

interface ModeToggleProps {
mode: JsonPreviewMode;
onChange: (mode: JsonPreviewMode) => void;
}

const ToggleGroup = styled.div`
display: inline-flex;
align-items: stretch;
border: 1px solid ${(props) => props.theme.border.border2};
border-radius: ${(props) => props.theme.border.radius.sm};
overflow: hidden;
background: ${(props) => props.theme.background.surface1};
font-size: 11px;
line-height: 1;
font-family: inherit;

button {
appearance: none;
background: transparent;
border: 0;
padding: 4px 12px;
cursor: pointer;
color: ${(props) => props.theme.colors.text.muted};
transition: background-color 120ms ease, color 120ms ease;
}

button + button {
border-left: 1px solid ${(props) => props.theme.border.border2};
}

button:hover {
color: ${(props) => props.theme.colors.text.white};
}

button.active {
background: ${(props) => props.theme.background.surface0};
color: ${(props) => props.theme.colors.text.yellow};
font-weight: 600;
}

button:focus {
outline: none;
}

button:focus-visible {
box-shadow: inset 0 0 0 2px ${(props) => props.theme.colors.text.yellow};
}
`;

const ModeToggle: React.FC<ModeToggleProps> = ({ mode, onChange }) => (
<ToggleGroup role="tablist" aria-label="JSON preview mode">
<button
type="button"
role="tab"
aria-selected={mode === 'tree'}
className={mode === 'tree' ? 'active' : ''}
onClick={() => onChange('tree')}
>
Tree
</button>
<button
type="button"
role="tab"
aria-selected={mode === 'table'}
className={mode === 'table' ? 'active' : ''}
onClick={() => onChange('table')}
>
Table
</button>
</ToggleGroup>
);

export default ModeToggle;
Loading