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:
-rw-r--r--.rubocop_todo/rails/include_url_helper.yml1
-rw-r--r--app/assets/javascripts/jobs/components/table/constants.js10
-rw-r--r--app/assets/javascripts/jobs/components/table/graphql/cache_config.js29
-rw-r--r--app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql15
-rw-r--r--app/assets/javascripts/jobs/components/table/index.js8
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table_app.vue67
-rw-r--r--app/controllers/search_controller.rb1
-rw-r--r--app/models/application_record.rb6
-rw-r--r--app/presenters/gitlab/blame_presenter.rb9
-rw-r--r--doc/administration/job_artifacts.md2
-rw-r--r--doc/administration/troubleshooting/img/AzureAD-scim_provisioning.pngbin539414 -> 80244 bytes
-rw-r--r--doc/api/alert_management_alerts.md60
-rw-r--r--doc/architecture/blueprints/runner_scaling/gitlab-autoscaling-overview.pngbin94088 -> 37761 bytes
-rw-r--r--doc/development/value_stream_analytics/img/object_hierarchy_example_V14_10.pngbin55849 -> 20826 bytes
-rw-r--r--doc/user/application_security/policies/img/scan_result_policy_yaml_mode_v14_6.pngbin76484 -> 19583 bytes
-rw-r--r--doc/user/group/planning_hierarchy/img/epic-view-ancestors-in-sidebar_v14_6.pngbin24780 -> 5010 bytes
-rw-r--r--doc/user/group/planning_hierarchy/img/issue-view-parent-epic-in-sidebar_v14_6.pngbin25077 -> 4994 bytes
-rw-r--r--doc/user/group/value_stream_analytics/img/vsa_stage_table_v14_7.pngbin242008 -> 79595 bytes
-rw-r--r--doc/user/profile/img/personal_readme_setup_v14_5.pngbin26192 -> 10328 bytes
-rw-r--r--doc/user/project/repository/forking_workflow.md4
-rw-r--r--doc/user/project/settings/import_export.md6
-rw-r--r--doc/user/search/img/code_search.pngbin113383 -> 35763 bytes
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb2
-rw-r--r--locale/gitlab.pot3
-rw-r--r--spec/frontend/jobs/components/table/graphql/cache_config_spec.js51
-rw-r--r--spec/frontend/jobs/components/table/job_table_app_spec.js59
-rw-r--r--spec/frontend/jobs/mock_data.js79
-rw-r--r--spec/services/groups/destroy_service_spec.rb2
-rw-r--r--spec/support/helpers/database_connection_helpers.rb11
-rw-r--r--workhorse/internal/upstream/routes.go5
-rw-r--r--workhorse/upload_test.go1
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
index b8edcfa31c2..b2c385151a6 100644
--- a/doc/administration/troubleshooting/img/AzureAD-scim_provisioning.png
+++ b/doc/administration/troubleshooting/img/AzureAD-scim_provisioning.png
Binary files differ
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
index c3ba615784f..2c8c753cddd 100644
--- a/doc/architecture/blueprints/runner_scaling/gitlab-autoscaling-overview.png
+++ b/doc/architecture/blueprints/runner_scaling/gitlab-autoscaling-overview.png
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
diff --git a/doc/user/profile/img/personal_readme_setup_v14_5.png b/doc/user/profile/img/personal_readme_setup_v14_5.png
index 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
Binary files differ
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
index 7c62bb6921b..a6658bfcd66 100644
--- a/doc/user/search/img/code_search.png
+++ b/doc/user/search/img/code_search.png
Binary files differ
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},