diff options
59 files changed, 998 insertions, 325 deletions
@@ -276,7 +276,6 @@ gem 'batch-loader', '~> 1.2.1' # Perf bar gem 'peek', '~> 1.0.1' gem 'peek-gc', '~> 0.0.2' -gem 'peek-host', '~> 1.0.0' gem 'peek-mysql2', '~> 1.1.0', group: :mysql gem 'peek-performance_bar', '~> 1.3.0' gem 'peek-pg', '~> 1.3.0', group: :postgres diff --git a/Gemfile.lock b/Gemfile.lock index 8a37b3c4152..1c6c7edb1a0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -593,8 +593,6 @@ GEM railties (>= 4.0.0) peek-gc (0.0.2) peek - peek-host (1.0.0) - peek peek-mysql2 (1.1.0) atomic (>= 1.0.0) mysql2 @@ -1124,7 +1122,6 @@ DEPENDENCIES org-ruby (~> 0.9.12) peek (~> 1.0.1) peek-gc (~> 0.0.2) - peek-host (~> 1.0.0) peek-mysql2 (~> 1.1.0) peek-performance_bar (~> 1.3.0) peek-pg (~> 1.3.0) diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js index 8d021de7998..84fef4d8b4f 100644 --- a/app/assets/javascripts/behaviors/index.js +++ b/app/assets/javascripts/behaviors/index.js @@ -1,6 +1,7 @@ import './autosize'; import './bind_in_out'; -import initCopyAsGFM from './copy_as_gfm'; +import './markdown/render_gfm'; +import initCopyAsGFM from './markdown/copy_as_gfm'; import initCopyToClipboard from './copy_to_clipboard'; import './details_behavior'; import installGlEmojiElement from './gl_emoji'; diff --git a/app/assets/javascripts/behaviors/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js index f5f4f00d587..75cf90de0b5 100644 --- a/app/assets/javascripts/behaviors/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js @@ -2,8 +2,8 @@ import $ from 'jquery'; import _ from 'underscore'; -import { insertText, getSelectedFragment, nodeMatchesSelector } from '../lib/utils/common_utils'; -import { placeholderImage } from '../lazy_loader'; +import { insertText, getSelectedFragment, nodeMatchesSelector } from '~/lib/utils/common_utils'; +import { placeholderImage } from '~/lazy_loader'; const gfmRules = { // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index 94fffcd2f61..dbff2bd4b10 100644 --- a/app/assets/javascripts/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -1,7 +1,7 @@ import $ from 'jquery'; +import syntaxHighlight from '~/syntax_highlight'; import renderMath from './render_math'; import renderMermaid from './render_mermaid'; -import syntaxHighlight from './syntax_highlight'; // Render Gitlab flavoured Markdown // diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js index 8572bf64d46..7dcf1aeed17 100644 --- a/app/assets/javascripts/render_math.js +++ b/app/assets/javascripts/behaviors/markdown/render_math.js @@ -1,6 +1,6 @@ import $ from 'jquery'; -import { __ } from './locale'; -import flash from './flash'; +import { __ } from '~/locale'; +import flash from '~/flash'; // Renders math using KaTeX in any element with the // `js-render-math` class diff --git a/app/assets/javascripts/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js index d4f18955bd2..56b1896e9f1 100644 --- a/app/assets/javascripts/render_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js @@ -1,3 +1,5 @@ +import flash from '~/flash'; + // Renders diagrams and flowcharts from text using Mermaid in any element with the // `js-render-mermaid` class. // @@ -12,8 +14,6 @@ // </pre> // -import Flash from './flash'; - export default function renderMermaid($els) { if (!$els.length) return; @@ -52,6 +52,6 @@ export default function renderMermaid($els) { }); }); }).catch((err) => { - Flash(`Can't load mermaid module: ${err}`); + flash(`Can't load mermaid module: ${err}`); }); } diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 42ecc415173..72f21f13860 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -53,8 +53,12 @@ function initPageShortcuts(page) { function initGFMInput() { $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { - const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); - const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete); + const gfm = new GfmAutoComplete( + gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources, + ); + const enableGFM = convertPermissionToBoolean( + el.dataset.supportsAutocomplete, + ); gfm.setup($(el), { emojis: true, members: enableGFM, @@ -67,9 +71,9 @@ function initGFMInput() { } function initPerformanceBar() { - if (document.querySelector('#peek')) { + if (document.querySelector('#js-peek')) { import('./performance_bar') - .then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap + .then(m => new m.default({ container: '#js-peek' })) // eslint-disable-line new-cap .catch(() => Flash('Error loading performance bar module')); } } diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 870285f7940..cedb6ef19f7 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -32,7 +32,6 @@ import LazyLoader from './lazy_loader'; import initLogoAnimation from './logo'; import './milestone_select'; import './projects_dropdown'; -import './render_gfm'; import initBreadcrumbs from './breadcrumb'; import initDispatcher from './dispatcher'; diff --git a/app/assets/javascripts/performance_bar.js b/app/assets/javascripts/performance_bar.js deleted file mode 100644 index c22598ee665..00000000000 --- a/app/assets/javascripts/performance_bar.js +++ /dev/null @@ -1,57 +0,0 @@ -import $ from 'jquery'; -import 'vendor/peek'; -import 'vendor/peek.performance_bar'; -import { getParameterValues } from './lib/utils/url_utility'; - -export default class PerformanceBar { - constructor(opts) { - if (!PerformanceBar.singleton) { - this.init(opts); - PerformanceBar.singleton = this; - } - return PerformanceBar.singleton; - } - - init(opts) { - const $container = $(opts.container); - this.$lineProfileLink = $container.find('.js-toggle-modal-peek-line-profile'); - this.$lineProfileModal = $('#modal-peek-line-profile'); - this.initEventListeners(); - this.showModalOnLoad(); - } - - initEventListeners() { - this.$lineProfileLink.on('click', e => this.handleLineProfileLink(e)); - $(document).on('click', '.js-lineprof-file', PerformanceBar.toggleLineProfileFile); - } - - showModalOnLoad() { - // When a lineprofiler query-string param is present, we show the line - // profiler modal upon page load - if (/lineprofiler/.test(window.location.search)) { - PerformanceBar.toggleModal(this.$lineProfileModal); - } - } - - handleLineProfileLink(e) { - const lineProfilerParameter = getParameterValues('lineprofiler'); - const lineProfilerParameterRegex = new RegExp(`lineprofiler=${lineProfilerParameter[0]}`); - const shouldToggleModal = lineProfilerParameter.length > 0 && - lineProfilerParameterRegex.test(e.currentTarget.href); - - if (shouldToggleModal) { - e.preventDefault(); - PerformanceBar.toggleModal(this.$lineProfileModal); - } - } - - static toggleModal($modal) { - if ($modal.length) { - $modal.modal('toggle'); - } - } - - static toggleLineProfileFile(e) { - $(e.currentTarget).parents('.peek-rblineprof-file').find('.data').toggle(); - } -} diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue new file mode 100644 index 00000000000..145465f4ee9 --- /dev/null +++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue @@ -0,0 +1,78 @@ +<script> +import GlModal from '~/vue_shared/components/gl_modal.vue'; + +export default { + components: { + GlModal, + }, + props: { + currentRequest: { + type: Object, + required: true, + }, + metric: { + type: String, + required: true, + }, + header: { + type: String, + required: true, + }, + details: { + type: String, + required: true, + }, + keys: { + type: Array, + required: true, + }, + }, +}; +</script> +<template> + <div + :id="`peek-view-${metric}`" + class="view" + > + <button + :data-target="`#modal-peek-${metric}-details`" + class="btn-blank btn-link bold" + type="button" + data-toggle="modal" + > + <span + v-if="currentRequest.details" + class="bold" + > + {{ currentRequest.details[metric].duration }} + / + {{ currentRequest.details[metric].calls }} + </span> + </button> + <gl-modal + v-if="currentRequest.details" + :id="`modal-peek-${metric}-details`" + :header-title-text="header" + class="performance-bar-modal" + > + <table class="table"> + <tr + v-for="(item, index) in currentRequest.details[metric][details]" + :key="index" + > + <td><strong>{{ item.duration }}ms</strong></td> + <td + v-for="key in keys" + :key="key" + > + {{ item[key] }} + </td> + </tr> + </table> + + <div slot="footer"> + </div> + </gl-modal> + {{ metric }} + </div> +</template> diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue new file mode 100644 index 00000000000..88345cf2ad9 --- /dev/null +++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue @@ -0,0 +1,191 @@ +<script> +import $ from 'jquery'; + +import PerformanceBarService from '../services/performance_bar_service'; +import detailedMetric from './detailed_metric.vue'; +import requestSelector from './request_selector.vue'; +import simpleMetric from './simple_metric.vue'; +import upstreamPerformanceBar from './upstream_performance_bar.vue'; + +import Flash from '../../flash'; + +export default { + components: { + detailedMetric, + requestSelector, + simpleMetric, + upstreamPerformanceBar, + }, + props: { + store: { + type: Object, + required: true, + }, + env: { + type: String, + required: true, + }, + requestId: { + type: String, + required: true, + }, + peekUrl: { + type: String, + required: true, + }, + profileUrl: { + type: String, + required: true, + }, + }, + detailedMetrics: [ + { metric: 'pg', header: 'SQL queries', details: 'queries', keys: ['sql'] }, + { + metric: 'gitaly', + header: 'Gitaly calls', + details: 'details', + keys: ['feature', 'request'], + }, + ], + simpleMetrics: ['redis', 'sidekiq'], + data() { + return { currentRequestId: '' }; + }, + computed: { + requests() { + return this.store.requestsWithDetails(); + }, + currentRequest: { + get() { + return this.store.findRequest(this.currentRequestId); + }, + set(requestId) { + this.currentRequestId = requestId; + }, + }, + initialRequest() { + return this.currentRequestId === this.requestId; + }, + lineProfileModal() { + return $('#modal-peek-line-profile'); + }, + }, + mounted() { + this.interceptor = PerformanceBarService.registerInterceptor( + this.peekUrl, + this.loadRequestDetails, + ); + + this.loadRequestDetails(this.requestId, window.location.href); + this.currentRequest = this.requestId; + + if (this.lineProfileModal.length) { + this.lineProfileModal.modal('toggle'); + } + }, + beforeDestroy() { + PerformanceBarService.removeInterceptor(this.interceptor); + }, + methods: { + loadRequestDetails(requestId, requestUrl) { + if (!this.store.canTrackRequest(requestUrl)) { + return; + } + + this.store.addRequest(requestId, requestUrl); + + PerformanceBarService.fetchRequestDetails(this.peekUrl, requestId) + .then(res => { + this.store.addRequestDetails(requestId, res.data.data); + }) + .catch(() => + Flash(`Error getting performance bar results for ${requestId}`), + ); + }, + changeCurrentRequest(newRequestId) { + this.currentRequest = newRequestId; + }, + }, +}; +</script> +<template> + <div + id="js-peek" + :class="env" + > + <request-selector + v-if="currentRequest" + :current-request="currentRequest" + :requests="requests" + @change-current-request="changeCurrentRequest" + /> + <div + id="peek-view-host" + class="view prepend-left-5" + > + <span + v-if="currentRequest && currentRequest.details" + class="current-host" + > + {{ currentRequest.details.host.hostname }} + </span> + </div> + <div + v-if="currentRequest" + class="wrapper" + > + <upstream-performance-bar + v-if="initialRequest && currentRequest.details" + /> + <detailed-metric + v-for="metric in $options.detailedMetrics" + :key="metric.metric" + :current-request="currentRequest" + :metric="metric.metric" + :header="metric.header" + :details="metric.details" + :keys="metric.keys" + /> + <div + v-if="initialRequest" + id="peek-view-rblineprof" + class="view" + > + <button + v-if="lineProfileModal.length" + class="btn-link btn-blank" + data-toggle="modal" + data-target="#modal-peek-line-profile" + > + profile + </button> + <a + v-else + :href="profileUrl" + > + profile + </a> + </div> + <simple-metric + v-for="metric in $options.simpleMetrics" + :current-request="currentRequest" + :key="metric" + :metric="metric" + /> + <div + id="peek-view-gc" + class="view" + > + <span + v-if="currentRequest.details" + class="bold" + > + <span title="Invoke Time">{{ currentRequest.details.gc.gc_time }}</span>ms + / + <span title="Invoke Count">{{ currentRequest.details.gc.invokes }}</span> + gc + </span> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue new file mode 100644 index 00000000000..2f360ea6f6c --- /dev/null +++ b/app/assets/javascripts/performance_bar/components/request_selector.vue @@ -0,0 +1,52 @@ +<script> +export default { + props: { + currentRequest: { + type: Object, + required: true, + }, + requests: { + type: Array, + required: true, + }, + }, + data() { + return { + currentRequestId: this.currentRequest.id, + }; + }, + watch: { + currentRequestId(newRequestId) { + this.$emit('change-current-request', newRequestId); + }, + }, + methods: { + truncatedUrl(requestUrl) { + const components = requestUrl.replace(/\/$/, '').split('/'); + let truncated = components[components.length - 1]; + + if (truncated.match(/^\d+$/)) { + truncated = `${components[components.length - 2]}/${truncated}`; + } + + return truncated; + }, + }, +}; +</script> +<template> + <div + id="peek-request-selector" + class="append-right-5 pull-right" + > + <select v-model="currentRequestId"> + <option + v-for="request in requests" + :key="request.id" + :value="request.id" + > + {{ truncatedUrl(request.url) }} + </option> + </select> + </div> +</template> diff --git a/app/assets/javascripts/performance_bar/components/simple_metric.vue b/app/assets/javascripts/performance_bar/components/simple_metric.vue new file mode 100644 index 00000000000..b654bc66249 --- /dev/null +++ b/app/assets/javascripts/performance_bar/components/simple_metric.vue @@ -0,0 +1,30 @@ +<script> +export default { + props: { + currentRequest: { + type: Object, + required: true, + }, + metric: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <div + :id="`peek-view-${metric}`" + class="view" + > + <span + v-if="currentRequest.details" + class="bold" + > + {{ currentRequest.details[metric].duration }} + / + {{ currentRequest.details[metric].calls }} + </span> + {{ metric }} + </div> +</template> diff --git a/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue b/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue new file mode 100644 index 00000000000..d438b1ec27b --- /dev/null +++ b/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue @@ -0,0 +1,18 @@ +<script> +export default { + mounted() { + const upstreamPerformanceBar = document + .getElementById('peek-view-performance-bar') + .cloneNode(true); + + this.$refs.wrapper.appendChild(upstreamPerformanceBar); + }, +}; +</script> +<template> + <div + id="peek-view-performance-bar-vue" + class="view" + ref="wrapper" + ></div> +</template> diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js new file mode 100644 index 00000000000..fca488120f6 --- /dev/null +++ b/app/assets/javascripts/performance_bar/index.js @@ -0,0 +1,37 @@ +import 'vendor/peek.performance_bar'; + +import Vue from 'vue'; +import performanceBarApp from './components/performance_bar_app.vue'; +import PerformanceBarStore from './stores/performance_bar_store'; + +export default () => + new Vue({ + el: '#js-peek', + components: { + performanceBarApp, + }, + data() { + const performanceBarData = document.querySelector(this.$options.el) + .dataset; + const store = new PerformanceBarStore(); + + return { + store, + env: performanceBarData.env, + requestId: performanceBarData.requestId, + peekUrl: performanceBarData.peekUrl, + profileUrl: performanceBarData.profileUrl, + }; + }, + render(createElement) { + return createElement('performance-bar-app', { + props: { + store: this.store, + env: this.env, + requestId: this.requestId, + peekUrl: this.peekUrl, + profileUrl: this.profileUrl, + }, + }); + }, + }); diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js new file mode 100644 index 00000000000..d8e792446c3 --- /dev/null +++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js @@ -0,0 +1,24 @@ +import axios from '../../lib/utils/axios_utils'; + +export default class PerformanceBarService { + static fetchRequestDetails(peekUrl, requestId) { + return axios.get(peekUrl, { params: { request_id: requestId } }); + } + + static registerInterceptor(peekUrl, callback) { + return axios.interceptors.response.use(response => { + const requestId = response.headers['x-request-id']; + const requestUrl = response.config.url; + + if (requestUrl !== peekUrl && requestId) { + callback(requestId, requestUrl); + } + + return response; + }); + } + + static removeInterceptor(interceptor) { + axios.interceptors.response.eject(interceptor); + } +} diff --git a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js new file mode 100644 index 00000000000..c6b2f55243c --- /dev/null +++ b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js @@ -0,0 +1,39 @@ +export default class PerformanceBarStore { + constructor() { + this.requests = []; + } + + addRequest(requestId, requestUrl, requestDetails) { + if (!this.findRequest(requestId)) { + this.requests.push({ + id: requestId, + url: requestUrl, + details: requestDetails, + }); + } + + return this.requests; + } + + findRequest(requestId) { + return this.requests.find(request => request.id === requestId); + } + + addRequestDetails(requestId, requestDetails) { + const request = this.findRequest(requestId); + + request.details = requestDetails; + + return request; + } + + requestsWithDetails() { + return this.requests.filter(request => request.details); + } + + canTrackRequest(requestUrl) { + return ( + this.requests.filter(request => request.url === requestUrl).length < 2 + ); + } +} diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 3031230277d..193788f754f 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -3,7 +3,7 @@ import Mousetrap from 'mousetrap'; import _ from 'underscore'; import Sidebar from './right_sidebar'; import Shortcuts from './shortcuts'; -import { CopyAsGFM } from './behaviors/copy_as_gfm'; +import { CopyAsGFM } from './behaviors/markdown/copy_as_gfm'; export default class ShortcutsIssuable extends Shortcuts { constructor(isMergeRequest) { diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss index 6e539e39ca1..d06148a7bf8 100644 --- a/app/assets/stylesheets/performance_bar.scss +++ b/app/assets/stylesheets/performance_bar.scss @@ -1,8 +1,8 @@ -@import "framework/variables"; -@import "peek/views/performance_bar"; -@import "peek/views/rblineprof"; +@import 'framework/variables'; +@import 'peek/views/performance_bar'; +@import 'peek/views/rblineprof'; -#peek { +#js-peek { position: fixed; left: 0; top: 0; @@ -21,14 +21,26 @@ &.production { background-color: $perf-bar-production; + + select { + background: $perf-bar-production; + } } &.staging { background-color: $perf-bar-staging; + + select { + background: $perf-bar-staging; + } } &.development { background-color: $perf-bar-development; + + select { + background: $perf-bar-development; + } } .wrapper { @@ -42,11 +54,12 @@ background: $perf-bar-bucket-bg; display: inline-block; padding: 4px 6px; - font-family: Consolas, "Liberation Mono", Courier, monospace; + font-family: Consolas, 'Liberation Mono', Courier, monospace; line-height: 1; color: $perf-bar-bucket-color; border-radius: 3px; - box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from, inset 0 1px 2px $perf-bar-bucket-box-shadow-to; + box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from, + inset 0 1px 2px $perf-bar-bucket-box-shadow-to; .hidden { display: none; @@ -94,6 +107,10 @@ max-width: 10000px !important; } } + + .performance-bar-modal .modal-footer { + display: none; + } } #modal-peek-pg-queries-content { diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index af9c8bf1bd3..3ddf8eb3369 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -300,7 +300,7 @@ module ApplicationHelper def linkedin_url(user) name = user.linkedin - if name =~ %r{\Ahttps?:\/\/(www\.)?linkedin\.com\/in\/} + if name =~ %r{\Ahttps?://(www\.)?linkedin\.com/in/} name else "https://www.linkedin.com/in/#{name}" @@ -309,10 +309,10 @@ module ApplicationHelper def twitter_url(user) name = user.twitter - if name =~ %r{\Ahttps?:\/\/(www\.)?twitter\.com\/} + if name =~ %r{\Ahttps?://(www\.)?twitter\.com/} name else - "https://www.twitter.com/#{name}" + "https://twitter.com/#{name}" end end diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index 240783bc7fd..f435c80c656 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -1,27 +1,4 @@ module ServicesHelper - def service_event_description(event) - case event - when "push", "push_events" - "Event will be triggered by a push to the repository" - when "tag_push", "tag_push_events" - "Event will be triggered when a new tag is pushed to the repository" - when "note", "note_events" - "Event will be triggered when someone adds a comment" - when "issue", "issue_events" - "Event will be triggered when an issue is created/updated/closed" - when "confidential_issue", "confidential_issue_events" - "Event will be triggered when a confidential issue is created/updated/closed" - when "merge_request", "merge_request_events" - "Event will be triggered when a merge request is created/updated/merged" - when "pipeline", "pipeline_events" - "Event will be triggered when a pipeline status changes" - when "wiki_page", "wiki_page_events" - "Event will be triggered when a wiki page is created/updated" - when "commit", "commit_events" - "Event will be triggered when a commit is created/updated" - end - end - def service_event_field_name(event) event = event.pluralize if %w[merge_request issue confidential_issue].include?(event) "#{event}_events" diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 601a6a077f5..ed4bbfb6cfc 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -14,9 +14,8 @@ class JiraService < IssueTrackerService alias_method :project_url, :url - # This is confusing, but JiraService does not really support these events. - # The values here are required to display correct options in the service - # configuration screen. + # When these are false GitLab does not create cross reference + # comments on JIRA except when an issue gets transitioned. def self.supported_events %w(commit merge_request) end @@ -318,4 +317,13 @@ class JiraService < IssueTrackerService url_changed? end + + def self.event_description(event) + case event + when "merge_request", "merge_request_events" + "JIRA comments will be created when an issue gets referenced in a merge request." + when "commit", "commit_events" + "JIRA comments will be created when an issue gets referenced in a commit." + end + end end diff --git a/app/models/service.rb b/app/models/service.rb index 2556db68146..1dcb79157a2 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -304,6 +304,29 @@ class Service < ActiveRecord::Base end end + def self.event_description(event) + case event + when "push", "push_events" + "Event will be triggered by a push to the repository" + when "tag_push", "tag_push_events" + "Event will be triggered when a new tag is pushed to the repository" + when "note", "note_events" + "Event will be triggered when someone adds a comment" + when "issue", "issue_events" + "Event will be triggered when an issue is created/updated/closed" + when "confidential_issue", "confidential_issue_events" + "Event will be triggered when a confidential issue is created/updated/closed" + when "merge_request", "merge_request_events" + "Event will be triggered when a merge request is created/updated/merged" + when "pipeline", "pipeline_events" + "Event will be triggered when a pipeline status changes" + when "wiki_page", "wiki_page_events" + "Event will be triggered when a wiki page is created/updated" + when "commit", "commit_events" + "Event will be triggered when a commit is created/updated" + end + end + def valid_recipients? activated? && !importing? end diff --git a/app/views/peek/_bar.html.haml b/app/views/peek/_bar.html.haml new file mode 100644 index 00000000000..14dafa197b5 --- /dev/null +++ b/app/views/peek/_bar.html.haml @@ -0,0 +1,12 @@ +- return unless peek_enabled? + +#js-peek{ data: { env: Peek.env, + request_id: Peek.request_id, + peek_url: peek_routes.results_url, + profile_url: url_for(params.merge(lineprofiler: 'true')) }, + class: Peek.env } + +#peek-view-performance-bar + = render_server_response_time + %span#serverstats + %ul.performance-bar diff --git a/app/views/peek/views/_gitaly.html.haml b/app/views/peek/views/_gitaly.html.haml deleted file mode 100644 index 945bb287429..00000000000 --- a/app/views/peek/views/_gitaly.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -- local_assigns.fetch(:view) - -%button.btn-blank.btn-link.bold{ type: 'button', data: { toggle: 'modal', target: '#modal-peek-gitaly-details' } } - %span{ data: { defer_to: "#{view.defer_key}-duration" } }... - \/ - %span{ data: { defer_to: "#{view.defer_key}-calls" } }... -#modal-peek-gitaly-details.modal{ tabindex: -1, role: 'dialog' } - .modal-dialog.modal-full - .modal-content - .modal-header - %button.close{ type: 'button', data: { dismiss: 'modal' }, 'aria-label' => 'Close' } - %span{ 'aria-hidden' => 'true' } - × - %h4 - Gitaly requests - .modal-body{ data: { defer_to: "#{view.defer_key}-details" } }... -gitaly diff --git a/app/views/peek/views/_host.html.haml b/app/views/peek/views/_host.html.haml deleted file mode 100644 index 40769b5c6f6..00000000000 --- a/app/views/peek/views/_host.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -%span.current-host - = truncate(view.hostname) diff --git a/app/views/peek/views/_mysql2.html.haml b/app/views/peek/views/_mysql2.html.haml deleted file mode 100644 index ac811a10ef5..00000000000 --- a/app/views/peek/views/_mysql2.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- local_assigns.fetch(:view) - -= render 'peek/views/sql', view: view -mysql diff --git a/app/views/peek/views/_pg.html.haml b/app/views/peek/views/_pg.html.haml deleted file mode 100644 index ee94c2f3274..00000000000 --- a/app/views/peek/views/_pg.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- local_assigns.fetch(:view) - -= render 'peek/views/sql', view: view -pg diff --git a/app/views/peek/views/_rblineprof.html.haml b/app/views/peek/views/_rblineprof.html.haml deleted file mode 100644 index 6c037930ca9..00000000000 --- a/app/views/peek/views/_rblineprof.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -Profile: - -= link_to 'all', url_for(lineprofiler: 'true'), class: 'js-toggle-modal-peek-line-profile' -\/ -= link_to 'app & lib', url_for(lineprofiler: 'app'), class: 'js-toggle-modal-peek-line-profile' -\/ -= link_to 'views', url_for(lineprofiler: 'views'), class: 'js-toggle-modal-peek-line-profile' diff --git a/app/views/peek/views/_sql.html.haml b/app/views/peek/views/_sql.html.haml deleted file mode 100644 index 36583df898a..00000000000 --- a/app/views/peek/views/_sql.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -%button.btn-blank.btn-link.bold{ type: 'button', data: { toggle: 'modal', target: '#modal-peek-pg-queries' } } - %span{ data: { defer_to: "#{view.defer_key}-duration" } }... - \/ - %span{ data: { defer_to: "#{view.defer_key}-calls" } }... -#modal-peek-pg-queries.modal{ tabindex: -1 } - .modal-dialog.modal-full - .modal-content - .modal-header - %button.close{ type: 'button', data: { dismiss: 'modal' }, 'aria-label' => 'Close' } - %span{ 'aria-hidden' => 'true' } - × - %h4 - SQL queries - .modal-body{ data: { defer_to: "#{view.defer_key}-queries" } }... diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index 355b3ac75ae..a41aaed66a3 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -33,7 +33,7 @@ = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] %p.light - = service_event_description(event) + = @service.class.event_description(event) - @service.global_fields.each do |field| - type = field[:type] diff --git a/changelogs/unreleased/44280-fix-code-search.yml b/changelogs/unreleased/44280-fix-code-search.yml new file mode 100644 index 00000000000..07f3abb224c --- /dev/null +++ b/changelogs/unreleased/44280-fix-code-search.yml @@ -0,0 +1,5 @@ +--- +title: Fix search results stripping last endline when parsing the results +merge_request: 17777 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/ajax-requests-in-performance-bar.yml b/changelogs/unreleased/ajax-requests-in-performance-bar.yml new file mode 100644 index 00000000000..88cc3678c2b --- /dev/null +++ b/changelogs/unreleased/ajax-requests-in-performance-bar.yml @@ -0,0 +1,5 @@ +--- +title: Allow viewing timings for AJAX requests in the performance bar +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/issue_25542.yml b/changelogs/unreleased/issue_25542.yml new file mode 100644 index 00000000000..eba491f7e2a --- /dev/null +++ b/changelogs/unreleased/issue_25542.yml @@ -0,0 +1,5 @@ +--- +title: Improve JIRA event descriptions +merge_request: +author: +type: other diff --git a/config/initializers/active_record_locking.rb b/config/initializers/active_record_locking.rb index 150aaa2a8c2..3e7111fd063 100644 --- a/config/initializers/active_record_locking.rb +++ b/config/initializers/active_record_locking.rb @@ -1,73 +1,77 @@ # rubocop:disable Lint/RescueException -# This patch fixes https://github.com/rails/rails/issues/26024 -# TODO: Remove it when it's no longer necessary - -module ActiveRecord - module Locking - module Optimistic - # We overwrite this method because we don't want to have default value - # for newly created records - def _create_record(attribute_names = self.attribute_names, *) # :nodoc: - super - end +# Remove this entire initializer when we are at rails 5.0. +# This file fixes the bug (see below) which has been fixed in the upstream. +unless Gitlab.rails5? + # This patch fixes https://github.com/rails/rails/issues/26024 + # TODO: Remove it when it's no longer necessary + + module ActiveRecord + module Locking + module Optimistic + # We overwrite this method because we don't want to have default value + # for newly created records + def _create_record(attribute_names = self.attribute_names, *) # :nodoc: + super + end - def _update_record(attribute_names = self.attribute_names) #:nodoc: - return super unless locking_enabled? - return 0 if attribute_names.empty? + def _update_record(attribute_names = self.attribute_names) #:nodoc: + return super unless locking_enabled? + return 0 if attribute_names.empty? - lock_col = self.class.locking_column + lock_col = self.class.locking_column - previous_lock_value = send(lock_col).to_i # rubocop:disable GitlabSecurity/PublicSend + previous_lock_value = send(lock_col).to_i # rubocop:disable GitlabSecurity/PublicSend - # This line is added as a patch - previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0 + # This line is added as a patch + previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0 - increment_lock + increment_lock - attribute_names += [lock_col] - attribute_names.uniq! + attribute_names += [lock_col] + attribute_names.uniq! - begin - relation = self.class.unscoped + begin + relation = self.class.unscoped - affected_rows = relation.where( - self.class.primary_key => id, - lock_col => previous_lock_value - ).update_all( - attributes_for_update(attribute_names).map do |name| - [name, _read_attribute(name)] - end.to_h - ) + affected_rows = relation.where( + self.class.primary_key => id, + lock_col => previous_lock_value + ).update_all( + attributes_for_update(attribute_names).map do |name| + [name, _read_attribute(name)] + end.to_h + ) - unless affected_rows == 1 - raise ActiveRecord::StaleObjectError.new(self, "update") - end + unless affected_rows == 1 + raise ActiveRecord::StaleObjectError.new(self, "update") + end - affected_rows + affected_rows - # If something went wrong, revert the version. - rescue Exception - send(lock_col + '=', previous_lock_value) # rubocop:disable GitlabSecurity/PublicSend - raise + # If something went wrong, revert the version. + rescue Exception + send(lock_col + '=', previous_lock_value) # rubocop:disable GitlabSecurity/PublicSend + raise + end end - end - # This is patched because we need it to query `lock_version IS NULL` - # rather than `lock_version = 0` whenever lock_version is NULL. - def relation_for_destroy - return super unless locking_enabled? + # This is patched because we need it to query `lock_version IS NULL` + # rather than `lock_version = 0` whenever lock_version is NULL. + def relation_for_destroy + return super unless locking_enabled? - column_name = self.class.locking_column - super.where(self.class.arel_table[column_name].eq(self[column_name])) + column_name = self.class.locking_column + super.where(self.class.arel_table[column_name].eq(self[column_name])) + end end - end - # This is patched because we want `lock_version` default to `NULL` - # rather than `0` - class LockingType < SimpleDelegator - def type_cast_from_database(value) - super + # This is patched because we want `lock_version` default to `NULL` + # rather than `0` + class LockingType < SimpleDelegator + def type_cast_from_database(value) + super + end end end end diff --git a/config/routes.rb b/config/routes.rb index 35fd76fb119..8769f433c39 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -44,7 +44,7 @@ Rails.application.routes.draw do get 'readiness' => 'health#readiness' post 'storage_check' => 'health#storage_check' resources :metrics, only: [:index] - mount Peek::Railtie => '/peek' + mount Peek::Railtie => '/peek', as: 'peek_routes' # Boards resources shared between group and projects resources :boards, only: [] do diff --git a/doc/administration/monitoring/performance/performance_bar.md b/doc/administration/monitoring/performance/performance_bar.md index ec1cbce1bad..dc4f685d843 100644 --- a/doc/administration/monitoring/performance/performance_bar.md +++ b/doc/administration/monitoring/performance/performance_bar.md @@ -13,12 +13,16 @@ It allows you to see (from left to right):  - time taken and number of [Gitaly] calls, click through for details of these calls  -- profile of the code used to generate the page, line by line for either _all_, _app & lib_ , or _views_. In the profile view, the numbers in the left panel represent wall time, cpu time, and number of calls (based on [rblineprof](https://github.com/tmm1/rblineprof)). +- profile of the code used to generate the page, line by line. In the profile view, the numbers in the left panel represent wall time, cpu time, and number of calls (based on [rblineprof](https://github.com/tmm1/rblineprof)).  - time taken and number of calls to Redis - time taken and number of background jobs created by Sidekiq - time taken and number of Ruby GC calls +On the far right is a request selector that allows you to view the same metrics +(excluding the page timing and line profiler) for any requests made while the +page was open. Only the first two requests per unique URL are captured. + ## Enable the Performance Bar via the Admin panel GitLab Performance Bar is disabled by default. To enable it for a given group, diff --git a/lib/api/services.rb b/lib/api/services.rb index 6c97659166d..794fdab8f2b 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -735,7 +735,7 @@ module API required: false, name: event_name.to_sym, type: String, - desc: ServicesHelper.service_event_description(event_name) + desc: service.event_description(event_name) } end end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 4001b8a85e3..8b2f05fffec 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -2,10 +2,10 @@ module Banzai module Pipeline class GfmPipeline < BasePipeline # These filters convert GitLab Flavored Markdown (GFM) to HTML. - # The handlers defined in app/assets/javascripts/copy_as_gfm.js + # The handlers defined in app/assets/javascripts/behaviors/markdown/copy_as_gfm.js # consequently convert that same HTML to GFM to be copied to the clipboard. # Every filter that generates HTML from GFM should have a handler in - # app/assets/javascripts/copy_as_gfm.js, in reverse order. + # app/assets/javascripts/behaviors/markdown/copy_as_gfm.js, in reverse order. # The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb. def self.filters @filters ||= FilterArray[ diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 9811c447a01..208710b0935 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -1390,7 +1390,7 @@ module Gitlab offset = 2 args = %W(grep -i -I -n -z --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) - run_git(args).first.scrub.split(/^--$/) + run_git(args).first.scrub.split(/^--\n/) end def can_be_merged?(source_sha, target_branch) diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 29277ec6481..390efda326a 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -58,7 +58,7 @@ module Gitlab data = "" startline = 0 - result.strip.each_line.each_with_index do |line, index| + result.each_line.each_with_index do |line, index| prefix ||= line.match(/^(?<ref>[^:]*):(?<filename>.*)\x00(?<startline>\d+)\x00/)&.tap do |matches| ref = matches[:ref] filename = matches[:filename] diff --git a/lib/peek/views/host.rb b/lib/peek/views/host.rb new file mode 100644 index 00000000000..43c8a35c7ea --- /dev/null +++ b/lib/peek/views/host.rb @@ -0,0 +1,9 @@ +module Peek + module Views + class Host < View + def results + { hostname: Gitlab::Environment.hostname } + end + end + end +end diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb index f82ed6300cc..4d897f09b57 100644 --- a/spec/features/markdown/copy_as_gfm_spec.rb +++ b/spec/features/markdown/copy_as_gfm_spec.rb @@ -20,7 +20,7 @@ describe 'Copy as GFM', :js do end # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML. - # The handlers defined in app/assets/javascripts/copy_as_gfm.js consequently convert that same HTML to GFM. + # The handlers defined in app/assets/javascripts/behaviors/markdown/copy_as_gfm.js consequently convert that same HTML to GFM. # To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle # by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper. diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb index 975c157bcf5..e069c2fddd1 100644 --- a/spec/features/user_can_display_performance_bar_spec.rb +++ b/spec/features/user_can_display_performance_bar_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' describe 'User can display performance bar', :js do shared_examples 'performance bar cannot be displayed' do it 'does not show the performance bar by default' do - expect(page).not_to have_css('#peek') + expect(page).not_to have_css('#js-peek') end context 'when user press `pb`' do @@ -12,14 +12,14 @@ describe 'User can display performance bar', :js do end it 'does not show the performance bar by default' do - expect(page).not_to have_css('#peek') + expect(page).not_to have_css('#js-peek') end end end shared_examples 'performance bar can be displayed' do it 'does not show the performance bar by default' do - expect(page).not_to have_css('#peek') + expect(page).not_to have_css('#js-peek') end context 'when user press `pb`' do @@ -28,7 +28,7 @@ describe 'User can display performance bar', :js do end it 'shows the performance bar' do - expect(page).to have_css('#peek') + expect(page).to have_css('#js-peek') end end end @@ -41,7 +41,7 @@ describe 'User can display performance bar', :js do it 'shows the performance bar by default' do refresh # Because we're stubbing Rails.env after the 1st visit to root_path - expect(page).to have_css('#peek') + expect(page).to have_css('#js-peek') end end diff --git a/spec/javascripts/behaviors/copy_as_gfm_spec.js b/spec/javascripts/behaviors/copy_as_gfm_spec.js index b8155144e2a..efbe09a10a2 100644 --- a/spec/javascripts/behaviors/copy_as_gfm_spec.js +++ b/spec/javascripts/behaviors/copy_as_gfm_spec.js @@ -1,4 +1,4 @@ -import { CopyAsGFM } from '~/behaviors/copy_as_gfm'; +import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm'; describe('CopyAsGFM', () => { describe('CopyAsGFM.pasteGFM', () => { diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 584db6c6632..d5a87b5ce20 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -1,8 +1,7 @@ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; -import '~/render_math'; -import '~/render_gfm'; +import '~/behaviors/markdown/render_gfm'; import * as urlUtils from '~/lib/utils/url_utility'; import issuableApp from '~/issue_show/components/app.vue'; import eventHub from '~/issue_show/event_hub'; diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js index eb644e698da..dc9dc4d4249 100644 --- a/spec/javascripts/merge_request_notes_spec.js +++ b/spec/javascripts/merge_request_notes_spec.js @@ -3,8 +3,7 @@ import _ from 'underscore'; import 'autosize'; import '~/gl_form'; import '~/lib/utils/text_utility'; -import '~/render_gfm'; -import '~/render_math'; +import '~/behaviors/markdown/render_gfm'; import Notes from '~/notes'; const upArrowKeyCode = 38; diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index ac39418c3e6..0e792eee5e9 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -3,7 +3,7 @@ import _ from 'underscore'; import Vue from 'vue'; import notesApp from '~/notes/components/notes_app.vue'; import service from '~/notes/services/notes_service'; -import '~/render_gfm'; +import '~/behaviors/markdown/render_gfm'; import * as mockData from '../mock_data'; const vueMatchers = { diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index ba0a70bed17..8f317b06792 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -7,7 +7,7 @@ import * as urlUtils from '~/lib/utils/url_utility'; import 'autosize'; import '~/gl_form'; import '~/lib/utils/text_utility'; -import '~/render_gfm'; +import '~/behaviors/markdown/render_gfm'; import Notes from '~/notes'; import timeoutPromise from './helpers/set_timeout_promise_helper'; diff --git a/spec/javascripts/performance_bar/components/detailed_metric_spec.js b/spec/javascripts/performance_bar/components/detailed_metric_spec.js new file mode 100644 index 00000000000..eee0210a2a9 --- /dev/null +++ b/spec/javascripts/performance_bar/components/detailed_metric_spec.js @@ -0,0 +1,88 @@ +import Vue from 'vue'; +import detailedMetric from '~/performance_bar/components/detailed_metric.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('detailedMetric', () => { + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('when the current request has no details', () => { + beforeEach(() => { + vm = mountComponent(Vue.extend(detailedMetric), { + currentRequest: {}, + metric: 'gitaly', + header: 'Gitaly calls', + details: 'details', + keys: ['feature', 'request'], + }); + }); + + it('does not display details', () => { + expect(vm.$el.innerText).not.toContain('/'); + }); + + it('does not display the modal', () => { + expect(vm.$el.querySelector('.performance-bar-modal')).toBeNull(); + }); + + it('displays the metric name', () => { + expect(vm.$el.innerText).toContain('gitaly'); + }); + }); + + describe('when the current request has details', () => { + const requestDetails = [ + { duration: '100', feature: 'find_commit', request: 'abcdef' }, + { duration: '23', feature: 'rebase_in_progress', request: '' }, + ]; + + beforeEach(() => { + vm = mountComponent(Vue.extend(detailedMetric), { + currentRequest: { + details: { + gitaly: { + duration: '123ms', + calls: '456', + details: requestDetails, + }, + }, + }, + metric: 'gitaly', + header: 'Gitaly calls', + details: 'details', + keys: ['feature', 'request'], + }); + }); + + it('diplays details', () => { + expect(vm.$el.innerText.replace(/\s+/g, ' ')).toContain('123ms / 456'); + }); + + it('adds a modal with a table of the details', () => { + vm.$el + .querySelectorAll('.performance-bar-modal td strong') + .forEach((duration, index) => { + expect(duration.innerText).toContain(requestDetails[index].duration); + }); + + vm.$el + .querySelectorAll('.performance-bar-modal td:nth-child(2)') + .forEach((feature, index) => { + expect(feature.innerText).toContain(requestDetails[index].feature); + }); + + vm.$el + .querySelectorAll('.performance-bar-modal td:nth-child(3)') + .forEach((request, index) => { + expect(request.innerText).toContain(requestDetails[index].request); + }); + }); + + it('displays the metric name', () => { + expect(vm.$el.innerText).toContain('gitaly'); + }); + }); +}); diff --git a/spec/javascripts/performance_bar/components/performance_bar_app_spec.js b/spec/javascripts/performance_bar/components/performance_bar_app_spec.js new file mode 100644 index 00000000000..9ab9ab1c9f4 --- /dev/null +++ b/spec/javascripts/performance_bar/components/performance_bar_app_spec.js @@ -0,0 +1,88 @@ +import Vue from 'vue'; +import axios from '~/lib/utils/axios_utils'; +import performanceBarApp from '~/performance_bar/components/performance_bar_app.vue'; +import PerformanceBarService from '~/performance_bar/services/performance_bar_service'; +import PerformanceBarStore from '~/performance_bar/stores/performance_bar_store'; + +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import MockAdapter from 'axios-mock-adapter'; + +describe('performance bar', () => { + let mock; + let vm; + + beforeEach(() => { + const store = new PerformanceBarStore(); + + mock = new MockAdapter(axios); + + mock.onGet('/-/peek/results').reply( + 200, + { + data: { + gc: { + invokes: 0, + invoke_time: '0.00', + use_size: 0, + total_size: 0, + total_object: 0, + gc_time: '0.00', + }, + host: { hostname: 'web-01' }, + }, + }, + {}, + ); + + vm = mountComponent(Vue.extend(performanceBarApp), { + store, + env: 'development', + requestId: '123', + peekUrl: '/-/peek/results', + profileUrl: '?lineprofiler=true', + }); + }); + + afterEach(() => { + vm.$destroy(); + mock.restore(); + }); + + it('sets the class to match the environment', () => { + expect(vm.$el.getAttribute('class')).toContain('development'); + }); + + describe('loadRequestDetails', () => { + beforeEach(() => { + spyOn(vm.store, 'addRequest').and.callThrough(); + }); + + it('does nothing if the request cannot be tracked', () => { + spyOn(vm.store, 'canTrackRequest').and.callFake(() => false); + + vm.loadRequestDetails('123', 'https://gitlab.com/'); + + expect(vm.store.addRequest).not.toHaveBeenCalled(); + }); + + it('adds the request immediately', () => { + vm.loadRequestDetails('123', 'https://gitlab.com/'); + + expect(vm.store.addRequest).toHaveBeenCalledWith( + '123', + 'https://gitlab.com/', + ); + }); + + it('makes an HTTP request for the request details', () => { + spyOn(PerformanceBarService, 'fetchRequestDetails').and.callThrough(); + + vm.loadRequestDetails('456', 'https://gitlab.com/'); + + expect(PerformanceBarService.fetchRequestDetails).toHaveBeenCalledWith( + '/-/peek/results', + '456', + ); + }); + }); +}); diff --git a/spec/javascripts/performance_bar/components/request_selector_spec.js b/spec/javascripts/performance_bar/components/request_selector_spec.js new file mode 100644 index 00000000000..6108a29f8c4 --- /dev/null +++ b/spec/javascripts/performance_bar/components/request_selector_spec.js @@ -0,0 +1,47 @@ +import Vue from 'vue'; +import requestSelector from '~/performance_bar/components/request_selector.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('request selector', () => { + const requests = [ + { id: '123', url: 'https://gitlab.com/' }, + { + id: '456', + url: 'https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1', + }, + { + id: '789', + url: + 'https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1.json?serializer=widget', + }, + ]; + + let vm; + + beforeEach(() => { + vm = mountComponent(Vue.extend(requestSelector), { + requests, + currentRequest: requests[1], + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + function optionText(requestId) { + return vm.$el.querySelector(`[value='${requestId}']`).innerText.trim(); + } + + it('displays the last component of the path', () => { + expect(optionText(requests[2].id)).toEqual('1.json?serializer=widget'); + }); + + it('keeps the last two components of the path when the last component is numeric', () => { + expect(optionText(requests[1].id)).toEqual('merge_requests/1'); + }); + + it('ignores trailing slashes', () => { + expect(optionText(requests[0].id)).toEqual('gitlab.com'); + }); +}); diff --git a/spec/javascripts/performance_bar/components/simple_metric_spec.js b/spec/javascripts/performance_bar/components/simple_metric_spec.js new file mode 100644 index 00000000000..98b843e9711 --- /dev/null +++ b/spec/javascripts/performance_bar/components/simple_metric_spec.js @@ -0,0 +1,47 @@ +import Vue from 'vue'; +import simpleMetric from '~/performance_bar/components/simple_metric.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('simpleMetric', () => { + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('when the current request has no details', () => { + beforeEach(() => { + vm = mountComponent(Vue.extend(simpleMetric), { + currentRequest: {}, + metric: 'gitaly', + }); + }); + + it('does not display details', () => { + expect(vm.$el.innerText).not.toContain('/'); + }); + + it('displays the metric name', () => { + expect(vm.$el.innerText).toContain('gitaly'); + }); + }); + + describe('when the current request has details', () => { + beforeEach(() => { + vm = mountComponent(Vue.extend(simpleMetric), { + currentRequest: { + details: { gitaly: { duration: '123ms', calls: '456' } }, + }, + metric: 'gitaly', + }); + }); + + it('diplays details', () => { + expect(vm.$el.innerText.replace(/\s+/g, ' ')).toContain('123ms / 456'); + }); + + it('displays the metric name', () => { + expect(vm.$el.innerText).toContain('gitaly'); + }); + }); +}); diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index faaf710cf6f..b0d714cbefb 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import initCopyAsGFM from '~/behaviors/copy_as_gfm'; +import initCopyAsGFM from '~/behaviors/markdown/copy_as_gfm'; import ShortcutsIssuable from '~/shortcuts_issuable'; initCopyAsGFM(); diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 57905a74e92..8351b967133 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -83,19 +83,19 @@ describe Gitlab::ProjectSearchResults do end context 'when the matching filename contains a colon' do - let(:search_result) { "\nmaster:testdata/project::function1.yaml\x001\x00---\n" } + let(:search_result) { "master:testdata/project::function1.yaml\x001\x00---\n" } it 'returns a valid FoundBlob' do expect(subject.filename).to eq('testdata/project::function1.yaml') expect(subject.basename).to eq('testdata/project::function1') expect(subject.ref).to eq('master') expect(subject.startline).to eq(1) - expect(subject.data).to eq('---') + expect(subject.data).to eq("---\n") end end context 'when the matching content contains a number surrounded by colons' do - let(:search_result) { "\nmaster:testdata/foo.txt\x001\x00blah:9:blah" } + let(:search_result) { "master:testdata/foo.txt\x001\x00blah:9:blah" } it 'returns a valid FoundBlob' do expect(subject.filename).to eq('testdata/foo.txt') @@ -106,6 +106,18 @@ describe Gitlab::ProjectSearchResults do end end + context 'when the search result ends with an empty line' do + let(:results) { project.repository.search_files_by_content('Role models', 'master') } + + it 'returns a valid FoundBlob that ends with an empty line' do + expect(subject.filename).to eq('files/markdown/ruby-style-guide.md') + expect(subject.basename).to eq('files/markdown/ruby-style-guide') + expect(subject.ref).to eq('master') + expect(subject.startline).to eq(1) + expect(subject.data).to eq("# Prelude\n\n> Role models are important. <br/>\n> -- Officer Alex J. Murphy / RoboCop\n\n") + end + end + context 'when the search returns non-ASCII data' do context 'with UTF-8' do let(:results) { project.repository.search_files_by_content('файл', 'master') } @@ -115,7 +127,7 @@ describe Gitlab::ProjectSearchResults do expect(subject.basename).to eq('encoding/russian') expect(subject.ref).to eq('master') expect(subject.startline).to eq(1) - expect(subject.data).to eq('Хороший файл') + expect(subject.data).to eq("Хороший файл\n") end end @@ -139,7 +151,7 @@ describe Gitlab::ProjectSearchResults do expect(subject.basename).to eq('encoding/iso8859') expect(subject.ref).to eq('master') expect(subject.startline).to eq(1) - expect(subject.data).to eq("Äü\n\nfoo") + expect(subject.data).to eq("Äü\n\nfoo\n") end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index a3893188c6e..e28b0ea5cf2 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -743,7 +743,7 @@ describe SystemNoteService do expect(cross_reference(type)).to eq("Events for #{type.pluralize.humanize.downcase} are disabled.") end - it "blocks cross reference when #{type.underscore}_events is true" do + it "creates cross reference when #{type.underscore}_events is true" do jira_tracker.update("#{type}_events" => true) expect(cross_reference(type)).to eq(success_message) diff --git a/spec/views/projects/services/_form.haml_spec.rb b/spec/views/projects/services/_form.haml_spec.rb new file mode 100644 index 00000000000..85167bca115 --- /dev/null +++ b/spec/views/projects/services/_form.haml_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe 'projects/services/_form' do + let(:project) { create(:redmine_project) } + let(:user) { create(:admin) } + + before do + assign(:project, project) + + allow(controller).to receive(:current_user).and_return(user) + + allow(view).to receive_messages(current_user: user, + can?: true, + current_application_settings: Gitlab::CurrentSettings.current_application_settings) + end + + context 'commit_events and merge_request_events' do + before do + assign(:service, project.redmine_service) + end + + it 'display merge_request_events and commit_events descriptions' do + allow(RedmineService).to receive(:supported_events).and_return(%w(commit merge_request)) + + render + + expect(rendered).to have_content('Event will be triggered when a commit is created/updated') + expect(rendered).to have_content('Event will be triggered when a merge request is created/updated/merged') + end + + context 'when service is JIRA' do + let(:project) { create(:jira_project) } + + before do + assign(:service, project.jira_service) + end + + it 'display merge_request_events and commit_events descriptions' do + render + + expect(rendered).to have_content('JIRA comments will be created when an issue gets referenced in a commit.') + expect(rendered).to have_content('JIRA comments will be created when an issue gets referenced in a merge request.') + end + end + end +end diff --git a/vendor/assets/javascripts/peek.js b/vendor/assets/javascripts/peek.js deleted file mode 100644 index 7c6d226fa6a..00000000000 --- a/vendor/assets/javascripts/peek.js +++ /dev/null @@ -1,86 +0,0 @@ -/* - * this is a modified version of https://github.com/peek/peek/blob/master/app/assets/javascripts/peek.js - * - * - Removed the dependency on jquery.tipsy - * - Removed the initializeTipsy and toggleBar functions - * - Customized updatePerformanceBar to handle SQL query and Gitaly call lists - * - Changed /peek/results to /-/peek/results - * - Removed the keypress, pjax:end, page:change, and turbolinks:load handlers - */ -(function($) { - var fetchRequestResults, getRequestId, peekEnabled, updatePerformanceBar, createTable, createTableRow; - getRequestId = function() { - return $('#peek').data('requestId'); - }; - peekEnabled = function() { - return $('#peek').length; - }; - updatePerformanceBar = function(results) { - Object.keys(results.data).forEach(function(key) { - Object.keys(results.data[key]).forEach(function(label) { - var data = results.data[key][label]; - var table = createTable(key, label, data); - var target = $('[data-defer-to="' + key + '-' + label + '"]'); - - if (table) { - target.html(table); - } else { - target.text(data); - } - }); - }); - return $(document).trigger('peek:render', [getRequestId(), results]); - }; - createTable = function(key, label, data) { - if (label !== 'queries' && label !== 'details') { - return; - } - - var table = document.createElement('table'); - - for (var i = 0; i < data.length; i += 1) { - table.appendChild(createTableRow(data[i])); - } - - table.className = 'table'; - - return table; - }; - createTableRow = function(row) { - var tr = document.createElement('tr'); - var durationTd = document.createElement('td'); - var strong = document.createElement('strong'); - - strong.append(row['duration'] + 'ms'); - durationTd.appendChild(strong); - tr.appendChild(durationTd); - - ['sql', 'feature', 'enabled', 'request'].forEach(function(key) { - if (!row[key]) { return; } - - var td = document.createElement('td'); - - td.appendChild(document.createTextNode(row[key])); - tr.appendChild(td); - }); - - return tr; - }; - fetchRequestResults = function() { - return $.ajax('/-/peek/results', { - data: { - request_id: getRequestId() - }, - success: function(data, textStatus, xhr) { - return updatePerformanceBar(data); - }, - error: function(xhr, textStatus, error) {} - }); - }; - $(document).on('peek:update', fetchRequestResults); - return $(function() { - if (peekEnabled()) { - return $(this).trigger('peek:update'); - } - }); -})(jQuery); |