diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a77c1a4178788151cd3508c2428053f5cb3852bc
--- /dev/null
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -0,0 +1,93 @@
+
+
+
+
+
diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..e0bbf12f3ebc346e38003db5394b7ea1194ef486
--- /dev/null
+++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql
@@ -0,0 +1,30 @@
+query getBlobInfo($projectPath: ID!, $filePath: String!) {
+ project(fullPath: $projectPath) {
+ id
+ repository {
+ blobs(path: $filePath) {
+ name
+ size
+ rawBlob
+ type
+ fileType
+ tooLarge
+ path
+ editBlobPath
+ ideEditPath
+ storedExternally
+ rawPath
+ externalStorageUrl
+ replacePath
+ deletePath
+ canLock
+ isLocked
+ lockLink
+ canModifyBlob
+ forkPath
+ simpleViewer
+ richViewer
+ }
+ }
+ }
+}
diff --git a/config/known_invalid_graphql_queries.yml b/config/known_invalid_graphql_queries.yml
index 2989b3a4262623bd44c59d9ed090c7bf48b1db8f..26188d6068f01928547bc26b8cf181192b1f032a 100644
--- a/config/known_invalid_graphql_queries.yml
+++ b/config/known_invalid_graphql_queries.yml
@@ -4,3 +4,4 @@ filenames:
- ee/app/assets/javascripts/security_configuration/api_fuzzing/graphql/api_fuzzing_ci_configuration.query.graphql
- ee/app/assets/javascripts/security_configuration/api_fuzzing/graphql/create_api_fuzzing_configuration.mutation.graphql
- ee/app/assets/javascripts/security_configuration/dast_profiles/graphql/dast_failed_site_validations.query.graphql
+ - app/assets/javascripts/repository/queries/blob_info.query.graphql
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6d474eae891618e6d588fd5689b2eba67510dea2..4db67aa1dc7f1d8ea5e4b29593d5e50ca826feb6 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3533,6 +3533,9 @@ msgstr ""
msgid "An error occurred while loading the file. Please try again later."
msgstr ""
+msgid "An error occurred while loading the file. Please try again."
+msgstr ""
+
msgid "An error occurred while loading the merge request changes."
msgstr ""
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..a0d00ff9a7d3a04abdb20a82c7892ab187a5ab1d
--- /dev/null
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -0,0 +1,85 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import BlobContent from '~/blob/components/blob_content.vue';
+import BlobHeader from '~/blob/components/blob_header.vue';
+import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
+
+let wrapper;
+const mockData = {
+ name: 'some_file.js',
+ size: 123,
+ rawBlob: 'raw content',
+ type: 'text',
+ fileType: 'text',
+ tooLarge: false,
+ path: 'some_file.js',
+ editBlobPath: 'some_file.js/edit',
+ ideEditPath: 'some_file.js/ide/edit',
+ storedExternally: false,
+ rawPath: 'some_file.js',
+ externalStorageUrl: 'some_file.js',
+ replacePath: 'some_file.js/replace',
+ deletePath: 'some_file.js/delete',
+ canLock: true,
+ isLocked: false,
+ lockLink: 'some_file.js/lock',
+ canModifyBlob: true,
+ forkPath: 'some_file.js/fork',
+ simpleViewer: {},
+ richViewer: {},
+};
+
+function factory(path, loading = false) {
+ wrapper = shallowMount(BlobContentViewer, {
+ propsData: {
+ path,
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ blobInfo: {
+ loading,
+ },
+ },
+ },
+ },
+ });
+
+ wrapper.setData({ blobInfo: mockData });
+}
+
+describe('Blob content viewer component', () => {
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findBlobHeader = () => wrapper.find(BlobHeader);
+ const findBlobContent = () => wrapper.find(BlobContent);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ beforeEach(() => {
+ factory('some_file.js');
+ });
+
+ it('renders a GlLoadingIcon component', () => {
+ factory('some_file.js', true);
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('renders a BlobHeader component', () => {
+ expect(findBlobHeader().exists()).toBe(true);
+ });
+
+ it('renders a BlobContent component', () => {
+ expect(findBlobContent().exists()).toBe(true);
+
+ expect(findBlobContent().props('loading')).toEqual(false);
+ expect(findBlobContent().props('content')).toEqual('raw content');
+ expect(findBlobContent().props('activeViewer')).toEqual({
+ fileType: 'text',
+ tooLarge: false,
+ type: 'text',
+ });
+ });
+});