diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index c88d084eff4..4c3f9ab5a51 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -424,7 +424,7 @@ .es8-services: services: - !reference [.zoekt-services, services] - - name: elasticsearch:8.11.4 + - name: elasticsearch:8.17.4 .es8-variables: variables: diff --git a/Gemfile.next.checksum b/Gemfile.next.checksum index 05bce3185bf..3c9280b26ab 100644 --- a/Gemfile.next.checksum +++ b/Gemfile.next.checksum @@ -723,8 +723,8 @@ {"name":"state_machines","version":"0.5.0","platform":"ruby","checksum":"23e6249d374a920b528dccade403518b4abbd83841a3e2c9ef13e6f1a009b102"}, {"name":"state_machines-activemodel","version":"0.8.0","platform":"ruby","checksum":"e932dab190d4be044fb5f9cab01a3ea0b092c5f113d4676c6c0a0d49bf738d2c"}, {"name":"state_machines-activerecord","version":"0.8.0","platform":"ruby","checksum":"072fb701b8ab03de0608297f6c55dc34ed096e556fa8f77e556f3c461c71aab6"}, -{"name":"stringio","version":"3.1.5","platform":"java","checksum":"d1e136540e41c833ba39c0468b212f33755b438517b45bebf5868eec2c9422a7"}, -{"name":"stringio","version":"3.1.5","platform":"ruby","checksum":"bca92461515a131535743bc81d5559fa1de7d80cff9a654d6c0af6f9f27e35c8"}, +{"name":"stringio","version":"3.1.6","platform":"java","checksum":"dbdb1ee4e6d75782bbc7e8cc7d84cd05e592df50494f363011cc7cd48153bbf7"}, +{"name":"stringio","version":"3.1.6","platform":"ruby","checksum":"292c495d1657adfcdf0a32eecf12a60e6691317a500c3112ad3b2e31068274f5"}, {"name":"strings","version":"0.2.1","platform":"ruby","checksum":"933293b3c95cf85b81eb44b3cf673e3087661ba739bbadfeadf442083158d6fb"}, {"name":"strings-ansi","version":"0.2.0","platform":"ruby","checksum":"90262d760ea4a94cc2ae8d58205277a343409c288cbe7c29416b1826bd511c88"}, {"name":"swd","version":"2.0.3","platform":"ruby","checksum":"4cdbe2a4246c19f093fce22e967ec3ebdd4657d37673672e621bf0c7eb770655"}, diff --git a/Gemfile.next.lock b/Gemfile.next.lock index 957d27bfa02..c0b3f86e0c8 100644 --- a/Gemfile.next.lock +++ b/Gemfile.next.lock @@ -1857,7 +1857,7 @@ GEM state_machines-activerecord (0.8.0) activerecord (>= 5.1) state_machines-activemodel (>= 0.8.0) - stringio (3.1.5) + stringio (3.1.6) strings (0.2.1) strings-ansi (~> 0.2) unicode-display_width (>= 1.5, < 3.0) diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue index dec7d6f7849..fbdf67d1f38 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue @@ -28,6 +28,12 @@ import { getNextPageParams, getPreviousPageParams, } from '~/packages_and_registries/shared/utils'; +import PageSizeSelector from '~/vue_shared/components/page_size_selector.vue'; +import { getStorageValue, saveStorageValue } from '~/lib/utils/local_storage'; +import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility'; +import { smoothScrollTop } from '~/behaviors/smooth_scroll'; + +const PAGE_SIZE_KEY = 'packages_page_size'; export default { components: { @@ -41,6 +47,7 @@ export default { PackageSearch, PersistedPagination, DeletePackages, + PageSizeSelector, }, directives: { GlTooltip: GlTooltipDirective, @@ -53,6 +60,7 @@ export default { filters: {}, isDeleteInProgress: false, pageParams: {}, + pageSize: GRAPHQL_PAGE_SIZE, groupSettings: {}, }; }, @@ -101,7 +109,7 @@ export default { fullPath: this.fullPath, sort: this.isGroupPage ? undefined : this.sort, groupSort: this.isGroupPage ? this.sort : undefined, - first: GRAPHQL_PAGE_SIZE, + first: this.pageSize, ...this.packageParams, ...this.pageParams, }; @@ -160,6 +168,14 @@ export default { ]; }, }, + created() { + const localStoragePageSize = getStorageValue(PAGE_SIZE_KEY); + if (localStoragePageSize.exists) { + this.pageSize = localStoragePageSize.value; + } else { + this.savePageSizeToLocalStorage(this.pageSize); + } + }, mounted() { this.checkDeleteAlert(); }, @@ -173,16 +189,35 @@ export default { historyReplaceState(cleanUrl); } }, + handlePageSizeChange(value) { + this.pageSize = value; + this.savePageSizeToLocalStorage(value); + this.pageParams = { + after: undefined, + before: undefined, + }; + + const url = mergeUrlParams(this.pageParams, window.location.search, { spreadArrays: true }); + updateHistory({ + url, + }); + smoothScrollTop(); + }, handleSearchUpdate({ sort, filters, pageInfo }) { - this.pageParams = getPageParams(pageInfo, GRAPHQL_PAGE_SIZE); + this.pageParams = getPageParams(pageInfo, this.pageSize); this.sort = sort; this.filters = { ...filters }; }, fetchNextPage() { - this.pageParams = getNextPageParams(this.pageInfo.endCursor, GRAPHQL_PAGE_SIZE); + this.pageParams = getNextPageParams(this.pageInfo.endCursor, this.pageSize); + smoothScrollTop(); }, fetchPreviousPage() { - this.pageParams = getPreviousPageParams(this.pageInfo.startCursor, GRAPHQL_PAGE_SIZE); + this.pageParams = getPreviousPageParams(this.pageInfo.startCursor, this.pageSize); + smoothScrollTop(); + }, + savePageSizeToLocalStorage(value) { + saveStorageValue(PAGE_SIZE_KEY, value); }, }, i18n: { @@ -259,6 +294,12 @@ export default { @prev="fetchPreviousPage" @next="fetchNextPage" /> + diff --git a/app/assets/javascripts/todos/components/todos_filter_bar.vue b/app/assets/javascripts/todos/components/todos_filter_bar.vue index dfd03cb99c4..6d88d5ec120 100644 --- a/app/assets/javascripts/todos/components/todos_filter_bar.vue +++ b/app/assets/javascripts/todos/components/todos_filter_bar.vue @@ -117,7 +117,7 @@ export const ACTION_TYPES = [ }, { id: '2', - value: TODO_ACTION_TYPE_MENTIONED, + value: `${TODO_ACTION_TYPE_MENTIONED};${TODO_ACTION_TYPE_DIRECTLY_ADDRESSED}`, title: s__('Todos|Mentioned'), }, { @@ -140,11 +140,6 @@ export const ACTION_TYPES = [ value: TODO_ACTION_TYPE_UNMERGEABLE, title: s__('Todos|Unmergeable'), }, - { - id: '7', - value: TODO_ACTION_TYPE_DIRECTLY_ADDRESSED, - title: s__('Todos|Directly addressed'), - }, { id: '8', value: TODO_ACTION_TYPE_MERGE_TRAIN_REMOVED, @@ -242,7 +237,11 @@ const FILTERS = [ return value; }, toUrlValueResolver: (value) => { - const { id } = ACTION_TYPES.find((option) => option.value === value); + const { id } = ACTION_TYPES.find((option) => { + // For combined values like "mentioned;directly_addressed" use their first value's id for the URL + const optionMainValue = option.value.split(';')[0]; + return optionMainValue === value; + }); return id; }, }, @@ -341,7 +340,7 @@ export default { return Object.fromEntries( FILTERS.map(({ apiParam, tokenType }) => { const selectedValue = this.filterTokens.find((token) => token.type === tokenType); - return [apiParam, selectedValue ? [selectedValue.value.data] : []]; + return [apiParam, selectedValue ? selectedValue.value.data.split(';') : []]; }), ); }, diff --git a/db/docs/vulnerability_occurrence_identifiers.yml b/db/docs/vulnerability_occurrence_identifiers.yml index df94e07c744..a9d5b0a143f 100644 --- a/db/docs/vulnerability_occurrence_identifiers.yml +++ b/db/docs/vulnerability_occurrence_identifiers.yml @@ -8,14 +8,6 @@ description: Join table between Findings and Identifiers introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6896 milestone: '11.4' gitlab_schema: gitlab_sec -desired_sharding_key: - project_id: - references: projects - backfill_via: - parent: - foreign_key: occurrence_id - table: vulnerability_occurrences - sharding_key: project_id - belongs_to: finding -desired_sharding_key_migration_job_name: BackfillVulnerabilityOccurrenceIdentifiersProjectId +sharding_key: + project_id: projects table_size: small diff --git a/db/post_migrate/20250326021934_add_vulnerability_occurrence_identifiers_project_id_index.rb b/db/post_migrate/20250326021934_add_vulnerability_occurrence_identifiers_project_id_index.rb new file mode 100644 index 00000000000..90f5bf14b67 --- /dev/null +++ b/db/post_migrate/20250326021934_add_vulnerability_occurrence_identifiers_project_id_index.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddVulnerabilityOccurrenceIdentifiersProjectIdIndex < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.11' + + INDEX_NAME = 'index_vulnerability_occurrence_identifiers_on_project_id' + + def up + add_concurrent_index :vulnerability_occurrence_identifiers, :project_id, name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :vulnerability_occurrence_identifiers, INDEX_NAME + end +end diff --git a/db/post_migrate/20250326022545_add_vulnerability_occurrence_identifiers_project_id_not_null.rb b/db/post_migrate/20250326022545_add_vulnerability_occurrence_identifiers_project_id_not_null.rb new file mode 100644 index 00000000000..f1e7cc904ee --- /dev/null +++ b/db/post_migrate/20250326022545_add_vulnerability_occurrence_identifiers_project_id_not_null.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class AddVulnerabilityOccurrenceIdentifiersProjectIdNotNull < Gitlab::Database::Migration[2.2] + milestone '17.11' + disable_ddl_transaction! + + def up + add_not_null_constraint :vulnerability_occurrence_identifiers, :project_id + end + + def down + remove_not_null_constraint :vulnerability_occurrence_identifiers, :project_id + end +end diff --git a/db/schema_migrations/20250326021934 b/db/schema_migrations/20250326021934 new file mode 100644 index 00000000000..53030399075 --- /dev/null +++ b/db/schema_migrations/20250326021934 @@ -0,0 +1 @@ +e2701c135af52f28a94c794bd2c21f0100ff6f25590a9a7eba7b99185a5578e5 \ No newline at end of file diff --git a/db/schema_migrations/20250326022545 b/db/schema_migrations/20250326022545 new file mode 100644 index 00000000000..1a0446f0430 --- /dev/null +++ b/db/schema_migrations/20250326022545 @@ -0,0 +1 @@ +1f339cff03af736fb77b4953f27e9c00d898eb789a483e30fbe0dbc73c17e7f5 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 9172601f856..af8a054f834 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -24918,7 +24918,8 @@ CREATE TABLE vulnerability_occurrence_identifiers ( updated_at timestamp with time zone NOT NULL, occurrence_id bigint NOT NULL, identifier_id bigint NOT NULL, - project_id bigint + project_id bigint, + CONSTRAINT check_67fe772bae CHECK ((project_id IS NOT NULL)) ); CREATE SEQUENCE vulnerability_occurrence_identifiers_id_seq @@ -37514,6 +37515,8 @@ CREATE INDEX index_vulnerability_merge_request_links_on_project_id ON vulnerabil CREATE INDEX index_vulnerability_occurrence_identifiers_on_identifier_id ON vulnerability_occurrence_identifiers USING btree (identifier_id); +CREATE INDEX index_vulnerability_occurrence_identifiers_on_project_id ON vulnerability_occurrence_identifiers USING btree (project_id); + CREATE UNIQUE INDEX index_vulnerability_occurrence_identifiers_on_unique_keys ON vulnerability_occurrence_identifiers USING btree (occurrence_id, identifier_id); CREATE INDEX index_vulnerability_occurrences_for_override_uuids_logic ON vulnerability_occurrences USING btree (project_id, report_type, location_fingerprint); diff --git a/doc/integration/exact_code_search/zoekt.md b/doc/integration/exact_code_search/zoekt.md index b8f1516f4af..3d6c99eca27 100644 --- a/doc/integration/exact_code_search/zoekt.md +++ b/doc/integration/exact_code_search/zoekt.md @@ -72,13 +72,39 @@ Prerequisites: - You must have administrator access to the instance. Indexing performance depends on the CPU and memory limits on the Zoekt indexer nodes. -To check indexing status, in the Rails console, run the following command: +To check indexing status: -```ruby -Search::Zoekt::Index.group(:state).count -Search::Zoekt::Repository.group(:state).count -Search::Zoekt::Task.group(:state).count -``` + {{< tabs >}} + + {{< tab title="GitLab 17.10 and later" >}} + + Run this Rake task: + + ```shell + gitlab-rake gitlab:zoekt:info + ``` + + To have the data refresh automatically every 10 seconds, run this task instead: + + ```shell + gitlab-rake "gitlab:zoekt:info[10]" + ``` + + {{< /tab >}} + + {{< tab title="GitLab 17.9 and earlier" >}} + + In a [Rails console](../../administration/operations/rails_console.md#starting-a-rails-console-session), run these commands: + + ```ruby + Search::Zoekt::Index.group(:state).count + Search::Zoekt::Repository.group(:state).count + Search::Zoekt::Task.group(:state).count + ``` + + {{< /tab >}} + + {{< /tabs >}} ## Delete offline nodes automatically @@ -225,7 +251,7 @@ To resolve this issue, ensure Silent Mode is disabled. In `application_json.log`, you might get the following error: ```plaintext -connections to all backends failing; last error: UNKNOWN: ipv4:1.2.3.4:5678: Trying to connect an http1.x server +connections to all backends failing; last error: UNKNOWN: ipv4:1.2.3.4:5678: Trying to connect an http1.x server ``` To resolve this issue, check if you're using any proxies. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bdc09bc5b70..27e3604d0b1 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -61489,9 +61489,6 @@ msgstr "" msgid "Todos|Design" msgstr "" -msgid "Todos|Directly addressed" -msgstr "" - msgid "Todos|Done" msgstr "" diff --git a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js index 9952b9880eb..5370409a07c 100644 --- a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js @@ -6,7 +6,11 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; +import setWindowLocation from 'helpers/set_window_location_helper'; import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants'; +import * as urlUtility from '~/lib/utils/url_utility'; +import { smoothScrollTop } from '~/behaviors/smooth_scroll'; import ListPage from '~/packages_and_registries/package_registry/pages/list.vue'; import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue'; import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue'; @@ -19,6 +23,7 @@ import { PACKAGE_HELP_URL, } from '~/packages_and_registries/package_registry/constants'; import PersistedPagination from '~/packages_and_registries/shared/components/persisted_pagination.vue'; +import PageSizeSelector from '~/vue_shared/components/page_size_selector.vue'; import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql'; import getGroupPackageSettings from '~/packages_and_registries/package_registry/graphql/queries/get_group_package_settings.query.graphql'; import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql'; @@ -31,8 +36,10 @@ import { } from '../mock_data'; jest.mock('~/alert'); +jest.mock('~/behaviors/smooth_scroll'); describe('PackagesListApp', () => { + useLocalStorageSpy(); let wrapper; let apolloProvider; @@ -69,6 +76,7 @@ describe('PackagesListApp', () => { const findDeletePackages = () => wrapper.findComponent(DeletePackages); const findSettingsLink = () => wrapper.findComponent(GlButton); const findPagination = () => wrapper.findComponent(PersistedPagination); + const findPageSizeSelector = () => wrapper.findComponent(PageSizeSelector); const mountComponent = ({ resolver = jest.fn().mockResolvedValue(packagesListQuery()), @@ -118,14 +126,96 @@ describe('PackagesListApp', () => { }); it('has persisted pagination', async () => { - const resolver = jest.fn().mockResolvedValue(packagesListQuery()); - - mountComponent({ resolver }); + mountComponent(); await waitForFirstRequest(); expect(findPagination().props('pagination')).toEqual(pagination()); }); + describe('page size', () => { + const resolver = jest.fn().mockResolvedValue(packagesListQuery()); + + describe('when local storage value is not set', () => { + beforeEach(async () => { + mountComponent({ resolver }); + await waitForFirstRequest(); + }); + + it('page size selector prop is set to default value', () => { + expect(findPageSizeSelector().props('value')).toBe(GRAPHQL_PAGE_SIZE); + }); + + it('sets local storage value', () => { + expect(localStorage.getItem('packages_page_size')).toBe(String(GRAPHQL_PAGE_SIZE)); + }); + + it('calls the resolver with default page size', () => { + expect(resolver).toHaveBeenCalledWith({ + first: 20, + fullPath: 'gitlab-org', + groupSort: 'NAME_DESC', + isGroupPage: true, + }); + }); + }); + + describe('when localstorage value is set', () => { + beforeEach(async () => { + localStorage.setItem('packages_page_size', '50'); + + mountComponent({ resolver }); + + await waitForFirstRequest(); + }); + + it('page size selector prop is set to value from local storage', () => { + expect(findPageSizeSelector().props('value')).toBe(50); + }); + + it('calls the resolver with page size value from local storage', () => { + expect(resolver).toHaveBeenCalledWith({ + first: 50, + fullPath: 'gitlab-org', + groupSort: 'NAME_DESC', + isGroupPage: true, + }); + }); + }); + + describe('is changed', () => { + beforeEach(async () => { + setWindowLocation('?before=123&name=test'); + jest.spyOn(urlUtility, 'updateHistory'); + mountComponent({ resolver }); + await waitForFirstRequest(); + await findPageSizeSelector().vm.$emit('input', 100); + }); + + it('sets local storage value', () => { + expect(localStorage.getItem('packages_page_size')).toBe(String(100)); + }); + + it('calls the resolver with default page size', () => { + expect(resolver).toHaveBeenLastCalledWith({ + first: 100, + fullPath: 'gitlab-org', + groupSort: 'NAME_DESC', + isGroupPage: true, + }); + }); + + it('updates query params', () => { + expect(urlUtility.updateHistory).toHaveBeenCalledWith({ + url: '?name=test', + }); + }); + + it('scrolls to top of page', () => { + expect(smoothScrollTop).toHaveBeenCalledTimes(1); + }); + }); + }); + it('has a package title', async () => { mountComponent(); @@ -257,6 +347,7 @@ describe('PackagesListApp', () => { expect(resolver).toHaveBeenCalledWith( expect.objectContaining({ after: pagination().endCursor, first: GRAPHQL_PAGE_SIZE }), ); + expect(smoothScrollTop).toHaveBeenCalledTimes(1); }); it('when pagination emits prev event fetches the prev set of records', async () => { @@ -270,6 +361,7 @@ describe('PackagesListApp', () => { last: GRAPHQL_PAGE_SIZE, }), ); + expect(smoothScrollTop).toHaveBeenCalledTimes(1); }); }); @@ -400,6 +492,10 @@ describe('PackagesListApp', () => { it('does not request for group package settings', () => { expect(groupPackageSettingsResolver).not.toHaveBeenCalled(); }); + + it('does not render page size selector', () => { + expect(findPageSizeSelector().exists()).toBe(false); + }); }); describe('filter without results', () => { diff --git a/spec/frontend/todos/components/todos_filter_bar_spec.js b/spec/frontend/todos/components/todos_filter_bar_spec.js index 13856054c4f..248370b3eb5 100644 --- a/spec/frontend/todos/components/todos_filter_bar_spec.js +++ b/spec/frontend/todos/components/todos_filter_bar_spec.js @@ -245,6 +245,25 @@ describe('TodosFilterBar', () => { expect(trackingSpy).toHaveBeenCalledTimes(2); }); + it('handles combined action types with semicolon delimiter', () => { + createComponent(); + const combinedActionValue = `mentioned;directly_addressed`; + + findGlFilteredSearch().vm.$emit( + 'input', + generateFilterTokens({ + action: combinedActionValue, + }), + ); + findGlFilteredSearch().vm.$emit('submit'); + + expect(wrapper.emitted('filters-changed')[0][0].action).toEqual([ + 'mentioned', + 'directly_addressed', + ]); + expect(window.location.search).toBe('?action_id=2'); + }); + it('shows a warning message when trying to text-search and only submits the supported filter tokens', async () => { createComponent(); expect(findGlAlert().exists()).toBe(false); @@ -404,5 +423,15 @@ describe('TodosFilterBar', () => { expect(wrapper.emitted('filters-changed')).toBeUndefined(); }); + + it('resolves URL action_id to the first value in combined actions', () => { + setWindowLocation('?action_id=2'); + createComponent(); + + expect(wrapper.emitted('filters-changed')[0][0].action).toEqual([ + 'mentioned', + 'directly_addressed', + ]); + }); }); }); diff --git a/spec/lib/gitlab/database/sharding_key_spec.rb b/spec/lib/gitlab/database/sharding_key_spec.rb index 180c5285686..3521f1c8b26 100644 --- a/spec/lib/gitlab/database/sharding_key_spec.rb +++ b/spec/lib/gitlab/database/sharding_key_spec.rb @@ -100,6 +100,7 @@ RSpec.describe 'new tables missing sharding_key', feature_category: :cell do 'dast_profiles_pipelines.project_id', # LFK already present on dast_profiles and will cascade delete 'dast_scanner_profiles_builds.project_id', # LFK already present on dast_scanner_profiles and will cascade delete 'vulnerability_finding_links.project_id', # LFK already present on vulnerability_occurrence with cascade delete + 'vulnerability_occurrence_identifiers.project_id', # LFK present on vulnerability_occurrence with cascade delete 'secret_detection_token_statuses.project_id', # LFK already present on vulnerability_occurrence with cascade delete. 'ldap_group_links.group_id',