diff options
145 files changed, 1844 insertions, 719 deletions
diff --git a/.gitlab/issue_templates/Actionable Insight.md b/.gitlab/issue_templates/Actionable Insight.md new file mode 100644 index 00000000000..b84af21199d --- /dev/null +++ b/.gitlab/issue_templates/Actionable Insight.md @@ -0,0 +1,28 @@ +<!-- Actionable insights must recommend an action that needs to take place. An actionable insight both defines the _insight_ and clearly calls out _action_ or next step required to improve based on the result of the research observation or data. Actionable insights are tracked over time and will include follow-up. Learn more in the handbook here: https://about.gitlab.com/handbook/engineering/ux/ux-research-training/research-insights/#actionable-insights --> + +### Insight +<!-- Describe the insight itself: often the problem, finding, or observation. _What_ is currently happening? --> + +### Supporting evidence +<!-- Describe _why_ the problem is happening, or more details behind the finding or observation. Try to include quotes or specific data collected. Feel free to link the Actionable insight from Dovetail here if applicable instead of retyping details. --> + +### Action +<!--Describe the next step or action that needs to take place as a result of the research. The action should be clearly defined, achievable, and directly tied back to the insight. Make sure to use directive terminology, such as: conduct, explore, redesign, etc. _How_ do we take a step toward improving the experience? --> + +### Resources + <!--Add resources as links below or as related issues. --> + +- **Dovetail link:** [{Paste URL here}](url) +- **Research issue link:** [{Paste URL here}](url) +- **Follow-up issue:** [{Paste URL here}](url) + +### Tasks +- [ ] Assign this issue to the appropriate Product Manager, Product Designer, or UX Researcher. +- [ ] Add the appropriate `Group` (such as `~"group::source code"`) label to the issue. This helps identify and track actionable insights at the group level. +- [ ] Link this issue back to the original research issue in the GitLab UX Research project and the Dovetail project. + + + + +/label ~"Actionable Insight" + diff --git a/.gitlab/issue_templates/actionable_insight.md b/.gitlab/issue_templates/actionable_insight.md deleted file mode 100644 index ff6a4f12918..00000000000 --- a/.gitlab/issue_templates/actionable_insight.md +++ /dev/null @@ -1,34 +0,0 @@ -## Actionable Insights -Actionable insights always have a follow-up action that needs to take place as a result of the research observation or data, and a clear recommendation or action associated with it. An actionable insight both defines the insight and clearly calls out the next step. These insights are tracked over time and at the group level. - -#### Link - -- [ ] Provide the link to the Dovetail actionable insight you created earlier (this should contain all the essential details) -- [ ] If applicable, link this actionable insight issue back to the original Research Issue in the GitLab UX Research project - -#### Assign - -- [ ] Assign this issue to the appropriate Product Manager, Product Designer, or UX Researcher - -#### Group label - -- [ ] Add the appropriate `Group` (such as `~"group::source code"`) label to the issue. This is done to identify and track actionable insights at the group level. - -#### Description - -- [ ] Provide some brief details on the actionable insight and the action to take - -------------------------------------------------------------------------------- - -| | PLEASE COMPLETE THE BELOW | -| ------ | ------ | -| Dovetail link: | (URL goes here) | -| Details: | (details go here) | -| Action to take: | (action goes here) | - - - - - - -/label ~"Actionable Insight" diff --git a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue index 02b1d08f9c3..66d6af6f0a4 100644 --- a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue +++ b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue @@ -8,17 +8,14 @@ import { GlSearchBoxByType, GlTooltipDirective as GlTooltip, } from '@gitlab/ui'; +import { cloneDeep } from 'lodash'; import { s__, __ } from '~/locale'; -// Mocks will be removed when integrating with BE is ready -// data format is defined and will be the same as mocked (maybe with some minor changes) -// feature rollout plan - https://gitlab.com/gitlab-org/gitlab/-/issues/262707#note_442529171 import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import { getMappingData, getPayloadFields, transformForSave, } from '../utils/mapping_transformations'; -import gitlabFieldsMock from './mocks/gitlabFields.json'; export const i18n = { columns: { @@ -46,12 +43,19 @@ export default { directives: { GlTooltip, }, - inject: { - gitlabAlertFields: { - default: gitlabFieldsMock, - }, - }, props: { + alertFields: { + type: Array, + required: true, + validator: (fields) => { + return ( + fields.length && + fields.every(({ name, types, label }) => { + return typeof name === 'string' && Array.isArray(types) && typeof label === 'string'; + }) + ); + }, + }, parsedPayload: { type: Array, required: false, @@ -65,7 +69,7 @@ export default { }, data() { return { - gitlabFields: this.gitlabAlertFields, + gitlabFields: cloneDeep(this.alertFields), }; }, computed: { @@ -75,6 +79,9 @@ export default { mappingData() { return getMappingData(this.gitlabFields, this.payloadFields, this.savedMapping); }, + hasFallbackColumn() { + return this.gitlabFields.some(({ numberOfFallbacks }) => Boolean(numberOfFallbacks)); + }, }, methods: { setMapping(gitlabKey, mappingKey, valueKey) { @@ -101,10 +108,10 @@ export default { this.$options.i18n.makeSelection ); }, - getFieldValue({ label, type }) { - const types = type.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(__(' or ')); + getFieldValue({ label, types }) { + const type = types.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(__(' or ')); - return `${label} (${types})`; + return `${label} (${type})`; }, noResults(searchTerm, fields) { return !this.filterFields(searchTerm, fields).length; @@ -123,7 +130,11 @@ export default { <h5 id="parsedFieldsHeader" class="gl-display-table-cell gl-py-3 gl-pr-3"> {{ $options.i18n.columns.payloadKeyTitle }} </h5> - <h5 id="fallbackFieldsHeader" class="gl-display-table-cell gl-py-3 gl-pr-3"> + <h5 + v-if="hasFallbackColumn" + id="fallbackFieldsHeader" + class="gl-display-table-cell gl-py-3 gl-pr-3" + > {{ $options.i18n.columns.fallbackKeyTitle }} <gl-icon v-gl-tooltip diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue index df6d38efee7..cef20321ce2 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue @@ -125,6 +125,9 @@ export default { prometheus: { default: {}, }, + multiIntegrations: { + default: false, + }, }, props: { loading: { @@ -135,6 +138,11 @@ export default { type: Boolean, required: true, }, + alertFields: { + type: Array, + required: false, + default: null, + }, }, apollo: { currentIntegration: { @@ -196,8 +204,10 @@ export default { }, showMappingBuilder() { return ( + this.multiIntegrations && this.glFeatures.multipleHttpIntegrationsCustomMapping && - this.selectedIntegration === typeSet.http + this.selectedIntegration === typeSet.http && + this.alertFields?.length ); }, parsedSamplePayload() { @@ -558,6 +568,7 @@ export default { <mapping-builder :parsed-payload="parsedSamplePayload" :saved-mapping="savedMapping" + :alert-fields="alertFields" @onMappingUpdate="updateMapping" /> </gl-form-group> diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue index def84f3ed94..71d094dbe6e 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue @@ -57,6 +57,13 @@ export default { default: false, }, }, + props: { + alertFields: { + type: Array, + required: false, + default: null, + }, + }, apollo: { integrations: { fetchPolicy: fetchPolicies.CACHE_AND_NETWORK, @@ -312,6 +319,7 @@ export default { <alert-settings-form :loading="isUpdating" :can-add-integration="canAddIntegration" + :alert-fields="alertFields" @create-new-integration="createNewIntegration" @update-integration="updateIntegration" @reset-token="resetToken" diff --git a/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json b/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json deleted file mode 100644 index e4d0e92a6f8..00000000000 --- a/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json +++ /dev/null @@ -1,123 +0,0 @@ -[ - { - "name": "TITLE", - "label": "Title", - "type": [ - "STRING" - ], - "compatibleTypes": [ - "STRING", - "NUMBER", - "DATETIME" - ], - "numberOfFallbacks": 1 - }, - { - "name": "DESCRIPTION", - "label": "Description", - "type": [ - "STRING" - ], - "compatibleTypes": [ - "STRING", - "NUMBER", - "DATETIME" - ] - }, - { - "name": "START_TIME", - "label": "Start time", - "type": [ - "DATETIME" - ], - "compatibleTypes": [ - "NUMBER", - "DATETIME" - ] - }, - { - "name": "END_TIME", - "label": "End time", - "type": [ - "DATETIME" - ], - "compatibleTypes": [ - "NUMBER", - "DATETIME" - ] - }, - { - "name": "SERVICE", - "label": "Service", - "type": [ - "STRING" - ], - "compatibleTypes": [ - "STRING", - "NUMBER", - "DATETIME" - ] - }, - { - "name": "MONITORING_TOOL", - "label": "Monitoring tool", - "type": [ - "STRING" - ], - "compatibleTypes": [ - "STRING", - "NUMBER", - "DATETIME" - ] - }, - { - "name": "HOSTS", - "label": "Hosts", - "type": [ - "STRING", - "ARRAY" - ], - "compatibleTypes": [ - "STRING", - "ARRAY", - "NUMBER", - "DATETIME" - ] - }, - { - "name": "SEVERITY", - "label": "Severity", - "type": [ - "STRING" - ], - "compatibleTypes": [ - "STRING", - "NUMBER", - "DATETIME" - ] - }, - { - "name": "FINGERPRINT", - "label": "Fingerprint", - "type": [ - "STRING" - ], - "compatibleTypes": [ - "STRING", - "NUMBER", - "DATETIME" - ] - }, - { - "name": "GITLAB_ENVIRONMENT_NAME", - "label": "Environment", - "type": [ - "STRING" - ], - "compatibleTypes": [ - "STRING", - "NUMBER", - "DATETIME" - ] - } -] diff --git a/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json b/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json index c1de0d6f0e0..80fbebf2a60 100644 --- a/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json +++ b/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json @@ -6,67 +6,67 @@ { "path": ["dashboardId"], "label": "Dashboard Id", - "type": "STRING" + "type": "string" }, { "path": ["evalMatches"], "label": "Eval Matches", - "type": "ARRAY" + "type": "array" }, { "path": ["createdAt"], "label": "Created At", - "type": "DATETIME" + "type": "datetime" }, { "path": ["imageUrl"], "label": "Image Url", - "type": "STRING" + "type": "string" }, { "path": ["message"], "label": "Message", - "type": "STRING" + "type": "string" }, { "path": ["orgId"], "label": "Org Id", - "type": "STRING" + "type": "string" }, { "path": ["panelId"], "label": "Panel Id", - "type": "STRING" + "type": "string" }, { "path": ["ruleId"], "label": "Rule Id", - "type": "STRING" + "type": "string" }, { "path": ["ruleName"], "label": "Rule Name", - "type": "STRING" + "type": "string" }, { "path": ["ruleUrl"], "label": "Rule Url", - "type": "STRING" + "type": "string" }, { "path": ["state"], "label": "State", - "type": "STRING" + "type": "string" }, { "path": ["title"], "label": "Title", - "type": "STRING" + "type": "string" }, { "path": ["tags", "tag"], "label": "Tags", - "type": "STRING" + "type": "string" } ] } diff --git a/app/assets/javascripts/alerts_settings/index.js b/app/assets/javascripts/alerts_settings/index.js index 85858956987..973f5d4ec54 100644 --- a/app/assets/javascripts/alerts_settings/index.js +++ b/app/assets/javascripts/alerts_settings/index.js @@ -31,6 +31,7 @@ export default (el) => { url, projectPath, multiIntegrations, + alertFields, } = el.dataset; return new Vue({ @@ -60,7 +61,14 @@ export default (el) => { }, apolloProvider, render(createElement) { - return createElement('alert-settings-wrapper'); + return createElement('alert-settings-wrapper', { + props: { + alertFields: + gon.features?.multipleHttpIntegrationsCustomMapping && parseBoolean(multiIntegrations) + ? JSON.parse(alertFields) + : null, + }, + }); }, }); }; diff --git a/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js b/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js index a7e43c93fbf..a86103540c0 100644 --- a/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js +++ b/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js @@ -10,9 +10,7 @@ export const getMappingData = (gitlabFields, payloadFields, savedMapping) => { return gitlabFields.map((gitlabField) => { // find fields from payload that match gitlab alert field by type - const mappingFields = payloadFields.filter(({ type }) => - gitlabField.compatibleTypes.includes(type), - ); + const mappingFields = payloadFields.filter(({ type }) => gitlabField.types.includes(type)); // find the mapping that was previously stored const foundMapping = savedMapping.find(({ fieldName }) => fieldName === gitlabField.name); @@ -42,9 +40,9 @@ export const transformForSave = (mappingData) => { if (mapped) { const { path, type, label } = mapped; acc.push({ - fieldName: field.name, + fieldName: field.name.toUpperCase(), path, - type, + type: type.toUpperCase(), label, }); } diff --git a/app/assets/javascripts/members/components/avatars/user_avatar.vue b/app/assets/javascripts/members/components/avatars/user_avatar.vue index ee2d5eba8e9..991f77cf3da 100644 --- a/app/assets/javascripts/members/components/avatars/user_avatar.vue +++ b/app/assets/javascripts/members/components/avatars/user_avatar.vue @@ -69,7 +69,10 @@ export default { > <template #meta> <div v-if="statusEmoji" class="gl-p-1"> - <span v-safe-html:[$options.safeHtmlConfig]="glEmojiTag(statusEmoji)"></span> + <span + v-safe-html:[$options.safeHtmlConfig]="glEmojiTag(statusEmoji)" + class="user-status-emoji gl-mr-0" + ></span> </div> <div v-for="badge in badges" :key="badge.text" class="gl-p-1"> <gl-badge size="sm" :variant="badge.variant"> diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 921925e15c5..badd87921d4 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -95,14 +95,19 @@ export default class MilestoneSelect { name: m.title, })) .sort((mA, mB) => { + const dueDateA = mA.due_date ? parsePikadayDate(mA.due_date) : null; + const dueDateB = mB.due_date ? parsePikadayDate(mB.due_date) : null; + // Move all expired milestones to the bottom. - if (mA.expired) { - return 1; - } - if (mB.expired) { - return -1; - } - return 0; + if (mA.expired) return 1; + if (mB.expired) return -1; + + // Move milestones without due dates just above expired milestones. + if (!dueDateA) return 1; + if (!dueDateB) return -1; + + // Sort by due date in ascending order. + return dueDateA - dueDateB; }), ) .then((data) => { diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue index 37d73a75c67..460aa427196 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue @@ -11,11 +11,11 @@ * 3. Merge request widget * 4. Commit widget */ - import $ from 'jquery'; -import { GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import { GlDropdown, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import { deprecatedCreateFlash as Flash } from '~/flash'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import axios from '~/lib/utils/axios_utils'; import eventHub from '../../event_hub'; import JobItem from '../graph/job_item.vue'; @@ -24,14 +24,14 @@ import { PIPELINES_TABLE } from '../../constants'; export default { components: { GlIcon, - JobItem, GlLoadingIcon, + GlDropdown, + JobItem, }, - directives: { GlTooltip: GlTooltipDirective, }, - + mixins: [glFeatureFlagsMixin()], props: { stage: { type: Object, @@ -50,30 +50,25 @@ export default { default: '', }, }, - data() { return { isLoading: false, - dropdownContent: '', + dropdownContent: [], }; }, - computed: { - dropdownClass() { - return this.dropdownContent.length > 0 - ? 'js-builds-dropdown-container' - : 'js-builds-dropdown-loading'; + isCiMiniPipelineGlDropdown() { + // Feature flag ci_mini_pipeline_gl_dropdown + // See more at https://gitlab.com/gitlab-org/gitlab/-/issues/300400 + return this.glFeatures?.ciMiniPipelineGlDropdown; }, - triggerButtonClass() { return `ci-status-icon-${this.stage.status.group}`; }, - borderlessIcon() { return `${this.stage.status.icon}_borderless`; }, }, - watch: { updateDropdown() { if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) { @@ -81,14 +76,17 @@ export default { } }, }, - updated() { - if (this.dropdownContent.length > 0) { + if (!this.isCiMiniPipelineGlDropdown && this.dropdownContent.length) { this.stopDropdownClickPropagation(); } }, - methods: { + onShowDropdown() { + eventHub.$emit('clickedDropdown'); + this.isLoading = true; + this.fetchJobs(); + }, onClickStage() { if (!this.isDropdownOpen()) { eventHub.$emit('clickedDropdown'); @@ -96,7 +94,6 @@ export default { this.fetchJobs(); } }, - fetchJobs() { axios .get(this.stage.dropdown_path) @@ -105,13 +102,16 @@ export default { this.isLoading = false; }) .catch(() => { - this.closeDropdown(); + if (this.isCiMiniPipelineGlDropdown) { + this.$refs.stageGlDropdown.hide(); + } else { + this.closeDropdown(); + } this.isLoading = false; Flash(__('Something went wrong on our end.')); }); }, - /** * When the user right clicks or cmd/ctrl + click in the job name * the dropdown should not be closed and the link should open in another tab, @@ -119,6 +119,8 @@ export default { * * Since this component is rendered multiple times per page we need to guarantee we only * target the click event of this component. + * + * Note: This should be removed once ci_mini_pipeline_gl_dropdown FF is removed as true. */ stopDropdownClickPropagation() { $( @@ -128,23 +130,24 @@ export default { e.stopPropagation(); }); }, - closeDropdown() { if (this.isDropdownOpen()) { $(this.$refs.dropdown).dropdown('toggle'); } }, - isDropdownOpen() { return this.$el.classList.contains('show'); }, - pipelineActionRequestComplete() { if (this.type === PIPELINES_TABLE) { // warn the table to update eventHub.$emit('refreshPipelinesTable'); + return; + } + // close the dropdown in mr widget + if (this.isCiMiniPipelineGlDropdown) { + this.$refs.stageGlDropdown.hide(); } else { - // close the dropdown in mr widget $(this.$refs.dropdown).dropdown('toggle'); } }, @@ -154,32 +157,30 @@ export default { <template> <div class="dropdown"> - <button - id="stageDropdown" - ref="dropdown" + <gl-dropdown + v-if="isCiMiniPipelineGlDropdown" + ref="stageGlDropdown" v-gl-tooltip.hover - :class="triggerButtonClass" + data-testid="mini-pipeline-graph-dropdown" :title="stage.title" - class="mini-pipeline-graph-dropdown-toggle" - data-testid="mini-pipeline-graph-dropdown-toggle" - data-toggle="dropdown" - data-display="static" - type="button" - aria-haspopup="true" - aria-expanded="false" - @click="onClickStage" - > - <span :aria-label="stage.title" aria-hidden="true" class="gl-pointer-events-none"> - <gl-icon :name="borderlessIcon" /> - </span> - </button> - - <div - class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container" - aria-labelledby="stageDropdown" + variant="link" + :lazy="true" + :popper-opts="{ placement: 'bottom' }" + :toggle-class="['mini-pipeline-graph-gl-dropdown-toggle', triggerButtonClass]" + menu-class="mini-pipeline-graph-dropdown-menu" + @show="onShowDropdown" > + <template #button-content> + <span class="gl-pointer-events-none"> + <gl-icon :name="borderlessIcon" /> + </span> + </template> <gl-loading-icon v-if="isLoading" /> - <ul v-else class="js-builds-dropdown-list scrollable-menu"> + <ul + v-else + class="js-builds-dropdown-list scrollable-menu" + data-testid="mini-pipeline-graph-dropdown-menu-list" + > <li v-for="job in dropdownContent" :key="job.id"> <job-item :dropdown-length="dropdownContent.length" @@ -189,6 +190,45 @@ export default { /> </li> </ul> - </div> + </gl-dropdown> + + <template v-else> + <button + id="stageDropdown" + ref="dropdown" + v-gl-tooltip.hover + :class="triggerButtonClass" + :title="stage.title" + class="mini-pipeline-graph-dropdown-toggle" + data-testid="mini-pipeline-graph-dropdown-toggle" + data-toggle="dropdown" + data-display="static" + type="button" + aria-haspopup="true" + aria-expanded="false" + @click="onClickStage" + > + <span :aria-label="stage.title" aria-hidden="true" class="gl-pointer-events-none"> + <gl-icon :name="borderlessIcon" /> + </span> + </button> + + <div + class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container" + aria-labelledby="stageDropdown" + > + <gl-loading-icon v-if="isLoading" /> + <ul v-else class="js-builds-dropdown-list scrollable-menu"> + <li v-for="job in dropdownContent" :key="job.id"> + <job-item + :dropdown-length="dropdownContent.length" + :job="job" + css-class-job-name="mini-pipeline-graph-dropdown-item" + @pipelineActionRequestComplete="pipelineActionRequestComplete" + /> + </li> + </ul> + </div> + </template> </div> </template> diff --git a/app/assets/stylesheets/page_bundles/pipelines.scss b/app/assets/stylesheets/page_bundles/pipelines.scss index dbde7933a8b..ae36f7e3ac1 100644 --- a/app/assets/stylesheets/page_bundles/pipelines.scss +++ b/app/assets/stylesheets/page_bundles/pipelines.scss @@ -67,7 +67,8 @@ // Mini Pipelines .stage-cell { - .mini-pipeline-graph-dropdown-toggle { + .mini-pipeline-graph-dropdown-toggle, + .mini-pipeline-graph-gl-dropdown-toggle { svg { height: $ci-action-icon-size; width: $ci-action-icon-size; @@ -138,7 +139,13 @@ } // Dropdown button in mini pipeline graph -button.mini-pipeline-graph-dropdown-toggle { +button.mini-pipeline-graph-dropdown-toggle, +// As the `mini-pipeline-item` mixin specificity is lower +// than the toggle of dropdown with 'variant="link"' we add +// classes ".gl-button.btn-link" to make it more specific. +// Once FF ci_mini_pipeline_gl_dropdown is removed, the `mini-pipeline-item` +// itself could increase its specificity to simplify this selector +button.gl-button.btn-link.mini-pipeline-graph-gl-dropdown-toggle { @include mini-pipeline-item(); } diff --git a/app/assets/stylesheets/page_bundles/signup.scss b/app/assets/stylesheets/page_bundles/signup.scss index 9ed48b693b9..a207c10b04f 100644 --- a/app/assets/stylesheets/page_bundles/signup.scss +++ b/app/assets/stylesheets/page_bundles/signup.scss @@ -73,3 +73,7 @@ text-decoration: none; } } + +.edit-profile { + max-width: 460px; +} diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index c295290a123..3cab198c1f9 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -12,6 +12,7 @@ module ServiceParams :api_version, :bamboo_url, :branches_to_be_notified, + :labels_to_be_notified, :build_key, :build_type, :ca_pem, diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 2e48f2f0e45..b694efbc1eb 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -18,6 +18,9 @@ class Projects::CommitController < Projects::ApplicationController before_action :define_commit_vars, only: [:show, :diff_for_path, :diff_files, :pipelines, :merge_requests] before_action :define_note_vars, only: [:show, :diff_for_path, :diff_files] before_action :authorize_edit_tree!, only: [:revert, :cherry_pick] + before_action only: [:pipelines] do + push_frontend_feature_flag(:ci_mini_pipeline_gl_dropdown, @project, type: :development, default_enabled: :yaml) + end BRANCH_SEARCH_LIMIT = 1000 diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 14e4f3e7dd8..b8467670e4b 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -45,6 +45,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:suggestions_custom_commit, @project) push_frontend_feature_flag(:local_file_reviews, default_enabled: :yaml) push_frontend_feature_flag(:paginated_notes, @project, default_enabled: :yaml) + push_frontend_feature_flag(:ci_mini_pipeline_gl_dropdown, @project, type: :development, default_enabled: :yaml) record_experiment_user(:invite_members_version_a) record_experiment_user(:invite_members_version_b) diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 68b59a3d61c..8edc2e732e0 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -16,6 +16,7 @@ class Projects::PipelinesController < Projects::ApplicationController push_frontend_feature_flag(:new_pipeline_form, project, default_enabled: true) push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: :yaml) push_frontend_feature_flag(:graphql_pipeline_details_users, current_user, type: :development, default_enabled: :yaml) + push_frontend_feature_flag(:ci_mini_pipeline_gl_dropdown, project, type: :development, default_enabled: :yaml) end before_action :ensure_pipeline, only: [:show] before_action :push_experiment_to_gon, only: :index, if: :html_request? diff --git a/app/controllers/projects/security/configuration_controller.rb b/app/controllers/projects/security/configuration_controller.rb new file mode 100644 index 00000000000..9366ca7b0ed --- /dev/null +++ b/app/controllers/projects/security/configuration_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Projects + module Security + class ConfigurationController < Projects::ApplicationController + feature_category :static_application_security_testing + + def show + return render_404 unless feature_enabled? + + render_403 unless can?(current_user, :read_security_configuration, project) + end + + private + + def feature_enabled? + ::Feature.enabled?(:secure_security_and_compliance_configuration_page_on_ce, @project, default_enabled: :yaml) + end + end + end +end + +Projects::Security::ConfigurationController.prepend_if_ee('EE::Projects::Security::ConfigurationController') diff --git a/app/graphql/mutations/alert_management/base.rb b/app/graphql/mutations/alert_management/base.rb index 3a57cb9670d..86908c1449c 100644 --- a/app/graphql/mutations/alert_management/base.rb +++ b/app/graphql/mutations/alert_management/base.rb @@ -21,7 +21,7 @@ module Mutations field :todo, Types::TodoType, null: true, - description: "The todo after mutation." + description: "The to-do item after mutation." field :issue, Types::IssueType, diff --git a/app/graphql/mutations/todos/create.rb b/app/graphql/mutations/todos/create.rb index 814f7ec4fc4..b6250b0228c 100644 --- a/app/graphql/mutations/todos/create.rb +++ b/app/graphql/mutations/todos/create.rb @@ -14,7 +14,7 @@ module Mutations field :todo, Types::TodoType, null: true, - description: 'The to-do created.' + description: 'The to-do item created.' def resolve(target_id:) id = ::Types::GlobalIDType[Todoable].coerce_isolated_input(target_id) diff --git a/app/graphql/mutations/todos/mark_all_done.rb b/app/graphql/mutations/todos/mark_all_done.rb index c8359953567..22a5893d4ec 100644 --- a/app/graphql/mutations/todos/mark_all_done.rb +++ b/app/graphql/mutations/todos/mark_all_done.rb @@ -10,12 +10,12 @@ module Mutations field :updated_ids, [::Types::GlobalIDType[::Todo]], null: false, - deprecated: { reason: 'Use todos', milestone: '13.2' }, - description: 'Ids of the updated todos.' + deprecated: { reason: 'Use to-do items', milestone: '13.2' }, + description: 'IDs of the updated to-do items.' field :todos, [::Types::TodoType], null: false, - description: 'Updated todos.' + description: 'Updated to-do items.' def resolve authorize!(current_user) diff --git a/app/graphql/mutations/todos/mark_done.rb b/app/graphql/mutations/todos/mark_done.rb index 1d799f11111..a78cc91da68 100644 --- a/app/graphql/mutations/todos/mark_done.rb +++ b/app/graphql/mutations/todos/mark_done.rb @@ -10,11 +10,11 @@ module Mutations argument :id, ::Types::GlobalIDType[::Todo], required: true, - description: 'The global ID of the to-do to mark as done.' + description: 'The global ID of the to-do item to mark as done.' field :todo, Types::TodoType, null: false, - description: 'The requested to-do.' + description: 'The requested to-do item.' def resolve(id:) todo = authorized_find!(id: id) diff --git a/app/graphql/mutations/todos/restore.rb b/app/graphql/mutations/todos/restore.rb index c0ac154f2a0..70c33c439c4 100644 --- a/app/graphql/mutations/todos/restore.rb +++ b/app/graphql/mutations/todos/restore.rb @@ -10,11 +10,11 @@ module Mutations argument :id, ::Types::GlobalIDType[::Todo], required: true, - description: 'The global ID of the to-do to restore.' + description: 'The global ID of the to-do item to restore.' field :todo, Types::TodoType, null: false, - description: 'The requested to-do.' + description: 'The requested to-do item.' def resolve(id:) todo = authorized_find!(id: id) diff --git a/app/graphql/mutations/todos/restore_many.rb b/app/graphql/mutations/todos/restore_many.rb index ed02c054293..dc02ffadada 100644 --- a/app/graphql/mutations/todos/restore_many.rb +++ b/app/graphql/mutations/todos/restore_many.rb @@ -10,16 +10,16 @@ module Mutations argument :ids, [::Types::GlobalIDType[::Todo]], required: true, - description: 'The global IDs of the to-dos to restore (a maximum of 50 is supported at once).' + description: 'The global IDs of the to-do items to restore (a maximum of 50 is supported at once).' field :updated_ids, [::Types::GlobalIDType[Todo]], null: false, description: 'The IDs of the updated to-do items.', - deprecated: { reason: 'Use todos', milestone: '13.2' } + deprecated: { reason: 'Use to-do items', milestone: '13.2' } field :todos, [::Types::TodoType], null: false, - description: 'Updated to-dos.' + description: 'Updated to-do items.' def resolve(ids:) check_update_amount_limit!(ids) @@ -46,7 +46,7 @@ module Mutations end def raise_too_many_todos_requested_error - raise Gitlab::Graphql::Errors::ArgumentError, 'Too many to-dos requested.' + raise Gitlab::Graphql::Errors::ArgumentError, 'Too many to-do items requested.' end def check_update_amount_limit!(ids) diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb index 180afd62299..6b7e7030c1f 100644 --- a/app/graphql/types/alert_management/alert_type.rb +++ b/app/graphql/types/alert_management/alert_type.rb @@ -112,7 +112,7 @@ module Types field :todos, Types::TodoType.connection_type, null: true, - description: 'To-dos of the current user for the alert.', + description: 'To-do items of the current user for the alert.', resolver: Resolvers::TodoResolver field :details_url, diff --git a/app/graphql/types/current_user_todos.rb b/app/graphql/types/current_user_todos.rb index d729f5a89bd..79a430af1d7 100644 --- a/app/graphql/types/current_user_todos.rb +++ b/app/graphql/types/current_user_todos.rb @@ -8,10 +8,10 @@ module Types field_class Types::BaseField field :current_user_todos, Types::TodoType.connection_type, - description: 'Todos for the current user.', + description: 'To-do items for the current user.', null: false do argument :state, Types::TodoStateEnum, - description: 'State of the todos.', + description: 'State of the to-do items.', required: false end diff --git a/app/graphql/types/todo_type.rb b/app/graphql/types/todo_type.rb index 919bff802c2..4cf2dbcab9e 100644 --- a/app/graphql/types/todo_type.rb +++ b/app/graphql/types/todo_type.rb @@ -10,42 +10,42 @@ module Types authorize :read_todo field :id, GraphQL::ID_TYPE, - description: 'ID of the to-do', + description: 'ID of the to-do item', null: false field :project, Types::ProjectType, - description: 'The project this to-do is associated with', + description: 'The project this to-do item is associated with', null: true, authorize: :read_project field :group, Types::GroupType, - description: 'Group this to-do is associated with', + description: 'Group this to-do item is associated with', null: true, authorize: :read_group field :author, Types::UserType, - description: 'The author of this to-do', + description: 'The author of this to-do item', null: false field :action, Types::TodoActionEnum, - description: 'Action of the to-do', + description: 'Action of the to-do item', null: false field :target_type, Types::TodoTargetEnum, - description: 'Target type of the to-do', + description: 'Target type of the to-do item', null: false field :body, GraphQL::STRING_TYPE, - description: 'Body of the to-do', + description: 'Body of the to-do item', null: false, calls_gitaly: true # TODO This is only true when `target_type` is `Commit`. See https://gitlab.com/gitlab-org/gitlab/issues/34757#note_234752665 field :state, Types::TodoStateEnum, - description: 'State of the to-do', + description: 'State of the to-do item', null: false field :created_at, Types::TimeType, - description: 'Timestamp this to-do was created', + description: 'Timestamp this to-do item was created', null: false def project diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb index 93503268319..c179c84ba84 100644 --- a/app/graphql/types/user_type.rb +++ b/app/graphql/types/user_type.rb @@ -31,7 +31,7 @@ module Types description: 'Web path of the user' field :todos, Types::TodoType.connection_type, null: false, resolver: Resolvers::TodoResolver, - description: 'Todos of the user' + description: 'To-do items of the user' field :group_memberships, Types::GroupMemberType.connection_type, null: true, description: 'Group memberships of the user' field :group_count, GraphQL::INT_TYPE, null: true, diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index e2fd6305892..a2e9952f350 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -139,6 +139,10 @@ module ProjectsHelper project_nav_tabs.include? name end + def any_project_nav_tab?(tabs) + tabs.any? { |tab| project_nav_tab?(tab) } + end + def project_for_deploy_key(deploy_key) if deploy_key.has_access_to?(@project) @project @@ -374,6 +378,20 @@ module ProjectsHelper private + def can_read_security_configuration?(project, current_user) + ::Feature.enabled?(:secure_security_and_compliance_configuration_page_on_ce, @subject, default_enabled: :yaml) && + can?(current_user, :read_security_configuration, project) + end + + def get_project_security_nav_tabs(project, current_user) + if can_read_security_configuration?(project, current_user) + [:security_and_compliance, :security_configuration] + else + [] + end + end + + # rubocop:disable Metrics/CyclomaticComplexity def get_project_nav_tabs(project, current_user) nav_tabs = [:home] @@ -382,6 +400,8 @@ module ProjectsHelper nav_tabs << :releases if can?(current_user, :read_release, project) end + nav_tabs += get_project_security_nav_tabs(project, current_user) + if project.repo_exists? && can?(current_user, :read_merge_request, project) nav_tabs << :merge_requests end @@ -415,6 +435,7 @@ module ProjectsHelper nav_tabs end + # rubocop:enable Metrics/CyclomaticComplexity def package_nav_tabs(project, current_user) [].tap do |tabs| @@ -695,6 +716,12 @@ module ProjectsHelper "#{request.path}?#{options.to_param}" end + def sidebar_security_configuration_paths + %w[ + projects/security/configuration#show + ] + end + def sidebar_projects_paths %w[ projects#show @@ -759,6 +786,10 @@ module ProjectsHelper ] end + def sidebar_security_paths + %w[projects/security/configuration#show] + end + def user_can_see_auto_devops_implicitly_enabled_banner?(project, user) Ability.allowed?(user, :admin_project, project) && project.has_auto_devops_implicitly_enabled? && diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index a5d4d6872df..1ea2d4412b1 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -299,6 +299,27 @@ module UsersHelper html_escape(s_('Profile|%{job_title} at %{organization}')) % { job_title: job_title, organization: organization } end + + def user_table_headers + [ + { + section_class_name: 'section-40', + header_text: _('Name') + }, + { + section_class_name: 'section-10', + header_text: _('Projects') + }, + { + section_class_name: 'section-15', + header_text: _('Created on') + }, + { + section_class_name: 'section-15', + header_text: _('Last activity') + } + ] + end end UsersHelper.prepend_if_ee('EE::UsersHelper') diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb index 473b430bb04..db5df6c2c9f 100644 --- a/app/models/concerns/triggerable_hooks.rb +++ b/app/models/concerns/triggerable_hooks.rb @@ -16,7 +16,8 @@ module TriggerableHooks deployment_hooks: :deployment_events, feature_flag_hooks: :feature_flag_events, release_hooks: :releases_events, - member_hooks: :member_events + member_hooks: :member_events, + subgroup_hooks: :subgroup_events }.freeze extend ActiveSupport::Concern diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index c9e97efb4ac..1d50d5cf19e 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -11,11 +11,13 @@ class ChatNotificationService < Service tag_push pipeline wiki_page deployment ].freeze + SUPPORTED_EVENTS_FOR_LABEL_FILTER = %w[issue confidential_issue merge_request note confidential_note].freeze + EVENT_CHANNEL = proc { |event| "#{event}_channel" } default_value_for :category, 'chat' - prop_accessor :webhook, :username, :channel, :branches_to_be_notified + prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified # Custom serialized properties initialization prop_accessor(*SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] }) @@ -62,12 +64,16 @@ class ChatNotificationService < Service { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}", required: true }.freeze, { type: 'text', name: 'username', placeholder: 'e.g. GitLab' }.freeze, { type: 'checkbox', name: 'notify_only_broken_pipelines' }.freeze, - { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }.freeze + { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }.freeze, + { type: 'text', name: 'labels_to_be_notified', placeholder: 'e.g. ~backend', help: 'Only supported for issue, merge request and note events.' }.freeze ].freeze end def execute(data) return unless supported_events.include?(data[:object_kind]) + + return unless notify_label?(data) + return unless webhook.present? object_kind = data[:object_kind] @@ -114,6 +120,22 @@ class ChatNotificationService < Service private + def labels_to_be_notified_list + return [] if labels_to_be_notified.nil? + + labels_to_be_notified.delete('~').split(',').map(&:strip) + end + + def notify_label?(data) + return true unless SUPPORTED_EVENTS_FOR_LABEL_FILTER.include?(data[:object_kind]) && labels_to_be_notified.present? + + issue_labels = data.dig(:issue, :labels) || [] + merge_request_labels = data.dig(:merge_request, :labels) || [] + label_titles = (issue_labels + merge_request_labels).pluck(:title) + + (labels_to_be_notified_list & label_titles).any? + end + # every notifier must implement this independently def notify(message, opts) raise NotImplementedError diff --git a/app/models/u2f_registration.rb b/app/models/u2f_registration.rb index 1a389081913..65dc7a47533 100644 --- a/app/models/u2f_registration.rb +++ b/app/models/u2f_registration.rb @@ -4,11 +4,19 @@ class U2fRegistration < ApplicationRecord belongs_to :user - after_commit :schedule_webauthn_migration, on: :create - after_commit :update_webauthn_registration, on: :update, if: :counter_changed? - def schedule_webauthn_migration - BackgroundMigrationWorker.perform_async('MigrateU2fWebauthn', [id, id]) + after_create :create_webauthn_registration + after_update :update_webauthn_registration, if: :counter_changed? + + def create_webauthn_registration + converter = Gitlab::Auth::U2fWebauthnConverter.new(self) + WebauthnRegistration.create!(converter.convert) + rescue StandardError => ex + Gitlab::AppJsonLogger.error( + event: 'u2f_migration', + error: ex.class.name, + backtrace: ::Gitlab::BacktraceCleaner.clean_backtrace(ex.backtrace), + message: "U2F to WebAuthn conversion failed") end def update_webauthn_registration diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index f6d1b376b92..83acf0c12d7 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -581,6 +581,10 @@ class ProjectPolicy < BasePolicy enable :read_issue_link end + rule { can?(:developer_access) }.policy do + enable :read_security_configuration + end + # Design abilities could also be prevented in the issue policy. rule { design_management_disabled }.policy do prevent :read_design diff --git a/app/services/resource_events/base_change_timebox_service.rb b/app/services/resource_events/base_change_timebox_service.rb index 5c83f7b12f7..d802bbee107 100644 --- a/app/services/resource_events/base_change_timebox_service.rb +++ b/app/services/resource_events/base_change_timebox_service.rb @@ -2,12 +2,11 @@ module ResourceEvents class BaseChangeTimeboxService - attr_reader :resource, :user, :event_created_at + attr_reader :resource, :user - def initialize(resource, user, created_at: Time.current) + def initialize(resource, user) @resource = resource @user = user - @event_created_at = created_at end def execute @@ -27,7 +26,7 @@ module ResourceEvents { user_id: user.id, - created_at: event_created_at, + created_at: resource.system_note_timestamp, key => resource.id } end diff --git a/app/services/resource_events/change_milestone_service.rb b/app/services/resource_events/change_milestone_service.rb index dcdf87599ac..24935a3327a 100644 --- a/app/services/resource_events/change_milestone_service.rb +++ b/app/services/resource_events/change_milestone_service.rb @@ -4,8 +4,8 @@ module ResourceEvents class ChangeMilestoneService < BaseChangeTimeboxService attr_reader :milestone, :old_milestone - def initialize(resource, user, created_at: Time.current, old_milestone:) - super(resource, user, created_at: created_at) + def initialize(resource, user, old_milestone:) + super(resource, user) @milestone = resource&.milestone @old_milestone = old_milestone diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml index 697c0175b4f..57edb9abe90 100644 --- a/app/views/admin/users/_users.html.haml +++ b/app/views/admin/users/_users.html.haml @@ -78,10 +78,8 @@ - else .table-holder .thead-white.text-nowrap.gl-responsive-table-row.table-row-header{ role: 'row' } - .table-section.section-40{ role: 'rowheader' }= _('Name') - .table-section.section-10{ role: 'rowheader' }= _('Projects') - .table-section.section-15{ role: 'rowheader' }= _('Created on') - .table-section.section-15{ role: 'rowheader' }= _('Last activity') + - user_table_headers.each do |header| + .table-section{ class: header[:section_class_name], role: 'rowheader' }= header[:header_text] = render partial: 'admin/users/user', collection: @users diff --git a/app/views/layouts/nav/sidebar/_project_security_link.html.haml b/app/views/layouts/nav/sidebar/_project_security_link.html.haml new file mode 100644 index 00000000000..426845639e3 --- /dev/null +++ b/app/views/layouts/nav/sidebar/_project_security_link.html.haml @@ -0,0 +1,21 @@ +- top_level_link = project_security_configuration_path(@project) +- top_level_qa_selector = 'security_configuration_link' +- if any_project_nav_tab?([:security_configuration]) + = nav_link(path: sidebar_security_paths) do + = link_to top_level_link, data: { qa_selector: top_level_qa_selector } do + .nav-icon-container + = sprite_icon('shield') + %span.nav-item-name + = _('Security & Compliance') + + %ul.sidebar-sub-level-items + = nav_link(path: sidebar_security_paths, html_options: { class: "fly-out-top-item" } ) do + = link_to top_level_link do + %strong.fly-out-top-item-name + = _('Security & Compliance') + + %li.divider.fly-out-top-item + - if project_nav_tab?(:security_configuration) + = nav_link(path: sidebar_security_configuration_paths) do + = link_to project_security_configuration_path(@project), title: _('Configuration'), data: { qa_selector: 'security_configuration_link'} do + %span= _('Configuration') diff --git a/app/views/layouts/welcome.html.haml b/app/views/layouts/welcome.html.haml index 30ba7f7f230..944f524d692 100644 --- a/app/views/layouts/welcome.html.haml +++ b/app/views/layouts/welcome.html.haml @@ -4,5 +4,5 @@ %body.ui-indigo.gl-display-flex.vh-100 = render "layouts/header/logo_with_title" = render "layouts/broadcast" - .container.d-flex.flex-grow-1.m-0 + .container.gl-display-flex.gl-flex-grow-1 = yield diff --git a/app/views/projects/security/configuration/show.html.haml b/app/views/projects/security/configuration/show.html.haml new file mode 100644 index 00000000000..1a371955be8 --- /dev/null +++ b/app/views/projects/security/configuration/show.html.haml @@ -0,0 +1,4 @@ +- breadcrumb_title _("Security Configuration") +- page_title _("Security Configuration") + +#js-security-configuration-static diff --git a/app/views/registrations/welcome/show.html.haml b/app/views/registrations/welcome/show.html.haml index d44a29a5bba..d2a2853ecd7 100644 --- a/app/views/registrations/welcome/show.html.haml +++ b/app/views/registrations/welcome/show.html.haml @@ -1,4 +1,5 @@ - page_title _('Your profile') +- add_page_specific_style 'page_bundles/signup' .row.gl-flex-grow-1 .d-flex.gl-flex-direction-column.gl-align-items-center.gl-w-full.gl-p-5 diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 667b56c725a..f3d9b9cfe27 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -50,6 +50,7 @@ = s_('Webhooks|This URL will be triggered when a confidential issue is created/updated/merged') - if @group = render_if_exists 'groups/hooks/member_events', form: form + = render_if_exists 'groups/hooks/subgroup_events', form: form %li = form.check_box :merge_requests_events, class: 'form-check-input' = form.label :merge_requests_events, class: 'list-label form-check-label gl-ml-1' do diff --git a/changelogs/unreleased/233708-fix-resource-event-timestamps.yml b/changelogs/unreleased/233708-fix-resource-event-timestamps.yml new file mode 100644 index 00000000000..17d19b82e15 --- /dev/null +++ b/changelogs/unreleased/233708-fix-resource-event-timestamps.yml @@ -0,0 +1,6 @@ +--- +title: Use user-provided timestamp when updating issue and merge request milestones, + iterations, and weights using the API +merge_request: 53237 +author: +type: fixed diff --git a/changelogs/unreleased/262047-order-milestones-by-due-date.yml b/changelogs/unreleased/262047-order-milestones-by-due-date.yml new file mode 100644 index 00000000000..99641240b34 --- /dev/null +++ b/changelogs/unreleased/262047-order-milestones-by-due-date.yml @@ -0,0 +1,5 @@ +--- +title: Sort milestone dropdown items by due date +merge_request: 53242 +author: +type: changed diff --git a/changelogs/unreleased/chat_notification_label_support.yml b/changelogs/unreleased/chat_notification_label_support.yml new file mode 100644 index 00000000000..b6e7b3a89bd --- /dev/null +++ b/changelogs/unreleased/chat_notification_label_support.yml @@ -0,0 +1,5 @@ +--- +title: Add chat notification label support +merge_request: 52105 +author: +type: added diff --git a/changelogs/unreleased/nicolasdular-fix-welcome-page.yml b/changelogs/unreleased/nicolasdular-fix-welcome-page.yml new file mode 100644 index 00000000000..b9d6914e1af --- /dev/null +++ b/changelogs/unreleased/nicolasdular-fix-welcome-page.yml @@ -0,0 +1,5 @@ +--- +title: Fix welcome page alignment on CE +merge_request: 53265 +author: +type: fixed diff --git a/changelogs/unreleased/peterhegman-fix-members-status-emoji-size.yml b/changelogs/unreleased/peterhegman-fix-members-status-emoji-size.yml new file mode 100644 index 00000000000..ccb4e06ad95 --- /dev/null +++ b/changelogs/unreleased/peterhegman-fix-members-status-emoji-size.yml @@ -0,0 +1,5 @@ +--- +title: Fix size of group member user status emoji +merge_request: 52730 +author: +type: fixed diff --git a/changelogs/unreleased/skr-refactor-users.yml b/changelogs/unreleased/skr-refactor-users.yml new file mode 100644 index 00000000000..c64cb1b6275 --- /dev/null +++ b/changelogs/unreleased/skr-refactor-users.yml @@ -0,0 +1,5 @@ +--- +title: Refactored admin user table headers +merge_request: 52891 +author: Shubham Kumar (@imskr) +type: changed diff --git a/config/feature_flags/development/ci_mini_pipeline_gl_dropdown.yml b/config/feature_flags/development/ci_mini_pipeline_gl_dropdown.yml new file mode 100644 index 00000000000..fccebf552a9 --- /dev/null +++ b/config/feature_flags/development/ci_mini_pipeline_gl_dropdown.yml @@ -0,0 +1,8 @@ +--- +name: ci_mini_pipeline_gl_dropdown +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52821 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300400 +milestone: '13.9' +type: development +group: group::continuous integration +default_enabled: false diff --git a/config/feature_flags/development/secure_security_and_compliance_configuration_page_on_ce.yml b/config/feature_flags/development/secure_security_and_compliance_configuration_page_on_ce.yml new file mode 100644 index 00000000000..e86211243a1 --- /dev/null +++ b/config/feature_flags/development/secure_security_and_compliance_configuration_page_on_ce.yml @@ -0,0 +1,8 @@ +--- +name: secure_security_and_compliance_configuration_page_on_ce +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50282 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/294076 +milestone: '13.9' +type: development +group: group::static analysis +default_enabled: false diff --git a/config/routes/project.rb b/config/routes/project.rb index 97834894ad1..e6df2532479 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -37,6 +37,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end + namespace :security do + resource :configuration, only: [:show], controller: :configuration + end + resources :artifacts, only: [:index, :destroy] resources :packages, only: [:index, :show, :destroy], module: :packages diff --git a/doc/.vale/gitlab/spelling-exceptions.txt b/doc/.vale/gitlab/spelling-exceptions.txt index 9e7fdef62c3..c3dc596acb7 100644 --- a/doc/.vale/gitlab/spelling-exceptions.txt +++ b/doc/.vale/gitlab/spelling-exceptions.txt @@ -1,3 +1,5 @@ +accessor +accessors Akismet Alertmanager Algolia @@ -75,6 +77,7 @@ burndown burnup burstable cacheable +Caddy callstack callstacks Camo @@ -291,6 +294,7 @@ Leiningen libFuzzer Libravatar liveness +Lodash Lograge logrotate Logrus @@ -582,11 +586,14 @@ swimlane swimlanes syncable Sysbench +syscall +syscalls syslog tanuki tcpdump templated Thanos +Thoughtbot throughputs Tiller timebox @@ -605,6 +612,7 @@ toolkits tooltip tooltips transpile +transpiled transpiles transpiling Trello @@ -636,6 +644,7 @@ unchecks uncomment uncommented uncommenting +uncordon unencode unencoded unencoder @@ -742,6 +751,7 @@ Worldline Xcode Xeon YouTrack +Yubico Zeitwerk Zendesk zsh diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 460f8470092..2b7a7300ad8 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -971,3 +971,21 @@ If the wildcard DNS [prerequisite](#prerequisites) can't be met, you can still u 1. [Move](../../user/project/settings/index.md#transferring-an-existing-project-into-another-namespace) all projects you need to use Pages with into a single group namespace, for example `pages`. 1. Configure a [DNS entry](#dns-configuration) without the `*.`-wildcard, for example `pages.example.io`. + +### Pages daemon fails with permission denied errors + +If `/tmp` is mounted with `noexec`, the Pages daemon fails to start with an error like: + +```plaintext +{"error":"fork/exec /gitlab-pages: permission denied","level":"fatal","msg":"could not create pages daemon","time":"2021-02-02T21:54:34Z"} +``` + +In this case, change `TMPDIR` to a location that is not mounted with `noexec`. Add the following to +`/etc/gitlab/gitlab.rb`: + +```ruby +gitlab_pages['env'] = {'TMPDIR' => '<new_tmp_path>'} +``` + +Once added, reconfigure with `sudo gitlab-ctl reconfigure` and restart GitLab with +`sudo gitlab-ctl restart`. diff --git a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md index 26e44a49aec..7b8828dc14e 100644 --- a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md +++ b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md @@ -88,7 +88,7 @@ and they will assist you with any issues you are having. - Minimal configuration that can be used to [test a Kubernetes Helm chart](https://gitlab.com/gitlab-org/charts/gitlab/-/issues/620). -- Tailing logs of a separate pod. An example for a Webservice pod: +- Tailing logs of a separate pod. An example for a `webservice` pod: ```shell kubectl logs gitlab-webservice-54fbf6698b-hpckq -c webservice diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 36f19032154..3f716767748 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -350,7 +350,7 @@ type AlertManagementAlert implements Noteable { title: String """ - To-dos of the current user for the alert. + To-do items of the current user for the alert. """ todos( """ @@ -1017,7 +1017,7 @@ type AlertSetAssigneesPayload { issue: Issue """ - The todo after mutation. + The to-do item after mutation. """ todo: Todo } @@ -1067,7 +1067,7 @@ type AlertTodoCreatePayload { issue: Issue """ - The todo after mutation. + The to-do item after mutation. """ todo: Todo } @@ -1734,7 +1734,7 @@ type BoardEpic implements CurrentUserTodos & Noteable { createdAt: Time """ - Todos for the current user. + To-do items for the current user. """ currentUserTodos( """ @@ -1758,7 +1758,7 @@ type BoardEpic implements CurrentUserTodos & Noteable { last: Int """ - State of the todos. + State of the to-do items. """ state: TodoStateEnum ): TodoConnection! @@ -4515,7 +4515,7 @@ type CreateAlertIssuePayload { issue: Issue """ - The todo after mutation. + The to-do item after mutation. """ todo: Todo } @@ -5480,7 +5480,7 @@ type CreateTestCasePayload { interface CurrentUserTodos { """ - Todos for the current user. + To-do items for the current user. """ currentUserTodos( """ @@ -5504,7 +5504,7 @@ interface CurrentUserTodos { last: Int """ - State of the todos. + State of the to-do items. """ state: TodoStateEnum ): TodoConnection! @@ -6622,7 +6622,7 @@ A single design """ type Design implements CurrentUserTodos & DesignFields & Noteable { """ - Todos for the current user. + To-do items for the current user. """ currentUserTodos( """ @@ -6646,7 +6646,7 @@ type Design implements CurrentUserTodos & DesignFields & Noteable { last: Int """ - State of the todos. + State of the to-do items. """ state: TodoStateEnum ): TodoConnection! @@ -8580,7 +8580,7 @@ type Epic implements CurrentUserTodos & Noteable { createdAt: Time """ - Todos for the current user. + To-do items for the current user. """ currentUserTodos( """ @@ -8604,7 +8604,7 @@ type Epic implements CurrentUserTodos & Noteable { last: Int """ - State of the todos. + State of the to-do items. """ state: TodoStateEnum ): TodoConnection! @@ -9260,7 +9260,7 @@ type EpicIssue implements CurrentUserTodos & Noteable { createdAt: Time! """ - Todos for the current user. + To-do items for the current user. """ currentUserTodos( """ @@ -9284,7 +9284,7 @@ type EpicIssue implements CurrentUserTodos & Noteable { last: Int """ - State of the todos. + State of the to-do items. """ state: TodoStateEnum ): TodoConnection! @@ -12471,7 +12471,7 @@ type Issue implements CurrentUserTodos & Noteable { createdAt: Time! """ - Todos for the current user. + To-do items for the current user. """ currentUserTodos( """ @@ -12495,7 +12495,7 @@ type Issue implements CurrentUserTodos & Noteable { last: Int """ - State of the todos. + State of the to-do items. """ state: TodoStateEnum ): TodoConnection! @@ -14554,7 +14554,7 @@ type MergeRequest implements CurrentUserTodos & Noteable { createdAt: Time! """ - Todos for the current user. + To-do items for the current user. """ currentUserTodos( """ @@ -14578,7 +14578,7 @@ type MergeRequest implements CurrentUserTodos & Noteable { last: Int """ - State of the todos. + State of the to-do items. """ state: TodoStateEnum ): TodoConnection! @@ -24925,47 +24925,47 @@ Representing a to-do entry """ type Todo { """ - Action of the to-do + Action of the to-do item """ action: TodoActionEnum! """ - The author of this to-do + The author of this to-do item """ author: User! """ - Body of the to-do + Body of the to-do item """ body: String! """ - Timestamp this to-do was created + Timestamp this to-do item was created """ createdAt: Time! """ - Group this to-do is associated with + Group this to-do item is associated with """ group: Group """ - ID of the to-do + ID of the to-do item """ id: ID! """ - The project this to-do is associated with + The project this to-do item is associated with """ project: Project """ - State of the to-do + State of the to-do item """ state: TodoStateEnum! """ - Target type of the to-do + Target type of the to-do item """ targetType: TodoTargetEnum! } @@ -25030,7 +25030,7 @@ type TodoCreatePayload { errors: [String!]! """ - The to-do created. + The to-do item created. """ todo: Todo } @@ -25065,7 +25065,7 @@ input TodoMarkDoneInput { clientMutationId: String """ - The global ID of the to-do to mark as done. + The global ID of the to-do item to mark as done. """ id: TodoID! } @@ -25085,7 +25085,7 @@ type TodoMarkDonePayload { errors: [String!]! """ - The requested to-do. + The requested to-do item. """ todo: Todo! } @@ -25100,7 +25100,7 @@ input TodoRestoreInput { clientMutationId: String """ - The global ID of the to-do to restore. + The global ID of the to-do item to restore. """ id: TodoID! } @@ -25115,7 +25115,7 @@ input TodoRestoreManyInput { clientMutationId: String """ - The global IDs of the to-dos to restore (a maximum of 50 is supported at once). + The global IDs of the to-do items to restore (a maximum of 50 is supported at once). """ ids: [TodoID!]! } @@ -25135,14 +25135,14 @@ type TodoRestoreManyPayload { errors: [String!]! """ - Updated to-dos. + Updated to-do items. """ todos: [Todo!]! """ - The IDs of the updated to-do items. Deprecated in 13.2: Use todos. + The IDs of the updated to-do items. Deprecated in 13.2: Use to-do items. """ - updatedIds: [TodoID!]! @deprecated(reason: "Use todos. Deprecated in 13.2.") + updatedIds: [TodoID!]! @deprecated(reason: "Use to-do items. Deprecated in 13.2.") } """ @@ -25160,7 +25160,7 @@ type TodoRestorePayload { errors: [String!]! """ - The requested to-do. + The requested to-do item. """ todo: Todo! } @@ -25232,14 +25232,14 @@ type TodosMarkAllDonePayload { errors: [String!]! """ - Updated todos. + Updated to-do items. """ todos: [Todo!]! """ - Ids of the updated todos. Deprecated in 13.2: Use todos. + IDs of the updated to-do items. Deprecated in 13.2: Use to-do items. """ - updatedIds: [TodoID!]! @deprecated(reason: "Use todos. Deprecated in 13.2.") + updatedIds: [TodoID!]! @deprecated(reason: "Use to-do items. Deprecated in 13.2.") } """ @@ -25509,7 +25509,7 @@ type UpdateAlertStatusPayload { issue: Issue """ - The todo after mutation. + The to-do item after mutation. """ todo: Todo } @@ -26839,7 +26839,7 @@ type User { status: UserStatus """ - Todos of the user + To-do items of the user """ todos( """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index be73d02f0f1..215ae78d5ff 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -953,7 +953,7 @@ }, { "name": "todos", - "description": "To-dos of the current user for the alert.", + "description": "To-do items of the current user for the alert.", "args": [ { "name": "action", @@ -2504,7 +2504,7 @@ }, { "name": "todo", - "description": "The todo after mutation.", + "description": "The to-do item after mutation.", "args": [ ], @@ -2648,7 +2648,7 @@ }, { "name": "todo", - "description": "The todo after mutation.", + "description": "The to-do item after mutation.", "args": [ ], @@ -4482,7 +4482,7 @@ }, { "name": "currentUserTodos", - "description": "Todos for the current user.", + "description": "To-do items for the current user.", "args": [ { "name": "after", @@ -4526,7 +4526,7 @@ }, { "name": "state", - "description": "State of the todos.", + "description": "State of the to-do items.", "type": { "kind": "ENUM", "name": "TodoStateEnum", @@ -12313,7 +12313,7 @@ }, { "name": "todo", - "description": "The todo after mutation.", + "description": "The to-do item after mutation.", "args": [ ], @@ -14902,7 +14902,7 @@ "fields": [ { "name": "currentUserTodos", - "description": "Todos for the current user.", + "description": "To-do items for the current user.", "args": [ { "name": "after", @@ -14946,7 +14946,7 @@ }, { "name": "state", - "description": "State of the todos.", + "description": "State of the to-do items.", "type": { "kind": "ENUM", "name": "TodoStateEnum", @@ -17991,7 +17991,7 @@ "fields": [ { "name": "currentUserTodos", - "description": "Todos for the current user.", + "description": "To-do items for the current user.", "args": [ { "name": "after", @@ -18035,7 +18035,7 @@ }, { "name": "state", - "description": "State of the todos.", + "description": "State of the to-do items.", "type": { "kind": "ENUM", "name": "TodoStateEnum", @@ -23713,7 +23713,7 @@ }, { "name": "currentUserTodos", - "description": "Todos for the current user.", + "description": "To-do items for the current user.", "args": [ { "name": "after", @@ -23757,7 +23757,7 @@ }, { "name": "state", - "description": "State of the todos.", + "description": "State of the to-do items.", "type": { "kind": "ENUM", "name": "TodoStateEnum", @@ -25584,7 +25584,7 @@ }, { "name": "currentUserTodos", - "description": "Todos for the current user.", + "description": "To-do items for the current user.", "args": [ { "name": "after", @@ -25628,7 +25628,7 @@ }, { "name": "state", - "description": "State of the todos.", + "description": "State of the to-do items.", "type": { "kind": "ENUM", "name": "TodoStateEnum", @@ -34062,7 +34062,7 @@ }, { "name": "currentUserTodos", - "description": "Todos for the current user.", + "description": "To-do items for the current user.", "args": [ { "name": "after", @@ -34106,7 +34106,7 @@ }, { "name": "state", - "description": "State of the todos.", + "description": "State of the to-do items.", "type": { "kind": "ENUM", "name": "TodoStateEnum", @@ -39886,7 +39886,7 @@ }, { "name": "currentUserTodos", - "description": "Todos for the current user.", + "description": "To-do items for the current user.", "args": [ { "name": "after", @@ -39930,7 +39930,7 @@ }, { "name": "state", - "description": "State of the todos.", + "description": "State of the to-do items.", "type": { "kind": "ENUM", "name": "TodoStateEnum", @@ -72418,7 +72418,7 @@ "fields": [ { "name": "action", - "description": "Action of the to-do", + "description": "Action of the to-do item", "args": [ ], @@ -72436,7 +72436,7 @@ }, { "name": "author", - "description": "The author of this to-do", + "description": "The author of this to-do item", "args": [ ], @@ -72454,7 +72454,7 @@ }, { "name": "body", - "description": "Body of the to-do", + "description": "Body of the to-do item", "args": [ ], @@ -72472,7 +72472,7 @@ }, { "name": "createdAt", - "description": "Timestamp this to-do was created", + "description": "Timestamp this to-do item was created", "args": [ ], @@ -72490,7 +72490,7 @@ }, { "name": "group", - "description": "Group this to-do is associated with", + "description": "Group this to-do item is associated with", "args": [ ], @@ -72504,7 +72504,7 @@ }, { "name": "id", - "description": "ID of the to-do", + "description": "ID of the to-do item", "args": [ ], @@ -72522,7 +72522,7 @@ }, { "name": "project", - "description": "The project this to-do is associated with", + "description": "The project this to-do item is associated with", "args": [ ], @@ -72536,7 +72536,7 @@ }, { "name": "state", - "description": "State of the to-do", + "description": "State of the to-do item", "args": [ ], @@ -72554,7 +72554,7 @@ }, { "name": "targetType", - "description": "Target type of the to-do", + "description": "Target type of the to-do item", "args": [ ], @@ -72780,7 +72780,7 @@ }, { "name": "todo", - "description": "The to-do created.", + "description": "The to-do item created.", "args": [ ], @@ -72863,7 +72863,7 @@ "inputFields": [ { "name": "id", - "description": "The global ID of the to-do to mark as done.", + "description": "The global ID of the to-do item to mark as done.", "type": { "kind": "NON_NULL", "name": null, @@ -72937,7 +72937,7 @@ }, { "name": "todo", - "description": "The requested to-do.", + "description": "The requested to-do item.", "args": [ ], @@ -72969,7 +72969,7 @@ "inputFields": [ { "name": "id", - "description": "The global ID of the to-do to restore.", + "description": "The global ID of the to-do item to restore.", "type": { "kind": "NON_NULL", "name": null, @@ -73004,7 +73004,7 @@ "inputFields": [ { "name": "ids", - "description": "The global IDs of the to-dos to restore (a maximum of 50 is supported at once).", + "description": "The global IDs of the to-do items to restore (a maximum of 50 is supported at once).", "type": { "kind": "NON_NULL", "name": null, @@ -73086,7 +73086,7 @@ }, { "name": "todos", - "description": "Updated to-dos.", + "description": "Updated to-do items.", "args": [ ], @@ -73112,7 +73112,7 @@ }, { "name": "updatedIds", - "description": "The IDs of the updated to-do items. Deprecated in 13.2: Use todos.", + "description": "The IDs of the updated to-do items. Deprecated in 13.2: Use to-do items.", "args": [ ], @@ -73134,7 +73134,7 @@ } }, "isDeprecated": true, - "deprecationReason": "Use todos. Deprecated in 13.2." + "deprecationReason": "Use to-do items. Deprecated in 13.2." } ], "inputFields": null, @@ -73191,7 +73191,7 @@ }, { "name": "todo", - "description": "The requested to-do.", + "description": "The requested to-do item.", "args": [ ], @@ -73363,7 +73363,7 @@ }, { "name": "todos", - "description": "Updated todos.", + "description": "Updated to-do items.", "args": [ ], @@ -73389,7 +73389,7 @@ }, { "name": "updatedIds", - "description": "Ids of the updated todos. Deprecated in 13.2: Use todos.", + "description": "IDs of the updated to-do items. Deprecated in 13.2: Use to-do items.", "args": [ ], @@ -73411,7 +73411,7 @@ } }, "isDeprecated": true, - "deprecationReason": "Use todos. Deprecated in 13.2." + "deprecationReason": "Use to-do items. Deprecated in 13.2." } ], "inputFields": null, @@ -74189,7 +74189,7 @@ }, { "name": "todo", - "description": "The todo after mutation.", + "description": "The to-do item after mutation.", "args": [ ], @@ -77580,7 +77580,7 @@ }, { "name": "todos", - "description": "Todos of the user", + "description": "To-do items of the user", "args": [ { "name": "action", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 8d0b25a2093..bb0baa2eeee 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -102,7 +102,7 @@ Describes an alert from the project's Alert Management. | `startedAt` | Time | Timestamp the alert was raised. | | `status` | AlertManagementStatus | Status of the alert. | | `title` | String | Title of the alert. | -| `todos` | TodoConnection | To-dos of the current user for the alert. | +| `todos` | TodoConnection | To-do items of the current user for the alert. | | `updatedAt` | Time | Timestamp the alert was last updated. | ### AlertManagementAlertStatusCountsType @@ -166,7 +166,7 @@ Autogenerated return type of AlertSetAssignees. | `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. | | `issue` | Issue | The issue created after mutation. | -| `todo` | Todo | The todo after mutation. | +| `todo` | Todo | The to-do item after mutation. | ### AlertTodoCreatePayload @@ -178,7 +178,7 @@ Autogenerated return type of AlertTodoCreate. | `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. | | `issue` | Issue | The issue created after mutation. | -| `todo` | Todo | The todo after mutation. | +| `todo` | Todo | The to-do item after mutation. | ### AwardEmoji @@ -278,7 +278,7 @@ Represents an epic on an issue board. | `closedAt` | Time | Timestamp of when the epic was closed. | | `confidential` | Boolean | Indicates if the epic is confidential. | | `createdAt` | Time | Timestamp of when the epic was created. | -| `currentUserTodos` | TodoConnection! | Todos for the current user. | +| `currentUserTodos` | TodoConnection! | To-do items for the current user. | | `descendantCounts` | EpicDescendantCount | Number of open and closed descendant epics and issues. | | `descendantWeightSum` | EpicDescendantWeights | Total weight of open and closed issues in the epic and its descendants. | | `description` | String | Description of the epic. | @@ -705,7 +705,7 @@ Autogenerated return type of CreateAlertIssue. | `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. | | `issue` | Issue | The issue created after mutation. | -| `todo` | Todo | The todo after mutation. | +| `todo` | Todo | The to-do item after mutation. | ### CreateAnnotationPayload @@ -1078,7 +1078,7 @@ A single design. | Field | Type | Description | | ----- | ---- | ----------- | -| `currentUserTodos` | TodoConnection! | Todos for the current user. | +| `currentUserTodos` | TodoConnection! | To-do items for the current user. | | `diffRefs` | DiffRefs! | The diff refs for this design. | | `discussions` | DiscussionConnection! | All discussions on this noteable | | `event` | DesignVersionEvent! | How this design was changed in the current version. | @@ -1400,7 +1400,7 @@ Represents an epic. | `closedAt` | Time | Timestamp of when the epic was closed. | | `confidential` | Boolean | Indicates if the epic is confidential. | | `createdAt` | Time | Timestamp of when the epic was created. | -| `currentUserTodos` | TodoConnection! | Todos for the current user. | +| `currentUserTodos` | TodoConnection! | To-do items for the current user. | | `descendantCounts` | EpicDescendantCount | Number of open and closed descendant epics and issues. | | `descendantWeightSum` | EpicDescendantWeights | Total weight of open and closed issues in the epic and its descendants. | | `description` | String | Description of the epic. | @@ -1516,7 +1516,7 @@ Relationship between an epic and an issue. | `confidential` | Boolean! | Indicates the issue is confidential | | `createNoteEmail` | String | User specific email address for the issue | | `createdAt` | Time! | Timestamp of when the issue was created | -| `currentUserTodos` | TodoConnection! | Todos for the current user. | +| `currentUserTodos` | TodoConnection! | To-do items for the current user. | | `description` | String | Description of the issue | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | | `designCollection` | DesignCollection | Collection of design images associated with this issue | @@ -1891,7 +1891,7 @@ Represents a recorded measurement (object count) for the Admins. | `confidential` | Boolean! | Indicates the issue is confidential | | `createNoteEmail` | String | User specific email address for the issue | | `createdAt` | Time! | Timestamp of when the issue was created | -| `currentUserTodos` | TodoConnection! | Todos for the current user. | +| `currentUserTodos` | TodoConnection! | To-do items for the current user. | | `description` | String | Description of the issue | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | | `designCollection` | DesignCollection | Collection of design images associated with this issue | @@ -2202,7 +2202,7 @@ Autogenerated return type of MarkAsSpamSnippet. | `commitsWithoutMergeCommits` | CommitConnection | Merge request commits excluding merge commits | | `conflicts` | Boolean! | Indicates if the merge request has conflicts | | `createdAt` | Time! | Timestamp of when the merge request was created | -| `currentUserTodos` | TodoConnection! | Todos for the current user. | +| `currentUserTodos` | TodoConnection! | To-do items for the current user. | | `defaultMergeCommitMessage` | String | Default merge commit message of the merge request | | `defaultMergeCommitMessageWithDescription` | String | Default merge commit message of the merge request with description | | `defaultSquashCommitMessage` | String | Default squash commit message of the merge request | @@ -3703,15 +3703,15 @@ Representing a to-do entry. | Field | Type | Description | | ----- | ---- | ----------- | -| `action` | TodoActionEnum! | Action of the to-do | -| `author` | User! | The author of this to-do | -| `body` | String! | Body of the to-do | -| `createdAt` | Time! | Timestamp this to-do was created | -| `group` | Group | Group this to-do is associated with | -| `id` | ID! | ID of the to-do | -| `project` | Project | The project this to-do is associated with | -| `state` | TodoStateEnum! | State of the to-do | -| `targetType` | TodoTargetEnum! | Target type of the to-do | +| `action` | TodoActionEnum! | Action of the to-do item | +| `author` | User! | The author of this to-do item | +| `body` | String! | Body of the to-do item | +| `createdAt` | Time! | Timestamp this to-do item was created | +| `group` | Group | Group this to-do item is associated with | +| `id` | ID! | ID of the to-do item | +| `project` | Project | The project this to-do item is associated with | +| `state` | TodoStateEnum! | State of the to-do item | +| `targetType` | TodoTargetEnum! | Target type of the to-do item | ### TodoCreatePayload @@ -3721,7 +3721,7 @@ Autogenerated return type of TodoCreate. | ----- | ---- | ----------- | | `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. | -| `todo` | Todo | The to-do created. | +| `todo` | Todo | The to-do item created. | ### TodoMarkDonePayload @@ -3731,7 +3731,7 @@ Autogenerated return type of TodoMarkDone. | ----- | ---- | ----------- | | `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. | -| `todo` | Todo! | The requested to-do. | +| `todo` | Todo! | The requested to-do item. | ### TodoRestoreManyPayload @@ -3741,8 +3741,8 @@ Autogenerated return type of TodoRestoreMany. | ----- | ---- | ----------- | | `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. | -| `todos` | Todo! => Array | Updated to-dos. | -| `updatedIds` **{warning-solid}** | TodoID! => Array | **Deprecated:** Use todos. Deprecated in 13.2. | +| `todos` | Todo! => Array | Updated to-do items. | +| `updatedIds` **{warning-solid}** | TodoID! => Array | **Deprecated:** Use to-do items. Deprecated in 13.2. | ### TodoRestorePayload @@ -3752,7 +3752,7 @@ Autogenerated return type of TodoRestore. | ----- | ---- | ----------- | | `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. | -| `todo` | Todo! | The requested to-do. | +| `todo` | Todo! | The requested to-do item. | ### TodosMarkAllDonePayload @@ -3762,8 +3762,8 @@ Autogenerated return type of TodosMarkAllDone. | ----- | ---- | ----------- | | `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. | -| `todos` | Todo! => Array | Updated todos. | -| `updatedIds` **{warning-solid}** | TodoID! => Array | **Deprecated:** Use todos. Deprecated in 13.2. | +| `todos` | Todo! => Array | Updated to-do items. | +| `updatedIds` **{warning-solid}** | TodoID! => Array | **Deprecated:** Use to-do items. Deprecated in 13.2. | ### ToggleAwardEmojiPayload @@ -3810,7 +3810,7 @@ Autogenerated return type of UpdateAlertStatus. | `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. | | `issue` | Issue | The issue created after mutation. | -| `todo` | Todo | The todo after mutation. | +| `todo` | Todo | The to-do item after mutation. | ### UpdateBoardEpicUserPreferencesPayload @@ -3976,7 +3976,7 @@ Autogenerated return type of UpdateSnippet. | `starredProjects` | ProjectConnection | Projects starred by the user | | `state` | UserState! | State of the user | | `status` | UserStatus | User status | -| `todos` | TodoConnection! | Todos of the user | +| `todos` | TodoConnection! | To-do items of the user | | `userPermissions` | UserPermissions! | Permissions for the current user on the resource | | `username` | String! | Username of the user. Unique within this instance of GitLab | | `webPath` | String! | Web path of the user | diff --git a/doc/api/groups.md b/doc/api/groups.md index 25571af9874..0aba679655b 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -1010,7 +1010,7 @@ GET /groups?search=foobar ] ``` -## Hooks +## Hooks **(PREMIUM)** Also called Group Hooks and Webhooks. These are different from [System Hooks](system_hooks.md) that are system wide and [Project Hooks](projects.md#hooks) that are limited to one project. @@ -1057,6 +1057,7 @@ GET /groups/:id/hooks/:hook_id "wiki_page_events": true, "deployment_events": true, "releases_events": true, + "subgroup_events": true, "enable_ssl_verification": true, "created_at": "2012-10-12T17:04:47Z" } @@ -1086,6 +1087,7 @@ POST /groups/:id/hooks | `wiki_page_events` | boolean | no | Trigger hook on wiki events | | `deployment_events` | boolean | no | Trigger hook on deployment events | | `releases_events` | boolean | no | Trigger hook on release events | +| `subgroup_events` | boolean | no | Trigger hook on subgroup events | | `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook | | `token` | string | no | Secret token to validate received payloads; not returned in the response | @@ -1114,6 +1116,7 @@ PUT /groups/:id/hooks/:hook_id | `wiki_events` | boolean | no | Trigger hook on wiki events | | `deployment_events` | boolean | no | Trigger hook on deployment events | | `releases_events` | boolean | no | Trigger hook on release events | +| `subgroup_events` | boolean | no | Trigger hook on subgroup events | | `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook | | `token` | string | no | Secret token to validate received payloads; not returned in the response | diff --git a/doc/api/settings.md b/doc/api/settings.md index 6c035dea1ef..6c996caed7b 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -302,7 +302,7 @@ listed in the descriptions of the relevant settings. | `housekeeping_incremental_repack_period` | integer | required by: `housekeeping_enabled` | Number of Git pushes after which an incremental `git repack` is run. | | `html_emails_enabled` | boolean | no | Enable HTML emails. | | `import_sources` | array of strings | no | Sources to allow project import from, possible values: `github`, `bitbucket`, `bitbucket_server`, `gitlab`, `fogbugz`, `git`, `gitlab_project`, `gitea`, `manifest`, and `phabricator`. | -| `invisible_captcha_enabled` | boolean | no | Enable Invisible Captcha spam detection during sign-up. Disabled by default. | +| `invisible_captcha_enabled` | boolean | no | <!-- vale gitlab.Spelling = NO --> Enable Invisible Captcha <!-- vale gitlab.Spelling = YES --> spam detection during sign-up. Disabled by default. | | `issues_create_limit` | integer | no | Max number of issue creation requests per minute per user. Disabled by default.| | `local_markdown_version` | integer | no | Increase this value when any cached Markdown should be invalidated. | | `maintenance_mode_message` | string | no | **(PREMIUM)** Message displayed when instance is in maintenance mode | diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 09ca3bb18ea..750ecd91085 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -6,7 +6,9 @@ type: reference --- <!-- markdownlint-disable MD044 --> +<!-- vale gitlab.Spelling = NO --> # Keyword reference for the .gitlab-ci.yml file +<!-- vale gitlab.Spelling = YES --> <!-- markdownlint-enable MD044 --> This document lists the configuration options for your GitLab `.gitlab-ci.yml` file. @@ -1403,7 +1405,7 @@ job: - spec/**.rb ``` -Glob patterns are interpreted with Ruby [File.fnmatch](https://docs.ruby-lang.org/en/2.7.0/File.html#method-c-fnmatch) +Glob patterns are interpreted with Ruby [`File.fnmatch`](https://docs.ruby-lang.org/en/2.7.0/File.html#method-c-fnmatch) with the flags `File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB`. For performance reasons, GitLab matches a maximum of 10,000 `exists` patterns. After the 10,000th check, rules with patterned globs always match. diff --git a/doc/development/cicd/index.md b/doc/development/cicd/index.md index 196b6461f72..d79d681263d 100644 --- a/doc/development/cicd/index.md +++ b/doc/development/cicd/index.md @@ -146,7 +146,7 @@ As we increase the number of runners in the pool we also increase the chances of ## The definition of "Job" in GitLab CI/CD -"Job" in GitLab CI context refers a task to drive Continuous Integartion, Delivery and Deployment. +"Job" in GitLab CI context refers a task to drive Continuous Integration, Delivery and Deployment. Typically, a pipeline contains multiple stages, and a stage contains multiple jobs. In Active Record modeling, Job is defined as `CommitStatus` class. @@ -164,4 +164,4 @@ we should use "Job" in general, instead of "Build". We have a few inconsistencies in our codebase that should be refactored. For example, `CommitStatus` should be `Ci::Job` and `Ci::JobArtifact` should be `Ci::BuildArtifact`. -Please read [this isse](https://gitlab.com/gitlab-org/gitlab/-/issues/16111) for the full refactoring plan. +See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/16111) for the full refactoring plan. diff --git a/doc/development/database/strings_and_the_text_data_type.md b/doc/development/database/strings_and_the_text_data_type.md index 703118b23d2..f338520c6ca 100644 --- a/doc/development/database/strings_and_the_text_data_type.md +++ b/doc/development/database/strings_and_the_text_data_type.md @@ -36,7 +36,7 @@ validations and index creation while it allows reads and writes). ### Exceptions -Text columns used by `attr_encrypted` are not required to have a limit, becuase the length of the +Text columns used by `attr_encrypted` are not required to have a limit, because the length of the text after encryption may be longer than the text itself. Instead, you can use an Active Record length validation on the attribute. diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index c1d77abf08e..81014b7624c 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -166,9 +166,9 @@ still having access the class's implementation with `super`. There are a few gotchas with it: - you should always [`extend ::Gitlab::Utils::Override`](utilities.md#override) and use `override` to - guard the "overrider" method to ensure that if the method gets renamed in + guard the `overrider` method to ensure that if the method gets renamed in CE, the EE override isn't silently forgotten. -- when the "overrider" would add a line in the middle of the CE +- when the `overrider` would add a line in the middle of the CE implementation, you should refactor the CE method and split it in smaller methods. Or create a "hook" method that is empty in CE, and with the EE-specific implementation in EE. @@ -971,7 +971,7 @@ information on managing page-specific JavaScript within EE. #### Child Component only used in EE -To separate Vue template differences we should [async import the components](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components). +To separate Vue template differences we should [import the components asynchronously](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components). Doing this allows for us to load the correct component in EE while in CE we can load a empty component that renders nothing. This code **should** @@ -1068,7 +1068,7 @@ export default { **For EE components that need different results for the same computed values, we can pass in props to the CE wrapper as seen in the example.** - **EE Child components** - - Since we are using the async loading to check which component to load, we'd still use the component's name, check [this example](#child-component-only-used-in-ee). + - Since we are using the asynchronous loading to check which component to load, we'd still use the component's name, check [this example](#child-component-only-used-in-ee). - **EE extra HTML** - For the templates that have extra HTML in EE we should move it into a new component and use the `ee_else_ce` dynamic import diff --git a/doc/development/elasticsearch.md b/doc/development/elasticsearch.md index e5d0bd7ee10..10fa331e1dd 100644 --- a/doc/development/elasticsearch.md +++ b/doc/development/elasticsearch.md @@ -210,7 +210,7 @@ class MigrationName < Elastic::Migration end ``` -Applied migrations are stored in `gitlab-#{RAILS_ENV}-migrations` index. All unexecuted migrations +Applied migrations are stored in `gitlab-#{RAILS_ENV}-migrations` index. All migrations not executed are applied by the [`Elastic::MigrationWorker`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/app/workers/elastic/migration_worker.rb) cron worker sequentially. diff --git a/doc/development/experiment_guide/index.md b/doc/development/experiment_guide/index.md index 2b0fd3ccc54..5d77c0ca0e9 100644 --- a/doc/development/experiment_guide/index.md +++ b/doc/development/experiment_guide/index.md @@ -44,7 +44,7 @@ addressed. > - It is not yet intended for use in GitLab self-managed instances. [GitLab Experiment](https://gitlab.com/gitlab-org/gitlab-experiment/) is a gem included -in GitLab that can be used for running experiments. +in GitLab that can be used for running experiments. ## How to create an A/B test using `experimentation.rb` @@ -368,7 +368,7 @@ Use a comma to list more than one experiment to be forced: document.cookie = "force_experiment=<EXPERIMENT_KEY>,<ANOTHER_EXPERIMENT_KEY>; path=/"; ``` -Clear the experiments by unsetting the `force_experiment` cookie: +To clear the experiments, unset the `force_experiment` cookie: ```javascript document.cookie = "force_experiment=; path=/"; diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md index 40ed5b383b6..a53d9fee029 100644 --- a/doc/development/fe_guide/graphql.md +++ b/doc/development/fe_guide/graphql.md @@ -18,6 +18,8 @@ info: "See the Technical Writers assigned to Development Guidelines: https://abo **GraphQL at GitLab**: +<!-- vale gitlab.Spelling = NO --> + - <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [GitLab Unfiltered GraphQL playlist](https://www.youtube.com/watch?v=wHPKZBDMfxE&list=PL05JrBw4t0KpcjeHjaRMB7IGB2oDWyJzv) - <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [GraphQL at GitLab: Deep Dive](../api_graphql_styleguide.md#deep-dive) (video) by Nick Thomas - An overview of the history of GraphQL at GitLab (not frontend-specific) @@ -29,6 +31,8 @@ info: "See the Technical Writers assigned to Development Guidelines: https://abo - [🛠Vuex -> Apollo Migration: a proof-of-concept project](https://gitlab.com/ntepluhina/vuex-to-apollo/blob/master/README.md) - A collection of examples that show the possible approaches for state management with Vue+GraphQL+(Vuex or Apollo) apps +<!-- vale gitlab.Spelling = YES --> + ### Libraries We use [Apollo](https://www.apollographql.com/) (specifically [Apollo Client](https://www.apollographql.com/docs/react/)) and [Vue Apollo](https://github.com/vuejs/vue-apollo) @@ -39,13 +43,21 @@ can help you learn how to integrate Vue Apollo. For other use cases, check out the [Usage outside of Vue](#usage-outside-of-vue) section. +<!-- vale gitlab.Spelling = NO --> + We use [Immer](https://immerjs.github.io/immer/docs/introduction) for immutable cache updates; see [Immutability and cache updates](#immutability-and-cache-updates) for more information. +<!-- vale gitlab.Spelling = YES --> + ### Tooling +<!-- vale gitlab.Spelling = NO --> + - [Apollo Client Devtools](https://github.com/apollographql/apollo-client-devtools) +<!-- vale gitlab.Spelling = YES --> + #### [Apollo GraphQL VS Code extension](https://marketplace.visualstudio.com/items?itemName=apollographql.vscode-apollo) If you use VS Code, the Apollo GraphQL extension supports autocompletion in `.graphql` files. To set up @@ -160,9 +172,14 @@ const primaryKeyId = getIdFromGraphQLId(data.id); From Apollo version 3.0.0 all the cache updates need to be immutable. It needs to be replaced entirely with a **new and updated** object. -To facilitate the process of updating the cache and returning the new object we use the library [Immer](https://immerjs.github.io/immer/docs/introduction). +<!-- vale gitlab.Spelling = NO --> + +To facilitate the process of updating the cache and returning the new object we +use the library [Immer](https://immerjs.github.io/immer/docs/introduction). When possible, follow these conventions: +<!-- vale gitlab.Spelling = YES --> + - The updated cache is named `data`. - The original cache data is named `sourceData`. @@ -753,9 +770,13 @@ export default { #### Mocking response as component data -With [Vue test utils](https://vue-test-utils.vuejs.org/) one can quickly test components that +<!-- vale gitlab.Spelling = NO --> + +With [Vue Test Utils](https://vue-test-utils.vuejs.org/) one can quickly test components that fetch GraphQL queries. The simplest way is to use `shallowMount` and then set -the data on the component +the data on the component: + +<!-- vale gitlab.Spelling = YES --> ```javascript it('tests apollo component', () => { diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md index 852de1f98b8..821334e3008 100644 --- a/doc/development/fe_guide/icons.md +++ b/doc/development/fe_guide/icons.md @@ -26,12 +26,16 @@ To use a sprite Icon in HAML or Rails we use a specific helper function: sprite_icon(icon_name, size: nil, css_class: '') ``` +<!-- vale gitlab.Spelling = NO --> + - **icon_name**: Use the icon_name for the SVG sprite in the list of ([GitLab SVGs](https://gitlab-org.gitlab.io/gitlab-svgs)). - **size (optional)**: Use one of the following sizes : 16, 24, 32, 48, 72 (this is translated into a `s16` class) - **css_class (optional)**: If you want to add additional CSS classes. +<!-- vale gitlab.Spelling = YES --> + **Example** ```haml diff --git a/doc/development/fe_guide/performance.md b/doc/development/fe_guide/performance.md index 7d249c65ebd..2a1c3daa6b8 100644 --- a/doc/development/fe_guide/performance.md +++ b/doc/development/fe_guide/performance.md @@ -202,15 +202,15 @@ help identify marks and measures coming from the different apps on the same page ## Best Practices -### Realtime Components +### Real-time Components -When writing code for realtime features we have to keep a couple of things in mind: +When writing code for real-time features we have to keep a couple of things in mind: 1. Do not overload the server with requests. -1. It should feel realtime. +1. It should feel real-time. -Thus, we must strike a balance between sending requests and the feeling of realtime. -Use the following rules when creating realtime solutions. +Thus, we must strike a balance between sending requests and the feeling of real-time. +Use the following rules when creating real-time solutions. 1. The server tells you how much to poll by sending `Poll-Interval` in the header. Use that as your polling interval. This enables system administrators to change the @@ -221,7 +221,7 @@ Use the following rules when creating realtime solutions. 1. Poll on active tabs only. Please use [Visibility](https://github.com/ai/visibilityjs). 1. Use regular polling intervals, do not use backoff polling, or jitter, as the interval is controlled by the server. -1. The backend code is likely to be using etags. You do not and should not check for status +1. The backend code is likely to be using ETags. You do not and should not check for status `304 Not Modified`. The browser transforms it for you. ### Lazy Loading Images @@ -333,7 +333,7 @@ browser's developer console from any page in GitLab. action(); ``` - For example, see how we use this in [app/assets/javascripts/pages/projects/graphs/charts/index.js](https://gitlab.com/gitlab-org/gitlab/-/commit/5e90885d6afd4497002df55bf015b338efcfc3c5#02e81de37f5b1716a3ef3222fa7f7edf22c40969_9_8): + For example, see how we use this in [`app/assets/javascripts/pages/projects/graphs/charts/index.js`](https://gitlab.com/gitlab-org/gitlab/-/commit/5e90885d6afd4497002df55bf015b338efcfc3c5#02e81de37f5b1716a3ef3222fa7f7edf22c40969_9_8): ```javascript waitForCSSLoaded(() => { diff --git a/doc/development/fe_guide/troubleshooting.md b/doc/development/fe_guide/troubleshooting.md index 641bbdcbd8d..abaf9cd68c7 100644 --- a/doc/development/fe_guide/troubleshooting.md +++ b/doc/development/fe_guide/troubleshooting.md @@ -12,7 +12,7 @@ Running into a problem? Maybe this will help ¯\_(ツ)_/¯. ### This guide doesn't contain the issue I ran into -If you run into a Frontend development issue that is not in this guide, please consider updating this guide with your issue and possible remedies. This way future adventurers can face these dragons with more success, being armed with your experience and knowedge. +If you run into a Frontend development issue that is not in this guide, please consider updating this guide with your issue and possible remedies. This way future adventurers can face these dragons with more success, being armed with your experience and knowledge. ## Testing issues diff --git a/doc/development/polling.md b/doc/development/polling.md index 18f9fb954dd..f854891a528 100644 --- a/doc/development/polling.md +++ b/doc/development/polling.md @@ -60,6 +60,6 @@ route matching easier. For more information see: -- [`Poll-Interval` header](fe_guide/performance.md#realtime-components) +- [`Poll-Interval` header](fe_guide/performance.md#real-time-components) - [RFC 7232](https://tools.ietf.org/html/rfc7232) - [ETag proposal](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/26926) diff --git a/doc/development/reactive_caching.md b/doc/development/reactive_caching.md index 1b8a3f91086..0223f5d91d6 100644 --- a/doc/development/reactive_caching.md +++ b/doc/development/reactive_caching.md @@ -103,7 +103,7 @@ not wait until the background worker completes. - An API that calls a model or service method that uses `ReactiveCaching` should return `202 accepted` when the cache is being calculated (when `#with_reactive_cache` returns `nil`). - It should also - [set the polling interval header](fe_guide/performance.md#realtime-components) with + [set the polling interval header](fe_guide/performance.md#real-time-components) with `Gitlab::PollingInterval.set_header`. - The consumer of the API is expected to poll the API. - You can also consider implementing [ETag caching](polling.md) to reduce the server diff --git a/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md b/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md index 50dabfaaec4..8a929737ebe 100644 --- a/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md +++ b/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md @@ -41,5 +41,5 @@ This is a partial list of the [RSpec metadata](https://relishapp.com/rspec/rspec | `:skip_signup_disabled` | The test uses UI to sign up a new user and is skipped in any environment that does not allow new user registration via the UI. | | `:smoke` | The test belongs to the test suite which verifies basic functionality of a GitLab instance.| | `:smtp` | The test requires a GitLab instance to be configured to use an SMTP server. Tests SMTP notification email delivery from GitLab by using MailHog. | -| `:testcase` | The link to the test case issue in the [Quality Testcases project](https://gitlab.com/gitlab-org/quality/testcases/). | +| `:testcase` | The link to the test case issue in the [Quality Test Cases project](https://gitlab.com/gitlab-org/quality/testcases/). | | `:transient` | The test tests transient bugs. It is excluded by default. | diff --git a/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md b/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md index 4dd19a524a9..3c319e221fb 100644 --- a/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md +++ b/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md @@ -78,7 +78,7 @@ CONTAINER ID ... PORTS NAMES d15d3386a0a8 ... 22/tcp, 443/tcp, 0.0.0.0:32772->80/tcp gitlab-gitaly-ha ``` -That shows that the GitLab instance running in the `gitlab-gitaly-ha` container can be reached via `http://localhost:32772`. However, Git operations like cloning and pushing are performed against the URL revealed via the UI as the clone URL. It uses the hostname configured for the GitLab instance, which in this case matches the Docker container name and network, `gitlab-gitaly-ha.test`. Before you can run the tests you need to configure your computer to access the container via that address. One option is to [use caddyserver as described for running tests against GDK](https://gitlab.com/gitlab-org/gitlab-qa/-/blob/master/docs/run_qa_against_gdk.md#workarounds). +That shows that the GitLab instance running in the `gitlab-gitaly-ha` container can be reached via `http://localhost:32772`. However, Git operations like cloning and pushing are performed against the URL revealed via the UI as the clone URL. It uses the hostname configured for the GitLab instance, which in this case matches the Docker container name and network, `gitlab-gitaly-ha.test`. Before you can run the tests you need to configure your computer to access the container via that address. One option is to [use Caddy server as described for running tests against GDK](https://gitlab.com/gitlab-org/gitlab-qa/-/blob/master/docs/run_qa_against_gdk.md#workarounds). Another option is to use NGINX. diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index 81b4689c521..7e7f62e41dd 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -258,7 +258,7 @@ it('exists', () => { ### Naming unit tests When writing describe test blocks to test specific functions/methods, -please use the method name as the describe block name. +use the method name as the describe block name. **Bad**: @@ -439,7 +439,7 @@ it('waits for an Ajax call', done => { }); ``` -If you are not able to register handlers to the `Promise`, for example because it is executed in a synchronous Vue life cycle hook, please take a look at the [waitFor](#wait-until-axios-requests-finish) helpers or you can flush all pending `Promise`s: +If you are not able to register handlers to the `Promise`, for example because it is executed in a synchronous Vue life cycle hook, take a look at the [waitFor](#wait-until-axios-requests-finish) helpers or you can flush all pending `Promise`s: **in Jest:** @@ -702,10 +702,10 @@ unit testing by mocking out modules which cannot be easily consumed in our test Jest supports [manual module mocks](https://jestjs.io/docs/en/manual-mocks) by placing a mock in a `__mocks__/` directory next to the source module (e.g. `app/assets/javascripts/ide/__mocks__`). **Don't do this.** We want to keep all of our test-related code in one place (the `spec/` folder). -If a manual mock is needed for a `node_modules` package, please use the `spec/frontend/__mocks__` folder. Here's an example of +If a manual mock is needed for a `node_modules` package, use the `spec/frontend/__mocks__` folder. Here's an example of a [Jest mock for the package `monaco-editor`](https://gitlab.com/gitlab-org/gitlab/blob/b7f914cddec9fc5971238cdf12766e79fa1629d7/spec/frontend/__mocks__/monaco-editor/index.js#L1). -If a manual mock is needed for a CE module, please place it in `spec/frontend/mocks/ce`. +If a manual mock is needed for a CE module, place it in `spec/frontend/mocks/ce`. - Files in `spec/frontend/mocks/ce` mocks the corresponding CE module from `app/assets/javascripts`, mirroring the source module's path. - Example: `spec/frontend/mocks/ce/lib/utils/axios_utils` mocks the module `~/lib/utils/axios_utils`. @@ -728,11 +728,11 @@ If a manual mock is needed for a CE module, please place it in `spec/frontend/mo Global mocks introduce magic and technically can reduce test coverage. When mocking is deemed profitable: - Keep the mock short and focused. -- Please leave a top-level comment in the mock on why it is necessary. +- Leave a top-level comment in the mock on why it is necessary. ### Additional mocking techniques -Please consult the [official Jest docs](https://jestjs.io/docs/en/jest-object#mock-modules) for a full overview of the available mocking features. +Consult the [official Jest docs](https://jestjs.io/docs/en/jest-object#mock-modules) for a full overview of the available mocking features. ## Running Frontend Tests @@ -865,7 +865,7 @@ end This will create a new fixture located at `tmp/tests/frontend/fixtures-ee/graphql/releases/queries/all_releases.query.graphql.json`. -Note that you will need to provide the paths to all fragments used by the query. +You will need to provide the paths to all fragments used by the query. `get_graphql_query_as_string` reads all of the provided file paths and returns the result as a single, concatenated string. @@ -929,7 +929,8 @@ it.each([ ); ``` -**Note**: only use template literal block if pretty print is **not** needed for spec output. For example, empty strings, nested objects etc. +NOTE: +Only use template literal block if pretty print is not needed for spec output. For example, empty strings, nested objects etc. For example, when testing the difference between an empty search string and a non-empty search string, the use of the array block syntax with the pretty print option would be preferred. That way the differences between an empty string e.g. `''` and a non-empty string e.g. `'search string'` would be visible in the spec output. Whereas with a template literal block, the empty string would be shown as a space, which could lead to a confusing developer experience @@ -1038,7 +1039,8 @@ import Subject from '~/feature/the_subject.vue'; import _Thing from '~/feature/path/to/thing.vue'; ``` -**PLEASE NOTE:** Do not simply disregard test timeouts. This could be a sign that there's +NOTE: +Do not disregard test timeouts. This could be a sign that there's actually a production problem. Use this opportunity to analyze the production webpack bundles and chunks and confirm that there is not a production issue with the async imports. @@ -1063,7 +1065,7 @@ See also [Notes on testing Vue components](../fe_guide/vue.md#testing-vue-compon ## Test helpers Test helpers can be found in [`spec/frontend/__helpers__`](https://gitlab.com/gitlab-org/gitlab/blob/master/spec/frontend/__helpers__). -If you introduce new helpers, please place them in that directory. +If you introduce new helpers, place them in that directory. ### Vuex Helper: `testAction` diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md index 68326879dd0..c22a4e0b3ad 100644 --- a/doc/development/testing_guide/index.md +++ b/doc/development/testing_guide/index.md @@ -9,9 +9,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w This document describes various guidelines and best practices for automated testing of the GitLab project. -It is meant to be an _extension_ of the [thoughtbot testing +It is meant to be an _extension_ of the [Thoughtbot testing style guide](https://github.com/thoughtbot/guides/tree/master/testing-rspec). If -this guide defines a rule that contradicts the thoughtbot guide, this guide +this guide defines a rule that contradicts the Thoughtbot guide, this guide takes precedence. Some guidelines may be repeated verbatim to stress their importance. diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md index 6e37b947767..f1c74f990cb 100644 --- a/doc/development/testing_guide/review_apps.md +++ b/doc/development/testing_guide/review_apps.md @@ -357,7 +357,7 @@ using `v232`. For the record, the debugging steps to find out this issue were: -1. Switch kubectl context to review-apps-ce (we recommend using [kubectx](https://github.com/ahmetb/kubectx/)) +1. Switch kubectl context to `review-apps-ce` (we recommend using [`kubectx`](https://github.com/ahmetb/kubectx/)) 1. `kubectl get pods | grep dns` 1. `kubectl describe pod <pod name>` & confirm exact error message 1. Web search for exact error message, following rabbit hole to [a relevant Kubernetes bug report](https://github.com/kubernetes/kubernetes/issues/57345) diff --git a/doc/development/testing_guide/testing_migrations_guide.md b/doc/development/testing_guide/testing_migrations_guide.md index 31054d0ffb2..d54ca0d3c64 100644 --- a/doc/development/testing_guide/testing_migrations_guide.md +++ b/doc/development/testing_guide/testing_migrations_guide.md @@ -32,7 +32,7 @@ migrate the database **down** to the previous migration version. With this approach you can test a migration against a database schema. -An `after` hook migrates the database **up** and reinstitutes the latest +An `after` hook migrates the database **up** and restores the latest schema version, so that the process does not affect subsequent specs and ensures proper isolation. diff --git a/doc/development/windows.md b/doc/development/windows.md index 08ff29a4e58..07f8a80e95f 100644 --- a/doc/development/windows.md +++ b/doc/development/windows.md @@ -63,7 +63,7 @@ Build a Google Cloud image with the above shared runners repository by doing the ## How to use a Windows image in GCP -1. In a web browser, go to <https://console.cloud.google.com/compute/images>. +1. In a web browser, go to the [Google Cloud Platform console](https://console.cloud.google.com/compute/images). 1. Filter images by the name you used when creating image, `windows` is likely all you need to filter by. 1. Click the image's name. 1. Click the **CREATE INSTANCE** link. @@ -81,7 +81,7 @@ Build a Google Cloud image with the above shared runners repository by doing the 1. Click **Continue** to accept the certificate. 1. Enter the password and click **Next**. -You should now be remoted into a Windows machine with a command prompt. +You should now be connected into a Windows machine with a command prompt. ### Optional: Use GCP VM Instance as a runner diff --git a/doc/integration/security_partners/index.md b/doc/integration/security_partners/index.md index 87307ec15ea..1053c612782 100644 --- a/doc/integration/security_partners/index.md +++ b/doc/integration/security_partners/index.md @@ -10,4 +10,8 @@ type: index You can integrate GitLab with its security partners. This page has information on how do this with each security partner: +<!-- vale gitlab.Spelling = NO --> + - [Anchore](https://docs.anchore.com/current/docs/using/integration/ci_cd/gitlab/) + +<!-- vale gitlab.Spelling = YES --> diff --git a/doc/operations/metrics/dashboards/yaml.md b/doc/operations/metrics/dashboards/yaml.md index 536757f5fe0..138d9b28c76 100644 --- a/doc/operations/metrics/dashboards/yaml.md +++ b/doc/operations/metrics/dashboards/yaml.md @@ -133,7 +133,7 @@ metrics: unit: 'count' ``` -This works by lowercasing the value of `label` and, if there are more words separated by spaces, replacing those spaces with an underscore (`_`). The transformed value is then checked against the labels of the time series returned by the Prometheus query. If a time series label is found that is equal to the transformed value, then the label value renders in the legend like this: +This works by converting the value of `label` to lower-case and, if there are more words separated by spaces, replacing those spaces with an underscore (`_`). The transformed value is then checked against the labels of the time series returned by the Prometheus query. If a time series label is found that is equal to the transformed value, then the label value renders in the legend like this: ![legend with label shorthand variable](img/prometheus_dashboard_label_variable_shorthand.png) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 0b8a1b961ea..24b1c7a6978 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -240,7 +240,7 @@ The resulting file is named `dump_gitlab_backup.tar`. This is useful for systems that make use of rsync and incremental backups, and results in considerably faster transfer speeds. -#### Rsyncable +#### Confirm archive can be transferred To ensure the generated archive is transferable by rsync, you can set the `GZIP_RSYNCABLE=yes` option. This sets the `--rsyncable` option to `gzip`, which is useful only in @@ -1204,9 +1204,9 @@ and the jobs begin running again. Use the information in the following sections at your own risk. -#### Check for undecryptable values +#### Verify that all values can be decrypted -You can determine if you have undecryptable values in the database by using the +You can determine if your database contains values that can't be decrypted by using the [Secrets Doctor Rake task](../administration/raketasks/doctor.md). #### Take a backup diff --git a/doc/security/password_storage.md b/doc/security/password_storage.md index ca39defe6b9..af4b57e342a 100644 --- a/doc/security/password_storage.md +++ b/doc/security/password_storage.md @@ -11,6 +11,6 @@ GitLab stores user passwords in a hashed format, to prevent passwords from being GitLab uses the [Devise](https://github.com/heartcombo/devise) authentication library, which handles the hashing of user passwords. Password hashes are created with the following attributes: -- **Hashing**: the [bcrypt](https://en.wikipedia.org/wiki/Bcrypt) hashing function is used to generate the hash of the provided password. This is a strong, industry-standard cryptographic hashing function. +- **Hashing**: the [`bcrypt`](https://en.wikipedia.org/wiki/Bcrypt) hashing function is used to generate the hash of the provided password. This is a strong, industry-standard cryptographic hashing function. - **Stretching**: Password hashes are [stretched](https://en.wikipedia.org/wiki/Key_stretching) to harden against brute-force attacks. GitLab uses a stretching factor of 10 by default. - **Salting**: A [cryptographic salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) is added to each password to harden against pre-computed hash and dictionary attacks. Each salt is randomly generated for each password, so that no two passwords share a salt, to further increase security. diff --git a/doc/ssh/README.md b/doc/ssh/README.md index f34e38fb7ca..f3fa7dd6033 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -201,7 +201,7 @@ Now you can copy the SSH key you created to your GitLab account. To do so, follo pbcopy < ~/.ssh/id_ed25519.pub ``` - **Linux (requires the xclip package):** + **Linux (requires the `xclip` package):** ```shell xclip -sel clip < ~/.ssh/id_ed25519.pub diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md index fd477a8a475..1988e3e2890 100644 --- a/doc/topics/authentication/index.md +++ b/doc/topics/authentication/index.md @@ -48,6 +48,10 @@ This page gathers all the resources for the topic **Authentication** within GitL ## Third-party resources +<!-- vale gitlab.Spelling = NO --> + - [Kanboard Plugin GitLab Authentication](https://github.com/kanboard/plugin-gitlab-auth) - [Jenkins GitLab OAuth Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+OAuth+Plugin) - [OKD - Configuring Authentication and User Agent](https://docs.okd.io/3.11/install_config/configuring_authentication.html#GitLab) + +<!-- vale gitlab.Spelling = YES -->
\ No newline at end of file diff --git a/doc/user/project/integrations/img/mattermost_configuration.png b/doc/user/project/integrations/img/mattermost_configuration.png Binary files differdeleted file mode 100644 index 18c0036846d..00000000000 --- a/doc/user/project/integrations/img/mattermost_configuration.png +++ /dev/null diff --git a/doc/user/project/integrations/img/mattermost_configuration_v2.png b/doc/user/project/integrations/img/mattermost_configuration_v2.png Binary files differnew file mode 100644 index 00000000000..e05b34fd77a --- /dev/null +++ b/doc/user/project/integrations/img/mattermost_configuration_v2.png diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md index 74dcce70cae..db190f47b01 100644 --- a/doc/user/project/integrations/mattermost.md +++ b/doc/user/project/integrations/mattermost.md @@ -59,5 +59,6 @@ At the end, fill in your Mattermost details: | **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. | | **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. | | **Branches to be notified** | Select which types of branches to send notifications for. | +| **Labels to be notified** | Optional labels that the issue or merge request must have in order to trigger a notification. Leave blank to get all notifications. | -![Mattermost configuration](img/mattermost_configuration.png) +![Mattermost configuration](img/mattermost_configuration_v2.png) diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md index 9e9f5b8297f..ab798675278 100644 --- a/doc/user/project/integrations/slack.md +++ b/doc/user/project/integrations/slack.md @@ -45,6 +45,7 @@ separately configured [Slack slash commands](slack_slash_commands.md). 1. Select the **Notify only broken pipelines** check box to only notify on failures. 1. In the **Branches to be notified** select box, choose which types of branches to send notifications for. +1. Leave the **Labels to be notified** field blank to get all notifications or add labels that the issue or merge request must have in order to trigger a notification. 1. Click **Test settings and save changes**. Your Slack team now starts receiving GitLab event notifications as configured. diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index bcc43c162af..27c2cb08d10 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -1221,7 +1221,7 @@ X-Gitlab-Event: Pipeline Hook "id": 380987, "description": "shared-runners-manager-6.gitlab.com", "active": true, - "is_shared": true, + "is_shared": true, "tags": [ "linux", "docker" @@ -1485,6 +1485,74 @@ X-Gitlab-Event: Member Hook } ``` +### Subgroup events **(PREMIUM)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/260419) in GitLab 13.9. + +Subgroup events are triggered when: + +- A [subgroup is created in a group](#subgroup-created-in-a-group) +- A [subgroup is removed from a group](#subgroup-removed-from-a-group) + +#### Subgroup created in a group + +**Request Header**: + +```plaintext +X-Gitlab-Event: Subgroup Hook +``` + +**Request Body**: + +```json +{ + + "created_at": "2021-01-20T09:40:12Z", + "updated_at": "2021-01-20T09:40:12Z", + "event_name": "subgroup_create", + "name": "subgroup1", + "path": "subgroup1", + "full_path": "group1/subgroup1", + "group_id": 10, + "parent_group_id": 7, + "parent_name": "group1", + "parent_path": "group1", + "parent_full_path": "group1" + +} +``` + +#### Subgroup removed from a group + +**Request Header**: + +```plaintext +X-Gitlab-Event: Subgroup Hook +``` + +**Request Body**: + +```json +{ + + "created_at": "2021-01-20T09:40:12Z", + "updated_at": "2021-01-20T09:40:12Z", + "event_name": "subgroup_destroy", + "name": "subgroup1", + "path": "subgroup1", + "full_path": "group1/subgroup1", + "group_id": 10, + "parent_group_id": 7, + "parent_name": "group1", + "parent_path": "group1", + "parent_full_path": "group1" + +} +``` + +NOTE: +Webhooks for when a [subgroup is removed from a group](#subgroup-removed-from-a-group) are not triggered when a [subgroup is transferred to a new parent group](../../group/index.md#transferring-groups) + ### Feature Flag events Triggered when a feature flag is turned on or off. diff --git a/doc/user/project/members/img/access_requests_management_13_8.png b/doc/user/project/members/img/access_requests_management_13_8.png Binary files differdeleted file mode 100644 index 950ef4dec01..00000000000 --- a/doc/user/project/members/img/access_requests_management_13_8.png +++ /dev/null diff --git a/doc/user/project/members/img/access_requests_management_v13_9.png b/doc/user/project/members/img/access_requests_management_v13_9.png Binary files differnew file mode 100644 index 00000000000..b7883e9d134 --- /dev/null +++ b/doc/user/project/members/img/access_requests_management_v13_9.png diff --git a/doc/user/project/members/img/add_user_email_accept_13_8.png b/doc/user/project/members/img/add_user_email_accept_13_8.png Binary files differdeleted file mode 100644 index ed980036af5..00000000000 --- a/doc/user/project/members/img/add_user_email_accept_13_8.png +++ /dev/null diff --git a/doc/user/project/members/img/add_user_email_accept_v13_9.png b/doc/user/project/members/img/add_user_email_accept_v13_9.png Binary files differnew file mode 100644 index 00000000000..a6b303e05ca --- /dev/null +++ b/doc/user/project/members/img/add_user_email_accept_v13_9.png diff --git a/doc/user/project/members/img/add_user_email_ready_13_8.png b/doc/user/project/members/img/add_user_email_ready_v13_8.png Binary files differindex a610b46a176..a610b46a176 100644 --- a/doc/user/project/members/img/add_user_email_ready_13_8.png +++ b/doc/user/project/members/img/add_user_email_ready_v13_8.png diff --git a/doc/user/project/members/img/add_user_email_search_13_8.png b/doc/user/project/members/img/add_user_email_search_v13_8.png Binary files differindex 934cf19bd3d..934cf19bd3d 100644 --- a/doc/user/project/members/img/add_user_email_search_13_8.png +++ b/doc/user/project/members/img/add_user_email_search_v13_8.png diff --git a/doc/user/project/members/img/add_user_give_permissions_13_8.png b/doc/user/project/members/img/add_user_give_permissions_v13_8.png Binary files differindex 1916d056a52..1916d056a52 100644 --- a/doc/user/project/members/img/add_user_give_permissions_13_8.png +++ b/doc/user/project/members/img/add_user_give_permissions_v13_8.png diff --git a/doc/user/project/members/img/add_user_import_members_from_another_project_13_8.png b/doc/user/project/members/img/add_user_import_members_from_another_project_v13_8.png Binary files differindex a6dddec3fb7..a6dddec3fb7 100644 --- a/doc/user/project/members/img/add_user_import_members_from_another_project_13_8.png +++ b/doc/user/project/members/img/add_user_import_members_from_another_project_v13_8.png diff --git a/doc/user/project/members/img/add_user_imported_members_13_8.png b/doc/user/project/members/img/add_user_imported_members_13_8.png Binary files differdeleted file mode 100644 index 725e447604f..00000000000 --- a/doc/user/project/members/img/add_user_imported_members_13_8.png +++ /dev/null diff --git a/doc/user/project/members/img/add_user_imported_members_v13_9.png b/doc/user/project/members/img/add_user_imported_members_v13_9.png Binary files differnew file mode 100644 index 00000000000..e40240df2b2 --- /dev/null +++ b/doc/user/project/members/img/add_user_imported_members_v13_9.png diff --git a/doc/user/project/members/img/add_user_list_members_13_8.png b/doc/user/project/members/img/add_user_list_members_13_8.png Binary files differdeleted file mode 100644 index b8c0160c6d8..00000000000 --- a/doc/user/project/members/img/add_user_list_members_13_8.png +++ /dev/null diff --git a/doc/user/project/members/img/add_user_list_members_v13_9.png b/doc/user/project/members/img/add_user_list_members_v13_9.png Binary files differnew file mode 100644 index 00000000000..7a07ea01d14 --- /dev/null +++ b/doc/user/project/members/img/add_user_list_members_v13_9.png diff --git a/doc/user/project/members/img/add_user_search_people_13_8.png b/doc/user/project/members/img/add_user_search_people_v13_8.png Binary files differindex e9aa58512ab..e9aa58512ab 100644 --- a/doc/user/project/members/img/add_user_search_people_13_8.png +++ b/doc/user/project/members/img/add_user_search_people_v13_8.png diff --git a/doc/user/project/members/img/project_groups_tab_13_8.png b/doc/user/project/members/img/project_groups_tab_13_8.png Binary files differdeleted file mode 100644 index 5d7948f0761..00000000000 --- a/doc/user/project/members/img/project_groups_tab_13_8.png +++ /dev/null diff --git a/doc/user/project/members/img/project_groups_tab_v13_9.png b/doc/user/project/members/img/project_groups_tab_v13_9.png Binary files differnew file mode 100644 index 00000000000..d1b6a640341 --- /dev/null +++ b/doc/user/project/members/img/project_groups_tab_v13_9.png diff --git a/doc/user/project/members/img/project_members_13_8.png b/doc/user/project/members/img/project_members_13_8.png Binary files differdeleted file mode 100644 index 9120d471b3b..00000000000 --- a/doc/user/project/members/img/project_members_13_8.png +++ /dev/null diff --git a/doc/user/project/members/img/project_members_filter_direct_v13_9.png b/doc/user/project/members/img/project_members_filter_direct_v13_9.png Binary files differnew file mode 100644 index 00000000000..50115ee4052 --- /dev/null +++ b/doc/user/project/members/img/project_members_filter_direct_v13_9.png diff --git a/doc/user/project/members/img/project_members_filter_inherited_v13_9.png b/doc/user/project/members/img/project_members_filter_inherited_v13_9.png Binary files differnew file mode 100644 index 00000000000..433003fe58b --- /dev/null +++ b/doc/user/project/members/img/project_members_filter_inherited_v13_9.png diff --git a/doc/user/project/members/img/project_members_search_v13_9.png b/doc/user/project/members/img/project_members_search_v13_9.png Binary files differnew file mode 100644 index 00000000000..67280d11dca --- /dev/null +++ b/doc/user/project/members/img/project_members_search_v13_9.png diff --git a/doc/user/project/members/img/project_members_sort_v13_9.png b/doc/user/project/members/img/project_members_sort_v13_9.png Binary files differnew file mode 100644 index 00000000000..47abe18ba49 --- /dev/null +++ b/doc/user/project/members/img/project_members_sort_v13_9.png diff --git a/doc/user/project/members/img/project_members_v13_9.png b/doc/user/project/members/img/project_members_v13_9.png Binary files differnew file mode 100644 index 00000000000..3b48c752c6a --- /dev/null +++ b/doc/user/project/members/img/project_members_v13_9.png diff --git a/doc/user/project/members/img/share_project_with_groups_tab_v13_8.png b/doc/user/project/members/img/share_project_with_groups_tab_v13_8.png Binary files differdeleted file mode 100644 index 6cbbb386396..00000000000 --- a/doc/user/project/members/img/share_project_with_groups_tab_v13_8.png +++ /dev/null diff --git a/doc/user/project/members/img/share_project_with_groups_tab_v13_9.png b/doc/user/project/members/img/share_project_with_groups_tab_v13_9.png Binary files differnew file mode 100644 index 00000000000..99be996c03e --- /dev/null +++ b/doc/user/project/members/img/share_project_with_groups_tab_v13_9.png diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md index cccb998fc31..00474098487 100644 --- a/doc/user/project/members/index.md +++ b/doc/user/project/members/index.md @@ -21,42 +21,74 @@ project's **Members**. When your project belongs to the group, group members inherit the membership and permission level for the project from the group. -![Project members page](img/project_members_13_8.png) +![Project members page](img/project_members_v13_9.png) From the image above, we can deduce the following things: - There are 3 members that have access to the project. - User0 is a Reporter and has inherited their permissions from group `demo` which contains current project. -- For User1 there is no indication of a group, therefore they belong directly +- User1 is shown as a **Direct member** in the **Source** column, therefore they belong directly to the project we're inspecting. - Administrator is the Owner and member of **all** groups and for that reason, there is an indication of an ancestor group and inherited Owner permissions. -[From GitLab 12.6](https://gitlab.com/gitlab-org/gitlab/-/issues/21727), you can filter this list -using the dropdown on the right side: +## Filter and sort members -![Project members filter](img/project_members_filter_v12_6.png) +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/21727) in GitLab 12.6. +> - [Improved](https://gitlab.com/groups/gitlab-org/-/epics/4901) in GitLab 13.9. +> - Improvements are [deployed behind a feature flag](../../feature_flags.md), enabled by default. +> - Improvements are enabled on GitLab.com. +> - Improvements are recommended for production use. +> - For GitLab self-managed instances, GitLab administrators can opt to [disable improvements](#enable-or-disable-improvements-to-project-member-management). **(FREE SELF)** -- **Show only direct members** displays only User1. -- **Show only inherited members** displays User0 and Administrator. +The following sections illustrate how you can filter and sort members in a project. To view these options, +navigate to your desired project, go to **Members**, and include the noted search terms. + +### Membership filter + +By default, inherited and direct members are displayed. The membership filter can be used to display only inherited or only direct members. + +#### Display inherited members + +To display inherited members, include `Membership` `=` `Inherited` in the search text box. + +![Project members filter inherited](img/project_members_filter_inherited_v13_9.png) + +#### Display direct members + +To display direct members, include `Membership` `=` `Direct` in the search text box. + +![Project members filter direct](img/project_members_filter_direct_v13_9.png) + +### Search + +You can search for members by name, username, or email. + +![Project members search](img/project_members_search_v13_9.png) + +### Sort + +You can sort members by **Account**, **Access granted**, **Max role**, or **Last sign-in** in ascending or descending order. + +![Project members sort](img/project_members_sort_v13_9.png) ## Add a user Right next to **People**, start typing the name or username of the user you want to add. -![Search for people](img/add_user_search_people_13_8.png) +![Search for people](img/add_user_search_people_v13_8.png) Select the user and the [permission level](../../permissions.md) that you'd like to give the user. Note that you can select more than one user. -![Give user permissions](img/add_user_give_permissions_13_8.png) +![Give user permissions](img/add_user_give_permissions_v13_8.png) Once done, select **Add users to project** and they are immediately added to your project with the permissions you gave them above. -![List members](img/add_user_list_members_13_8.png) +![List members](img/add_user_list_members_v13_9.png) From there on, you can either remove an existing user or change their access level to the project. @@ -68,14 +100,14 @@ You can import another project's users in your own project by hitting the In the dropdown menu, you can see only the projects you are Maintainer on. -![Import members from another project](img/add_user_import_members_from_another_project_13_8.png) +![Import members from another project](img/add_user_import_members_from_another_project_v13_8.png) Select the one you want and hit **Import project members**. A flash message displays, notifying you that the import was successful, and the new members are now in the project's members list. Notice that the permissions that they had on the project you imported from are retained. -![Members list of new members](img/add_user_imported_members_13_8.png) +![Members list of new members](img/add_user_imported_members_v13_9.png) ## Invite people using their e-mail address @@ -83,18 +115,18 @@ If a user you want to give access to doesn't have an account on your GitLab instance, you can invite them just by typing their e-mail address in the user search field. -![Invite user by mail](img/add_user_email_search_13_8.png) +![Invite user by mail](img/add_user_email_search_v13_8.png) As you can imagine, you can mix inviting multiple people and adding existing GitLab users to the project. -![Invite user by mail ready to submit](img/add_user_email_ready_13_8.png) +![Invite user by mail ready to submit](img/add_user_email_ready_v13_8.png) Once done, hit **Add users to project** and watch that there is a new member with the e-mail address we used above. From there on, you can resend the invitation, change their access level, or even delete them. -![Invite user members list](img/add_user_email_accept_13_8.png) +![Invite user members list](img/add_user_email_accept_v13_9.png) While unaccepted, the system automatically sends reminder emails on the second, fifth, and tenth day after the invitation was initially sent. @@ -130,7 +162,7 @@ NOTE: If a project does not have any maintainers, the notification is sent to the most recently active owners of the project's group. -![Manage access requests](img/access_requests_management_13_8.png) +![Manage access requests](img/access_requests_management_v13_9.png) If you change your mind before your request is approved, just click the **Withdraw Access Request** button. @@ -167,3 +199,27 @@ To remove a member from a project: A **Remove member** modal appears. 1. (Optional) Select the **Also unassign this user from related issues and merge requests** checkbox. 1. Click **Remove member**. + +## Enable or disable improvements to project member management **(FREE SELF)** + +Project member management improvements are deployed behind a feature flag that is **enabled by default**. +[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) +can opt to disable the improvements. + +To disable them: + +```ruby +# For the instance +Feature.disable(:vue_project_members_list) +# For a single project +Feature.disable(:vue_project_members_list, Project.find(<project id>)) +``` + +To enable them: + +```ruby +# For the instance +Feature.enable(:vue_project_members_list) +# For a single project +Feature.enable(:vue_project_members_list, Project.find(<project id>)) +``` diff --git a/doc/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md index d17717fb29c..7000988d9bf 100644 --- a/doc/user/project/members/share_project_with_groups.md +++ b/doc/user/project/members/share_project_with_groups.md @@ -26,7 +26,7 @@ To share 'Project Acme' with the 'Engineering' group: 1. For 'Project Acme' use the left navigation menu to go to **Members**. - ![share project with groups](img/share_project_with_groups_tab_v13_8.png) + ![share project with groups](img/share_project_with_groups_tab_v13_9.png) 1. Select the **Invite group** tab. 1. Add the 'Engineering' group with the maximum access level of your choice. @@ -35,7 +35,7 @@ To share 'Project Acme' with the 'Engineering' group: 1. After sharing 'Project Acme' with 'Engineering': - The group is listed in the **Groups** tab. - !['Engineering' group is listed in Groups tab](img/project_groups_tab_13_8.png) + !['Engineering' group is listed in Groups tab](img/project_groups_tab_v13_9.png) - The project is listed on the group dashboard. diff --git a/doc/user/shortcuts.md b/doc/user/shortcuts.md index a8435424d62..014555cffed 100644 --- a/doc/user/shortcuts.md +++ b/doc/user/shortcuts.md @@ -35,7 +35,7 @@ These shortcuts are available in most areas of GitLab | <kbd>Shift</kbd> + <kbd>m</kbd> | Go to your Merge requests page.| | <kbd>Shift</kbd> + <kbd>t</kbd> | Go to your To-Do List page. | | <kbd>p</kbd> + <kbd>b</kbd> | Show/hide the Performance Bar. | -| <kbd>g</kbd> + <kbd>x</kbd> | Toggle between [GitLab](https://gitlab.com/) and [GitLab Next](https://next.gitlab.com/). | +| <kbd>g</kbd> + <kbd>x</kbd> | Toggle between [GitLab](https://gitlab.com/) and [GitLab Next](https://next.gitlab.com/) (GitLab SaaS only). | Additionally, the following shortcuts are available when editing text in text fields, for example comments, replies, issue descriptions, and merge request descriptions: diff --git a/lib/gitlab/auth/u2f_webauthn_converter.rb b/lib/gitlab/auth/u2f_webauthn_converter.rb new file mode 100644 index 00000000000..f85b2248aeb --- /dev/null +++ b/lib/gitlab/auth/u2f_webauthn_converter.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Gitlab + module Auth + class U2fWebauthnConverter + def initialize(u2f_registration) + @u2f_registration = u2f_registration + end + + def convert + now = Time.current + + converted_credential = WebAuthn::U2fMigrator.new( + app_id: Gitlab.config.gitlab.url, + certificate: u2f_registration.certificate, + key_handle: u2f_registration.key_handle, + public_key: u2f_registration.public_key, + counter: u2f_registration.counter + ).credential + + { + credential_xid: Base64.strict_encode64(converted_credential.id), + public_key: Base64.strict_encode64(converted_credential.public_key), + counter: u2f_registration.counter || 0, + name: u2f_registration.name || '', + user_id: u2f_registration.user_id, + u2f_registration_id: u2f_registration.id, + created_at: now, + updated_at: now + } + end + + private + + attr_reader :u2f_registration + end + end +end diff --git a/lib/gitlab/background_migration/migrate_u2f_webauthn.rb b/lib/gitlab/background_migration/migrate_u2f_webauthn.rb index b8c14aa2573..091e6660bac 100644 --- a/lib/gitlab/background_migration/migrate_u2f_webauthn.rb +++ b/lib/gitlab/background_migration/migrate_u2f_webauthn.rb @@ -16,26 +16,9 @@ module Gitlab def perform(start_id, end_id) old_registrations = U2fRegistration.where(id: start_id..end_id) old_registrations.each_slice(100) do |slice| - now = Time.now values = slice.map do |u2f_registration| - converted_credential = WebAuthn::U2fMigrator.new( - app_id: Gitlab.config.gitlab.url, - certificate: u2f_registration.certificate, - key_handle: u2f_registration.key_handle, - public_key: u2f_registration.public_key, - counter: u2f_registration.counter - ).credential - - { - credential_xid: Base64.strict_encode64(converted_credential.id), - public_key: Base64.strict_encode64(converted_credential.public_key), - counter: u2f_registration.counter || 0, - name: u2f_registration.name || '', - user_id: u2f_registration.user_id, - u2f_registration_id: u2f_registration.id, - created_at: now, - updated_at: now - } + converter = Gitlab::Auth::U2fWebauthnConverter.new(u2f_registration) + converter.convert end WebauthnRegistration.insert_all(values, unique_by: :credential_xid, returning: false) diff --git a/lib/gitlab/hook_data/subgroup_builder.rb b/lib/gitlab/hook_data/subgroup_builder.rb new file mode 100644 index 00000000000..a620219675a --- /dev/null +++ b/lib/gitlab/hook_data/subgroup_builder.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Gitlab + module HookData + class SubgroupBuilder < GroupBuilder + # Sample data + # { + # :created_at=>"2021-01-20T09:40:12Z", + # :updated_at=>"2021-01-20T09:40:12Z", + # :event_name=>"subgroup_create", + # :name=>"subgroup1", + # :path=>"subgroup1", + # :full_path=>"group1/subgroup1", + # :group_id=>10, + # :parent_group_id=>7, + # :parent_name=>group1, + # :parent_path=>group1, + # :parent_full_path=>group1 + # } + + private + + def event_data(event) + event_name = case event + when :create + 'subgroup_create' + when :destroy + 'subgroup_destroy' + end + + { event_name: event_name } + end + + def group_data + parent = group.parent + + super.merge( + parent_group_id: parent.id, + parent_name: parent.name, + parent_path: parent.path, + parent_full_path: parent.full_path + ) + end + + def event_specific_group_data(event) + {} + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1bd254b4794..8277195c1d1 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -32536,6 +32536,9 @@ msgstr "" msgid "Webhooks|Secret Token" msgstr "" +msgid "Webhooks|Subgroup events" +msgstr "" + msgid "Webhooks|Tag push events" msgstr "" @@ -32563,6 +32566,9 @@ msgstr "" msgid "Webhooks|This URL will be triggered when a new tag is pushed to the repository" msgstr "" +msgid "Webhooks|This URL will be triggered when a subgroup is created/removed" +msgstr "" + msgid "Webhooks|This URL will be triggered when a wiki page is created/updated" msgstr "" diff --git a/spec/controllers/projects/security/configuration_controller_spec.rb b/spec/controllers/projects/security/configuration_controller_spec.rb new file mode 100644 index 00000000000..afbebbad3d1 --- /dev/null +++ b/spec/controllers/projects/security/configuration_controller_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Security::ConfigurationController do + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + + before do + sign_in(user) + end + + describe 'GET show' do + context 'when feature flag is disabled' do + before do + stub_feature_flags(secure_security_and_compliance_configuration_page_on_ce: false) + end + + it 'renders not found' do + get :show, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when feature flag is enabled' do + context 'when user has guest access' do + before do + project.add_guest(user) + end + + it 'denies access' do + get :show, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when user has developer access' do + before do + project.add_developer(user) + end + + it 'grants access' do + get :show, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:show) + end + end + end + end +end diff --git a/spec/factories/u2f_registrations.rb b/spec/factories/u2f_registrations.rb index 7017b0ee9e7..40ad221415c 100644 --- a/spec/factories/u2f_registrations.rb +++ b/spec/factories/u2f_registrations.rb @@ -2,6 +2,8 @@ FactoryBot.define do factory :u2f_registration do + user + certificate { FFaker::BaconIpsum.characters(728) } key_handle { FFaker::BaconIpsum.characters(86) } public_key { FFaker::BaconIpsum.characters(88) } diff --git a/spec/features/alerts_settings/user_views_alerts_settings_spec.rb b/spec/features/alerts_settings/user_views_alerts_settings_spec.rb index 87bd02c3684..07c87f98eb6 100644 --- a/spec/features/alerts_settings/user_views_alerts_settings_spec.rb +++ b/spec/features/alerts_settings/user_views_alerts_settings_spec.rb @@ -19,6 +19,7 @@ RSpec.describe 'Alert integrations settings form', :js do describe 'when viewing alert integrations as a maintainer' do context 'with the default page permissions' do before do + stub_feature_flags(multiple_http_integrations_custom_mapping: false) visit project_settings_operations_path(project, anchor: 'js-alert-management-settings') wait_for_requests end diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 59fba5f65e0..ca44978d223 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -11,6 +11,11 @@ RSpec.describe 'Issue Sidebar' do let!(:label) { create(:label, project: project, title: 'bug') } let(:issue) { create(:labeled_issue, project: project, labels: [label]) } let!(:xss_label) { create(:label, project: project, title: '<script>alert("xss");</script>') } + let!(:milestone_expired) { create(:milestone, project: project, due_date: 5.days.ago) } + let!(:milestone_no_duedate) { create(:milestone, project: project, title: 'Foo - No due date') } + let!(:milestone1) { create(:milestone, project: project, title: 'Milestone-1', due_date: 20.days.from_now) } + let!(:milestone2) { create(:milestone, project: project, title: 'Milestone-2', due_date: 15.days.from_now) } + let!(:milestone3) { create(:milestone, project: project, title: 'Milestone-3', due_date: 10.days.from_now) } before do stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") @@ -134,6 +139,36 @@ RSpec.describe 'Issue Sidebar' do end end + context 'editing issue milestone', :js do + before do + page.within('.block.milestone > .title') do + click_on 'Edit' + end + end + + it 'shows milestons list in the dropdown' do + page.within('.block.milestone .dropdown-content') do + # 5 milestones + "No milestone" = 6 items + expect(page.find('ul')).to have_selector('li[data-milestone-id]', count: 6) + end + end + + it 'shows expired milestone at the bottom of the list' do + page.within('.block.milestone .dropdown-content ul') do + expect(page.find('li:last-child')).to have_content milestone_expired.title + end + end + + it 'shows milestone due earliest at the top of the list' do + page.within('.block.milestone .dropdown-content ul') do + expect(page.all('li[data-milestone-id]')[1]).to have_content milestone3.title + expect(page.all('li[data-milestone-id]')[2]).to have_content milestone2.title + expect(page.all('li[data-milestone-id]')[3]).to have_content milestone1.title + expect(page.all('li[data-milestone-id]')[4]).to have_content milestone_no_duedate.title + end + end + end + context 'editing issue labels', :js do before do issue.update(labels: [label]) diff --git a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb index be6205598d8..1ef6d2a1068 100644 --- a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb +++ b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb @@ -9,142 +9,158 @@ RSpec.describe 'Merge request < User sees mini pipeline graph', :js do let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running', sha: project.commit.id) } let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test') } - dropdown_toggle_selector = '[data-testid="mini-pipeline-graph-dropdown-toggle"]' - - before do - build.run - build.trace.set('hello') - sign_in(user) - visit_merge_request - end - - def visit_merge_request(format: :html, serializer: nil) - visit project_merge_request_path(project, merge_request, format: format, serializer: serializer) - end + shared_examples 'mini pipeline renders' do |ci_mini_pipeline_gl_dropdown_enabled| + before do + build.run + build.trace.set('hello') + sign_in(user) + stub_feature_flags(ci_mini_pipeline_gl_dropdown: ci_mini_pipeline_gl_dropdown_enabled) + visit_merge_request + end - it 'displays a mini pipeline graph' do - expect(page).to have_selector('.mr-widget-pipeline-graph') - end + let_it_be(:dropdown_toggle_selector) do + if ci_mini_pipeline_gl_dropdown_enabled + '[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle' + else + '[data-testid="mini-pipeline-graph-dropdown-toggle"]' + end + end - context 'as json' do - let(:artifacts_file1) { fixture_file_upload(File.join('spec/fixtures/banana_sample.gif'), 'image/gif') } - let(:artifacts_file2) { fixture_file_upload(File.join('spec/fixtures/dk.png'), 'image/png') } + def visit_merge_request(format: :html, serializer: nil) + visit project_merge_request_path(project, merge_request, format: format, serializer: serializer) + end - before do - job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline) - create(:ci_job_artifact, :archive, file: artifacts_file1, job: job) - create(:ci_build, :manual, pipeline: pipeline, when: 'manual') + it 'displays a mini pipeline graph' do + expect(page).to have_selector('.mr-widget-pipeline-graph') end - # TODO: https://gitlab.com/gitlab-org/gitlab-foss/issues/48034 - xit 'avoids repeated database queries' do - before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') } + context 'as json' do + let(:artifacts_file1) { fixture_file_upload(File.join('spec/fixtures/banana_sample.gif'), 'image/gif') } + let(:artifacts_file2) { fixture_file_upload(File.join('spec/fixtures/dk.png'), 'image/png') } - job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline) - create(:ci_job_artifact, :archive, file: artifacts_file2, job: job) - create(:ci_build, :manual, pipeline: pipeline, when: 'manual') + before do + job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline) + create(:ci_job_artifact, :archive, file: artifacts_file1, job: job) + create(:ci_build, :manual, pipeline: pipeline, when: 'manual') + end - after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') } + # TODO: https://gitlab.com/gitlab-org/gitlab-foss/issues/48034 + xit 'avoids repeated database queries' do + before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') } - expect(before.count).to eq(after.count) - expect(before.cached_count).to eq(after.cached_count) - end - end + job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline) + create(:ci_job_artifact, :archive, file: artifacts_file2, job: job) + create(:ci_build, :manual, pipeline: pipeline, when: 'manual') - describe 'build list toggle' do - let(:toggle) do - find(dropdown_toggle_selector) - first(dropdown_toggle_selector) + after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') } + + expect(before.count).to eq(after.count) + expect(before.cached_count).to eq(after.cached_count) + end end - # Status icon button styles should update as described in - # https://gitlab.com/gitlab-org/gitlab-foss/issues/42769 - it 'has unique styles for default, :hover, :active, and :focus states' do - default_background_color, default_foreground_color, default_box_shadow = get_toggle_colors(dropdown_toggle_selector) + describe 'build list toggle' do + let(:toggle) do + find(dropdown_toggle_selector) + first(dropdown_toggle_selector) + end - toggle.hover - hover_background_color, hover_foreground_color, hover_box_shadow = get_toggle_colors(dropdown_toggle_selector) + # Status icon button styles should update as described in + # https://gitlab.com/gitlab-org/gitlab-foss/issues/42769 + it 'has unique styles for default, :hover, :active, and :focus states' do + default_background_color, default_foreground_color, default_box_shadow = get_toggle_colors(dropdown_toggle_selector) - page.driver.browser.action.click_and_hold(toggle.native).perform - active_background_color, active_foreground_color, active_box_shadow = get_toggle_colors(dropdown_toggle_selector) + toggle.hover + hover_background_color, hover_foreground_color, hover_box_shadow = get_toggle_colors(dropdown_toggle_selector) - page.driver.browser.action.release(toggle.native) - .move_by(100, 100) - .perform - focus_background_color, focus_foreground_color, focus_box_shadow = get_toggle_colors(dropdown_toggle_selector) + page.driver.browser.action.click_and_hold(toggle.native).perform + active_background_color, active_foreground_color, active_box_shadow = get_toggle_colors(dropdown_toggle_selector) + page.driver.browser.action.release(toggle.native).perform - expect(default_background_color).not_to eq(hover_background_color) - expect(hover_background_color).not_to eq(active_background_color) - expect(default_background_color).not_to eq(active_background_color) + page.driver.browser.action.click(toggle.native).move_by(100, 100).perform + focus_background_color, focus_foreground_color, focus_box_shadow = get_toggle_colors(dropdown_toggle_selector) - expect(default_foreground_color).not_to eq(hover_foreground_color) - expect(hover_foreground_color).not_to eq(active_foreground_color) - expect(default_foreground_color).not_to eq(active_foreground_color) + expect(default_background_color).not_to eq(hover_background_color) + expect(hover_background_color).not_to eq(active_background_color) + expect(default_background_color).not_to eq(active_background_color) - expect(focus_background_color).to eq(hover_background_color) - expect(focus_foreground_color).to eq(hover_foreground_color) + expect(default_foreground_color).not_to eq(hover_foreground_color) + expect(hover_foreground_color).not_to eq(active_foreground_color) + expect(default_foreground_color).not_to eq(active_foreground_color) - expect(default_box_shadow).to eq('none') - expect(hover_box_shadow).to eq('none') - expect(active_box_shadow).not_to eq('none') - expect(focus_box_shadow).not_to eq('none') - end + expect(focus_background_color).to eq(hover_background_color) + expect(focus_foreground_color).to eq(hover_foreground_color) - it 'shows tooltip when hovered' do - toggle.hover + expect(default_box_shadow).to eq('none') + expect(hover_box_shadow).to eq('none') + expect(active_box_shadow).not_to eq('none') + expect(focus_box_shadow).not_to eq('none') + end - expect(page).to have_selector('.tooltip') - end - end + it 'shows tooltip when hovered' do + toggle.hover - describe 'builds list menu' do - let(:toggle) do - find(dropdown_toggle_selector) - first(dropdown_toggle_selector) + expect(page).to have_selector('.tooltip') + end end - before do - toggle.click - wait_for_requests - end + describe 'builds list menu' do + let(:toggle) do + find(dropdown_toggle_selector) + first(dropdown_toggle_selector) + end - it 'pens when toggle is clicked' do - expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu') - end + before do + toggle.click + wait_for_requests + end - it 'closes when toggle is clicked again' do - toggle.click + it 'pens when toggle is clicked' do + expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu') + end - expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') - end + it 'closes when toggle is clicked again' do + toggle.click - it 'closes when clicking somewhere else' do - find('body').click + expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') + end - expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') - end + it 'closes when clicking somewhere else' do + find('body').click - describe 'build list build item' do - let(:build_item) do - find('.mini-pipeline-graph-dropdown-item') - first('.mini-pipeline-graph-dropdown-item') + expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') end - it 'visits the build page when clicked' do - build_item.click - find('.build-page') + describe 'build list build item' do + let(:build_item) do + find('.mini-pipeline-graph-dropdown-item') + first('.mini-pipeline-graph-dropdown-item') + end - expect(current_path).to eql(project_job_path(project, build)) - end + it 'visits the build page when clicked' do + build_item.click + find('.build-page') - it 'shows tooltip when hovered' do - build_item.hover + expect(current_path).to eql(project_job_path(project, build)) + end - expect(page).to have_selector('.tooltip') + it 'shows tooltip when hovered' do + build_item.hover + + expect(page).to have_selector('.tooltip') + end end end end + context 'with ci_mini_pipeline_gl_dropdown disabled' do + it_behaves_like "mini pipeline renders", false + end + + context 'with ci_mini_pipeline_gl_dropdown enabled' do + it_behaves_like "mini pipeline renders", true + end + private def get_toggle_colors(selector) diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index d9a23edc03c..e0a0591fe6b 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -518,58 +518,75 @@ RSpec.describe 'Pipelines', :js do end end - context 'mini pipeline graph' do - let!(:build) do - create(:ci_build, :pending, pipeline: pipeline, - stage: 'build', - name: 'build') - end + shared_examples 'mini pipeline renders' do |ci_mini_pipeline_gl_dropdown_enabled| + context 'mini pipeline graph' do + let!(:build) do + create(:ci_build, :pending, pipeline: pipeline, + stage: 'build', + name: 'build') + end - dropdown_toggle_selector = '[data-testid="mini-pipeline-graph-dropdown-toggle"]' + before do + stub_feature_flags(ci_mini_pipeline_gl_dropdown: ci_mini_pipeline_gl_dropdown_enabled) + visit_project_pipelines + end - before do - visit_project_pipelines - end + let_it_be(:dropdown_toggle_selector) do + if ci_mini_pipeline_gl_dropdown_enabled + '[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle' + else + '[data-testid="mini-pipeline-graph-dropdown-toggle"]' + end + end - it 'renders a mini pipeline graph' do - expect(page).to have_selector('[data-testid="widget-mini-pipeline-graph"]') - expect(page).to have_selector(dropdown_toggle_selector) - end + it 'renders a mini pipeline graph' do + expect(page).to have_selector('[data-testid="widget-mini-pipeline-graph"]') + expect(page).to have_selector(dropdown_toggle_selector) + end - context 'when clicking a stage badge' do - it 'opens a dropdown' do - find(dropdown_toggle_selector).click + context 'when clicking a stage badge' do + it 'opens a dropdown' do + find(dropdown_toggle_selector).click - expect(page).to have_link build.name - end + expect(page).to have_link build.name + end - it 'is possible to cancel pending build' do - find(dropdown_toggle_selector).click - find('.js-ci-action').click - wait_for_requests + it 'is possible to cancel pending build' do + find(dropdown_toggle_selector).click + find('.js-ci-action').click + wait_for_requests - expect(build.reload).to be_canceled + expect(build.reload).to be_canceled + end end - end - context 'for a failed pipeline' do - let!(:build) do - create(:ci_build, :failed, pipeline: pipeline, - stage: 'build', - name: 'build') - end + context 'for a failed pipeline' do + let!(:build) do + create(:ci_build, :failed, pipeline: pipeline, + stage: 'build', + name: 'build') + end - it 'displays the failure reason' do - find(dropdown_toggle_selector).click + it 'displays the failure reason' do + find(dropdown_toggle_selector).click - within('.js-builds-dropdown-list') do - build_element = page.find('.mini-pipeline-graph-dropdown-item') - expect(build_element['title']).to eq('build - failed - (unknown failure)') + within('.js-builds-dropdown-list') do + build_element = page.find('.mini-pipeline-graph-dropdown-item') + expect(build_element['title']).to eq('build - failed - (unknown failure)') + end end end end end + context 'with ci_mini_pipeline_gl_dropdown disabled' do + it_behaves_like "mini pipeline renders", false + end + + context 'with ci_mini_pipeline_gl_dropdown enabled' do + it_behaves_like "mini pipeline renders", true + end + context 'with pagination' do before do allow(Ci::Pipeline).to receive(:default_per_page).and_return(1) diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb index 5762a54a717..eed67e3ac78 100644 --- a/spec/features/u2f_spec.rb +++ b/spec/features/u2f_spec.rb @@ -39,6 +39,11 @@ RSpec.describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :j end it 'allows the same device to be registered for multiple users' do + # U2f specs will be removed after WebAuthn migration completed + pending('FakeU2fDevice has static key handle, '\ + 'leading to duplicate credential_xid for WebAuthn during migration, '\ + 'resulting in unique constraint violation') + # First user visit profile_account_path manage_two_factor_authentication @@ -148,6 +153,11 @@ RSpec.describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :j describe "and also the current user" do it "allows logging in with that particular device" do + # U2f specs will be removed after WebAuthn migration completed + pending('FakeU2fDevice has static key handle, '\ + 'leading to duplicate credential_xid for WebAuthn during migration, '\ + 'resulting in unique constraint violation') + # Register current user with the same U2F device current_user = gitlab_sign_in(:user) current_user.update_attribute(:otp_required_for_login, true) diff --git a/spec/features/webauthn_spec.rb b/spec/features/webauthn_spec.rb index 2ffb6bb3477..4eebc9d2c1e 100644 --- a/spec/features/webauthn_spec.rb +++ b/spec/features/webauthn_spec.rb @@ -129,6 +129,10 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js do end it 'falls back to U2F' do + # WebAuthn registration is automatically created with the U2fRegistration because of the after_create callback + # so we need to delete it + WebauthnRegistration.delete_all + gitlab_sign_in(user) u2f_device.respond_to_u2f_authentication diff --git a/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap index 66e3f180d92..eb2b82a0211 100644 --- a/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap +++ b/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AlertsSettingsFormNew with default values renders the initial template 1`] = ` +exports[`AlertsSettingsForm with default values renders the initial template 1`] = ` <form class="gl-mt-6" > diff --git a/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js index 92ea8b2f33c..1ec7b5c18f6 100644 --- a/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js +++ b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js @@ -1,10 +1,10 @@ import { GlIcon, GlFormInput, GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import AlertMappingBuilder, { i18n } from '~/alerts_settings/components/alert_mapping_builder.vue'; -import gitlabFields from '~/alerts_settings/components/mocks/gitlabFields.json'; import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import * as transformationUtils from '~/alerts_settings/utils/mapping_transformations'; +import alertFields from '../mocks/alertFields.json'; describe('AlertMappingBuilder', () => { let wrapper; @@ -14,6 +14,7 @@ describe('AlertMappingBuilder', () => { propsData: { parsedPayload: parsedMapping.samplePayload.payloadAlerFields.nodes, savedMapping: parsedMapping.storedMapping.nodes, + alertFields, }, }); } @@ -44,28 +45,28 @@ describe('AlertMappingBuilder', () => { }); it('renders disabled form input for each mapped field', () => { - gitlabFields.forEach((field, index) => { + alertFields.forEach((field, index) => { const input = findColumnInRow(index + 1, 0).find(GlFormInput); - const types = field.type.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(' or '); + const types = field.types.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(' or '); expect(input.attributes('value')).toBe(`${field.label} (${types})`); expect(input.attributes('disabled')).toBe(''); }); }); it('renders right arrow next to each input', () => { - gitlabFields.forEach((field, index) => { + alertFields.forEach((field, index) => { const arrow = findColumnInRow(index + 1, 1).find('.right-arrow'); expect(arrow.exists()).toBe(true); }); }); it('renders mapping dropdown for each field', () => { - gitlabFields.forEach(({ compatibleTypes }, index) => { + alertFields.forEach(({ types }, index) => { const dropdown = findColumnInRow(index + 1, 2).find(GlDropdown); const searchBox = dropdown.findComponent(GlSearchBoxByType); const dropdownItems = dropdown.findAllComponents(GlDropdownItem); const { nodes } = parsedMapping.samplePayload.payloadAlerFields; - const mappingOptions = nodes.filter(({ type }) => compatibleTypes.includes(type)); + const mappingOptions = nodes.filter(({ type }) => types.includes(type)); expect(dropdown.exists()).toBe(true); expect(searchBox.exists()).toBe(true); @@ -74,7 +75,7 @@ describe('AlertMappingBuilder', () => { }); it('renders fallback dropdown only for the fields that have fallback', () => { - gitlabFields.forEach(({ compatibleTypes, numberOfFallbacks }, index) => { + alertFields.forEach(({ types, numberOfFallbacks }, index) => { const dropdown = findColumnInRow(index + 1, 3).find(GlDropdown); expect(dropdown.exists()).toBe(Boolean(numberOfFallbacks)); @@ -82,7 +83,7 @@ describe('AlertMappingBuilder', () => { const searchBox = dropdown.findComponent(GlSearchBoxByType); const dropdownItems = dropdown.findAllComponents(GlDropdownItem); const { nodes } = parsedMapping.samplePayload.payloadAlerFields; - const mappingOptions = nodes.filter(({ type }) => compatibleTypes.includes(type)); + const mappingOptions = nodes.filter(({ type }) => types.includes(type)); expect(searchBox.exists()).toBe(Boolean(numberOfFallbacks)); expect(dropdownItems).toHaveLength(mappingOptions.length); diff --git a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js index 2afa714fcf8..b43c1318a86 100644 --- a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js +++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js @@ -11,9 +11,10 @@ import waitForPromises from 'helpers/wait_for_promises'; import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue'; import MappingBuilder from '~/alerts_settings/components/alert_mapping_builder.vue'; import { typeSet } from '~/alerts_settings/constants'; +import alertFields from '../mocks/alertFields.json'; import { defaultAlertSettingsConfig } from './util'; -describe('AlertsSettingsFormNew', () => { +describe('AlertsSettingsForm', () => { let wrapper; const mockToastShow = jest.fn(); @@ -21,6 +22,7 @@ describe('AlertsSettingsFormNew', () => { data = {}, props = {}, multipleHttpIntegrationsCustomMapping = false, + multiIntegrations = true, } = {}) => { wrapper = mount(AlertsSettingsForm, { data() { @@ -32,8 +34,9 @@ describe('AlertsSettingsFormNew', () => { ...props, }, provide: { - glFeatures: { multipleHttpIntegrationsCustomMapping }, ...defaultAlertSettingsConfig, + glFeatures: { multipleHttpIntegrationsCustomMapping }, + multiIntegrations, }, mocks: { $toast: { @@ -132,7 +135,11 @@ describe('AlertsSettingsFormNew', () => { }); it('create with custom mapping', async () => { - createComponent({ multipleHttpIntegrationsCustomMapping: true }); + createComponent({ + multipleHttpIntegrationsCustomMapping: true, + multiIntegrations: true, + props: { alertFields }, + }); const integrationName = 'Test integration'; await selectOptionAtIndex(1); @@ -275,6 +282,7 @@ describe('AlertsSettingsFormNew', () => { currentIntegration: { type: typeSet.http, }, + alertFields, }, }); }); @@ -347,18 +355,27 @@ describe('AlertsSettingsFormNew', () => { describe('Mapping builder section', () => { describe.each` - featureFlag | integrationOption | visible - ${true} | ${1} | ${true} - ${true} | ${2} | ${false} - ${false} | ${1} | ${false} - ${false} | ${2} | ${false} - `('', ({ featureFlag, integrationOption, visible }) => { + alertFieldsProvided | multiIntegrations | featureFlag | integrationOption | visible + ${true} | ${true} | ${true} | ${1} | ${true} + ${true} | ${true} | ${true} | ${2} | ${false} + ${true} | ${true} | ${false} | ${1} | ${false} + ${true} | ${true} | ${false} | ${2} | ${false} + ${true} | ${false} | ${true} | ${1} | ${false} + ${false} | ${true} | ${true} | ${1} | ${false} + `('', ({ alertFieldsProvided, multiIntegrations, featureFlag, integrationOption, visible }) => { const visibleMsg = visible ? 'is rendered' : 'is not rendered'; const featureFlagMsg = featureFlag ? 'is enabled' : 'is disabled'; + const alertFieldsMsg = alertFieldsProvided ? 'are provided' : 'are not provided'; const integrationType = integrationOption === 1 ? typeSet.http : typeSet.prometheus; - it(`${visibleMsg} when multipleHttpIntegrationsCustomMapping feature flag ${featureFlagMsg} and integration type is ${integrationType}`, async () => { - createComponent({ multipleHttpIntegrationsCustomMapping: featureFlag }); + it(`${visibleMsg} when multipleHttpIntegrationsCustomMapping feature flag ${featureFlagMsg} and integration type is ${integrationType} and alert fields ${alertFieldsMsg}`, async () => { + createComponent({ + multipleHttpIntegrationsCustomMapping: featureFlag, + multiIntegrations, + props: { + alertFields: alertFieldsProvided ? alertFields : [], + }, + }); await selectOptionAtIndex(integrationOption); expect(findMappingBuilderSection().exists()).toBe(visible); diff --git a/spec/frontend/alerts_settings/mocks/alertFields.json b/spec/frontend/alerts_settings/mocks/alertFields.json new file mode 100644 index 00000000000..ffe59dd0c05 --- /dev/null +++ b/spec/frontend/alerts_settings/mocks/alertFields.json @@ -0,0 +1,123 @@ +[ + { + "name": "title", + "label": "Title", + "type": [ + "string" + ], + "types": [ + "string", + "number", + "datetime" + ], + "numberOfFallbacks": 1 + }, + { + "name": "description", + "label": "Description", + "type": [ + "string" + ], + "types": [ + "string", + "number", + "datetime" + ] + }, + { + "name": "start_time", + "label": "Start time", + "type": [ + "datetime" + ], + "types": [ + "number", + "datetime" + ] + }, + { + "name": "end_time", + "label": "End time", + "type": [ + "datetime" + ], + "types": [ + "number", + "datetime" + ] + }, + { + "name": "service", + "label": "Service", + "type": [ + "string" + ], + "types": [ + "string", + "number", + "datetime" + ] + }, + { + "name": "monitoring_tool", + "label": "Monitoring tool", + "type": [ + "string" + ], + "types": [ + "string", + "number", + "datetime" + ] + }, + { + "name": "hosts", + "label": "Hosts", + "type": [ + "string", + "ARRAY" + ], + "types": [ + "string", + "ARRAY", + "number", + "datetime" + ] + }, + { + "name": "severity", + "label": "Severity", + "type": [ + "string" + ], + "types": [ + "string", + "number", + "datetime" + ] + }, + { + "name": "fingerprint", + "label": "Fingerprint", + "type": [ + "string" + ], + "types": [ + "string", + "number", + "datetime" + ] + }, + { + "name": "gitlab_environment_name", + "label": "Environment", + "type": [ + "string" + ], + "types": [ + "string", + "number", + "datetime" + ] + } +] diff --git a/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js b/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js index f725712fdd2..f8d2a7aa23b 100644 --- a/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js +++ b/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js @@ -3,24 +3,23 @@ import { getPayloadFields, transformForSave, } from '~/alerts_settings/utils/mapping_transformations'; -import gitlabFieldsMock from '~/alerts_settings/components/mocks/gitlabFields.json'; import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json'; +import alertFields from '../mocks/alertFields.json'; describe('Mapping Transformation Utilities', () => { const nameField = { label: 'Name', path: ['alert', 'name'], - type: 'STRING', + type: 'string', }; const dashboardField = { label: 'Dashboard Id', path: ['alert', 'dashboardId'], - type: 'STRING', + type: 'string', }; describe('getMappingData', () => { it('should return mapping data', () => { - const alertFields = gitlabFieldsMock.slice(0, 3); const result = getMappingData( alertFields, getPayloadFields(parsedMapping.samplePayload.payloadAlerFields.nodes.slice(0, 3)), @@ -51,7 +50,9 @@ describe('Mapping Transformation Utilities', () => { ]; const result = transformForSave(mockMappingData); const { path, type, label } = nameField; - expect(result).toEqual([{ fieldName, path, type, label }]); + expect(result).toEqual([ + { fieldName: fieldName.toUpperCase(), path, type: type.toUpperCase(), label }, + ]); }); it('should return empty array if no mapping provided', () => { diff --git a/spec/frontend/pipelines/stage_spec.js b/spec/frontend/pipelines/stage_spec.js index e4782a1dab1..8c914705d97 100644 --- a/spec/frontend/pipelines/stage_spec.js +++ b/spec/frontend/pipelines/stage_spec.js @@ -1,4 +1,6 @@ import 'bootstrap/js/dist/dropdown'; +import $ from 'jquery'; +import { GlDropdown } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; @@ -9,6 +11,7 @@ import { stageReply } from './mock_data'; describe('Pipelines stage component', () => { let wrapper; let mock; + let glFeatures; const defaultProps = { stage: { @@ -22,8 +25,6 @@ describe('Pipelines stage component', () => { updateDropdown: false, }; - const isDropdownOpen = () => wrapper.classes('show'); - const createComponent = (props = {}) => { wrapper = mount(StageComponent, { attachTo: document.body, @@ -31,110 +32,265 @@ describe('Pipelines stage component', () => { ...defaultProps, ...props, }, + provide: { + glFeatures, + }, }); }; beforeEach(() => { mock = new MockAdapter(axios); + jest.spyOn(eventHub, '$emit'); + glFeatures = {}; }); afterEach(() => { wrapper.destroy(); wrapper = null; + eventHub.$emit.mockRestore(); mock.restore(); }); - describe('default', () => { - beforeEach(() => { - createComponent(); + describe('when ci_mini_pipeline_gl_dropdown feature flag is disabled', () => { + const isDropdownOpen = () => wrapper.classes('show'); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('should render a dropdown with the status icon', () => { + expect(wrapper.attributes('class')).toEqual('dropdown'); + expect(wrapper.find('svg').exists()).toBe(true); + expect(wrapper.find('button').attributes('data-toggle')).toEqual('dropdown'); + }); }); - it('should render a dropdown with the status icon', () => { - expect(wrapper.attributes('class')).toEqual('dropdown'); - expect(wrapper.find('svg').exists()).toBe(true); - expect(wrapper.find('button').attributes('data-toggle')).toEqual('dropdown'); + describe('with successful request', () => { + beforeEach(() => { + mock.onGet('path.json').reply(200, stageReply); + createComponent(); + }); + + it('should render the received data and emit `clickedDropdown` event', async () => { + wrapper.find('button').trigger('click'); + + await axios.waitForAll(); + expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain( + stageReply.latest_statuses[0].name, + ); + + expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown'); + }); }); - }); - describe('with successful request', () => { - beforeEach(() => { - mock.onGet('path.json').reply(200, stageReply); + it('when request fails should close the dropdown', async () => { + mock.onGet('path.json').reply(500); createComponent(); - }); + wrapper.find({ ref: 'dropdown' }).trigger('click'); - it('should render the received data and emit `clickedDropdown` event', async () => { - jest.spyOn(eventHub, '$emit'); - wrapper.find('button').trigger('click'); + expect(isDropdownOpen()).toBe(true); + wrapper.find('button').trigger('click'); await axios.waitForAll(); - expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain( - stageReply.latest_statuses[0].name, - ); - expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown'); + expect(isDropdownOpen()).toBe(false); }); - }); - it('when request fails should close the dropdown', async () => { - mock.onGet('path.json').reply(500); - createComponent(); - wrapper.find({ ref: 'dropdown' }).trigger('click'); - expect(isDropdownOpen()).toBe(true); + describe('update endpoint correctly', () => { + beforeEach(() => { + const copyStage = { ...stageReply }; + copyStage.latest_statuses[0].name = 'this is the updated content'; + mock.onGet('bar.json').reply(200, copyStage); + createComponent({ + stage: { + status: { + group: 'running', + icon: 'status_running', + title: 'running', + }, + dropdown_path: 'bar.json', + }, + }); + return axios.waitForAll(); + }); + + it('should update the stage to request the new endpoint provided', async () => { + wrapper.find('button').trigger('click'); + await axios.waitForAll(); + + expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain( + 'this is the updated content', + ); + }); + }); + + describe('pipelineActionRequestComplete', () => { + beforeEach(() => { + mock.onGet('path.json').reply(200, stageReply); + mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); + }); + + const clickCiAction = async () => { + wrapper.find('button').trigger('click'); + await axios.waitForAll(); + + wrapper.find('.js-ci-action').trigger('click'); + await axios.waitForAll(); + }; + + describe('within pipeline table', () => { + it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => { + createComponent({ type: 'PIPELINES_TABLE' }); + + await clickCiAction(); + + expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); + }); + }); + + describe('in MR widget', () => { + beforeEach(() => { + jest.spyOn($.fn, 'dropdown'); + }); - wrapper.find('button').trigger('click'); - await axios.waitForAll(); + it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => { + createComponent(); - expect(isDropdownOpen()).toBe(false); + await clickCiAction(); + + expect($.fn.dropdown).toHaveBeenCalledWith('toggle'); + }); + }); + }); }); - describe('update endpoint correctly', () => { + describe('when ci_mini_pipeline_gl_dropdown feature flag is enabled', () => { + const findDropdown = () => wrapper.find(GlDropdown); + const findDropdownToggle = () => wrapper.find('button.gl-dropdown-toggle'); + const findDropdownMenu = () => + wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]'); + const findCiActionBtn = () => wrapper.find('.js-ci-action'); + + const openGlDropdown = () => { + findDropdownToggle().trigger('click'); + return new Promise((resolve) => { + wrapper.vm.$root.$on('bv::dropdown::show', resolve); + }); + }; + beforeEach(() => { - const copyStage = { ...stageReply }; - copyStage.latest_statuses[0].name = 'this is the updated content'; - mock.onGet('bar.json').reply(200, copyStage); - createComponent({ - stage: { - status: { - group: 'running', - icon: 'status_running', - title: 'running', - }, - dropdown_path: 'bar.json', - }, + glFeatures = { ciMiniPipelineGlDropdown: true }; + }); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('should render a dropdown with the status icon', () => { + expect(findDropdown().exists()).toBe(true); + expect(findDropdownToggle().classes('gl-dropdown-toggle')).toEqual(true); + expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true); }); - return axios.waitForAll(); }); - it('should update the stage to request the new endpoint provided', async () => { - wrapper.find('button').trigger('click'); + describe('with successful request', () => { + beforeEach(() => { + mock.onGet('path.json').reply(200, stageReply); + createComponent(); + }); + + it('should render the received data and emit `clickedDropdown` event', async () => { + await openGlDropdown(); + await axios.waitForAll(); + + expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name); + expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown'); + }); + }); + + it('when request fails should close the dropdown', async () => { + mock.onGet('path.json').reply(500); + + createComponent(); + + await openGlDropdown(); await axios.waitForAll(); - expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain( - 'this is the updated content', - ); + expect(findDropdown().classes('show')).toBe(false); }); - }); - describe('pipelineActionRequestComplete', () => { - beforeEach(() => { - mock.onGet('path.json').reply(200, stageReply); - mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); + describe('update endpoint correctly', () => { + beforeEach(async () => { + const copyStage = { ...stageReply }; + copyStage.latest_statuses[0].name = 'this is the updated content'; + mock.onGet('bar.json').reply(200, copyStage); + createComponent({ + stage: { + status: { + group: 'running', + icon: 'status_running', + title: 'running', + }, + dropdown_path: 'bar.json', + }, + }); + await axios.waitForAll(); + }); - createComponent({ type: 'PIPELINES_TABLE' }); + it('should update the stage to request the new endpoint provided', async () => { + await openGlDropdown(); + await axios.waitForAll(); + + expect(findDropdownMenu().text()).toContain('this is the updated content'); + }); }); - describe('within pipeline table', () => { - it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => { - jest.spyOn(eventHub, '$emit'); + describe('pipelineActionRequestComplete', () => { + beforeEach(() => { + mock.onGet('path.json').reply(200, stageReply); + mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); + }); - wrapper.find('button').trigger('click'); + const clickCiAction = async () => { + await openGlDropdown(); await axios.waitForAll(); - wrapper.find('.js-ci-action').trigger('click'); + findCiActionBtn().trigger('click'); await axios.waitForAll(); + }; + + describe('within pipeline table', () => { + beforeEach(() => { + createComponent({ type: 'PIPELINES_TABLE' }); + }); + + it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => { + await clickCiAction(); + + expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); + }); + }); + + describe('in MR widget', () => { + beforeEach(() => { + jest.spyOn($.fn, 'dropdown'); + createComponent(); + }); + + it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => { + const hidden = jest.fn(); + + wrapper.vm.$root.$on('bv::dropdown::hide', hidden); + + expect(hidden).toHaveBeenCalledTimes(0); + + await clickCiAction(); - expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); + expect(hidden).toHaveBeenCalledTimes(1); + }); }); }); }); diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 8e4da5a317d..b61db537159 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -398,6 +398,45 @@ RSpec.describe ProjectsHelper do helper.send(:get_project_nav_tabs, project, user) end + context 'Security & Compliance tabs' do + before do + stub_feature_flags(secure_security_and_compliance_configuration_page_on_ce: feature_flag_enabled) + allow(helper).to receive(:can?).with(user, :read_security_configuration, project).and_return(can_read_security_configuration) + end + + context 'when user cannot read security configuration' do + let(:can_read_security_configuration) { false } + + context 'when feature flag is disabled' do + let(:feature_flag_enabled) { false } + + it { is_expected.not_to include(:security_configuration) } + end + + context 'when feature flag is enabled' do + let(:feature_flag_enabled) { true } + + it { is_expected.not_to include(:security_configuration) } + end + end + + context 'when user can read security configuration' do + let(:can_read_security_configuration) { true } + + context 'when feature flag is disabled' do + let(:feature_flag_enabled) { false } + + it { is_expected.not_to include(:security_configuration) } + end + + context 'when feature flag is enabled' do + let(:feature_flag_enabled) { true } + + it { is_expected.to include(:security_configuration) } + end + end + end + context 'when builds feature is enabled' do before do allow(project).to receive(:builds_enabled?).and_return(true) diff --git a/spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb b/spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb new file mode 100644 index 00000000000..deddc7f5294 --- /dev/null +++ b/spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Auth::U2fWebauthnConverter do + let_it_be(:u2f_registration) do + device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5)) + create(:u2f_registration, name: 'u2f_device', + certificate: Base64.strict_encode64(device.cert_raw), + key_handle: U2F.urlsafe_encode64(device.key_handle_raw), + public_key: Base64.strict_encode64(device.origin_public_key_raw)) + end + + it 'converts u2f registration' do + webauthn_credential = WebAuthn::U2fMigrator.new( + app_id: Gitlab.config.gitlab.url, + certificate: u2f_registration.certificate, + key_handle: u2f_registration.key_handle, + public_key: u2f_registration.public_key, + counter: u2f_registration.counter + ).credential + + converted_webauthn = described_class.new(u2f_registration).convert + + expect(converted_webauthn).to( + include(user_id: u2f_registration.user_id, + credential_xid: Base64.strict_encode64(webauthn_credential.id))) + end +end diff --git a/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb b/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb new file mode 100644 index 00000000000..89e5dffd7b4 --- /dev/null +++ b/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::HookData::SubgroupBuilder do + let_it_be(:parent_group) { create(:group) } + let_it_be(:subgroup) { create(:group, parent: parent_group) } + + describe '#build' do + let(:data) { described_class.new(subgroup).build(event) } + let(:event_name) { data[:event_name] } + let(:attributes) do + [ + :event_name, :created_at, :updated_at, :name, :path, :full_path, :group_id, + :parent_group_id, :parent_name, :parent_path, :parent_full_path + ] + end + + context 'data' do + shared_examples_for 'includes the required attributes' do + it 'includes the required attributes' do + expect(data).to include(*attributes) + + expect(data[:name]).to eq(subgroup.name) + expect(data[:path]).to eq(subgroup.path) + expect(data[:full_path]).to eq(subgroup.full_path) + expect(data[:group_id]).to eq(subgroup.id) + expect(data[:created_at]).to eq(subgroup.created_at.xmlschema) + expect(data[:updated_at]).to eq(subgroup.updated_at.xmlschema) + expect(data[:parent_name]).to eq(parent_group.name) + expect(data[:parent_path]).to eq(parent_group.path) + expect(data[:parent_full_path]).to eq(parent_group.full_path) + expect(data[:parent_group_id]).to eq(parent_group.id) + end + end + + context 'on create' do + let(:event) { :create } + + it { expect(event_name).to eq('subgroup_create') } + it_behaves_like 'includes the required attributes' + end + + context 'on destroy' do + let(:event) { :destroy } + + it { expect(event_name).to eq('subgroup_destroy') } + it_behaves_like 'includes the required attributes' + end + end + end +end diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb index 77a1377c138..476d99364b6 100644 --- a/spec/models/project_services/chat_notification_service_spec.rb +++ b/spec/models/project_services/chat_notification_service_spec.rb @@ -75,6 +75,39 @@ RSpec.describe ChatNotificationService do end end + context 'when the data object has a label' do + let(:label) { create(:label, project: project, name: 'Bug')} + let(:issue) { create(:labeled_issue, project: project, labels: [label]) } + let(:note) { create(:note, noteable: issue, project: project)} + let(:data) { Gitlab::DataBuilder::Note.build(note, user) } + + it 'notifies the chat service' do + expect(chat_service).to receive(:notify).with(any_args) + + chat_service.execute(data) + end + + context 'and the chat_service has a label filter that does not matches the label' do + subject(:chat_service) { described_class.new(labels_to_be_notified: '~some random label') } + + it 'does not notify the chat service' do + expect(chat_service).not_to receive(:notify) + + chat_service.execute(data) + end + end + + context 'and the chat_service has a label filter that matches the label' do + subject(:chat_service) { described_class.new(labels_to_be_notified: '~Backend, ~Bug') } + + it 'notifies the chat service' do + expect(chat_service).to receive(:notify).with(any_args) + + chat_service.execute(data) + end + end + end + context 'with "channel" property' do before do allow(chat_service).to receive(:channel).and_return(channel) diff --git a/spec/models/u2f_registration_spec.rb b/spec/models/u2f_registration_spec.rb new file mode 100644 index 00000000000..1f2e4d1e447 --- /dev/null +++ b/spec/models/u2f_registration_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe U2fRegistration do + let_it_be(:user) { create(:user) } + let(:u2f_registration) do + device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5)) + create(:u2f_registration, name: 'u2f_device', + user: user, + certificate: Base64.strict_encode64(device.cert_raw), + key_handle: U2F.urlsafe_encode64(device.key_handle_raw), + public_key: Base64.strict_encode64(device.origin_public_key_raw)) + end + + describe 'callbacks' do + describe '#create_webauthn_registration' do + it 'creates webauthn registration' do + u2f_registration.save! + + webauthn_registration = WebauthnRegistration.where(u2f_registration_id: u2f_registration.id) + expect(webauthn_registration).to exist + end + + it 'logs error' do + allow(Gitlab::Auth::U2fWebauthnConverter).to receive(:new).and_raise('boom!') + expect(Gitlab::AppJsonLogger).to( + receive(:error).with(a_hash_including(event: 'u2f_migration', + error: 'RuntimeError', + message: 'U2F to WebAuthn conversion failed')) + ) + + u2f_registration.save! + end + end + end +end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 6a4a7ecd13e..efa1afb758f 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -865,6 +865,28 @@ RSpec.describe ProjectPolicy do end end + context 'security configuration feature' do + %w(guest reporter).each do |role| + context role do + let(:current_user) { send(role) } + + it 'prevents reading security configuration' do + expect_disallowed(:read_security_configuration) + end + end + end + + %w(developer maintainer owner).each do |role| + context role do + let(:current_user) { send(role) } + + it 'allows reading security configuration' do + expect_allowed(:read_security_configuration) + end + end + end + end + describe 'design permissions' do let(:current_user) { guest } diff --git a/spec/routing/projects/security/configuration_controller_routing_spec.rb b/spec/routing/projects/security/configuration_controller_routing_spec.rb new file mode 100644 index 00000000000..c2b10a49dea --- /dev/null +++ b/spec/routing/projects/security/configuration_controller_routing_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Security::ConfigurationController, 'routing' do + let(:base_params) { { namespace_id: 'gitlab', project_id: 'gitlabhq' } } + + before do + allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq', any_args).and_return(true) + end + + it 'routes to #show' do + expect(get('/gitlab/gitlabhq/-/security/configuration')).to route_to('projects/security/configuration#show', namespace_id: 'gitlab', project_id: 'gitlabhq') + end +end diff --git a/spec/services/resource_events/change_milestone_service_spec.rb b/spec/services/resource_events/change_milestone_service_spec.rb index a2131c5c1b0..ed234376381 100644 --- a/spec/services/resource_events/change_milestone_service_spec.rb +++ b/spec/services/resource_events/change_milestone_service_spec.rb @@ -6,8 +6,8 @@ RSpec.describe ResourceEvents::ChangeMilestoneService do let_it_be(:timebox) { create(:milestone) } let(:created_at_time) { Time.utc(2019, 12, 30) } - let(:add_timebox_args) { { created_at: created_at_time, old_milestone: nil } } - let(:remove_timebox_args) { { created_at: created_at_time, old_milestone: timebox } } + let(:add_timebox_args) { { old_milestone: nil } } + let(:remove_timebox_args) { { old_milestone: timebox } } [:issue, :merge_request].each do |issuable| it_behaves_like 'timebox(milestone or iteration) resource events creator', ResourceMilestoneEvent do diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 57d8320b76a..3fd4f2698e9 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -18,7 +18,8 @@ RSpec.shared_context 'project navbar structure' do { nav_item: _('Security & Compliance'), nav_sub_items: [ - _('Audit Events') + _('Configuration'), + (_('Audit Events') if Gitlab.ee?) ] } end @@ -71,7 +72,7 @@ RSpec.shared_context 'project navbar structure' do _('Schedules') ] }, - (security_and_compliance_nav_item if Gitlab.ee?), + security_and_compliance_nav_item, { nav_item: _('Operations'), nav_sub_items: [ @@ -190,7 +191,7 @@ RSpec.shared_context 'group navbar structure' do nav_item: _('Merge Requests'), nav_sub_items: [] }, - (security_and_compliance_nav_item if Gitlab.ee?), + security_and_compliance_nav_item, (push_rules_nav_item if Gitlab.ee?), { nav_item: _('Kubernetes'), diff --git a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb index 7d42bda7090..fac9f1d6253 100644 --- a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb +++ b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb @@ -3,6 +3,10 @@ RSpec.shared_examples 'timebox(milestone or iteration) resource events creator' do |timebox_event_class| let_it_be(:user) { create(:user) } + before do + resource.system_note_timestamp = created_at_time + end + context 'when milestone/iteration is added' do let(:service) { described_class.new(resource, user, **add_timebox_args) } diff --git a/spec/views/layouts/nav/sidebar/_project_security_link.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project_security_link.html.haml_spec.rb new file mode 100644 index 00000000000..d3fb35bff6d --- /dev/null +++ b/spec/views/layouts/nav/sidebar/_project_security_link.html.haml_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'layouts/nav/sidebar/_project_security_link' do + let_it_be_with_reload(:project) { create(:project) } + context 'on security configuration' do + before do + assign(:project, project) + allow(controller).to receive(:controller_name).and_return('configuration') + allow(controller).to receive(:controller_path).and_return('projects/security/configuration') + allow(controller).to receive(:action_name).and_return('show') + allow(view).to receive(:any_project_nav_tab?).and_return(true) + allow(view).to receive(:project_nav_tab?).and_return(true) + end + + it 'activates Security & Compliance tab' do + render + + expect(rendered).to have_css('li.active', text: 'Security & Compliance') + end + + it 'activates Configuration sub tab' do + render + + expect(rendered).to have_css('.sidebar-sub-level-items > li.active', text: 'Configuration') + end + end +end |