Commit b133cb24 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 0e9eea40
......@@ -23,7 +23,7 @@ module Ci
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
scope :preloaded, -> { preload(:owner, :project) }
scope :preloaded, -> { preload(:owner, project: [:route]) }
accepts_nested_attributes_for :variables, allow_destroy: true
......
......@@ -2,7 +2,7 @@
class PipelineScheduleWorker
include ApplicationWorker
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
include CronjobQueue
feature_category :continuous_integration
worker_resource_boundary :cpu
......@@ -10,7 +10,9 @@ class PipelineScheduleWorker
def perform
Ci::PipelineSchedule.runnable_schedules.preloaded.find_in_batches do |schedules|
schedules.each do |schedule|
Ci::PipelineScheduleService.new(schedule.project, schedule.owner).execute(schedule)
with_context(project: schedule.project, user: schedule.owner) do
Ci::PipelineScheduleService.new(schedule.project, schedule.owner).execute(schedule)
end
end
end
end
......
# frozen_string_literal: true
class AddEsBulkConfig < ActiveRecord::Migration[6.0]
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def change
add_column :application_settings, :elasticsearch_max_bulk_size_mb, :smallint, null: false, default: 10
add_column :application_settings, :elasticsearch_max_bulk_concurrency, :smallint, null: false, default: 10
end
end
......@@ -344,6 +344,8 @@ ActiveRecord::Schema.define(version: 2020_02_11_152410) do
t.boolean "updating_name_disabled_for_users", default: false, null: false
t.integer "instance_administrators_group_id"
t.integer "elasticsearch_indexed_field_length_limit", default: 0, null: false
t.integer "elasticsearch_max_bulk_size_mb", limit: 2, default: 10, null: false
t.integer "elasticsearch_max_bulk_concurrency", limit: 2, default: 10, null: false
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
......
......@@ -151,6 +151,8 @@ The following Elasticsearch settings are available:
| `AWS Access Key` | The AWS access key. |
| `AWS Secret Access Key` | The AWS secret access key. |
| `Maximum field length` | See [the explanation in instance limits.](../administration/instance_limits.md#maximum-field-length). |
| `Maximum bulk request size (MiB)` | Repository indexing uses the Elasticsearch bulk request API. This setting determines the maximum size of an individual bulk request during these operations. |
| `Bulk request concurrency` | Each repository indexing operation may submit bulk requests in parallel. This increases indexing performance, but fills the Elasticsearch bulk requests queue faster. |
### Limiting namespaces and projects
......
......@@ -356,6 +356,44 @@ Note the following properties:
![anomaly panel type](img/prometheus_dashboard_column_panel_type.png)
##### Stacked column
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/30583) in GitLab 12.8.
To add a stacked column panel type to a dashboard, look at the following sample dashboard file:
```yaml
dashboard: 'Dashboard title'
priority: 1
panel_groups:
- group: 'Group Title'
priority: 5
panels:
- type: 'stacked-column'
title: "Stacked column"
y_label: "y label"
x_label: 'x label'
metrics:
- id: memory_1
query_range: 'memory_query'
label: "memory query 1"
unit: "count"
series_name: 'group 1'
- id: memory_2
query_range: 'memory_query_2'
label: "memory query 2"
unit: "count"
series_name: 'group 2'
```
![stacked column panel type](img/prometheus_dashboard_stacked_column_panel_type_v12_8.png)
| Property | Type | Required | Description |
| ------ | ------ | ------ | ------ |
| `type` | string | yes | Type of panel to be rendered. For stacked column panel types, set to `stacked-column` |
| `query_range` | yes | yes | For stacked column panel types, you must use a [range query](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries) |
##### Single Stat
To add a single stat panel type to a dashboard, look at the following sample dashboard file:
......
......@@ -147,6 +147,22 @@ reduce the number of approvals left for all rules that the approver belongs to.
![Approvals premium merge request widget](img/approvals_premium_mr_widget_v12_7.png)
### Scoped to Protected Branch **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/460) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.8.
Approval rules are often only relevant to specific branches, like `master`.
When configuring [**Default Approval Rules**](#adding--editing-a-default-approval-rule)
these can be scoped to all the protected branches at once by navigating to your project's
**Settings**, expanding **Merge request approvals**, and selecting **Any branch** from
the **Target branch** dropdown.
Alternatively, you can select a very specific protected branch from the **Target branch** dropdown:
![Scoped to Protected Branch](img/scoped_to_protected_branch_v12_8.png)
To enable this configuration, see [Code Owner’s approvals for protected branches](../protected_branches.md#protected-branches-approval-by-code-owners-premium).
## Adding or removing an approval
When an [eligible approver](#eligible-approvers) visits an open merge request,
......
......@@ -2999,6 +2999,9 @@ msgstr ""
msgid "Built-in"
msgstr ""
msgid "Bulk request concurrency"
msgstr ""
msgid "Burndown chart"
msgstr ""
......@@ -11728,9 +11731,15 @@ msgstr ""
msgid "Maximum attachment size (MB)"
msgstr ""
msgid "Maximum bulk request size (MiB)"
msgstr ""
msgid "Maximum capacity"
msgstr ""
msgid "Maximum concurrency of Elasticsearch bulk requests per indexing operation."
msgstr ""
msgid "Maximum delay (Minutes)"
msgstr ""
......@@ -11773,6 +11782,9 @@ msgstr ""
msgid "Maximum size limit for each repository."
msgstr ""
msgid "Maximum size of Elasticsearch bulk indexing requests."
msgstr ""
msgid "Maximum size of individual attachments in comments."
msgstr ""
......@@ -19578,6 +19590,9 @@ msgstr ""
msgid "This namespace has already been taken! Please choose another one."
msgstr ""
msgid "This only applies to repository indexing operations."
msgstr ""
msgid "This option is only available on GitLab.com"
msgstr ""
......
import { mount, shallowMount } from '@vue/test-utils';
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
import Container from '~/environments/components/container.vue';
import EmptyState from '~/environments/components/empty_state.vue';
import EnvironmentsApp from '~/environments/components/environments_app.vue';
import { environment, folder } from './mock_data';
describe('Environment', () => {
let mock;
let wrapper;
const mockData = {
endpoint: 'environments.json',
canCreateEnvironment: true,
canReadEnvironment: true,
newEnvironmentPath: 'environments/new',
helpPagePath: 'help',
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
};
const mockRequest = (response, body) => {
mock.onGet(mockData.endpoint).reply(response, body, {
'X-nExt-pAge': '2',
'x-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '2',
});
};
const createWrapper = (shallow = false) => {
const fn = shallow ? shallowMount : mount;
wrapper = fn(EnvironmentsApp, { propsData: mockData });
return axios.waitForAll();
};
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
mock.restore();
});
describe('successful request', () => {
describe('without environments', () => {
beforeEach(() => {
mockRequest(200, { environments: [] });
return createWrapper(true);
});
it('should render the empty state', () => {
expect(wrapper.find(EmptyState).exists()).toBe(true);
});
describe('when it is possible to enable a review app', () => {
beforeEach(() => {
mockRequest(200, { environments: [], review_app: { can_setup_review_app: true } });
return createWrapper();
});
it('should render the enable review app button', () => {
expect(wrapper.find('.js-enable-review-app-button').text()).toContain(
'Enable review app',
);
});
});
});
describe('with paginated environments', () => {
const environmentList = [environment];
beforeEach(() => {
mockRequest(200, {
environments: environmentList,
stopped_count: 1,
available_count: 0,
});
return createWrapper();
});
it('should render a conatiner table with environments', () => {
const containerTable = wrapper.find(Container);
expect(containerTable.exists()).toBe(true);
expect(containerTable.props('environments').length).toEqual(environmentList.length);
expect(containerTable.find('.environment-name').text()).toEqual(environmentList[0].name);
});
describe('pagination', () => {
it('should render pagination', () => {
expect(wrapper.findAll('.gl-pagination li').length).toEqual(9);
});
it('should make an API request when page is clicked', () => {
jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
wrapper.find('.gl-pagination li:nth-child(3) .page-link').trigger('click');
expect(wrapper.vm.updateContent).toHaveBeenCalledWith({ scope: 'available', page: '2' });
});
it('should make an API request when using tabs', () => {
jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
wrapper.find('.js-environments-tab-stopped').trigger('click');
expect(wrapper.vm.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
});
});
});
});
describe('unsuccessful request', () => {
beforeEach(() => {
mockRequest(500, {});
return createWrapper(true);
});
it('should render empty state', () => {
expect(wrapper.find(EmptyState).exists()).toBe(true);
});
});
describe('expandable folders', () => {
beforeEach(() => {
mockRequest(200, {
environments: [folder],
stopped_count: 1,
available_count: 0,
});
mock.onGet(environment.folder_path).reply(200, { environments: [environment] });
return createWrapper().then(() => {
// open folder
wrapper.find('.folder-name').trigger('click');
return axios.waitForAll();
});
});
it('should open a closed folder', () => {
expect(wrapper.find('.folder-icon.ic-chevron-right').exists()).toBe(false);
});
it('should close an opened folder', () => {
expect(wrapper.find('.folder-icon.ic-chevron-down').exists()).toBe(true);
// close folder
wrapper.find('.folder-name').trigger('click');
wrapper.vm.$nextTick(() => {
expect(wrapper.find('.folder-icon.ic-chevron-down').exists()).toBe(false);
});
});
it('should show children environments', () => {
expect(wrapper.findAll('.js-child-row').length).toEqual(1);
});
it('should show a button to show all environments', () => {
expect(wrapper.find('.text-center > a.btn').text()).toContain('Show all');
});
});
});
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import axios from '~/lib/utils/axios_utils';
import environmentsComponent from '~/environments/components/environments_app.vue';
import { environment, folder } from './mock_data';
describe('Environment', () => {
const mockData = {
endpoint: 'environments.json',
canCreateEnvironment: true,
canReadEnvironment: true,
newEnvironmentPath: 'environments/new',
helpPagePath: 'help',
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
};
let EnvironmentsComponent;
let component;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
EnvironmentsComponent = Vue.extend(environmentsComponent);
});
afterEach(() => {
component.$destroy();
mock.restore();
});
describe('successful request', () => {
describe('without environments', () => {
beforeEach(done => {
mock.onGet(mockData.endpoint).reply(200, { environments: [] });
component = mountComponent(EnvironmentsComponent, mockData);
setTimeout(() => {
done();
}, 0);
});
it('should render the empty state', () => {
expect(component.$el.querySelector('.js-new-environment-button').textContent).toContain(
'New environment',
);
expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain(
"You don't have any environments right now",
);
});
describe('when it is possible to enable a review app', () => {
beforeEach(done => {
mock
.onGet(mockData.endpoint)
.reply(200, { environments: [], review_app: { can_setup_review_app: true } });
component = mountComponent(EnvironmentsComponent, mockData);
setTimeout(() => {
done();
}, 0);
});
it('should render the enable review app button', () => {
expect(component.$el.querySelector('.js-enable-review-app-button').textContent).toContain(
'Enable review app',
);
});
});
});
describe('with paginated environments', () => {
beforeEach(done => {
mock.onGet(mockData.endpoint).reply(
200,
{
environments: [environment],
stopped_count: 1,
available_count: 0,
},
{
'X-nExt-pAge': '2',
'x-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '2',
},
);
component = mountComponent(EnvironmentsComponent, mockData);
setTimeout(() => {
done();
}, 0);
});
it('should render a table with environments', () => {
expect(component.$el.querySelectorAll('table')).not.toBeNull();
expect(component.$el.querySelector('.environment-name').textContent.trim()).toEqual(
environment.name,
);
});
describe('pagination', () => {
it('should render pagination', () => {
expect(component.$el.querySelectorAll('.gl-pagination li').length).toEqual(9);
});
it('should make an API request when page is clicked', done => {
spyOn(component, 'updateContent');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(3) .page-link').click();
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'available', page: '2' });
done();
}, 0);
});
it('should make an API request when using tabs', done => {
setTimeout(() => {
spyOn(component, 'updateContent');
component.$el.querySelector('.js-environments-tab-stopped').click();
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
done();
}, 0);
});
});
});
});
describe('unsuccessfull request', () => {
beforeEach(done => {
mock.onGet(mockData.endpoint).reply(500, {});
component = mountComponent(EnvironmentsComponent, mockData);
setTimeout(() => {
done();
}, 0);
});
it('should render empty state', () => {
expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain(
"You don't have any environments right now",
);
});
});
describe('expandable folders', () => {
beforeEach(() => {
mock.onGet(mockData.endpoint).reply(
200,
{
environments: [folder],
stopped_count: 0,
available_count: 1,
},
{
'X-nExt-pAge': '2',
'x-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '2',
},
);
mock.onGet(environment.folder_path).reply(200, { environments: [environment] });
component = mountComponent(EnvironmentsComponent, mockData);
});
it('should open a closed folder', done => {
setTimeout(() => {
component.$el.querySelector('.folder-name').click();
Vue.nextTick(() => {
expect(component.$el.querySelector('.folder-icon.ic-chevron-right')).toBe(null);
done();
});
}, 0);
});
it('should close an opened folder', done => {
setTimeout(() => {
// open folder
component.$el.querySelector('.folder-name').click();
Vue.nextTick(() => {
// close folder
component.$el.querySelector('.folder-name').click();
Vue.nextTick(() => {
expect(component.$el.querySelector('.folder-icon.ic-chevron-down')).toBe(null);
done();
});
});
}, 0);
});
it('should show children environments and a button to show all environments', done => {
setTimeout(() => {
// open folder
component.$el.querySelector('.folder-name').click();
Vue.nextTick(() => {
// wait for next async request
setTimeout(() => {
expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1);
expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain(
'Show all',
);
done();
});
});
}, 0);
});
});
describe('methods', () => {
beforeEach(() => {
mock.onGet(mockData.endpoint).reply(
200,
{
environments: [],
stopped_count: 0,
available_count: 1,
},
{},
);
component = mountComponent(EnvironmentsComponent, mockData);
spyOn(window.history, 'pushState').and.stub();
});
describe('updateContent', () => {
it('should set given parameters', done => {
component
.updateContent({ scope: 'stopped', page: '3' })
.then(() => {
expect(component.page).toEqual('3');
expect(component.scope).toEqual('stopped');
expect(component.requestData.scope).toEqual('stopped');
expect(component.requestData.page).toEqual('3');
done();
})
.catch(done.fail);
});
});
describe('onChangeTab', () => {
it('should set page to 1', () => {
spyOn(component, 'updateContent');
component.onChangeTab('stopped');
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
});
});
describe('onChangePage', () => {
it('should update page and keep scope', () => {
spyOn(component, 'updateContent');
component.onChangePage(4);
expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' });
});
});
});
});
......@@ -80,9 +80,9 @@ describe Ci::PipelineSchedule do
it 'preloads the associations' do
subject
query = ActiveRecord::QueryRecorder.new { subject.each(&:project) }
query = ActiveRecord::QueryRecorder.new { subject.map(&:project).each(&:route) }
expect(query.count).to eq(2)
expect(query.count).to eq(3)
end
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment