diff options
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) |