Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-01-14 15:10:54 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-01-14 15:10:54 +0300
commit18873553de98259d0558157f78198b38ddd02b31 (patch)
treecbdf0261e8a72975b7044fe7df8a1439c93ed4d5 /app/assets
parent0ea7b5c8a3f7afaae6b03279af56cd880d538bd7 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/jira_connect/components/app.vue10
-rw-r--r--app/assets/javascripts/jira_connect/index.js4
-rw-r--r--app/assets/javascripts/lib/utils/grammar.js6
-rw-r--r--app/assets/javascripts/notes/components/comment_field_layout.vue69
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue78
-rw-r--r--app/assets/javascripts/notes/components/email_participants_warning.vue70
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue82
-rw-r--r--app/assets/javascripts/notes/mixins/issuable_state.js11
-rw-r--r--app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue20
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue108
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue10
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue9
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue9
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js (renamed from app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js)24
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue137
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue86
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js2
-rw-r--r--app/assets/javascripts/pipelines/utils.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue2
-rw-r--r--app/assets/stylesheets/framework/mixins.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/jira_connect.scss9
-rw-r--r--app/assets/stylesheets/pages/note_form.scss8
25 files changed, 605 insertions, 163 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;
+ }
+}