Commit 8e5ebe60 authored by Robert Speicher's avatar Robert Speicher

Merge branch '14174-csv-tables-blob' into 'master'

Feat(Blob): Add CSV render support for Blob Viewer

See merge request gitlab-org/gitlab!65050
parents 14b6e6c8 ccc0d8ae
<script>
import { GlAlert, GlLoadingIcon, GlTable } from '@gitlab/ui';
import Papa from 'papaparse';
export default {
components: {
GlTable,
GlAlert,
GlLoadingIcon,
},
props: {
csv: {
type: String,
required: true,
},
},
data() {
return {
items: [],
errorMessage: null,
loading: true,
};
},
mounted() {
const parsed = Papa.parse(this.csv, { skipEmptyLines: true });
this.items = parsed.data;
if (parsed.errors.length) {
this.errorMessage = parsed.errors.map((e) => e.message).join('. ');
}
this.loading = false;
},
};
</script>
<template>
<div class="container-fluid md gl-mt-3 gl-mb-3">
<div v-if="loading" class="gl-text-center loading">
<gl-loading-icon class="gl-mt-5" size="lg" />
</div>
<div v-else>
<gl-alert v-if="errorMessage" variant="danger" :dismissible="false">
{{ errorMessage }}
</gl-alert>
<gl-table
:empty-text="__('No CSV data to display.')"
:items="items"
:fields="$options.fields"
show-empty
thead-class="gl-display-none"
/>
</div>
</div>
</template>
import Vue from 'vue';
import CsvViewer from './csv_viewer.vue';
export default () => {
const el = document.getElementById('js-csv-viewer');
return new Vue({
el,
render(createElement) {
return createElement(CsvViewer, {
props: {
csv: el.dataset.data,
},
});
},
});
};
import renderCSV from './csv';
export default renderCSV;
......@@ -21,6 +21,8 @@ const loadRichBlobViewer = (type) => {
return import(/* webpackChunkName: 'notebook_viewer' */ '../notebook_viewer');
case 'openapi':
return import(/* webpackChunkName: 'openapi_viewer' */ '../openapi_viewer');
case 'csv':
return import(/* webpackChunkName: 'csv_viewer' */ '../csv_viewer');
case 'pdf':
return import(/* webpackChunkName: 'pdf_viewer' */ '../pdf_viewer');
case 'sketch':
......
......@@ -27,6 +27,7 @@ class Blob < SimpleDelegator
# type. LFS pointers to `.stl` files are assumed to always be the binary kind,
# and use the `BinarySTL` viewer.
RICH_VIEWERS = [
BlobViewer::CSV,
BlobViewer::Markup,
BlobViewer::Notebook,
BlobViewer::SVG,
......
# frozen_string_literal: true
module BlobViewer
class CSV < Base
include Rich
include ClientSide
self.binary = false
self.extensions = %w(csv)
self.partial_name = 'csv'
self.switcher_icon = 'table'
end
end
.file-content#js-csv-viewer{ data: { data: viewer.blob.data } }
......@@ -49,6 +49,7 @@ Rails.autoloaders.each do |autoloader|
'ldap_key' => 'LDAPKey',
'mr_note' => 'MRNote',
'pdf' => 'PDF',
'csv' => 'CSV',
'rsa_token' => 'RSAToken',
'san_extension' => 'SANExtension',
'sca' => 'SCA',
......
......@@ -22029,6 +22029,9 @@ msgstr ""
msgid "No %{providerTitle} repositories found"
msgstr ""
msgid "No CSV data to display."
msgstr ""
msgid "No Epic"
msgstr ""
......
import { GlAlert, GlLoadingIcon, GlTable } from '@gitlab/ui';
import { getAllByRole } from '@testing-library/dom';
import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import CSVViewer from '~/blob/csv/csv_viewer.vue';
const validCsv = 'one,two,three';
const brokenCsv = '{\n "json": 1,\n "key": [1, 2, 3]\n}';
describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => {
let wrapper;
const createComponent = ({ csv = validCsv, mountFunction = shallowMount } = {}) => {
wrapper = mountFunction(CSVViewer, {
propsData: {
csv,
},
});
};
const findCsvTable = () => wrapper.findComponent(GlTable);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAlert = () => wrapper.findComponent(GlAlert);
afterEach(() => {
wrapper.destroy();
});
it('should render loading spinner', () => {
createComponent();
expect(findLoadingIcon().props()).toMatchObject({
size: 'lg',
});
});
describe('when the CSV contains errors', () => {
it('should render alert', async () => {
createComponent({ csv: brokenCsv });
await nextTick;
expect(findAlert().props()).toMatchObject({
variant: 'danger',
});
});
});
describe('when the CSV contains no errors', () => {
it('should not render alert', async () => {
createComponent();
await nextTick;
expect(findAlert().exists()).toBe(false);
});
it('renders the CSV table with the correct attributes', async () => {
createComponent();
await nextTick;
expect(findCsvTable().attributes()).toMatchObject({
'empty-text': 'No CSV data to display.',
items: validCsv,
});
});
it('renders the CSV table with the correct content', async () => {
createComponent({ mountFunction: mount });
await nextTick;
expect(getAllByRole(wrapper.element, 'row', { name: /One/i })).toHaveLength(1);
expect(getAllByRole(wrapper.element, 'row', { name: /Two/i })).toHaveLength(1);
expect(getAllByRole(wrapper.element, 'row', { name: /Three/i })).toHaveLength(1);
});
});
});
......@@ -9154,6 +9154,11 @@ pako@~1.0.2, pako@~1.0.5:
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
integrity sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==
papaparse@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.3.1.tgz#770b7a9124d821d4b2132132b7bd7dce7194b5b1"
integrity sha512-Dbt2yjLJrCwH2sRqKFFJaN5XgIASO9YOFeFP8rIBRG2Ain8mqk5r1M6DkfvqEVozVcz3r3HaUGw253hA1nLIcA==
parallel-transform@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06"
......
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