diff options
31 files changed, 265 insertions, 166 deletions
diff --git a/.rubocop_todo/rails/include_url_helper.yml b/.rubocop_todo/rails/include_url_helper.yml index dcafeafb9f0..1d8f88e9be1 100644 --- a/.rubocop_todo/rails/include_url_helper.yml +++ b/.rubocop_todo/rails/include_url_helper.yml @@ -19,7 +19,6 @@ Rails/IncludeUrlHelper: - app/models/integrations/redmine.rb - app/models/integrations/webex_teams.rb - app/models/integrations/youtrack.rb - - app/presenters/gitlab/blame_presenter.rb - ee/app/models/integrations/github.rb - ee/spec/helpers/ee/projects/security/configuration_helper_spec.rb - ee/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb diff --git a/app/assets/javascripts/jobs/components/table/constants.js b/app/assets/javascripts/jobs/components/table/constants.js index 962979ba573..951d9324813 100644 --- a/app/assets/javascripts/jobs/components/table/constants.js +++ b/app/assets/javascripts/jobs/components/table/constants.js @@ -1,16 +1,6 @@ import { s__, __ } from '~/locale'; import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants'; -export const GRAPHQL_PAGE_SIZE = 30; - -export const initialPaginationState = { - currentPage: 1, - prevPageCursor: '', - nextPageCursor: '', - first: GRAPHQL_PAGE_SIZE, - last: null, -}; - /* Error constants */ export const POST_FAILURE = 'post_failure'; export const DEFAULT = 'default'; diff --git a/app/assets/javascripts/jobs/components/table/graphql/cache_config.js b/app/assets/javascripts/jobs/components/table/graphql/cache_config.js new file mode 100644 index 00000000000..846efdf21ee --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/graphql/cache_config.js @@ -0,0 +1,29 @@ +import { isEqual } from 'lodash'; + +export default { + typePolicies: { + Project: { + fields: { + jobs: { + keyArgs: false, + }, + }, + }, + CiJobConnection: { + merge(existing = {}, incoming, { args = {} }) { + let nodes; + + if (Object.keys(existing).length !== 0 && isEqual(existing?.statuses, args?.statuses)) { + nodes = [...existing.nodes, ...incoming.nodes]; + } else { + nodes = [...incoming.nodes]; + } + + return { + nodes, + statuses: Array.isArray(args.statuses) ? [...args.statuses] : args.statuses, + }; + }, + }, + }, +}; diff --git a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql index 88937185a8c..151e49af87e 100644 --- a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql +++ b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql @@ -1,25 +1,22 @@ -query getJobs( - $fullPath: ID! - $first: Int - $last: Int - $after: String - $before: String - $statuses: [CiJobStatus!] -) { +query getJobs($fullPath: ID!, $after: String, $statuses: [CiJobStatus!]) { project(fullPath: $fullPath) { id - jobs(after: $after, before: $before, first: $first, last: $last, statuses: $statuses) { + __typename + jobs(after: $after, first: 30, statuses: $statuses) { pageInfo { endCursor hasNextPage hasPreviousPage startCursor + __typename } nodes { + __typename artifacts { nodes { downloadPath fileType + __typename } } allowFailure diff --git a/app/assets/javascripts/jobs/components/table/index.js b/app/assets/javascripts/jobs/components/table/index.js index f24daf90815..1b9c7cdcfdd 100644 --- a/app/assets/javascripts/jobs/components/table/index.js +++ b/app/assets/javascripts/jobs/components/table/index.js @@ -4,12 +4,18 @@ import VueApollo from 'vue-apollo'; import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue'; import createDefaultClient from '~/lib/graphql'; import { parseBoolean } from '~/lib/utils/common_utils'; +import cacheConfig from './graphql/cache_config'; Vue.use(VueApollo); Vue.use(GlToast); const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), + defaultClient: createDefaultClient( + {}, + { + cacheConfig, + }, + ), }); export default (containerId = 'js-jobs-table') => { diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue index 81f42c1e293..864e322eecd 100644 --- a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue +++ b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue @@ -1,7 +1,6 @@ <script> -import { GlAlert, GlPagination, GlSkeletonLoader } from '@gitlab/ui'; +import { GlAlert, GlSkeletonLoader, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui'; import { __ } from '~/locale'; -import { GRAPHQL_PAGE_SIZE, initialPaginationState } from './constants'; import eventHub from './event_hub'; import GetJobs from './graphql/queries/get_jobs.query.graphql'; import JobsTable from './jobs_table.vue'; @@ -11,14 +10,16 @@ import JobsTableTabs from './jobs_table_tabs.vue'; export default { i18n: { errorMsg: __('There was an error fetching the jobs for your project.'), + loadingAriaLabel: __('Loading'), }, components: { GlAlert, - GlPagination, GlSkeletonLoader, JobsTable, JobsTableEmptyState, JobsTableTabs, + GlIntersectionObserver, + GlLoadingIcon, }, inject: { fullPath: { @@ -31,10 +32,6 @@ export default { variables() { return { fullPath: this.fullPath, - first: this.pagination.first, - last: this.pagination.last, - after: this.pagination.nextPageCursor, - before: this.pagination.prevPageCursor, }; }, update(data) { @@ -57,7 +54,7 @@ export default { hasError: false, isAlertDismissed: false, scope: null, - pagination: initialPaginationState, + firstLoad: true, }; }, computed: { @@ -67,14 +64,8 @@ export default { showEmptyState() { return this.jobs.list.length === 0 && !this.scope; }, - prevPage() { - return Math.max(this.pagination.currentPage - 1, 0); - }, - nextPage() { - return this.jobs.pageInfo?.hasNextPage ? this.pagination.currentPage + 1 : null; - }, - showPaginationControls() { - return Boolean(this.prevPage || this.nextPage) && !this.$apollo.loading; + hasNextPage() { + return this.jobs?.pageInfo?.hasNextPage; }, }, mounted() { @@ -88,26 +79,22 @@ export default { this.$apollo.queries.jobs.refetch({ statuses: this.scope }); }, fetchJobsByStatus(scope) { + this.firstLoad = true; + this.scope = scope; this.$apollo.queries.jobs.refetch({ statuses: scope }); }, - handlePageChange(page) { - const { startCursor, endCursor } = this.jobs.pageInfo; + fetchMoreJobs() { + this.firstLoad = false; - if (page > this.pagination.currentPage) { - this.pagination = { - ...initialPaginationState, - nextPageCursor: endCursor, - currentPage: page, - }; - } else { - this.pagination = { - last: GRAPHQL_PAGE_SIZE, - first: null, - prevPageCursor: startCursor, - currentPage: page, - }; + if (!this.$apollo.queries.jobs.loading) { + this.$apollo.queries.jobs.fetchMore({ + variables: { + fullPath: this.fullPath, + after: this.jobs?.pageInfo?.endCursor, + }, + }); } }, }, @@ -128,7 +115,7 @@ export default { <jobs-table-tabs @fetchJobsByStatus="fetchJobsByStatus" /> - <div v-if="$apollo.loading" class="gl-mt-5"> + <div v-if="$apollo.loading && firstLoad" class="gl-mt-5"> <gl-skeleton-loader :width="1248" :height="73"> <circle cx="748.031" cy="37.7193" r="15.0307" /> <circle cx="787.241" cy="37.7193" r="15.0307" /> @@ -149,14 +136,12 @@ export default { <jobs-table v-else :jobs="jobs.list" /> - <gl-pagination - v-if="showPaginationControls" - :value="pagination.currentPage" - :prev-page="prevPage" - :next-page="nextPage" - align="center" - class="gl-mt-3" - @input="handlePageChange" - /> + <gl-intersection-observer v-if="hasNextPage" @appear="fetchMoreJobs"> + <gl-loading-icon + v-if="$apollo.loading" + size="md" + :aria-label="$options.i18n.loadingAriaLabel" + /> + </gl-intersection-observer> </div> </template> diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index e38eeaed367..0c3d400875d 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -25,6 +25,7 @@ class SearchController < ApplicationController feature_category :global_search urgency :high, [:opensearch] + urgency :low, [:count] def show @project = search_service.project diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 06ff18ca409..c9c46250a6d 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -60,8 +60,10 @@ class ApplicationRecord < ActiveRecord::Base end # Start a new transaction with a shorter-than-usual statement timeout. This is - # currently one third of the default 15-second timeout - def self.with_fast_read_statement_timeout(timeout_ms = 5000) + # currently one third of the default 15-second timeout with a 500ms buffer + # to allow callers gracefully handling the errors to still complete within + # the 5s target duration of a low urgency request. + def self.with_fast_read_statement_timeout(timeout_ms = 4500) ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions connection.exec_query("SET LOCAL statement_timeout = #{timeout_ms}") diff --git a/app/presenters/gitlab/blame_presenter.rb b/app/presenters/gitlab/blame_presenter.rb index e9340a42e51..5dd2f3adda5 100644 --- a/app/presenters/gitlab/blame_presenter.rb +++ b/app/presenters/gitlab/blame_presenter.rb @@ -2,7 +2,6 @@ module Gitlab class BlamePresenter < Gitlab::View::Presenter::Simple - include ActionView::Helpers::UrlHelper include ActionView::Helpers::TranslationHelper include ActionView::Context include AvatarsHelper @@ -75,5 +74,13 @@ module Gitlab def project_duration @project_duration ||= age_map_duration(groups, project) end + + def link_to(*args, &block) + ActionController::Base.helpers.link_to(*args, &block) + end + + def mail_to(*args, &block) + ActionController::Base.helpers.mail_to(*args, &block) + end end end diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md index 8f824fcefb3..a4b53fd43b1 100644 --- a/doc/administration/job_artifacts.md +++ b/doc/administration/job_artifacts.md @@ -435,7 +435,7 @@ list = arts.order(size: :desc).limit(50).each do |art| end ``` -To change the number of projects listed, change the number in `limit(50)`. +To change the number of job artifacts listed, change the number in `limit(50)`. #### Delete job artifacts from jobs completed before a specific date diff --git a/doc/administration/troubleshooting/img/AzureAD-scim_provisioning.png b/doc/administration/troubleshooting/img/AzureAD-scim_provisioning.png Binary files differindex b8edcfa31c2..b2c385151a6 100644 --- a/doc/administration/troubleshooting/img/AzureAD-scim_provisioning.png +++ b/doc/administration/troubleshooting/img/AzureAD-scim_provisioning.png diff --git a/doc/api/alert_management_alerts.md b/doc/api/alert_management_alerts.md index ce27249bddc..8fde91dd7f2 100644 --- a/doc/api/alert_management_alerts.md +++ b/doc/api/alert_management_alerts.md @@ -11,6 +11,35 @@ This is the documentation of Alert Management Alerts API. NOTE: This API is limited to metric images. For more API endpoints please refer to the [GraphQL API](graphql/reference/index.md#alertmanagementalert). +## Upload metric image + +```plaintext +POST /projects/:id/alert_management_alerts/:alert_iid/metric_images +``` + +| Attribute | Type | Required | Description | +|-------------|---------|----------|--------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `alert_iid` | integer | yes | The internal ID of a project's alert. | + +```shell +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form 'file=@/path/to/file.png' \ +--form 'url=http://example.com' --form 'url_text=Example website' "https://gitlab.example.com/api/v4/projects/5/alert_management_alerts/93/metric_images" +``` + +Example response: + +```json +{ + "id": 17, + "created_at": "2020-11-12T20:07:58.156Z", + "filename": "sample_2054", + "file_path": "/uploads/-/system/alert_metric_image/file/17/sample_2054.png", + "url": "example.com/metric", + "url_text": "An example metric" +} +``` + ## List metric images ```plaintext @@ -48,3 +77,34 @@ Example response: } ] ``` + +## Update metric image + +```plaintext +PUT /projects/:id/alert_management_alerts/:alert_iid/metric_image/:image_id +``` + +| Attribute | Type | Required | Description | +|-------------|---------|----------|--------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `alert_iid` | integer | yes | The internal ID of a project's alert. | +| `image_id` | integer | yes | The ID of the image. | +| `url` | string | no | The URL to view more metrics information. | +| `url_text` | string | no | A description of the image or URL. | + +```shell +curl --header "PRIVATE-TOKEN: <your_access_token>" --request PUT --form 'url=http://example.com' --form 'url_text=Example website' "https://gitlab.example.com/api/v4/projects/5/alert_management_alerts/93/metric_images/1" +``` + +Example response: + +```json +{ + "id": 23, + "created_at": "2020-11-13T00:06:18.084Z", + "filename": "file.png", + "file_path": "/uploads/-/system/alert_metric_image/file/23/file.png", + "url": "http://example.com", + "url_text": "Example website" +} +``` diff --git a/doc/architecture/blueprints/runner_scaling/gitlab-autoscaling-overview.png b/doc/architecture/blueprints/runner_scaling/gitlab-autoscaling-overview.png Binary files differindex c3ba615784f..2c8c753cddd 100644 --- a/doc/architecture/blueprints/runner_scaling/gitlab-autoscaling-overview.png +++ b/doc/architecture/blueprints/runner_scaling/gitlab-autoscaling-overview.png diff --git a/doc/development/value_stream_analytics/img/object_hierarchy_example_V14_10.png b/doc/development/value_stream_analytics/img/object_hierarchy_example_V14_10.png Binary files differindex 0e3c760fa3c..9c4d4ae7718 100644 --- a/doc/development/value_stream_analytics/img/object_hierarchy_example_V14_10.png +++ b/doc/development/value_stream_analytics/img/object_hierarchy_example_V14_10.png diff --git a/doc/user/application_security/policies/img/scan_result_policy_yaml_mode_v14_6.png b/doc/user/application_security/policies/img/scan_result_policy_yaml_mode_v14_6.png Binary files differindex 57649c58d8b..e37e7ba336d 100644 --- a/doc/user/application_security/policies/img/scan_result_policy_yaml_mode_v14_6.png +++ b/doc/user/application_security/policies/img/scan_result_policy_yaml_mode_v14_6.png diff --git a/doc/user/group/planning_hierarchy/img/epic-view-ancestors-in-sidebar_v14_6.png b/doc/user/group/planning_hierarchy/img/epic-view-ancestors-in-sidebar_v14_6.png Binary files differindex 373b861239b..d4ba8acf9b9 100644 --- a/doc/user/group/planning_hierarchy/img/epic-view-ancestors-in-sidebar_v14_6.png +++ b/doc/user/group/planning_hierarchy/img/epic-view-ancestors-in-sidebar_v14_6.png diff --git a/doc/user/group/planning_hierarchy/img/issue-view-parent-epic-in-sidebar_v14_6.png b/doc/user/group/planning_hierarchy/img/issue-view-parent-epic-in-sidebar_v14_6.png Binary files differindex 95a5777674a..f3b6a80ea66 100644 --- a/doc/user/group/planning_hierarchy/img/issue-view-parent-epic-in-sidebar_v14_6.png +++ b/doc/user/group/planning_hierarchy/img/issue-view-parent-epic-in-sidebar_v14_6.png diff --git a/doc/user/group/value_stream_analytics/img/vsa_stage_table_v14_7.png b/doc/user/group/value_stream_analytics/img/vsa_stage_table_v14_7.png Binary files differindex c9074cbb5ea..7c3305792bb 100644 --- a/doc/user/group/value_stream_analytics/img/vsa_stage_table_v14_7.png +++ b/doc/user/group/value_stream_analytics/img/vsa_stage_table_v14_7.png diff --git a/doc/user/profile/img/personal_readme_setup_v14_5.png b/doc/user/profile/img/personal_readme_setup_v14_5.png Binary files differindex 92d8e0ec936..6a776506ac6 100644 --- a/doc/user/profile/img/personal_readme_setup_v14_5.png +++ b/doc/user/profile/img/personal_readme_setup_v14_5.png diff --git a/doc/user/project/repository/forking_workflow.md b/doc/user/project/repository/forking_workflow.md index ed0d94aa508..ddeef65a6b5 100644 --- a/doc/user/project/repository/forking_workflow.md +++ b/doc/user/project/repository/forking_workflow.md @@ -47,7 +47,7 @@ Without mirroring, to work locally you must use `git pull` to update your local with the upstream project, then push the changes back to your fork to update it. WARNING: -With mirroring, before approving a merge request, you are asked to sync. Because of this, automating it is recommended. +With mirroring, before approving a merge request, you are asked to sync. We recommend you automate it. Read more about [How to keep your fork up to date with its origin](https://about.gitlab.com/blog/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/). @@ -63,7 +63,7 @@ When creating a merge request, if the forked project's visibility is more restri ![Selecting branches](img/forking_workflow_branch_select.png) Then you can add labels, a milestone, and assign the merge request to someone who can review -your changes. Then click **Submit merge request** to conclude the process. When successfully merged, your +your changes. Then select **Submit merge request** to conclude the process. When successfully merged, your changes are added to the repository and branch you're merging into. ## Removing a fork relationship diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index 8cee567ae93..bb6ee720d74 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -229,7 +229,7 @@ and the exports between them are compatible. ## Related topics - [Project import/export API](../../../api/project_import_export.md) -- [Project import/export administration Rake tasks](../../../administration/raketasks/project_import_export.md) **(FREE SELF)** +- [Project import/export administration Rake tasks](../../../administration/raketasks/project_import_export.md) - [Group import/export](../../group/settings/import_export.md) - [Group import/export API](../../../api/group_import_export.md) @@ -351,8 +351,8 @@ Rather than attempting to push all changes at once, this workaround: git push -u origin ${COMMIT_SHA}:refs/heads/main done git push -u origin main - git push -u origin -—all - git push -u origin -—tags + git push -u origin --all + git push -u origin --tags ``` ### Manually execute export steps diff --git a/doc/user/search/img/code_search.png b/doc/user/search/img/code_search.png Binary files differindex 7c62bb6921b..a6658bfcd66 100644 --- a/doc/user/search/img/code_search.png +++ b/doc/user/search/img/code_search.png diff --git a/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb b/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb index a604f79dc41..a53da514df2 100644 --- a/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb +++ b/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb @@ -94,7 +94,7 @@ module Gitlab if schemas.many? message = "Cross-database data modification of '#{schemas.to_a.join(", ")}' were detected within " \ - "a transaction modifying the '#{all_tables.to_a.join(", ")}' tables." \ + "a transaction modifying the '#{all_tables.to_a.join(", ")}' tables. " \ "Please refer to https://docs.gitlab.com/ee/development/database/multiple_databases.html#removing-cross-database-transactions for details on how to resolve this exception." if schemas.any? { |s| s.to_s.start_with?("undefined") } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a7f227f89cc..73edf1d822f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -41693,6 +41693,9 @@ msgstr "" msgid "You are not authorized to update this scanner profile" msgstr "" +msgid "You are not authorized to upload metric images" +msgstr "" + msgid "You are now impersonating %{username}" msgstr "" diff --git a/spec/frontend/jobs/components/table/graphql/cache_config_spec.js b/spec/frontend/jobs/components/table/graphql/cache_config_spec.js new file mode 100644 index 00000000000..a387572839d --- /dev/null +++ b/spec/frontend/jobs/components/table/graphql/cache_config_spec.js @@ -0,0 +1,51 @@ +import cacheConfig from '~/jobs/components/table/graphql/cache_config'; +import { + CIJobConnectionExistingCache, + CIJobConnectionIncomingCache, + CIJobConnectionIncomingCacheRunningStatus, +} from '../../../mock_data'; + +const firstLoadArgs = { first: 3, statuses: 'PENDING' }; +const runningArgs = { first: 3, statuses: 'RUNNING' }; + +describe('jobs/components/table/graphql/cache_config', () => { + describe('when fetching data with the same statuses', () => { + it('should contain cache nodes and a status when merging caches on first load', () => { + const res = cacheConfig.typePolicies.CiJobConnection.merge({}, CIJobConnectionIncomingCache, { + args: firstLoadArgs, + }); + + expect(res.nodes).toHaveLength(CIJobConnectionIncomingCache.nodes.length); + expect(res.statuses).toBe('PENDING'); + }); + + it('should add to existing caches when merging caches after first load', () => { + const res = cacheConfig.typePolicies.CiJobConnection.merge( + CIJobConnectionExistingCache, + CIJobConnectionIncomingCache, + { + args: firstLoadArgs, + }, + ); + + expect(res.nodes).toHaveLength( + CIJobConnectionIncomingCache.nodes.length + CIJobConnectionExistingCache.nodes.length, + ); + }); + }); + + describe('when fetching data with different statuses', () => { + it('should reset cache when a cache already exists', () => { + const res = cacheConfig.typePolicies.CiJobConnection.merge( + CIJobConnectionExistingCache, + CIJobConnectionIncomingCacheRunningStatus, + { + args: runningArgs, + }, + ); + + expect(res.nodes).not.toEqual(CIJobConnectionExistingCache.nodes); + expect(res.nodes).toHaveLength(CIJobConnectionIncomingCacheRunningStatus.nodes.length); + }); + }); +}); diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/jobs/components/table/job_table_app_spec.js index 5ccd38af735..4d51624dfff 100644 --- a/spec/frontend/jobs/components/table/job_table_app_spec.js +++ b/spec/frontend/jobs/components/table/job_table_app_spec.js @@ -1,4 +1,4 @@ -import { GlSkeletonLoader, GlAlert, GlEmptyState, GlPagination } from '@gitlab/ui'; +import { GlSkeletonLoader, GlAlert, GlEmptyState, GlIntersectionObserver } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; @@ -8,12 +8,7 @@ import getJobsQuery from '~/jobs/components/table/graphql/queries/get_jobs.query import JobsTable from '~/jobs/components/table/jobs_table.vue'; import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue'; import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue'; -import { - mockJobsQueryResponse, - mockJobsQueryEmptyResponse, - mockJobsQueryResponseLastPage, - mockJobsQueryResponseFirstPage, -} from '../../mock_data'; +import { mockJobsQueryResponse, mockJobsQueryEmptyResponse } from '../../mock_data'; const projectPath = 'gitlab-org/gitlab'; Vue.use(VueApollo); @@ -30,10 +25,9 @@ describe('Job table app', () => { const findTabs = () => wrapper.findComponent(JobsTableTabs); const findAlert = () => wrapper.findComponent(GlAlert); const findEmptyState = () => wrapper.findComponent(GlEmptyState); - const findPagination = () => wrapper.findComponent(GlPagination); - const findPrevious = () => findPagination().findAll('.page-item').at(0); - const findNext = () => findPagination().findAll('.page-item').at(1); + const triggerInfiniteScroll = () => + wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear'); const createMockApolloProvider = (handler) => { const requestHandlers = [[getJobsQuery, handler]]; @@ -53,7 +47,7 @@ describe('Job table app', () => { }; }, provide: { - projectPath, + fullPath: projectPath, }, apolloProvider: createMockApolloProvider(handler), }); @@ -69,7 +63,6 @@ describe('Job table app', () => { expect(findSkeletonLoader().exists()).toBe(true); expect(findTable().exists()).toBe(false); - expect(findPagination().exists()).toBe(false); }); }); @@ -83,7 +76,6 @@ describe('Job table app', () => { it('should display the jobs table with data', () => { expect(findTable().exists()).toBe(true); expect(findSkeletonLoader().exists()).toBe(false); - expect(findPagination().exists()).toBe(true); }); it('should refetch jobs query on fetchJobsByStatus event', async () => { @@ -95,41 +87,24 @@ describe('Job table app', () => { expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1); }); - }); - describe('pagination', () => { - it('should disable the next page button on the last page', async () => { - createComponent({ - handler: jest.fn().mockResolvedValue(mockJobsQueryResponseLastPage), - mountFn: mount, - data: { - pagination: { currentPage: 3 }, - }, + describe('when infinite scrolling is triggered', () => { + beforeEach(() => { + triggerInfiniteScroll(); }); - await waitForPromises(); - - expect(findPrevious().exists()).toBe(true); - expect(findNext().exists()).toBe(true); - expect(findNext().classes('disabled')).toBe(true); - }); - - it('should disable the previous page button on the first page', async () => { - createComponent({ - handler: jest.fn().mockResolvedValue(mockJobsQueryResponseFirstPage), - mountFn: mount, - data: { - pagination: { - currentPage: 1, - }, - }, + it('does not display a skeleton loader', () => { + expect(findSkeletonLoader().exists()).toBe(false); }); - await waitForPromises(); + it('handles infinite scrolling by calling fetch more', async () => { + await waitForPromises(); - expect(findPrevious().exists()).toBe(true); - expect(findPrevious().classes('disabled')).toBe(true); - expect(findNext().exists()).toBe(true); + expect(successHandler).toHaveBeenCalledWith({ + after: 'eyJpZCI6IjIzMTcifQ', + fullPath: 'gitlab-org/gitlab', + }); + }); }); }); diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js index 2be78bac8a9..868dd13654c 100644 --- a/spec/frontend/jobs/mock_data.js +++ b/spec/frontend/jobs/mock_data.js @@ -1579,44 +1579,6 @@ export const mockJobsQueryResponse = { }, }; -export const mockJobsQueryResponseLastPage = { - data: { - project: { - id: '1', - jobs: { - ...mockJobsQueryResponse.data.project.jobs, - pageInfo: { - endCursor: 'eyJpZCI6IjIzMTcifQ', - hasNextPage: false, - hasPreviousPage: true, - startCursor: 'eyJpZCI6IjIzMzYifQ', - __typename: 'PageInfo', - }, - }, - __typename: 'Project', - }, - }, -}; - -export const mockJobsQueryResponseFirstPage = { - data: { - project: { - id: '1', - jobs: { - ...mockJobsQueryResponse.data.project.jobs, - pageInfo: { - endCursor: 'eyJpZCI6IjIzMTcifQ', - hasNextPage: true, - hasPreviousPage: false, - startCursor: 'eyJpZCI6IjIzMzYifQ', - __typename: 'PageInfo', - }, - }, - __typename: 'Project', - }, - }, -}; - export const mockJobsQueryEmptyResponse = { data: { project: { @@ -1910,3 +1872,44 @@ export const cannotPlayScheduledJob = { __typename: 'JobPermissions', }, }; + +export const CIJobConnectionIncomingCache = { + __typename: 'CiJobConnection', + pageInfo: { + __typename: 'PageInfo', + endCursor: 'eyJpZCI6IjIwNTEifQ', + hasNextPage: true, + hasPreviousPage: false, + startCursor: 'eyJpZCI6IjIxNzMifQ', + }, + nodes: [ + { __ref: 'CiJob:gid://gitlab/Ci::Build/2057' }, + { __ref: 'CiJob:gid://gitlab/Ci::Build/2056' }, + { __ref: 'CiJob:gid://gitlab/Ci::Build/2051' }, + ], +}; + +export const CIJobConnectionIncomingCacheRunningStatus = { + __typename: 'CiJobConnection', + pageInfo: { + __typename: 'PageInfo', + endCursor: 'eyJpZCI6IjIwNTEifQ', + hasNextPage: true, + hasPreviousPage: false, + startCursor: 'eyJpZCI6IjIxNzMifQ', + }, + nodes: [ + { __ref: 'CiJob:gid://gitlab/Ci::Build/2000' }, + { __ref: 'CiJob:gid://gitlab/Ci::Build/2001' }, + { __ref: 'CiJob:gid://gitlab/Ci::Build/2002' }, + ], +}; + +export const CIJobConnectionExistingCache = { + nodes: [ + { __ref: 'CiJob:gid://gitlab/Ci::Build/2057' }, + { __ref: 'CiJob:gid://gitlab/Ci::Build/2056' }, + { __ref: 'CiJob:gid://gitlab/Ci::Build/2051' }, + ], + statuses: 'PENDING', +}; diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index dd1335ec4e6..628943e40ff 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' RSpec.describe Groups::DestroyService do - include DatabaseConnectionHelpers - let!(:user) { create(:user) } let!(:group) { create(:group) } let!(:nested_group) { create(:group, parent: group) } diff --git a/spec/support/helpers/database_connection_helpers.rb b/spec/support/helpers/database_connection_helpers.rb deleted file mode 100644 index 10ea7b5de91..00000000000 --- a/spec/support/helpers/database_connection_helpers.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module DatabaseConnectionHelpers - def run_with_new_database_connection - pool = ActiveRecord::Base.connection_pool - conn = pool.checkout - yield conn - ensure - pool.checkin(conn) - end -end diff --git a/workhorse/internal/upstream/routes.go b/workhorse/internal/upstream/routes.go index b8089865ffe..a7c5af60d1a 100644 --- a/workhorse/internal/upstream/routes.go +++ b/workhorse/internal/upstream/routes.go @@ -318,9 +318,12 @@ func configureRoutes(u *upstream) { // Group Import via UI upload acceleration u.route("POST", importPattern+`gitlab_group`, upload.Multipart(api, signingProxy, preparers.uploads)), - // Metric image upload + // Issuable Metric image upload u.route("POST", apiProjectPattern+`issues/[0-9]+/metric_images\z`, upload.Multipart(api, signingProxy, preparers.uploads)), + // Alert Metric image upload + u.route("POST", apiProjectPattern+`alert_management_alerts/[0-9]+/metric_images\z`, upload.Multipart(api, signingProxy, preparers.uploads)), + // Requirements Import via UI upload acceleration u.route("POST", projectPattern+`requirements_management/requirements/import_csv`, upload.Multipart(api, signingProxy, preparers.uploads)), diff --git a/workhorse/upload_test.go b/workhorse/upload_test.go index 478cbdb1a44..180598ab260 100644 --- a/workhorse/upload_test.go +++ b/workhorse/upload_test.go @@ -141,6 +141,7 @@ func TestAcceleratedUpload(t *testing.T) { {"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/packages/pypi`, true}, {"POST", `/api/v4/projects/9001/issues/30/metric_images`, true}, {"POST", `/api/v4/projects/group%2Fproject/issues/30/metric_images`, true}, + {"POST", `/api/v4/projects/9001/alert_management_alerts/30/metric_images`, true}, {"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/issues/30/metric_images`, true}, {"POST", `/my/project/-/requirements_management/requirements/import_csv`, true}, {"POST", `/my/project/-/requirements_management/requirements/import_csv/`, true}, |