diff options
Diffstat (limited to 'app')
41 files changed, 371 insertions, 259 deletions
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 |