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
97 changes: 97 additions & 0 deletions FILE_PREVIEW_IMPLEMENTATION.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this file doing here?

Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# File Preview Feature Implementation

## Summary

This implementation addresses GitHub issue #1067 by adding better handling for non-renderable content in the web monitoring UI. Instead of trying to display raw content that may cause download prompts or blank spaces, the system now shows informative file previews for content that cannot be rendered inline.

## Changes Made

### New Components

1. **`FilePreview` Component** (`src/components/file-preview.jsx`)
- Displays file information including name, media type, size, hash, and capture time
- Provides "View Raw File" and "Download File" buttons
- Shows clear messaging about why the file cannot be rendered inline

2. **`SideBySideFilePreview` Component** (`src/components/side-by-side-file-preview.jsx`)
- Displays two file previews side-by-side for version comparisons
- Consistent with existing side-by-side layout patterns

3. **File Preview Styles** (`src/components/file-preview.css`)
- Clean, responsive styling for file preview components
- Warning messages and action buttons properly styled
- Mobile-friendly responsive design

### Enhanced Diff Types

4. **New Diff Types** (in `src/constants/diff-types.js`)
- `FILE_PREVIEW`: Single file preview
- `SIDE_BY_SIDE_FILE_PREVIEW`: Side-by-side file preview
- Updated `diffTypesFor()` function to automatically detect non-renderable content

5. **Smart Content Detection**
- Added `isNonRenderableType()` function to identify content that should use file preview
- Maintains existing behavior for renderable content (HTML, text, images, PDFs, etc.)
- Defaults to file preview for Office documents, archives, and other binary formats

### Integration Updates

6. **`DiffView` Component Updates** (`src/components/diff-view.jsx`)
- Added support for rendering new file preview diff types
- Imports and handles new components

7. **`ChangeView` Component Updates** (`src/components/change-view/change-view.jsx`)
- Smart default diff type selection based on content type
- Non-renderable content defaults to `SIDE_BY_SIDE_FILE_PREVIEW`
- Renderable content maintains existing defaults

## File Types Handled

### Non-Renderable (uses File Preview):
- Microsoft Office documents (.xlsx, .docx, .ppt, etc.)
- Archive files (.zip, .tar.gz, etc.)
- Binary executables and data files
- Specialized formats without browser support

### Renderable (uses existing views):
- HTML content
- Plain text and code files
- PDFs (browser-native support)
- Images (PNG, JPEG, GIF, SVG, etc.)
- Audio/video files
- JSON and XML

## User Experience

- When viewing non-renderable content, users see a clean information card instead of download prompts
- Clear file metadata including type, size, and hash
- Easy access to raw file content via "View Raw File" button
- Download option with proper filename
- Consistent side-by-side layout for comparisons
- Informative messaging explaining why inline rendering isn't available

## Screenshot

![File Preview Example](screenshots/file-preview-example.png)
*New file preview interface showing non-renderable content with metadata instead of triggering browser downloads*

## Testing

- Added comprehensive test coverage for new components
- Tests verify proper file information display
- Tests confirm appropriate diff type selection based on content type
- All existing tests continue to pass

## Backward Compatibility

- Existing functionality for renderable content is unchanged
- Raw view options remain available as fallback
- No breaking changes to existing APIs or user workflows

## Future Enhancements

This foundation could be extended to support:
- Specialized viewers for specific file types
- File size information from server metadata
- Thumbnail generation for supported formats
- More detailed file analysis and comparison
Binary file added screenshots/file-preview-example.png
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this file here for? It is not used. (It also doesn’t actually look like what File Preview component renders.)

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 73 additions & 0 deletions src/components/__tests__/file-preview.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* eslint-env jest */

/**
* @jest-environment jsdom
*/

import { render } from '@testing-library/react';
import FilePreview from '../file-preview';

describe('FilePreview', () => {
const mockPage = {
url: 'https://example.com/test.xlsx',
uuid: 'mock-page-uuid'
};

const mockVersion = {
uuid: 'mock-version-uuid',
media_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
body_url: 'https://example.com/versions/test.xlsx',
body_hash: 'abc123def456',
capture_time: '2023-01-01T12:00:00Z'
};

it('renders file information correctly', () => {
const { getByText } = render(
<FilePreview page={mockPage} version={mockVersion} />
);

expect(getByText('File Information')).toBeInTheDocument();
expect(getByText('test.xlsx')).toBeInTheDocument();
expect(getByText('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')).toBeInTheDocument();
expect(getByText('abc123def456')).toBeInTheDocument();
});

it('renders download and view buttons', () => {
const { getByText } = render(
<FilePreview page={mockPage} version={mockVersion} />
);

const viewButton = getByText('View Raw File');
const downloadButton = getByText('Download File');

expect(viewButton).toBeInTheDocument();
expect(viewButton.getAttribute('href')).toBe('https://example.com/versions/test.xlsx');
expect(downloadButton).toBeInTheDocument();
expect(downloadButton.getAttribute('href')).toBe('https://example.com/versions/test.xlsx');
});

it('shows non-renderable content warning', () => {
const { getByText } = render(
<FilePreview page={mockPage} version={mockVersion} />
);

expect(getByText(/This file type cannot be rendered inline/)).toBeInTheDocument();
});

it('handles missing filename gracefully', () => {
const versionWithoutFilename = {
...mockVersion,
body_url: 'https://example.com/versions/'
};
const pageWithoutFilename = {
...mockPage,
url: 'https://example.com/'
};

const { getByText } = render(
<FilePreview page={pageWithoutFilename} version={versionWithoutFilename} />
);

expect(getByText('Unknown')).toBeInTheDocument();
});
});
58 changes: 58 additions & 0 deletions src/components/__tests__/side-by-side-file-preview.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/* eslint-env jest */

/**
* @jest-environment jsdom
*/

import { render } from '@testing-library/react';
import SideBySideFilePreview from '../side-by-side-file-preview';

// Mock the FilePreview component since we test it separately
jest.mock('../file-preview', () => {
return function MockFilePreview ({ page, version, content }) {
return <div data-testid="file-preview">{version.uuid}</div>;
};
});

describe('SideBySideFilePreview', () => {
const mockPage = {
url: 'https://example.com/test.xlsx',
uuid: 'mock-page-uuid'
};

const mockVersionA = {
uuid: 'version-a-uuid',
media_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
body_url: 'https://example.com/versions/test-a.xlsx',
};

const mockVersionB = {
uuid: 'version-b-uuid',
media_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
body_url: 'https://example.com/versions/test-b.xlsx',
};

const mockDiffData = {
rawA: 'raw content A',
rawB: 'raw content B'
};

it('renders both versions side by side', () => {
const { getByText, getAllByTestId } = render(
<SideBySideFilePreview
page={mockPage}
a={mockVersionA}
b={mockVersionB}
diffData={mockDiffData}
/>
);

expect(getByText('From Version')).toBeInTheDocument();
expect(getByText('To Version')).toBeInTheDocument();

const filePreviews = getAllByTestId('file-preview');
expect(filePreviews).toHaveLength(2);
expect(filePreviews[0]).toHaveTextContent('version-a-uuid');
expect(filePreviews[1]).toHaveTextContent('version-b-uuid');
});
});
10 changes: 9 additions & 1 deletion src/components/change-view/change-view.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import baseStyles from '../../css/base.css';
import viewStyles from './change-view.css';

export const defaultDiffType = 'SIDE_BY_SIDE_RENDERED';
export const defaultFilePreviewDiffType = 'SIDE_BY_SIDE_FILE_PREVIEW';
export const diffTypeStorage = 'edgi.wm.ui.diff_type';

const collapsedViewStorage = 'WebMonitoring.ChangeView.collapsedView';
Expand Down Expand Up @@ -431,7 +432,14 @@ function saveDiffSettings (settings) {

function ensureValidDiffType (from, to, page, stateDiffType = null) {
const relevantTypes = relevantDiffTypes(from, to, page);
const typesToTry = ([stateDiffType, loadDiffType(), defaultDiffType]).filter(t => t);

// Determine the appropriate default based on content type
const isNonRenderable = relevantTypes.some(type =>
type.value === 'FILE_PREVIEW' || type.value === 'SIDE_BY_SIDE_FILE_PREVIEW'
);
const appropriateDefault = isNonRenderable ? defaultFilePreviewDiffType : defaultDiffType;

const typesToTry = ([stateDiffType, loadDiffType(), appropriateDefault]).filter(t => t);
Comment on lines -434 to +442
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a little overcomplicated. The goal of this new “file preview” view is to have something to show if we don’t believe we have a diff that can render something appropriate, so I think it would make a little more sense to just replace defaultDiffType with it and (probably?) leave this entire section of code alone, rather than having some complicated logic around choosing different defaults here.


return typesToTry.find(diffType => isDiffTypeRelevant(relevantTypes, diffType))
|| relevantTypes[0].value;
Expand Down
10 changes: 10 additions & 0 deletions src/components/diff-view.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import SideBySideRenderedDiff from './side-by-side-rendered-diff';
import ChangesOnlyDiff from './changes-only-diff';
import RawVersion from './raw-version';
import SideBySideRawVersions from './side-by-side-raw-versions';
import FilePreview from './file-preview';
import SideBySideFilePreview from './side-by-side-file-preview';

import styles from '../css/base.css';

Expand Down Expand Up @@ -186,6 +188,14 @@ export default class DiffView extends Component {
return (
<ChangesOnlyDiff diffData={this.state.diffData} className='diff-source-inline' />
);
case diffTypes.FILE_PREVIEW.value:
return (
<FilePreview page={this.props.page} version={this.props.b} content={this.state.diffData.rawB} />
);
case diffTypes.SIDE_BY_SIDE_FILE_PREVIEW.value:
return (
<SideBySideFilePreview {...commonProps} />
);
Comment on lines +191 to +198
Copy link
Member

@Mr0grog Mr0grog Sep 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize I wrote this in the original issue:

We should probably have new FilePreview and SideBySideFilePreview views to use in cases where we don’t know we can actually render the file inline.

…but looking at this in practice, we probably should not have two different options here (so, my bad on not thinking this through originally). This should just be one diff type labeled “file preview” that shows the previews for both versions. Since the preview is a brief summary and doesn’t show any actual content or anything, it doesn’t really make sense to give people an option to only view one version’s preview.

(That said, it’s still probably nice to break this up into two React components! The component that is the diff view [SideBySideFilePreview] can/should still render the actual preview component twice inside it.)

default:
return null;
}
Expand Down
113 changes: 113 additions & 0 deletions src/components/file-preview.css
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few notes:

  1. The component’s CSS, JSX, and test files should be grouped together in a single directory, not directly in src/components/. See change-view for an example.
  2. This project uses CSS modules, so you should not be using BEM-style class names, since you do not need them for scoping or to avoid conflicts.

Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/* File Preview Styles */
.file-preview {
max-width: 100%;
margin: 1rem 0;
}

.file-preview__info {
padding: 1.5rem;
margin-bottom: 1rem;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}

.file-preview__title {
margin-top: 0;
margin-bottom: 1rem;
color: #333;
font-size: 1.25rem;
}

.file-preview__details {
margin-bottom: 1.5rem;
}

.file-preview__detail {
margin-bottom: 0.75rem;
line-height: 1.4;
}

.file-preview__detail strong {
display: inline-block;
min-width: 100px;
color: #555;
}

.file-preview__hash {
font-family: 'Courier New', Courier, monospace;
font-size: 0.85em;
background-color: #f4f4f4;
padding: 0.2em 0.4em;
border-radius: 3px;
word-break: break-all;
margin-left: 0.5rem;
}

.file-preview__actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}

.file-preview__download {
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
font-weight: 500;
transition: background-color 0.2s;
}

.file-preview__download:hover {
text-decoration: none;
}

.file-preview__warning {
background-color: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 4px;
padding: 1rem;
color: #856404;
}

.file-preview__warning p {
margin: 0;
line-height: 1.4;
}

/* Side-by-side file preview styles */
.side-by-side-file-preview {
display: flex;
gap: 1rem;
width: 100%;
}

.side-by-side-file-preview__version {
flex: 1;
min-width: 0; /* Allow flex items to shrink below their content size */
}

.side-by-side-file-preview__version-title {
margin-top: 0;
margin-bottom: 1rem;
padding: 0.5rem 0;
border-bottom: 2px solid #007cba;
color: #007cba;
font-size: 1.1rem;
font-weight: 600;
}
Comment on lines +78 to +98
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These belong to a separate component and so should be in a separate file dedicated to that component (also in a directory with that component’s JSX file, like this file should be).


/* Responsive design for smaller screens */
@media (max-width: 768px) {
.side-by-side-file-preview {
flex-direction: column;
}

.file-preview__actions {
flex-direction: column;
}

.file-preview__download {
text-align: center;
}
}
Loading
Loading