Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
74d35955aa
commit
eefbee4451
26 changed files with 643 additions and 57 deletions
|
@ -1,11 +1,17 @@
|
|||
import { queryToObject } from '~/lib/utils/url_utility';
|
||||
import createStore from './store';
|
||||
import initDropdownFilters from './dropdown_filter';
|
||||
import { initSidebar } from './sidebar';
|
||||
import initGroupFilter from './group_filter';
|
||||
|
||||
export default () => {
|
||||
const store = createStore({ query: queryToObject(window.location.search) });
|
||||
|
||||
initDropdownFilters(store);
|
||||
if (gon.features.searchFacets) {
|
||||
initSidebar(store);
|
||||
} else {
|
||||
initDropdownFilters(store);
|
||||
}
|
||||
|
||||
initGroupFilter(store);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { confidentialFilterData } from '../constants/confidential_filter_data';
|
||||
import RadioFilter from './radio_filter.vue';
|
||||
|
||||
export default {
|
||||
name: 'ConfidentialityFilter',
|
||||
components: {
|
||||
RadioFilter,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['query']),
|
||||
showDropdown() {
|
||||
return Object.values(confidentialFilterData.scopes).includes(this.query.scope);
|
||||
},
|
||||
},
|
||||
confidentialFilterData,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="showDropdown">
|
||||
<radio-filter :filter-data="$options.confidentialFilterData" />
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,68 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
|
||||
import { sprintf, s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'RadioFilter',
|
||||
components: {
|
||||
GlFormRadioGroup,
|
||||
GlFormRadio,
|
||||
},
|
||||
props: {
|
||||
filterData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['query']),
|
||||
ANY() {
|
||||
return this.filterData.filters.ANY;
|
||||
},
|
||||
scope() {
|
||||
return this.query.scope;
|
||||
},
|
||||
initialFilter() {
|
||||
return this.query[this.filterData.filterParam];
|
||||
},
|
||||
filter() {
|
||||
return this.initialFilter || this.ANY.value;
|
||||
},
|
||||
filtersArray() {
|
||||
return this.filterData.filterByScope[this.scope];
|
||||
},
|
||||
selectedFilter: {
|
||||
get() {
|
||||
if (this.filtersArray.some(({ value }) => value === this.filter)) {
|
||||
return this.filter;
|
||||
}
|
||||
|
||||
return this.ANY.value;
|
||||
},
|
||||
set(value) {
|
||||
this.setQuery({ key: this.filterData.filterParam, value });
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setQuery']),
|
||||
radioLabel(filter) {
|
||||
return filter.value === this.ANY.value
|
||||
? sprintf(s__('Any %{header}'), { header: this.filterData.header.toLowerCase() })
|
||||
: filter.label;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h5 class="gl-mt-0">{{ filterData.header }}</h5>
|
||||
<gl-form-radio-group v-model="selectedFilter">
|
||||
<gl-form-radio v-for="f in filtersArray" :key="f.value" :value="f.value">
|
||||
{{ radioLabel(f) }}
|
||||
</gl-form-radio>
|
||||
</gl-form-radio-group>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,25 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { stateFilterData } from '../constants/state_filter_data';
|
||||
import RadioFilter from './radio_filter.vue';
|
||||
|
||||
export default {
|
||||
name: 'StatusFilter',
|
||||
components: {
|
||||
RadioFilter,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['query']),
|
||||
showDropdown() {
|
||||
return Object.values(stateFilterData.scopes).includes(this.query.scope);
|
||||
},
|
||||
},
|
||||
stateFilterData,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="showDropdown">
|
||||
<radio-filter :filter-data="$options.stateFilterData" />
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,36 @@
|
|||
import { __ } from '~/locale';
|
||||
|
||||
const header = __('Confidentiality');
|
||||
|
||||
const filters = {
|
||||
ANY: {
|
||||
label: __('Any'),
|
||||
value: null,
|
||||
},
|
||||
CONFIDENTIAL: {
|
||||
label: __('Confidential'),
|
||||
value: 'yes',
|
||||
},
|
||||
NOT_CONFIDENTIAL: {
|
||||
label: __('Not confidential'),
|
||||
value: 'no',
|
||||
},
|
||||
};
|
||||
|
||||
const scopes = {
|
||||
ISSUES: 'issues',
|
||||
};
|
||||
|
||||
const filterByScope = {
|
||||
[scopes.ISSUES]: [filters.ANY, filters.CONFIDENTIAL, filters.NOT_CONFIDENTIAL],
|
||||
};
|
||||
|
||||
const filterParam = 'confidential';
|
||||
|
||||
export const confidentialFilterData = {
|
||||
header,
|
||||
filters,
|
||||
scopes,
|
||||
filterByScope,
|
||||
filterParam,
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
import { __ } from '~/locale';
|
||||
|
||||
const header = __('Status');
|
||||
|
||||
const filters = {
|
||||
ANY: {
|
||||
label: __('Any'),
|
||||
value: 'all',
|
||||
},
|
||||
OPEN: {
|
||||
label: __('Open'),
|
||||
value: 'opened',
|
||||
},
|
||||
CLOSED: {
|
||||
label: __('Closed'),
|
||||
value: 'closed',
|
||||
},
|
||||
MERGED: {
|
||||
label: __('Merged'),
|
||||
value: 'merged',
|
||||
},
|
||||
};
|
||||
|
||||
const scopes = {
|
||||
ISSUES: 'issues',
|
||||
MERGE_REQUESTS: 'merge_requests',
|
||||
};
|
||||
|
||||
const filterByScope = {
|
||||
[scopes.ISSUES]: [filters.ANY, filters.OPEN, filters.CLOSED],
|
||||
[scopes.MERGE_REQUESTS]: [filters.ANY, filters.OPEN, filters.MERGED, filters.CLOSED],
|
||||
};
|
||||
|
||||
const filterParam = 'state';
|
||||
|
||||
export const stateFilterData = {
|
||||
header,
|
||||
filters,
|
||||
scopes,
|
||||
filterByScope,
|
||||
filterParam,
|
||||
};
|
34
app/assets/javascripts/search/sidebar/index.js
Normal file
34
app/assets/javascripts/search/sidebar/index.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import Vue from 'vue';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import StatusFilter from './components/status_filter.vue';
|
||||
import ConfidentialityFilter from './components/confidentiality_filter.vue';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
const mountRadioFilters = (store, { id, component }) => {
|
||||
const el = document.getElementById(id);
|
||||
|
||||
if (!el) return false;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store,
|
||||
render(createElement) {
|
||||
return createElement(component);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const radioFilters = [
|
||||
{
|
||||
id: 'js-search-filter-by-state',
|
||||
component: StatusFilter,
|
||||
},
|
||||
{
|
||||
id: 'js-search-filter-by-confidential',
|
||||
component: ConfidentialityFilter,
|
||||
},
|
||||
];
|
||||
|
||||
export const initSidebar = store =>
|
||||
[...radioFilters].map(filter => mountRadioFilters(store, filter));
|
|
@ -14,3 +14,7 @@ export const fetchGroups = ({ commit }, search) => {
|
|||
commit(types.RECEIVE_GROUPS_ERROR);
|
||||
});
|
||||
};
|
||||
|
||||
export const setQuery = ({ commit }, { key, value }) => {
|
||||
commit(types.SET_QUERY, { key, value });
|
||||
};
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
export const REQUEST_GROUPS = 'REQUEST_GROUPS';
|
||||
export const RECEIVE_GROUPS_SUCCESS = 'RECEIVE_GROUPS_SUCCESS';
|
||||
export const RECEIVE_GROUPS_ERROR = 'RECEIVE_GROUPS_ERROR';
|
||||
|
||||
export const SET_QUERY = 'SET_QUERY';
|
||||
|
|
|
@ -12,4 +12,7 @@ export default {
|
|||
state.fetchingGroups = false;
|
||||
state.groups = [];
|
||||
},
|
||||
[types.SET_QUERY](state, { key, value }) {
|
||||
state.query[key] = value;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -13,6 +13,6 @@ class Admin::InstanceStatisticsController < Admin::ApplicationController
|
|||
end
|
||||
|
||||
def check_feature_flag
|
||||
render_404 unless Feature.enabled?(:instance_statistics)
|
||||
render_404 unless Feature.enabled?(:instance_statistics, default_enabled: true)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,6 +23,10 @@ class SearchController < ApplicationController
|
|||
search_term_present && !params[:project_id].present?
|
||||
end
|
||||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:search_facets)
|
||||
end
|
||||
|
||||
layout 'search'
|
||||
|
||||
feature_category :global_search
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
= link_to admin_cohorts_path, title: _('Cohorts') do
|
||||
%span
|
||||
= _('Cohorts')
|
||||
- if Feature.enabled?(:instance_statistics)
|
||||
- if Feature.enabled?(:instance_statistics, default_enabled: true)
|
||||
= nav_link(controller: :instance_statistics) do
|
||||
= link_to admin_instance_statistics_path, title: _('Instance Statistics') do
|
||||
%span
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Enable 'instance_statistics' feature flag by default
|
||||
merge_request: 46962
|
||||
author:
|
||||
type: changed
|
|
@ -4,4 +4,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40583
|
|||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/241711
|
||||
group: group::analytics
|
||||
type: development
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
7
config/feature_flags/development/search_facets.yml
Normal file
7
config/feature_flags/development/search_facets.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: search_facets
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46809
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46595
|
||||
group: group::global search
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -36,22 +36,3 @@ in the categories shown in [Total counts](#total-counts).
|
|||
These charts help you visualize how rapidly these records are being created on your instance.
|
||||
|
||||
![Instance Activity Pipelines chart](img/instance_activity_pipelines_chart_v13_6.png)
|
||||
|
||||
### Enable or disable Instance Statistics
|
||||
|
||||
Instance Statistics is under development and not ready for production use. It is
|
||||
deployed behind a feature flag that is **disabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
|
||||
can enable it.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:instance_statistics)
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:instance_statistics)
|
||||
```
|
||||
|
|
|
@ -43,38 +43,70 @@ module Gitlab
|
|||
end
|
||||
|
||||
def sql_to_replace_table
|
||||
@sql_to_replace_table ||= [
|
||||
drop_default_sql(original_table, primary_key_column),
|
||||
set_default_sql(replacement_table, primary_key_column, "nextval('#{quote_table_name(sequence)}'::regclass)"),
|
||||
|
||||
change_sequence_owner_sql(sequence, replacement_table, primary_key_column),
|
||||
|
||||
rename_table_sql(original_table, replaced_table),
|
||||
rename_constraint_sql(replaced_table, original_primary_key, replaced_primary_key),
|
||||
|
||||
rename_table_sql(replacement_table, original_table),
|
||||
rename_constraint_sql(original_table, replacement_primary_key, original_primary_key)
|
||||
].join(DELIMITER)
|
||||
@sql_to_replace_table ||= combined_sql_statements.map(&:chomp).join(DELIMITER)
|
||||
end
|
||||
|
||||
def drop_default_sql(table, column)
|
||||
"ALTER TABLE #{quote_table_name(table)} ALTER COLUMN #{quote_column_name(column)} DROP DEFAULT"
|
||||
def combined_sql_statements
|
||||
statements = []
|
||||
|
||||
statements << alter_column_default(original_table, primary_key_column, expression: nil)
|
||||
statements << alter_column_default(replacement_table, primary_key_column,
|
||||
expression: "nextval('#{quote_table_name(sequence)}'::regclass)")
|
||||
|
||||
statements << alter_sequence_owned_by(sequence, replacement_table, primary_key_column)
|
||||
|
||||
rename_table_objects(statements, original_table, replaced_table, original_primary_key, replaced_primary_key)
|
||||
rename_table_objects(statements, replacement_table, original_table, replacement_primary_key, original_primary_key)
|
||||
|
||||
statements
|
||||
end
|
||||
|
||||
def set_default_sql(table, column, expression)
|
||||
"ALTER TABLE #{quote_table_name(table)} ALTER COLUMN #{quote_column_name(column)} SET DEFAULT #{expression}"
|
||||
def rename_table_objects(statements, old_table, new_table, old_primary_key, new_primary_key)
|
||||
statements << rename_table(old_table, new_table)
|
||||
statements << rename_constraint(new_table, old_primary_key, new_primary_key)
|
||||
|
||||
rename_partitions(statements, old_table, new_table)
|
||||
end
|
||||
|
||||
def change_sequence_owner_sql(sequence, table, column)
|
||||
"ALTER SEQUENCE #{quote_table_name(sequence)} OWNED BY #{quote_table_name(table)}.#{quote_column_name(column)}"
|
||||
def rename_partitions(statements, old_table_name, new_table_name)
|
||||
Gitlab::Database::PostgresPartition.for_parent_table(old_table_name).each do |partition|
|
||||
new_partition_name = partition.name.sub(/#{old_table_name}/, new_table_name)
|
||||
old_primary_key = default_primary_key(partition.name)
|
||||
new_primary_key = default_primary_key(new_partition_name)
|
||||
|
||||
statements << rename_constraint(partition.identifier, old_primary_key, new_primary_key)
|
||||
statements << rename_table(partition.identifier, new_partition_name)
|
||||
end
|
||||
end
|
||||
|
||||
def rename_table_sql(old_name, new_name)
|
||||
"ALTER TABLE #{quote_table_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
|
||||
def alter_column_default(table_name, column_name, expression:)
|
||||
default_clause = expression.nil? ? 'DROP DEFAULT' : "SET DEFAULT #{expression}"
|
||||
|
||||
<<~SQL
|
||||
ALTER TABLE #{quote_table_name(table_name)}
|
||||
ALTER COLUMN #{quote_column_name(column_name)} #{default_clause}
|
||||
SQL
|
||||
end
|
||||
|
||||
def rename_constraint_sql(table, old_name, new_name)
|
||||
"ALTER TABLE #{quote_table_name(table)} RENAME CONSTRAINT #{quote_column_name(old_name)} TO #{quote_column_name(new_name)}"
|
||||
def alter_sequence_owned_by(sequence_name, table_name, column_name)
|
||||
<<~SQL
|
||||
ALTER SEQUENCE #{quote_table_name(sequence_name)}
|
||||
OWNED BY #{quote_table_name(table_name)}.#{quote_column_name(column_name)}
|
||||
SQL
|
||||
end
|
||||
|
||||
def rename_table(old_name, new_name)
|
||||
<<~SQL
|
||||
ALTER TABLE #{quote_table_name(old_name)}
|
||||
RENAME TO #{quote_table_name(new_name)}
|
||||
SQL
|
||||
end
|
||||
|
||||
def rename_constraint(table_name, old_name, new_name)
|
||||
<<~SQL
|
||||
ALTER TABLE #{quote_table_name(table_name)}
|
||||
RENAME CONSTRAINT #{quote_column_name(old_name)} TO #{quote_column_name(new_name)}
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -179,7 +179,8 @@ module Gitlab
|
|||
# Replaces a non-partitioned table with its partitioned copy. This is the final step in a partitioning
|
||||
# migration, which makes the partitioned table ready for use by the application. The partitioned copy should be
|
||||
# replaced with the original table in such a way that it appears seamless to any database clients. The replaced
|
||||
# table will be renamed to "#{replaced_table}_archived"
|
||||
# table will be renamed to "#{replaced_table}_archived". Partitions and primary key constraints will also be
|
||||
# renamed to match the naming scheme of the parent table.
|
||||
#
|
||||
# **NOTE** This method should only be used after all other migration steps have completed successfully.
|
||||
# There are several limitations to this method that MUST be handled before, or during, the swap migration:
|
||||
|
@ -415,7 +416,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def replace_table(original_table_name, replacement_table_name, replaced_table_name, primary_key_name)
|
||||
replace_table = Gitlab::Database::Partitioning::ReplaceTable.new(original_table_name,
|
||||
replace_table = Gitlab::Database::Partitioning::ReplaceTable.new(original_table_name.to_s,
|
||||
replacement_table_name, replaced_table_name, primary_key_name)
|
||||
|
||||
with_lock_retries do
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import Vuex from 'vuex';
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import { MOCK_QUERY } from 'jest/search/mock_data';
|
||||
import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue';
|
||||
import RadioFilter from '~/search/sidebar/components/radio_filter.vue';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('ConfidentialityFilter', () => {
|
||||
let wrapper;
|
||||
|
||||
const actionSpies = {
|
||||
applyQuery: jest.fn(),
|
||||
resetQuery: jest.fn(),
|
||||
};
|
||||
|
||||
const createComponent = initialState => {
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
query: MOCK_QUERY,
|
||||
...initialState,
|
||||
},
|
||||
actions: actionSpies,
|
||||
});
|
||||
|
||||
wrapper = shallowMount(ConfidentialityFilter, {
|
||||
localVue,
|
||||
store,
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
const findRadioFilter = () => wrapper.find(RadioFilter);
|
||||
|
||||
describe('template', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
describe.each`
|
||||
scope | showFilter
|
||||
${'issues'} | ${true}
|
||||
${'merge_requests'} | ${false}
|
||||
${'projects'} | ${false}
|
||||
${'milestones'} | ${false}
|
||||
${'users'} | ${false}
|
||||
${'notes'} | ${false}
|
||||
${'wiki_blobs'} | ${false}
|
||||
${'blobs'} | ${false}
|
||||
`(`dropdown`, ({ scope, showFilter }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ query: { scope } });
|
||||
});
|
||||
|
||||
it(`does${showFilter ? '' : ' not'} render when scope is ${scope}`, () => {
|
||||
expect(findRadioFilter().exists()).toBe(showFilter);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
111
spec/frontend/search/sidebar/components/radio_filter_spec.js
Normal file
111
spec/frontend/search/sidebar/components/radio_filter_spec.js
Normal file
|
@ -0,0 +1,111 @@
|
|||
import Vuex from 'vuex';
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import { GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
|
||||
import { MOCK_QUERY } from 'jest/search/mock_data';
|
||||
import RadioFilter from '~/search/sidebar/components/radio_filter.vue';
|
||||
import { stateFilterData } from '~/search/sidebar/constants/state_filter_data';
|
||||
import { confidentialFilterData } from '~/search/sidebar/constants/confidential_filter_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('RadioFilter', () => {
|
||||
let wrapper;
|
||||
|
||||
const actionSpies = {
|
||||
setQuery: jest.fn(),
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
filterData: stateFilterData,
|
||||
};
|
||||
|
||||
const createComponent = (initialState, props = {}) => {
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
query: MOCK_QUERY,
|
||||
...initialState,
|
||||
},
|
||||
actions: actionSpies,
|
||||
});
|
||||
|
||||
wrapper = shallowMount(RadioFilter, {
|
||||
localVue,
|
||||
store,
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
const findGlRadioButtonGroup = () => wrapper.find(GlFormRadioGroup);
|
||||
const findGlRadioButtons = () => findGlRadioButtonGroup().findAll(GlFormRadio);
|
||||
const findGlRadioButtonsText = () => findGlRadioButtons().wrappers.map(w => w.text());
|
||||
|
||||
describe('template', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders GlRadioButtonGroup always', () => {
|
||||
expect(findGlRadioButtonGroup().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('Radio Buttons', () => {
|
||||
describe('Status Filter', () => {
|
||||
it('renders a radio button for each filterOption', () => {
|
||||
expect(findGlRadioButtonsText()).toStrictEqual(
|
||||
stateFilterData.filterByScope[stateFilterData.scopes.ISSUES].map(f => {
|
||||
return f.value === stateFilterData.filters.ANY.value
|
||||
? `Any ${stateFilterData.header.toLowerCase()}`
|
||||
: f.label;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('clicking a radio button item calls setQuery', () => {
|
||||
const filter = stateFilterData.filters[Object.keys(stateFilterData.filters)[0]].value;
|
||||
findGlRadioButtonGroup().vm.$emit('input', filter);
|
||||
|
||||
expect(actionSpies.setQuery).toHaveBeenCalledWith(expect.any(Object), {
|
||||
key: stateFilterData.filterParam,
|
||||
value: filter,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Confidentiality Filter', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({}, { filterData: confidentialFilterData });
|
||||
});
|
||||
|
||||
it('renders a radio button for each filterOption', () => {
|
||||
expect(findGlRadioButtonsText()).toStrictEqual(
|
||||
confidentialFilterData.filterByScope[confidentialFilterData.scopes.ISSUES].map(f => {
|
||||
return f.value === confidentialFilterData.filters.ANY.value
|
||||
? `Any ${confidentialFilterData.header.toLowerCase()}`
|
||||
: f.label;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('clicking a radio button item calls setQuery', () => {
|
||||
const filter =
|
||||
confidentialFilterData.filters[Object.keys(confidentialFilterData.filters)[0]].value;
|
||||
findGlRadioButtonGroup().vm.$emit('input', filter);
|
||||
|
||||
expect(actionSpies.setQuery).toHaveBeenCalledWith(expect.any(Object), {
|
||||
key: confidentialFilterData.filterParam,
|
||||
value: filter,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,65 @@
|
|||
import Vuex from 'vuex';
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import { MOCK_QUERY } from 'jest/search/mock_data';
|
||||
import StatusFilter from '~/search/sidebar/components/status_filter.vue';
|
||||
import RadioFilter from '~/search/sidebar/components/radio_filter.vue';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('StatusFilter', () => {
|
||||
let wrapper;
|
||||
|
||||
const actionSpies = {
|
||||
applyQuery: jest.fn(),
|
||||
resetQuery: jest.fn(),
|
||||
};
|
||||
|
||||
const createComponent = initialState => {
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
query: MOCK_QUERY,
|
||||
...initialState,
|
||||
},
|
||||
actions: actionSpies,
|
||||
});
|
||||
|
||||
wrapper = shallowMount(StatusFilter, {
|
||||
localVue,
|
||||
store,
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
const findRadioFilter = () => wrapper.find(RadioFilter);
|
||||
|
||||
describe('template', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
describe.each`
|
||||
scope | showFilter
|
||||
${'issues'} | ${true}
|
||||
${'merge_requests'} | ${true}
|
||||
${'projects'} | ${false}
|
||||
${'milestones'} | ${false}
|
||||
${'users'} | ${false}
|
||||
${'notes'} | ${false}
|
||||
${'wiki_blobs'} | ${false}
|
||||
${'blobs'} | ${false}
|
||||
`(`dropdown`, ({ scope, showFilter }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ query: { scope } });
|
||||
});
|
||||
|
||||
it(`does${showFilter ? '' : ' not'} render when scope is ${scope}`, () => {
|
||||
expect(findRadioFilter().exists()).toBe(showFilter);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -43,3 +43,11 @@ describe('Global Search Store Actions', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setQuery', () => {
|
||||
const payload = { key: 'key1', value: 'value1' };
|
||||
|
||||
it('calls the SET_QUERY mutation', done => {
|
||||
testAction(actions.setQuery, payload, state, [{ type: types.SET_QUERY, payload }], [], done);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -35,4 +35,14 @@ describe('Global Search Store Mutations', () => {
|
|||
expect(state.groups).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SET_QUERY', () => {
|
||||
const payload = { key: 'key1', value: 'value1' };
|
||||
|
||||
it('sets query key to value', () => {
|
||||
mutations[types.SET_QUERY](state, payload);
|
||||
|
||||
expect(state.query[payload.key]).toBe(payload.value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -30,9 +30,6 @@ RSpec.describe Gitlab::Database::Partitioning::ReplaceTable, '#perform' do
|
|||
created_at timestamptz NOT NULL,
|
||||
PRIMARY KEY (id, created_at))
|
||||
PARTITION BY RANGE (created_at);
|
||||
|
||||
CREATE TABLE #{replacement_table}_202001 PARTITION OF #{replacement_table}
|
||||
FOR VALUES FROM ('2020-01-01') TO ('2020-02-01');
|
||||
SQL
|
||||
end
|
||||
|
||||
|
@ -56,13 +53,58 @@ RSpec.describe Gitlab::Database::Partitioning::ReplaceTable, '#perform' do
|
|||
end
|
||||
|
||||
it 'renames the primary key constraints to match the new table names' do
|
||||
expect(primary_key_constraint_name(original_table)).to eq(original_primary_key)
|
||||
expect(primary_key_constraint_name(replacement_table)).to eq(replacement_primary_key)
|
||||
expect_primary_keys_after_tables([original_table, replacement_table])
|
||||
|
||||
expect_table_to_be_replaced { replace_table }
|
||||
|
||||
expect(primary_key_constraint_name(original_table)).to eq(original_primary_key)
|
||||
expect(primary_key_constraint_name(archived_table)).to eq(archived_primary_key)
|
||||
expect_primary_keys_after_tables([original_table, archived_table])
|
||||
end
|
||||
|
||||
context 'when the table has partitions' do
|
||||
before do
|
||||
connection.execute(<<~SQL)
|
||||
CREATE TABLE gitlab_partitions_dynamic.#{replacement_table}_202001 PARTITION OF #{replacement_table}
|
||||
FOR VALUES FROM ('2020-01-01') TO ('2020-02-01');
|
||||
|
||||
CREATE TABLE gitlab_partitions_dynamic.#{replacement_table}_202002 PARTITION OF #{replacement_table}
|
||||
FOR VALUES FROM ('2020-02-01') TO ('2020-03-01');
|
||||
SQL
|
||||
end
|
||||
|
||||
it 'renames the partitions to match the new table name' do
|
||||
expect(partitions_for_parent_table(original_table).count).to eq(0)
|
||||
expect(partitions_for_parent_table(replacement_table).count).to eq(2)
|
||||
|
||||
expect_table_to_be_replaced { replace_table }
|
||||
|
||||
expect(partitions_for_parent_table(archived_table).count).to eq(0)
|
||||
|
||||
partitions = partitions_for_parent_table(original_table).all
|
||||
|
||||
expect(partitions.size).to eq(2)
|
||||
|
||||
expect(partitions[0]).to have_attributes(
|
||||
identifier: "gitlab_partitions_dynamic.#{original_table}_202001",
|
||||
condition: "FOR VALUES FROM ('2020-01-01 00:00:00+00') TO ('2020-02-01 00:00:00+00')")
|
||||
|
||||
expect(partitions[1]).to have_attributes(
|
||||
identifier: "gitlab_partitions_dynamic.#{original_table}_202002",
|
||||
condition: "FOR VALUES FROM ('2020-02-01 00:00:00+00') TO ('2020-03-01 00:00:00+00')")
|
||||
end
|
||||
|
||||
it 'renames the primary key constraints to match the new partition names' do
|
||||
original_partitions = ["#{replacement_table}_202001", "#{replacement_table}_202002"]
|
||||
expect_primary_keys_after_tables(original_partitions, schema: 'gitlab_partitions_dynamic')
|
||||
|
||||
expect_table_to_be_replaced { replace_table }
|
||||
|
||||
renamed_partitions = ["#{original_table}_202001", "#{original_table}_202002"]
|
||||
expect_primary_keys_after_tables(renamed_partitions, schema: 'gitlab_partitions_dynamic')
|
||||
end
|
||||
end
|
||||
|
||||
def partitions_for_parent_table(table)
|
||||
Gitlab::Database::PostgresPartition.for_parent_table(table)
|
||||
end
|
||||
|
||||
def expect_table_to_be_replaced(&block)
|
||||
|
|
|
@ -24,6 +24,14 @@ module TableSchemaHelpers
|
|||
expect(index_exists_by_name(name, schema: schema)).to be_nil
|
||||
end
|
||||
|
||||
def expect_primary_keys_after_tables(tables, schema: nil)
|
||||
tables.each do |table|
|
||||
primary_key = primary_key_constraint_name(table, schema: schema)
|
||||
|
||||
expect(primary_key).to eq("#{table}_pkey")
|
||||
end
|
||||
end
|
||||
|
||||
def table_oid(name)
|
||||
connection.select_value(<<~SQL)
|
||||
SELECT oid
|
||||
|
@ -75,13 +83,15 @@ module TableSchemaHelpers
|
|||
SQL
|
||||
end
|
||||
|
||||
def primary_key_constraint_name(table_name)
|
||||
def primary_key_constraint_name(table_name, schema: nil)
|
||||
table_name = schema ? "#{schema}.#{table_name}" : table_name
|
||||
|
||||
connection.select_value(<<~SQL)
|
||||
SELECT
|
||||
conname AS constraint_name
|
||||
FROM pg_catalog.pg_constraint
|
||||
WHERE conrelid = '#{table_name}'::regclass
|
||||
AND contype = 'p'
|
||||
WHERE pg_constraint.conrelid = '#{table_name}'::regclass
|
||||
AND pg_constraint.contype = 'p'
|
||||
SQL
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue