Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot
2020-10-22 09:08:26 +00:00
parent f0fa9f7acf
commit 9dab4d7b64
22 changed files with 172 additions and 181 deletions

View File

@ -1 +1 @@
dac25a9c19af0a168a7927784295dd12fb5c8075
19109e7090eb6dab2b8c0c168bbef76ceca79a31

View File

@ -1 +1 @@
8.51.0
8.52.0

View File

@ -307,9 +307,6 @@ gem 'rack-attack', '~> 6.3.0'
# Sentry integration
gem 'sentry-raven', '~> 3.0'
# PostgreSQL query parsing
gem 'pg_query', '~> 1.2'
gem 'premailer-rails', '~> 1.10.3'
# LabKit: Tracing and Correlation

View File

@ -828,7 +828,6 @@ GEM
peek (1.1.0)
railties (>= 4.0.0)
pg (1.2.3)
pg_query (1.2.0)
png_quantizator (0.2.1)
po_to_json (1.0.1)
json (>= 1.6.0)
@ -1425,7 +1424,6 @@ DEPENDENCIES
parallel (~> 1.19)
peek (~> 1.1)
pg (~> 1.1)
pg_query (~> 1.2)
png_quantizator (~> 0.2.1)
premailer-rails (~> 1.10.3)
prometheus-client-mmap (~> 0.12.0)

View File

@ -21,6 +21,7 @@ import {
updateImageDiffNoteOptimisticResponse,
toDiffNoteGid,
extractDesignNoteId,
getPageLayoutElement,
} from '../../utils/design_management_utils';
import {
updateStoreAfterAddImageDiffNote,
@ -38,7 +39,7 @@ import {
} from '../../utils/error_messages';
import { trackDesignDetailView } from '../../utils/tracking';
import { DESIGNS_ROUTE_NAME } from '../../router/constants';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
import { ACTIVE_DISCUSSION_SOURCE_TYPES, DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../../constants';
const DEFAULT_SCALE = 1;
@ -300,6 +301,22 @@ export default {
this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded;
},
},
beforeRouteEnter(to, from, next) {
const pageEl = getPageLayoutElement();
if (pageEl) {
pageEl.classList.add(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
}
next();
},
beforeRouteLeave(to, from, next) {
const pageEl = getPageLayoutElement();
if (pageEl) {
pageEl.classList.remove(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
}
next();
},
createImageDiffNoteMutation,
DESIGNS_ROUTE_NAME,
};

View File

@ -1,9 +1,6 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import routes from './routes';
import { DESIGN_ROUTE_NAME } from './constants';
import { getPageLayoutElement } from '~/design_management/utils/design_management_utils';
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../constants';
Vue.use(VueRouter);
@ -13,20 +10,6 @@ export default function createRouter(base) {
mode: 'history',
routes,
});
const pageEl = getPageLayoutElement();
router.beforeEach(({ name }, _, next) => {
// apply a fullscreen layout style in Design View (a.k.a design detail)
if (pageEl) {
if (name === DESIGN_ROUTE_NAME) {
pageEl.classList.add(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
} else {
pageEl.classList.remove(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
}
}
next();
});
return router;
}

View File

@ -13,7 +13,6 @@ import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
import { validateParams } from '../../utils';
import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../../constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
@ -23,7 +22,7 @@ export default {
PipelinesFilteredSearch,
GlIcon,
},
mixins: [pipelinesMixin, CIPaginationMixin, glFeatureFlagsMixin()],
mixins: [pipelinesMixin, CIPaginationMixin],
props: {
store: {
type: Object,
@ -209,9 +208,6 @@ export default {
},
];
},
canFilterPipelines() {
return this.glFeatures.filterPipelinesSearch;
},
validatedParams() {
return validateParams(this.params);
},
@ -306,7 +302,6 @@ export default {
</div>
<pipelines-filtered-search
v-if="canFilterPipelines"
:project-id="projectId"
:params="validatedParams"
@filterPipelines="filterPipelines"

View File

@ -1,8 +1,7 @@
<script>
import $ from 'jquery';
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import Clipboard from 'clipboard';
import { __ } from '~/locale';
import { uniqueId } from 'lodash';
export default {
components: {
@ -17,6 +16,11 @@ export default {
required: false,
default: '',
},
id: {
type: String,
required: false,
default: () => uniqueId('modal-copy-button-'),
},
container: {
type: String,
required: false,
@ -52,7 +56,6 @@ export default {
default: null,
},
},
copySuccessText: __('Copied'),
computed: {
modalDomId() {
return this.modalId ? `#${this.modalId}` : '';
@ -68,11 +71,11 @@ export default {
});
this.clipboard
.on('success', e => {
this.updateTooltip(e.trigger);
this.$root.$emit('bv::hide::tooltip', this.id);
this.$emit('success', e);
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
$(e.trigger).blur();
e.trigger.blur();
})
.on('error', e => this.$emit('error', e));
});
@ -82,29 +85,11 @@ export default {
this.clipboard.destroy();
}
},
methods: {
updateTooltip(target) {
const $target = $(target);
const originalTitle = $target.data('originalTitle');
if ($target.tooltip) {
/**
* The original tooltip will continue staying there unless we remove it by hand.
* $target.tooltip('hide') isn't working.
*/
$('.tooltip').remove();
$target.attr('title', this.$options.copySuccessText);
$target.tooltip('_fixTitle');
$target.tooltip('show');
$target.attr('title', originalTitle);
$target.tooltip('_fixTitle');
}
},
},
};
</script>
<template>
<gl-button
:id="id"
v-gl-tooltip="{ placement: tooltipPlacement, container: tooltipContainer }"
:class="cssClasses"
:data-clipboard-target="target"

View File

@ -12,7 +12,6 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action do
push_frontend_feature_flag(:filter_pipelines_search, project, default_enabled: true)
push_frontend_feature_flag(:dag_pipeline_tab, project, default_enabled: true)
push_frontend_feature_flag(:pipelines_security_report_summary, project)
push_frontend_feature_flag(:new_pipeline_form, project)

View File

@ -47,6 +47,8 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController
payload.transform_values do |value|
if value.is_a?(String) || value.is_a?(Integer)
value
elsif value.nil?
''
else
value.to_json
end

View File

@ -0,0 +1,5 @@
---
title: Upgrade Workhorse to v8.52.0
merge_request: 45778
author:
type: fixed

View File

@ -1,7 +0,0 @@
---
name: filter_pipelines_search
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: true

View File

@ -339,12 +339,14 @@ Feature.disable(:value_stream_analytics_create_multiple_value_streams)
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21631) in GitLab 12.6.
> - [Chart median line removed](https://gitlab.com/gitlab-org/gitlab/-/issues/235455) in GitLab 13.4.
This chart visually depicts the total number of days it takes for cycles to be completed.
This chart visually depicts the total number of days it takes for cycles to be completed. (Totals are being replaced with averages in [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/262070).)
This chart uses the global page filters for displaying data based on the selected
group, projects, and timeframe. In addition, specific stages can be selected
from within the chart itself.
The chart data is limited to the last 500 items.
### Disabling chart
This chart is enabled by default. If you have a self-managed instance, an

View File

@ -123,7 +123,6 @@ module Gitlab
end
extra = sanitize_request_parameters(extra)
inject_sql_query_into_extra(exception, extra)
if sentry && Raven.configuration.server
Raven.capture_exception(exception, tags: default_tags, extra: extra)
@ -150,12 +149,6 @@ module Gitlab
filter.filter(parameters)
end
def inject_sql_query_into_extra(exception, extra)
return unless exception.is_a?(ActiveRecord::StatementInvalid)
extra[:sql] = PgQuery.normalize(exception.sql.to_s)
end
def sentry_dsn
return unless Rails.env.production? || Rails.env.development?
return unless Gitlab.config.sentry.enabled

View File

@ -8147,6 +8147,9 @@ msgstr ""
msgid "CycleAnalytics|The given date range is larger than 180 days"
msgstr ""
msgid "CycleAnalytics|The total time spent in the selected stage for the items that were completed on each date. Data limited to the last 500 items."
msgstr ""
msgid "CycleAnalytics|Total days to completion"
msgstr ""

View File

@ -105,7 +105,8 @@ RSpec.describe Projects::StaticSiteEditorController do
foo: 'bar'
}
},
a_boolean: true
a_boolean: true,
a_nil: nil
}
end
@ -130,6 +131,10 @@ RSpec.describe Projects::StaticSiteEditorController do
it 'serializes data values which are hashes to JSON' do
expect(assigns_data[:a_hash]).to eq('{"a_deeper_hash":{"foo":"bar"}}')
end
it 'serializes data values which are nil to an empty string' do
expect(assigns_data[:a_nil]).to eq('')
end
end
end
end

View File

@ -24,7 +24,13 @@ import mockAllVersions from '../../mock_data/all_versions';
jest.mock('~/flash');
const focusInput = jest.fn();
const mutate = jest.fn().mockResolvedValue();
const mockPageLayoutElement = {
classList: {
add: jest.fn(),
remove: jest.fn(),
},
};
const DesignReplyForm = {
template: '<div><textarea ref="textarea"></textarea></div>',
methods: {
@ -37,6 +43,32 @@ const mockDesignNoDiscussions = {
nodes: [],
},
};
const newComment = 'new comment';
const annotationCoordinates = {
x: 10,
y: 10,
width: 100,
height: 100,
};
const createDiscussionMutationVariables = {
mutation: createImageDiffNoteMutation,
update: expect.anything(),
variables: {
input: {
body: newComment,
noteableId: design.id,
position: {
headSha: 'headSha',
baseSha: 'baseSha',
startSha: 'startSha',
paths: {
newPath: 'full-design-path',
},
...annotationCoordinates,
},
},
},
};
const localVue = createLocalVue();
localVue.use(VueRouter);
@ -45,35 +77,6 @@ describe('Design management design index page', () => {
let wrapper;
let router;
const newComment = 'new comment';
const annotationCoordinates = {
x: 10,
y: 10,
width: 100,
height: 100,
};
const createDiscussionMutationVariables = {
mutation: createImageDiffNoteMutation,
update: expect.anything(),
variables: {
input: {
body: newComment,
noteableId: design.id,
position: {
headSha: 'headSha',
baseSha: 'baseSha',
startSha: 'startSha',
paths: {
newPath: 'full-design-path',
},
...annotationCoordinates,
},
},
},
};
const mutate = jest.fn().mockResolvedValue();
const findDiscussionForm = () => wrapper.find(DesignReplyForm);
const findSidebar = () => wrapper.find(DesignSidebar);
const findDesignPresentation = () => wrapper.find(DesignPresentation);
@ -122,19 +125,44 @@ describe('Design management design index page', () => {
wrapper.destroy();
});
describe('when navigating', () => {
it('applies fullscreen layout', () => {
const mockEl = {
classList: {
add: jest.fn(),
remove: jest.fn(),
},
};
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockEl);
describe('when navigating to component', () => {
it('applies fullscreen layout class', () => {
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageLayoutElement);
createComponent({ loading: true });
expect(mockEl.classList.add).toHaveBeenCalledTimes(1);
expect(mockEl.classList.add).toHaveBeenCalledWith(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
expect(mockPageLayoutElement.classList.add).toHaveBeenCalledTimes(1);
expect(mockPageLayoutElement.classList.add).toHaveBeenCalledWith(
...DESIGN_DETAIL_LAYOUT_CLASSLIST,
);
});
});
describe('when navigating within the component', () => {
it('`scale` prop of DesignPresentation component is 1', async () => {
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageLayoutElement);
createComponent({ loading: false }, { data: { design, scale: 2 } });
await wrapper.vm.$nextTick();
expect(findDesignPresentation().props('scale')).toBe(2);
DesignIndex.beforeRouteUpdate.call(wrapper.vm, {}, {}, jest.fn());
await wrapper.vm.$nextTick();
expect(findDesignPresentation().props('scale')).toBe(1);
});
});
describe('when navigating away from component', () => {
it('removes fullscreen layout class', async () => {
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageLayoutElement);
createComponent({ loading: true });
wrapper.vm.$options.beforeRouteLeave[0].call(wrapper.vm, {}, {}, jest.fn());
expect(mockPageLayoutElement.classList.remove).toHaveBeenCalledTimes(1);
expect(mockPageLayoutElement.classList.remove).toHaveBeenCalledWith(
...DESIGN_DETAIL_LAYOUT_CLASSLIST,
);
});
});

View File

@ -19,7 +19,6 @@ import {
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createRouter from '~/design_management/router';
import * as utils from '~/design_management/utils/design_management_utils';
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants';
import {
designListQueryResponse,
designUploadMutationCreatedResponse,
@ -682,13 +681,6 @@ describe('Design management index page', () => {
});
describe('when navigating', () => {
it('ensures fullscreen layout is not applied', () => {
createComponent({ loading: true });
expect(mockPageEl.classList.remove).toHaveBeenCalledTimes(1);
expect(mockPageEl.classList.remove).toHaveBeenCalledWith(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
});
it('should trigger a scrollIntoView method if designs route is detected', () => {
router.replace({
path: '/designs',

View File

@ -74,7 +74,6 @@ describe('Pipelines', () => {
const createComponent = (props = defaultProps, methods) => {
wrapper = mount(PipelinesComponent, {
provide: { glFeatures: { filterPipelinesSearch: true } },
propsData: {
store: new Store(),
projectId: '21',

View File

@ -1,9 +1,7 @@
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import modalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import { shallowMount, createWrapper } from '@vue/test-utils';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
describe('modal copy button', () => {
const Component = Vue.extend(modalCopyButton);
let wrapper;
afterEach(() => {
@ -11,16 +9,18 @@ describe('modal copy button', () => {
});
beforeEach(() => {
wrapper = shallowMount(Component, {
wrapper = shallowMount(ModalCopyButton, {
propsData: {
text: 'copy me',
title: 'Copy this value',
id: 'test-id',
},
});
});
describe('clipboard', () => {
it('should fire a `success` event on click', () => {
const root = createWrapper(wrapper.vm.$root);
document.execCommand = jest.fn(() => true);
window.getSelection = jest.fn(() => ({
toString: jest.fn(() => 'test'),
@ -31,6 +31,7 @@ describe('modal copy button', () => {
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted().success).not.toBeEmpty();
expect(document.execCommand).toHaveBeenCalledWith('copy');
expect(root.emitted('bv::hide::tooltip')).toEqual([['test-id']]);
});
});
it("should propagate the clipboard error event if execCommand doesn't work", () => {

View File

@ -134,8 +134,8 @@ RSpec.describe Resolvers::ProjectsResolver do
is_expected.to eq([named_project3, named_project1, named_project2])
end
it 'returns projects not in order of similarity to search if flag is off' do
is_expected.not_to eq([named_project3, named_project1, named_project2])
it 'returns projects in any order if flag is off' do
is_expected.to match_array([named_project3, named_project1, named_project2])
end
end
end

View File

@ -198,39 +198,47 @@ RSpec.describe Gitlab::ErrorTracking do
end
describe '.track_exception' do
let(:extra) { { issue_url: issue_url, some_other_info: 'info' } }
subject(:track_exception) { described_class.track_exception(exception, extra) }
before do
allow(Raven).to receive(:capture_exception).and_call_original
allow(Gitlab::ErrorTracking::Logger).to receive(:error)
end
it 'calls Raven.capture_exception' do
track_exception
expected_extras = {
some_other_info: 'info',
issue_url: issue_url
}
expect(Raven).to have_received(:capture_exception)
.with(exception,
tags: a_hash_including(correlation_id: 'cid'),
extra: a_hash_including(some_other_info: 'info', issue_url: issue_url))
expected_tags = {
correlation_id: 'cid'
}
expect(Raven).to receive(:capture_exception)
.with(exception,
tags: a_hash_including(expected_tags),
extra: a_hash_including(expected_extras))
described_class.track_exception(
exception,
issue_url: issue_url,
some_other_info: 'info'
)
end
it 'calls Gitlab::ErrorTracking::Logger.error with formatted payload' do
track_exception
expect(Gitlab::ErrorTracking::Logger).to receive(:error)
.with(a_hash_including(*expected_payload_includes))
expect(Gitlab::ErrorTracking::Logger).to have_received(:error)
.with(a_hash_including(*expected_payload_includes))
described_class.track_exception(
exception,
issue_url: issue_url,
some_other_info: 'info'
)
end
context 'with filterable parameters' do
let(:extra) { { test: 1, my_token: 'test' } }
it 'filters parameters' do
track_exception
expect(Gitlab::ErrorTracking::Logger).to receive(:error).with(
hash_including({ 'extra.test' => 1, 'extra.my_token' => '[FILTERED]' }))
expect(Gitlab::ErrorTracking::Logger).to have_received(:error)
.with(hash_including({ 'extra.test' => 1, 'extra.my_token' => '[FILTERED]' }))
described_class.track_exception(exception, extra)
end
end
@ -239,58 +247,44 @@ RSpec.describe Gitlab::ErrorTracking do
let(:exception) { double(message: 'bang!', sentry_extra_data: extra_info, backtrace: caller) }
it 'includes the extra data from the exception in the tracking information' do
track_exception
expect(Raven).to receive(:capture_exception)
.with(exception, a_hash_including(extra: a_hash_including(extra_info)))
expect(Raven).to have_received(:capture_exception)
.with(exception, a_hash_including(extra: a_hash_including(extra_info)))
described_class.track_exception(exception)
end
end
context 'the exception implements :sentry_extra_data, which returns nil' do
let(:exception) { double(message: 'bang!', sentry_extra_data: nil, backtrace: caller) }
let(:extra) { { issue_url: issue_url } }
it 'just includes the other extra info' do
track_exception
extra_info = { issue_url: issue_url }
expect(Raven).to receive(:capture_exception)
.with(exception, a_hash_including(extra: a_hash_including(extra_info)))
expect(Raven).to have_received(:capture_exception)
.with(exception, a_hash_including(extra: a_hash_including(extra)))
described_class.track_exception(exception, extra_info)
end
end
context 'with sidekiq args' do
context 'when the args does not have anything sensitive' do
let(:extra) { { sidekiq: { 'class' => 'PostReceive', 'args' => [1, { 'id' => 2, 'name' => 'hello' }, 'some-value', 'another-value'] } } }
it 'ensures extra.sidekiq.args is a string' do
extra = { sidekiq: { 'class' => 'PostReceive', 'args' => [1, { 'id' => 2, 'name' => 'hello' }, 'some-value', 'another-value'] } }
it 'ensures extra.sidekiq.args is a string' do
track_exception
expect(Gitlab::ErrorTracking::Logger).to receive(:error).with(
hash_including({ 'extra.sidekiq' => { 'class' => 'PostReceive', 'args' => ['1', '{"id"=>2, "name"=>"hello"}', 'some-value', 'another-value'] } }))
expect(Gitlab::ErrorTracking::Logger).to have_received(:error).with(
hash_including({ 'extra.sidekiq' => { 'class' => 'PostReceive', 'args' => ['1', '{"id"=>2, "name"=>"hello"}', 'some-value', 'another-value'] } }))
end
described_class.track_exception(exception, extra)
end
context 'when the args has sensitive information' do
let(:extra) { { sidekiq: { 'class' => 'UnknownWorker', 'args' => ['sensitive string', 1, 2] } } }
it 'filters sensitive arguments before sending' do
extra = { sidekiq: { 'class' => 'UnknownWorker', 'args' => ['sensitive string', 1, 2] } }
it 'filters sensitive arguments before sending' do
track_exception
expect(Gitlab::ErrorTracking::Logger).to receive(:error).with(
hash_including('extra.sidekiq' => { 'class' => 'UnknownWorker', 'args' => ['[FILTERED]', '1', '2'] }))
expect(sentry_event.dig('extra', 'sidekiq', 'args')).to eq(['[FILTERED]', 1, 2])
expect(Gitlab::ErrorTracking::Logger).to have_received(:error).with(
hash_including('extra.sidekiq' => { 'class' => 'UnknownWorker', 'args' => ['[FILTERED]', '1', '2'] }))
end
end
end
described_class.track_exception(exception, extra)
context 'when the error is kind of an `ActiveRecord::StatementInvalid`' do
let(:exception) { ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1') }
it 'injects the normalized sql query into extra' do
track_exception
expect(Raven).to have_received(:capture_exception)
.with(exception, a_hash_including(extra: a_hash_including(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1')))
expect(sentry_event.dig('extra', 'sidekiq', 'args')).to eq(['[FILTERED]', 1, 2])
end
end
end