diff --git a/.storybook/bundle.ts b/.storybook/bundle.ts
index c7dd58daae..76649c5398 100644
--- a/.storybook/bundle.ts
+++ b/.storybook/bundle.ts
@@ -13,4 +13,4 @@ import FilterEditor from '../src/components/FilterEditor.vue';
import ListUniqueValues from '../src/components/ListUniqueValues.vue';
export { FilterEditor, ConditionsEditor, DataViewer, RenameStepForm, Pipeline, ResizablePanels, Step, ListUniqueValues };
-export { setupStore } from '../src/store';
+export { setupStore, registerModule, VQBnamespace } from '../src/store';
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae729fbc79..50c8cf270b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# Changelog
+# Unreleased
+
+### Changed
+
+- Add a `isUniqueValuesLoading` key to columns in the vuex state. This will allow us to display a spinner when loading column's unique values.
+
+### Added
+
+- New component `ListUniqueValues` which will be used to display column's unique values.
+
+### Fixed
+
+- Fix setup of store in Storybook
+
## [0.15.1] - 2020-04-06
### Changed
diff --git a/src/components/ListUniqueValues.vue b/src/components/ListUniqueValues.vue
index 45741d5fc0..6c33e1313f 100644
--- a/src/components/ListUniqueValues.vue
+++ b/src/components/ListUniqueValues.vue
@@ -6,21 +6,24 @@
Select all
Clear all
-
-
-
-
-
List maybe incomplete
-
@@ -31,6 +34,9 @@ import _union from 'lodash/union';
import { Component, Prop, Vue } from 'vue-property-decorator';
import { FilterConditionInclusion } from '@/lib/steps.ts';
+import { VQBModule } from '@/store';
+
+import CheckboxWidget from './stepforms/widgets/Checkbox.vue';
// This type will be imported from @/lib/dataset/helpers.ts
type ColumnValueStat = {
@@ -46,7 +52,6 @@ type ColumnValueStat = {
value: ['France', 'UK', 'Spain']
}
*/
-import CheckboxWidget from './stepforms/widgets/Checkbox.vue';
@Component({
name: 'list-unique-values',
components: { CheckboxWidget },
@@ -70,6 +75,8 @@ export default class ListUniqueValues extends Vue {
})
loaded!: boolean;
+ @VQBModule.Getter('isUniqueValuesLoading') isLoadingFunction!: (column: string) => boolean;
+
search = '';
isChecked(option: ColumnValueStat): boolean {
@@ -138,7 +145,7 @@ export default class ListUniqueValues extends Vue {
}
loadAllValues() {
- throw new Error('Not implemented');
+ this.$emit('loadAllValues');
}
}
@@ -202,6 +209,23 @@ export default class ListUniqueValues extends Vue {
.list-unique-values__load-all-values-button {
text-decoration: underline;
font-weight: 700;
+ cursor: pointer;
+ }
+}
+
+.list-unique-values__loader-spinner {
+ border-radius: 50%;
+ border: 4px solid #efefef;
+ border-top-color: $active-color;
+ width: 30px;
+ height: 30px;
+ animation: spin 1500ms ease-in-out infinite;
+ margin: 30px auto;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(1turn);
}
}
diff --git a/src/lib/dataset/index.ts b/src/lib/dataset/index.ts
index 93cb39d589..eb47bbf362 100644
--- a/src/lib/dataset/index.ts
+++ b/src/lib/dataset/index.ts
@@ -12,6 +12,11 @@ export type ColumnTypeMapping = {
export type DataSetColumn = {
name: string;
type?: DataSetColumnType;
+ /**
+ * Indicates if there's an ongoing operation to fetch all unique values for this column from the database.
+ * This value is updated by the `backendify` function.
+ */
+ isUniqueValuesLoading?: boolean;
};
/**
diff --git a/src/store/getters.ts b/src/store/getters.ts
index 9c4fc2b5bc..ed0ab30f82 100644
--- a/src/store/getters.ts
+++ b/src/store/getters.ts
@@ -4,6 +4,8 @@
import _ from 'lodash';
import { GetterTree } from 'vuex';
+import { DataSetColumn } from '@/lib/dataset/index.ts';
+
import { activePipeline, currentPipeline, inactivePipeline, VQBState } from './state';
const getters: GetterTree
= {
@@ -82,6 +84,11 @@ const getters: GetterTree = {
*/
pageno: (state: VQBState) =>
state.dataset.paginationContext ? state.dataset.paginationContext.pageno : 1,
+ /**
+ * helper that is True if unique values are loading
+ */
+ isUniqueValuesLoading: (state: VQBState) => (column: string) =>
+ (state.dataset.headers.find(hdr => hdr.name === column) as DataSetColumn).isUniqueValuesLoading,
/**
* Return current edited pipeline
*/
diff --git a/src/store/mutations.ts b/src/store/mutations.ts
index f1b320cbc2..48eaf1abc4 100644
--- a/src/store/mutations.ts
+++ b/src/store/mutations.ts
@@ -6,6 +6,7 @@ import Vue from 'vue';
import { MutationTree } from 'vuex';
import { BackendError } from '@/lib/backend-response';
+import { DataSetColumn } from '@/lib/dataset/index.ts';
import { DomainStep, Pipeline, PipelineStepName } from '@/lib/steps';
import { currentPipeline, VQBState } from './state';
@@ -284,6 +285,18 @@ class Mutations {
state.isLoading = isLoading;
}
+ /**
+ * Set unique values loading
+ */
+ setUniqueValuesLoading(
+ state: VQBState,
+ { isLoading, column }: { isLoading: boolean; column: string },
+ ) {
+ (state.dataset.headers.find(
+ hdr => hdr.name === column,
+ ) as DataSetColumn).isUniqueValuesLoading = isLoading;
+ }
+
/**
* Update translator.
*/
diff --git a/stories/data-viewer.js b/stories/data-viewer.js
index 0bad28c102..7fd343a06e 100644
--- a/stories/data-viewer.js
+++ b/stories/data-viewer.js
@@ -1,10 +1,29 @@
import { storiesOf } from '@storybook/vue';
+import Vuex from 'vuex';
+import Vue from 'vue';
+
+import { DataViewer, registerModule } from '../dist/storybook/components';
-import { DataViewer, setupStore } from '../dist/storybook/components';
const stories = storiesOf('DataViewer', module);
+Vue.use(Vuex)
stories.add('empty', () => ({
- store: setupStore({}, [], true),
+ store: new Vuex.Store({}),
+ created: function() {
+ registerModule(this.$store, {
+ dataset: {
+ headers:[],
+ data: [],
+ paginationContext: {
+ pagesize: 50,
+ pageno: 1,
+ totalCount: 0,
+ },
+ },
+ currentPipelineName: "test",
+ pipelines: {test: []}
+ })
+ },
components: { DataViewer },
template: `
@@ -13,28 +32,36 @@ stories.add('empty', () => ({
}));
stories.add('simple', () => ({
- store: setupStore({
- dataset: {
- headers:
- [
- { name: 'columnA' },
- { name: 'columnB' },
- { name: 'columnC' },
+ store: new Vuex.Store({}),
+ created: function() {
+ registerModule(this.$store, {
+ dataset: {
+ headers:
+ [
+ { name: 'columnA', type: 'string' },
+ { name: 'columnB', type: 'date' },
+ { name: 'columnC', type: 'integer' },
+ ],
+ data: [
+ ['value1', 'value2', 'value3'],
+ ['value4', 'value5', 'value6'],
+ ['value7', 'value8', 'value9'],
+ ['value10', 'value11', 'value12'],
+ ['value10', { obj: 'value14' }, null],
],
- data: [
- ['value1', 'value2', 'value3'],
- ['value4', 'value5', 'value6'],
- ['value7', 'value8', 'value9'],
- ['value10', 'value11', 'value12'],
- ['value10', { obj: 'value14' }, null],
- ]
- },
- }, [], true),
+ paginationContext: {
+ pagesize: 50,
+ pageno: 1,
+ totalCount: 0,
+ },
+ },
+ currentPipelineName: "test",
+ pipelines: {test: []}
+ });
+ },
components: { DataViewer },
template: `
-
+
`,
}));
diff --git a/stories/form-rename-step.js b/stories/form-rename-step.js
deleted file mode 100644
index 4a9e25bcfa..0000000000
--- a/stories/form-rename-step.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import {
- storiesOf
-} from '@storybook/vue';
-
-import {
- RenameStepForm,
- setupStore
-} from '../dist/storybook/components';
-
-const stories = storiesOf('RenameStepForm', module);
-
-stories.add('simple', () => ({
- components: {
- RenameStepForm
- },
- store: setupStore({
- dataset: {
- headers: [{
- name: 'columnA'
- }, {
- name: 'columnB'
- }, {
- name: 'columnC'
- }],
- data: [
- ['value1', 'value2', 'value3'],
- ['value4', 'value5', 'value6'],
- ['value7', 'value8', 'value9'],
- ['value10', 'value11', 'value12'],
- ['value13', 'value14', 'value15'],
- ],
- },
- }, [], true),
- template: `
-
-
- `,
-}));
diff --git a/stories/index.js b/stories/index.js
index 82d8946c2e..42267f1341 100644
--- a/stories/index.js
+++ b/stories/index.js
@@ -63,7 +63,6 @@ stories
}));
import './data-viewer';
-import './form-rename-step';
import './resizable-panel';
import './filter-editor';
import './list-unique-values';
diff --git a/stories/list-unique-values.js b/stories/list-unique-values.js
index 1d135daf9b..fee7520dd8 100644
--- a/stories/list-unique-values.js
+++ b/stories/list-unique-values.js
@@ -1,22 +1,55 @@
import { storiesOf } from '@storybook/vue';
+import Vuex from 'vuex';
+import Vue from 'vue';
-import { ListUniqueValues } from '../dist/storybook/components';
+import { ListUniqueValues, registerModule, VQBnamespace } from '../dist/storybook/components';
const stories = storiesOf('ListUniqueValues', module);
+Vue.use(Vuex)
stories.add('default', () => ({
+ store: new Vuex.Store({}),
+ created: function() {
+ registerModule(this.$store, {
+ dataset: {
+ headers: [{name: 'col1', isUniqueValuesLoading: false}],
+ data: [],
+ }
+ });
+ },
components: { ListUniqueValues },
+ methods: {
+ loadAllValues(){
+ this.$store.commit(VQBnamespace('setUniqueValuesLoading'), { isLoading: true, column: 'col1' });
+ // simulate the call to get all unique values of dataset:
+ setTimeout(() => {
+ this.options = [
+ {value: 'Germany', count: 120},
+ {value: 'US', count: 34},
+ {value: 'Italy', count: 33},
+ {value: 'China', count: 24},
+ {value: 'France', count: 12},
+ {value: 'Framboise', count: 10},
+ {value: 'UK', count: 1},
+ ];
+ this.loaded = false;
+ this.$store.commit(VQBnamespace('setUniqueValuesLoading'), { isLoading: false, column: 'col1' });
+ }, 3000); // the call take 3sec
+ }
+ },
data(){
return {
options: [
- {value: 'France', count: 12},
- {value: 'UK', count: 1},
+ {value: 'Italy', count: 23},
{value: 'China', count: 14},
+ {value: 'France', count: 12},
{value: 'Framboise', count: 10},
- {value: 'Italy', count: 23},
+ {value: 'UK', count: 1},
],
filter: { column: "col1", operator: "in", value: []},
- loaded: false,
+ loaded: true,
}
},
- template: `
`,
+ template: `
+
+
`,
}));
diff --git a/tests/unit/list-unique-values.spec.ts b/tests/unit/list-unique-values.spec.ts
index 6b369ead0c..46a01c0069 100644
--- a/tests/unit/list-unique-values.spec.ts
+++ b/tests/unit/list-unique-values.spec.ts
@@ -1,31 +1,60 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { createLocalVue, shallowMount, Wrapper } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import ListUniqueValues from '@/components/ListUniqueValues.vue';
-import { FilterConditionInclusion } from '@/lib/steps.ts';
+
+import { setupMockStore } from './utils';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('List Unique Value', () => {
- const DUMMY_UNIQUE_VALUES = [
- { value: 'France', count: 10 },
- { value: 'Framboise', count: 9 },
- { value: 'UK', count: 4 },
- { value: 'Spain', count: 2 },
- ];
- let filter!: FilterConditionInclusion;
- let wrapper: any;
-
+ let wrapper: Wrapper;
+ /**
+ `shallowMountWrapper` is utility function for the `ListUniqueValues` component
+ It shallow mount the component with several options:
+ @param filterValue the value key of the prop `filter`
+ @param operator the operator key of the prop `filter`
+ @param loaded the `loaded` props of the component
+ @param isUniqueValuesLoading the store parameter indicating if column unique value is loading
+
+ By default the wrapper is mounted with 4 options: "France", "Framboise", "Spain" and "UK"
+ - The checked values are "France" and "Spain"
+ - All uniques are loaded
+ */
+ const shallowMountWrapper = (
+ filterValue: string[] = ['France', 'Spain'],
+ operator: 'in' | 'nin' = 'in',
+ loaded = true,
+ isUniqueValuesLoading = false,
+ ): Wrapper => {
+ return shallowMount(ListUniqueValues, {
+ store: setupMockStore({
+ dataset: {
+ headers: [{ name: 'col1', isUniqueValuesLoading }],
+ data: [],
+ },
+ }),
+ localVue,
+ propsData: {
+ filter: {
+ column: 'col1',
+ operator,
+ value: filterValue,
+ },
+ options: [
+ { value: 'France', count: 10 },
+ { value: 'Framboise', count: 9 },
+ { value: 'UK', count: 4 },
+ { value: 'Spain', count: 2 },
+ ],
+ loaded,
+ },
+ });
+ };
beforeEach(() => {
- filter = {
- column: 'col1',
- operator: 'in',
- value: ['France', 'Spain'],
- };
- wrapper = shallowMount(ListUniqueValues, {
- propsData: { filter, options: DUMMY_UNIQUE_VALUES, loaded: true },
- });
+ wrapper = shallowMountWrapper();
});
it('should display the list of unique values and how much time they are present in the whole dataset', () => {
@@ -47,14 +76,7 @@ describe('List Unique Value', () => {
});
it('should display the list of unique values and how much time they are present in the whole dataset (with "nin" operator)', () => {
- filter = {
- column: 'col1',
- operator: 'nin',
- value: ['Framboise', 'UK'],
- };
- wrapper = shallowMount(ListUniqueValues, {
- propsData: { filter, options: DUMMY_UNIQUE_VALUES, loaded: true },
- });
+ wrapper = shallowMountWrapper(['France', 'Spain'], 'nin');
expect(wrapper.exists()).toBeTruthy();
const CheckboxWidgetArray = wrapper.findAll('CheckboxWidget-stub');
expect(CheckboxWidgetArray.length).toEqual(4);
@@ -65,14 +87,7 @@ describe('List Unique Value', () => {
});
it('should instantiate with correct value checked (with "nin" operator)', () => {
- filter = {
- column: 'col1',
- operator: 'nin',
- value: ['Framboise', 'UK'],
- };
- wrapper = shallowMount(ListUniqueValues, {
- propsData: { filter, options: DUMMY_UNIQUE_VALUES, loaded: true },
- });
+ wrapper = shallowMountWrapper(['Framboise', 'UK'], 'nin');
const CheckboxWidgetArray = wrapper.findAll('CheckboxWidget-stub');
expect(CheckboxWidgetArray.at(0).vm.$props.value).toBeTruthy();
expect(CheckboxWidgetArray.at(1).vm.$props.value).toBeFalsy();
@@ -80,23 +95,27 @@ describe('List Unique Value', () => {
expect(CheckboxWidgetArray.at(3).vm.$props.value).toBeTruthy();
});
+ it('should instantiate without the spinner', async () => {
+ expect(wrapper.find('.list-unique-values__loader-spinner').exists()).toBeFalsy();
+ });
+
+ it('should instantiate with the spinner', async () => {
+ wrapper = shallowMountWrapper(['France', 'Spain'], 'in', true, true);
+ expect(wrapper.find('.list-unique-values__loader-spinner').exists()).toBeTruthy();
+ });
+
describe('"load all values" message', () => {
it('should instantiate with the "load all values" message', () => {
expect(wrapper.find('.list-unique-values__load-all-values').exists()).toBeTruthy();
});
- it('should throw an error when click on "load all values"', async () => {
- try {
- await wrapper.find('.list-unique-values__load-all-values-button').trigger('click');
- } catch (e) {
- expect(e).toEqual(Error('Not implemented'));
- }
+ it('should emit "loadAllValues" when click on "load all values"', async () => {
+ await wrapper.find('.list-unique-values__load-all-values-button').trigger('click');
+ expect(wrapper.emitted().loadAllValues.length).toBe(1);
});
it('should not instantiate with the "load all values" message', () => {
- wrapper = shallowMount(ListUniqueValues, {
- propsData: { filter, options: DUMMY_UNIQUE_VALUES, loaded: false },
- });
+ wrapper = shallowMountWrapper(['France', 'Spain'], 'in', false);
expect(wrapper.find('.list-unique-values__load-all-values').exists()).toBeFalsy();
});
});
@@ -186,14 +205,7 @@ describe('List Unique Value', () => {
describe('search box with "nin" operator', () => {
beforeEach(async () => {
- filter = {
- column: 'col1',
- operator: 'nin',
- value: ['France', 'Spain'],
- };
- wrapper = shallowMount(ListUniqueValues, {
- propsData: { filter, options: DUMMY_UNIQUE_VALUES, loaded: true },
- });
+ wrapper = shallowMountWrapper(['France', 'Spain'], 'nin');
const input = wrapper.find('.list-unique-values__search-box').element as HTMLInputElement;
input.value = 'Fr'; // "Fr" like the start of "France" and "Framboise"
await wrapper.find('.list-unique-values__search-box').trigger('input');
diff --git a/tests/unit/store.spec.ts b/tests/unit/store.spec.ts
index 839bf4226e..6dc99ae39c 100644
--- a/tests/unit/store.spec.ts
+++ b/tests/unit/store.spec.ts
@@ -124,6 +124,24 @@ describe('getter tests', () => {
});
});
+ describe('column isUniqueValuesLoading tests', () => {
+ it('should return a boolean indicating if column unique values are loading types', () => {
+ const state = buildState({
+ dataset: {
+ headers: [
+ { name: 'col1', isUniqueValuesLoading: true },
+ { name: 'col2', isUniqueValuesLoading: false },
+ { name: 'col3' },
+ ],
+ data: [],
+ },
+ });
+ expect(getters.isUniqueValuesLoading(state, {}, {}, {})('col1')).toEqual(true);
+ expect(getters.isUniqueValuesLoading(state, {}, {}, {})('col2')).toEqual(false);
+ expect(getters.isUniqueValuesLoading(state, {}, {}, {})('col3')).toBeUndefined();
+ });
+ });
+
describe('domain extraction tests', () => {
it('should not return anything if no pipeline is selected', function() {
const state = buildState({});
@@ -538,4 +556,24 @@ describe('mutation tests', () => {
});
expect(state.translator).toEqual('mongo40');
});
+
+ it('set isUniqueValuesLoading to true', () => {
+ const state = buildState({
+ dataset: {
+ headers: [{ name: 'col1', isUniqueValuesLoading: false }, { name: 'col2' }],
+ data: [],
+ },
+ });
+ mutations.setUniqueValuesLoading(state, {
+ isLoading: true,
+ column: 'col1',
+ });
+ expect(state.dataset.headers[0].isUniqueValuesLoading).toEqual(true);
+
+ mutations.setUniqueValuesLoading(state, {
+ isLoading: true,
+ column: 'col2',
+ });
+ expect(state.dataset.headers[1].isUniqueValuesLoading).toEqual(true);
+ });
});