Commit 33309d29 authored by Jacques's avatar Jacques

Ensure hljs is behind a FF + general cleanup

Load highlight.js behind a FF + cleanup the blob viewer
parent 1b2fd94b
......@@ -9,12 +9,14 @@ import axios from '~/lib/utils/axios_utils';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getRefMixin from '../mixins/get_ref';
import blobInfoQuery from '../queries/blob_info.query.graphql';
import { DEFAULT_BLOB_INFO, TEXT_FILE_TYPE, LFS_STORAGE } from '../constants';
import BlobButtonGroup from './blob_button_group.vue';
import BlobEdit from './blob_edit.vue';
import ForkSuggestion from './fork_suggestion.vue';
import { loadViewer, viewerProps } from './blob_viewers';
import { loadViewer } from './blob_viewers';
export default {
i18n: {
......@@ -29,7 +31,7 @@ export default {
GlButton,
ForkSuggestion,
},
mixins: [getRefMixin],
mixins: [getRefMixin, glFeatureFlagMixin()],
inject: {
originalBranch: {
default: '',
......@@ -78,52 +80,7 @@ export default {
isBinary: false,
isLoadingLegacyViewer: false,
activeViewerType: SIMPLE_BLOB_VIEWER,
project: {
userPermissions: {
pushCode: false,
downloadCode: false,
createMergeRequestIn: false,
forkProject: false,
},
pathLocks: {
nodes: [],
},
repository: {
empty: true,
blobs: {
nodes: [
{
name: '',
size: '',
rawTextBlob: '',
type: '',
fileType: '',
tooLarge: false,
path: '',
editBlobPath: '',
ideEditPath: '',
forkAndEditPath: '',
ideForkAndEditPath: '',
storedExternally: false,
externalStorage: '',
environmentFormattedExternalUrl: '',
environmentExternalUrlForRouteMap: '',
canModifyBlob: false,
canCurrentUserPushToBranch: false,
archived: false,
rawPath: '',
externalStorageUrl: '',
replacePath: '',
pipelineEditorPath: '',
deletePath: '',
simpleViewer: {},
richViewer: null,
webPath: '',
},
],
},
},
},
project: DEFAULT_BLOB_INFO,
};
},
computed: {
......@@ -134,7 +91,7 @@ export default {
return this.$apollo.queries.project.loading;
},
isBinaryFileType() {
return this.isBinary || this.blobInfo.simpleViewer?.fileType !== 'text';
return this.isBinary || this.blobInfo.simpleViewer?.fileType !== TEXT_FILE_TYPE;
},
blobInfo() {
const nodes = this.project?.repository?.blobs?.nodes || [];
......@@ -153,11 +110,16 @@ export default {
},
blobViewer() {
const { fileType } = this.viewer;
return loadViewer(fileType, this.isUsingLfs);
return this.shouldLoadLegacyViewer ? null : loadViewer(fileType, this.isUsingLfs);
},
viewerProps() {
const { fileType } = this.viewer;
return viewerProps(fileType, this.blobInfo);
shouldLoadLegacyViewer() {
return this.viewer.fileType === TEXT_FILE_TYPE && !this.glFeatures.highlightJs;
},
legacyViewerLoaded() {
return (
(this.activeViewerType === SIMPLE_BLOB_VIEWER && this.legacySimpleViewer) ||
(this.activeViewerType === RICH_BLOB_VIEWER && this.legacyRichViewer)
);
},
canLock() {
const { pushCode, downloadCode } = this.project.userPermissions;
......@@ -186,20 +148,22 @@ export default {
: this.blobInfo.forkAndEditPath;
},
isUsingLfs() {
return this.blobInfo.storedExternally && this.blobInfo.externalStorage === 'lfs';
return this.blobInfo.storedExternally && this.blobInfo.externalStorage === LFS_STORAGE;
},
},
methods: {
loadLegacyViewer(type) {
if (this.legacyViewerLoaded(type)) {
loadLegacyViewer() {
if (this.legacyViewerLoaded) {
return;
}
const type = this.activeViewerType;
this.isLoadingLegacyViewer = true;
axios
.get(`${this.blobInfo.webPath}?format=json&viewer=${type}`)
.then(({ data: { html, binary } }) => {
if (type === 'simple') {
if (type === SIMPLE_BLOB_VIEWER) {
this.legacySimpleViewer = html;
} else {
this.legacyRichViewer = html;
......@@ -210,12 +174,6 @@ export default {
})
.catch(() => this.displayError());
},
legacyViewerLoaded(type) {
return (
(type === SIMPLE_BLOB_VIEWER && this.legacySimpleViewer) ||
(type === RICH_BLOB_VIEWER && this.legacyRichViewer)
);
},
displayError() {
createFlash({ message: __('An error occurred while loading the file. Please try again.') });
},
......@@ -223,7 +181,7 @@ export default {
this.activeViewerType = newViewer || SIMPLE_BLOB_VIEWER;
if (!this.blobViewer) {
this.loadLegacyViewer(this.activeViewerType);
this.loadLegacyViewer();
}
},
editBlob(target) {
......@@ -309,7 +267,7 @@ export default {
:hide-line-numbers="true"
:loading="isLoadingLegacyViewer"
/>
<component :is="blobViewer" v-else v-bind="viewerProps" class="blob-viewer" />
<component :is="blobViewer" v-else :blob="blobInfo" class="blob-viewer" />
</div>
</div>
</template>
......@@ -9,19 +9,17 @@ export default {
GlLink,
},
props: {
fileName: {
type: String,
blob: {
type: Object,
required: true,
},
filePath: {
type: String,
required: true,
},
fileSize: {
type: Number,
required: false,
default: 0,
},
},
data() {
return {
fileName: this.blob.name,
filePath: this.blob.rawPath,
fileSize: this.blob.rawSize || 0,
};
},
computed: {
downloadFileSize() {
......
<script>
export default {
props: {
url: {
type: String,
required: true,
},
alt: {
type: String,
blob: {
type: Object,
required: true,
},
},
data() {
return {
url: this.blob.rawPath,
alt: this.blob.name,
};
},
};
</script>
<template>
......
......@@ -17,34 +17,3 @@ export const loadViewer = (type, isUsingLfs) => {
return viewer;
};
export const viewerProps = (type, blob) => {
const props = {
text: {
content: blob.rawTextBlob,
autoDetect: true, // We'll eventually disable autoDetect and pass the language explicitly to reduce the footprint (https://gitlab.com/gitlab-org/gitlab/-/issues/348145)
},
download: {
fileName: blob.name,
filePath: blob.rawPath,
fileSize: blob.rawSize,
},
image: {
url: blob.rawPath,
alt: blob.name,
},
video: {
url: blob.rawPath,
},
pdf: {
url: blob.rawPath,
fileSize: blob.rawSize,
},
lfs: {
fileName: blob.name,
filePath: blob.rawPath,
},
};
return props[type] || props[blob.externalStorage];
};
......@@ -13,15 +13,17 @@ export default {
GlSprintf,
},
props: {
fileName: {
type: String,
required: true,
},
filePath: {
type: String,
blob: {
type: Object,
required: true,
},
},
data() {
return {
fileName: this.blob.name,
filePath: this.blob.rawPath,
};
},
};
</script>
......
......@@ -11,17 +11,17 @@ export default {
tooLargeButtonText: __('Download PDF'),
},
props: {
url: {
type: String,
required: true,
},
fileSize: {
type: Number,
blob: {
type: Object,
required: true,
},
},
data() {
return { totalPages: 0 };
return {
url: this.blob.rawPath,
fileSize: this.blob.rawSize,
totalPages: 0,
};
},
computed: {
tooLargeToDisplay() {
......
<script>
export default {
props: {
url: {
type: String,
blob: {
type: Object,
required: true,
},
},
data() {
return {
url: this.blob.rawPath,
};
},
};
</script>
<template>
......
......@@ -25,3 +25,54 @@ export const PDF_MAX_FILE_SIZE = 10000000; // 10 MB
export const PDF_MAX_PAGE_LIMIT = 50;
export const ROW_APPEAR_DELAY = 150;
export const DEFAULT_BLOB_INFO = {
userPermissions: {
pushCode: false,
downloadCode: false,
createMergeRequestIn: false,
forkProject: false,
},
pathLocks: {
nodes: [],
},
repository: {
empty: true,
blobs: {
nodes: [
{
name: '',
size: '',
rawTextBlob: '',
type: '',
fileType: '',
tooLarge: false,
path: '',
editBlobPath: '',
ideEditPath: '',
forkAndEditPath: '',
ideForkAndEditPath: '',
storedExternally: false,
externalStorage: '',
environmentFormattedExternalUrl: '',
environmentExternalUrlForRouteMap: '',
canModifyBlob: false,
canCurrentUserPushToBranch: false,
archived: false,
rawPath: '',
externalStorageUrl: '',
replacePath: '',
pipelineEditorPath: '',
deletePath: '',
simpleViewer: {},
richViewer: null,
webPath: '',
},
],
},
},
};
export const TEXT_FILE_TYPE = 'text';
export const LFS_STORAGE = 'lfs';
......@@ -4,6 +4,7 @@ import LineNumbers from '~/vue_shared/components/line_numbers.vue';
import { sanitize } from '~/lib/dompurify';
const LINE_SELECT_CLASS_NAME = 'hll';
const PLAIN_TEXT_LANGUAGE = 'plaintext';
export default {
components: {
......@@ -13,24 +14,21 @@ export default {
SafeHtml: GlSafeHtmlDirective,
},
props: {
content: {
type: String,
blob: {
type: Object,
required: true,
},
language: {
type: String,
required: false,
default: 'plaintext',
},
autoDetect: {
type: Boolean,
required: false,
default: false,
default: true, // We'll eventually disable autoDetect and pass the language explicitly to reduce the footprint (https://gitlab.com/gitlab-org/gitlab/-/issues/348145)
},
},
data() {
return {
languageDefinition: null,
content: this.blob.rawTextBlob,
language: this.blob.language || PLAIN_TEXT_LANGUAGE,
hljs: null,
};
},
......
import Vue from 'vue';
import VueRouter from 'vue-router';
import VueApollo from 'vue-apollo';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
......@@ -19,6 +21,7 @@ import {
jest.mock('~/lib/utils/common_utils');
Vue.use(VueRouter);
const router = new VueRouter();
const mockAxios = new MockAdapter(axios);
let wrapper;
let mockResolver;
......@@ -79,6 +82,7 @@ describe('Blob content viewer component', () => {
afterEach(() => {
wrapper.destroy();
mockAxios.reset();
});
describe('BlobHeader action slot', () => {
......
......@@ -3,7 +3,6 @@ import { mount, shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
......@@ -13,7 +12,7 @@ import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import BlobEdit from '~/repository/components/blob_edit.vue';
import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
import { loadViewer, viewerProps } from '~/repository/components/blob_viewers';
import { loadViewer } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
import SourceViewer from '~/vue_shared/components/source_viewer.vue';
......@@ -51,6 +50,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount) => {
createMergeRequestIn = userPermissionsMock.createMergeRequestIn,
isBinary,
inject = {},
highlightJs = true,
} = mockData;
const project = {
......@@ -78,7 +78,12 @@ const createComponent = async (mockData = {}, mountFn = shallowMount) => {
apolloProvider: fakeApollo,
propsData: propsMock,
mixins: [{ data: () => ({ ref: refMock }) }],
provide: { ...inject },
provide: {
...inject,
glFeatures: {
highlightJs,
},
},
}),
);
......@@ -99,7 +104,6 @@ describe('Blob content viewer component', () => {
const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);
beforeEach(() => {
gon.features = { highlightJs: true };
isLoggedIn.mockReturnValue(true);
});
......@@ -137,6 +141,15 @@ describe('Blob content viewer component', () => {
});
describe('legacy viewers', () => {
it('loads a legacy viewer when a the fileType is text and the highlightJs feature is turned off', async () => {
await createComponent({
blob: { ...simpleViewerMock, fileType: 'text', highlightJs: false },
});
expect(mockAxios.history.get).toHaveLength(1);
expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=simple');
});
it('loads a legacy viewer when a viewer component is not available', async () => {
await createComponent({ blob: { ...simpleViewerMock, fileType: 'unknown' } });
......@@ -202,7 +215,6 @@ describe('Blob content viewer component', () => {
describe('Blob viewer', () => {
afterEach(() => {
loadViewer.mockRestore();
viewerProps.mockRestore();
});
it('does not render a BlobContent component if a Blob viewer is available', async () => {
......@@ -213,33 +225,29 @@ describe('Blob content viewer component', () => {
});
it.each`
viewer | loadViewerReturnValue | viewerPropsReturnValue
${'empty'} | ${EmptyViewer} | ${{}}
${'download'} | ${DownloadViewer} | ${{ filePath: '/some/file/path', fileName: 'test.js', fileSize: 100 }}
${'text'} | ${SourceViewer} | ${{ content: 'test', autoDetect: true }}
`(
'renders viewer component for $viewer files',
async ({ viewer, loadViewerReturnValue, viewerPropsReturnValue }) => {
loadViewer.mockReturnValue(loadViewerReturnValue);
viewerProps.mockReturnValue(viewerPropsReturnValue);
createComponent({
blob: {
...simpleViewerMock,
fileType: 'null',
simpleViewer: {
...simpleViewerMock.simpleViewer,
fileType: viewer,
},
viewer | loadViewerReturnValue
${'empty'} | ${EmptyViewer}
${'download'} | ${DownloadViewer}
${'text'} | ${SourceViewer}
`('renders viewer component for $viewer files', async ({ viewer, loadViewerReturnValue }) => {
loadViewer.mockReturnValue(loadViewerReturnValue);
createComponent({
blob: {
...simpleViewerMock,
fileType: 'null',
simpleViewer: {
...simpleViewerMock.simpleViewer,
fileType: viewer,
},
});
},
});
await waitForPromises();
await waitForPromises();
expect(loadViewer).toHaveBeenCalledWith(viewer, false);
expect(wrapper.findComponent(loadViewerReturnValue).exists()).toBe(true);
},
);
expect(loadViewer).toHaveBeenCalledWith(viewer, false);
expect(wrapper.findComponent(loadViewerReturnValue).exists()).toBe(true);
});
});
describe('BlobHeader action slot', () => {
......
......@@ -6,42 +6,33 @@ import DownloadViewer from '~/repository/components/blob_viewers/download_viewer
describe('Text Viewer', () => {
let wrapper;
const DEFAULT_PROPS = {
fileName: 'file_name.js',
filePath: '/some/file/path',
fileSize: 2269674,
const DEFAULT_BLOB_DATA = {
name: 'file_name.js',
rawPath: '/some/file/path',
rawSize: 2269674,
};
const createComponent = (props = {}) => {
const createComponent = (blobData = {}) => {
wrapper = shallowMount(DownloadViewer, {
propsData: {
...DEFAULT_PROPS,
...props,
blob: {
...DEFAULT_BLOB_DATA,
...blobData,
},
},
});
};
it('renders component', () => {
createComponent();
const { fileName, filePath, fileSize } = DEFAULT_PROPS;
expect(wrapper.props()).toMatchObject({
fileName,
filePath,
fileSize,
});
});
it('renders download human readable file size text', () => {
createComponent();
const downloadText = `Download (${numberToHumanSize(DEFAULT_PROPS.fileSize)})`;
const downloadText = `Download (${numberToHumanSize(DEFAULT_BLOB_DATA.rawSize)})`;
expect(wrapper.text()).toBe(downloadText);
});
it('renders download text', () => {
createComponent({
fileSize: 0,
rawSize: 0,
});
expect(wrapper.text()).toBe('Download');
......@@ -49,13 +40,13 @@ describe('Text Viewer', () => {
it('renders download link', () => {
createComponent();
const { filePath, fileName } = DEFAULT_PROPS;
const { rawPath, name } = DEFAULT_BLOB_DATA;
expect(wrapper.findComponent(GlLink).attributes()).toMatchObject({
rel: 'nofollow',
target: '_blank',
href: filePath,
download: fileName,
href: rawPath,
download: name,
});
});
......
......@@ -4,13 +4,13 @@ import ImageViewer from '~/repository/components/blob_viewers/image_viewer.vue';
describe('Image Viewer', () => {
let wrapper;
const propsData = {
url: 'some/image.png',
alt: 'image.png',
const DEFAULT_BLOB_DATA = {
rawPath: 'some/image.png',
name: 'image.png',
};
const createComponent = () => {
wrapper = shallowMount(ImageViewer, { propsData });
wrapper = shallowMount(ImageViewer, { propsData: { blob: DEFAULT_BLOB_DATA } });
};
const findImage = () => wrapper.find('[data-testid="image"]');
......@@ -19,7 +19,7 @@ describe('Image Viewer', () => {
createComponent();
expect(findImage().exists()).toBe(true);
expect(findImage().attributes('src')).toBe(propsData.url);
expect(findImage().attributes('alt')).toBe(propsData.alt);
expect(findImage().attributes('src')).toBe(DEFAULT_BLOB_DATA.rawPath);
expect(findImage().attributes('alt')).toBe(DEFAULT_BLOB_DATA.name);
});
});
......@@ -5,14 +5,14 @@ import LfsViewer from '~/repository/components/blob_viewers/lfs_viewer.vue';
describe('LFS Viewer', () => {
let wrapper;
const DEFAULT_PROPS = {
fileName: 'file_name.js',
filePath: '/some/file/path',
const DEFAULT_BLOB_DATA = {
name: 'file_name.js',
rawPath: '/some/file/path',
};
const createComponent = () => {
wrapper = shallowMount(LfsViewer, {
propsData: { ...DEFAULT_PROPS },
propsData: { blob: { ...DEFAULT_BLOB_DATA } },
stubs: { GlSprintf },
});
};
......@@ -30,12 +30,12 @@ describe('LFS Viewer', () => {
});
it('renders download link', () => {
const { filePath, fileName } = DEFAULT_PROPS;
const { rawPath, name } = DEFAULT_BLOB_DATA;
expect(findLink().attributes()).toMatchObject({
target: '_blank',
href: filePath,
download: fileName,
href: rawPath,
download: name,
});
});
});
......@@ -6,10 +6,12 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('PDF Viewer', () => {
let wrapper;
const defaultPropsData = { url: 'some/pdf_blob.pdf' };
const DEFAULT_BLOB_DATA = { rawPath: 'some/pdf_blob.pdf' };
const createComponent = (fileSize = 999) => {
wrapper = shallowMountExtended(Component, { propsData: { ...defaultPropsData, fileSize } });
const createComponent = (rawSize = 999) => {
wrapper = shallowMountExtended(Component, {
propsData: { blob: { ...DEFAULT_BLOB_DATA, rawSize } },
});
};
const findPDFViewer = () => wrapper.findComponent(PdfViewer);
......@@ -20,7 +22,7 @@ describe('PDF Viewer', () => {
createComponent();
expect(findPDFViewer().exists()).toBe(true);
expect(findPDFViewer().props('pdf')).toBe(defaultPropsData.url);
expect(findPDFViewer().props('pdf')).toBe(DEFAULT_BLOB_DATA.rawPath);
});
describe('Too large', () => {
......
......@@ -4,10 +4,10 @@ import VideoViewer from '~/repository/components/blob_viewers/video_viewer.vue';
describe('Video Viewer', () => {
let wrapper;
const propsData = { url: 'some/video.mp4' };
const DEFAULT_BLOB_DATA = { rawPath: 'some/video.mp4' };
const createComponent = () => {
wrapper = shallowMountExtended(VideoViewer, { propsData });
wrapper = shallowMountExtended(VideoViewer, { propsData: { blob: { ...DEFAULT_BLOB_DATA } } });
};
const findVideo = () => wrapper.findByTestId('video');
......@@ -16,7 +16,7 @@ describe('Video Viewer', () => {
createComponent();
expect(findVideo().exists()).toBe(true);
expect(findVideo().attributes('src')).toBe(propsData.url);
expect(findVideo().attributes('src')).toBe(DEFAULT_BLOB_DATA.rawPath);
expect(findVideo().attributes('controls')).not.toBeUndefined();
});
});
......@@ -12,17 +12,18 @@ const router = new VueRouter();
describe('Source Viewer component', () => {
let wrapper;
const language = 'javascript';
const content = `// Some source code`;
const DEFAULT_BLOB_DATA = { language, rawTextBlob: content };
const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
const language = 'javascript';
hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
const createComponent = async (props = {}) => {
const createComponent = async (props = { autoDetect: false }) => {
wrapper = shallowMountExtended(SourceViewer, {
router,
propsData: { content, language, ...props },
propsData: { blob: { ...DEFAULT_BLOB_DATA }, ...props },
});
await waitForPromises();
};
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment