Commit 3ed712f5 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '222483-use-backend-filtering-for-vulnerabilities-autocomplete' into 'master'

Use backend filtering to autocomplete vulnerabilities

See merge request gitlab-org/gitlab!42946
parents 540c453b d8ecae2d
...@@ -4,6 +4,7 @@ import { escape, template } from 'lodash'; ...@@ -4,6 +4,7 @@ import { escape, template } from 'lodash';
import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarMediator from '~/sidebar/sidebar_mediator';
import glRegexp from './lib/utils/regexp'; import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache'; import AjaxCache from './lib/utils/ajax_cache';
import axios from '~/lib/utils/axios_utils';
import { spriteIcon } from './lib/utils/common_utils'; import { spriteIcon } from './lib/utils/common_utils';
import * as Emoji from '~/emoji'; import * as Emoji from '~/emoji';
...@@ -61,6 +62,7 @@ class GfmAutoComplete { ...@@ -61,6 +62,7 @@ class GfmAutoComplete {
this.dataSources = dataSources; this.dataSources = dataSources;
this.cachedData = {}; this.cachedData = {};
this.isLoadingData = {}; this.isLoadingData = {};
this.previousQuery = '';
} }
setup(input, enableMap = defaultAutocompleteConfig) { setup(input, enableMap = defaultAutocompleteConfig) {
...@@ -526,7 +528,7 @@ class GfmAutoComplete { ...@@ -526,7 +528,7 @@ class GfmAutoComplete {
} }
getDefaultCallbacks() { getDefaultCallbacks() {
const fetchData = this.fetchData.bind(this); const self = this;
return { return {
sorter(query, items, searchKey) { sorter(query, items, searchKey) {
...@@ -539,7 +541,15 @@ class GfmAutoComplete { ...@@ -539,7 +541,15 @@ class GfmAutoComplete {
}, },
filter(query, data, searchKey) { filter(query, data, searchKey) {
if (GfmAutoComplete.isLoading(data)) { if (GfmAutoComplete.isLoading(data)) {
fetchData(this.$inputor, this.at); self.fetchData(this.$inputor, this.at);
return data;
}
if (
GfmAutoComplete.typesWithBackendFiltering.includes(GfmAutoComplete.atTypeMap[this.at]) &&
self.previousQuery !== query
) {
self.fetchData(this.$inputor, this.at, query);
self.previousQuery = query;
return data; return data;
} }
return $.fn.atwho.default.callbacks.filter(query, data, searchKey); return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
...@@ -587,13 +597,22 @@ class GfmAutoComplete { ...@@ -587,13 +597,22 @@ class GfmAutoComplete {
}; };
} }
fetchData($input, at) { fetchData($input, at, search) {
if (this.isLoadingData[at]) return; if (this.isLoadingData[at]) return;
this.isLoadingData[at] = true; this.isLoadingData[at] = true;
const dataSource = this.dataSources[GfmAutoComplete.atTypeMap[at]]; const dataSource = this.dataSources[GfmAutoComplete.atTypeMap[at]];
if (this.cachedData[at]) { if (GfmAutoComplete.typesWithBackendFiltering.includes(GfmAutoComplete.atTypeMap[at])) {
axios
.get(dataSource, { params: { search } })
.then(({ data }) => {
this.loadData($input, at, data);
})
.catch(() => {
this.isLoadingData[at] = false;
});
} else if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]); this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
Emoji.initEmojiMap() Emoji.initEmojiMap()
...@@ -687,6 +706,8 @@ GfmAutoComplete.atTypeMap = { ...@@ -687,6 +706,8 @@ GfmAutoComplete.atTypeMap = {
$: 'snippets', $: 'snippets',
}; };
GfmAutoComplete.typesWithBackendFiltering = ['vulnerabilities'];
// Emoji // Emoji
GfmAutoComplete.glEmojiTag = null; GfmAutoComplete.glEmojiTag = null;
GfmAutoComplete.Emoji = { GfmAutoComplete.Emoji = {
......
...@@ -7,15 +7,228 @@ import GfmAutoComplete, { membersBeforeSave } from 'ee_else_ce/gfm_auto_complete ...@@ -7,15 +7,228 @@ import GfmAutoComplete, { membersBeforeSave } from 'ee_else_ce/gfm_auto_complete
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { getJSONFixture } from 'helpers/fixtures'; import { getJSONFixture } from 'helpers/fixtures';
import waitForPromises from 'jest/helpers/wait_for_promises';
import MockAdapter from 'axios-mock-adapter';
import AjaxCache from '~/lib/utils/ajax_cache';
import axios from '~/lib/utils/axios_utils';
const labelsFixture = getJSONFixture('autocomplete_sources/labels.json'); const labelsFixture = getJSONFixture('autocomplete_sources/labels.json');
describe('GfmAutoComplete', () => { describe('GfmAutoComplete', () => {
const gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({ const fetchDataMock = { fetchData: jest.fn() };
fetchData: () => {}, let gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call(fetchDataMock);
});
let atwhoInstance; let atwhoInstance;
let sorterValue; let sorterValue;
let filterValue;
describe('.typesWithBackendFiltering', () => {
it('should contain vulnerabilities', () => {
expect(GfmAutoComplete.typesWithBackendFiltering).toContain('vulnerabilities');
});
});
describe('DefaultOptions.filter', () => {
let items;
beforeEach(() => {
jest.spyOn(fetchDataMock, 'fetchData');
jest.spyOn($.fn.atwho.default.callbacks, 'filter').mockImplementation(() => {});
});
describe('assets loading', () => {
beforeEach(() => {
atwhoInstance = { setting: {}, $inputor: 'inputor', at: '+' };
items = ['loading'];
filterValue = gfmAutoCompleteCallbacks.filter.call(atwhoInstance, '', items);
});
it('should call the fetchData function without query', () => {
expect(fetchDataMock.fetchData).toHaveBeenCalledWith('inputor', '+');
});
it('should not call the default atwho filter', () => {
expect($.fn.atwho.default.callbacks.filter).not.toHaveBeenCalled();
});
it('should return the passed unfiltered items', () => {
expect(filterValue).toEqual(items);
});
});
describe('backend filtering', () => {
beforeEach(() => {
atwhoInstance = { setting: {}, $inputor: 'inputor', at: '+' };
items = [];
});
describe('when previous query is different from current one', () => {
beforeEach(() => {
gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({
previousQuery: 'oldquery',
...fetchDataMock,
});
filterValue = gfmAutoCompleteCallbacks.filter.call(atwhoInstance, 'newquery', items);
});
it('should call the fetchData function with query', () => {
expect(fetchDataMock.fetchData).toHaveBeenCalledWith('inputor', '+', 'newquery');
});
it('should not call the default atwho filter', () => {
expect($.fn.atwho.default.callbacks.filter).not.toHaveBeenCalled();
});
it('should return the passed unfiltered items', () => {
expect(filterValue).toEqual(items);
});
});
describe('when previous query is not different from current one', () => {
beforeEach(() => {
gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({
previousQuery: 'oldquery',
...fetchDataMock,
});
filterValue = gfmAutoCompleteCallbacks.filter.call(
atwhoInstance,
'oldquery',
items,
'searchKey',
);
});
it('should not call the fetchData function', () => {
expect(fetchDataMock.fetchData).not.toHaveBeenCalled();
});
it('should call the default atwho filter', () => {
expect($.fn.atwho.default.callbacks.filter).toHaveBeenCalledWith(
'oldquery',
items,
'searchKey',
);
});
});
});
});
describe('fetchData', () => {
const { fetchData } = GfmAutoComplete.prototype;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
jest.spyOn(axios, 'get');
jest.spyOn(AjaxCache, 'retrieve');
});
afterEach(() => {
mock.restore();
});
describe('already loading data', () => {
beforeEach(() => {
const context = {
isLoadingData: { '+': true },
dataSources: {},
cachedData: {},
};
fetchData.call(context, {}, '+', '');
});
it('should not call either axios nor AjaxCache', () => {
expect(axios.get).not.toHaveBeenCalled();
expect(AjaxCache.retrieve).not.toHaveBeenCalled();
});
});
describe('backend filtering', () => {
describe('data is not in cache', () => {
let context;
beforeEach(() => {
context = {
isLoadingData: { '+': false },
dataSources: { vulnerabilities: 'vulnerabilities_autocomplete_url' },
cachedData: {},
};
});
it('should call axios with query', () => {
fetchData.call(context, {}, '+', 'query');
expect(axios.get).toHaveBeenCalledWith('vulnerabilities_autocomplete_url', {
params: { search: 'query' },
});
});
it.each([200, 500])('should set the loading state', async responseStatus => {
mock.onGet('vulnerabilities_autocomplete_url').replyOnce(responseStatus);
fetchData.call(context, {}, '+', 'query');
expect(context.isLoadingData['+']).toBe(true);
await waitForPromises();
expect(context.isLoadingData['+']).toBe(false);
});
});
describe('data is in cache', () => {
beforeEach(() => {
const context = {
isLoadingData: { '+': false },
dataSources: { vulnerabilities: 'vulnerabilities_autocomplete_url' },
cachedData: { '+': [{}] },
};
fetchData.call(context, {}, '+', 'query');
});
it('should anyway call axios with query ignoring cache', () => {
expect(axios.get).toHaveBeenCalledWith('vulnerabilities_autocomplete_url', {
params: { search: 'query' },
});
});
});
});
describe('frontend filtering', () => {
describe('data is not in cache', () => {
beforeEach(() => {
const context = {
isLoadingData: { '#': false },
dataSources: { issues: 'issues_autocomplete_url' },
cachedData: {},
};
fetchData.call(context, {}, '#', 'query');
});
it('should call AjaxCache', () => {
expect(AjaxCache.retrieve).toHaveBeenCalledWith('issues_autocomplete_url', true);
});
});
describe('data is in cache', () => {
beforeEach(() => {
const context = {
isLoadingData: { '#': false },
dataSources: { issues: 'issues_autocomplete_url' },
cachedData: { '#': [{}] },
loadData: () => {},
};
fetchData.call(context, {}, '#', 'query');
});
it('should not call AjaxCache', () => {
expect(AjaxCache.retrieve).not.toHaveBeenCalled();
});
});
});
});
describe('DefaultOptions.sorter', () => { describe('DefaultOptions.sorter', () => {
describe('assets loading', () => { describe('assets loading', () => {
......
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