summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/monitoring/requests/index.js43
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js49
-rw-r--r--app/assets/javascripts/snippets/components/show.vue16
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_view.vue17
-rw-r--r--app/serializers/pipeline_serializer.rb1
-rw-r--r--app/views/notify/service_desk_new_note_email.html.haml2
-rw-r--r--app/views/notify/service_desk_new_note_email.text.erb4
-rw-r--r--app/views/notify/service_desk_thank_you_email.html.haml2
-rw-r--r--app/views/notify/service_desk_thank_you_email.text.erb4
-rw-r--r--app/views/shared/promotions/_promote_servicedesk.html.haml8
-rw-r--r--changelogs/unreleased/220799-clone0-button.yml5
-rw-r--r--changelogs/unreleased/id-preload-pipeline-reports.yml5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql2
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json2
-rw-r--r--locale/gitlab.pot21
-rw-r--r--qa/qa/page/component/snippet.rb2
-rw-r--r--spec/frontend/helpers/backoff_helper.js33
-rw-r--r--spec/frontend/monitoring/requests/index_spec.js149
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js57
-rw-r--r--spec/frontend/snippets/components/show_spec.js53
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js19
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb2
23 files changed, 352 insertions, 146 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 923a5429a57..be3ab66e03b 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-43e8389d447f471e889dd4521a13037f36d8a230
+00c7c2a5a1820397e65fbcff67cdd05bb961a40b
diff --git a/app/assets/javascripts/monitoring/requests/index.js b/app/assets/javascripts/monitoring/requests/index.js
new file mode 100644
index 00000000000..2be8048327f
--- /dev/null
+++ b/app/assets/javascripts/monitoring/requests/index.js
@@ -0,0 +1,43 @@
+import axios from '~/lib/utils/axios_utils';
+import statusCodes from '~/lib/utils/http_status';
+import { backOff } from '~/lib/utils/common_utils';
+import { PROMETHEUS_TIMEOUT } from '../constants';
+
+const backOffRequest = makeRequestCallback =>
+ backOff((next, stop) => {
+ makeRequestCallback()
+ .then(resp => {
+ if (resp.status === statusCodes.NO_CONTENT) {
+ next();
+ } else {
+ stop(resp);
+ }
+ })
+ .catch(stop);
+ }, PROMETHEUS_TIMEOUT);
+
+export const getDashboard = (dashboardEndpoint, params) =>
+ backOffRequest(() => axios.get(dashboardEndpoint, { params })).then(
+ axiosResponse => axiosResponse.data,
+ );
+
+export const getPrometheusQueryData = (prometheusEndpoint, params) =>
+ backOffRequest(() => axios.get(prometheusEndpoint, { params }))
+ .then(axiosResponse => axiosResponse.data)
+ .then(prometheusResponse => prometheusResponse.data)
+ .catch(error => {
+ // Prometheus returns errors in specific cases
+ // https://prometheus.io/docs/prometheus/latest/querying/api/#format-overview
+ const { response = {} } = error;
+ if (
+ response.status === statusCodes.BAD_REQUEST ||
+ response.status === statusCodes.UNPROCESSABLE_ENTITY ||
+ response.status === statusCodes.SERVICE_UNAVAILABLE
+ ) {
+ const { data } = response;
+ if (data?.status === 'error' && data?.error) {
+ throw new Error(data.error);
+ }
+ }
+ throw error;
+ });
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 05cbdcf8797..466d791f9b2 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -13,16 +13,11 @@ import trackDashboardLoad from '../monitoring_tracking_helper';
import getEnvironments from '../queries/getEnvironments.query.graphql';
import getAnnotations from '../queries/getAnnotations.query.graphql';
import getDashboardValidationWarnings from '../queries/getDashboardValidationWarnings.query.graphql';
-import statusCodes from '../../lib/utils/http_status';
-import { backOff, convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
+import { convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
import { s__, sprintf } from '../../locale';
+import { getDashboard, getPrometheusQueryData } from '../requests';
-import {
- PROMETHEUS_TIMEOUT,
- ENVIRONMENT_AVAILABLE_STATE,
- DEFAULT_DASHBOARD_PATH,
- VARIABLE_TYPES,
-} from '../constants';
+import { ENVIRONMENT_AVAILABLE_STATE, DEFAULT_DASHBOARD_PATH, VARIABLE_TYPES } from '../constants';
function prometheusMetricQueryParams(timeRange) {
const { start, end } = convertToFixedRange(timeRange);
@@ -38,31 +33,6 @@ function prometheusMetricQueryParams(timeRange) {
};
}
-function backOffRequest(makeRequestCallback) {
- return backOff((next, stop) => {
- makeRequestCallback()
- .then(resp => {
- if (resp.status === statusCodes.NO_CONTENT) {
- next();
- } else {
- stop(resp);
- }
- })
- .catch(stop);
- }, PROMETHEUS_TIMEOUT);
-}
-
-function getPrometheusQueryData(prometheusEndpoint, params) {
- return backOffRequest(() => axios.get(prometheusEndpoint, { params }))
- .then(res => res.data)
- .then(response => {
- if (response.status === 'error') {
- throw new Error(response.error);
- }
- return response.data;
- });
-}
-
// Setup
export const setGettingStartedEmptyState = ({ commit }) => {
@@ -126,8 +96,7 @@ export const fetchDashboard = ({ state, commit, dispatch, getters }) => {
params.dashboard = getters.fullDashboardPath;
}
- return backOffRequest(() => axios.get(state.dashboardEndpoint, { params }))
- .then(resp => resp.data)
+ return getDashboard(state.dashboardEndpoint, params)
.then(response => {
dispatch('receiveMetricsDashboardSuccess', { response });
/**
@@ -484,12 +453,10 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery
if (variable.type === VARIABLE_TYPES.metric_label_values) {
const { prometheusEndpointPath, label } = variable.options;
- const optionsRequest = backOffRequest(() =>
- axios.get(prometheusEndpointPath, {
- params: { start_time, end_time },
- }),
- )
- .then(({ data }) => data.data)
+ const optionsRequest = getPrometheusQueryData(prometheusEndpointPath, {
+ start_time,
+ end_time,
+ })
.then(data => {
commit(types.UPDATE_VARIABLE_METRIC_LABEL_VALUES, { variable, label, data });
})
diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue
index 0779e87e6b6..ae259b10648 100644
--- a/app/assets/javascripts/snippets/components/show.vue
+++ b/app/assets/javascripts/snippets/components/show.vue
@@ -3,6 +3,7 @@ import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import SnippetHeader from './snippet_header.vue';
import SnippetTitle from './snippet_title.vue';
import SnippetBlob from './snippet_blob_view.vue';
+import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
import { GlLoadingIcon } from '@gitlab/ui';
import { getSnippetMixin } from '../mixins/snippets';
@@ -15,12 +16,16 @@ export default {
SnippetTitle,
GlLoadingIcon,
SnippetBlob,
+ CloneDropdownButton,
},
mixins: [getSnippetMixin],
computed: {
embeddable() {
return this.snippet.visibilityLevel === SNIPPET_VISIBILITY_PUBLIC;
},
+ canBeCloned() {
+ return Boolean(this.snippet.sshUrlToRepo || this.snippet.httpUrlToRepo);
+ },
},
};
</script>
@@ -35,7 +40,16 @@ export default {
<template v-else>
<snippet-header :snippet="snippet" />
<snippet-title :snippet="snippet" />
- <blob-embeddable v-if="embeddable" class="gl-mb-5" :url="snippet.webUrl" />
+ <div class="gl-display-flex gl-justify-content-end gl-mb-5">
+ <blob-embeddable v-if="embeddable" class="gl-flex-fill-1" :url="snippet.webUrl" />
+ <clone-dropdown-button
+ v-if="canBeCloned"
+ class="gl-ml-3"
+ :ssh-link="snippet.sshUrlToRepo"
+ :http-link="snippet.httpUrlToRepo"
+ data-qa-selector="clone_button"
+ />
+ </div>
<div v-for="blob in blobs" :key="blob.path">
<snippet-blob :snippet="snippet" :blob="blob" />
</div>
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
index afd038eef58..63c95daae5d 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
@@ -1,7 +1,6 @@
<script>
import BlobHeader from '~/blob/components/blob_header.vue';
import BlobContent from '~/blob/components/blob_content.vue';
-import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
import GetBlobContent from '../queries/snippet.blob.content.query.graphql';
@@ -16,7 +15,6 @@ export default {
components: {
BlobHeader,
BlobContent,
- CloneDropdownButton,
},
apollo: {
blobContent: {
@@ -66,9 +64,6 @@ export default {
const { richViewer, simpleViewer } = this.blob;
return this.activeViewerType === RICH_BLOB_VIEWER ? richViewer : simpleViewer;
},
- canBeCloned() {
- return this.snippet.sshUrlToRepo || this.snippet.httpUrlToRepo;
- },
hasRenderError() {
return Boolean(this.viewer.renderError);
},
@@ -93,17 +88,7 @@ export default {
:active-viewer-type="viewer.type"
:has-render-error="hasRenderError"
@viewer-changed="switchViewer"
- >
- <template #actions>
- <clone-dropdown-button
- v-if="canBeCloned"
- class="gl-mr-3"
- :ssh-link="snippet.sshUrlToRepo"
- :http-link="snippet.httpUrlToRepo"
- data-qa-selector="clone_button"
- />
- </template>
- </blob-header>
+ />
<blob-content
:loading="isContentLoading"
:content="blobContent"
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index bfd6851647f..45c5a1d3e1c 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -42,6 +42,7 @@ class PipelineSerializer < BaseSerializer
[
:cancelable_statuses,
:latest_statuses_ordered_by_stage,
+ :latest_builds_report_results,
:manual_actions,
:retryable_builds,
:scheduled_actions,
diff --git a/app/views/notify/service_desk_new_note_email.html.haml b/app/views/notify/service_desk_new_note_email.html.haml
index 7c6be6688d0..824b4ab712e 100644
--- a/app/views/notify/service_desk_new_note_email.html.haml
+++ b/app/views/notify/service_desk_new_note_email.html.haml
@@ -1,5 +1,5 @@
- if Gitlab::CurrentSettings.email_author_in_body
%div
- #{link_to @note.author_name, user_url(@note.author)} wrote:
+ = _("%{author_link} wrote:").html_safe % { author_link: link_to(@note.author_name, user_url(@note.author)) }
%div
= markdown(@note.note, pipeline: :email, author: @note.author)
diff --git a/app/views/notify/service_desk_new_note_email.text.erb b/app/views/notify/service_desk_new_note_email.text.erb
index 208953a437d..79144fc1bf4 100644
--- a/app/views/notify/service_desk_new_note_email.text.erb
+++ b/app/views/notify/service_desk_new_note_email.text.erb
@@ -1,6 +1,6 @@
-New response for issue #<%= @issue.iid %>:
+<%= _("New response for issue #%{issue_iid}:") % { issue_iid: @issue.iid } %>
-Author: <%= sanitize_name(@note.author_name) %>
+<%= _("Author: %{author_name}") % { author_name: sanitize_name(@note.author_name) } %>
<%= @note.note %>
<%# EE-specific start %><%= render_if_exists 'layouts/mailer/additional_text'%><%# EE-specific end %>
diff --git a/app/views/notify/service_desk_thank_you_email.html.haml b/app/views/notify/service_desk_thank_you_email.html.haml
index a3407acd9ba..ee61db40f07 100644
--- a/app/views/notify/service_desk_thank_you_email.html.haml
+++ b/app/views/notify/service_desk_thank_you_email.html.haml
@@ -1,2 +1,2 @@
%p
- Thank you for your support request! We are tracking your request as ticket ##{@issue.iid}, and will respond as soon as we can.
+ = _("Thank you for your support request! We are tracking your request as ticket #%{issue_iid}, and will respond as soon as we can.") % { issue_iid: @issue.iid }
diff --git a/app/views/notify/service_desk_thank_you_email.text.erb b/app/views/notify/service_desk_thank_you_email.text.erb
index 8281607a4a8..8b52219c83b 100644
--- a/app/views/notify/service_desk_thank_you_email.text.erb
+++ b/app/views/notify/service_desk_thank_you_email.text.erb
@@ -1,6 +1,6 @@
-Thank you for your support request! We are tracking your request as ticket #<%= @issue.iid %>, and will respond as soon as we can.
+<%= _("Thank you for your support request! We are tracking your request as ticket #%{issue_iid}, and will respond as soon as we can.") % { issue_iid: @issue.iid } %>
-To unsubscribe from this issue, please paste the following link into your browser:
+<%= _("To unsubscribe from this issue, please paste the following link into your browser:") %>
<%= @unsubscribe_url %>
<%# EE-specific start %><%= render_if_exists 'layouts/mailer/additional_text' %><%# EE-specific end %>
diff --git a/app/views/shared/promotions/_promote_servicedesk.html.haml b/app/views/shared/promotions/_promote_servicedesk.html.haml
index f7f65c34c75..fbac5ef0bbd 100644
--- a/app/views/shared/promotions/_promote_servicedesk.html.haml
+++ b/app/views/shared/promotions/_promote_servicedesk.html.haml
@@ -5,9 +5,9 @@
.svg-container
= custom_icon('icon_service_desk')
.user-callout-copy
- -# haml-lint:disable NoPlainNodes
%h4
- Improve customer support with GitLab Service Desk.
+ = _("Improve customer support with GitLab Service Desk.")
%p
- GitLab Service Desk is a simple way to allow people to create issues in your GitLab instance without needing their own user account. It provides a unique email address for end users to create issues in a project, and replies can be sent either through the GitLab interface or by email. End users will only see the thread through email.
- = link_to 'Read more', help_page_path('user/project/service_desk.md'), target: '_blank'
+ = _("GitLab Service Desk is a simple way to allow people to create issues in your GitLab instance without needing their own user account. It provides a unique email address for end users to create issues in a project, and replies can be sent either through the GitLab interface or by email. End users will only see the thread through email.")
+ = link_to _('Read more'), help_page_path('user/project/service_desk.md'), target: '_blank'
+
diff --git a/changelogs/unreleased/220799-clone0-button.yml b/changelogs/unreleased/220799-clone0-button.yml
new file mode 100644
index 00000000000..8195b917981
--- /dev/null
+++ b/changelogs/unreleased/220799-clone0-button.yml
@@ -0,0 +1,5 @@
+---
+title: Move clone button out of blob header
+merge_request: 37696
+author:
+type: changed
diff --git a/changelogs/unreleased/id-preload-pipeline-reports.yml b/changelogs/unreleased/id-preload-pipeline-reports.yml
new file mode 100644
index 00000000000..121a6505bde
--- /dev/null
+++ b/changelogs/unreleased/id-preload-pipeline-reports.yml
@@ -0,0 +1,5 @@
+---
+title: Preload build report results for pipeline builds
+merge_request: 37582
+author:
+type: performance
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 5ab7b8f3e7f..65add93e0bc 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -4006,7 +4006,7 @@ input EpicAddIssueInput {
issueIid: String!
"""
- The project the issue belongs to
+ The full path of the project the issue belongs to
"""
projectPath: ID!
}
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 7b1aea7a321..732a8d4416e 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -11161,7 +11161,7 @@
},
{
"name": "projectPath",
- "description": "The project the issue belongs to",
+ "description": "The full path of the project the issue belongs to",
"type": {
"kind": "NON_NULL",
"name": null,
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5fcd236e3e1..899c066b2a4 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -292,6 +292,9 @@ msgstr[1] ""
msgid "%{actionText} & %{openOrClose} %{noteable}"
msgstr ""
+msgid "%{author_link} wrote:"
+msgstr ""
+
msgid "%{authorsName}'s thread"
msgstr ""
@@ -3432,6 +3435,9 @@ msgstr ""
msgid "Author"
msgstr ""
+msgid "Author: %{author_name}"
+msgstr ""
+
msgid "Authored %{timeago} by %{author}"
msgstr ""
@@ -11203,6 +11209,9 @@ msgstr ""
msgid "GitLab Issue"
msgstr ""
+msgid "GitLab Service Desk is a simple way to allow people to create issues in your GitLab instance without needing their own user account. It provides a unique email address for end users to create issues in a project, and replies can be sent either through the GitLab interface or by email. End users will only see the thread through email."
+msgstr ""
+
msgid "GitLab Shared Runners execute code of different projects on the same Runner unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is on GitLab.com)."
msgstr ""
@@ -12650,6 +12659,9 @@ msgstr ""
msgid "Improve Merge Requests and customer support with GitLab Enterprise Edition."
msgstr ""
+msgid "Improve customer support with GitLab Service Desk."
+msgstr ""
+
msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
msgstr ""
@@ -15773,6 +15785,9 @@ msgstr ""
msgid "New requirement"
msgstr ""
+msgid "New response for issue #%{issue_iid}:"
+msgstr ""
+
msgid "New runners registration token has been generated!"
msgstr ""
@@ -23464,6 +23479,9 @@ msgstr ""
msgid "Thank you for your report. A GitLab administrator will look into it shortly."
msgstr ""
+msgid "Thank you for your support request! We are tracking your request as ticket #%{issue_iid}, and will respond as soon as we can."
+msgstr ""
+
msgid "Thanks for your purchase!"
msgstr ""
@@ -25011,6 +25029,9 @@ msgstr ""
msgid "To start serving your jobs you can either add specific Runners to your project or use shared Runners"
msgstr ""
+msgid "To unsubscribe from this issue, please paste the following link into your browser:"
+msgstr ""
+
msgid "To view all %{scannedResourcesCount} scanned URLs, please download the CSV file"
msgstr ""
diff --git a/qa/qa/page/component/snippet.rb b/qa/qa/page/component/snippet.rb
index 443ec3c34d5..b84166ccefd 100644
--- a/qa/qa/page/component/snippet.rb
+++ b/qa/qa/page/component/snippet.rb
@@ -38,7 +38,7 @@ module QA
element :delete_snippet_button
end
- base.view 'app/assets/javascripts/snippets/components/snippet_blob_view.vue' do
+ base.view 'app/assets/javascripts/snippets/components/show.vue' do
element :clone_button
end
diff --git a/spec/frontend/helpers/backoff_helper.js b/spec/frontend/helpers/backoff_helper.js
new file mode 100644
index 00000000000..e5c0308d3fb
--- /dev/null
+++ b/spec/frontend/helpers/backoff_helper.js
@@ -0,0 +1,33 @@
+/**
+ * A mock version of a commonUtils `backOff` to test multiple
+ * retries.
+ *
+ * Usage:
+ *
+ * ```
+ * import * as commonUtils from '~/lib/utils/common_utils';
+ * import { backoffMockImplementation } from '../../helpers/backoff_helper';
+ *
+ * beforeEach(() => {
+ * // ...
+ * jest.spyOn(commonUtils, 'backOff').mockImplementation(backoffMockImplementation);
+ * });
+ * ```
+ *
+ * @param {Function} callback
+ */
+export const backoffMockImplementation = callback => {
+ const q = new Promise((resolve, reject) => {
+ const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
+ const next = () => callback(next, stop);
+ // Define a timeout based on a mock timer
+ setTimeout(() => {
+ callback(next, stop);
+ });
+ });
+ // Run all resolved promises in chain
+ jest.runOnlyPendingTimers();
+ return q;
+};
+
+export default { backoffMockImplementation };
diff --git a/spec/frontend/monitoring/requests/index_spec.js b/spec/frontend/monitoring/requests/index_spec.js
new file mode 100644
index 00000000000..7c22e0bc5d7
--- /dev/null
+++ b/spec/frontend/monitoring/requests/index_spec.js
@@ -0,0 +1,149 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import statusCodes from '~/lib/utils/http_status';
+import * as commonUtils from '~/lib/utils/common_utils';
+import { backoffMockImplementation } from 'jest/helpers/backoff_helper';
+import { metricsDashboardResponse } from '../fixture_data';
+import { getDashboard, getPrometheusQueryData } from '~/monitoring/requests';
+
+describe('monitoring metrics_requests', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ jest.spyOn(commonUtils, 'backOff').mockImplementation(backoffMockImplementation);
+ });
+
+ afterEach(() => {
+ mock.reset();
+
+ commonUtils.backOff.mockReset();
+ });
+
+ describe('getDashboard', () => {
+ const response = metricsDashboardResponse;
+ const dashboardEndpoint = '/dashboard';
+ const params = {
+ start_time: 'start_time',
+ end_time: 'end_time',
+ };
+
+ it('returns a dashboard response', () => {
+ mock.onGet(dashboardEndpoint).reply(statusCodes.OK, response);
+
+ return getDashboard(dashboardEndpoint, params).then(data => {
+ expect(data).toEqual(metricsDashboardResponse);
+ });
+ });
+
+ it('returns a dashboard response after retrying twice', () => {
+ mock.onGet(dashboardEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(dashboardEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(dashboardEndpoint).reply(statusCodes.OK, response);
+
+ return getDashboard(dashboardEndpoint, params).then(data => {
+ expect(data).toEqual(metricsDashboardResponse);
+ expect(mock.history.get).toHaveLength(3);
+ });
+ });
+
+ it('rejects after getting an error', () => {
+ mock.onGet(dashboardEndpoint).reply(500);
+
+ return getDashboard(dashboardEndpoint, params).catch(error => {
+ expect(error).toEqual(expect.any(Error));
+ expect(mock.history.get).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('getPrometheusQueryData', () => {
+ const response = {
+ status: 'success',
+ data: {
+ resultType: 'matrix',
+ result: [],
+ },
+ };
+ const prometheusEndpoint = '/query_range';
+ const params = {
+ start_time: 'start_time',
+ end_time: 'end_time',
+ };
+
+ it('returns a dashboard response', () => {
+ mock.onGet(prometheusEndpoint).reply(statusCodes.OK, response);
+
+ return getPrometheusQueryData(prometheusEndpoint, params).then(data => {
+ expect(data).toEqual(response.data);
+ });
+ });
+
+ it('returns a dashboard response after retrying twice', () => {
+ // Mock multiple attempts while the cache is filling up
+ mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(prometheusEndpoint).reply(statusCodes.OK, response); // 3rd attempt
+
+ return getPrometheusQueryData(prometheusEndpoint, params).then(data => {
+ expect(data).toEqual(response.data);
+ expect(mock.history.get).toHaveLength(3);
+ });
+ });
+
+ it('rejects after getting an HTTP 500 error', () => {
+ mock.onGet(prometheusEndpoint).reply(500, {
+ status: 'error',
+ error: 'An error ocurred',
+ });
+
+ return getPrometheusQueryData(prometheusEndpoint, params).catch(error => {
+ expect(error).toEqual(new Error('Request failed with status code 500'));
+ });
+ });
+
+ it('rejects after retrying twice and getting an HTTP 401 error', () => {
+ // Mock multiple attempts while the cache is filling up and fails
+ mock.onGet(prometheusEndpoint).reply(statusCodes.UNAUTHORIZED, {
+ status: 'error',
+ error: 'An error ocurred',
+ });
+
+ return getPrometheusQueryData(prometheusEndpoint, params).catch(error => {
+ expect(error).toEqual(new Error('Request failed with status code 401'));
+ });
+ });
+
+ it('rejects after retrying twice and getting an HTTP 500 error', () => {
+ // Mock multiple attempts while the cache is filling up and fails
+ mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(prometheusEndpoint).reply(500, {
+ status: 'error',
+ error: 'An error ocurred',
+ }); // 3rd attempt
+
+ return getPrometheusQueryData(prometheusEndpoint, params).catch(error => {
+ expect(error).toEqual(new Error('Request failed with status code 500'));
+ expect(mock.history.get).toHaveLength(3);
+ });
+ });
+
+ test.each`
+ code | reason
+ ${statusCodes.BAD_REQUEST} | ${'Parameters are missing or incorrect'}
+ ${statusCodes.UNPROCESSABLE_ENTITY} | ${"Expression can't be executed"}
+ ${statusCodes.SERVICE_UNAVAILABLE} | ${'Query timed out or aborted'}
+ `('rejects with details: "$reason" after getting an HTTP $code error', ({ code, reason }) => {
+ mock.onGet(prometheusEndpoint).reply(code, {
+ status: 'error',
+ error: reason,
+ });
+
+ return getPrometheusQueryData(prometheusEndpoint, params).catch(error => {
+ expect(error).toEqual(new Error(reason));
+ expect(mock.history.get).toHaveLength(1);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index 22f2b2e3c77..a6d6accbe23 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -8,6 +8,7 @@ import createFlash from '~/flash';
import { defaultTimeRange } from '~/vue_shared/constants';
import * as getters from '~/monitoring/stores/getters';
import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
+import { backoffMockImplementation } from 'jest/helpers/backoff_helper';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
@@ -73,19 +74,7 @@ describe('Monitoring store actions', () => {
commit = jest.fn();
dispatch = jest.fn();
- jest.spyOn(commonUtils, 'backOff').mockImplementation(callback => {
- const q = new Promise((resolve, reject) => {
- const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
- const next = () => callback(next, stop);
- // Define a timeout based on a mock timer
- setTimeout(() => {
- callback(next, stop);
- });
- });
- // Run all resolved promises in chain
- jest.runOnlyPendingTimers();
- return q;
- });
+ jest.spyOn(commonUtils, 'backOff').mockImplementation(backoffMockImplementation);
});
afterEach(() => {
@@ -483,7 +472,6 @@ describe('Monitoring store actions', () => {
],
[],
() => {
- expect(mock.history.get).toHaveLength(1);
done();
},
).catch(done.fail);
@@ -569,46 +557,8 @@ describe('Monitoring store actions', () => {
});
});
- it('commits result, when waiting for results', done => {
- // Mock multiple attempts while the cache is filling up
- mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpointPath).reply(200, { data }); // 4th attempt
-
- testAction(
- fetchPrometheusMetric,
- { metric, defaultQueryParams },
- state,
- [
- {
- type: types.REQUEST_METRIC_RESULT,
- payload: {
- metricId: metric.metricId,
- },
- },
- {
- type: types.RECEIVE_METRIC_RESULT_SUCCESS,
- payload: {
- metricId: metric.metricId,
- data,
- },
- },
- ],
- [],
- () => {
- expect(mock.history.get).toHaveLength(4);
- done();
- },
- ).catch(done.fail);
- });
-
it('commits failure, when waiting for results and getting a server error', done => {
- // Mock multiple attempts while the cache is filling up and fails
- mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpointPath).reply(500); // 4th attempt
+ mock.onGet(prometheusEndpointPath).reply(500);
const error = new Error('Request failed with status code 500');
@@ -633,7 +583,6 @@ describe('Monitoring store actions', () => {
],
[],
).catch(e => {
- expect(mock.history.get).toHaveLength(4);
expect(e).toEqual(error);
done();
});
diff --git a/spec/frontend/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js
index b5446e70028..1906380e60e 100644
--- a/spec/frontend/snippets/components/show_spec.js
+++ b/spec/frontend/snippets/components/show_spec.js
@@ -3,17 +3,25 @@ import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import SnippetHeader from '~/snippets/components/snippet_header.vue';
import SnippetTitle from '~/snippets/components/snippet_title.vue';
import SnippetBlob from '~/snippets/components/snippet_blob_view.vue';
+import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
import { GlLoadingIcon } from '@gitlab/ui';
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import { shallowMount } from '@vue/test-utils';
-import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants';
+import {
+ SNIPPET_VISIBILITY_INTERNAL,
+ SNIPPET_VISIBILITY_PRIVATE,
+ SNIPPET_VISIBILITY_PUBLIC,
+} from '~/snippets/constants';
describe('Snippet view app', () => {
let wrapper;
const defaultProps = {
snippetGid: 'gid://gitlab/PersonalSnippet/42',
};
+ const webUrl = 'http://foo.bar';
+ const dummyHTTPUrl = webUrl;
+ const dummySSHUrl = 'ssh://foo.bar';
function createComponent({ props = defaultProps, data = {}, loading = false } = {}) {
const $apollo = {
@@ -72,4 +80,47 @@ describe('Snippet view app', () => {
expect(blobs.at(0).props('blob')).toEqual(Blob);
expect(blobs.at(1).props('blob')).toEqual(BinaryBlob);
});
+
+ describe('Embed dropdown rendering', () => {
+ it.each`
+ visibilityLevel | condition | isRendered
+ ${SNIPPET_VISIBILITY_INTERNAL} | ${'not render'} | ${false}
+ ${SNIPPET_VISIBILITY_PRIVATE} | ${'not render'} | ${false}
+ ${'foo'} | ${'not render'} | ${false}
+ ${SNIPPET_VISIBILITY_PUBLIC} | ${'render'} | ${true}
+ `('does $condition blob-embeddable by default', ({ visibilityLevel, isRendered }) => {
+ createComponent({
+ data: {
+ snippet: {
+ visibilityLevel,
+ webUrl,
+ },
+ },
+ });
+ expect(wrapper.contains(BlobEmbeddable)).toBe(isRendered);
+ });
+ });
+
+ describe('Clone button rendering', () => {
+ it.each`
+ httpUrlToRepo | sshUrlToRepo | shouldRender | isRendered
+ ${null} | ${null} | ${'Should not'} | ${false}
+ ${null} | ${dummySSHUrl} | ${'Should'} | ${true}
+ ${dummyHTTPUrl} | ${null} | ${'Should'} | ${true}
+ ${dummyHTTPUrl} | ${dummySSHUrl} | ${'Should'} | ${true}
+ `(
+ '$shouldRender render "Clone" button when `httpUrlToRepo` is $httpUrlToRepo and `sshUrlToRepo` is $sshUrlToRepo',
+ ({ httpUrlToRepo, sshUrlToRepo, isRendered }) => {
+ createComponent({
+ data: {
+ snippet: {
+ sshUrlToRepo,
+ httpUrlToRepo,
+ },
+ },
+ });
+ expect(wrapper.contains(CloneDropdownButton)).toBe(isRendered);
+ },
+ );
+ });
});
diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js
index c8f1c8fc8a9..0de130aa667 100644
--- a/spec/frontend/snippets/components/snippet_blob_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js
@@ -1,7 +1,6 @@
import { mount } from '@vue/test-utils';
import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
-import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import BlobContent from '~/blob/components/blob_content.vue';
import {
BLOB_RENDER_EVENT_LOAD,
@@ -9,11 +8,7 @@ import {
BLOB_RENDER_ERRORS,
} from '~/blob/components/constants';
import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
-import {
- SNIPPET_VISIBILITY_PRIVATE,
- SNIPPET_VISIBILITY_INTERNAL,
- SNIPPET_VISIBILITY_PUBLIC,
-} from '~/snippets/constants';
+import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants';
import { Blob as BlobMock, SimpleViewerMock, RichViewerMock } from 'jest/blob/components/mock_data';
@@ -72,18 +67,6 @@ describe('Blob Embeddable', () => {
expect(wrapper.find(BlobContent).exists()).toBe(true);
});
- it.each([SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PRIVATE, 'foo'])(
- 'does not render blob-embeddable by default',
- visibilityLevel => {
- createComponent({
- snippetProps: {
- visibilityLevel,
- },
- });
- expect(wrapper.find(BlobEmbeddable).exists()).toBe(false);
- },
- );
-
it('sets simple viewer correctly', () => {
createComponent();
expect(wrapper.find(SimpleViewer).exists()).toBe(true);
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index c1386ac4eb2..7a1b27d563c 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -231,7 +231,7 @@ RSpec.describe PipelineSerializer do
# :source_pipeline and :source_job
# Existing numbers are high and require performance optimization
# https://gitlab.com/gitlab-org/gitlab/-/issues/225156
- expected_queries = Gitlab.ee? ? 101 : 92
+ expected_queries = Gitlab.ee? ? 95 : 86
expect(recorded.count).to be_within(2).of(expected_queries)
expect(recorded.cached_count).to eq(0)