From eefbee4451565989727256d36176dc2950e3a0b7 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 6 Nov 2020 06:08:51 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- app/assets/javascripts/search/index.js | 8 +- .../components/confidentiality_filter.vue | 25 ++++ .../sidebar/components/radio_filter.vue | 68 +++++++++++ .../sidebar/components/status_filter.vue | 25 ++++ .../constants/confidential_filter_data.js | 36 ++++++ .../sidebar/constants/state_filter_data.js | 42 +++++++ .../javascripts/search/sidebar/index.js | 34 ++++++ .../javascripts/search/store/actions.js | 4 + .../search/store/mutation_types.js | 2 + .../javascripts/search/store/mutations.js | 3 + .../admin/instance_statistics_controller.rb | 2 +- app/controllers/search_controller.rb | 4 + .../layouts/nav/sidebar/_admin.html.haml | 2 +- ...able_instance_statistics_ff_by_default.yml | 5 + .../development/instance_statistics.yml | 2 +- .../development/search_facets.yml | 7 ++ .../analytics/instance_statistics.md | 19 --- .../database/partitioning/replace_table.rb | 76 ++++++++---- .../table_management_helpers.rb | 5 +- .../components/confidentiality_filter_spec.js | 65 ++++++++++ .../sidebar/components/radio_filter_spec.js | 111 ++++++++++++++++++ .../sidebar/components/status_filter_spec.js | 65 ++++++++++ spec/frontend/search/store/actions_spec.js | 8 ++ spec/frontend/search/store/mutations_spec.js | 10 ++ .../partitioning/replace_table_spec.rb | 56 +++++++-- spec/support/helpers/table_schema_helpers.rb | 16 ++- 26 files changed, 643 insertions(+), 57 deletions(-) create mode 100644 app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue create mode 100644 app/assets/javascripts/search/sidebar/components/radio_filter.vue create mode 100644 app/assets/javascripts/search/sidebar/components/status_filter.vue create mode 100644 app/assets/javascripts/search/sidebar/constants/confidential_filter_data.js create mode 100644 app/assets/javascripts/search/sidebar/constants/state_filter_data.js create mode 100644 app/assets/javascripts/search/sidebar/index.js create mode 100644 changelogs/unreleased/enable_instance_statistics_ff_by_default.yml create mode 100644 config/feature_flags/development/search_facets.yml create mode 100644 spec/frontend/search/sidebar/components/confidentiality_filter_spec.js create mode 100644 spec/frontend/search/sidebar/components/radio_filter_spec.js create mode 100644 spec/frontend/search/sidebar/components/status_filter_spec.js diff --git a/app/assets/javascripts/search/index.js b/app/assets/javascripts/search/index.js index 7d175bace4f..275d6351adc 100644 --- a/app/assets/javascripts/search/index.js +++ b/app/assets/javascripts/search/index.js @@ -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); }; diff --git a/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue b/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue new file mode 100644 index 00000000000..f8938e799aa --- /dev/null +++ b/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue @@ -0,0 +1,25 @@ + + + diff --git a/app/assets/javascripts/search/sidebar/components/radio_filter.vue b/app/assets/javascripts/search/sidebar/components/radio_filter.vue new file mode 100644 index 00000000000..b27c4e26fb5 --- /dev/null +++ b/app/assets/javascripts/search/sidebar/components/radio_filter.vue @@ -0,0 +1,68 @@ + + + diff --git a/app/assets/javascripts/search/sidebar/components/status_filter.vue b/app/assets/javascripts/search/sidebar/components/status_filter.vue new file mode 100644 index 00000000000..876123ccc52 --- /dev/null +++ b/app/assets/javascripts/search/sidebar/components/status_filter.vue @@ -0,0 +1,25 @@ + + + diff --git a/app/assets/javascripts/search/sidebar/constants/confidential_filter_data.js b/app/assets/javascripts/search/sidebar/constants/confidential_filter_data.js new file mode 100644 index 00000000000..ecb63ed9eea --- /dev/null +++ b/app/assets/javascripts/search/sidebar/constants/confidential_filter_data.js @@ -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, +}; diff --git a/app/assets/javascripts/search/sidebar/constants/state_filter_data.js b/app/assets/javascripts/search/sidebar/constants/state_filter_data.js new file mode 100644 index 00000000000..7c9a029ffe4 --- /dev/null +++ b/app/assets/javascripts/search/sidebar/constants/state_filter_data.js @@ -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, +}; diff --git a/app/assets/javascripts/search/sidebar/index.js b/app/assets/javascripts/search/sidebar/index.js new file mode 100644 index 00000000000..b19016edf3d --- /dev/null +++ b/app/assets/javascripts/search/sidebar/index.js @@ -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)); diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js index 4bc8b2a9a5d..722ed2eec26 100644 --- a/app/assets/javascripts/search/store/actions.js +++ b/app/assets/javascripts/search/store/actions.js @@ -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 }); +}; diff --git a/app/assets/javascripts/search/store/mutation_types.js b/app/assets/javascripts/search/store/mutation_types.js index 8469f3517c0..2482621d4d7 100644 --- a/app/assets/javascripts/search/store/mutation_types.js +++ b/app/assets/javascripts/search/store/mutation_types.js @@ -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'; diff --git a/app/assets/javascripts/search/store/mutations.js b/app/assets/javascripts/search/store/mutations.js index 34fe14716f9..e57850b870e 100644 --- a/app/assets/javascripts/search/store/mutations.js +++ b/app/assets/javascripts/search/store/mutations.js @@ -12,4 +12,7 @@ export default { state.fetchingGroups = false; state.groups = []; }, + [types.SET_QUERY](state, { key, value }) { + state.query[key] = value; + }, }; diff --git a/app/controllers/admin/instance_statistics_controller.rb b/app/controllers/admin/instance_statistics_controller.rb index dfbd704cb0c..05a0a1ce314 100644 --- a/app/controllers/admin/instance_statistics_controller.rb +++ b/app/controllers/admin/instance_statistics_controller.rb @@ -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 diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 643ad18cc31..048fa792185 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -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 diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index 0da4d4f7ddd..1e0e9628c89 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -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 diff --git a/changelogs/unreleased/enable_instance_statistics_ff_by_default.yml b/changelogs/unreleased/enable_instance_statistics_ff_by_default.yml new file mode 100644 index 00000000000..0d56d5fd765 --- /dev/null +++ b/changelogs/unreleased/enable_instance_statistics_ff_by_default.yml @@ -0,0 +1,5 @@ +--- +title: Enable 'instance_statistics' feature flag by default +merge_request: 46962 +author: +type: changed diff --git a/config/feature_flags/development/instance_statistics.yml b/config/feature_flags/development/instance_statistics.yml index 212b7b39615..f4d606b099f 100644 --- a/config/feature_flags/development/instance_statistics.yml +++ b/config/feature_flags/development/instance_statistics.yml @@ -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 \ No newline at end of file +default_enabled: true diff --git a/config/feature_flags/development/search_facets.yml b/config/feature_flags/development/search_facets.yml new file mode 100644 index 00000000000..b100c4a6490 --- /dev/null +++ b/config/feature_flags/development/search_facets.yml @@ -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 diff --git a/doc/user/admin_area/analytics/instance_statistics.md b/doc/user/admin_area/analytics/instance_statistics.md index 8165c4f133e..87ada8316b9 100644 --- a/doc/user/admin_area/analytics/instance_statistics.md +++ b/doc/user/admin_area/analytics/instance_statistics.md @@ -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) -``` diff --git a/lib/gitlab/database/partitioning/replace_table.rb b/lib/gitlab/database/partitioning/replace_table.rb index 13178431703..6f6af223fa2 100644 --- a/lib/gitlab/database/partitioning/replace_table.rb +++ b/lib/gitlab/database/partitioning/replace_table.rb @@ -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 diff --git a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb index 6adbe046cb0..686dda80207 100644 --- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb +++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb @@ -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 diff --git a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js new file mode 100644 index 00000000000..68d20b2480e --- /dev/null +++ b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js @@ -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); + }); + }); + }); +}); diff --git a/spec/frontend/search/sidebar/components/radio_filter_spec.js b/spec/frontend/search/sidebar/components/radio_filter_spec.js new file mode 100644 index 00000000000..31a4a8859ee --- /dev/null +++ b/spec/frontend/search/sidebar/components/radio_filter_spec.js @@ -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, + }); + }); + }); + }); + }); +}); diff --git a/spec/frontend/search/sidebar/components/status_filter_spec.js b/spec/frontend/search/sidebar/components/status_filter_spec.js new file mode 100644 index 00000000000..188d47b38cd --- /dev/null +++ b/spec/frontend/search/sidebar/components/status_filter_spec.js @@ -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); + }); + }); + }); +}); diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js index a44b42e6363..0bab4ce17a6 100644 --- a/spec/frontend/search/store/actions_spec.js +++ b/spec/frontend/search/store/actions_spec.js @@ -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); + }); +}); diff --git a/spec/frontend/search/store/mutations_spec.js b/spec/frontend/search/store/mutations_spec.js index 097a2ff46fd..28d9646b97e 100644 --- a/spec/frontend/search/store/mutations_spec.js +++ b/spec/frontend/search/store/mutations_spec.js @@ -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); + }); + }); }); diff --git a/spec/lib/gitlab/database/partitioning/replace_table_spec.rb b/spec/lib/gitlab/database/partitioning/replace_table_spec.rb index 8800ff845ae..d47666eeffd 100644 --- a/spec/lib/gitlab/database/partitioning/replace_table_spec.rb +++ b/spec/lib/gitlab/database/partitioning/replace_table_spec.rb @@ -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) diff --git a/spec/support/helpers/table_schema_helpers.rb b/spec/support/helpers/table_schema_helpers.rb index f3f36491e7c..28794211190 100644 --- a/spec/support/helpers/table_schema_helpers.rb +++ b/spec/support/helpers/table_schema_helpers.rb @@ -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