diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-24 00:11:46 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-24 00:11:46 +0300 |
commit | 66e3f84f5200d00e3ce3137dad80592096ef3401 (patch) | |
tree | d564786eec6b40a17c8450051887f949517d2454 /app | |
parent | 5421d61b1d5ffe11a9c7afbe2259b4e4d0e7c993 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
36 files changed, 527 insertions, 174 deletions
diff --git a/app/assets/javascripts/alert_management/components/alert_management_table.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue index c0cac958a42..fe18b9da560 100644 --- a/app/assets/javascripts/alert_management/components/alert_management_table.vue +++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue @@ -312,6 +312,7 @@ export default { <template #table> <gl-table class="alert-management-table" + data-qa-selector="alert_table_container" :items=" alerts ? alerts.list diff --git a/app/assets/javascripts/alerts_settings/components/alerts_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_form.vue index 388d925196b..a0d5cb7f4c3 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_form.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_form.vue @@ -83,7 +83,7 @@ export default { </p> <form ref="settingsForm" @submit.prevent="updateAlertsIntegrationSettings"> <gl-form-group class="gl-pl-0"> - <gl-form-checkbox v-model="createIssueEnabled" data-qa-selector="create_issue_checkbox"> + <gl-form-checkbox v-model="createIssueEnabled" data-qa-selector="create_incident_checkbox"> <span>{{ $options.i18n.createIncident.label }}</span> </gl-form-checkbox> </gl-form-group> 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 03bc4b825ae..65c3bc732ed 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue @@ -430,6 +430,7 @@ export default { v-model="integrationForm.type" :disabled="isSelectDisabled" class="gl-max-w-full" + data-qa-selector="integration_type_dropdown" :options="integrationTypesOptions" /> @@ -461,6 +462,7 @@ export default { v-model="integrationForm.name" type="text" :placeholder="$options.i18n.integrationFormSteps.nameIntegration.placeholder" + data-qa-selector="integration_name_field" @input="validateName" /> </gl-form-group> @@ -483,6 +485,7 @@ export default { v-model="integrationForm.active" :is-loading="loading" :label="$options.i18n.integrationFormSteps.nameIntegration.activeToggle" + data-qa-selector="active_toggle_container" class="gl-mt-4 gl-font-weight-normal" /> </gl-form-group> @@ -594,6 +597,7 @@ export default { category="secondary" class="gl-ml-3 js-no-auto-disable" data-testid="integration-form-test-and-submit" + data-qa-selector="save_and_create_alert_button" @click="submit(true)" > {{ $options.i18n.saveAndTestIntegration }} @@ -695,6 +699,7 @@ export default { :debounce="$options.JSON_VALIDATE_DELAY" rows="6" max-rows="10" + data-qa-selector="test_payload_field" @input="validateJson(false)" /> </gl-form-group> @@ -706,6 +711,7 @@ export default { data-testid="send-test-alert" variant="confirm" class="js-no-auto-disable" + data-qa-selector="send_test_alert_button" @click="isFormDirty ? null : sendTestAlert()" > {{ $options.i18n.send }} 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 bf456b6adaa..010cb5721a1 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue @@ -375,6 +375,7 @@ export default { category="secondary" variant="confirm" data-testid="add-integration-btn" + data-qa-selector="add_integration_button" class="gl-mt-3" @click="setFormVisibility(true)" > diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 5f35dbdc5e7..3c9c0b1ade1 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -7,6 +7,7 @@ import { EDITOR_TYPE_CODE, EDITOR_CODE_INSTANCE_FN, EDITOR_DIFF_INSTANCE_FN, + EXTENSION_CI_SCHEMA_FILE_NAME_MATCH, } from '~/editor/constants'; import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base'; import { EditorWebIdeExtension } from '~/editor/extensions/source_editor_webide_ext'; @@ -26,6 +27,7 @@ import { performanceMarkAndMeasure } from '~/performance/utils'; import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils'; import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { leftSidebarViews, viewerTypes, @@ -53,6 +55,7 @@ export default { DiffViewer, FileTemplatesBar, }, + mixins: [glFeatureFlagMixin()], props: { file: { type: Object, @@ -145,6 +148,12 @@ export default { showTabs() { return !this.shouldHideEditor && this.isEditModeActive && this.previewMode; }, + isCiConfigFile() { + return ( + this.file.path === EXTENSION_CI_SCHEMA_FILE_NAME_MATCH && + this.editor?.getEditorType() === EDITOR_TYPE_CODE + ); + }, }, watch: { 'file.name': { @@ -232,8 +241,6 @@ export default { return; } - this.registerSchemaForFile(); - Promise.all([this.fetchFileData(), this.fetchEditorconfigRules()]) .then(() => { this.createEditorInstance(); @@ -357,6 +364,8 @@ export default { this.model.updateOptions(this.rules); + this.registerSchemaForFile(); + this.model.onChange((model) => { const { file } = model; if (!file.active) return; @@ -446,8 +455,33 @@ export default { return Promise.resolve(); }, registerSchemaForFile() { - const schema = this.getJsonSchemaForPath(this.file.path); - registerSchema(schema); + const registerExternalSchema = () => { + const schema = this.getJsonSchemaForPath(this.file.path); + return registerSchema(schema); + }; + const registerLocalSchema = async () => { + if (!this.CiSchemaExtension) { + const { CiSchemaExtension } = await import( + '~/editor/extensions/source_editor_ci_schema_ext' + ).catch((e) => + createAlert({ + message: e, + }), + ); + this.CiSchemaExtension = CiSchemaExtension; + } + this.editor.use({ definition: this.CiSchemaExtension }); + this.editor.registerCiSchema(); + }; + + if (this.isCiConfigFile && this.glFeatures.schemaLinting) { + registerLocalSchema(); + } else { + if (this.CiSchemaExtension) { + this.editor.unuse(this.CiSchemaExtension); + } + registerExternalSchema(); + } }, updateEditor(data) { // Looks like our model wrapper `.dispose` causes the monaco editor to emit some position changes after diff --git a/app/assets/javascripts/jobs/components/job/empty_state.vue b/app/assets/javascripts/jobs/components/job/empty_state.vue index 65b9600e664..053d5a4e740 100644 --- a/app/assets/javascripts/jobs/components/job/empty_state.vue +++ b/app/assets/javascripts/jobs/components/job/empty_state.vue @@ -20,6 +20,14 @@ export default { type: String, required: true, }, + isRetryable: { + type: Boolean, + required: false, + }, + jobId: { + type: Number, + required: true, + }, title: { type: String, required: true, @@ -54,8 +62,8 @@ export default { }, }, computed: { - isGraphQL() { - return this.glFeatures?.graphqlJobApp; + showGraphQLManualVariablesForm() { + return this.glFeatures?.graphqlJobApp && this.isRetryable; }, shouldRenderManualVariables() { return this.playable && !this.scheduled; @@ -77,14 +85,18 @@ export default { <p v-if="content" data-testid="job-empty-state-content">{{ content }}</p> </div> - <template v-if="isGraphQL"> - <manual-variables-form v-if="shouldRenderManualVariables" :action="action" /> + <template v-if="showGraphQLManualVariablesForm"> + <manual-variables-form + v-if="shouldRenderManualVariables" + :job-id="jobId" + @hideManualVariablesForm="$emit('hideManualVariablesForm')" + /> </template> <template v-else> <legacy-manual-variables-form v-if="shouldRenderManualVariables" :action="action" /> </template> - <div class="text-content"> - <div v-if="action && !shouldRenderManualVariables" class="text-center"> + <div v-if="action && !shouldRenderManualVariables" class="text-content"> + <div class="text-center"> <gl-link :href="action.path" :data-method="action.method" diff --git a/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql b/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql new file mode 100644 index 00000000000..2b79892a072 --- /dev/null +++ b/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql @@ -0,0 +1,16 @@ +mutation retryJobWithVariables($id: CiBuildID!, $variables: [CiVariableInput!]) { + jobRetry(input: { id: $id, variables: $variables }) { + job { + id + manualVariables { + nodes { + id + key + value + } + } + webPath + } + errors + } +} diff --git a/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql b/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql new file mode 100644 index 00000000000..aaf1dec8e0f --- /dev/null +++ b/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql @@ -0,0 +1,17 @@ +query getJob($fullPath: ID!, $id: JobID!) { + project(fullPath: $fullPath) { + id + job(id: $id) { + id + manualJob + manualVariables { + nodes { + id + key + value + } + } + name + } + } +} diff --git a/app/assets/javascripts/jobs/components/job/job_app.vue b/app/assets/javascripts/jobs/components/job/job_app.vue index e5fbf77be1e..c6d900ef13e 100644 --- a/app/assets/javascripts/jobs/components/job/job_app.vue +++ b/app/assets/javascripts/jobs/components/job/job_app.vue @@ -72,6 +72,7 @@ export default { data() { return { searchResults: [], + showUpdateVariablesState: false, }; }, computed: { @@ -122,6 +123,10 @@ export default { return this.shouldRenderCalloutMessage && !this.hasUnmetPrerequisitesFailure; }, + isJobRetryable() { + return Boolean(this.job.retry_path); + }, + itemName() { return sprintf(__('Job %{jobName}'), { jobName: this.job.name }); }, @@ -169,10 +174,16 @@ export default { 'toggleScrollButtons', 'toggleScrollAnimation', ]), + onHideManualVariablesForm() { + this.showUpdateVariablesState = false; + }, onResize() { this.updateSidebar(); this.updateScroll(); }, + onUpdateVariables() { + this.showUpdateVariablesState = true; + }, updateSidebar() { const breakpoint = bp.getBreakpointSize(); if (breakpoint === 'xs' || breakpoint === 'sm') { @@ -272,14 +283,12 @@ export default { </div> <!-- job log --> <div - v-if="hasJobLog" + v-if="hasJobLog && !showUpdateVariablesState" class="build-log-container gl-relative" :class="{ 'gl-mt-3': !job.archived }" > <log-top-bar :class="{ - 'sidebar-expanded': isSidebarOpen, - 'sidebar-collapsed': !isSidebarOpen, 'has-archived-block': job.archived, }" :size="jobLogSize" @@ -300,14 +309,17 @@ export default { <!-- empty state --> <empty-state - v-if="!hasJobLog" + v-if="!hasJobLog || showUpdateVariablesState" :illustration-path="emptyStateIllustration.image" :illustration-size-class="emptyStateIllustration.size" + :is-retryable="isJobRetryable" + :job-id="job.id" :title="emptyStateTitle" :content="emptyStateIllustration.content" :action="emptyStateAction" :playable="job.playable" :scheduled="job.scheduled" + @hideManualVariablesForm="onHideManualVariablesForm()" /> <!-- EO empty state --> @@ -321,9 +333,9 @@ export default { 'right-sidebar-expanded': isSidebarOpen, 'right-sidebar-collapsed': !isSidebarOpen, }" - :erase-path="job.erase_path" :artifact-help-url="artifactHelpUrl" data-testid="job-sidebar" + @updateVariables="onUpdateVariables()" /> </div> </template> diff --git a/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue b/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue index 1898e02c94e..2b6b6f8e59e 100644 --- a/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue +++ b/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue @@ -6,6 +6,7 @@ import { GlButton, GlLink, GlSprintf, + GlTooltipDirective, } from '@gitlab/ui'; import { uniqueId } from 'lodash'; import { mapActions } from 'vuex'; @@ -13,7 +14,7 @@ import { helpPagePath } from '~/helpers/help_page_helper'; import { s__ } from '~/locale'; export default { - name: 'ManualVariablesForm', + name: 'LegacyManualVariablesForm', components: { GlFormInputGroup, GlInputGroupText, @@ -22,6 +23,9 @@ export default { GlLink, GlSprintf, }, + directives: { + GlTooltip: GlTooltipDirective, + }, props: { action: { type: Object, @@ -42,6 +46,7 @@ export default { value: 'value', }, i18n: { + clearInputs: s__('CiVariables|Clear inputs'), header: s__('CiVariables|Variables'), keyLabel: s__('CiVariables|Key'), valueLabel: s__('CiVariables|Value'), @@ -152,11 +157,13 @@ export default { <gl-button v-if="canRemove(index)" + v-gl-tooltip + :aria-label="$options.i18n.clearInputs" + :title="$options.i18n.clearInputs" class="gl-flex-grow-0 gl-flex-basis-0" category="tertiary" variant="danger" icon="clear" - :aria-label="__('Delete variable')" data-testid="delete-variable-btn" @click="deleteVariable(variable.id)" /> diff --git a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue index 2f97301979c..e8edc7fc56f 100644 --- a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue +++ b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue @@ -5,15 +5,23 @@ import { GlFormInput, GlButton, GlLink, + GlLoadingIcon, GlSprintf, + GlTooltipDirective, } from '@gitlab/ui'; -import { uniqueId } from 'lodash'; -import { mapActions } from 'vuex'; +import { cloneDeep, uniqueId } from 'lodash'; +import { fetchPolicies } from '~/lib/graphql'; +import { createAlert } from '~/flash'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { JOB_GRAPHQL_ERRORS, GRAPHQL_ID_TYPES } from '~/jobs/constants'; import { helpPagePath } from '~/helpers/help_page_helper'; +import { redirectTo } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; +import GetJob from './graphql/queries/get_job.query.graphql'; +import retryJobWithVariablesMutation from './graphql/mutations/job_retry_with_variables.mutation.graphql'; // This component is a port of ~/jobs/components/job/legacy_manual_variables_form.vue -// It is meant to fetch the job information via GraphQL instead of REST API. +// It is meant to fetch/update the job information via GraphQL instead of REST API. export default { name: 'ManualVariablesForm', @@ -23,63 +31,73 @@ export default { GlFormInput, GlButton, GlLink, + GlLoadingIcon, GlSprintf, }, - props: { - action: { - type: Object, - required: false, - default: null, - validator(value) { - return ( - value === null || - (Object.prototype.hasOwnProperty.call(value, 'path') && - Object.prototype.hasOwnProperty.call(value, 'method') && - Object.prototype.hasOwnProperty.call(value, 'button_title')) - ); + directives: { + GlTooltip: GlTooltipDirective, + }, + inject: ['projectPath'], + apollo: { + variables: { + query: GetJob, + variables() { + return { + fullPath: this.projectPath, + id: convertToGraphQLId(GRAPHQL_ID_TYPES.commitStatus, this.jobId), + }; + }, + fetchPolicy: fetchPolicies.CACHE_AND_NETWORK, + update(data) { + const jobVariables = cloneDeep(data?.project?.job?.manualVariables?.nodes); + return [...jobVariables.reverse(), ...this.variables]; + }, + error() { + createAlert({ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText }); }, }, }, + props: { + jobId: { + type: Number, + required: true, + }, + }, inputTypes: { key: 'key', value: 'value', }, i18n: { + clearInputs: s__('CiVariables|Clear inputs'), + formHelpText: s__( + 'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default', + ), header: s__('CiVariables|Variables'), keyLabel: s__('CiVariables|Key'), - valueLabel: s__('CiVariables|Value'), keyPlaceholder: s__('CiVariables|Input variable key'), + runAgainButtonText: s__('CiVariables|Run job again'), + valueLabel: s__('CiVariables|Value'), valuePlaceholder: s__('CiVariables|Input variable value'), - formHelpText: s__( - 'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default', - ), }, data() { return { + job: {}, variables: [ { - key: '', - secretValue: '', id: uniqueId(), + key: '', + value: '', }, ], - triggerBtnDisabled: false, + runAgainBtnDisabled: false, }; }, computed: { variableSettings() { return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' }); }, - preparedVariables() { - // we need to ensure no empty variables are passed to the API - // and secretValue should be snake_case when passed to the API - return this.variables - .filter((variable) => variable.key !== '') - .map(({ key, secretValue }) => ({ key, secret_value: secretValue })); - }, }, methods: { - ...mapActions(['triggerManualJob']), addEmptyVariable() { const lastVar = this.variables[this.variables.length - 1]; @@ -88,9 +106,9 @@ export default { } this.variables.push({ - key: '', - secret_value: '', id: uniqueId(), + key: '', + value: '', }); }, canRemove(index) { @@ -105,16 +123,45 @@ export default { inputRef(type, id) { return `${this.$options.inputTypes[type]}-${id}`; }, - trigger() { - this.triggerBtnDisabled = true; + navigateToRetriedJob(retryPath) { + redirectTo(retryPath); + }, + async retryJob() { + try { + // filtering out 'id' along with empty variables to send only key, value in the mutation. + // This will be removed in: https://gitlab.com/gitlab-org/gitlab/-/issues/377268 + const preparedVariables = this.variables + .filter((variable) => variable.key !== '') + .map(({ key, value }) => ({ key, value })); - this.triggerManualJob(this.preparedVariables); + const { data } = await this.$apollo.mutate({ + mutation: retryJobWithVariablesMutation, + variables: { + id: convertToGraphQLId(GRAPHQL_ID_TYPES.ciBuild, this.jobId), + // we need to ensure no empty variables are passed to the API + variables: preparedVariables, + }, + }); + if (data.jobRetry?.errors?.length) { + createAlert({ message: data.jobRetry.errors[0] }); + } else { + this.navigateToRetriedJob(data.jobRetry?.job?.webPath); + } + } catch (error) { + createAlert({ message: JOB_GRAPHQL_ERRORS.retryMutationErrorText }); + } + }, + runAgain() { + this.runAgainBtnDisabled = true; + + this.retryJob(); }, }, }; </script> <template> - <div class="row gl-justify-content-center"> + <gl-loading-icon v-if="$apollo.queries.variables.loading" class="gl-mt-9" size="lg" /> + <div v-else class="row gl-justify-content-center"> <div class="col-10" data-testid="manual-vars-form"> <label>{{ $options.i18n.header }}</label> @@ -147,7 +194,7 @@ export default { </template> <gl-form-input :ref="inputRef('value', variable.id)" - v-model="variable.secretValue" + v-model="variable.value" :placeholder="$options.i18n.valuePlaceholder" data-testid="ci-variable-value" /> @@ -155,11 +202,13 @@ export default { <gl-button v-if="canRemove(index)" + v-gl-tooltip + :aria-label="$options.i18n.clearInputs" + :title="$options.i18n.clearInputs" class="gl-flex-grow-0 gl-flex-basis-0" category="tertiary" variant="danger" icon="clear" - :aria-label="__('Delete variable')" data-testid="delete-variable-btn" @click="deleteVariable(variable.id)" /> @@ -180,14 +229,21 @@ export default { <div class="gl-display-flex gl-justify-content-center gl-mt-5"> <gl-button class="gl-mt-5" + :aria-label="__('Cancel')" + data-testid="cancel-btn" + @click="$emit('hideManualVariablesForm')" + >{{ __('Cancel') }}</gl-button + > + <gl-button + class="gl-mt-5" variant="confirm" category="primary" - :aria-label="__('Trigger manual job')" - :disabled="triggerBtnDisabled" - data-testid="trigger-manual-job-btn" - @click="trigger" + :aria-label="__('Run manual job again')" + :disabled="runAgainBtnDisabled" + data-testid="run-manual-job-btn" + @click="runAgain" > - {{ action.button_title }} + {{ $options.i18n.runAgainButtonText }} </gl-button> </div> </div> diff --git a/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue b/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue index dd620977f0c..65175df555a 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue @@ -1,19 +1,23 @@ <script> -import { GlButton, GlModalDirective } from '@gitlab/ui'; +import { GlButton, GlDropdown, GlDropdownItem, GlModalDirective } from '@gitlab/ui'; import { mapGetters } from 'vuex'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { JOB_SIDEBAR_COPY } from '~/jobs/constants'; export default { name: 'JobSidebarRetryButton', i18n: { - retryLabel: JOB_SIDEBAR_COPY.retry, + ...JOB_SIDEBAR_COPY, }, components: { GlButton, + GlDropdown, + GlDropdownItem, }, directives: { GlModal: GlModalDirective, }, + mixins: [glFeatureFlagsMixin()], props: { modalId: { type: String, @@ -23,9 +27,16 @@ export default { type: String, required: true, }, + isManualJob: { + type: Boolean, + required: true, + }, }, computed: { ...mapGetters(['hasForwardDeploymentFailure']), + showRetryDropdown() { + return this.glFeatures?.graphqlJobApp && this.isManualJob; + }, }, }; </script> @@ -33,17 +44,30 @@ export default { <gl-button v-if="hasForwardDeploymentFailure" v-gl-modal="modalId" - :aria-label="$options.i18n.retryLabel" + :aria-label="$options.i18n.retryJobLabel" category="primary" variant="confirm" icon="retry" data-testid="retry-job-button" /> - + <gl-dropdown + v-else-if="showRetryDropdown" + icon="retry" + category="primary" + :right="true" + variant="confirm" + > + <gl-dropdown-item :href="href" data-method="post"> + {{ $options.i18n.runAgainJobButtonLabel }} + </gl-dropdown-item> + <gl-dropdown-item @click="$emit('updateVariablesClicked')"> + {{ $options.i18n.updateVariables }} + </gl-dropdown-item> + </gl-dropdown> <gl-button v-else :href="href" - :aria-label="$options.i18n.retryLabel" + :aria-label="$options.i18n.retryJobLabel" category="primary" variant="confirm" icon="retry" diff --git a/app/assets/javascripts/jobs/components/job/sidebar/legacy_job_sidebar_retry_button.vue b/app/assets/javascripts/jobs/components/job/sidebar/legacy_job_sidebar_retry_button.vue new file mode 100644 index 00000000000..8a821b69f8c --- /dev/null +++ b/app/assets/javascripts/jobs/components/job/sidebar/legacy_job_sidebar_retry_button.vue @@ -0,0 +1,53 @@ +<script> +import { GlButton, GlModalDirective } from '@gitlab/ui'; +import { mapGetters } from 'vuex'; +import { JOB_SIDEBAR_COPY } from '~/jobs/constants'; + +export default { + name: 'LegacyJobSidebarRetryButton', + i18n: { + retryLabel: JOB_SIDEBAR_COPY.retryJobLabel, + }, + components: { + GlButton, + }, + directives: { + GlModal: GlModalDirective, + }, + props: { + modalId: { + type: String, + required: true, + }, + href: { + type: String, + required: true, + }, + }, + computed: { + ...mapGetters(['hasForwardDeploymentFailure']), + }, +}; +</script> +<template> + <gl-button + v-if="hasForwardDeploymentFailure" + v-gl-modal="modalId" + :aria-label="$options.i18n.retryLabel" + category="primary" + variant="confirm" + icon="retry" + data-testid="retry-job-button" + /> + + <gl-button + v-else + :href="href" + :aria-label="$options.i18n.retryLabel" + category="primary" + variant="confirm" + icon="retry" + data-method="post" + data-testid="retry-job-link" + /> +</template> diff --git a/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue b/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue index 64b497c3550..5bbb831a293 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue @@ -2,7 +2,7 @@ import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import { mapActions } from 'vuex'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; -import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants'; +import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId, PASSED_STATUS } from '~/jobs/constants'; import JobSidebarRetryButton from './job_sidebar_retry_button.vue'; export default { @@ -25,20 +25,15 @@ export default { required: true, default: () => ({}), }, - erasePath: { - type: String, - required: false, - default: null, - }, }, computed: { retryButtonCategory() { return this.job.status && this.job.recoverable ? 'primary' : 'secondary'; }, buttonTitle() { - return this.job.status && this.job.status.text === 'passed' + return this.job.status && this.job.status.text === PASSED_STATUS ? this.$options.i18n.runAgainJobButtonLabel - : this.$options.i18n.retryJobButtonLabel; + : this.$options.i18n.retryJobLabel; }, }, methods: { @@ -50,17 +45,15 @@ export default { <template> <div class="gl-py-5 gl-display-flex gl-align-items-center"> <tooltip-on-truncate :title="job.name" truncate-target="child" - ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate"> - {{ job.name }} - </h4> + ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate">{{ job.name }}</h4> </tooltip-on-truncate> <div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right"> <gl-button - v-if="erasePath" + v-if="job.erase_path" v-gl-tooltip.left :title="$options.i18n.eraseLogButtonLabel" :aria-label="$options.i18n.eraseLogButtonLabel" - :href="erasePath" + :href="job.erase_path" :data-confirm="$options.i18n.eraseLogConfirmText" class="gl-mr-2" data-testid="job-log-erase-link" @@ -76,6 +69,7 @@ export default { :category="retryButtonCategory" :href="job.retry_path" :modal-id="$options.forwardDeploymentFailureModalId" + :is-manual-job="false" variant="confirm" data-qa-selector="retry_button" data-testid="retry-button" diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue index aac6a0ad6d3..02c3d60557b 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue @@ -2,13 +2,13 @@ import { GlButton, GlIcon } from '@gitlab/ui'; import { isEmpty } from 'lodash'; import { mapActions, mapGetters, mapState } from 'vuex'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import ArtifactsBlock from './artifacts_block.vue'; import CommitBlock from './commit_block.vue'; import JobsContainer from './jobs_container.vue'; import JobRetryForwardDeploymentModal from './job_retry_forward_deployment_modal.vue'; import JobSidebarDetailsContainer from './sidebar_job_details_container.vue'; -import ArtifactsBlock from './artifacts_block.vue'; import LegacySidebarHeader from './legacy_sidebar_header.vue'; import SidebarHeader from './sidebar_header.vue'; import StagesDropdown from './stages_dropdown.vue'; @@ -41,11 +41,6 @@ export default { required: false, default: '', }, - erasePath: { - type: String, - required: false, - default: null, - }, }, computed: { ...mapGetters(['hasForwardDeploymentFailure']), @@ -89,8 +84,13 @@ export default { <aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix"> <div class="sidebar-container"> <div class="blocks-container"> - <sidebar-header v-if="isGraphQL" :erase-path="erasePath" :job="job" /> - <legacy-sidebar-header v-else :erase-path="erasePath" :job="job" /> + <sidebar-header + v-if="isGraphQL" + :rest-job="job" + :job-id="job.id" + @updateVariables="$emit('updateVariables')" + /> + <legacy-sidebar-header v-else :job="job" /> <div v-if="job.terminal_path || job.new_issue_path" class="gl-py-5" diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue index 523710598bf..c124f52ae79 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue @@ -1,8 +1,17 @@ <script> import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import { mapActions } from 'vuex'; +import { createAlert } from '~/flash'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; -import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants'; +import { + JOB_GRAPHQL_ERRORS, + GRAPHQL_ID_TYPES, + JOB_SIDEBAR_COPY, + forwardDeploymentFailureModalId, + PASSED_STATUS, +} from '~/jobs/constants'; +import GetJob from '../graphql/queries/get_job.query.graphql'; import JobSidebarRetryButton from './job_sidebar_retry_button.vue'; // This component is a port of ~/jobs/components/job/sidebar/legacy_sidebar_header.vue @@ -22,21 +31,58 @@ export default { JobSidebarRetryButton, TooltipOnTruncate, }, - props: { + inject: ['projectPath'], + apollo: { job: { + query: GetJob, + variables() { + return { + fullPath: this.projectPath, + id: convertToGraphQLId(GRAPHQL_ID_TYPES.commitStatus, this.jobId), + }; + }, + update(data) { + const { name, manualJob } = data?.project?.job || {}; + return { + name, + manualJob, + }; + }, + error() { + createAlert({ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText }); + }, + }, + }, + props: { + jobId: { + type: Number, + required: true, + }, + restJob: { type: Object, required: true, default: () => ({}), }, - erasePath: { - type: String, - required: false, - default: null, - }, + }, + data() { + return { + job: {}, + }; }, computed: { + buttonTitle() { + return this.restJob.status?.text === PASSED_STATUS + ? this.$options.i18n.runAgainJobButtonLabel + : this.$options.i18n.retryJobLabel; + }, + canShowJobRetryButton() { + return this.restJob.retry_path && !this.$apollo.queries.job.loading; + }, + isManualJob() { + return this.job?.manualJob; + }, retryButtonCategory() { - return this.job.status && this.job.recoverable ? 'primary' : 'secondary'; + return this.restJob.status && this.restJob.recoverable ? 'primary' : 'secondary'; }, }, methods: { @@ -48,17 +94,15 @@ export default { <template> <div class="gl-py-5 gl-display-flex gl-align-items-center"> <tooltip-on-truncate :title="job.name" truncate-target="child" - ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate"> - {{ job.name }} - </h4> + ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate" data-testid="job-name">{{ job.name }}</h4> </tooltip-on-truncate> <div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right"> <gl-button - v-if="erasePath" + v-if="restJob.erase_path" v-gl-tooltip.left :title="$options.i18n.eraseLogButtonLabel" :aria-label="$options.i18n.eraseLogButtonLabel" - :href="erasePath" + :href="restJob.erase_path" :data-confirm="$options.i18n.eraseLogConfirmText" class="gl-mr-2" data-testid="job-log-erase-link" @@ -67,23 +111,25 @@ export default { icon="remove" /> <job-sidebar-retry-button - v-if="job.retry_path" + v-if="canShowJobRetryButton" v-gl-tooltip.left - :title="$options.i18n.retryJobButtonLabel" - :aria-label="$options.i18n.retryJobButtonLabel" + :title="buttonTitle" + :aria-label="buttonTitle" + :is-manual-job="isManualJob" :category="retryButtonCategory" - :href="job.retry_path" + :href="restJob.retry_path" :modal-id="$options.forwardDeploymentFailureModalId" variant="confirm" data-qa-selector="retry_button" data-testid="retry-button" + @updateVariablesClicked="$emit('updateVariables')" /> <gl-button - v-if="job.cancel_path" + v-if="restJob.cancel_path" v-gl-tooltip.left :title="$options.i18n.cancelJobButtonLabel" :aria-label="$options.i18n.cancelJobButtonLabel" - :href="job.cancel_path" + :href="restJob.cancel_path" variant="danger" icon="cancel" data-method="post" diff --git a/app/assets/javascripts/jobs/constants.js b/app/assets/javascripts/jobs/constants.js index e9475994e8b..405aea11181 100644 --- a/app/assets/javascripts/jobs/constants.js +++ b/app/assets/javascripts/jobs/constants.js @@ -5,6 +5,11 @@ const moreInfo = __('More information'); export const forwardDeploymentFailureModalId = 'forward-deployment-failure'; +export const GRAPHQL_ID_TYPES = { + commitStatus: 'CommitStatus', + ciBuild: 'Ci::Build', +}; + export const JOB_SIDEBAR_COPY = { cancel, cancelJobButtonLabel: s__('Job|Cancel'), @@ -12,10 +17,15 @@ export const JOB_SIDEBAR_COPY = { eraseLogButtonLabel: s__('Job|Erase job log and artifacts'), eraseLogConfirmText: s__('Job|Are you sure you want to erase this job log and artifacts?'), newIssue: __('New issue'), - retry: __('Retry'), - retryJobButtonLabel: s__('Job|Retry'), + retryJobLabel: s__('Job|Retry'), toggleSidebar: __('Toggle Sidebar'), runAgainJobButtonLabel: s__('Job|Run again'), + updateVariables: s__('Job|Update CI/CD variables'), +}; + +export const JOB_GRAPHQL_ERRORS = { + retryMutationErrorText: __('There was an error running the job. Please try again.'), + jobQueryErrorText: __('There was an error fetching the job.'), }; export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = { @@ -31,3 +41,4 @@ export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = { }; export const SUCCESS_STATUS = 'SUCCESS'; +export const PASSED_STATUS = 'passed'; diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js index 9dd47f4046c..44bb1ffb1bc 100644 --- a/app/assets/javascripts/jobs/index.js +++ b/app/assets/javascripts/jobs/index.js @@ -1,10 +1,17 @@ import { GlToast } from '@gitlab/ui'; import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; import JobApp from './components/job/job_app.vue'; import createStore from './store'; +Vue.use(VueApollo); Vue.use(GlToast); +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); + const initializeJobPage = (element) => { const store = createStore(); @@ -26,11 +33,13 @@ const initializeJobPage = (element) => { return new Vue({ el: element, + apolloProvider, store, components: { JobApp, }, provide: { + projectPath, retryOutdatedJobDocsUrl, }, render(createElement) { diff --git a/app/assets/javascripts/lib/dompurify.js b/app/assets/javascripts/lib/dompurify.js index 27760e483aa..5372f6555d2 100644 --- a/app/assets/javascripts/lib/dompurify.js +++ b/app/assets/javascripts/lib/dompurify.js @@ -18,7 +18,7 @@ export const defaultConfig = { 'data-disable', 'data-turbo', ], - FORBID_TAGS: ['style', 'mstyle'], + FORBID_TAGS: ['style', 'mstyle', 'form'], ALLOW_UNKNOWN_PROTOCOLS: true, }; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 19318d87731..dd24e3fcb5d 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -338,11 +338,6 @@ color: $gl-text-color; } -.commit .gpg-popover-help-link { - display: block; - color: $link-color; -} - .add-review-item { .gl-tab-nav-item { height: 100%; diff --git a/app/finders/git_refs_finder.rb b/app/finders/git_refs_finder.rb index dbe0060d8ae..0492dd9934f 100644 --- a/app/finders/git_refs_finder.rb +++ b/app/finders/git_refs_finder.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class GitRefsFinder + include Gitlab::Utils::StrongMemoize + def initialize(repository, params = {}) @repository = repository @params = params @@ -10,44 +12,28 @@ class GitRefsFinder attr_reader :repository, :params - def search - @params[:search].to_s.presence - end - - def sort - @params[:sort].to_s.presence || 'name' - end - def by_search(refs) return refs unless search - case search - when ->(v) { v.starts_with?('^') } - filter_refs_with_prefix(refs, search.slice(1..-1)) - when ->(v) { v.ends_with?('$') } - filter_refs_with_suffix(refs, search.chop) - else - matches = filter_refs_by_name(refs, search) - set_exact_match_as_first_result(matches, search) - end - end - - def filter_refs_with_prefix(refs, prefix) - prefix = prefix.downcase + matches = filter_refs(refs, search) + return matches if regex_search? - refs.select { |ref| ref.name.downcase.starts_with?(prefix) } + set_exact_match_as_first_result(matches, search) end - def filter_refs_with_suffix(refs, suffix) - suffix = suffix.downcase - - refs.select { |ref| ref.name.downcase.ends_with?(suffix) } + def search + @params[:search].to_s.presence end + strong_memoize_attr :search - def filter_refs_by_name(refs, term) - term = term.downcase + def sort + @params[:sort].to_s.presence || 'name' + end - refs.select { |ref| ref.name.downcase.include?(term) } + def filter_refs(refs, term) + regex_string = Regexp.quote(term.downcase) + regex_string = unescape_regex_operators(regex_string) if regex_search? + refs.select { |ref| /#{regex_string}/ === ref.name.downcase } end def set_exact_match_as_first_result(matches, term) @@ -59,4 +45,13 @@ class GitRefsFinder def find_exact_match_index(matches, term) matches.index { |ref| ref.name.casecmp(term) == 0 } end + + def regex_search? + Regexp.union('^', '$', '*') === search + end + strong_memoize_attr :regex_search?, :regex_search + + def unescape_regex_operators(regex_string) + regex_string.sub('\^', '^').gsub('\*', '.*?').sub('\$', '$') + end end diff --git a/app/graphql/types/commit_signatures/gpg_signature_type.rb b/app/graphql/types/commit_signatures/gpg_signature_type.rb index 2a845fff3e2..3baf2d9d21d 100644 --- a/app/graphql/types/commit_signatures/gpg_signature_type.rb +++ b/app/graphql/types/commit_signatures/gpg_signature_type.rb @@ -11,6 +11,7 @@ module Types authorize :download_code field :user, Types::UserType, null: true, + method: :signed_by_user, description: 'User associated with the key.' field :gpg_key_user_name, GraphQL::Types::String, diff --git a/app/graphql/types/commit_signatures/x509_signature_type.rb b/app/graphql/types/commit_signatures/x509_signature_type.rb index 9ac96dbc015..2d58c3d5b5d 100644 --- a/app/graphql/types/commit_signatures/x509_signature_type.rb +++ b/app/graphql/types/commit_signatures/x509_signature_type.rb @@ -11,6 +11,7 @@ module Types authorize :download_code field :user, Types::UserType, null: true, + method: :signed_by_user, calls_gitaly: true, description: 'User associated with the key.' diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index e05adc5cd0e..6868fed711e 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -227,7 +227,7 @@ module DiffHelper end def conflicts(allow_tree_conflicts: false) - return unless merge_request.cannot_be_merged? + return unless merge_request.cannot_be_merged? && merge_request.source_branch_exists? && merge_request.target_branch_exists? conflicts_service = MergeRequests::Conflicts::ListService.new(merge_request, allow_tree_conflicts: allow_tree_conflicts) # rubocop:disable CodeReuse/ServiceClass diff --git a/app/helpers/x509_helper.rb b/app/helpers/x509_helper.rb index 1a9dbefceef..599be0b91f7 100644 --- a/app/helpers/x509_helper.rb +++ b/app/helpers/x509_helper.rb @@ -16,8 +16,4 @@ module X509Helper rescue StandardError {} end - - def x509_signature?(sig) - sig.is_a?(CommitSignatures::X509CommitSignature) || sig.is_a?(Gitlab::X509::Signature) - end end diff --git a/app/models/commit.rb b/app/models/commit.rb index 54de45ebba7..5175842e5de 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -359,6 +359,10 @@ class Commit end def has_signature? + if signature_type == :SSH && !ssh_signatures_enabled? + return false + end + signature_type && signature_type != :NONE end @@ -378,6 +382,10 @@ class Commit @signature_type ||= raw_signature_type || :NONE end + def ssh_signatures_enabled? + Feature.enabled?(:ssh_commit_signatures, project) + end + def signature strong_memoize(:signature) do case signature_type @@ -385,6 +393,8 @@ class Commit gpg_commit.signature when :X509 Gitlab::X509::Commit.new(self).signature + when :SSH + Gitlab::Ssh::Commit.new(self).signature if ssh_signatures_enabled? else nil end diff --git a/app/models/commit_signatures/gpg_signature.rb b/app/models/commit_signatures/gpg_signature.rb index 2ae59853520..a9e8ca2dd33 100644 --- a/app/models/commit_signatures/gpg_signature.rb +++ b/app/models/commit_signatures/gpg_signature.rb @@ -2,6 +2,7 @@ module CommitSignatures class GpgSignature < ApplicationRecord include CommitSignature + include SignatureType sha_attribute :gpg_key_primary_keyid @@ -10,6 +11,14 @@ module CommitSignatures validates :gpg_key_primary_keyid, presence: true + def signed_by_user + gpg_key&.user + end + + def type + :gpg + end + def self.with_key_and_subkeys(gpg_key) subkey_ids = gpg_key.subkeys.pluck(:id) diff --git a/app/models/commit_signatures/ssh_signature.rb b/app/models/commit_signatures/ssh_signature.rb index 7a8d0653fcd..1e64e2b2978 100644 --- a/app/models/commit_signatures/ssh_signature.rb +++ b/app/models/commit_signatures/ssh_signature.rb @@ -3,7 +3,16 @@ module CommitSignatures class SshSignature < ApplicationRecord include CommitSignature + include SignatureType belongs_to :key, optional: true + + def type + :ssh + end + + def signed_by_user + key&.user + end end end diff --git a/app/models/commit_signatures/x509_commit_signature.rb b/app/models/commit_signatures/x509_commit_signature.rb index 2cbb331dd7e..4edbc147502 100644 --- a/app/models/commit_signatures/x509_commit_signature.rb +++ b/app/models/commit_signatures/x509_commit_signature.rb @@ -2,15 +2,24 @@ module CommitSignatures class X509CommitSignature < ApplicationRecord include CommitSignature + include SignatureType belongs_to :x509_certificate, class_name: 'X509Certificate', foreign_key: 'x509_certificate_id', optional: false validates :x509_certificate_id, presence: true + def type + :x509 + end + def x509_commit return unless commit Gitlab::X509::Commit.new(commit) end + + def signed_by_user + commit&.committer + end end end diff --git a/app/models/concerns/commit_signature.rb b/app/models/concerns/commit_signature.rb index 5bdfa9a2966..7f1fbbefd94 100644 --- a/app/models/concerns/commit_signature.rb +++ b/app/models/concerns/commit_signature.rb @@ -44,7 +44,7 @@ module CommitSignature project.commit(commit_sha) end - def user - commit.committer + def signed_by_user + raise NoMethodError, 'must implement `signed_by_user` method' end end diff --git a/app/models/concerns/signature_type.rb b/app/models/concerns/signature_type.rb new file mode 100644 index 00000000000..804f42b6f72 --- /dev/null +++ b/app/models/concerns/signature_type.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module SignatureType + TYPES = %i[gpg ssh x509].freeze + + def type + raise NoMethodError, 'must implement `type` method' + end + + TYPES.each do |type| + define_method("#{type}?") { self.type == type } + end +end diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb index 7de56c037ed..71dd9501648 100644 --- a/app/services/git/branch_hooks_service.rb +++ b/app/services/git/branch_hooks_service.rb @@ -164,16 +164,27 @@ module Git end end - def unsigned_x509_shas(commits) - CommitSignatures::X509CommitSignature.unsigned_commit_shas(commits.map(&:sha)) + def signature_types + types = [ + ::CommitSignatures::GpgSignature, + ::CommitSignatures::X509CommitSignature + ] + + types.push(::CommitSignatures::SshSignature) if Feature.enabled?(:ssh_commit_signatures, project) + + types end - def unsigned_gpg_shas(commits) - CommitSignatures::GpgSignature.unsigned_commit_shas(commits.map(&:sha)) + def unsigned_commit_shas(commits) + commit_shas = commits.map(&:sha) + + signature_types + .map { |signature| signature.unsigned_commit_shas(commit_shas) } + .reduce(&:&) end def enqueue_update_signatures - unsigned = unsigned_x509_shas(limited_commits) & unsigned_gpg_shas(limited_commits) + unsigned = unsigned_commit_shas(limited_commits) return if unsigned.empty? signable = Gitlab::Git::Commit.shas_with_signatures(project.repository, unsigned) diff --git a/app/views/projects/commit/_signature.html.haml b/app/views/projects/commit/_signature.html.haml index 978d83bf2b4..c6f1e51049e 100644 --- a/app/views/projects/commit/_signature.html.haml +++ b/app/views/projects/commit/_signature.html.haml @@ -1,3 +1,3 @@ - if signature - - uri = "projects/commit/#{'x509/' if x509_signature?(signature)}" + - uri = "projects/commit/#{'x509/' if signature.x509?}" = render partial: "#{uri}#{signature.verification_status}_signature_badge", locals: { signature: signature } diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index fb30bfc2953..ad6b524c01b 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -17,18 +17,23 @@ - content = capture do - if show_user .clearfix - - uri_signature_badge_user = "projects/commit/#{'x509/' if x509_signature?(signature)}signature_badge_user" + - uri_signature_badge_user = "projects/commit/#{'x509/' if signature.x509?}signature_badge_user" = render partial: "#{uri_signature_badge_user}", locals: { signature: signature } - - if x509_signature?(signature) + - if signature.x509? = render partial: "projects/commit/x509/certificate_details", locals: { signature: signature } - = link_to(_('Learn more about X.509 signed commits'), help_page_path('user/project/repository/x509_signed_commits/index.md'), class: 'gpg-popover-help-link') + = link_to(_('Learn more about X.509 signed commits'), help_page_path('user/project/repository/x509_signed_commits/index.md'), class: 'gl-link gl-display-block') + - elsif ::Feature.enabled?(:ssh_commit_signatures, signature.project) && signature.ssh? + = _('SSH key fingerprint:') + %span.gl-font-monospace= signature.key&.fingerprint_sha256 || _('Unknown') + + = link_to(_('Learn about signing commits with SSH keys.'), help_page_path('user/project/repository/ssh_signed_commits/index.md'), class: 'gl-link gl-display-block') - else = _('GPG Key ID:') - %span.monospace= signature.gpg_key_primary_keyid + %span.gl-font-monospace= signature.gpg_key_primary_keyid - = link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link gl-display-block') + = link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gl-link gl-display-block') %a{ role: 'button', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } } = label diff --git a/app/views/projects/commit/_signature_badge_user.html.haml b/app/views/projects/commit/_signature_badge_user.html.haml index b20198e76db..656adef6a72 100644 --- a/app/views/projects/commit/_signature_badge_user.html.haml +++ b/app/views/projects/commit/_signature_badge_user.html.haml @@ -1,7 +1,4 @@ -- gpg_key = signature.gpg_key -- user = gpg_key&.user -- user_name = signature.gpg_key_user_name -- user_email = signature.gpg_key_user_email +- user = signature.signed_by_user - if user = link_to user_path(user), class: 'gpg-popover-user-link' do @@ -11,11 +8,14 @@ %div %strong= user.name %div= user.to_reference -- else - = mail_to user_email do - %div - = user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32) +- elsif signature.gpg? # SSH signatures do not have an email embedded in them + - user_name = signature.gpg_key_user_name + - user_email = signature.gpg_key_user_email + - if user_name && user_email + = mail_to user_email do + %div + = user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32) - %div - %strong= user_name - %div= user_email + %div + %strong= user_name + %div= user_email diff --git a/app/views/projects/settings/operations/_alert_management.html.haml b/app/views/projects/settings/operations/_alert_management.html.haml index d80f1e4597c..7433e81c11c 100644 --- a/app/views/projects/settings/operations/_alert_management.html.haml +++ b/app/views/projects/settings/operations/_alert_management.html.haml @@ -3,7 +3,7 @@ - add_page_specific_style 'page_bundles/alert_management_settings' - add_page_specific_style 'page_bundles/incident_management_list' -%section.settings.no-animate#js-alert-management-settings{ class: ('expanded' if expanded) } +%section.settings.no-animate#js-alert-management-settings{ class: ('expanded' if expanded), data: { qa_selector: 'alerts_settings_content' } } .settings-header %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('Alerts') |