Skip to content
Snippets Groups Projects
Commit 08f4ce10 authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent b4cdff15
No related branches found
No related tags found
No related merge requests found
Showing
with 384 additions and 87 deletions
Loading
Loading
@@ -19,6 +19,7 @@ package-and-qa-manual:
except:
refs:
- master
- /^\d+-\d+-auto-deploy-\d+$/
when: manual
needs: ["build-qa-image", "gitlab:assets:compile pull-cache"]
 
Loading
Loading
@@ -29,6 +30,7 @@ package-and-qa:
except:
refs:
- master
- /^\d+-\d+-auto-deploy-\d+$/
needs: ["build-qa-image", "gitlab:assets:compile pull-cache"]
allow_failure: true
 
Loading
Loading
<script>
import RequestWarning from './request_warning.vue';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import Icon from '~/vue_shared/components/icon.vue';
 
export default {
components: {
RequestWarning,
GlModal: DeprecatedModal2,
Icon,
},
Loading
Loading
@@ -39,6 +42,16 @@ export default {
detailsList() {
return this.metricDetails.details;
},
warnings() {
return this.metricDetails.warnings || [];
},
htmlId() {
if (this.currentRequest) {
return `performance-bar-warning-${this.currentRequest.id}-${this.metric}`;
}
return '';
},
},
};
</script>
Loading
Loading
@@ -105,5 +118,6 @@ export default {
<div slot="footer"></div>
</gl-modal>
{{ title }}
<request-warning :html-id="htmlId" :warnings="warnings" />
</div>
</template>
<script>
import { glEmojiTag } from '~/emoji';
 
import detailedMetric from './detailed_metric.vue';
import requestSelector from './request_selector.vue';
import DetailedMetric from './detailed_metric.vue';
import RequestSelector from './request_selector.vue';
import { s__ } from '~/locale';
 
export default {
components: {
detailedMetric,
requestSelector,
DetailedMetric,
RequestSelector,
},
props: {
store: {
Loading
Loading
<script>
import { glEmojiTag } from '~/emoji';
import { n__ } from '~/locale';
import { GlPopover } from '@gitlab/ui';
export default {
components: {
GlPopover,
},
props: {
currentRequest: {
type: Object,
Loading
Loading
@@ -15,6 +22,18 @@ export default {
currentRequestId: this.currentRequest.id,
};
},
computed: {
requestsWithWarnings() {
return this.requests.filter(request => request.hasWarnings);
},
warningMessage() {
return n__(
'%d request with warnings',
'%d requests with warnings',
this.requestsWithWarnings.length,
);
},
},
watch: {
currentRequestId(newRequestId) {
this.$emit('change-current-request', newRequestId);
Loading
Loading
@@ -31,6 +50,7 @@ export default {
 
return truncated;
},
glEmojiTag,
},
};
</script>
Loading
Loading
@@ -44,7 +64,16 @@ export default {
class="qa-performance-bar-request"
>
{{ truncatedUrl(request.url) }}
<span v-if="request.hasWarnings">(!)</span>
</option>
</select>
<span v-if="requestsWithWarnings.length">
<span id="performance-bar-request-selector-warning" v-html="glEmojiTag('warning')"></span>
<gl-popover
target="performance-bar-request-selector-warning"
:content="warningMessage"
triggers="hover focus"
/>
</span>
</div>
</template>
<script>
import { glEmojiTag } from '~/emoji';
import { GlPopover } from '@gitlab/ui';
export default {
components: {
GlPopover,
},
props: {
htmlId: {
type: String,
required: true,
},
warnings: {
type: Array,
required: true,
},
},
computed: {
hasWarnings() {
return this.warnings && this.warnings.length;
},
warningMessage() {
if (!this.hasWarnings) {
return '';
}
return this.warnings.join('\n');
},
},
methods: {
glEmojiTag,
},
};
</script>
<template>
<span v-if="hasWarnings">
<span :id="htmlId" v-html="glEmojiTag('warning')"></span>
<gl-popover :target="htmlId" :content="warningMessage" triggers="hover focus" />
</span>
</template>
Loading
Loading
@@ -6,7 +6,7 @@ export default ({ container }) =>
new Vue({
el: container,
components: {
performanceBarApp: () => import('./components/performance_bar_app.vue'),
PerformanceBarApp: () => import('./components/performance_bar_app.vue'),
},
data() {
const performanceBarData = document.querySelector(this.$options.el).dataset;
Loading
Loading
@@ -41,7 +41,7 @@ export default ({ container }) =>
 
PerformanceBarService.fetchRequestDetails(this.peekUrl, requestId)
.then(res => {
this.store.addRequestDetails(requestId, res.data.data);
this.store.addRequestDetails(requestId, res.data);
})
.catch(() =>
// eslint-disable-next-line no-console
Loading
Loading
Loading
Loading
@@ -3,12 +3,13 @@ export default class PerformanceBarStore {
this.requests = [];
}
 
addRequest(requestId, requestUrl, requestDetails) {
addRequest(requestId, requestUrl) {
if (!this.findRequest(requestId)) {
this.requests.push({
id: requestId,
url: requestUrl,
details: requestDetails,
details: {},
hasWarnings: false,
});
}
 
Loading
Loading
@@ -22,7 +23,8 @@ export default class PerformanceBarStore {
addRequestDetails(requestId, requestDetails) {
const request = this.findRequest(requestId);
 
request.details = requestDetails;
request.details = requestDetails.data;
request.hasWarnings = requestDetails.has_warnings;
 
return request;
}
Loading
Loading
Loading
Loading
@@ -6,6 +6,7 @@ module Clusters
include Gitlab::Kubernetes
include EnumWithNil
include AfterCommitQueue
include ReactiveCaching
 
RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze
 
Loading
Loading
- page_title "System Info"
- page_title _('System Info')
 
.prepend-top-default
.row
.col-sm-4
.card.bg-light.light-well
%h4 CPU
.col-sm
.bg-light.light-well
%h4= _('CPU')
.data
- if @cpus
%h1 #{@cpus.length} cores
%h2= _('%{cores} cores') % { cores: @cpus.length }
- else
= icon('warning', class: 'text-warning')
Unable to collect CPU info
.col-sm-4
.card.bg-light.light-well
%h4 Memory Usage
= _('Unable to collect CPU info')
.bg-light.light-well.prepend-top-default
%h4= _('Memory Usage')
.data
- if @memory
%h1 #{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)}
%h2 #{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)}
- else
= icon('warning', class: 'text-warning')
Unable to collect memory info
.col-sm-4
.card.bg-light.light-well
%h4 Disk Usage
= _('Unable to collect memory info')
.bg-light.light-well.prepend-top-default
%h4= _('Uptime')
.data
- @disks.each do |disk|
%h1 #{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}
%p= disk[:disk_name]
%p= disk[:mount_path]
.col-sm-4
.card.bg-light.light-well
%h4 Uptime
%h2= distance_of_time_in_words_to_now(Rails.application.config.booted_at)
.col-sm
.bg-light.light-well
%h4= _('Disk Usage')
.data
%h1= distance_of_time_in_words_to_now(Rails.application.config.booted_at)
%ul
- @disks.each do |disk|
%li
%h2 #{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}
%p= disk[:disk_name]
%p= disk[:mount_path]
---
title: Improve admin/system_info page ui
merge_request: 17829
author:
type: changed
---
title: Add warnings to performance bar when page shows signs of poor performance
merge_request: 17612
author:
type: changed
doc/administration/monitoring/performance/img/performance_bar_gitaly_threshold.png

63.8 KiB

Loading
Loading
@@ -21,6 +21,24 @@ 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.
 
## Request warnings
For requests exceeding pre-defined limits, a warning icon will be shown
next to the failing metric, along with an explanation. In this example,
the Gitaly call duration exceeded the threshold:
![Gitaly call duration exceeded threshold](img/performance_bar_gitaly_threshold.png)
If any requests on the current page generated warnings, the icon will
appear next to the request selector:
![Request selector showing two requests with warnings](img/performance_bar_request_selector_warning.png)
And requests with warnings are indicated in the request selector with a
`(!)` after their path:
![Request selector showing dropdown](img/performance_bar_request_selector_warning_expanded.png)
## Enable the Performance Bar via the Admin panel
 
GitLab Performance Bar is disabled by default. To enable it for a given group,
Loading
Loading
Loading
Loading
@@ -150,6 +150,11 @@ msgid_plural "%d more comments"
msgstr[0] ""
msgstr[1] ""
 
msgid "%d request with warnings"
msgid_plural "%d requests with warnings"
msgstr[0] ""
msgstr[1] ""
msgid "%d second"
msgid_plural "%d seconds"
msgstr[0] ""
Loading
Loading
@@ -179,6 +184,9 @@ msgstr ""
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr ""
 
msgid "%{cores} cores"
msgstr ""
msgid "%{count} LOC/commit"
msgstr ""
 
Loading
Loading
@@ -2695,6 +2703,9 @@ msgstr ""
msgid "CONTRIBUTING"
msgstr ""
 
msgid "CPU"
msgstr ""
msgid "Callback URL"
msgstr ""
 
Loading
Loading
@@ -5347,6 +5358,9 @@ msgstr ""
msgid "Discussion"
msgstr ""
 
msgid "Disk Usage"
msgstr ""
msgid "Dismiss"
msgstr ""
 
Loading
Loading
@@ -8923,6 +8937,9 @@ msgstr ""
msgid "Kubernetes"
msgstr ""
 
msgid "Kubernetes API returned status code: %{error_code}"
msgstr ""
msgid "Kubernetes Cluster"
msgstr ""
 
Loading
Loading
@@ -9654,6 +9671,9 @@ msgstr ""
msgid "Members with pending access to %{strong_start}%{group_name}%{strong_end}"
msgstr ""
 
msgid "Memory Usage"
msgstr ""
msgid "Merge"
msgstr ""
 
Loading
Loading
@@ -11500,6 +11520,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
 
msgid "Pod not found"
msgstr ""
msgid "Pods in use"
msgstr ""
 
Loading
Loading
@@ -14565,6 +14588,9 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
 
msgid "Something went wrong on our end. %{message}"
msgstr ""
msgid "Something went wrong on our end. Please try again!"
msgstr ""
 
Loading
Loading
@@ -16779,6 +16805,12 @@ msgstr ""
msgid "Unable to build Slack link."
msgstr ""
 
msgid "Unable to collect CPU info"
msgstr ""
msgid "Unable to collect memory info"
msgstr ""
msgid "Unable to connect to Prometheus server"
msgstr ""
 
Loading
Loading
@@ -17055,6 +17087,9 @@ msgstr ""
msgid "Upstream"
msgstr ""
 
msgid "Uptime"
msgstr ""
msgid "Upvotes"
msgstr ""
 
Loading
Loading
@@ -18835,6 +18870,9 @@ msgstr ""
msgid "connecting"
msgstr ""
 
msgid "container_name cannot be larger than %{max_length} chars"
msgstr ""
msgid "could not read private key, is the passphrase correct?"
msgstr ""
 
Loading
Loading
@@ -19405,6 +19443,9 @@ msgstr ""
msgid "pipeline"
msgstr ""
 
msgid "pod_name cannot be larger than %{max_length} chars"
msgstr ""
msgid "point"
msgid_plural "points"
msgstr[0] ""
Loading
Loading
# frozen_string_literal: true
require 'spec_helper'
describe 'Issue page tabs', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, author: user, assignees: [user], project: project) }
describe 'discussions tab counter' do
before do
stub_licensed_features(design_management: true)
stub_feature_flags(design_management_flag: true)
allow(Ability).to receive(:allowed?) { true }
end
subject do
sign_in(user)
visit project_issue_path(project, issue)
wait_for_requests
find('#discussion')
end
context 'new issue' do
it 'displays count of 0' do
is_expected.to have_content('Discussion 0')
end
end
context 'issue with 2 system notes and 1 discussion' do
let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: project, note: "This is good") }
before do
create(:system_note, noteable: issue, project: project, author: user, note: 'description updated')
create(:system_note, noteable: issue, project: project, author: user, note: 'description updated')
end
it 'displays count of 1' do
is_expected.to have_content('Discussion 1')
end
context 'with 1 reply' do
before do
create(:note, noteable: issue, in_reply_to: discussion, discussion_id: discussion.discussion_id, note: 'I also think this is good')
end
it 'displays count of 2' do
is_expected.to have_content('Discussion 2')
end
end
end
end
end
import Vue from 'vue';
import detailedMetric from '~/performance_bar/components/detailed_metric.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import DetailedMetric from '~/performance_bar/components/detailed_metric.vue';
import RequestWarning from '~/performance_bar/components/request_warning.vue';
import { shallowMount } from '@vue/test-utils';
 
describe('detailedMetric', () => {
let vm;
afterEach(() => {
vm.$destroy();
});
const createComponent = props =>
shallowMount(DetailedMetric, {
propsData: {
...props,
},
});
 
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'],
});
const wrapper = createComponent({
currentRequest: {},
metric: 'gitaly',
header: 'Gitaly calls',
details: 'details',
keys: ['feature', 'request'],
});
 
it('does not render the element', () => {
expect(vm.$el.innerHTML).toEqual(undefined);
expect(wrapper.isEmpty()).toBe(true);
});
});
 
Loading
Loading
@@ -31,14 +30,15 @@ describe('detailedMetric', () => {
{ duration: '23', feature: 'rebase_in_progress', request: '', backtrace: ['world', 'hello'] },
];
 
beforeEach(() => {
vm = mountComponent(Vue.extend(detailedMetric), {
describe('with a default metric name', () => {
const wrapper = createComponent({
currentRequest: {
details: {
gitaly: {
duration: '123ms',
calls: '456',
details: requestDetails,
warnings: ['gitaly calls: 456 over 30'],
},
},
},
Loading
Loading
@@ -46,63 +46,65 @@ describe('detailedMetric', () => {
header: 'Gitaly calls',
keys: ['feature', 'request'],
});
});
 
it('diplays details', () => {
expect(vm.$el.innerText.replace(/\s+/g, ' ')).toContain('123ms / 456');
});
it('displays details', () => {
expect(wrapper.text().replace(/\s+/g, ' ')).toContain('123ms / 456');
});
 
it('adds a modal with a table of the details', () => {
vm.$el
.querySelectorAll('.performance-bar-modal td:nth-child(1)')
.forEach((duration, index) => {
expect(duration.innerText).toContain(requestDetails[index].duration);
});
it('adds a modal with a table of the details', () => {
wrapper
.findAll('.performance-bar-modal td:nth-child(1)')
.wrappers.forEach((duration, index) => {
expect(duration.text()).toContain(requestDetails[index].duration);
});
 
vm.$el
.querySelectorAll('.performance-bar-modal td:nth-child(2)')
.forEach((feature, index) => {
expect(feature.innerText).toContain(requestDetails[index].feature);
});
wrapper
.findAll('.performance-bar-modal td:nth-child(2)')
.wrappers.forEach((feature, index) => {
expect(feature.text()).toContain(requestDetails[index].feature);
});
 
vm.$el
.querySelectorAll('.performance-bar-modal td:nth-child(2)')
.forEach((request, index) => {
expect(request.innerText).toContain(requestDetails[index].request);
});
wrapper
.findAll('.performance-bar-modal td:nth-child(2)')
.wrappers.forEach((request, index) => {
expect(request.text()).toContain(requestDetails[index].request);
});
 
expect(vm.$el.querySelector('.text-expander.js-toggle-button')).not.toBeNull();
expect(wrapper.find('.text-expander.js-toggle-button')).not.toBeNull();
 
vm.$el.querySelectorAll('.performance-bar-modal td:nth-child(2)').forEach(request => {
expect(request.innerText).toContain('world');
wrapper.findAll('.performance-bar-modal td:nth-child(2)').wrappers.forEach(request => {
expect(request.text()).toContain('world');
});
});
});
 
it('displays the metric title', () => {
expect(vm.$el.innerText).toContain('gitaly');
it('displays the metric title', () => {
expect(wrapper.text()).toContain('gitaly');
});
it('displays request warnings', () => {
expect(wrapper.find(RequestWarning).exists()).toBe(true);
});
});
 
describe('when using a custom metric title', () => {
beforeEach(() => {
vm = mountComponent(Vue.extend(detailedMetric), {
currentRequest: {
details: {
gitaly: {
duration: '123ms',
calls: '456',
details: requestDetails,
},
const wrapper = createComponent({
currentRequest: {
details: {
gitaly: {
duration: '123ms',
calls: '456',
details: requestDetails,
},
},
metric: 'gitaly',
title: 'custom',
header: 'Gitaly calls',
keys: ['feature', 'request'],
});
},
metric: 'gitaly',
title: 'custom',
header: 'Gitaly calls',
keys: ['feature', 'request'],
});
 
it('displays the custom title', () => {
expect(vm.$el.innerText).toContain('custom');
expect(wrapper.text()).toContain('custom');
});
});
});
Loading
Loading
import Vue from 'vue';
import performanceBarApp from '~/performance_bar/components/performance_bar_app.vue';
import PerformanceBarApp from '~/performance_bar/components/performance_bar_app.vue';
import PerformanceBarStore from '~/performance_bar/stores/performance_bar_store';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { shallowMount } from '@vue/test-utils';
 
describe('performance bar app', () => {
let vm;
beforeEach(() => {
const store = new PerformanceBarStore();
vm = mountComponent(Vue.extend(performanceBarApp), {
const store = new PerformanceBarStore();
const wrapper = shallowMount(PerformanceBarApp, {
propsData: {
store,
env: 'development',
requestId: '123',
peekUrl: '/-/peek/results',
profileUrl: '?lineprofiler=true',
});
});
afterEach(() => {
vm.$destroy();
},
});
 
it('sets the class to match the environment', () => {
expect(vm.$el.getAttribute('class')).toContain('development');
expect(wrapper.element.getAttribute('class')).toContain('development');
});
});
import Vue from 'vue';
import requestSelector from '~/performance_bar/components/request_selector.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import RequestSelector from '~/performance_bar/components/request_selector.vue';
import { shallowMount } from '@vue/test-utils';
 
describe('request selector', () => {
const requests = [
{ id: '123', url: 'https://gitlab.com/' },
{
id: '123',
url: 'https://gitlab.com/',
hasWarnings: false,
},
{
id: '456',
url: 'https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/1',
hasWarnings: false,
},
{
id: '789',
url: 'https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/1.json?serializer=widget',
hasWarnings: false,
},
{
id: 'abc',
url: 'https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/1/discussions.json',
hasWarnings: true,
},
];
 
let vm;
beforeEach(() => {
vm = mountComponent(Vue.extend(requestSelector), {
const wrapper = shallowMount(RequestSelector, {
propsData: {
requests,
currentRequest: requests[1],
});
});
afterEach(() => {
vm.$destroy();
},
});
 
function optionText(requestId) {
return vm.$el.querySelector(`[value='${requestId}']`).innerText.trim();
return wrapper
.find(`[value='${requestId}']`)
.text()
.trim();
}
 
it('displays the last component of the path', () => {
Loading
Loading
@@ -43,4 +50,15 @@ describe('request selector', () => {
it('ignores trailing slashes', () => {
expect(optionText(requests[0].id)).toEqual('gitlab.com');
});
it('has a warning icon if any requests have warnings', () => {
expect(wrapper.find('span > gl-emoji').element.dataset.name).toEqual('warning');
});
it('adds a warning glyph to requests with warnings', () => {
const requestValue = wrapper.find('[value="abc"]').text();
expect(requestValue).toContain('discussions.json');
expect(requestValue).toContain('(!)');
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment