Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-30 00:09:00 +00:00
parent 5d47d3f8ed
commit 42883e12f1
23 changed files with 510 additions and 38 deletions

View File

@ -1 +1 @@
092d4e489d7de1dcc38d57a4c667e85df8b8377f
6aabab39e06f8a4f786a99449a49d5f0cb332310

View File

@ -0,0 +1,115 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import { GlButton, GlDatepicker } from '@gitlab/ui';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
import createFlash from '~/flash';
import { __ } from '~/locale';
export default {
components: {
BoardEditableItem,
GlButton,
GlDatepicker,
},
data() {
return {
loading: false,
};
},
computed: {
...mapGetters({ issue: 'getActiveIssue' }),
hasDueDate() {
return this.issue.dueDate != null;
},
parsedDueDate() {
if (!this.hasDueDate) {
return null;
}
return parsePikadayDate(this.issue.dueDate);
},
formattedDueDate() {
if (!this.hasDueDate) {
return '';
}
return dateInWords(this.parsedDueDate, true);
},
projectPath() {
const referencePath = this.issue.referencePath || '';
return referencePath.slice(0, referencePath.indexOf('#'));
},
},
methods: {
...mapActions(['setActiveIssueDueDate']),
async openDatePicker() {
await this.$nextTick();
this.$refs.datePicker.calendar.show();
},
async setDueDate(date) {
this.loading = true;
this.$refs.sidebarItem.collapse();
try {
const dueDate = date ? formatDate(date, 'yyyy-mm-dd') : null;
await this.setActiveIssueDueDate({ dueDate, projectPath: this.projectPath });
} catch (e) {
createFlash({ message: this.$options.i18n.updateDueDateError });
} finally {
this.loading = false;
}
},
},
i18n: {
dueDate: __('Due date'),
removeDueDate: __('remove due date'),
updateDueDateError: __('An error occurred when updating the issue due date'),
},
};
</script>
<template>
<board-editable-item
ref="sidebarItem"
class="board-sidebar-due-date"
:title="$options.i18n.dueDate"
:loading="loading"
@open="openDatePicker"
>
<template v-if="hasDueDate" #collapsed>
<div class="gl-display-flex gl-align-items-center">
<strong class="gl-text-gray-900">{{ formattedDueDate }}</strong>
<span class="gl-mx-2">-</span>
<gl-button
variant="link"
class="gl-text-gray-400!"
data-testid="reset-button"
:disabled="loading"
@click="setDueDate(null)"
>
{{ $options.i18n.removeDueDate }}
</gl-button>
</div>
</template>
<template>
<gl-datepicker
ref="datePicker"
:value="parsedDueDate"
show-clear-button
@input="setDueDate"
@clear="setDueDate(null)"
/>
</template>
</board-editable-item>
</template>
<style>
/*
* This can be removed after closing:
* https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1048
*/
.board-sidebar-due-date .gl-datepicker,
.board-sidebar-due-date .gl-datepicker-input {
width: 100%;
}
</style>

View File

@ -0,0 +1,8 @@
mutation issueSetDueDate($input: UpdateIssueInput!) {
updateIssue(input: $input) {
issue {
dueDate
}
errors
}
}

View File

@ -19,6 +19,7 @@ import createBoardListMutation from '../queries/board_list_create.mutation.graph
import updateBoardListMutation from '../queries/board_list_update.mutation.graphql';
import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql';
import issueSetLabels from '../queries/issue_set_labels.mutation.graphql';
import issueSetDueDate from '../queries/issue_set_due_date.mutation.graphql';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
@ -327,6 +328,30 @@ export default {
});
},
setActiveIssueDueDate: async ({ commit, getters }, input) => {
const activeIssue = getters.getActiveIssue;
const { data } = await gqlClient.mutate({
mutation: issueSetDueDate,
variables: {
input: {
iid: String(activeIssue.iid),
projectPath: input.projectPath,
dueDate: input.dueDate,
},
},
});
if (data.updateIssue?.errors?.length > 0) {
throw new Error(data.updateIssue.errors);
}
commit(types.UPDATE_ISSUE_BY_ID, {
issueId: activeIssue.id,
prop: 'dueDate',
value: data.updateIssue.issue.dueDate,
});
},
fetchBacklog: () => {
notImplemented();
},

View File

@ -21,6 +21,9 @@
= f.check_box :send_user_confirmation_email, class: 'form-check-input'
= f.label :send_user_confirmation_email, class: 'form-check-label' do
Send confirmation email on sign-up
= render_if_exists 'admin/application_settings/new_user_signups_cap', form: f
.form-group
= f.label :minimum_password_length, _('Minimum password length (number of characters)'), class: 'label-bold'
= f.number_field :minimum_password_length, class: 'form-control', rows: 4, min: ApplicationSetting::DEFAULT_MINIMUM_PASSWORD_LENGTH, max: Devise.password_length.max

View File

@ -43,6 +43,10 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Configure paths to be protected by Rack Attack.')
.help-block
= _('These paths are protected for POST requests.')
= link_to _('More information'), help_page_path('security/rack_attack', anchor: 'protected-paths-throttle'), target: '_blank'
.settings-content
= render 'protected_paths'

View File

@ -0,0 +1,5 @@
---
title: Add support for .md.erb files in Static Site Editor
merge_request: 42353
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add migration to add a new configuration option for setting the new user signups count
merge_request: 45643
author:
type: other

View File

@ -1,7 +1,7 @@
---
name: resource_access_token
name: resource_access_token_feature
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29622
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/235765
group: group::access
type: licensed
type: development
default_enabled: true

View File

@ -1,7 +0,0 @@
---
name: sse_erb_support
introduced_by_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/235460
group: group::static site editor
type: development
default_enabled: false

View File

@ -14,7 +14,6 @@ if Gitlab.ee? && Gitlab.dev_or_test_env?
# being unique to licensed names. These feature flags should be reworked to
# be "development" with explicit check
IGNORED_FEATURE_FLAGS = %i[
resource_access_token
ci_secrets_management
feature_flags_related_issues
group_coverage_reports

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddNewUserSignupsCapToApplicationSettings < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column :application_settings, :new_user_signups_cap, :integer
end
end

View File

@ -0,0 +1 @@
fe57e8e74ebbe0e9567c1487e6e4f8b499afa6404c73424157c43ae79c005f08

View File

@ -9305,6 +9305,7 @@ CREATE TABLE application_settings (
secret_detection_token_revocation_url text,
encrypted_secret_detection_token_revocation_token text,
encrypted_secret_detection_token_revocation_token_iv text,
new_user_signups_cap integer,
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
CONSTRAINT check_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)),
CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)),

View File

@ -347,6 +347,7 @@ proxied
proxies
proxyable
proxying
pseudocode
pseudonymized
pseudonymizer
Puma

View File

@ -112,7 +112,8 @@ you list:
## Queue selector (experimental)
> [Introduced](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/45) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.8.
> - [Introduced](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/45) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.8.
> - [Sidekiq cluster including queue selector moved](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/181) to GitLab [Core](https://about.gitlab.com/pricing/#self-managed) in GitLab 12.10.
CAUTION: **Caution:**
As this is marked as **experimental**, it is subject to change at any
@ -130,6 +131,9 @@ in a more general way using the following components:
- Attributes that can be selected.
- Operators used to construct a query.
When `experimental_queue_selector` is set, all `queue_groups` must be in
the queue selector syntax.
### Available attributes
- [Introduced](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/261) in GitLab 13.1, `tags`.

View File

@ -59,13 +59,13 @@ Some of the benefits and tradeoffs of keyset pagination are
- Performance is much better.
- Data stability is greater since you're not going to miss records due to
- More data stability for end-users since records are not missing from lists due to
deletions or insertions.
- It's the best way to do infinite scrolling.
- It's more difficult to program and maintain. Easy for `updated_at` and
`sort_order`, complicated (or impossible) for complex sorting scenarios.
`sort_order`, complicated (or impossible) for [complex sorting scenarios](#limitations-of-query-complexity).
## Implementation
@ -80,7 +80,124 @@ However, there are some cases where we have to use the offset
pagination connection, `OffsetActiveRecordRelationConnection`, such as when
sorting by label priority in issues, due to the complexity of the sort.
<!-- ### Keyset pagination -->
### Keyset pagination
The keyset pagination implementation is a subclass of `GraphQL::Pagination::ActiveRecordRelationConnection`,
which is a part of the `graphql` gem. This is installed as the default for all `ActiveRecord::Relation`.
However, instead of using a cursor based on an offset (which is the default), GitLab uses a more specialized cursor.
The cursor is created by encoding a JSON object which contains the relevant ordering fields. For example:
```ruby
ordering = {"id"=>"72410125", "created_at"=>"2020-10-08 18:05:21.953398000 UTC"}
json = ordering.to_json
cursor = Base64Bp.urlsafe_encode64(json, padding: false)
"eyJpZCI6IjcyNDEwMTI1IiwiY3JlYXRlZF9hdCI6IjIwMjAtMTAtMDggMTg6MDU6MjEuOTUzMzk4MDAwIFVUQyJ9"
json = Base64Bp.urlsafe_decode64(cursor)
Gitlab::Json.parse(json)
{"id"=>"72410125", "created_at"=>"2020-10-08 18:05:21.953398000 UTC"}
```
The benefits of storing the order attribute values in the cursor:
- If only the ID of the object were stored, the object and its attributes could be queried.
That would require an additional query, and if the object is no longer there, then the needed
attributes are not available.
- If an attribute is `NULL`, then one SQL query can be used. If it's not `NULL`, then a
different SQL query can be used.
Based on whether the main attribute field being sorted on is `NULL` in the cursor, the proper query
condition is built. The last ordering field is considered to be unique (a primary key), meaning the
column never contains `NULL` values.
#### Limitations of query complexity
We only support two ordering fields, and one of those fields needs to be the primary key.
Here are two examples of pseudocode for the query:
- **Two-condition query.** `X` represents the values from the cursor. `C` represents
the columns in the database, sorted in ascending order, using an `:after` cursor, and with `NULL`
values sorted last.
```plaintext
X1 IS NOT NULL
AND
(C1 > X1)
OR
(C1 IS NULL)
OR
(C1 = X1
AND
C2 > X2)
X1 IS NULL
AND
(C1 IS NULL
AND
C2 > X2)
```
Below is an example based on the relation `Issue.order(relative_position: :asc).order(id: :asc)`
with an after cursor of `relative_position: 1500, id: 500`:
```plaintext
when cursor[relative_position] is not NULL
("issues"."relative_position" > 1500)
OR (
"issues"."relative_position" = 1500
AND
"issues"."id" > 500
)
OR ("issues"."relative_position" IS NULL)
when cursor[relative_position] is NULL
"issues"."relative_position" IS NULL
AND
"issues"."id" > 500
```
- **Three-condition query.** The example below is not complete, but shows the
complexity of adding one more condition. `X` represents the values from the cursor. `C` represents
the columns in the database, sorted in ascending order, using an `:after` cursor, and with `NULL`
values sorted last.
```plaintext
X1 IS NOT NULL
AND
(C1 > X1)
OR
(C1 IS NULL)
OR
(C1 = X1 AND C2 > X2)
OR
(C1 = X1
AND
X2 IS NOT NULL
AND
((C2 > X2)
OR
(C2 IS NULL)
OR
(C2 = X2 AND C3 > X3)
OR
X2 IS NULL.....
```
By using
[`Gitlab::Graphql::Pagination::Keyset::QueryBuilder`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/graphql/pagination/keyset/query_builder.rb),
we're able to build the necessary SQL conditions and apply them to the Active Record relation.
Complex queries can be difficult or impossible to use. For example,
in [`issuable.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/concerns/issuable.rb),
the `order_due_date_and_labels_priority` method creates a very complex query.
These types of queries are not supported. In these instances, you can use offset pagination.
<!-- ### Offset pagination -->

View File

@ -34,7 +34,7 @@ module Gitlab
delegate :project, to: :repository
def supported_extensions
%w[.md].freeze
%w[.md .md.erb].freeze
end
def commit_id
@ -50,8 +50,6 @@ module Gitlab
end
def extension_supported?
return true if path.end_with?('.md.erb') && Feature.enabled?(:sse_erb_support, project)
supported_extensions.any? { |ext| path.end_with?(ext) }
end

View File

@ -1914,6 +1914,9 @@ msgstr ""
msgid "AdminArea|New user"
msgstr ""
msgid "AdminArea|Once the instance reaches the user cap, any user who is added or requests access will have to be approved by an admin. Leave the field empty for unlimited."
msgstr ""
msgid "AdminArea|Owner"
msgstr ""
@ -1938,6 +1941,9 @@ msgstr ""
msgid "AdminArea|Total users"
msgstr ""
msgid "AdminArea|User cap"
msgstr ""
msgid "AdminArea|Users statistics"
msgstr ""
@ -2867,6 +2873,9 @@ msgstr ""
msgid "An error occurred when toggling the notification subscription"
msgstr ""
msgid "An error occurred when updating the issue due date"
msgstr ""
msgid "An error occurred when updating the issue weight"
msgstr ""
@ -26978,6 +26987,9 @@ msgstr ""
msgid "These existing issues have a similar title. It might be better to comment there instead of creating another similar issue."
msgstr ""
msgid "These paths are protected for POST requests."
msgstr ""
msgid "These variables are configured in the parent group settings, and will be active in the current project in addition to the project variables."
msgstr ""

View File

@ -52,10 +52,10 @@ gitlab:
resources:
requests:
cpu: 855m
memory: 1071M
memory: 1285M
limits:
cpu: 1282m
memory: 1606M
memory: 1927M
hpa:
targetAverageValue: 650m
task-runner:

View File

@ -0,0 +1,137 @@
import { shallowMount } from '@vue/test-utils';
import { GlDatepicker } from '@gitlab/ui';
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import { createStore } from '~/boards/stores';
import createFlash from '~/flash';
const TEST_DUE_DATE = '2020-02-20';
const TEST_FORMATTED_DUE_DATE = 'Feb 20, 2020';
const TEST_PARSED_DATE = new Date(2020, 1, 20);
const TEST_ISSUE = { id: 'gid://gitlab/Issue/1', iid: 9, dueDate: null, referencePath: 'h/b#2' };
jest.mock('~/flash');
describe('~/boards/components/sidebar/board_sidebar_due_date.vue', () => {
let wrapper;
let store;
afterEach(() => {
wrapper.destroy();
store = null;
wrapper = null;
});
const createWrapper = ({ dueDate = null } = {}) => {
store = createStore();
store.state.issues = { [TEST_ISSUE.id]: { ...TEST_ISSUE, dueDate } };
store.state.activeId = TEST_ISSUE.id;
wrapper = shallowMount(BoardSidebarDueDate, {
store,
provide: {
canUpdate: true,
},
stubs: {
'board-editable-item': BoardEditableItem,
},
});
};
const findDatePicker = () => wrapper.find(GlDatepicker);
const findResetButton = () => wrapper.find('[data-testid="reset-button"]');
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
it('renders "None" when no due date is set', () => {
createWrapper();
expect(findCollapsed().text()).toBe('None');
expect(findResetButton().exists()).toBe(false);
});
it('renders formatted due date with reset button when set', () => {
createWrapper({ dueDate: TEST_DUE_DATE });
expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE);
expect(findResetButton().exists()).toBe(true);
});
describe('when due date is submitted', () => {
beforeEach(async () => {
createWrapper();
jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
store.state.issues[TEST_ISSUE.id].dueDate = TEST_DUE_DATE;
});
findDatePicker().vm.$emit('input', TEST_PARSED_DATE);
await wrapper.vm.$nextTick();
});
it('collapses sidebar and renders formatted due date with reset button', () => {
expect(findCollapsed().isVisible()).toBe(true);
expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE);
expect(findResetButton().exists()).toBe(true);
});
it('commits change to the server', () => {
expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalledWith({
dueDate: TEST_DUE_DATE,
projectPath: 'h/b',
});
});
});
describe('when due date is cleared', () => {
beforeEach(async () => {
createWrapper();
jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
store.state.issues[TEST_ISSUE.id].dueDate = null;
});
findDatePicker().vm.$emit('clear');
await wrapper.vm.$nextTick();
});
it('collapses sidebar and renders "None"', () => {
expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalled();
expect(findCollapsed().isVisible()).toBe(true);
expect(findCollapsed().text()).toBe('None');
});
});
describe('when due date is resetted', () => {
beforeEach(async () => {
createWrapper({ dueDate: TEST_DUE_DATE });
jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
store.state.issues[TEST_ISSUE.id].dueDate = null;
});
findResetButton().vm.$emit('click');
await wrapper.vm.$nextTick();
});
it('collapses sidebar and renders "None"', () => {
expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalled();
expect(findCollapsed().isVisible()).toBe(true);
expect(findCollapsed().text()).toBe('None');
});
});
describe('when the mutation fails', () => {
beforeEach(async () => {
createWrapper({ dueDate: TEST_DUE_DATE });
jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
throw new Error(['failed mutation']);
});
findDatePicker().vm.$emit('input', 'Invalid date');
await wrapper.vm.$nextTick();
});
it('collapses sidebar and renders former issue due date', () => {
expect(findCollapsed().isVisible()).toBe(true);
expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE);
expect(createFlash).toHaveBeenCalled();
});
});
});

View File

@ -640,6 +640,57 @@ describe('setActiveIssueLabels', () => {
});
});
describe('setActiveIssueDueDate', () => {
const state = { issues: { [mockIssue.id]: mockIssue } };
const getters = { getActiveIssue: mockIssue };
const testDueDate = '2020-02-20';
const input = {
dueDate: testDueDate,
projectPath: 'h/b',
};
it('should commit due date after setting the issue', done => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
updateIssue: {
issue: {
dueDate: testDueDate,
},
errors: [],
},
},
});
const payload = {
issueId: getters.getActiveIssue.id,
prop: 'dueDate',
value: testDueDate,
};
testAction(
actions.setActiveIssueDueDate,
input,
{ ...state, ...getters },
[
{
type: types.UPDATE_ISSUE_BY_ID,
payload,
},
],
[],
done,
);
});
it('throws error if fails', async () => {
jest
.spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } });
await expect(actions.setActiveIssueDueDate({ getters }, input)).rejects.toThrow(Error);
});
});
describe('fetchBacklog', () => {
expectNotImplemented(actions.fetchBacklog);
});

View File

@ -58,25 +58,9 @@ RSpec.describe Gitlab::StaticSiteEditor::Config::GeneratedConfig do
)
end
context 'when feature flag is enabled' do
let(:path) { 'FEATURE_ON.md.erb' }
let(:path) { 'README.md.erb' }
before do
stub_feature_flags(sse_erb_support: project)
end
it { is_expected.to include(is_supported_content: true) }
end
context 'when feature flag is disabled' do
let(:path) { 'FEATURE_OFF.md.erb' }
before do
stub_feature_flags(sse_erb_support: false)
end
it { is_expected.to include(is_supported_content: false) }
end
it { is_expected.to include(is_supported_content: true) }
end
context 'when file path is nested' do