summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.lock11
-rw-r--r--app/assets/javascripts/behaviors/index.js3
-rw-r--r--app/assets/javascripts/behaviors/markdown/copy_as_gfm.js (renamed from app/assets/javascripts/behaviors/copy_as_gfm.js)4
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js (renamed from app/assets/javascripts/render_gfm.js)2
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_math.js (renamed from app/assets/javascripts/render_math.js)4
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_mermaid.js (renamed from app/assets/javascripts/render_mermaid.js)6
-rw-r--r--app/assets/javascripts/dispatcher.js12
-rw-r--r--app/assets/javascripts/main.js1
-rw-r--r--app/assets/javascripts/performance_bar.js57
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue78
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue191
-rw-r--r--app/assets/javascripts/performance_bar/components/request_selector.vue52
-rw-r--r--app/assets/javascripts/performance_bar/components/simple_metric.vue30
-rw-r--r--app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue18
-rw-r--r--app/assets/javascripts/performance_bar/index.js37
-rw-r--r--app/assets/javascripts/performance_bar/services/performance_bar_service.js24
-rw-r--r--app/assets/javascripts/performance_bar/stores/performance_bar_store.js39
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js2
-rw-r--r--app/assets/stylesheets/framework/header.scss108
-rw-r--r--app/assets/stylesheets/pages/notes.scss6
-rw-r--r--app/assets/stylesheets/performance_bar.scss29
-rw-r--r--app/models/notification_recipient.rb3
-rw-r--r--app/views/peek/_bar.html.haml12
-rw-r--r--app/views/peek/views/_gitaly.html.haml17
-rw-r--r--app/views/peek/views/_host.html.haml2
-rw-r--r--app/views/peek/views/_mysql2.html.haml4
-rw-r--r--app/views/peek/views/_pg.html.haml4
-rw-r--r--app/views/peek/views/_rblineprof.html.haml7
-rw-r--r--app/views/peek/views/_sql.html.haml14
-rw-r--r--changelogs/unreleased/43933-always-notify-mentions.yml6
-rw-r--r--changelogs/unreleased/44330-docs-for-ingress-ip.yml5
-rw-r--r--changelogs/unreleased/44383-cleanup-framework-header.yml5
-rw-r--r--changelogs/unreleased/44384-cleanup-css-for-nested-lists.yml5
-rw-r--r--changelogs/unreleased/ajax-requests-in-performance-bar.yml5
-rw-r--r--changelogs/unreleased/optional-api-delimiter.yml5
-rw-r--r--config/initializers/active_record_locking.rb108
-rw-r--r--config/routes.rb2
-rw-r--r--doc/administration/monitoring/performance/performance_bar.md6
-rw-r--r--doc/api/search.md26
-rw-r--r--doc/user/project/clusters/index.md29
-rw-r--r--doc/user/project/integrations/prometheus.md30
-rw-r--r--lib/api/search.rb4
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb4
-rw-r--r--lib/peek/views/host.rb9
-rw-r--r--spec/features/markdown/copy_as_gfm_spec.rb2
-rw-r--r--spec/features/user_can_display_performance_bar_spec.rb10
-rw-r--r--spec/javascripts/behaviors/copy_as_gfm_spec.js2
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js3
-rw-r--r--spec/javascripts/merge_request_notes_spec.js3
-rw-r--r--spec/javascripts/notes/components/note_app_spec.js2
-rw-r--r--spec/javascripts/notes_spec.js2
-rw-r--r--spec/javascripts/performance_bar/components/detailed_metric_spec.js88
-rw-r--r--spec/javascripts/performance_bar/components/performance_bar_app_spec.js88
-rw-r--r--spec/javascripts/performance_bar/components/request_selector_spec.js47
-rw-r--r--spec/javascripts/performance_bar/components/simple_metric_spec.js47
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js2
-rw-r--r--spec/requests/api/search_spec.rb48
-rw-r--r--spec/services/notification_service_spec.rb21
-rw-r--r--vendor/assets/javascripts/peek.js86
60 files changed, 1071 insertions, 409 deletions
diff --git a/Gemfile b/Gemfile
index bbb97cd696b..e423f4ba32f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -113,7 +113,7 @@ gem 'fog-rackspace', '~> 0.1.1'
gem 'fog-aliyun', '~> 0.2.0'
# for Google storage
-gem 'google-api-client', '~> 0.13.6'
+gem 'google-api-client', '~> 0.19.8'
# for aws storage
gem 'unf', '~> 0.1.4'
@@ -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 817772dfdcb..1c6c7edb1a0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -336,9 +336,9 @@ GEM
json
multi_json
request_store (>= 1.0)
- google-api-client (0.13.6)
+ google-api-client (0.19.8)
addressable (~> 2.5, >= 2.5.1)
- googleauth (~> 0.5)
+ googleauth (>= 0.5, < 0.7.0)
httpclient (>= 2.8.1, < 3.0)
mime-types (~> 3.0)
representable (~> 3.0)
@@ -415,7 +415,7 @@ GEM
httparty (0.13.7)
json (~> 1.8)
multi_xml (>= 0.5.2)
- httpclient (2.8.2)
+ httpclient (2.8.3)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
@@ -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
@@ -1067,7 +1065,7 @@ DEPENDENCIES
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
- google-api-client (~> 0.13.6)
+ google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)
gpgme
grape (~> 1.0)
@@ -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/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index bea58bade9d..0136af76a13 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -1,60 +1,24 @@
.navbar-gitlab {
- &.navbar-gitlab {
- padding: 0 16px;
- z-index: 1000;
- margin-bottom: 0;
- min-height: $header-height;
- border: 0;
- border-bottom: 1px solid $border-color;
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- border-radius: 0;
-
- .logo-text {
- line-height: initial;
-
- svg {
- width: 55px;
- height: 14px;
- margin: 0;
- fill: $white-light;
- }
- }
-
- .container-fluid {
- padding: 0;
-
- .user-counter {
- svg {
- margin-right: 3px;
- }
- }
-
- .navbar-toggle {
- right: -10px;
- border-radius: 0;
- min-width: 45px;
- padding: 0;
- margin-right: -7px;
- font-size: 14px;
- text-align: center;
- color: currentColor;
-
- &:hover,
- &:focus,
- &.active {
- color: currentColor;
- background-color: transparent;
- }
-
- .more-icon,
- .close-icon {
- fill: $white-light;
- margin: auto;
- }
- }
+ padding: 0 16px;
+ z-index: 1000;
+ margin-bottom: 0;
+ min-height: $header-height;
+ border: 0;
+ border-bottom: 1px solid $border-color;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ border-radius: 0;
+
+ .logo-text {
+ line-height: initial;
+
+ svg {
+ width: 55px;
+ height: 14px;
+ margin: 0;
+ fill: $white-light;
}
}
@@ -184,6 +148,38 @@
}
.container-fluid {
+ padding: 0;
+
+ .user-counter {
+ svg {
+ margin-right: 3px;
+ }
+ }
+
+ .navbar-toggle {
+ right: -10px;
+ border-radius: 0;
+ min-width: 45px;
+ padding: 0;
+ margin-right: -7px;
+ font-size: 14px;
+ text-align: center;
+ color: currentColor;
+
+ &:hover,
+ &:focus,
+ &.active {
+ color: currentColor;
+ background-color: transparent;
+ }
+
+ .more-icon,
+ .close-icon {
+ fill: $white-light;
+ margin: auto;
+ }
+ }
+
.navbar-nav {
@media (max-width: $screen-xs-max) {
display: -webkit-flex;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 085a2e74328..81e98f358a8 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -140,12 +140,6 @@ ul.notes {
@include bulleted-list;
word-wrap: break-word;
- ul.task-list {
- ul:not(.task-list) {
- padding-left: 1.3em;
- }
- }
-
table {
@include markdown-table;
}
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/models/notification_recipient.rb b/app/models/notification_recipient.rb
index fd70e920c7e..e95655e19f8 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -35,7 +35,8 @@ class NotificationRecipient
# check this last because it's expensive
# nobody should receive notifications if they've specifically unsubscribed
- return false if unsubscribed?
+ # except if they were mentioned.
+ return false if @type != :mention && unsubscribed?
true
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' }
- &times;
- %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' }
- &times;
- %h4
- SQL queries
- .modal-body{ data: { defer_to: "#{view.defer_key}-queries" } }...
diff --git a/changelogs/unreleased/43933-always-notify-mentions.yml b/changelogs/unreleased/43933-always-notify-mentions.yml
new file mode 100644
index 00000000000..7b494d38541
--- /dev/null
+++ b/changelogs/unreleased/43933-always-notify-mentions.yml
@@ -0,0 +1,6 @@
+---
+title: Send @mention notifications even if a user has explicitly unsubscribed from
+ item
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/44330-docs-for-ingress-ip.yml b/changelogs/unreleased/44330-docs-for-ingress-ip.yml
new file mode 100644
index 00000000000..3dfaea6e17e
--- /dev/null
+++ b/changelogs/unreleased/44330-docs-for-ingress-ip.yml
@@ -0,0 +1,5 @@
+---
+title: Add documentation for displayed K8s Ingress IP address (#44330)
+merge_request: 17836
+author:
+type: other
diff --git a/changelogs/unreleased/44383-cleanup-framework-header.yml b/changelogs/unreleased/44383-cleanup-framework-header.yml
new file mode 100644
index 00000000000..ef9be9f48de
--- /dev/null
+++ b/changelogs/unreleased/44383-cleanup-framework-header.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up selectors in framework/header.scss
+merge_request: 17822
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/44384-cleanup-css-for-nested-lists.yml b/changelogs/unreleased/44384-cleanup-css-for-nested-lists.yml
new file mode 100644
index 00000000000..79c470ea4e1
--- /dev/null
+++ b/changelogs/unreleased/44384-cleanup-css-for-nested-lists.yml
@@ -0,0 +1,5 @@
+---
+title: Unify format for nested non-task lists
+merge_request: 17823
+author: Takuya Noguchi
+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/optional-api-delimiter.yml b/changelogs/unreleased/optional-api-delimiter.yml
new file mode 100644
index 00000000000..0bcd0787306
--- /dev/null
+++ b/changelogs/unreleased/optional-api-delimiter.yml
@@ -0,0 +1,5 @@
+---
+title: Make /-/ delimiter optional for search endpoints
+merge_request:
+author:
+type: changed
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):
![SQL profiling using the Performance Bar](img/performance_bar_sql_queries.png)
- time taken and number of [Gitaly] calls, click through for details of these calls
![Gitaly profiling using the Performance Bar](img/performance_bar_gitaly_calls.png)
-- 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)).
![Line profiling using the Performance Bar](img/performance_bar_line_profiling.png)
- 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/doc/api/search.md b/doc/api/search.md
index d441b556186..107ddaffa6a 100644
--- a/doc/api/search.md
+++ b/doc/api/search.md
@@ -289,7 +289,7 @@ Search within the specified group.
If a user is not a member of a group and the group is private, a `GET` request on that group will result to a `404` status code.
```
-GET /groups/:id/-/search
+GET /groups/:id/search
```
| Attribute | Type | Required | Description |
@@ -305,7 +305,7 @@ The response depends on the requested scope.
### Scope: projects
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/-/search?scope=projects&search=flight
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/search?scope=projects&search=flight
```
Example response:
@@ -336,7 +336,7 @@ Example response:
### Scope: issues
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/-/search?scope=issues&search=file
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/search?scope=issues&search=file
```
Example response:
@@ -401,7 +401,7 @@ Example response:
### Scope: merge_requests
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/-/search?scope=merge_requests&search=file
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/search?scope=merge_requests&search=file
```
Example response:
@@ -478,7 +478,7 @@ Example response:
### Scope: milestones
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/-/search?scope=milestones&search=release
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/search?scope=milestones&search=release
```
Example response:
@@ -507,7 +507,7 @@ Search within the specified project.
If a user is not a member of a project and the project is private, a `GET` request on that project will result to a `404` status code.
```
-GET /projects/:id/-/search
+GET /projects/:id/search
```
| Attribute | Type | Required | Description |
@@ -524,7 +524,7 @@ The response depends on the requested scope.
### Scope: issues
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/12/-/search?scope=issues&search=file
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/12/search?scope=issues&search=file
```
Example response:
@@ -589,7 +589,7 @@ Example response:
### Scope: merge_requests
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/-/search?scope=merge_requests&search=file
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=merge_requests&search=file
```
Example response:
@@ -666,7 +666,7 @@ Example response:
### Scope: milestones
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/12/-/search?scope=milestones&search=release
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/12/search?scope=milestones&search=release
```
Example response:
@@ -691,7 +691,7 @@ Example response:
### Scope: notes
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/-/search?scope=notes&search=maxime
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=notes&search=maxime
```
Example response:
@@ -723,7 +723,7 @@ Example response:
### Scope: wiki_blobs
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/-/search?scope=wiki_blobs&search=bye
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=wiki_blobs&search=bye
```
Example response:
@@ -746,7 +746,7 @@ Example response:
### Scope: commits
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/-/search?scope=commits&search=bye
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=commits&search=bye
```
Example response:
@@ -777,7 +777,7 @@ Example response:
### Scope: blobs
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/-/search?scope=blobs&search=installation
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=blobs&search=installation
```
Example response:
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 661697aaeb7..bd9bcfadb99 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -167,6 +167,17 @@ external IP address with the following procedure. It can be deployed using the
In order to publish your web application, you first need to find the external IP
address associated to your load balancer.
+### Let GitLab fetch the IP address
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17052) in GitLab 10.6.
+
+If you installed the Ingress [via the **Applications**](#installing-applications),
+you should see the Ingress IP address on this same page within a few minutes.
+If you don't see this, GitLab might not be able to determine the IP address of
+your ingress application in which case you should manually determine it.
+
+### Manually determining the IP address
+
If the cluster is on GKE, click on the **Google Kubernetes Engine** link in the
**Advanced settings**, or go directly to the
[Google Kubernetes Engine dashboard](https://console.cloud.google.com/kubernetes/)
@@ -193,6 +204,24 @@ The output is the external IP address of your cluster. This information can then
be used to set up DNS entries and forwarding rules that allow external access to
your deployed applications.
+### Using a static IP
+
+By default, an ephemeral external IP address is associated to the cluster's load
+balancer. If you associate the ephemeral IP with your DNS and the IP changes,
+your apps will not be able to be reached, and you'd have to change the DNS
+record again. In order to avoid that, you should change it into a static
+reserved IP.
+
+[Read how to promote an ephemeral external IP address in GKE.](https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address#promote_ephemeral_ip)
+
+### Pointing your DNS at the cluster IP
+
+Once you've set up the static IP, you should associate it to a [wildcard DNS
+record](https://en.wikipedia.org/wiki/Wildcard_DNS_record), in order to be able
+to reach your apps. This heavily depends on your domain provider, but in case
+you aren't sure, just create an A record with a wildcard host like
+`*.example.com.`.
+
## Setting the environment scope
NOTE: **Note:**
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index 249463fb86e..fa7e504c4aa 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -2,7 +2,7 @@
> [Introduced][ce-8935] in GitLab 9.0.
-GitLab offers powerful integration with [Prometheus] for monitoring key metrics your apps, directly within GitLab.
+GitLab offers powerful integration with [Prometheus] for monitoring key metrics of your apps, directly within GitLab.
Metrics for each environment are retrieved from Prometheus, and then displayed
within the GitLab interface.
@@ -12,17 +12,21 @@ There are two ways to setup Prometheus integration, depending on where your apps
* For deployments on Kubernetes, GitLab can automatically [deploy and manage Prometheus](#managed-prometheus-on-kubernetes)
* For other deployment targets, simply [specify the Prometheus server](#manual-configuration-of-prometheus).
-## Managed Prometheus on Kubernetes
+Once enabled, GitLab will automatically detect metrics from known services in the [metric library](#monitoring-ci-cd-environments).
+
+## Enabling Prometheus Integration
+
+### Managed Prometheus on Kubernetes
> **Note**: [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/28916) in GitLab 10.5
GitLab can seamlessly deploy and manage Prometheus on a [connected Kubernetes cluster](../clusters/index.md), making monitoring of your apps easy.
-### Requirements
+#### Requirements
* A [connected Kubernetes cluster](../clusters/index.md)
* Helm Tiller [installed by GitLab](../clusters/index.md#installing-applications)
-### Getting started
+#### Getting started
Once you have a connected Kubernetes cluster with Helm installed, deploying a managed Prometheus is as easy as a single click.
@@ -32,7 +36,7 @@ Once you have a connected Kubernetes cluster with Helm installed, deploying a ma
![Managed Prometheus Deploy](img/prometheus_deploy.png)
-### About managed Prometheus deployments
+#### About managed Prometheus deployments
Prometheus is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/prometheus). Prometheus is only accessible within the cluster, with GitLab communicating through the [Kubernetes API](https://kubernetes.io/docs/concepts/overview/kubernetes-api/).
@@ -45,9 +49,9 @@ CPU and Memory consumption is monitored, but requires [naming conventions](prome
The [NGINX Ingress](../clusters/index.md#installing-applications) that is deployed by GitLab to clusters, is automatically annotated for monitoring providing key response metrics: latency, throughput, and error rates.
-## Manual configuration of Prometheus
+### Manual configuration of Prometheus
-### Requirements
+#### Requirements
Integration with Prometheus requires the following:
@@ -56,7 +60,7 @@ Integration with Prometheus requires the following:
1. Each metric must be have a label to indicate the environment
1. GitLab must have network connectivity to the Prometheus server
-### Getting started
+#### Getting started
Installing and configuring Prometheus to monitor applications is fairly straight forward.
@@ -64,7 +68,7 @@ Installing and configuring Prometheus to monitor applications is fairly straight
1. Set up one of the [supported monitoring targets](prometheus_library/metrics.md)
1. Configure the Prometheus server to [collect their metrics](https://prometheus.io/docs/operating/configuration/#scrape_config)
-### Configuration in GitLab
+#### Configuration in GitLab
The actual configuration of Prometheus integration within GitLab is very simple.
All you will need is the DNS or IP address of the Prometheus server you'd like
@@ -83,9 +87,9 @@ to integrate with.
Once configured, GitLab will attempt to retrieve performance metrics for any
environment which has had a successful deployment.
-GitLab will automatically scan the Prometheus server for known metrics and attempt to identify the metrics for a particular environment. The supported metrics and scan process is detailed in our [Prometheus Metric Library documentation](prometheus_library/metrics.html).
+GitLab will automatically scan the Prometheus server for metrics from known serves like Kubernetes and NGINX, and attempt to identify individual environment. The supported metrics and scan process is detailed in our [Prometheus Metric Library documentation](prometheus_library/metrics.html).
-[Learn more about monitoring environments.](../../../ci/environments.md#monitoring-environments)
+You can view the performance dashboard for an environment by [clicking on the monitoring button](../../../ci/environments.md#monitoring-environments).
## Determining the performance impact of a merge
@@ -93,7 +97,7 @@ GitLab will automatically scan the Prometheus server for known metrics and attem
> GitLab 9.3 added the [numeric comparison](https://gitlab.com/gitlab-org/gitlab-ce/issues/27439) of the 30 minute averages.
> Requires [Kubernetes](prometheus_library/kubernetes.md) metrics
-Developers can view theperformance impact of their changes within the merge
+Developers can view the performance impact of their changes within the merge
request workflow. When a source branch has been deployed to an environment, a sparkline and numeric comparison of the average memory consumption will appear. On the sparkline, a dot
indicates when the current changes were deployed, with up to 30 minutes of
performance data displayed before and after. The comparison shows the difference between the 30 minute average before and after the deployment. This information is updated after
@@ -109,7 +113,7 @@ Prometheus server.
## Troubleshooting
-If the "Attempting to load performance data" screen continues to appear, it could be due to:
+If the "No data found" screen continues to appear, it could be due to:
- No successful deployments have occurred to this environment.
- Prometheus does not have performance data for this environment, or the metrics
diff --git a/lib/api/search.rb b/lib/api/search.rb
index 3556ad98c52..5d9ec617cb7 100644
--- a/lib/api/search.rb
+++ b/lib/api/search.rb
@@ -84,7 +84,7 @@ module API
values: %w(projects issues merge_requests milestones)
use :pagination
end
- get ':id/-/search' do
+ get ':id/(-/)search' do
present search(group_id: user_group.id), with: entity
end
end
@@ -103,7 +103,7 @@ module API
values: %w(issues merge_requests milestones notes wiki_blobs commits blobs)
use :pagination
end
- get ':id/-/search' do
+ get ':id/(-/)search' do
present search(project_id: user_project.id), with: entity
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/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/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 9052a18c60b..f8d5258a8d9 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -99,10 +99,10 @@ describe API::Search do
end
end
- describe "GET /groups/:id/-/search" do
+ describe "GET /groups/:id/search" do
context 'when user is not authenticated' do
it 'returns 401 error' do
- get api("/groups/#{group.id}/-/search"), scope: 'projects', search: 'awesome'
+ get api("/groups/#{group.id}/search"), scope: 'projects', search: 'awesome'
expect(response).to have_gitlab_http_status(401)
end
@@ -110,7 +110,7 @@ describe API::Search do
context 'when scope is not supported' do
it 'returns 400 error' do
- get api("/groups/#{group.id}/-/search", user), scope: 'unsupported', search: 'awesome'
+ get api("/groups/#{group.id}/search", user), scope: 'unsupported', search: 'awesome'
expect(response).to have_gitlab_http_status(400)
end
@@ -118,7 +118,7 @@ describe API::Search do
context 'when scope is missing' do
it 'returns 400 error' do
- get api("/groups/#{group.id}/-/search", user), search: 'awesome'
+ get api("/groups/#{group.id}/search", user), search: 'awesome'
expect(response).to have_gitlab_http_status(400)
end
@@ -126,7 +126,7 @@ describe API::Search do
context 'when group does not exist' do
it 'returns 404 error' do
- get api('/groups/9999/-/search', user), scope: 'issues', search: 'awesome'
+ get api('/groups/9999/search', user), scope: 'issues', search: 'awesome'
expect(response).to have_gitlab_http_status(404)
end
@@ -136,7 +136,7 @@ describe API::Search do
it 'returns 404 error' do
private_group = create(:group, :private)
- get api("/groups/#{private_group.id}/-/search", user), scope: 'issues', search: 'awesome'
+ get api("/groups/#{private_group.id}/search", user), scope: 'issues', search: 'awesome'
expect(response).to have_gitlab_http_status(404)
end
@@ -145,7 +145,7 @@ describe API::Search do
context 'with correct params' do
context 'for projects scope' do
before do
- get api("/groups/#{group.id}/-/search", user), scope: 'projects', search: 'awesome'
+ get api("/groups/#{group.id}/search", user), scope: 'projects', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/projects'
@@ -155,7 +155,7 @@ describe API::Search do
before do
create(:issue, project: project, title: 'awesome issue')
- get api("/groups/#{group.id}/-/search", user), scope: 'issues', search: 'awesome'
+ get api("/groups/#{group.id}/search", user), scope: 'issues', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/issues'
@@ -165,7 +165,7 @@ describe API::Search do
before do
create(:merge_request, source_project: repo_project, title: 'awesome mr')
- get api("/groups/#{group.id}/-/search", user), scope: 'merge_requests', search: 'awesome'
+ get api("/groups/#{group.id}/search", user), scope: 'merge_requests', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/merge_requests'
@@ -175,7 +175,7 @@ describe API::Search do
before do
create(:milestone, project: project, title: 'awesome milestone')
- get api("/groups/#{group.id}/-/search", user), scope: 'milestones', search: 'awesome'
+ get api("/groups/#{group.id}/search", user), scope: 'milestones', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
@@ -187,7 +187,7 @@ describe API::Search do
create(:milestone, project: project, title: 'awesome milestone')
create(:milestone, project: another_project, title: 'awesome milestone other project')
- get api("/groups/#{CGI.escape(group.full_path)}/-/search", user), scope: 'milestones', search: 'awesome'
+ get api("/groups/#{CGI.escape(group.full_path)}/search", user), scope: 'milestones', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
@@ -198,7 +198,7 @@ describe API::Search do
describe "GET /projects/:id/search" do
context 'when user is not authenticated' do
it 'returns 401 error' do
- get api("/projects/#{project.id}/-/search"), scope: 'issues', search: 'awesome'
+ get api("/projects/#{project.id}/search"), scope: 'issues', search: 'awesome'
expect(response).to have_gitlab_http_status(401)
end
@@ -206,7 +206,7 @@ describe API::Search do
context 'when scope is not supported' do
it 'returns 400 error' do
- get api("/projects/#{project.id}/-/search", user), scope: 'unsupported', search: 'awesome'
+ get api("/projects/#{project.id}/search", user), scope: 'unsupported', search: 'awesome'
expect(response).to have_gitlab_http_status(400)
end
@@ -214,7 +214,7 @@ describe API::Search do
context 'when scope is missing' do
it 'returns 400 error' do
- get api("/projects/#{project.id}/-/search", user), search: 'awesome'
+ get api("/projects/#{project.id}/search", user), search: 'awesome'
expect(response).to have_gitlab_http_status(400)
end
@@ -222,7 +222,7 @@ describe API::Search do
context 'when project does not exist' do
it 'returns 404 error' do
- get api('/projects/9999/-/search', user), scope: 'issues', search: 'awesome'
+ get api('/projects/9999/search', user), scope: 'issues', search: 'awesome'
expect(response).to have_gitlab_http_status(404)
end
@@ -232,7 +232,7 @@ describe API::Search do
it 'returns 404 error' do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- get api("/projects/#{project.id}/-/search", user), scope: 'issues', search: 'awesome'
+ get api("/projects/#{project.id}/search", user), scope: 'issues', search: 'awesome'
expect(response).to have_gitlab_http_status(404)
end
@@ -243,7 +243,7 @@ describe API::Search do
before do
create(:issue, project: project, title: 'awesome issue')
- get api("/projects/#{project.id}/-/search", user), scope: 'issues', search: 'awesome'
+ get api("/projects/#{project.id}/search", user), scope: 'issues', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/issues'
@@ -253,7 +253,7 @@ describe API::Search do
before do
create(:merge_request, source_project: repo_project, title: 'awesome mr')
- get api("/projects/#{repo_project.id}/-/search", user), scope: 'merge_requests', search: 'awesome'
+ get api("/projects/#{repo_project.id}/search", user), scope: 'merge_requests', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/merge_requests'
@@ -263,7 +263,7 @@ describe API::Search do
before do
create(:milestone, project: project, title: 'awesome milestone')
- get api("/projects/#{project.id}/-/search", user), scope: 'milestones', search: 'awesome'
+ get api("/projects/#{project.id}/search", user), scope: 'milestones', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
@@ -273,7 +273,7 @@ describe API::Search do
before do
create(:note_on_merge_request, project: project, note: 'awesome note')
- get api("/projects/#{project.id}/-/search", user), scope: 'notes', search: 'awesome'
+ get api("/projects/#{project.id}/search", user), scope: 'notes', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/notes'
@@ -284,7 +284,7 @@ describe API::Search do
wiki = create(:project_wiki, project: project)
create(:wiki_page, wiki: wiki, attrs: { title: 'home', content: "Awesome page" })
- get api("/projects/#{project.id}/-/search", user), scope: 'wiki_blobs', search: 'awesome'
+ get api("/projects/#{project.id}/search", user), scope: 'wiki_blobs', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/blobs'
@@ -292,7 +292,7 @@ describe API::Search do
context 'for commits scope' do
before do
- get api("/projects/#{repo_project.id}/-/search", user), scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6'
+ get api("/projects/#{repo_project.id}/search", user), scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/commits_details'
@@ -300,7 +300,7 @@ describe API::Search do
context 'for commits scope with project path as id' do
before do
- get api("/projects/#{CGI.escape(repo_project.full_path)}/-/search", user), scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6'
+ get api("/projects/#{CGI.escape(repo_project.full_path)}/search", user), scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/commits_details'
@@ -308,7 +308,7 @@ describe API::Search do
context 'for blobs scope' do
before do
- get api("/projects/#{repo_project.id}/-/search", user), scope: 'blobs', search: 'monitors'
+ get api("/projects/#{repo_project.id}/search", user), scope: 'blobs', search: 'monitors'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/blobs', size: 2
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 62fdf870090..3943148f0db 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -34,6 +34,12 @@ describe NotificationService, :mailer do
should_not_email_anyone
end
+ it 'emails new mentions despite being unsubscribed' do
+ send_notifications(@unsubscribed_mentioned)
+
+ should_only_email(@unsubscribed_mentioned)
+ end
+
it 'sends the proper notification reason header' do
send_notifications(@u_watcher)
should_only_email(@u_watcher)
@@ -122,7 +128,7 @@ describe NotificationService, :mailer do
let(:project) { create(:project, :private) }
let(:issue) { create(:issue, project: project, assignees: [assignee]) }
let(:mentioned_issue) { create(:issue, assignees: issue.assignees) }
- let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@mention referenced, @outsider also') }
+ let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@mention referenced, @unsubscribed_mentioned and @outsider also') }
before do
build_team(note.project)
@@ -150,7 +156,7 @@ describe NotificationService, :mailer do
add_users_with_subscription(note.project, issue)
reset_delivered_emails!
- expect(SentNotification).to receive(:record).with(issue, any_args).exactly(9).times
+ expect(SentNotification).to receive(:record).with(issue, any_args).exactly(10).times
notification.new_note(note)
@@ -163,6 +169,7 @@ describe NotificationService, :mailer do
should_email(@watcher_and_subscriber)
should_email(@subscribed_participant)
should_email(@u_custom_off)
+ should_email(@unsubscribed_mentioned)
should_not_email(@u_guest_custom)
should_not_email(@u_guest_watcher)
should_not_email(note.author)
@@ -279,6 +286,7 @@ describe NotificationService, :mailer do
before do
build_team(note.project)
note.project.add_master(note.author)
+ add_users_with_subscription(note.project, issue)
reset_delivered_emails!
end
@@ -286,6 +294,9 @@ describe NotificationService, :mailer do
it 'notifies the team members' do
notification.new_note(note)
+ # Make sure @unsubscribed_mentioned is part of the team
+ expect(note.project.team.members).to include(@unsubscribed_mentioned)
+
# Notify all team members
note.project.team.members.each do |member|
# User with disabled notification should not be notified
@@ -486,7 +497,7 @@ describe NotificationService, :mailer do
let(:group) { create(:group) }
let(:project) { create(:project, :public, namespace: group) }
let(:another_project) { create(:project, :public, namespace: group) }
- let(:issue) { create :issue, project: project, assignees: [assignee], description: 'cc @participant' }
+ let(:issue) { create :issue, project: project, assignees: [assignee], description: 'cc @participant @unsubscribed_mentioned' }
before do
build_team(issue.project)
@@ -510,6 +521,7 @@ describe NotificationService, :mailer do
should_email(@u_participant_mentioned)
should_email(@g_global_watcher)
should_email(@g_watcher)
+ should_email(@unsubscribed_mentioned)
should_not_email(@u_mentioned)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -1823,6 +1835,7 @@ describe NotificationService, :mailer do
def add_users_with_subscription(project, issuable)
@subscriber = create :user
@unsubscriber = create :user
+ @unsubscribed_mentioned = create :user, username: 'unsubscribed_mentioned'
@subscribed_participant = create_global_setting_for(create(:user, username: 'subscribed_participant'), :participating)
@watcher_and_subscriber = create_global_setting_for(create(:user), :watch)
@@ -1830,7 +1843,9 @@ describe NotificationService, :mailer do
project.add_master(@subscriber)
project.add_master(@unsubscriber)
project.add_master(@watcher_and_subscriber)
+ project.add_master(@unsubscribed_mentioned)
+ issuable.subscriptions.create(user: @unsubscribed_mentioned, project: project, subscribed: false)
issuable.subscriptions.create(user: @subscriber, project: project, subscribed: true)
issuable.subscriptions.create(user: @subscribed_participant, project: project, subscribed: true)
issuable.subscriptions.create(user: @unsubscriber, project: project, subscribed: false)
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);