diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-20 18:09:16 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-20 18:09:16 +0300 |
commit | 8738992b79824278b090f08e16945affc923ff6f (patch) | |
tree | 5581bd02d1d860f023f6d4e8fae743ccc9757d43 | |
parent | aa874f42425bf3b8fdb4d86de591a06f719ecb7e (diff) |
Add latest changes from gitlab-org/gitlab@master
34 files changed, 933 insertions, 85 deletions
diff --git a/GITLAB_ELASTICSEARCH_INDEXER_VERSION b/GITLAB_ELASTICSEARCH_INDEXER_VERSION index 24ba9a38de6..834f2629538 100644 --- a/GITLAB_ELASTICSEARCH_INDEXER_VERSION +++ b/GITLAB_ELASTICSEARCH_INDEXER_VERSION @@ -1 +1 @@ -2.7.0 +2.8.0 diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue index d85ba2038a7..722bd20f227 100644 --- a/app/assets/javascripts/boards/components/board_list_header.vue +++ b/app/assets/javascripts/boards/components/board_list_header.vue @@ -190,7 +190,8 @@ export default { :title="chevronTooltip" :icon="chevronIcon" class="board-title-caret no-drag gl-cursor-pointer" - variant="link" + category="tertiary" + size="small" @click="toggleExpanded" /> <!-- The following is only true in EE and if it is a milestone --> diff --git a/app/assets/javascripts/boards/components/board_list_header_new.vue b/app/assets/javascripts/boards/components/board_list_header_new.vue index 99347a4cd4d..447fef4b49c 100644 --- a/app/assets/javascripts/boards/components/board_list_header_new.vue +++ b/app/assets/javascripts/boards/components/board_list_header_new.vue @@ -198,7 +198,8 @@ export default { :title="chevronTooltip" :icon="chevronIcon" class="board-title-caret no-drag gl-cursor-pointer" - variant="link" + category="tertiary" + size="small" @click="toggleExpanded" /> <!-- EE start --> diff --git a/app/assets/javascripts/issue_show/utils/parse_data.js b/app/assets/javascripts/issue_show/utils/parse_data.js index 620974901fb..aacbb6a9c6f 100644 --- a/app/assets/javascripts/issue_show/utils/parse_data.js +++ b/app/assets/javascripts/issue_show/utils/parse_data.js @@ -23,5 +23,3 @@ export const parseIssuableData = () => { return {}; } }; - -export default {}; diff --git a/app/assets/javascripts/jira_connect/components/app.vue b/app/assets/javascripts/jira_connect/components/app.vue index 6d32ba41eae..7b8b46cb048 100644 --- a/app/assets/javascripts/jira_connect/components/app.vue +++ b/app/assets/javascripts/jira_connect/components/app.vue @@ -1,7 +1,3 @@ -<script> -export default {}; -</script> - <template> <div></div> </template> diff --git a/app/assets/javascripts/logs/utils.js b/app/assets/javascripts/logs/utils.js index 8e537a4025f..880f762e225 100644 --- a/app/assets/javascripts/logs/utils.js +++ b/app/assets/javascripts/logs/utils.js @@ -23,5 +23,3 @@ export const getTimeRange = (seconds = 0) => { }; export const formatDate = timestamp => dateFormat(timestamp, dateFormatMask); - -export default {}; diff --git a/app/assets/javascripts/monitoring/stores/variable_mapping.js b/app/assets/javascripts/monitoring/stores/variable_mapping.js index 9245ffdb3b9..4ae5cf04ff9 100644 --- a/app/assets/javascripts/monitoring/stores/variable_mapping.js +++ b/app/assets/javascripts/monitoring/stores/variable_mapping.js @@ -271,5 +271,3 @@ export const optionsFromSeriesData = ({ label, data = [] }) => { return [...optionsSet].map(parseSimpleCustomValues); }; - -export default {}; diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js index 92bbce498d5..a4c5a881fae 100644 --- a/app/assets/javascripts/monitoring/utils.js +++ b/app/assets/javascripts/monitoring/utils.js @@ -404,5 +404,3 @@ export const barChartsDataParser = (data = []) => }), {}, ); - -export default {}; diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js index d94fc626a3f..f34247d4eb0 100644 --- a/app/assets/javascripts/notes/stores/collapse_utils.js +++ b/app/assets/javascripts/notes/stores/collapse_utils.js @@ -70,6 +70,3 @@ export const collapseSystemNotes = notes => { return acc; }, []); }; - -// for babel-rewire -export default {}; diff --git a/app/assets/javascripts/registry/explorer/pages/index.vue b/app/assets/javascripts/registry/explorer/pages/index.vue index 4ac0bca84c1..dca63e1a569 100644 --- a/app/assets/javascripts/registry/explorer/pages/index.vue +++ b/app/assets/javascripts/registry/explorer/pages/index.vue @@ -1,7 +1,3 @@ -<script> -export default {}; -</script> - <template> <div> <router-view ref="router-view" /> diff --git a/app/assets/javascripts/registry/settings/components/expiration_dropdown.vue b/app/assets/javascripts/registry/settings/components/expiration_dropdown.vue new file mode 100644 index 00000000000..d75fb31fd98 --- /dev/null +++ b/app/assets/javascripts/registry/settings/components/expiration_dropdown.vue @@ -0,0 +1,50 @@ +<script> +import { GlFormGroup, GlFormSelect } from '@gitlab/ui'; + +export default { + components: { + GlFormGroup, + GlFormSelect, + }, + props: { + formOptions: { + type: Array, + required: false, + default: () => [], + }, + disabled: { + type: Boolean, + required: false, + default: false, + }, + value: { + type: String, + required: false, + default: '', + }, + name: { + type: String, + required: true, + }, + label: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <gl-form-group :id="`${name}-form-group`" :label-for="name" :label="label"> + <gl-form-select :id="name" :value="value" :disabled="disabled" @input="$emit('input', $event)"> + <option + v-for="option in formOptions" + :key="option.key" + :value="option.key" + data-testid="option" + > + {{ option.label }} + </option> + </gl-form-select> + </gl-form-group> +</template> diff --git a/app/assets/javascripts/registry/settings/components/expiration_run_text.vue b/app/assets/javascripts/registry/settings/components/expiration_run_text.vue new file mode 100644 index 00000000000..186ad2f34b9 --- /dev/null +++ b/app/assets/javascripts/registry/settings/components/expiration_run_text.vue @@ -0,0 +1,31 @@ +<script> +import { GlFormGroup, GlFormInput } from '@gitlab/ui'; +import { NEXT_CLEANUP_LABEL, NOT_SCHEDULED_POLICY_TEXT } from '~/registry/settings/constants'; + +export default { + components: { + GlFormGroup, + GlFormInput, + }, + props: { + value: { + type: String, + required: false, + default: NOT_SCHEDULED_POLICY_TEXT, + }, + }, + i18n: { + NEXT_CLEANUP_LABEL, + }, +}; +</script> + +<template> + <gl-form-group + id="expiration-policy-info-text-group" + :label="$options.i18n.NEXT_CLEANUP_LABEL" + label-for="expiration-policy-info-text" + > + <gl-form-input id="expiration-policy-info-text" class="gl-pl-0!" plaintext :value="value" /> + </gl-form-group> +</template> diff --git a/app/assets/javascripts/registry/settings/components/expiration_textarea.vue b/app/assets/javascripts/registry/settings/components/expiration_textarea.vue new file mode 100644 index 00000000000..1e1194ebb5c --- /dev/null +++ b/app/assets/javascripts/registry/settings/components/expiration_textarea.vue @@ -0,0 +1,109 @@ +<script> +import { GlFormGroup, GlFormTextarea, GlSprintf, GlLink } from '@gitlab/ui'; +import { NAME_REGEX_LENGTH, TEXT_AREA_INVALID_FEEDBACK } from '../constants'; + +export default { + components: { + GlFormGroup, + GlFormTextarea, + GlSprintf, + GlLink, + }, + inject: ['tagsRegexHelpPagePath'], + props: { + error: { + type: String, + required: false, + default: '', + }, + disabled: { + type: Boolean, + required: false, + default: false, + }, + value: { + type: String, + required: false, + default: '', + }, + name: { + type: String, + required: true, + }, + label: { + type: String, + required: true, + }, + placeholder: { + type: String, + required: true, + }, + description: { + type: String, + required: true, + }, + }, + computed: { + textAreaLengthErrorMessage() { + return this.isInputValid(this.value) ? '' : TEXT_AREA_INVALID_FEEDBACK; + }, + textAreaValidation() { + const nameRegexErrors = this.error || this.textAreaLengthErrorMessage; + return { + state: nameRegexErrors === null ? null : !nameRegexErrors, + message: nameRegexErrors, + }; + }, + internalValue: { + get() { + return this.value; + }, + set(value) { + this.$emit('input', value); + this.$emit('validation', this.isInputValid(value)); + }, + }, + }, + methods: { + isInputValid(value) { + return !value || value.length <= NAME_REGEX_LENGTH; + }, + }, +}; +</script> + +<template> + <gl-form-group + :id="`${name}-form-group`" + :label-for="name" + :state="textAreaValidation.state" + :invalid-feedback="textAreaValidation.message" + > + <template #label> + <span data-testid="label"> + <gl-sprintf :message="label"> + <template #italic="{content}"> + <i>{{ content }}</i> + </template> + </gl-sprintf> + </span> + </template> + <gl-form-textarea + :id="name" + v-model="internalValue" + :placeholder="placeholder" + :state="textAreaValidation.state" + :disabled="disabled" + trim + /> + <template #description> + <span data-testid="description" class="gl-text-gray-400"> + <gl-sprintf :message="description"> + <template #link="{content}"> + <gl-link :href="tagsRegexHelpPagePath" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </span> + </template> + </gl-form-group> +</template> diff --git a/app/assets/javascripts/registry/settings/components/expiration_toggle.vue b/app/assets/javascripts/registry/settings/components/expiration_toggle.vue new file mode 100644 index 00000000000..9dabe8ac51a --- /dev/null +++ b/app/assets/javascripts/registry/settings/components/expiration_toggle.vue @@ -0,0 +1,55 @@ +<script> +import { GlFormGroup, GlToggle, GlSprintf } from '@gitlab/ui'; +import { ENABLED_TEXT, DISABLED_TEXT, ENABLE_TOGGLE_DESCRIPTION } from '../constants'; + +export default { + components: { + GlFormGroup, + GlToggle, + GlSprintf, + }, + props: { + disabled: { + type: Boolean, + required: false, + default: false, + }, + value: { + type: Boolean, + required: false, + default: false, + }, + }, + i18n: { + ENABLE_TOGGLE_DESCRIPTION, + }, + computed: { + enabled: { + get() { + return this.value; + }, + set(value) { + this.$emit('input', value); + }, + }, + toggleStatusText() { + return this.enabled ? ENABLED_TEXT : DISABLED_TEXT; + }, + }, +}; +</script> + +<template> + <gl-form-group id="expiration-policy-toggle-group" label-for="expiration-policy-toggle"> + <div class="gl-display-flex"> + <gl-toggle id="expiration-policy-toggle" v-model="enabled" :disabled="disabled" /> + <span class="gl-ml-5 gl-line-height-24" data-testid="description"> + <gl-sprintf :message="$options.i18n.ENABLE_TOGGLE_DESCRIPTION"> + <template #toggleStatus> + <strong>{{ toggleStatusText }}</strong> + </template> + </gl-sprintf> + </span> + </div> + </gl-form-group> +</template> diff --git a/app/assets/javascripts/registry/settings/constants.js b/app/assets/javascripts/registry/settings/constants.js index e790658f491..bc3ec3104ad 100644 --- a/app/assets/javascripts/registry/settings/constants.js +++ b/app/assets/javascripts/registry/settings/constants.js @@ -1,6 +1,6 @@ import { s__, __ } from '~/locale'; -export const SET_CLEANUP_POLICY_BUTTON = s__('ContainerRegistry|Set cleanup policy'); +export const SET_CLEANUP_POLICY_BUTTON = __('Save'); export const CLEANUP_POLICY_CARD_HEADER = s__('ContainerRegistry|Tag expiration policy'); export const UNAVAILABLE_FEATURE_TITLE = s__( `ContainerRegistry|Cleanup policy for tags is disabled`, @@ -12,3 +12,46 @@ export const UNAVAILABLE_USER_FEATURE_TEXT = __(`Please contact your administrat export const UNAVAILABLE_ADMIN_FEATURE_TEXT = s__( `ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature.`, ); + +export const TEXT_AREA_INVALID_FEEDBACK = s__( + 'ContainerRegistry|The value of this input should be less than 256 characters', +); + +export const KEEP_HEADER_TEXT = s__('ContainerRegistry|Keep these tags'); +export const KEEP_INFO_TEXT = s__( + 'ContainerRegistry|Tags that match these rules will always be %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{secondStrongStart}latest%{secondStrongEnd} tag will always be kept.', +); +export const KEEP_N_LABEL = s__('ContainerRegistry|Keep the most recent:'); +export const NAME_REGEX_KEEP_LABEL = s__('ContainerRegistry|Keep tags matching:'); +export const NAME_REGEX_KEEP_PLACEHOLDER = 'production-v.*'; +export const NAME_REGEX_KEEP_DESCRIPTION = s__( + 'ContainerRegistry|Tags with names matching this regex pattern will be kept. %{linkStart}More information%{linkEnd}', +); + +export const REMOVE_HEADER_TEXT = s__('ContainerRegistry|Remove these tags'); +export const REMOVE_INFO_TEXT = s__( + 'ContainerRegistry|Tags that match these rules will be %{strongStart}removed%{strongEnd}, unless kept by a rule above.', +); +export const EXPIRATION_SCHEDULE_LABEL = s__('ContainerRegistry|Remove tags older than:'); +export const NAME_REGEX_LABEL = s__('ContainerRegistry|Remove tags matching:'); +export const NAME_REGEX_PLACEHOLDER = '.*'; +export const NAME_REGEX_DESCRIPTION = s__( + 'ContainerRegistry|Tags with names matching this regex pattern will be removed. %{linkStart}More information%{linkEnd}', +); + +export const ENABLED_TEXT = __('Enabled'); +export const DISABLED_TEXT = __('Disabled'); + +export const ENABLE_TOGGLE_DESCRIPTION = s__( + 'ContainerRegistry|%{toggleStatus} - Tags matching the rules defined below will be automatically scheduled for deletion.', +); + +export const CADENCE_LABEL = s__('ContainerRegistry|Run cleanup every:'); + +export const NEXT_CLEANUP_LABEL = s__('ContainerRegistry|Next cleanup scheduled to run on:'); +export const NOT_SCHEDULED_POLICY_TEXT = s__('ContainerRegistry|Not yet scheduled'); +export const EXPIRATION_POLICY_FOOTER_NOTE = s__( + 'ContainerRegistry|Note: Any policy update will result in a change to the scheduled run date and time', +); + +export const NAME_REGEX_LENGTH = 255; diff --git a/app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql b/app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql index 224e0ed9472..1d6c89133af 100644 --- a/app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql +++ b/app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql @@ -5,4 +5,5 @@ fragment ContainerExpirationPolicyFields on ContainerExpirationPolicy { nameRegex nameRegexKeep olderThan + nextRunAt } diff --git a/app/assets/javascripts/registry/settings/registry_settings_bundle.js b/app/assets/javascripts/registry/settings/registry_settings_bundle.js index f7b1c5abd3a..6a4584b1b28 100644 --- a/app/assets/javascripts/registry/settings/registry_settings_bundle.js +++ b/app/assets/javascripts/registry/settings/registry_settings_bundle.js @@ -13,7 +13,13 @@ export default () => { if (!el) { return null; } - const { projectPath, isAdmin, adminSettingsPath, enableHistoricEntries } = el.dataset; + const { + isAdmin, + enableHistoricEntries, + projectPath, + adminSettingsPath, + tagsRegexHelpPagePath, + } = el.dataset; return new Vue({ el, apolloProvider, @@ -21,10 +27,11 @@ export default () => { RegistrySettingsApp, }, provide: { - projectPath, isAdmin: parseBoolean(isAdmin), - adminSettingsPath, enableHistoricEntries: parseBoolean(enableHistoricEntries), + projectPath, + adminSettingsPath, + tagsRegexHelpPagePath, }, render(createElement) { return createElement('registry-settings-app', {}); diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js b/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js index d4c1808eec2..106dd7a3b97 100644 --- a/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js +++ b/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js @@ -1,3 +1 @@ export const HIGHLIGHT_CLASS_NAME = 'hll'; - -export default {}; diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js index 40708453d79..aaadc9766db 100644 --- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js +++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js @@ -89,5 +89,3 @@ export const inputStringToIsoDate = (value, utc = false) => { */ export const isoDateToInputString = (date, utc = false) => dateformat(date, dateFormats.inputFormat, utc); - -export default {}; diff --git a/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js b/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js index 02f28da8bb0..61ab2a698ce 100644 --- a/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js +++ b/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js @@ -1,5 +1,3 @@ export function pixeliseValue(val) { return val ? `${val}px` : ''; } - -export default {}; diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss index ffc15af6329..a88ca474bb7 100644 --- a/app/assets/stylesheets/page_bundles/boards.scss +++ b/app/assets/stylesheets/page_bundles/boards.scss @@ -92,7 +92,6 @@ .board-title-caret { border-radius: $border-radius-default; line-height: $gl-spacing-scale-5; - height: $gl-spacing-scale-5; &.btn svg { top: 0; diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml index c1565cf42e1..b06070d15d4 100644 --- a/app/views/admin/application_settings/_ip_limits.html.haml +++ b/app/views/admin/application_settings/_ip_limits.html.haml @@ -2,44 +2,52 @@ = form_errors(@application_setting) %fieldset + %h5 + = _('Unauthenticated request rate limit') .form-group .form-check = f.check_box :throttle_unauthenticated_enabled, class: 'form-check-input', data: { qa_selector: 'throttle_unauthenticated_checkbox' } - = f.label :throttle_unauthenticated_enabled, class: 'form-check-label' do + = f.label :throttle_unauthenticated_enabled, class: 'form-check-label label-bold' do Enable unauthenticated request rate limit %span.form-text.text-muted Helps reduce request volume (e.g. from crawlers or abusive bots) .form-group - = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'label-bold' + = f.label :throttle_unauthenticated_requests_per_period, 'Max unauthenticated requests per period per IP', class: 'label-bold' = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control' .form-group - = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'label-bold' + = f.label :throttle_unauthenticated_period_in_seconds, 'Unauthenticated rate limit period in seconds', class: 'label-bold' = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control' + %hr + %h5 + = _('Authenticated API request rate limit') .form-group .form-check = f.check_box :throttle_authenticated_api_enabled, class: 'form-check-input', data: { qa_selector: 'throttle_authenticated_api_checkbox' } - = f.label :throttle_authenticated_api_enabled, class: 'form-check-label' do + = f.label :throttle_authenticated_api_enabled, class: 'form-check-label label-bold' do Enable authenticated API request rate limit %span.form-text.text-muted Helps reduce request volume (e.g. from crawlers or abusive bots) .form-group - = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'label-bold' + = f.label :throttle_authenticated_api_requests_per_period, 'Max authenticated API requests per period per user', class: 'label-bold' = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control' .form-group - = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'label-bold' + = f.label :throttle_authenticated_api_period_in_seconds, 'Authenticated API rate limit period in seconds', class: 'label-bold' = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control' + %hr + %h5 + = _('Authenticated web request rate limit') .form-group .form-check = f.check_box :throttle_authenticated_web_enabled, class: 'form-check-input', data: { qa_selector: 'throttle_authenticated_web_checkbox' } - = f.label :throttle_authenticated_web_enabled, class: 'form-check-label' do + = f.label :throttle_authenticated_web_enabled, class: 'form-check-label label-bold' do Enable authenticated web request rate limit %span.form-text.text-muted Helps reduce request volume (e.g. from crawlers or abusive bots) .form-group - = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'label-bold' + = f.label :throttle_authenticated_web_requests_per_period, 'Max authenticated web requests per period per user', class: 'label-bold' = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control' .form-group - = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'label-bold' + = f.label :throttle_authenticated_web_period_in_seconds, 'Authenticated web rate limit period in seconds', class: 'label-bold' = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control' = f.submit 'Save changes', class: "gl-button btn btn-success", data: { qa_selector: 'save_changes_button' } diff --git a/app/views/projects/registry/settings/_index.haml b/app/views/projects/registry/settings/_index.haml index c6fae2cc7a1..a4d4a1bb2dd 100644 --- a/app/views/projects/registry/settings/_index.haml +++ b/app/views/projects/registry/settings/_index.haml @@ -5,4 +5,5 @@ older_than_options: older_than_options.to_json, is_admin: current_user&.admin.to_s, admin_settings_path: ci_cd_admin_application_settings_path(anchor: 'js-registry-settings'), - enable_historic_entries: container_expiration_policies_historic_entry_enabled?(@project).to_s} } + enable_historic_entries: container_expiration_policies_historic_entry_enabled?(@project).to_s, + tags_regex_help_page_path: help_page_path('user/packages/container_registry/index', anchor: 'regex-pattern-examples') } } diff --git a/changelogs/unreleased/djensen-improve-differentiation-in-ip-rate-limit-ui.yml b/changelogs/unreleased/djensen-improve-differentiation-in-ip-rate-limit-ui.yml new file mode 100644 index 00000000000..0aea51649a4 --- /dev/null +++ b/changelogs/unreleased/djensen-improve-differentiation-in-ip-rate-limit-ui.yml @@ -0,0 +1,5 @@ +--- +title: Improve clarity of admin Rate Limiting UI +merge_request: 46142 +author: +type: changed diff --git a/doc/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md index 89e4224c735..b7862ff52a3 100644 --- a/doc/api/merge_request_approvals.md +++ b/doc/api/merge_request_approvals.md @@ -173,6 +173,104 @@ GET /projects/:id/approval_rules ] ``` +### Get a single project-level rule + +> - Introduced 13.7. + +You can request information about a single project approval rules using the following endpoint: + +```plaintext +GET /projects/:id/approval_rules/:approval_rule_id +``` + +**Parameters:** + +| Attribute | Type | Required | Description | +|----------------------|---------|----------|-----------------------------------------------------------| +| `id` | integer | yes | The ID of a project | +| `approval_rule_id` | integer | yes | The ID of a approval rule | + +```json +{ + "id": 1, + "name": "security", + "rule_type": "regular", + "eligible_approvers": [ + { + "id": 5, + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe" + }, + { + "id": 50, + "name": "Group Member 1", + "username": "group_member_1", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/group_member_1" + } + ], + "approvals_required": 3, + "users": [ + { + "id": 5, + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe" + } + ], + "groups": [ + { + "id": 5, + "name": "group1", + "path": "group1", + "description": "", + "visibility": "public", + "lfs_enabled": false, + "avatar_url": null, + "web_url": "http://localhost/groups/group1", + "request_access_enabled": false, + "full_name": "group1", + "full_path": "group1", + "parent_id": null, + "ldap_cn": null, + "ldap_access": null + } + ], + "protected_branches": [ + { + "id": 1, + "name": "master", + "push_access_levels": [ + { + "access_level": 30, + "access_level_description": "Developers + Maintainers" + } + ], + "merge_access_levels": [ + { + "access_level": 30, + "access_level_description": "Developers + Maintainers" + } + ], + "unprotect_access_levels": [ + { + "access_level": 40, + "access_level_description": "Maintainers" + } + ], + "code_owner_approval_required": "false" + } + ], + "contains_hidden_groups": false +} +``` + ### Create project-level rule > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11877) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.3. diff --git a/doc/user/admin_area/settings/img/user_and_ip_rate_limits.png b/doc/user/admin_area/settings/img/user_and_ip_rate_limits.png Binary files differindex 53dc0e4ac87..5056e8354a9 100644 --- a/doc/user/admin_area/settings/img/user_and_ip_rate_limits.png +++ b/doc/user/admin_area/settings/img/user_and_ip_rate_limits.png diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3cd0d686e8c..18bca31d274 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3968,6 +3968,12 @@ msgstr "" msgid "Authenticate with GitHub" msgstr "" +msgid "Authenticated API request rate limit" +msgstr "" + +msgid "Authenticated web request rate limit" +msgstr "" + msgid "Authenticating" msgstr "" @@ -7240,6 +7246,9 @@ msgstr "" msgid "ContainerRegistry|%{toggleStatus} - Tags matching the patterns defined below will be scheduled for deletion" msgstr "" +msgid "ContainerRegistry|%{toggleStatus} - Tags matching the rules defined below will be automatically scheduled for deletion." +msgstr "" + msgid "ContainerRegistry|Build an image" msgstr "" @@ -7315,6 +7324,15 @@ msgstr "" msgid "ContainerRegistry|Invalid tag: missing manifest digest" msgstr "" +msgid "ContainerRegistry|Keep tags matching:" +msgstr "" + +msgid "ContainerRegistry|Keep the most recent:" +msgstr "" + +msgid "ContainerRegistry|Keep these tags" +msgstr "" + msgid "ContainerRegistry|Login" msgstr "" @@ -7324,6 +7342,15 @@ msgstr "" msgid "ContainerRegistry|Missing or insufficient permission, delete button disabled" msgstr "" +msgid "ContainerRegistry|Next cleanup scheduled to run on:" +msgstr "" + +msgid "ContainerRegistry|Not yet scheduled" +msgstr "" + +msgid "ContainerRegistry|Note: Any policy update will result in a change to the scheduled run date and time" +msgstr "" + msgid "ContainerRegistry|Number of tags to retain:" msgstr "" @@ -7347,7 +7374,16 @@ msgid_plural "ContainerRegistry|Remove tags" msgstr[0] "" msgstr[1] "" -msgid "ContainerRegistry|Set cleanup policy" +msgid "ContainerRegistry|Remove tags matching:" +msgstr "" + +msgid "ContainerRegistry|Remove tags older than:" +msgstr "" + +msgid "ContainerRegistry|Remove these tags" +msgstr "" + +msgid "ContainerRegistry|Run cleanup every:" msgstr "" msgid "ContainerRegistry|Some tags were not deleted" @@ -7389,12 +7425,24 @@ msgstr "" msgid "ContainerRegistry|Tags successfully marked for deletion." msgstr "" +msgid "ContainerRegistry|Tags that match these rules will always be %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{secondStrongStart}latest%{secondStrongEnd} tag will always be kept." +msgstr "" + +msgid "ContainerRegistry|Tags that match these rules will be %{strongStart}removed%{strongEnd}, unless kept by a rule above." +msgstr "" + msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}" msgstr "" msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}" msgstr "" +msgid "ContainerRegistry|Tags with names matching this regex pattern will be kept. %{linkStart}More information%{linkEnd}" +msgstr "" + +msgid "ContainerRegistry|Tags with names matching this regex pattern will be removed. %{linkStart}More information%{linkEnd}" +msgstr "" + msgid "ContainerRegistry|The cleanup policy timed out before it could delete all tags. An administrator can %{adminLinkStart}manually run cleanup now%{adminLinkEnd} or you can wait for the cleanup policy to automatically run again. %{docLinkStart}More information%{docLinkEnd}" msgstr "" @@ -8428,9 +8476,6 @@ msgstr "" msgid "DastProfiles|Authentication URL" msgstr "" -msgid "DastProfiles|Copy HTTP header to clipboard" -msgstr "" - msgid "DastProfiles|Could not create the scanner profile. Please try again." msgstr "" @@ -8476,9 +8521,6 @@ msgstr "" msgid "DastProfiles|Do you want to discard your changes?" msgstr "" -msgid "DastProfiles|Download validation text file" -msgstr "" - msgid "DastProfiles|Edit scanner profile" msgstr "" @@ -8491,9 +8533,6 @@ msgstr "" msgid "DastProfiles|Error Details" msgstr "" -msgid "DastProfiles|Header validation" -msgstr "" - msgid "DastProfiles|Hide debug messages" msgstr "" @@ -8566,58 +8605,64 @@ msgstr "" msgid "DastProfiles|Site Profiles" msgstr "" -msgid "DastProfiles|Site is not validated yet, please follow the steps." +msgid "DastProfiles|Spider timeout" msgstr "" -msgid "DastProfiles|Spider timeout" +msgid "DastProfiles|Target URL" +msgstr "" + +msgid "DastProfiles|Target timeout" +msgstr "" + +msgid "DastProfiles|The maximum number of minutes allowed for the spider to traverse the site." msgstr "" -msgid "DastProfiles|Step 1 - Choose site validation method" +msgid "DastProfiles|The maximum number of seconds allowed for the site under test to respond to a request." msgstr "" -msgid "DastProfiles|Step 2 - Add following HTTP header to your site" +msgid "DastProfiles|Turn on AJAX spider" msgstr "" -msgid "DastProfiles|Step 2 - Add following text to the target site" +msgid "DastProfiles|Username" msgstr "" -msgid "DastProfiles|Step 3 - Confirm header location and validate" +msgid "DastProfiles|Username form field" msgstr "" -msgid "DastProfiles|Step 3 - Confirm text file location and validate" +msgid "DastSiteValidation|Copy HTTP header to clipboard" msgstr "" -msgid "DastProfiles|Target URL" +msgid "DastSiteValidation|Could not create validation token. Please try again." msgstr "" -msgid "DastProfiles|Target timeout" +msgid "DastSiteValidation|Download validation text file" msgstr "" -msgid "DastProfiles|Text file validation" +msgid "DastSiteValidation|Header validation" msgstr "" -msgid "DastProfiles|The maximum number of minutes allowed for the spider to traverse the site." +msgid "DastSiteValidation|Step 1 - Choose site validation method" msgstr "" -msgid "DastProfiles|The maximum number of seconds allowed for the site under test to respond to a request." +msgid "DastSiteValidation|Step 2 - Add following HTTP header to your site" msgstr "" -msgid "DastProfiles|Turn on AJAX spider" +msgid "DastSiteValidation|Step 2 - Add following text to the target site" msgstr "" -msgid "DastProfiles|Username" +msgid "DastSiteValidation|Step 3 - Confirm header location and validate" msgstr "" -msgid "DastProfiles|Username form field" +msgid "DastSiteValidation|Step 3 - Confirm text file location and validate" msgstr "" -msgid "DastProfiles|Validate" +msgid "DastSiteValidation|Text file validation" msgstr "" -msgid "DastProfiles|Validating..." +msgid "DastSiteValidation|Validate" msgstr "" -msgid "DastProfiles|Validation failed, please make sure that you follow the steps above with the chosen method." +msgid "DastSiteValidation|Validate target site" msgstr "" msgid "Data is still calculating..." @@ -28967,6 +29012,9 @@ msgstr "" msgid "Unassigned" msgstr "" +msgid "Unauthenticated request rate limit" +msgstr "" + msgid "Undo" msgstr "" diff --git a/spec/frontend/blob/components/mock_data.js b/spec/frontend/blob/components/mock_data.js index 8cfcec2693c..95789ca13cb 100644 --- a/spec/frontend/blob/components/mock_data.js +++ b/spec/frontend/blob/components/mock_data.js @@ -55,5 +55,3 @@ export const SimpleBlobContentMock = { path: 'foo.js', plainData: 'Plain', }; - -export default {}; diff --git a/spec/frontend/registry/settings/components/expiration_dropdown_spec.js b/spec/frontend/registry/settings/components/expiration_dropdown_spec.js new file mode 100644 index 00000000000..e0cac317ad6 --- /dev/null +++ b/spec/frontend/registry/settings/components/expiration_dropdown_spec.js @@ -0,0 +1,83 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlFormGroup, GlFormSelect } from 'jest/registry/shared/stubs'; +import component from '~/registry/settings/components/expiration_dropdown.vue'; + +describe('ExpirationDropdown', () => { + let wrapper; + + const defaultProps = { + name: 'foo', + label: 'label-bar', + formOptions: [{ key: 'foo', label: 'bar' }, { key: 'baz', label: 'zab' }], + }; + + const findFormSelect = () => wrapper.find(GlFormSelect); + const findFormGroup = () => wrapper.find(GlFormGroup); + const findOptions = () => wrapper.findAll('[data-testid="option"]'); + + const mountComponent = props => { + wrapper = shallowMount(component, { + stubs: { + GlFormGroup, + GlFormSelect, + }, + propsData: { + ...defaultProps, + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('structure', () => { + it('has a form-select component', () => { + mountComponent(); + expect(findFormSelect().exists()).toBe(true); + }); + + it('has the correct options', () => { + mountComponent(); + + expect(findOptions()).toHaveLength(defaultProps.formOptions.length); + }); + }); + + describe('model', () => { + it('assign the right props to the form-select component', () => { + const value = 'foobar'; + const disabled = true; + + mountComponent({ value, disabled }); + + expect(findFormSelect().props()).toMatchObject({ + value, + disabled, + }); + expect(findFormSelect().attributes('id')).toBe(defaultProps.name); + }); + + it('assign the right props to the form-group component', () => { + mountComponent(); + + expect(findFormGroup().attributes()).toMatchObject({ + id: `${defaultProps.name}-form-group`, + 'label-for': defaultProps.name, + label: defaultProps.label, + }); + }); + + it('emits input event when form-select emits input', () => { + const emittedValue = 'barfoo'; + + mountComponent(); + + findFormSelect().vm.$emit('input', emittedValue); + + expect(wrapper.emitted('input')).toEqual([[emittedValue]]); + }); + }); +}); diff --git a/spec/frontend/registry/settings/components/expiration_run_text_spec.js b/spec/frontend/registry/settings/components/expiration_run_text_spec.js new file mode 100644 index 00000000000..d023f1fd05a --- /dev/null +++ b/spec/frontend/registry/settings/components/expiration_run_text_spec.js @@ -0,0 +1,66 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlFormInput } from '@gitlab/ui'; +import { GlFormGroup } from 'jest/registry/shared/stubs'; +import component from '~/registry/settings/components/expiration_run_text.vue'; +import { NEXT_CLEANUP_LABEL, NOT_SCHEDULED_POLICY_TEXT } from '~/registry/settings/constants'; + +describe('ExpirationToggle', () => { + let wrapper; + const value = 'foo'; + + const findInput = () => wrapper.find(GlFormInput); + const findFormGroup = () => wrapper.find(GlFormGroup); + + const mountComponent = propsData => { + wrapper = shallowMount(component, { + stubs: { + GlFormGroup, + }, + propsData, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('structure', () => { + it('has an input component', () => { + mountComponent(); + expect(findInput().exists()).toBe(true); + }); + }); + + describe('model', () => { + it('assigns the right props to the input component', () => { + mountComponent({ value, disabled: true }); + + expect(findInput().attributes()).toMatchObject({ + value, + }); + }); + + it('assigns the right props to the form-group component', () => { + mountComponent(); + + expect(findFormGroup().attributes()).toMatchObject({ + label: NEXT_CLEANUP_LABEL, + }); + }); + }); + + describe('formattedValue', () => { + it('displays the values when it exists', () => { + mountComponent({ value }); + + expect(findInput().attributes('value')).toBe(value); + }); + + it('displays a placeholder when no value is present', () => { + mountComponent(); + + expect(findInput().attributes('value')).toBe(NOT_SCHEDULED_POLICY_TEXT); + }); + }); +}); diff --git a/spec/frontend/registry/settings/components/expiration_textarea_spec.js b/spec/frontend/registry/settings/components/expiration_textarea_spec.js new file mode 100644 index 00000000000..80464c61117 --- /dev/null +++ b/spec/frontend/registry/settings/components/expiration_textarea_spec.js @@ -0,0 +1,169 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlSprintf, GlFormTextarea, GlLink } from '@gitlab/ui'; +import { GlFormGroup } from 'jest/registry/shared/stubs'; +import component from '~/registry/settings/components/expiration_textarea.vue'; +import { NAME_REGEX_LENGTH } from '~/registry/shared/constants'; + +describe('ExpirationTextarea', () => { + let wrapper; + + const defaultProps = { + name: 'foo', + label: 'label-bar', + placeholder: 'placeholder-baz', + description: '%{linkStart}description-foo%{linkEnd}', + }; + + const tagsRegexHelpPagePath = 'fooPath'; + + const findTextArea = () => wrapper.find(GlFormTextarea); + const findFormGroup = () => wrapper.find(GlFormGroup); + const findLabel = () => wrapper.find('[data-testid="label"]'); + const findDescription = () => wrapper.find('[data-testid="description"]'); + const findDescriptionLink = () => wrapper.find(GlLink); + + const mountComponent = props => { + wrapper = shallowMount(component, { + stubs: { + GlSprintf, + GlFormGroup, + }, + provide: { + tagsRegexHelpPagePath, + }, + propsData: { + ...defaultProps, + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('structure', () => { + it('has a label', () => { + mountComponent(); + + expect(findLabel().text()).toBe(defaultProps.label); + }); + + it('has a textarea component', () => { + mountComponent(); + + expect(findTextArea().exists()).toBe(true); + }); + + it('has a description', () => { + mountComponent(); + + expect(findDescription().text()).toMatchInterpolatedText(defaultProps.description); + }); + + it('has a description link', () => { + mountComponent(); + + const link = findDescriptionLink(); + expect(link.exists()).toBe(true); + expect(link.attributes('href')).toBe(tagsRegexHelpPagePath); + }); + }); + + describe('model', () => { + it('assigns the right props to the textarea component', () => { + const value = 'foobar'; + const disabled = true; + + mountComponent({ value, disabled }); + + expect(findTextArea().attributes()).toMatchObject({ + id: defaultProps.name, + value, + placeholder: defaultProps.placeholder, + disabled: `${disabled}`, + trim: '', + }); + }); + + it('emits input event when textarea emits input', () => { + const emittedValue = 'barfoo'; + + mountComponent(); + + findTextArea().vm.$emit('input', emittedValue); + expect(wrapper.emitted('input')).toEqual([[emittedValue]]); + }); + }); + + describe('regex textarea validation', () => { + const invalidString = new Array(NAME_REGEX_LENGTH + 2).join(','); + + describe('when error contains an error message', () => { + const errorMessage = 'something went wrong'; + + it('shows the error message on the relevant field', () => { + mountComponent({ error: errorMessage }); + + expect(findFormGroup().attributes('invalid-feedback')).toBe(errorMessage); + }); + + it('gives precedence to API errors compared to local ones', () => { + mountComponent({ + error: errorMessage, + value: invalidString, + }); + + expect(findFormGroup().attributes('invalid-feedback')).toBe(errorMessage); + }); + }); + + describe('when error is empty', () => { + describe('if the user did not type', () => { + it('validation is not emitted', () => { + mountComponent(); + + expect(wrapper.emitted('validation')).toBeUndefined(); + }); + + it('no error message is shown', () => { + mountComponent(); + + expect(findFormGroup().props('state')).toBe(true); + expect(findFormGroup().attributes('invalid-feedback')).toBe(''); + }); + }); + + describe('when the user typed something', () => { + describe(`when name regex is longer than ${NAME_REGEX_LENGTH}`, () => { + beforeEach(() => { + // since the component has no state we both emit the event and set the prop + mountComponent({ value: invalidString }); + + findTextArea().vm.$emit('input', invalidString); + }); + + it('textAreaValidation state is false', () => { + expect(findFormGroup().props('state')).toBe(false); + expect(findTextArea().attributes('state')).toBeUndefined(); + }); + + it('emits the @validation event with false payload', () => { + expect(wrapper.emitted('validation')).toEqual([[false]]); + }); + }); + + it(`when user input is less than ${NAME_REGEX_LENGTH} state is "true"`, () => { + mountComponent(); + + findTextArea().vm.$emit('input', 'foo'); + + expect(findFormGroup().props('state')).toBe(true); + expect(findTextArea().attributes('state')).toBe('true'); + expect(wrapper.emitted('validation')).toEqual([[true]]); + }); + }); + }); + }); +}); diff --git a/spec/frontend/registry/settings/components/expiration_toggle_spec.js b/spec/frontend/registry/settings/components/expiration_toggle_spec.js new file mode 100644 index 00000000000..8b670c98dc1 --- /dev/null +++ b/spec/frontend/registry/settings/components/expiration_toggle_spec.js @@ -0,0 +1,80 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlToggle, GlSprintf } from '@gitlab/ui'; +import { GlFormGroup } from 'jest/registry/shared/stubs'; +import component from '~/registry/settings/components/expiration_toggle.vue'; +import { + ENABLE_TOGGLE_DESCRIPTION, + ENABLED_TEXT, + DISABLED_TEXT, +} from '~/registry/settings/constants'; + +describe('ExpirationToggle', () => { + let wrapper; + + const findToggle = () => wrapper.find(GlToggle); + const findDescription = () => wrapper.find('[data-testid="description"]'); + + const mountComponent = propsData => { + wrapper = shallowMount(component, { + stubs: { + GlFormGroup, + GlSprintf, + }, + propsData, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('structure', () => { + it('has a toggle component', () => { + mountComponent(); + + expect(findToggle().exists()).toBe(true); + }); + + it('has a description', () => { + mountComponent(); + + expect(findDescription().text()).toContain( + ENABLE_TOGGLE_DESCRIPTION.replace('%{toggleStatus}', ''), + ); + }); + }); + + describe('model', () => { + it('assigns the right props to the toggle component', () => { + mountComponent({ value: true, disabled: true }); + + expect(findToggle().props()).toMatchObject({ + value: true, + disabled: true, + }); + }); + + it('emits input event when toggle is updated', () => { + mountComponent(); + + findToggle().vm.$emit('change', false); + + expect(wrapper.emitted('input')).toEqual([[false]]); + }); + }); + + describe('toggle description', () => { + it('says enabled when the toggle is on', () => { + mountComponent({ value: true }); + + expect(findDescription().text()).toContain(ENABLED_TEXT); + }); + + it('says disabled when the toggle is off', () => { + mountComponent({ value: false }); + + expect(findDescription().text()).toContain(DISABLED_TEXT); + }); + }); +}); diff --git a/spec/frontend/registry/settings/mock_data.js b/spec/frontend/registry/settings/mock_data.js index 7f3772ce7fe..7cc645fcf55 100644 --- a/spec/frontend/registry/settings/mock_data.js +++ b/spec/frontend/registry/settings/mock_data.js @@ -1,13 +1,18 @@ +export const containerExpirationPolicyData = () => ({ + cadence: 'EVERY_DAY', + enabled: true, + keepN: 'TEN_TAGS', + nameRegex: 'asdasdssssdfdf', + nameRegexKeep: 'sss', + olderThan: 'FOURTEEN_DAYS', + nextRunAt: '2020-11-19T07:37:03.941Z', +}); + export const expirationPolicyPayload = override => ({ data: { project: { containerExpirationPolicy: { - cadence: 'EVERY_DAY', - enabled: true, - keepN: 'TEN_TAGS', - nameRegex: 'asdasdssssdfdf', - nameRegexKeep: 'sss', - olderThan: 'FOURTEEN_DAYS', + ...containerExpirationPolicyData(), ...override, }, }, @@ -26,12 +31,7 @@ export const expirationPolicyMutationPayload = ({ override, errors = [] } = {}) data: { updateContainerExpirationPolicy: { containerExpirationPolicy: { - cadence: 'EVERY_DAY', - enabled: true, - keepN: 'TEN_TAGS', - nameRegex: 'asdasdssssdfdf', - nameRegexKeep: 'sss', - olderThan: 'FOURTEEN_DAYS', + ...containerExpirationPolicyData(), ...override, }, errors, diff --git a/spec/frontend/registry/shared/stubs.js b/spec/frontend/registry/shared/stubs.js index f6b88d70e49..ad41eb42df4 100644 --- a/spec/frontend/registry/shared/stubs.js +++ b/spec/frontend/registry/shared/stubs.js @@ -9,3 +9,23 @@ export const GlCard = { </div> `, }; + +export const GlFormGroup = { + name: 'gl-form-group-stub', + props: ['state'], + template: ` + <div> + <slot name="label"></slot> + <slot></slot> + <slot name="description"></slot> + </div>`, +}; + +export const GlFormSelect = { + name: 'gl-form-select-stub', + props: ['disabled', 'value'], + template: ` + <div> + <slot></slot> + </div>`, +}; |