diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-01-14 15:10:54 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-01-14 15:10:54 +0300 |
commit | 18873553de98259d0558157f78198b38ddd02b31 (patch) | |
tree | cbdf0261e8a72975b7044fe7df8a1439c93ed4d5 /app | |
parent | 0ea7b5c8a3f7afaae6b03279af56cd880d538bd7 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
47 files changed, 784 insertions, 208 deletions
diff --git a/app/assets/javascripts/jira_connect/components/app.vue b/app/assets/javascripts/jira_connect/components/app.vue index 275a66d5956..e08918a6720 100644 --- a/app/assets/javascripts/jira_connect/components/app.vue +++ b/app/assets/javascripts/jira_connect/components/app.vue @@ -1,9 +1,13 @@ <script> import { mapState } from 'vuex'; +import { GlAlert } from '@gitlab/ui'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { name: 'JiraConnectApp', + components: { + GlAlert, + }, mixins: [glFeatureFlagsMixin()], computed: { ...mapState(['errorMessage']), @@ -16,6 +20,12 @@ export default { <template> <div> + <gl-alert v-if="errorMessage" class="gl-mb-6" variant="danger" :dismissible="false"> + {{ errorMessage }} + </gl-alert> + + <h1>GitLab for Jira Configuration</h1> + <div v-if="showNewUi"> <h3 data-testid="new-jira-connect-ui-heading">{{ s__('Integrations|Linked namespaces') }}</h3> </div> diff --git a/app/assets/javascripts/jira_connect/index.js b/app/assets/javascripts/jira_connect/index.js index 05f13de6f53..0dbcb778a6c 100644 --- a/app/assets/javascripts/jira_connect/index.js +++ b/app/assets/javascripts/jira_connect/index.js @@ -23,11 +23,9 @@ const initJiraFormHandlers = () => { }; const reqFailed = (res, fallbackErrorMessage) => { - const { responseJSON: { error = fallbackErrorMessage } = {} } = res || {}; + const { error = fallbackErrorMessage } = res || {}; store.commit(SET_ERROR_MESSAGE, error); - // eslint-disable-next-line no-alert - alert(error); }; if (typeof AP.getLocation === 'function') { diff --git a/app/assets/javascripts/lib/utils/grammar.js b/app/assets/javascripts/lib/utils/grammar.js index f68767b9acd..6d6361d19b6 100644 --- a/app/assets/javascripts/lib/utils/grammar.js +++ b/app/assets/javascripts/lib/utils/grammar.js @@ -16,12 +16,12 @@ import { sprintf, s__ } from '~/locale'; * * @param {String[]} items */ -export const toNounSeriesText = (items) => { +export const toNounSeriesText = (items, { onlyCommas = false } = {}) => { if (items.length === 0) { return ''; } else if (items.length === 1) { return sprintf(s__(`nounSeries|%{item}`), { item: items[0] }, false); - } else if (items.length === 2) { + } else if (items.length === 2 && !onlyCommas) { return sprintf( s__('nounSeries|%{firstItem} and %{lastItem}'), { @@ -33,7 +33,7 @@ export const toNounSeriesText = (items) => { } return items.reduce((item, nextItem, idx) => - idx === items.length - 1 + idx === items.length - 1 && !onlyCommas ? sprintf(s__('nounSeries|%{item}, and %{lastItem}'), { item, lastItem: nextItem }, false) : sprintf(s__('nounSeries|%{item}, %{nextItem}'), { item, nextItem }, false), ); diff --git a/app/assets/javascripts/notes/components/comment_field_layout.vue b/app/assets/javascripts/notes/components/comment_field_layout.vue new file mode 100644 index 00000000000..aaf64702ffd --- /dev/null +++ b/app/assets/javascripts/notes/components/comment_field_layout.vue @@ -0,0 +1,69 @@ +<script> +import EmailParticipantsWarning from './email_participants_warning.vue'; +import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'; + +const DEFAULT_NOTEABLE_TYPE = 'Issue'; + +export default { + components: { + EmailParticipantsWarning, + NoteableWarning, + }, + props: { + noteableData: { + type: Object, + required: true, + }, + noteableType: { + type: String, + required: false, + default: DEFAULT_NOTEABLE_TYPE, + }, + withAlertContainer: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + isLocked() { + return Boolean(this.noteableData.discussion_locked); + }, + isConfidential() { + return Boolean(this.noteableData.confidential); + }, + hasWarning() { + return this.isConfidential || this.isLocked; + }, + emailParticipants() { + return this.noteableData.issue_email_participants?.map(({ email }) => email) || []; + }, + }, +}; +</script> +<template> + <div + class="comment-warning-wrapper gl-border-solid gl-border-1 gl-rounded-base gl-border-gray-100" + > + <div + v-if="withAlertContainer" + class="error-alert" + data-testid="comment-field-alert-container" + ></div> + <noteable-warning + v-if="hasWarning" + class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-100 gl-rounded-base gl-rounded-bottom-left-none gl-rounded-bottom-right-none" + :is-locked="isLocked" + :is-confidential="isConfidential" + :noteable-type="noteableType" + :locked-noteable-docs-path="noteableData.locked_discussion_docs_path" + :confidential-noteable-docs-path="noteableData.confidential_issues_docs_path" + /> + <slot></slot> + <email-participants-warning + v-if="emailParticipants.length" + class="gl-border-t-1 gl-border-t-solid gl-border-t-gray-100 gl-rounded-base gl-rounded-top-left-none! gl-rounded-top-right-none!" + :emails="emailParticipants" + /> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 67e9b8b2c19..111af977ec5 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -17,18 +17,17 @@ import { import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import * as constants from '../constants'; import eventHub from '../event_hub'; -import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'; import markdownField from '~/vue_shared/components/markdown/field.vue'; import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import noteSignedOutWidget from './note_signed_out_widget.vue'; import discussionLockedWidget from './discussion_locked_widget.vue'; import issuableStateMixin from '../mixins/issuable_state'; +import CommentFieldLayout from './comment_field_layout.vue'; export default { name: 'CommentForm', components: { - NoteableWarning, noteSignedOutWidget, discussionLockedWidget, markdownField, @@ -36,6 +35,7 @@ export default { GlButton, TimelineEntryItem, GlIcon, + CommentFieldLayout, }, mixins: [glFeatureFlagsMixin(), issuableStateMixin], props: { @@ -287,6 +287,9 @@ export default { Autosize.update(this.$refs.textarea); }); }, + hasEmailParticipants() { + return this.getNoteableData.issue_email_participants?.length; + }, }, }; </script> @@ -309,46 +312,41 @@ export default { </div> <div class="timeline-content timeline-content-form"> <form ref="commentForm" class="new-note common-note-form gfm-form js-main-target-form"> - <div class="error-alert"></div> - - <noteable-warning - v-if="hasWarning(getNoteableData)" - :is-locked="isLocked(getNoteableData)" - :is-confidential="isConfidential(getNoteableData)" + <comment-field-layout + :with-alert-container="true" + :noteable-data="getNoteableData" :noteable-type="noteableType" - :locked-noteable-docs-path="lockedIssueDocsPath" - :confidential-noteable-docs-path="confidentialIssueDocsPath" - /> - - <markdown-field - ref="markdownField" - :is-submitting="isSubmitting" - :markdown-preview-path="markdownPreviewPath" - :markdown-docs-path="markdownDocsPath" - :quick-actions-docs-path="quickActionsDocsPath" - :add-spacing-classes="false" - :textarea-value="note" > - <textarea - id="note-body" - ref="textarea" - slot="textarea" - v-model="note" - dir="auto" - :disabled="isSubmitting" - name="note[note]" - class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area" - data-qa-selector="comment_field" - data-testid="comment-field" - :data-supports-quick-actions="!glFeatures.tributeAutocomplete" - :aria-label="__('Description')" - :placeholder="__('Write a comment or drag your files here…')" - @keydown.up="editCurrentUserLastNote()" - @keydown.meta.enter="handleSave()" - @keydown.ctrl.enter="handleSave()" - ></textarea> - </markdown-field> - + <markdown-field + ref="markdownField" + :is-submitting="isSubmitting" + :markdown-preview-path="markdownPreviewPath" + :markdown-docs-path="markdownDocsPath" + :quick-actions-docs-path="quickActionsDocsPath" + :add-spacing-classes="false" + :textarea-value="note" + > + <template #textarea> + <textarea + id="note-body" + ref="textarea" + v-model="note" + dir="auto" + :disabled="isSubmitting" + name="note[note]" + class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area" + data-qa-selector="comment_field" + data-testid="comment-field" + :data-supports-quick-actions="!glFeatures.tributeAutocomplete" + :aria-label="__('Description')" + :placeholder="__('Write a comment or drag your files here…')" + @keydown.up="editCurrentUserLastNote()" + @keydown.meta.enter="handleSave()" + @keydown.ctrl.enter="handleSave()" + ></textarea> + </template> + </markdown-field> + </comment-field-layout> <div class="note-form-actions"> <div class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" diff --git a/app/assets/javascripts/notes/components/email_participants_warning.vue b/app/assets/javascripts/notes/components/email_participants_warning.vue new file mode 100644 index 00000000000..bb1ff58120a --- /dev/null +++ b/app/assets/javascripts/notes/components/email_participants_warning.vue @@ -0,0 +1,70 @@ +<script> +import { GlSprintf } from '@gitlab/ui'; +import { s__, sprintf } from '~/locale'; +import { toNounSeriesText } from '~/lib/utils/grammar'; + +export default { + components: { + GlSprintf, + }, + props: { + emails: { + type: Array, + required: true, + }, + numberOfLessParticipants: { + type: Number, + required: false, + default: 3, + }, + }, + data() { + return { + isShowingMoreParticipants: false, + }; + }, + computed: { + title() { + return this.moreParticipantsAvailable + ? toNounSeriesText(this.lessParticipants, { onlyCommas: true }) + : toNounSeriesText(this.emails); + }, + lessParticipants() { + return this.emails.slice(0, this.numberOfLessParticipants); + }, + moreLabel() { + return sprintf(s__('EmailParticipantsWarning|and %{moreCount} more'), { + moreCount: this.emails.length - this.numberOfLessParticipants, + }); + }, + moreParticipantsAvailable() { + return !this.isShowingMoreParticipants && this.emails.length > this.numberOfLessParticipants; + }, + message() { + return this.moreParticipantsAvailable + ? s__('EmailParticipantsWarning|%{emails}, %{andMore} will be notified of your comment.') + : s__('EmailParticipantsWarning|%{emails} will be notified of your comment.'); + }, + }, + methods: { + showMoreParticipants() { + this.isShowingMoreParticipants = true; + }, + }, +}; +</script> + +<template> + <div class="issuable-note-warning" data-testid="email-participants-warning"> + <gl-sprintf :message="message"> + <template #andMore> + <button type="button" class="btn-transparent btn-link" @click="showMoreParticipants"> + {{ moreLabel }} + </button> + </template> + <template #emails> + <span>{{ title }}</span> + </template> + </gl-sprintf> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 47202d0e241..9acb837c27f 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -3,19 +3,19 @@ import { mapGetters, mapActions, mapState } from 'vuex'; import { mergeUrlParams } from '~/lib/utils/url_utility'; import eventHub from '../event_hub'; -import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'; import markdownField from '~/vue_shared/components/markdown/field.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import issuableStateMixin from '../mixins/issuable_state'; import resolvable from '../mixins/resolvable'; import { __, sprintf } from '~/locale'; import { getDraft, updateDraft } from '~/lib/utils/autosave'; +import CommentFieldLayout from './comment_field_layout.vue'; export default { name: 'NoteForm', components: { - NoteableWarning, markdownField, + CommentFieldLayout, }, mixins: [glFeatureFlagsMixin(), issuableStateMixin, resolvable], props: { @@ -303,6 +303,9 @@ export default { this.$emit('handleFormUpdateAddToReview', this.updatedNoteBody, shouldResolve); }, + hasEmailParticipants() { + return this.getNoteableData.issue_email_participants?.length; + }, }, }; </script> @@ -316,46 +319,41 @@ export default { ></div> <div class="flash-container timeline-content"></div> <form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form"> - <noteable-warning - v-if="hasWarning(getNoteableData)" - :is-locked="isLocked(getNoteableData)" - :is-confidential="isConfidential(getNoteableData)" - :locked-noteable-docs-path="lockedIssueDocsPath" - :confidential-noteable-docs-path="confidentialIssueDocsPath" - /> - - <markdown-field - :markdown-preview-path="markdownPreviewPath" - :markdown-docs-path="markdownDocsPath" - :quick-actions-docs-path="quickActionsDocsPath" - :line="line" - :note="discussionNote" - :can-suggest="canSuggest" - :add-spacing-classes="false" - :help-page-path="helpPagePath" - :show-suggest-popover="showSuggestPopover" - :textarea-value="updatedNoteBody" - @handleSuggestDismissed="() => $emit('handleSuggestDismissed')" - > - <textarea - id="note_note" - ref="textarea" - slot="textarea" - v-model="updatedNoteBody" - :data-supports-quick-actions="!isEditing && !glFeatures.tributeAutocomplete" - name="note[note]" - class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form" - data-qa-selector="reply_field" - dir="auto" - :aria-label="__('Description')" - :placeholder="__('Write a comment or drag your files here…')" - @keydown.meta.enter="handleKeySubmit()" - @keydown.ctrl.enter="handleKeySubmit()" - @keydown.exact.up="editMyLastNote()" - @keydown.exact.esc="cancelHandler(true)" - @input="onInput" - ></textarea> - </markdown-field> + <comment-field-layout :noteable-data="getNoteableData"> + <markdown-field + :markdown-preview-path="markdownPreviewPath" + :markdown-docs-path="markdownDocsPath" + :quick-actions-docs-path="quickActionsDocsPath" + :line="line" + :note="discussionNote" + :can-suggest="canSuggest" + :add-spacing-classes="false" + :help-page-path="helpPagePath" + :show-suggest-popover="showSuggestPopover" + :textarea-value="updatedNoteBody" + @handleSuggestDismissed="() => $emit('handleSuggestDismissed')" + > + <template #textarea> + <textarea + id="note_note" + ref="textarea" + v-model="updatedNoteBody" + :data-supports-quick-actions="!isEditing && !glFeatures.tributeAutocomplete" + name="note[note]" + class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form" + data-qa-selector="reply_field" + dir="auto" + :aria-label="__('Description')" + :placeholder="__('Write a comment or drag your files here…')" + @keydown.meta.enter="handleKeySubmit()" + @keydown.ctrl.enter="handleKeySubmit()" + @keydown.exact.up="editMyLastNote()" + @keydown.exact.esc="cancelHandler(true)" + @input="onInput" + ></textarea> + </template> + </markdown-field> + </comment-field-layout> <div class="note-form-actions clearfix"> <template v-if="showBatchCommentsActions"> <p v-if="showResolveDiscussionToggle"> diff --git a/app/assets/javascripts/notes/mixins/issuable_state.js b/app/assets/javascripts/notes/mixins/issuable_state.js index 0ca8c8c98a3..52b67764b70 100644 --- a/app/assets/javascripts/notes/mixins/issuable_state.js +++ b/app/assets/javascripts/notes/mixins/issuable_state.js @@ -12,21 +12,10 @@ export default { lockedIssueDocsPath() { return this.getNoteableDataByProp('locked_discussion_docs_path'); }, - confidentialIssueDocsPath() { - return this.getNoteableDataByProp('confidential_issues_docs_path'); - }, }, methods: { - isConfidential(issue) { - return Boolean(issue.confidential); - }, - isLocked(issue) { return Boolean(issue.discussion_locked); }, - - hasWarning(issue) { - return this.isConfidential(issue) || this.isLocked(issue); - }, }, }; diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue index e2529613844..ef2be2a5fba 100644 --- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue +++ b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue @@ -44,13 +44,21 @@ export default { <template> <div data-testid="ci-lint-value"> - <pre v-if="scripts.beforeScript.show" data-testid="ci-lint-before-script">{{ - scripts.beforeScript.content - }}</pre> - <pre v-if="scripts.script.show" data-testid="ci-lint-script">{{ scripts.script.content }}</pre> - <pre v-if="scripts.afterScript.show" data-testid="ci-lint-after-script">{{ - scripts.afterScript.content + <pre + v-if="scripts.beforeScript.show" + class="gl-white-space-pre-wrap" + data-testid="ci-lint-before-script" + >{{ scripts.beforeScript.content }}</pre + > + <pre v-if="scripts.script.show" class="gl-white-space-pre-wrap" data-testid="ci-lint-script">{{ + scripts.script.content }}</pre> + <pre + v-if="scripts.afterScript.show" + class="gl-white-space-pre-wrap" + data-testid="ci-lint-after-script" + >{{ scripts.afterScript.content }}</pre + > <ul class="gl-list-style-none gl-pl-0 gl-mb-0"> <li v-if="tagList"> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 5958d198be2..96a674e342f 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,5 +1,6 @@ <script> import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue'; +import LinksLayer from '../graph_shared/links_layer.vue'; import LinkedPipelinesColumn from './linked_pipelines_column.vue'; import StageColumnComponent from './stage_column_component.vue'; import { DOWNSTREAM, MAIN, UPSTREAM } from './constants'; @@ -8,6 +9,7 @@ import { reportToSentry } from './utils'; export default { name: 'PipelineGraph', components: { + LinksLayer, LinkedGraphWrapper, LinkedPipelinesColumn, StageColumnComponent, @@ -32,9 +34,15 @@ export default { DOWNSTREAM, UPSTREAM, }, + CONTAINER_REF: 'PIPELINE_LINKS_CONTAINER_REF', + BASE_CONTAINER_ID: 'pipeline-links-container', data() { return { hoveredJobName: '', + measurements: { + width: 0, + height: 0, + }, pipelineExpanded: { jobName: '', expanded: false, @@ -42,6 +50,9 @@ export default { }; }, computed: { + containerId() { + return `${this.$options.BASE_CONTAINER_ID}-${this.pipeline.id}`; + }, downstreamPipelines() { return this.hasDownstreamPipelines ? this.pipeline.downstream : []; }, @@ -54,12 +65,13 @@ export default { hasUpstreamPipelines() { return Boolean(this.pipeline?.upstream?.length > 0); }, - // The two show checks prevent upstream / downstream from showing redundant linked columns + // The show downstream check prevents showing redundant linked columns showDownstreamPipelines() { return ( this.hasDownstreamPipelines && this.type !== this.$options.pipelineTypeConstants.UPSTREAM ); }, + // The show upstream check prevents showing redundant linked columns showUpstreamPipelines() { return ( this.hasUpstreamPipelines && this.type !== this.$options.pipelineTypeConstants.DOWNSTREAM @@ -72,7 +84,19 @@ export default { errorCaptured(err, _vm, info) { reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); }, + mounted() { + this.measurements = this.getMeasurements(); + }, methods: { + getMeasurements() { + return { + width: this.$refs[this.containerId].scrollWidth, + height: this.$refs[this.containerId].scrollHeight, + }; + }, + onError(errorType) { + this.$emit('error', errorType); + }, setJob(jobName) { this.hoveredJobName = jobName; }, @@ -88,43 +112,57 @@ export default { <template> <div class="js-pipeline-graph"> <div + :id="containerId" + :ref="containerId" class="gl-pipeline-min-h gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap" :class="{ 'gl-py-5': !isLinkedPipeline }" > - <linked-graph-wrapper> - <template #upstream> - <linked-pipelines-column - v-if="showUpstreamPipelines" - :linked-pipelines="upstreamPipelines" - :column-title="__('Upstream')" - :type="$options.pipelineTypeConstants.UPSTREAM" - @error="emit('error', errorType)" - /> - </template> - <template #main> - <stage-column-component - v-for="stage in graph" - :key="stage.name" - :title="stage.name" - :groups="stage.groups" - :action="stage.status.action" - :job-hovered="hoveredJobName" - :pipeline-expanded="pipelineExpanded" - @refreshPipelineGraph="$emit('refreshPipelineGraph')" - /> - </template> - <template #downstream> - <linked-pipelines-column - v-if="showDownstreamPipelines" - :linked-pipelines="downstreamPipelines" - :column-title="__('Downstream')" - :type="$options.pipelineTypeConstants.DOWNSTREAM" - @downstreamHovered="setJob" - @pipelineExpandToggle="togglePipelineExpanded" - @error="emit('error', errorType)" - /> - </template> - </linked-graph-wrapper> + <links-layer + :pipeline-data="graph" + :pipeline-id="pipeline.id" + :container-id="containerId" + :container-measurements="measurements" + :highlighted-job="hoveredJobName" + default-link-color="gl-stroke-transparent" + @error="onError" + > + <linked-graph-wrapper> + <template #upstream> + <linked-pipelines-column + v-if="showUpstreamPipelines" + :linked-pipelines="upstreamPipelines" + :column-title="__('Upstream')" + :type="$options.pipelineTypeConstants.UPSTREAM" + @error="onError" + /> + </template> + <template #main> + <stage-column-component + v-for="stage in graph" + :key="stage.name" + :title="stage.name" + :groups="stage.groups" + :action="stage.status.action" + :job-hovered="hoveredJobName" + :pipeline-expanded="pipelineExpanded" + :pipeline-id="pipeline.id" + @refreshPipelineGraph="$emit('refreshPipelineGraph')" + @jobHover="setJob" + /> + </template> + <template #downstream> + <linked-pipelines-column + v-if="showDownstreamPipelines" + :linked-pipelines="downstreamPipelines" + :column-title="__('Downstream')" + :type="$options.pipelineTypeConstants.DOWNSTREAM" + @downstreamHovered="setJob" + @pipelineExpandToggle="togglePipelineExpanded" + @error="onError" + /> + </template> + </linked-graph-wrapper> + </links-layer> </div> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue index b6e756ceaba..08d6162aeb8 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue @@ -23,8 +23,16 @@ export default { type: Object, required: true, }, + pipelineId: { + type: Number, + required: false, + default: -1, + }, }, computed: { + computedJobId() { + return this.pipelineId > -1 ? `${this.group.name}-${this.pipelineId}` : ''; + }, tooltipText() { const { name, status } = this.group; return `${name} - ${status.label}`; @@ -41,7 +49,7 @@ export default { }; </script> <template> - <div class="ci-job-dropdown-container dropdown dropright"> + <div :id="computedJobId" class="ci-job-dropdown-container dropdown dropright"> <button v-gl-tooltip.hover="{ boundary: 'viewport' }" :title="tooltipText" diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index 4f414cbb31f..8262d728a24 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -74,6 +74,11 @@ export default { required: false, default: () => ({}), }, + pipelineId: { + type: Number, + required: false, + default: -1, + }, }, computed: { boundary() { @@ -85,6 +90,9 @@ export default { hasDetails() { return accessValue(this.dataMethod, 'hasDetails', this.status); }, + computedJobId() { + return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : ''; + }, status() { return this.job && this.job.status ? this.job.status : {}; }, @@ -146,6 +154,7 @@ export default { </script> <template> <div + :id="computedJobId" class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between" data-qa-selector="job_item_container" > diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 45d183cce97..e65ae318952 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -24,6 +24,10 @@ export default { type: Array, required: true, }, + pipelineId: { + type: Number, + required: true, + }, action: { type: Object, required: false, @@ -94,16 +98,19 @@ export default { :key="getGroupId(group)" data-testid="stage-column-group" class="gl-relative gl-mb-3 gl-white-space-normal gl-pipeline-job-width" + @mouseenter="$emit('jobHover', group.name)" + @mouseleave="$emit('jobHover', '')" > <job-item v-if="group.size === 1" :job="group.jobs[0]" :job-hovered="jobHovered" :pipeline-expanded="pipelineExpanded" + :pipeline-id="pipelineId" css-class-job-name="gl-build-content" @pipelineActionRequestComplete="$emit('refreshPipelineGraph')" /> - <job-group-dropdown v-else :group="group" /> + <job-group-dropdown v-else :group="group" :pipeline-id="pipelineId" /> </div> </template> </main-graph-wrapper> diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js index b550c599fda..fe59af12011 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js +++ b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js @@ -1,5 +1,7 @@ import * as d3 from 'd3'; -import { createUniqueLinkId } from '../../utils'; + +export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobName}`; + /** * This function expects its first argument data structure * to be the same shaped as the one generated by `parseData`, @@ -7,12 +9,11 @@ import { createUniqueLinkId } from '../../utils'; * we find the nodes in the graph, calculate their coordinates and * trace the lines that represent the needs of each job. * @param {Object} nodeDict - Resulting object of `parseData` with nodes and links - * @param {Object} jobs - An object where each key is the job name that contains the job data - * @param {ref} svg - Reference to the svg we draw in + * @param {String} containerID - Id for the svg the links will be draw in * @returns {Array} Links that contain all the information about them */ -export const generateLinksData = ({ links }, containerID) => { +export const generateLinksData = ({ links }, containerID, modifier = '') => { const containerEl = document.getElementById(containerID); return links.map((link) => { const path = d3.path(); @@ -20,8 +21,11 @@ export const generateLinksData = ({ links }, containerID) => { const sourceId = link.source; const targetId = link.target; - const sourceNodeEl = document.getElementById(sourceId); - const targetNodeEl = document.getElementById(targetId); + const modifiedSourceId = `${sourceId}${modifier}`; + const modifiedTargetId = `${targetId}${modifier}`; + + const sourceNodeEl = document.getElementById(modifiedSourceId); + const targetNodeEl = document.getElementById(modifiedTargetId); const sourceNodeCoordinates = sourceNodeEl.getBoundingClientRect(); const targetNodeCoordinates = targetNodeEl.getBoundingClientRect(); @@ -35,11 +39,11 @@ export const generateLinksData = ({ links }, containerID) => { // from the total to make sure it's aligned properly. We then make the line // positioned in the center of the job node by adding half the height // of the job pill. - const paddingLeft = Number( - window.getComputedStyle(containerEl, null).getPropertyValue('padding-left').replace('px', ''), + const paddingLeft = parseFloat( + window.getComputedStyle(containerEl, null).getPropertyValue('padding-left'), ); - const paddingTop = Number( - window.getComputedStyle(containerEl, null).getPropertyValue('padding-top').replace('px', ''), + const paddingTop = parseFloat( + window.getComputedStyle(containerEl, null).getPropertyValue('padding-top'), ); const sourceNodeX = sourceNodeCoordinates.right - containerCoordinates.x - paddingLeft; diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue new file mode 100644 index 00000000000..480cb032e11 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue @@ -0,0 +1,137 @@ +<script> +import { isEmpty } from 'lodash'; +import { DRAW_FAILURE } from '../../constants'; +import { createJobsHash, generateJobNeedsDict } from '../../utils'; +import { parseData } from '../parsing_utils'; +import { generateLinksData } from './drawing_utils'; + +export default { + name: 'LinksInner', + STROKE_WIDTH: 2, + props: { + containerId: { + type: String, + required: true, + }, + containerMeasurements: { + type: Object, + required: true, + }, + pipelineId: { + type: Number, + required: true, + }, + pipelineData: { + type: Array, + required: true, + }, + defaultLinkColor: { + type: String, + required: false, + default: 'gl-stroke-gray-200', + }, + highlightedJob: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + links: [], + needsObject: null, + }; + }, + computed: { + hasHighlightedJob() { + return Boolean(this.highlightedJob); + }, + isPipelineDataEmpty() { + return isEmpty(this.pipelineData); + }, + highlightedJobs() { + // If you are hovering on a job, then the jobs we want to highlight are: + // The job you are currently hovering + all of its needs. + return this.hasHighlightedJob + ? [this.highlightedJob, ...this.needsObject[this.highlightedJob]] + : []; + }, + highlightedLinks() { + // If you are hovering on a job, then the links we want to highlight are: + // All the links whose `source` and `target` are highlighted jobs. + if (this.hasHighlightedJob) { + const filteredLinks = this.links.filter((link) => { + return ( + this.highlightedJobs.includes(link.source) && this.highlightedJobs.includes(link.target) + ); + }); + + return filteredLinks.map((link) => link.ref); + } + + return []; + }, + viewBox() { + return [0, 0, this.containerMeasurements.width, this.containerMeasurements.height]; + }, + }, + watch: { + highlightedJob() { + // On first hover, generate the needs reference + if (!this.needsObject) { + const jobs = createJobsHash(this.pipelineData); + this.needsObject = generateJobNeedsDict(jobs) ?? {}; + } + }, + }, + mounted() { + if (!isEmpty(this.pipelineData)) { + this.prepareLinkData(); + } + }, + methods: { + isLinkHighlighted(linkRef) { + return this.highlightedLinks.includes(linkRef); + }, + prepareLinkData() { + try { + const arrayOfJobs = this.pipelineData.flatMap(({ groups }) => groups); + const parsedData = parseData(arrayOfJobs); + this.links = generateLinksData(parsedData, this.containerId, `-${this.pipelineId}`); + } catch { + this.$emit('error', DRAW_FAILURE); + } + }, + getLinkClasses(link) { + return [ + this.isLinkHighlighted(link.ref) ? 'gl-stroke-blue-400' : this.defaultLinkColor, + { 'gl-opacity-3': this.hasHighlightedJob && !this.isLinkHighlighted(link.ref) }, + ]; + }, + }, +}; +</script> +<template> + <div class="gl-display-flex gl-relative"> + <svg + id="link-svg" + class="gl-absolute" + :viewBox="viewBox" + :width="`${containerMeasurements.width}px`" + :height="`${containerMeasurements.height}px`" + > + <template> + <path + v-for="link in links" + :key="link.path" + :ref="link.ref" + :d="link.path" + class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease" + :class="getLinkClasses(link)" + :stroke-width="$options.STROKE_WIDTH" + /> + </template> + </svg> + <slot></slot> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue new file mode 100644 index 00000000000..0993892a574 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue @@ -0,0 +1,86 @@ +<script> +import { GlAlert } from '@gitlab/ui'; +import { __ } from '~/locale'; +import LinksInner from './links_inner.vue'; + +export default { + name: 'LinksLayer', + components: { + GlAlert, + LinksInner, + }, + MAX_GROUPS: 200, + props: { + containerMeasurements: { + type: Object, + required: true, + }, + pipelineData: { + type: Array, + required: true, + }, + }, + data() { + return { + alertDismissed: false, + showLinksOverride: false, + }; + }, + i18n: { + showLinksAnyways: __('Show links anyways'), + tooManyJobs: __( + 'This graph has a large number of jobs and showing the links between them may have performance implications.', + ), + }, + computed: { + containerZero() { + return !this.containerMeasurements.width || !this.containerMeasurements.height; + }, + numGroups() { + return this.pipelineData.reduce((acc, { groups }) => { + return acc + Number(groups.length); + }, 0); + }, + showAlert() { + return !this.showLinkedLayers && !this.alertDismissed; + }, + showLinkedLayers() { + return ( + !this.containerZero && (this.showLinksOverride || this.numGroups < this.$options.MAX_GROUPS) + ); + }, + }, + methods: { + dismissAlert() { + this.alertDismissed = true; + }, + overrideShowLinks() { + this.dismissAlert(); + this.showLinksOverride = true; + }, + }, +}; +</script> +<template> + <links-inner + v-if="showLinkedLayers" + :container-measurements="containerMeasurements" + :pipeline-data="pipelineData" + v-bind="$attrs" + v-on="$listeners" + > + <slot></slot> + </links-inner> + <div v-else> + <gl-alert + v-if="showAlert" + class="gl-w-max-content gl-ml-4" + :primary-button-text="$options.i18n.showLinksAnyways" + @primaryAction="overrideShowLinks" + @dismiss="dismissAlert" + > + {{ $options.i18n.tooManyJobs }} + </gl-alert> + <slot></slot> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue index 6c957d09e46..8636808b69e 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue @@ -1,9 +1,9 @@ <script> import { GlAlert } from '@gitlab/ui'; import { __ } from '~/locale'; +import { generateLinksData } from '../graph_shared/drawing_utils'; import JobPill from './job_pill.vue'; import StagePill from './stage_pill.vue'; -import { generateLinksData } from './drawing_utils'; import { parseData } from '../parsing_utils'; import { DRAW_FAILURE, DEFAULT, INVALID_CI_CONFIG, EMPTY_PIPELINE_DATA } from '../../constants'; import { createJobsHash, generateJobNeedsDict } from '../../utils'; diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 8c9040dce04..133608b9801 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -158,7 +158,7 @@ export default async function () { ); const { pipelineProjectPath, pipelineIid } = dataset; - createPipelinesDetailApp(SELECTORS.PIPELINE_DETAILS, pipelineProjectPath, pipelineIid); + createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, pipelineProjectPath, pipelineIid); } catch { Flash(__('An error occurred while loading the pipeline.')); } diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js index 54fcd5502de..50bb23b7e63 100644 --- a/app/assets/javascripts/pipelines/utils.js +++ b/app/assets/javascripts/pipelines/utils.js @@ -6,8 +6,6 @@ export const validateParams = (params) => { return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val); }; -export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobName}`; - /** * This function takes the stages array and transform it * into a hash where each key is a job name and the job data diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue index 67be76604a3..24386c90954 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue @@ -111,6 +111,6 @@ export default { <template> <div ref="markdownPreview" class="md-previewer" data-testid="md-previewer"> <gl-skeleton-loading v-if="isLoading" /> - <div v-else class="md" v-html="previewContent"></div> + <div v-else class="md gl-ml-auto gl-mr-auto" v-html="previewContent"></div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue index 4dac72df2b5..a4c5ca28494 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue @@ -256,7 +256,7 @@ export default { return { ...filter, value: { - data: stripQuotes(valueString), + data: typeof valueString === 'string' ? stripQuotes(valueString) : valueString, operator: filter.value.operator, }, }; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 7ba9236b833..e3d02d01496 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -99,6 +99,10 @@ line-height: $list-text-height; color: $gl-text-color-secondary; + @include media-breakpoint-down(xs) { + padding-top: $gl-padding-6; + } + span { margin-right: 15px; } diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 15cc10d1532..f6b9473d235 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -179,6 +179,10 @@ $ide-commit-header-height: 48px; overflow: auto; padding: $gl-padding; background-color: var(--ide-empty-state-background, transparent); + + .md { + max-width: $limited-layout-width; + } } .file-container { diff --git a/app/assets/stylesheets/page_bundles/jira_connect.scss b/app/assets/stylesheets/page_bundles/jira_connect.scss index ff989b474ad..81e9b04b18e 100644 --- a/app/assets/stylesheets/page_bundles/jira_connect.scss +++ b/app/assets/stylesheets/page_bundles/jira_connect.scss @@ -1,11 +1,14 @@ @import 'mixins_and_variables_and_functions'; -// We should only import styles that we actually use. -// @import '@gitlab/ui/src/scss/gitlab_ui'; - +/** +NOTE: We should only import styles that we actually use. +Ex: + @import '@gitlab/ui/src/scss/gitlab_ui'; +*/ @import '@gitlab/ui/src/scss/bootstrap'; @import 'bootstrap-vue/src/index'; @import '@gitlab/ui/src/scss/utilities'; +@import '@gitlab/ui/src/components/base/alert/alert'; $atlaskit-border-color: #dfe1e6; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 0c24ea9ccc6..254ad96bb57 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -100,8 +100,6 @@ color: $orange-600; background-color: $orange-50; border-radius: $border-radius-default $border-radius-default 0 0; - border: 1px solid $border-gray-normal; - border-bottom: 0; padding: 3px 12px; margin: auto; align-items: center; @@ -454,3 +452,9 @@ table { .markdown-selector { color: $blue-600; } + +.comment-warning-wrapper { + .md-area { + border: 0; + } +} diff --git a/app/graphql/resolvers/package_details_resolver.rb b/app/graphql/resolvers/package_details_resolver.rb new file mode 100644 index 00000000000..dcf4430e55f --- /dev/null +++ b/app/graphql/resolvers/package_details_resolver.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Resolvers + # No return types defined because they can be different. + # rubocop: disable Graphql/ResolverType + class PackageDetailsResolver < BaseResolver + argument :id, ::Types::GlobalIDType[::Packages::Package], + required: true, + description: 'The global ID of the package.' + + def resolve(id:) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = ::Types::GlobalIDType[::Packages::Package].coerce_isolated_input(id) + GitlabSchema.find_by_gid(id) + end + end +end diff --git a/app/graphql/resolvers/packages_resolver.rb b/app/graphql/resolvers/packages_resolver.rb index 519fb87183e..d19099e94d4 100644 --- a/app/graphql/resolvers/packages_resolver.rb +++ b/app/graphql/resolvers/packages_resolver.rb @@ -2,7 +2,7 @@ module Resolvers class PackagesResolver < BaseResolver - type Types::PackageType, null: true + type Types::Packages::PackageType, null: true def resolve(**args) return unless packages_available? diff --git a/app/graphql/types/package_type.rb b/app/graphql/types/package_type.rb deleted file mode 100644 index 0604bf827a5..00000000000 --- a/app/graphql/types/package_type.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module Types - class PackageType < BaseObject - graphql_name 'Package' - description 'Represents a package' - authorize :read_package - - field :id, GraphQL::ID_TYPE, null: false, description: 'The ID of the package' - field :name, GraphQL::STRING_TYPE, null: false, description: 'The name of the package' - field :created_at, Types::TimeType, null: false, description: 'The created date' - field :updated_at, Types::TimeType, null: false, description: 'The update date' - field :version, GraphQL::STRING_TYPE, null: true, description: 'The version of the package' - field :package_type, Types::PackageTypeEnum, null: false, description: 'The type of the package' - end -end diff --git a/app/graphql/types/package_type_enum.rb b/app/graphql/types/package_type_enum.rb deleted file mode 100644 index 6f50c166da3..00000000000 --- a/app/graphql/types/package_type_enum.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Types - class PackageTypeEnum < BaseEnum - PACKAGE_TYPE_NAMES = { - pypi: 'PyPI', - npm: 'NPM' - }.freeze - - ::Packages::Package.package_types.keys.each do |package_type| - type_name = PACKAGE_TYPE_NAMES.fetch(package_type.to_sym, package_type.capitalize) - value package_type.to_s.upcase, "Packages from the #{type_name} package manager", value: package_type.to_s - end - end -end diff --git a/app/graphql/types/packages/composer/details_type.rb b/app/graphql/types/packages/composer/details_type.rb new file mode 100644 index 00000000000..8c6845a6fb3 --- /dev/null +++ b/app/graphql/types/packages/composer/details_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + module Packages + module Composer + class DetailsType < Types::Packages::PackageType + graphql_name 'PackageComposerDetails' + description 'Details of a Composer package' + + authorize :read_package + + field :composer_metadatum, Types::Packages::Composer::MetadatumType, null: false, description: 'The Composer metadatum.' + end + end + end +end diff --git a/app/graphql/types/packages/composer/json_type.rb b/app/graphql/types/packages/composer/json_type.rb new file mode 100644 index 00000000000..b7aa32f0170 --- /dev/null +++ b/app/graphql/types/packages/composer/json_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + module Packages + module Composer + # rubocop: disable Graphql/AuthorizeTypes + class JsonType < BaseObject + graphql_name 'PackageComposerJsonType' + description 'Represents a composer JSON file' + + field :name, GraphQL::STRING_TYPE, null: true, description: 'The name set in the Composer JSON file.' + field :type, GraphQL::STRING_TYPE, null: true, description: 'The type set in the Composer JSON file.' + field :license, GraphQL::STRING_TYPE, null: true, description: 'The license set in the Composer JSON file.' + field :version, GraphQL::STRING_TYPE, null: true, description: 'The version set in the Composer JSON file.' + end + end + end +end diff --git a/app/graphql/types/packages/composer/metadatum_type.rb b/app/graphql/types/packages/composer/metadatum_type.rb new file mode 100644 index 00000000000..a97818b1fb8 --- /dev/null +++ b/app/graphql/types/packages/composer/metadatum_type.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + module Packages + module Composer + class MetadatumType < BaseObject + graphql_name 'PackageComposerMetadatumType' + description 'Composer metadatum' + + authorize :read_package + + field :target_sha, GraphQL::STRING_TYPE, null: false, description: 'Target SHA of the package.' + field :composer_json, Types::Packages::Composer::JsonType, null: false, description: 'Data of the Composer JSON file.' + end + end + end +end diff --git a/app/graphql/types/packages/package_tag_type.rb b/app/graphql/types/packages/package_tag_type.rb new file mode 100644 index 00000000000..a05ce03da67 --- /dev/null +++ b/app/graphql/types/packages/package_tag_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + module Packages + class PackageTagType < BaseObject + graphql_name 'PackageTag' + description 'Represents a package tag' + authorize :read_package + + field :id, GraphQL::ID_TYPE, null: false, description: 'The ID of the tag.' + field :name, GraphQL::STRING_TYPE, null: false, description: 'The name of the tag.' + field :created_at, Types::TimeType, null: false, description: 'The created date.' + field :updated_at, Types::TimeType, null: false, description: 'The updated date.' + end + end +end diff --git a/app/graphql/types/packages/package_type.rb b/app/graphql/types/packages/package_type.rb new file mode 100644 index 00000000000..b13d16e91c6 --- /dev/null +++ b/app/graphql/types/packages/package_type.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Types + module Packages + class PackageType < BaseObject + graphql_name 'Package' + description 'Represents a package in the Package Registry' + + authorize :read_package + + field :id, GraphQL::ID_TYPE, null: false, description: 'The ID of the package.' + field :name, GraphQL::STRING_TYPE, null: false, description: 'The name of the package.' + field :created_at, Types::TimeType, null: false, description: 'The created date.' + field :updated_at, Types::TimeType, null: false, description: 'The updated date.' + field :version, GraphQL::STRING_TYPE, null: true, description: 'The version of the package.' + field :package_type, Types::Packages::PackageTypeEnum, null: false, description: 'The type of the package.' + field :tags, Types::Packages::PackageTagType.connection_type, null: true, description: 'The package tags.' + field :project, Types::ProjectType, null: false, description: 'Project where the package is stored.' + field :pipelines, Types::Ci::PipelineType.connection_type, null: true, description: 'Pipelines that built the package.' + field :versions, Types::Packages::PackageType.connection_type, null: true, description: 'The other versions of the package.' + + def project + Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find + end + end + end +end diff --git a/app/graphql/types/packages/package_type_enum.rb b/app/graphql/types/packages/package_type_enum.rb new file mode 100644 index 00000000000..9713c9d49b1 --- /dev/null +++ b/app/graphql/types/packages/package_type_enum.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + module Packages + class PackageTypeEnum < BaseEnum + PACKAGE_TYPE_NAMES = { + pypi: 'PyPI', + npm: 'NPM' + }.freeze + + ::Packages::Package.package_types.keys.each do |package_type| + type_name = PACKAGE_TYPE_NAMES.fetch(package_type.to_sym, package_type.capitalize) + value package_type.to_s.upcase, "Packages from the #{type_name} package manager", value: package_type.to_s + end + end + end +end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 195da01f41f..f66d8926a9f 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -175,7 +175,7 @@ module Types description: 'A single issue of the project', resolver: Resolvers::IssuesResolver.single - field :packages, Types::PackageType.connection_type, null: true, + field :packages, Types::Packages::PackageType.connection_type, null: true, description: 'Packages of the project', resolver: Resolvers::PackagesResolver diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 05bb371088c..0e0c060f374 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -58,6 +58,11 @@ module Types argument :id, ::Types::GlobalIDType[::ContainerRepository], required: true, description: 'The global ID of the container repository' end + field :package_composer_details, Types::Packages::Composer::DetailsType, + null: true, + description: 'Find a composer package', + resolver: Resolvers::PackageDetailsResolver + field :user, Types::UserType, null: true, description: 'Find a user', diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index f1f99771b40..ef3891908f7 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -210,7 +210,8 @@ module Ci project: downstream_project, source: :pipeline, target_revision: { - ref: target_ref || downstream_project.default_branch + ref: target_ref || downstream_project.default_branch, + variables_attributes: downstream_variables }, execute_params: { ignore_skip_ci: true, @@ -230,7 +231,8 @@ module Ci checkout_sha: parent_pipeline.sha, before: parent_pipeline.before_sha, source_sha: parent_pipeline.source_sha, - target_sha: parent_pipeline.target_sha + target_sha: parent_pipeline.target_sha, + variables_attributes: downstream_variables }, execute_params: { ignore_skip_ci: true, diff --git a/app/models/project.rb b/app/models/project.rb index b9911fd308b..b03dcde9f3f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -412,6 +412,8 @@ class Project < ApplicationRecord delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci delegate :forward_deployment_enabled, :forward_deployment_enabled=, :forward_deployment_enabled?, to: :ci_cd_settings, prefix: :ci delegate :keep_latest_artifact, :keep_latest_artifact=, :keep_latest_artifact?, to: :ci_cd_settings, prefix: :ci + delegate :restrict_user_defined_variables, :restrict_user_defined_variables=, :restrict_user_defined_variables?, + to: :ci_cd_settings delegate :actual_limits, :actual_plan_name, to: :namespace, allow_nil: true delegate :allow_merge_on_skipped_pipeline, :allow_merge_on_skipped_pipeline?, :allow_merge_on_skipped_pipeline=, :has_confluence?, :allow_editing_commit_messages?, diff --git a/app/policies/packages/composer/metadatum_policy.rb b/app/policies/packages/composer/metadatum_policy.rb new file mode 100644 index 00000000000..66bac31f48f --- /dev/null +++ b/app/policies/packages/composer/metadatum_policy.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +module Packages + module Composer + class MetadatumPolicy < BasePolicy + delegate { @subject.package } + end + end +end diff --git a/app/policies/packages/tag_policy.rb b/app/policies/packages/tag_policy.rb new file mode 100644 index 00000000000..84bad30470a --- /dev/null +++ b/app/policies/packages/tag_policy.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +module Packages + class TagPolicy < BasePolicy + delegate { @subject.package } + end +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index e9bdf88fa5e..03cb53f55be 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -135,6 +135,10 @@ class ProjectPolicy < BasePolicy ::Feature.enabled?(:build_service_proxy, @subject) end + condition(:user_defined_variables_allowed) do + !@subject.restrict_user_defined_variables? + end + with_scope :subject condition(:packages_disabled) { !@subject.packages_enabled } @@ -616,6 +620,10 @@ class ProjectPolicy < BasePolicy enable :admin_resource_access_tokens end + rule { user_defined_variables_allowed | can?(:maintainer_access) }.policy do + enable :set_pipeline_variables + end + private def user_is_user? diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb index 7c12e0956f3..647a73495f8 100644 --- a/app/serializers/issue_entity.rb +++ b/app/serializers/issue_entity.rb @@ -71,6 +71,10 @@ class IssueEntity < IssuableEntity expose :archived_project_docs_path, if: -> (issue) { issue.project.archived? } do |issue| help_page_path('user/project/settings/index.md', anchor: 'archiving-a-project') end + + expose :issue_email_participants do |issue| + issue.issue_email_participants.map { |x| { email: x.email } } + end end IssueEntity.prepend_if_ee('::EE::IssueEntity') diff --git a/app/services/ci/create_downstream_pipeline_service.rb b/app/services/ci/create_downstream_pipeline_service.rb index 86d0cf079fc..629d85b041f 100644 --- a/app/services/ci/create_downstream_pipeline_service.rb +++ b/app/services/ci/create_downstream_pipeline_service.rb @@ -33,9 +33,7 @@ module Ci pipeline_params.fetch(:target_revision)) downstream_pipeline = service.execute( - pipeline_params.fetch(:source), **pipeline_params[:execute_params]) do |pipeline| - pipeline.variables.build(@bridge.downstream_variables) - end + pipeline_params.fetch(:source), **pipeline_params[:execute_params]) downstream_pipeline.tap do |pipeline| update_bridge_status!(@bridge, pipeline) diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb index d9f41b7040e..a31f5e9056e 100644 --- a/app/services/ci/pipeline_trigger_service.rb +++ b/app/services/ci/pipeline_trigger_service.rb @@ -21,10 +21,10 @@ module Ci # this check is to not leak the presence of the project if user cannot read it return unless trigger.project == project - pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref]) + pipeline = Ci::CreatePipelineService + .new(project, trigger.owner, ref: params[:ref], variables_attributes: variables) .execute(:trigger, ignore_skip_ci: true) do |pipeline| pipeline.trigger_requests.build(trigger: trigger) - pipeline.variables.build(variables) end if pipeline.persisted? @@ -44,7 +44,8 @@ module Ci # this check is to not leak the presence of the project if user cannot read it return unless can?(job.user, :read_project, project) - pipeline = Ci::CreatePipelineService.new(project, job.user, ref: params[:ref]) + pipeline = Ci::CreatePipelineService + .new(project, job.user, ref: params[:ref], variables_attributes: variables) .execute(:pipeline, ignore_skip_ci: true) do |pipeline| source = job.sourced_pipelines.build( source_pipeline: job.pipeline, @@ -53,7 +54,6 @@ module Ci project: project) pipeline.source_pipeline = source - pipeline.variables.build(variables) end if pipeline.persisted? diff --git a/app/services/ci/play_build_service.rb b/app/services/ci/play_build_service.rb index 6adeca624a8..ebc980a9053 100644 --- a/app/services/ci/play_build_service.rb +++ b/app/services/ci/play_build_service.rb @@ -5,6 +5,10 @@ module Ci def execute(build, job_variables_attributes = nil) raise Gitlab::Access::AccessDeniedError unless can?(current_user, :play_job, build) + if job_variables_attributes.present? && !can?(current_user, :set_pipeline_variables, project) + raise Gitlab::Access::AccessDeniedError + end + # Try to enqueue the build, otherwise create a duplicate. # if build.enqueue diff --git a/app/views/jira_connect/subscriptions/index.html.haml b/app/views/jira_connect/subscriptions/index.html.haml index 90a92f5c6d5..30dad19be37 100644 --- a/app/views/jira_connect/subscriptions/index.html.haml +++ b/app/views/jira_connect/subscriptions/index.html.haml @@ -9,10 +9,9 @@ = link_to _('Sign in to GitLab'), jira_connect_users_path, target: '_blank', rel: 'noopener noreferrer', class: 'js-jira-connect-sign-in' .jira-connect-app - %h1 - GitLab for Jira Configuration - - if current_user.blank? && @subscriptions.empty? + %h1 + GitLab for Jira Configuration %h2.heading-with-border Sign in to GitLab.com to get started. .gl-mt-5 |