Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-01-31 09:09:30 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-01-31 09:09:30 +0300
commite6c495fe40320eb01bf8c4fb132c9f22449ae9d2 (patch)
treeadd32dcc4b186baf21b77431ce5718e0b7cdbf36
parent25805c16335ed6466f0e475417e3005cd09848c2 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/components/ml_experiment.vue31
-rw-r--r--app/assets/javascripts/pages/projects/ml/experiments/show/index.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/incubation/pagination.vue62
-rw-r--r--app/controllers/projects/ml/experiments_controller.rb16
-rw-r--r--app/helpers/projects/ml/experiments_helper.rb24
-rw-r--r--app/models/ml/candidate.rb20
-rw-r--r--app/views/projects/ml/experiments/show.html.haml3
-rw-r--r--db/docs/wiki_repository_states.yml10
-rw-r--r--db/migrate/20230109232316_create_wiki_repository_states.rb44
-rw-r--r--db/schema_migrations/202301092323161
-rw-r--r--db/structure.sql40
-rw-r--r--spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js103
-rw-r--r--spec/frontend/vue_shared/components/incubation/pagination_spec.js76
-rw-r--r--spec/helpers/projects/ml/experiments_helper_spec.rb78
-rw-r--r--spec/requests/projects/ml/experiments_controller_spec.rb61
15 files changed, 380 insertions, 193 deletions
diff --git a/app/assets/javascripts/ml/experiment_tracking/components/ml_experiment.vue b/app/assets/javascripts/ml/experiment_tracking/components/ml_experiment.vue
index 61278f52112..4b3f491266d 100644
--- a/app/assets/javascripts/ml/experiment_tracking/components/ml_experiment.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/components/ml_experiment.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTable, GlLink, GlPagination, GlTooltipDirective } from '@gitlab/ui';
+import { GlTable, GlLink, GlTooltipDirective } from '@gitlab/ui';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -11,6 +11,7 @@ import {
import { s__ } from '~/locale';
import { queryToObject, setUrlParams, visitUrl } from '~/lib/utils/url_utility';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import KeysetPagination from '~/vue_shared/components/incubation/pagination.vue';
import IncubationAlert from './incubation_alert.vue';
export default {
@@ -20,13 +21,13 @@ export default {
GlLink,
TimeAgo,
IncubationAlert,
- GlPagination,
RegistrySearch,
+ KeysetPagination,
},
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: ['candidates', 'metricNames', 'paramNames', 'pagination'],
+ inject: ['candidates', 'metricNames', 'paramNames', 'pageInfo'],
data() {
const query = queryToObject(window.location.search);
@@ -39,7 +40,6 @@ export default {
}
return {
- page: parseInt(query.page, 10) || 1,
filters: filter,
sorting: {
orderBy,
@@ -64,12 +64,6 @@ export default {
displayPagination() {
return this.candidates.length > 0;
},
- prevPage() {
- return this.pagination.page > 1 ? this.pagination.page - 1 : null;
- },
- nextPage() {
- return !this.pagination.isLastPage ? this.pagination.page + 1 : null;
- },
sortableFields() {
return [
...BASE_SORT_FIELDS,
@@ -100,11 +94,8 @@ export default {
},
},
methods: {
- generateLink(page) {
- return setUrlParams({ ...this.parsedQuery, page });
- },
submitFilters() {
- return visitUrl(setUrlParams({ ...this.parsedQuery, page: this.page }));
+ return visitUrl(setUrlParams({ ...this.parsedQuery }));
},
updateFilters(newValue) {
this.filters = newValue;
@@ -197,16 +188,6 @@ export default {
</template>
</gl-table>
- <gl-pagination
- v-if="displayPagination"
- v-model="pagination.page"
- :prev-page="prevPage"
- :next-page="nextPage"
- :total-items="pagination.totalItems"
- :per-page="pagination.perPage"
- :link-gen="generateLink"
- align="center"
- class="w-100"
- />
+ <keyset-pagination v-if="displayPagination" v-bind="pageInfo" />
</div>
</template>
diff --git a/app/assets/javascripts/pages/projects/ml/experiments/show/index.js b/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
index 6947b15dcbe..0e64d8c17db 100644
--- a/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
+++ b/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
@@ -14,7 +14,7 @@ const initShowExperiment = () => {
const candidates = JSON.parse(element.dataset.candidates);
const metricNames = JSON.parse(element.dataset.metrics);
const paramNames = JSON.parse(element.dataset.params);
- const pagination = convertObjectPropsToCamelCase(JSON.parse(element.dataset.pagination));
+ const pageInfo = convertObjectPropsToCamelCase(JSON.parse(element.dataset.pageInfo));
// eslint-disable-next-line no-new
new Vue({
@@ -23,7 +23,7 @@ const initShowExperiment = () => {
candidates,
metricNames,
paramNames,
- pagination,
+ pageInfo,
},
render(h) {
return h(MlExperiment);
diff --git a/app/assets/javascripts/vue_shared/components/incubation/pagination.vue b/app/assets/javascripts/vue_shared/components/incubation/pagination.vue
new file mode 100644
index 00000000000..b5afe92316a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/incubation/pagination.vue
@@ -0,0 +1,62 @@
+<script>
+import { GlKeysetPagination } from '@gitlab/ui';
+import { setUrlParams } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
+
+export default {
+ name: 'KeysetPagination',
+ components: {
+ GlKeysetPagination,
+ },
+ props: {
+ startCursor: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ endCursor: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ hasNextPage: {
+ type: Boolean,
+ required: true,
+ },
+ hasPreviousPage: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ previousPageLink() {
+ return setUrlParams({ cursor: this.startCursor });
+ },
+ nextPageLink() {
+ return setUrlParams({ cursor: this.endCursor });
+ },
+ isPaginationVisible() {
+ return this.hasPreviousPage || this.hasNextPage;
+ },
+ },
+ i18n: {
+ previousPageButtonLabel: __('Prev'),
+ nextPageButtonLabel: __('Next'),
+ },
+};
+</script>
+
+<template>
+ <div v-if="isPaginationVisible" class="gl--flex-center">
+ <gl-keyset-pagination
+ :start-cursor="startCursor"
+ :end-cursor="endCursor"
+ :has-previous-page="hasPreviousPage"
+ :has-next-page="hasNextPage"
+ :prev-text="$options.i18n.previousPageButtonLabel"
+ :next-text="$options.i18n.nextPageButtonLabel"
+ :prev-button-link="previousPageLink"
+ :next-button-link="nextPageLink"
+ />
+ </div>
+</template>
diff --git a/app/controllers/projects/ml/experiments_controller.rb b/app/controllers/projects/ml/experiments_controller.rb
index 5e7e6828a3b..20f5b39bac7 100644
--- a/app/controllers/projects/ml/experiments_controller.rb
+++ b/app/controllers/projects/ml/experiments_controller.rb
@@ -25,17 +25,13 @@ module Projects
.transform_keys(&:underscore)
.permit(:name, :order_by, :sort, :order_by_type)
- @candidates = CandidateFinder.new(@experiment, find_params).execute
+ paginator = CandidateFinder
+ .new(@experiment, find_params)
+ .execute
+ .keyset_paginate(cursor: params[:cursor], per_page: MAX_CANDIDATES_PER_PAGE)
- page = [params[:page].to_i, 1].max
-
- @candidates, @pagination_info = paginate_candidates(@candidates, page, MAX_CANDIDATES_PER_PAGE)
-
- return if @pagination_info[:total_pages] == 0
-
- redirect_to(url_for(safe_params.merge(page: @pagination_info[:total_pages]))) if @pagination_info[:out_of_range]
-
- @candidates.each(&:artifact_lazy)
+ @candidates = paginator.records.each(&:artifact_lazy)
+ @page_info = page_info(paginator)
end
private
diff --git a/app/helpers/projects/ml/experiments_helper.rb b/app/helpers/projects/ml/experiments_helper.rb
index afb6f9c99e3..9f41db2f8b9 100644
--- a/app/helpers/projects/ml/experiments_helper.rb
+++ b/app/helpers/projects/ml/experiments_helper.rb
@@ -42,24 +42,18 @@ module Projects
Gitlab::Json.generate(data)
end
- def paginate_candidates(candidates, page, max_per_page)
- return [candidates, nil] unless candidates
-
- candidates = candidates.page(page).per(max_per_page)
-
- pagination_info = {
- page: page,
- is_last_page: candidates.last_page?,
- per_page: max_per_page,
- total_items: candidates.total_count,
- total_pages: candidates.total_pages,
- out_of_range: candidates.out_of_range?
+ def page_info(paginator)
+ {
+ has_next_page: paginator.has_next_page?,
+ has_previous_page: paginator.has_previous_page?,
+ start_cursor: paginator.cursor_for_previous_page,
+ end_cursor: paginator.cursor_for_next_page
}
-
- [candidates, pagination_info]
end
- private
+ def formatted_page_info(page_info)
+ Gitlab::Json.generate(page_info)
+ end
def link_to_artifact(candidate)
artifact = candidate.artifact
diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb
index 16e49305fde..f973b00c568 100644
--- a/app/models/ml/candidate.rb
+++ b/app/models/ml/candidate.rb
@@ -24,8 +24,26 @@ module Ml
scope :by_name, ->(name) { where("ml_candidates.name LIKE ?", "%#{sanitize_sql_like(name)}%") } # rubocop:disable GitlabSecurity/SqlInjection
scope :order_by_metric, ->(metric, direction) do
subquery = Ml::CandidateMetric.latest.where(name: metric)
+ column_expression = Arel::Table.new('latest')[:value]
+ metric_order_expression = direction.to_sym == :desc ? column_expression.desc : column_expression.asc
+
joins("INNER JOIN (#{subquery.to_sql}) latest ON latest.candidate_id = ml_candidates.id")
- .order("latest.value #{direction}, ml_candidates.id DESC")
+ .select("ml_candidates.*", "latest.value as metric_value")
+ .order(
+ Gitlab::Pagination::Keyset::Order.build(
+ [
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'metric_value',
+ order_expression: metric_order_expression,
+ nullable: :nulls_last,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'id',
+ order_expression: arel_table[:id].desc
+ )
+ ])
+ )
end
delegate :project_id, :project, to: :experiment
diff --git a/app/views/projects/ml/experiments/show.html.haml b/app/views/projects/ml/experiments/show.html.haml
index c938c804dcf..4433d1fafe9 100644
--- a/app/views/projects/ml/experiments/show.html.haml
+++ b/app/views/projects/ml/experiments/show.html.haml
@@ -4,6 +4,7 @@
- items = candidates_table_items(@candidates)
- metrics = unique_logged_names(@candidates, &:latest_metrics)
- params = unique_logged_names(@candidates, &:params)
+- page_info = formatted_page_info(@page_info)
.page-title-holder.d-flex.align-items-center
%h1.page-title.gl-font-size-h-display= @experiment.name
@@ -12,5 +13,5 @@
candidates: items,
metrics: metrics,
params: params,
- pagination: @pagination_info.to_json
+ page_info: page_info
} }
diff --git a/db/docs/wiki_repository_states.yml b/db/docs/wiki_repository_states.yml
new file mode 100644
index 00000000000..7f579b9098e
--- /dev/null
+++ b/db/docs/wiki_repository_states.yml
@@ -0,0 +1,10 @@
+---
+table_name: wiki_repository_states
+classes:
+- Geo::WikiRepositoryState
+feature_categories:
+- geo_replication
+description: Separate table for project wikis containing Geo verification metadata.
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109033
+milestone: '15.9'
+gitlab_schema: gitlab_main
diff --git a/db/migrate/20230109232316_create_wiki_repository_states.rb b/db/migrate/20230109232316_create_wiki_repository_states.rb
new file mode 100644
index 00000000000..61afc18c0f6
--- /dev/null
+++ b/db/migrate/20230109232316_create_wiki_repository_states.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+class CreateWikiRepositoryStates < Gitlab::Database::Migration[2.0]
+ VERIFICATION_STATE_INDEX_NAME = "index_wiki_repository_states_on_verification_state"
+ PENDING_VERIFICATION_INDEX_NAME = "index_wiki_repository_states_pending_verification"
+ FAILED_VERIFICATION_INDEX_NAME = "index_wiki_repository_states_failed_verification"
+ NEEDS_VERIFICATION_INDEX_NAME = "index_wiki_repository_states_needs_verification"
+
+ enable_lock_retries!
+
+ def up
+ create_table :wiki_repository_states do |t|
+ t.datetime_with_timezone :verification_started_at
+ t.datetime_with_timezone :verification_retry_at
+ t.datetime_with_timezone :verified_at
+ t.references :project_wiki_repository, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+ t.integer :verification_state, default: 0, limit: 2, null: false
+ t.integer :verification_retry_count, limit: 2
+ t.binary :verification_checksum, using: 'verification_checksum::bytea'
+ t.text :verification_failure, limit: 255
+
+ t.index :verification_state,
+ name: VERIFICATION_STATE_INDEX_NAME
+
+ t.index :verified_at,
+ where: "(verification_state = 0)",
+ order: { verified_at: 'ASC NULLS FIRST' },
+ name: PENDING_VERIFICATION_INDEX_NAME
+
+ t.index :verification_retry_at,
+ where: "(verification_state = 3)",
+ order: { verification_retry_at: 'ASC NULLS FIRST' },
+ name: FAILED_VERIFICATION_INDEX_NAME
+
+ t.index :verification_state,
+ where: "(verification_state = 0 OR verification_state = 3)",
+ name: NEEDS_VERIFICATION_INDEX_NAME
+ end
+ end
+
+ def down
+ drop_table :wiki_repository_states
+ end
+end
diff --git a/db/schema_migrations/20230109232316 b/db/schema_migrations/20230109232316
new file mode 100644
index 00000000000..813483d4496
--- /dev/null
+++ b/db/schema_migrations/20230109232316
@@ -0,0 +1 @@
+94981a0226e10e8f1c711e5b3e110486c019595b822c5d8bf728285233ebd22b \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 9ecd0e460fd..c0617ec779b 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -23696,6 +23696,28 @@ CREATE SEQUENCE wiki_page_slugs_id_seq
ALTER SEQUENCE wiki_page_slugs_id_seq OWNED BY wiki_page_slugs.id;
+CREATE TABLE wiki_repository_states (
+ id bigint NOT NULL,
+ verification_started_at timestamp with time zone,
+ verification_retry_at timestamp with time zone,
+ verified_at timestamp with time zone,
+ project_wiki_repository_id bigint NOT NULL,
+ verification_state smallint DEFAULT 0 NOT NULL,
+ verification_retry_count smallint,
+ verification_checksum bytea,
+ verification_failure text,
+ CONSTRAINT check_2933ff60dc CHECK ((char_length(verification_failure) <= 255))
+);
+
+CREATE SEQUENCE wiki_repository_states_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE wiki_repository_states_id_seq OWNED BY wiki_repository_states.id;
+
CREATE TABLE work_item_hierarchy_restrictions (
id bigint NOT NULL,
parent_type_id bigint NOT NULL,
@@ -24954,6 +24976,8 @@ ALTER TABLE ONLY wiki_page_meta ALTER COLUMN id SET DEFAULT nextval('wiki_page_m
ALTER TABLE ONLY wiki_page_slugs ALTER COLUMN id SET DEFAULT nextval('wiki_page_slugs_id_seq'::regclass);
+ALTER TABLE ONLY wiki_repository_states ALTER COLUMN id SET DEFAULT nextval('wiki_repository_states_id_seq'::regclass);
+
ALTER TABLE ONLY work_item_hierarchy_restrictions ALTER COLUMN id SET DEFAULT nextval('work_item_hierarchy_restrictions_id_seq'::regclass);
ALTER TABLE ONLY work_item_parent_links ALTER COLUMN id SET DEFAULT nextval('work_item_parent_links_id_seq'::regclass);
@@ -27413,6 +27437,9 @@ ALTER TABLE ONLY wiki_page_meta
ALTER TABLE ONLY wiki_page_slugs
ADD CONSTRAINT wiki_page_slugs_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY wiki_repository_states
+ ADD CONSTRAINT wiki_repository_states_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY work_item_hierarchy_restrictions
ADD CONSTRAINT work_item_hierarchy_restrictions_pkey PRIMARY KEY (id);
@@ -31854,6 +31881,16 @@ CREATE UNIQUE INDEX index_wiki_page_slugs_on_slug_and_wiki_page_meta_id ON wiki_
CREATE INDEX index_wiki_page_slugs_on_wiki_page_meta_id ON wiki_page_slugs USING btree (wiki_page_meta_id);
+CREATE INDEX index_wiki_repository_states_failed_verification ON wiki_repository_states USING btree (verification_retry_at NULLS FIRST) WHERE (verification_state = 3);
+
+CREATE INDEX index_wiki_repository_states_needs_verification ON wiki_repository_states USING btree (verification_state) WHERE ((verification_state = 0) OR (verification_state = 3));
+
+CREATE UNIQUE INDEX index_wiki_repository_states_on_project_wiki_repository_id ON wiki_repository_states USING btree (project_wiki_repository_id);
+
+CREATE INDEX index_wiki_repository_states_on_verification_state ON wiki_repository_states USING btree (verification_state);
+
+CREATE INDEX index_wiki_repository_states_pending_verification ON wiki_repository_states USING btree (verified_at NULLS FIRST) WHERE (verification_state = 0);
+
CREATE INDEX index_work_item_hierarchy_restrictions_on_child_type_id ON work_item_hierarchy_restrictions USING btree (child_type_id);
CREATE UNIQUE INDEX index_work_item_hierarchy_restrictions_on_parent_and_child ON work_item_hierarchy_restrictions USING btree (parent_type_id, child_type_id);
@@ -35483,6 +35520,9 @@ ALTER TABLE ONLY ci_pipeline_artifacts
ALTER TABLE ONLY merge_request_user_mentions
ADD CONSTRAINT fk_rails_aa1b2961b1 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;
+ALTER TABLE ONLY wiki_repository_states
+ ADD CONSTRAINT fk_rails_aa2f8a61ba FOREIGN KEY (project_wiki_repository_id) REFERENCES project_wiki_repositories(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY x509_commit_signatures
ADD CONSTRAINT fk_rails_ab07452314 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
diff --git a/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js b/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js
index a8637bc35ce..f307d2c5a58 100644
--- a/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js
+++ b/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js
@@ -1,27 +1,34 @@
-import { GlAlert, GlPagination, GlTable, GlLink } from '@gitlab/ui';
+import { GlAlert, GlTable, GlLink } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import MlExperiment from '~/ml/experiment_tracking/components/ml_experiment.vue';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+import Pagination from '~/vue_shared/components/incubation/pagination.vue';
import setWindowLocation from 'helpers/set_window_location_helper';
import * as urlHelpers from '~/lib/utils/url_utility';
describe('MlExperiment', () => {
let wrapper;
+ const startCursor = 'eyJpZCI6IjE2In0';
+ const defaultPageInfo = {
+ startCursor,
+ endCursor: 'eyJpZCI6IjIifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ };
+
const createWrapper = (
candidates = [],
metricNames = [],
paramNames = [],
- pagination = { page: 1, isLastPage: false, per_page: 2, totalItems: 0 },
+ pageInfo = defaultPageInfo,
) => {
wrapper = mountExtended(MlExperiment, {
- provide: { candidates, metricNames, paramNames, pagination },
+ provide: { candidates, metricNames, paramNames, pageInfo },
});
};
- const defaultPagination = { page: 1, isLastPage: false, per_page: 2, totalItems: 5 };
-
const candidates = [
{
rmse: 1,
@@ -66,12 +73,12 @@ describe('MlExperiment', () => {
},
];
- const createWrapperWithCandidates = (pagination = defaultPagination) => {
- createWrapper(candidates, ['rmse', 'auc', 'mae'], ['l1_ratio'], pagination);
+ const createWrapperWithCandidates = (pageInfo = defaultPageInfo) => {
+ createWrapper(candidates, ['rmse', 'auc', 'mae'], ['l1_ratio'], pageInfo);
};
const findAlert = () => wrapper.findComponent(GlAlert);
- const findPagination = () => wrapper.findComponent(GlPagination);
+ const findPagination = () => wrapper.findComponent(Pagination);
const findEmptyState = () => wrapper.findByText('No candidates to display');
const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
const findTable = () => wrapper.findComponent(GlTable);
@@ -119,30 +126,6 @@ describe('MlExperiment', () => {
});
});
- describe('generateLink', () => {
- it('generates the correct url', () => {
- setWindowLocation(
- 'https://blah.com/?name=query&orderBy=name&orderByType=column&sort=asc&page=1',
- );
-
- createWrapperWithCandidates();
-
- expect(findPagination().props('linkGen')(2)).toBe(
- 'https://blah.com/?name=query&orderBy=name&orderByType=column&sort=asc&page=2',
- );
- });
-
- it('generates the correct url when no name', () => {
- setWindowLocation('https://blah.com/?orderBy=auc&orderByType=metric&sort=asc');
-
- createWrapperWithCandidates();
-
- expect(findPagination().props('linkGen')(2)).toBe(
- 'https://blah.com/?orderBy=auc&orderByType=metric&sort=asc&page=2',
- );
- });
- });
-
describe('Search', () => {
it('shows search box', () => {
createWrapper();
@@ -194,30 +177,30 @@ describe('MlExperiment', () => {
createWrapper();
});
- it('On submit, reloads to correct page', () => {
+ it('On submit, resets the cursor and reloads to correct page', () => {
findRegistrySearch().vm.$emit('filter:submit');
expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
expect(urlHelpers.visitUrl).toHaveBeenCalledWith(
- 'https://blah.com/?name=query&orderBy=name&orderByType=column&sort=asc&page=1',
+ 'https://blah.com/?name=query&orderBy=name&orderByType=column&sort=asc',
);
});
- it('On sorting changed, reloads to correct page', () => {
+ it('On sorting changed, resets cursor and reloads to correct page', () => {
findRegistrySearch().vm.$emit('sorting:changed', { orderBy: 'created_at' });
expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
expect(urlHelpers.visitUrl).toHaveBeenCalledWith(
- 'https://blah.com/?name=query&orderBy=created_at&orderByType=column&sort=asc&page=1',
+ 'https://blah.com/?name=query&orderBy=created_at&orderByType=column&sort=asc',
);
});
- it('On sorting changed and is metric, reloads to correct page', () => {
+ it('On sorting changed and is metric, resets cursor and reloads to correct page', () => {
findRegistrySearch().vm.$emit('sorting:changed', { orderBy: 'metric.auc' });
expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
expect(urlHelpers.visitUrl).toHaveBeenCalledWith(
- 'https://blah.com/?name=query&orderBy=auc&orderByType=metric&sort=asc&page=1',
+ 'https://blah.com/?name=query&orderBy=auc&orderByType=metric&sort=asc',
);
});
@@ -226,47 +209,25 @@ describe('MlExperiment', () => {
expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
expect(urlHelpers.visitUrl).toHaveBeenCalledWith(
- 'https://blah.com/?name=query&orderBy=name&orderByType=column&sort=desc&page=1',
+ 'https://blah.com/?name=query&orderBy=name&orderByType=column&sort=desc',
);
});
});
});
- describe('with candidates', () => {
- describe('Pagination behaviour', () => {
- beforeEach(() => {
- createWrapperWithCandidates();
- });
-
- it('should show', () => {
- expect(findPagination().exists()).toBe(true);
- });
-
- it('should get the page number from the URL', () => {
- createWrapperWithCandidates({ ...defaultPagination, page: 2 });
-
- expect(findPagination().props().value).toBe(2);
- });
-
- it('should not have a prevPage if the page is 1', () => {
- expect(findPagination().props().prevPage).toBe(null);
- });
-
- it('should set the prevPage to 1 if the page is 2', () => {
- createWrapperWithCandidates({ ...defaultPagination, page: 2 });
-
- expect(findPagination().props().prevPage).toBe(1);
- });
+ describe('Pagination behaviour', () => {
+ beforeEach(() => {
+ createWrapperWithCandidates();
+ });
- it('should not have a nextPage if isLastPage is true', async () => {
- createWrapperWithCandidates({ ...defaultPagination, isLastPage: true });
+ it('should show', () => {
+ expect(findPagination().exists()).toBe(true);
+ });
- expect(findPagination().props().nextPage).toBe(null);
- });
+ it('Passes pagination to pagination component', () => {
+ createWrapperWithCandidates();
- it('should set the nextPage to 2 if the page is 1', () => {
- expect(findPagination().props().nextPage).toBe(2);
- });
+ expect(findPagination().props('startCursor')).toBe(startCursor);
});
});
diff --git a/spec/frontend/vue_shared/components/incubation/pagination_spec.js b/spec/frontend/vue_shared/components/incubation/pagination_spec.js
new file mode 100644
index 00000000000..a621e60c627
--- /dev/null
+++ b/spec/frontend/vue_shared/components/incubation/pagination_spec.js
@@ -0,0 +1,76 @@
+import { GlKeysetPagination } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import Pagination from '~/vue_shared/components/incubation/pagination.vue';
+
+describe('~/vue_shared/incubation/components/pagination.vue', () => {
+ let wrapper;
+
+ const pageInfo = {
+ startCursor: 'eyJpZCI6IjE2In0',
+ endCursor: 'eyJpZCI6IjIifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ };
+
+ const findPagination = () => wrapper.findComponent(GlKeysetPagination);
+
+ const createWrapper = (pageInfoProp) => {
+ wrapper = mountExtended(Pagination, {
+ propsData: pageInfoProp,
+ });
+ };
+
+ describe('when neither next nor previous page exists', () => {
+ beforeEach(() => {
+ const emptyPageInfo = { ...pageInfo, hasPreviousPage: false, hasNextPage: false };
+
+ createWrapper(emptyPageInfo);
+ });
+
+ it('should not render pagination component', () => {
+ expect(wrapper.html()).toBe('');
+ });
+ });
+
+ describe('when Pagination is rendered for environment details page', () => {
+ beforeEach(() => {
+ createWrapper(pageInfo);
+ });
+
+ it('should pass correct props to keyset pagination', () => {
+ expect(findPagination().exists()).toBe(true);
+ expect(findPagination().props()).toEqual(expect.objectContaining(pageInfo));
+ });
+
+ describe.each([
+ {
+ testPageInfo: pageInfo,
+ expectedAfter: `cursor=${pageInfo.endCursor}`,
+ expectedBefore: `cursor=${pageInfo.startCursor}`,
+ },
+ {
+ testPageInfo: { ...pageInfo, hasNextPage: true, hasPreviousPage: false },
+ expectedAfter: `cursor=${pageInfo.endCursor}`,
+ expectedBefore: '',
+ },
+ {
+ testPageInfo: { ...pageInfo, hasNextPage: false, hasPreviousPage: true },
+ expectedAfter: '',
+ expectedBefore: `cursor=${pageInfo.startCursor}`,
+ },
+ ])(
+ 'button links generation for $testPageInfo',
+ ({ testPageInfo, expectedAfter, expectedBefore }) => {
+ beforeEach(() => {
+ createWrapper(testPageInfo);
+ });
+
+ it(`should have button links defined as ${expectedAfter || 'empty'} and
+ ${expectedBefore || 'empty'}`, () => {
+ expect(findPagination().props().prevButtonLink).toContain(expectedBefore);
+ expect(findPagination().props().nextButtonLink).toContain(expectedAfter);
+ });
+ },
+ );
+ });
+});
diff --git a/spec/helpers/projects/ml/experiments_helper_spec.rb b/spec/helpers/projects/ml/experiments_helper_spec.rb
index 283a3f73238..d9194d19a7f 100644
--- a/spec/helpers/projects/ml/experiments_helper_spec.rb
+++ b/spec/helpers/projects/ml/experiments_helper_spec.rb
@@ -110,67 +110,47 @@ RSpec.describe Projects::Ml::ExperimentsHelper, feature_category: :mlops do
end
end
- describe '#paginate_candidates' do
- let(:page) { 1 }
- let(:max_per_page) { 1 }
-
- subject { helper.paginate_candidates(experiment.candidates.order(:id), page, max_per_page) }
-
- it 'paginates' do
- expect(subject[1]).not_to be_nil
+ describe '#page_info' do
+ def paginator(cursor = nil)
+ experiment.candidates.keyset_paginate(cursor: cursor, per_page: 1)
end
- it 'only returns max_per_page elements' do
- expect(subject[0].size).to eq(1)
- end
+ let_it_be(:first_page) { paginator }
+ let_it_be(:second_page) { paginator(first_page.cursor_for_next_page) }
- it 'fetches the items on the first page' do
- expect(subject[0]).to eq([candidate0])
- end
+ let(:page) { nil }
- it 'creates the pagination info' do
- expect(subject[1]).to eq({
- page: 1,
- is_last_page: false,
- per_page: 1,
- total_items: 2,
- total_pages: 2,
- out_of_range: false
- })
- end
+ subject { helper.page_info(page) }
- context 'when not the first page' do
- let(:page) { 2 }
+ context 'when is first page' do
+ let(:page) { first_page }
- it 'fetches the right page' do
- expect(subject[0]).to eq([candidate1])
- end
-
- it 'creates the pagination info' do
- expect(subject[1]).to eq({
- page: 2,
- is_last_page: true,
- per_page: 1,
- total_items: 2,
- total_pages: 2,
- out_of_range: false
+ it 'generates the correct page_info' do
+ is_expected.to include({
+ has_next_page: true,
+ has_previous_page: false,
+ start_cursor: nil
})
end
end
- context 'when out of bounds' do
- let(:page) { 3 }
-
- it 'creates the pagination info' do
- expect(subject[1]).to eq({
- page: page,
- is_last_page: false,
- per_page: 1,
- total_items: 2,
- total_pages: 2,
- out_of_range: true
+ context 'when is last page' do
+ let(:page) { second_page }
+
+ it 'generates the correct page_info' do
+ is_expected.to include({
+ has_next_page: false,
+ has_previous_page: true,
+ start_cursor: second_page.cursor_for_previous_page,
+ end_cursor: nil
})
end
end
end
+
+ describe '#formatted_page_info' do
+ it 'formats to json' do
+ expect(helper.formatted_page_info({ a: 1, b: 'c' })).to eq("{\"a\":1,\"b\":\"c\"}")
+ end
+ end
end
diff --git a/spec/requests/projects/ml/experiments_controller_spec.rb b/spec/requests/projects/ml/experiments_controller_spec.rb
index a1fd07b0a65..474194cf1d4 100644
--- a/spec/requests/projects/ml/experiments_controller_spec.rb
+++ b/spec/requests/projects/ml/experiments_controller_spec.rb
@@ -75,35 +75,58 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
end
describe 'pagination' do
- let_it_be(:candidates) { create_list(:ml_candidates, 5, experiment: experiment) }
+ let_it_be(:candidates) do
+ create_list(:ml_candidates, 5, experiment: experiment).tap do |c|
+ c.first.metrics.create!(name: 'metric1', value: 0.3)
+ c[1].metrics.create!(name: 'metric1', value: 0.2)
+ c.last.metrics.create!(name: 'metric1', value: 0.6)
+ end
+ end
+
+ let(:params) { basic_params.merge(id: experiment.iid) }
before do
stub_const("Projects::Ml::ExperimentsController::MAX_CANDIDATES_PER_PAGE", 2)
- candidates
show_experiment
end
- context 'when out of bounds' do
- let(:params) { basic_params.merge(id: experiment.iid, page: 10000) }
+ it 'fetches only MAX_CANDIDATES_PER_PAGE candidates' do
+ expect(assigns(:candidates).size).to eq(2)
+ end
- it 'redirects to last page' do
- last_page = (experiment.candidates.size + 1) / 2
+ it 'paginates' do
+ received = assigns(:page_info)
- expect(response).to redirect_to(project_ml_experiment_path(project, experiment.iid, page: last_page))
- end
+ expect(received).to include({
+ has_next_page: true,
+ has_previous_page: false,
+ start_cursor: nil
+ })
end
- context 'when bad page' do
- let(:params) { basic_params.merge(id: experiment.iid, page: 's') }
+ context 'when order by metric' do
+ let(:params) do
+ {
+ order_by: "metric1",
+ order_by_type: "metric",
+ sort: "desc"
+ }
+ end
+
+ it 'paginates', :aggregate_failures do
+ page = assigns(:candidates)
+
+ expect(page.first).to eq(candidates.last)
+ expect(page.last).to eq(candidates.first)
+
+ new_params = params.merge(cursor: assigns(:page_info)[:end_cursor])
+
+ show_experiment(new_params)
+
+ new_page = assigns(:candidates)
- it 'uses first page' do
- expect(assigns(:pagination_info)).to include(
- page: 1,
- is_last_page: false,
- per_page: 2,
- total_items: experiment.candidates&.size
- )
+ expect(new_page.first).to eq(candidates[1])
end
end
end
@@ -151,8 +174,8 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
private
- def show_experiment
- get project_ml_experiment_path(project, experiment.iid), params: params
+ def show_experiment(new_params = nil)
+ get project_ml_experiment_path(project, experiment.iid), params: new_params || params
end
def list_experiments