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>2023-05-17 19:05:49 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-17 19:05:49 +0300
commit43a25d93ebdabea52f99b05e15b06250cd8f07d7 (patch)
treedceebdc68925362117480a5d672bcff122fb625b /app/assets/javascripts/ci/pipeline_editor
parent20c84b99005abd1c82101dfeff264ac50d2df211 (diff)
Add latest changes from gitlab-org/gitlab@16-0-stable-eev16.0.0-rc42
Diffstat (limited to 'app/assets/javascripts/ci/pipeline_editor')
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/commit/commit_section.vue9
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue26
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/text_editor.vue11
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue109
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item.vue104
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue50
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue90
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item.vue105
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue90
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js113
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue168
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/utils.js53
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue11
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue8
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/constants.js18
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/event_hub.js5
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/runner_tags.query.graphql8
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/index.js6
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue26
22 files changed, 925 insertions, 93 deletions
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue b/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue
index 4775836fcc6..3fe9103c2b3 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue
@@ -146,7 +146,7 @@ export default {
</gl-sprintf>
</gl-form-checkbox>
</gl-form-group>
- <div class="gl-display-flex gl-py-5 gl-border-t-gray-100 gl-border-t-solid gl-border-t-1">
+ <div class="gl-display-flex gl-py-5">
<gl-button
type="submit"
class="js-no-auto-disable gl-mr-3"
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_section.vue b/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_section.vue
index 9cbf60b1c8f..b7616c02601 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_section.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_section.vue
@@ -1,11 +1,13 @@
<script>
import { __, s__, sprintf } from '~/locale';
+import Tracking from '~/tracking';
import {
COMMIT_ACTION_CREATE,
COMMIT_ACTION_UPDATE,
COMMIT_FAILURE,
COMMIT_SUCCESS,
COMMIT_SUCCESS_WITH_REDIRECT,
+ pipelineEditorTrackingOptions,
} from '../../constants';
import commitCIFile from '../../graphql/mutations/commit_ci_file.mutation.graphql';
import updateCurrentBranchMutation from '../../graphql/mutations/client/update_current_branch.mutation.graphql';
@@ -26,6 +28,7 @@ export default {
components: {
CommitForm,
},
+ mixins: [Tracking.mixin()],
inject: ['projectFullPath', 'ciConfigPath'],
props: {
ciFileContent: {
@@ -78,6 +81,8 @@ export default {
async onCommitSubmit({ message, sourceBranch, openMergeRequest }) {
this.isSaving = true;
+ this.trackCommitEvent();
+
try {
const {
data: {
@@ -131,6 +136,10 @@ export default {
this.isSaving = false;
}
},
+ trackCommitEvent() {
+ const { label, actions } = pipelineEditorTrackingOptions;
+ this.track(actions.commitCiConfig, { label, property: this.action });
+ },
updateCurrentBranch(currentBranch) {
this.$apollo.mutate({
mutation: updateCurrentBranchMutation,
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue
index 42e2d34fa3a..9179fe9d075 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue
@@ -6,7 +6,7 @@ import SourceEditor from '~/vue_shared/components/source_editor.vue';
export default {
i18n: {
- viewOnlyMessage: s__('Pipelines|Merged YAML is view only'),
+ viewOnlyMessage: s__('Pipelines|Full configuration is view only'),
},
components: {
SourceEditor,
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
index b78224e93b0..eabf4749e9c 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
@@ -10,12 +10,14 @@ export default {
browseTemplates: __('Browse templates'),
help: __('Help'),
jobAssistant: s__('JobAssistant|Job assistant'),
+ aiAssistant: s__('PipelinesAiAssistant|Ai assistant'),
},
TEMPLATE_REPOSITORY_URL,
components: {
GlButton,
},
mixins: [glFeatureFlagMixin(), Tracking.mixin()],
+ inject: ['aiChatAvailable'],
props: {
showDrawer: {
type: Boolean,
@@ -25,6 +27,15 @@ export default {
type: Boolean,
required: true,
},
+ showAiAssistantDrawer: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ isAiConfigChatAvailable() {
+ return this.glFeatures.aiCiConfigGenerator && this.aiChatAvailable;
+ },
},
methods: {
toggleDrawer() {
@@ -40,6 +51,11 @@ export default {
this.showJobAssistantDrawer ? 'close-job-assistant-drawer' : 'open-job-assistant-drawer',
);
},
+ toggleAiAssistantDrawer() {
+ this.$emit(
+ this.showAiAssistantDrawer ? 'close-ai-assistant-drawer' : 'open-ai-assistant-drawer',
+ );
+ },
trackHelpDrawerClick() {
const { label, actions } = pipelineEditorTrackingOptions;
this.track(actions.openHelpDrawer, { label });
@@ -85,5 +101,15 @@ export default {
>
{{ $options.i18n.jobAssistant }}
</gl-button>
+ <gl-button
+ v-if="isAiConfigChatAvailable"
+ icon="bulb"
+ size="small"
+ data-testid="ai-assistant-drawer-toggle"
+ data-qa-selector="ai_assistant_drawer_toggle"
+ @click="toggleAiAssistantDrawer"
+ >
+ {{ $options.i18n.aiAssistant }}
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/editor/text_editor.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/text_editor.vue
index 891c40482d3..1192f0bf418 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/editor/text_editor.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/text_editor.vue
@@ -2,6 +2,7 @@
import { EDITOR_READY_EVENT } from '~/editor/constants';
import { CiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
+import eventHub, { SCROLL_EDITOR_TO_BOTTOM } from '~/ci/pipeline_editor/event_hub';
import { SOURCE_EDITOR_DEBOUNCE } from '../../constants';
export default {
@@ -16,6 +17,12 @@ export default {
},
inject: ['ciConfigPath'],
inheritAttrs: false,
+ created() {
+ eventHub.$on(SCROLL_EDITOR_TO_BOTTOM, this.scrollEditorToBottom);
+ },
+ beforeDestroy() {
+ eventHub.$off(SCROLL_EDITOR_TO_BOTTOM, this.scrollEditorToBottom);
+ },
methods: {
onCiConfigUpdate(content) {
this.$emit('updateCiConfig', content);
@@ -24,6 +31,10 @@ export default {
instance.use({ definition: CiSchemaExtension });
instance.registerCiSchema();
},
+ scrollEditorToBottom() {
+ const editor = this.$refs.editor.getEditor();
+ editor.setScrollTop(editor.getScrollHeight());
+ },
},
readyEvent: EDITOR_READY_EVENT,
};
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue
index 84c0eef441f..8553256f13a 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue
@@ -1,8 +1,7 @@
<script>
-import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
-import { __, s__, sprintf } from '~/locale';
+import { GlIcon, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
import getAppStatus from '~/ci/pipeline_editor/graphql/queries/client/app_status.query.graphql';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import {
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_LINT_UNAVAILABLE,
@@ -11,15 +10,20 @@ import {
} from '../../constants';
export const i18n = {
- empty: __(
- "We'll continuously validate your pipeline configuration. The validation results will appear here.",
+ empty: s__(
+ "Pipelines|We'll continuously validate your pipeline configuration. The validation results will appear here.",
),
- learnMore: __('Learn more'),
loading: s__('Pipelines|Validating GitLab CI configuration…'),
- invalid: s__('Pipelines|This GitLab CI configuration is invalid.'),
- invalidWithReason: s__('Pipelines|This GitLab CI configuration is invalid: %{reason}.'),
- unavailableValidation: s__('Pipelines|Configuration validation currently not available.'),
- valid: s__('Pipelines|Pipeline syntax is correct.'),
+ invalid: s__(
+ 'Pipelines|This GitLab CI configuration is invalid. %{linkStart}Learn more%{linkEnd}',
+ ),
+ invalidWithReason: s__(
+ 'Pipelines|This GitLab CI configuration is invalid: %{reason}. %{linkStart}Learn more%{linkEnd}',
+ ),
+ unavailableValidation: s__(
+ 'Pipelines|Unable to validate CI/CD configuration. See the %{linkStart}GitLab CI/CD troubleshooting guide%{linkEnd} for more details.',
+ ),
+ valid: s__('Pipelines|Pipeline syntax is correct. %{linkStart}Learn more%{linkEnd}'),
};
export default {
@@ -28,10 +32,10 @@ export default {
GlIcon,
GlLink,
GlLoadingIcon,
- TooltipOnTruncate,
+ GlSprintf,
},
inject: {
- lintUnavailableHelpPagePath: {
+ ciTroubleshootingPath: {
default: '',
},
ymlHelpPagePath: {
@@ -54,49 +58,48 @@ export default {
},
},
computed: {
- helpPath() {
- return this.isLintUnavailable ? this.lintUnavailableHelpPagePath : this.ymlHelpPagePath;
+ APP_STATUS_CONFIG() {
+ return {
+ [EDITOR_APP_STATUS_EMPTY]: {
+ icon: 'check',
+ message: this.$options.i18n.empty,
+ },
+ [EDITOR_APP_STATUS_LINT_UNAVAILABLE]: {
+ icon: 'time-out',
+ link: this.ciTroubleshootingPath,
+ message: this.$options.i18n.unavailableValidation,
+ },
+ [EDITOR_APP_STATUS_VALID]: {
+ icon: 'check',
+ message: this.$options.i18n.valid,
+ },
+ };
},
- isEmpty() {
- return this.appStatus === EDITOR_APP_STATUS_EMPTY;
+ currentAppStatusConfig() {
+ return this.APP_STATUS_CONFIG[this.appStatus] || {};
},
- isLintUnavailable() {
- return this.appStatus === EDITOR_APP_STATUS_LINT_UNAVAILABLE;
+ hasLink() {
+ return this.appStatus !== EDITOR_APP_STATUS_EMPTY;
+ },
+ helpPath() {
+ return this.currentAppStatusConfig.link || this.ymlHelpPagePath;
},
isLoading() {
return this.appStatus === EDITOR_APP_STATUS_LOADING;
},
- isValid() {
- return this.appStatus === EDITOR_APP_STATUS_VALID;
- },
icon() {
- switch (this.appStatus) {
- case EDITOR_APP_STATUS_EMPTY:
- return 'check';
- case EDITOR_APP_STATUS_LINT_UNAVAILABLE:
- return 'time-out';
- case EDITOR_APP_STATUS_VALID:
- return 'check';
- default:
- return 'warning-solid';
- }
+ return this.currentAppStatusConfig.icon || 'warning-solid';
},
message() {
const [reason] = this.ciConfig?.errors || [];
- switch (this.appStatus) {
- case EDITOR_APP_STATUS_EMPTY:
- return this.$options.i18n.empty;
- case EDITOR_APP_STATUS_LINT_UNAVAILABLE:
- return this.$options.i18n.unavailableValidation;
- case EDITOR_APP_STATUS_VALID:
- return this.$options.i18n.valid;
- default:
- // Only display first error as a reason
- return this.ciConfig?.errors?.length > 0
- ? sprintf(this.$options.i18n.invalidWithReason, { reason }, false)
- : this.$options.i18n.invalid;
- }
+ return (
+ this.currentAppStatusConfig.message ||
+ // Only display first error as a reason
+ (reason
+ ? sprintf(this.$options.i18n.invalidWithReason, { reason }, false)
+ : this.$options.i18n.invalid)
+ );
},
},
};
@@ -108,18 +111,14 @@ export default {
<gl-loading-icon size="sm" inline />
{{ $options.i18n.loading }}
</template>
-
- <span v-else class="gl-display-inline-flex gl-white-space-nowrap gl-max-w-full">
- <tooltip-on-truncate :title="message" class="gl-text-truncate">
+ <span v-else data-testid="validation-segment">
+ <span class="gl-max-w-full" data-qa-selector="validation_message_content">
<gl-icon :name="icon" />
- <span data-qa-selector="validation_message_content" data-testid="validationMsg">
- {{ message }}
- </span>
- </tooltip-on-truncate>
- <span v-if="!isEmpty" class="gl-flex-shrink-0 gl-pl-2">
- <gl-link data-testid="learnMoreLink" :href="helpPath">
- {{ $options.i18n.learnMore }}
- </gl-link>
+ <gl-sprintf :message="message">
+ <template v-if="hasLink" #link="{ content }">
+ <gl-link :href="helpPath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
</span>
</span>
</div>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item.vue
new file mode 100644
index 00000000000..25bbd6b3180
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item.vue
@@ -0,0 +1,104 @@
+<script>
+import { GlAccordionItem, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui';
+import { get, toPath } from 'lodash';
+import { i18n } from '../constants';
+
+export default {
+ i18n,
+ components: {
+ GlFormGroup,
+ GlAccordionItem,
+ GlFormInput,
+ GlButton,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ formOptions() {
+ return [
+ {
+ key: 'artifacts.paths',
+ title: i18n.ARTIFACTS_AND_CACHE,
+ paths: this.job.artifacts.paths,
+ generateInputDataTestId: (index) => `artifacts-paths-input-${index}`,
+ generateDeleteButtonDataTestId: (index) => `delete-artifacts-paths-button-${index}`,
+ addButtonDataTestId: 'add-artifacts-paths-button',
+ },
+ {
+ key: 'artifacts.exclude',
+ title: i18n.ARTIFACTS_EXCLUDE_PATHS,
+ paths: this.job.artifacts.exclude,
+ generateInputDataTestId: (index) => `artifacts-exclude-input-${index}`,
+ generateDeleteButtonDataTestId: (index) => `delete-artifacts-exclude-button-${index}`,
+ addButtonDataTestId: 'add-artifacts-exclude-button',
+ },
+ {
+ key: 'cache.paths',
+ title: i18n.CACHE_PATHS,
+ paths: this.job.cache.paths,
+ generateInputDataTestId: (index) => `cache-paths-input-${index}`,
+ generateDeleteButtonDataTestId: (index) => `delete-cache-paths-button-${index}`,
+ addButtonDataTestId: 'add-cache-paths-button',
+ },
+ ];
+ },
+ },
+ methods: {
+ deleteStringArrayItem(path) {
+ const parentPath = toPath(path).slice(0, -1);
+ const array = get(this.job, parentPath);
+ if (array.length <= 1) {
+ return;
+ }
+ this.$emit('update-job', path);
+ },
+ },
+};
+</script>
+<template>
+ <gl-accordion-item :title="$options.i18n.ARTIFACTS_AND_CACHE">
+ <div v-for="entry in formOptions" :key="entry.key" class="form-group">
+ <div class="gl-display-flex">
+ <label class="gl-font-weight-bold gl-mb-3">{{ entry.title }}</label>
+ </div>
+ <div
+ v-for="(path, index) in entry.paths"
+ :key="index"
+ class="gl-display-flex gl-align-items-center gl-mb-3"
+ >
+ <div class="gl-flex-grow-1 gl-flex-basis-0 gl-mr-3">
+ <gl-form-input
+ class="gl-w-full!"
+ :value="path"
+ :data-testid="entry.generateInputDataTestId(index)"
+ @input="$emit('update-job', `${entry.key}[${index}]`, $event)"
+ />
+ </div>
+ <gl-button
+ category="tertiary"
+ icon="remove"
+ :data-testid="entry.generateDeleteButtonDataTestId(index)"
+ @click="deleteStringArrayItem(`${entry.key}[${index}]`)"
+ />
+ </div>
+ <gl-button
+ category="secondary"
+ variant="confirm"
+ :data-testid="entry.addButtonDataTestId"
+ @click="$emit('update-job', `${entry.key}[${entry.paths.length}]`, '')"
+ >{{ $options.i18n.ADD_PATH }}</gl-button
+ >
+ </div>
+ <gl-form-group :label="$options.i18n.CACHE_KEY">
+ <gl-form-input
+ :value="job.cache.key"
+ data-testid="cache-key-input"
+ @input="$emit('update-job', 'cache.key', $event)"
+ />
+ </gl-form-group>
+ </gl-accordion-item>
+</template>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue
new file mode 100644
index 00000000000..b4b468987d8
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlFormGroup, GlAccordionItem, GlFormInput, GlFormTextarea } from '@gitlab/ui';
+import { i18n } from '../constants';
+
+export default {
+ i18n,
+ placeholderText: i18n.ENTRYPOINT_PLACEHOLDER_TEXT,
+ components: {
+ GlAccordionItem,
+ GlFormInput,
+ GlFormTextarea,
+ GlFormGroup,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ imageEntryPoint() {
+ return this.job.image.entrypoint.join('\n');
+ },
+ },
+};
+</script>
+<template>
+ <gl-accordion-item :title="$options.i18n.IMAGE">
+ <gl-form-group :label="$options.i18n.IMAGE_NAME">
+ <gl-form-input
+ :value="job.image.name"
+ data-testid="image-name-input"
+ @input="$emit('update-job', 'image.name', $event)"
+ />
+ </gl-form-group>
+ <gl-form-group
+ :label="$options.i18n.IMAGE_ENTRYPOINT"
+ :description="$options.i18n.ARRAY_FIELD_DESCRIPTION"
+ class="gl-mb-0"
+ >
+ <gl-form-textarea
+ :no-resize="false"
+ :placeholder="$options.placeholderText"
+ data-testid="image-entrypoint-input"
+ :value="imageEntryPoint"
+ @input="$emit('update-job', 'image.entrypoint', $event.split('\n'))"
+ />
+ </gl-form-group>
+ </gl-accordion-item>
+</template>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue
new file mode 100644
index 00000000000..511003d3ad4
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue
@@ -0,0 +1,90 @@
+<script>
+import {
+ GlAccordionItem,
+ GlFormGroup,
+ GlFormInput,
+ GlFormTextarea,
+ GlTokenSelector,
+ GlFormCombobox,
+} from '@gitlab/ui';
+import { i18n } from '../constants';
+
+export default {
+ i18n,
+ components: {
+ GlAccordionItem,
+ GlFormGroup,
+ GlFormInput,
+ GlFormTextarea,
+ GlFormCombobox,
+ GlTokenSelector,
+ },
+ props: {
+ tagOptions: {
+ type: Array,
+ required: true,
+ },
+ job: {
+ type: Object,
+ required: true,
+ },
+ isNameValid: {
+ type: Boolean,
+ required: true,
+ },
+ isScriptValid: {
+ type: Boolean,
+ required: true,
+ },
+ availableStages: {
+ type: Array,
+ required: true,
+ default: () => [],
+ },
+ },
+};
+</script>
+<template>
+ <gl-accordion-item :title="$options.i18n.JOB_SETUP" visible>
+ <gl-form-group
+ :invalid-feedback="$options.i18n.THIS_FIELD_IS_REQUIRED"
+ :state="isNameValid"
+ :label="$options.i18n.JOB_NAME"
+ >
+ <gl-form-input
+ :value="job.name"
+ :state="isNameValid"
+ data-testid="job-name-input"
+ @input="$emit('update-job', 'name', $event)"
+ />
+ </gl-form-group>
+ <gl-form-combobox
+ :value="job.stage"
+ :token-list="availableStages"
+ :label-text="$options.i18n.STAGE"
+ data-testid="job-stage-input"
+ @input="$emit('update-job', 'stage', $event)"
+ />
+ <gl-form-group
+ :invalid-feedback="$options.i18n.THIS_FIELD_IS_REQUIRED"
+ :state="isScriptValid"
+ :label="$options.i18n.SCRIPT"
+ >
+ <gl-form-textarea
+ :value="job.script"
+ :state="isScriptValid"
+ :no-resize="false"
+ data-testid="job-script-input"
+ @input="$emit('update-job', 'script', $event)"
+ />
+ </gl-form-group>
+ <gl-form-group :label="$options.i18n.TAGS">
+ <gl-token-selector
+ :dropdown-items="tagOptions"
+ :selected-tokens="job.tags"
+ data-testid="job-tags-input"
+ @input="$emit('update-job', 'tags', $event)"
+ />
+ </gl-form-group>
+ </gl-accordion-item>
+</template>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item.vue
new file mode 100644
index 00000000000..d068b370852
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item.vue
@@ -0,0 +1,105 @@
+<script>
+import {
+ GlFormGroup,
+ GlAccordionItem,
+ GlFormInput,
+ GlFormSelect,
+ GlFormCheckbox,
+} from '@gitlab/ui';
+import { i18n, JOB_RULES_WHEN, JOB_RULES_START_IN } from '../constants';
+
+export default {
+ i18n,
+ whenOptions: Object.values(JOB_RULES_WHEN),
+ unitOptions: Object.values(JOB_RULES_START_IN),
+ components: {
+ GlAccordionItem,
+ GlFormInput,
+ GlFormSelect,
+ GlFormCheckbox,
+ GlFormGroup,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ isStartValid: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ startInNumber: 1,
+ startInUnit: JOB_RULES_START_IN.second.value,
+ };
+ },
+ computed: {
+ isDelayed() {
+ return this.job.rules[0].when === JOB_RULES_WHEN.delayed.value;
+ },
+ },
+ methods: {
+ updateStartIn() {
+ const plural = this.startInNumber > 1 ? 's' : '';
+ this.$emit(
+ 'update-job',
+ 'rules[0].start_in',
+ `${this.startInNumber} ${this.startInUnit}${plural}`,
+ );
+ },
+ },
+};
+</script>
+<template>
+ <gl-accordion-item :title="$options.i18n.RULES">
+ <div class="gl-display-flex">
+ <gl-form-group class="gl-flex-grow-1 gl-flex-basis-half gl-mr-3" :label="$options.i18n.WHEN">
+ <gl-form-select
+ class="gl-flex-grow-1 gl-flex-basis-half gl-mr-3"
+ :options="$options.whenOptions"
+ data-testid="rules-when-select"
+ :value="job.rules[0].when"
+ @input="$emit('update-job', 'rules[0].when', $event)"
+ />
+ </gl-form-group>
+ <gl-form-group
+ class="gl-flex-grow-1 gl-flex-basis-half"
+ :invalid-feedback="$options.i18n.INVALID_START_IN"
+ :state="isStartValid"
+ >
+ <div class="gl-display-flex gl-mt-5">
+ <gl-form-input
+ v-model="startInNumber"
+ class="gl-flex-grow-1 gl-flex-basis-half gl-mr-3"
+ data-testid="rules-start-in-number-input"
+ type="number"
+ :state="isStartValid"
+ :class="{ 'gl-visibility-hidden': !isDelayed }"
+ number
+ @input="updateStartIn"
+ />
+ <gl-form-select
+ v-model="startInUnit"
+ class="gl-flex-grow-1 gl-flex-basis-half"
+ data-testid="rules-start-in-unit-select"
+ :state="isStartValid"
+ :class="{ 'gl-visibility-hidden': !isDelayed }"
+ :options="$options.unitOptions"
+ @input="updateStartIn"
+ />
+ </div>
+ </gl-form-group>
+ </div>
+ <gl-form-group>
+ <gl-form-checkbox
+ :checked="job.rules[0].allow_failure"
+ data-testid="rules-allow-failure-checkbox"
+ @input="$emit('update-job', 'rules[0].allow_failure', $event)"
+ >
+ {{ $options.i18n.ALLOW_FAILURE }}
+ </gl-form-checkbox>
+ </gl-form-group>
+ </gl-accordion-item>
+</template>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue
new file mode 100644
index 00000000000..9bada3ef110
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue
@@ -0,0 +1,90 @@
+<script>
+import { GlAccordionItem, GlFormInput, GlButton, GlFormGroup, GlFormTextarea } from '@gitlab/ui';
+import { i18n } from '../constants';
+
+export default {
+ i18n,
+ placeholderText: i18n.ENTRYPOINT_PLACEHOLDER_TEXT,
+ components: {
+ GlAccordionItem,
+ GlFormGroup,
+ GlFormInput,
+ GlFormTextarea,
+ GlButton,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ canDeleteServices() {
+ return this.job.services.length > 1;
+ },
+ },
+ methods: {
+ deleteService(index) {
+ if (!this.canDeleteServices) {
+ return;
+ }
+ this.$emit('update-job', `services[${index}]`);
+ },
+ addService() {
+ this.$emit('update-job', `services[${this.job.services.length}]`, {
+ name: '',
+ entrypoint: [''],
+ });
+ },
+ serviceEntryPoint(service) {
+ const { entrypoint = [''] } = service;
+ return entrypoint.join('\n');
+ },
+ },
+};
+</script>
+<template>
+ <gl-accordion-item :title="$options.i18n.SERVICE">
+ <div
+ v-for="(service, index) in job.services"
+ :key="index"
+ class="gl-relative gl-bg-gray-10 gl-mb-5 gl-p-5"
+ >
+ <gl-button
+ v-if="canDeleteServices"
+ class="gl-absolute gl-right-3 gl-top-3"
+ category="tertiary"
+ icon="remove"
+ :data-testid="`delete-job-service-button-${index}`"
+ @click="deleteService(index)"
+ />
+ <gl-form-group :label="$options.i18n.SERVICE_NAME">
+ <gl-form-input
+ :data-testid="`service-name-input-${index}`"
+ :value="service.name"
+ @input="$emit('update-job', `services[${index}].name`, $event)"
+ />
+ </gl-form-group>
+ <gl-form-group
+ :label="$options.i18n.SERVICE_ENTRYPOINT"
+ :description="$options.i18n.ARRAY_FIELD_DESCRIPTION"
+ class="gl-mb-0"
+ >
+ <gl-form-textarea
+ :no-resize="false"
+ :placeholder="$options.placeholderText"
+ :data-testid="`service-entrypoint-input-${index}`"
+ :value="serviceEntryPoint(service)"
+ @input="$emit('update-job', `services[${index}].entrypoint`, $event.split('\n'))"
+ />
+ </gl-form-group>
+ </div>
+ <gl-button
+ category="secondary"
+ variant="confirm"
+ data-testid="add-job-service-button"
+ @click="addService"
+ >{{ $options.i18n.ADD_SERVICE }}</gl-button
+ >
+ </gl-accordion-item>
+</template>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js
index 1c122fd5e38..e93a9e84302 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js
+++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js
@@ -1,7 +1,118 @@
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
export const DRAWER_CONTAINER_CLASS = '.content-wrapper';
+export const JOB_RULES_WHEN = {
+ onSuccess: {
+ value: 'on_success',
+ text: s__('JobAssistant|on_success'),
+ },
+ onFailure: {
+ value: 'on_failure',
+ text: s__('JobAssistant|on_failure'),
+ },
+ manual: {
+ value: 'manual',
+ text: s__('JobAssistant|manual'),
+ },
+ always: {
+ value: 'always',
+ text: s__('JobAssistant|always'),
+ },
+ delayed: {
+ value: 'delayed',
+ text: s__('JobAssistant|delayed'),
+ },
+ never: {
+ value: 'never',
+ text: s__('JobAssistant|never'),
+ },
+};
+
+export const JOB_RULES_START_IN = {
+ second: {
+ value: 'second',
+ text: s__('JobAssistant|second(s)'),
+ },
+ minute: {
+ value: 'minute',
+ text: s__('JobAssistant|minute(s)'),
+ },
+ day: {
+ value: 'day',
+ text: s__('JobAssistant|day(s)'),
+ },
+ week: {
+ value: 'week',
+ text: s__('JobAssistant|week(s)'),
+ },
+};
+
+export const SECONDS_MULTIPLE_MAP = {
+ second: 1,
+ minute: 60,
+ day: 3600 * 24,
+ week: 3600 * 24 * 7,
+};
+
+export const JOB_TEMPLATE = {
+ name: '',
+ stage: '',
+ script: '',
+ tags: [],
+ image: {
+ name: '',
+ entrypoint: [''],
+ },
+ services: [
+ {
+ name: '',
+ entrypoint: [''],
+ },
+ ],
+ artifacts: {
+ paths: [''],
+ exclude: [''],
+ },
+ cache: {
+ paths: [''],
+ key: '',
+ },
+ rules: [
+ {
+ allow_failure: false,
+ when: 'on_success',
+ start_in: '',
+ },
+ ],
+};
+
export const i18n = {
+ ARRAY_FIELD_DESCRIPTION: s__('JobAssistant|Please separate array type fields with new lines'),
+ INPUT_FORMAT: s__('JobAssistant|Input format'),
ADD_JOB: s__('JobAssistant|Add job'),
+ SCRIPT: s__('JobAssistant|Script'),
+ JOB_NAME: s__('JobAssistant|Job name'),
+ JOB_SETUP: s__('JobAssistant|Job Setup'),
+ STAGE: s__('JobAssistant|Stage (optional)'),
+ TAGS: s__('JobAssistant|Tags (optional)'),
+ IMAGE: s__('JobAssistant|Image'),
+ IMAGE_NAME: s__('JobAssistant|Image name (optional)'),
+ IMAGE_ENTRYPOINT: s__('JobAssistant|Image entrypoint (optional)'),
+ THIS_FIELD_IS_REQUIRED: __('This field is required'),
+ CACHE_PATHS: s__('JobAssistant|Cache paths (optional)'),
+ CACHE_KEY: s__('JobAssistant|Cache key (optional)'),
+ ARTIFACTS_EXCLUDE_PATHS: s__('JobAssistant|Artifacts exclude paths (optional)'),
+ ARTIFACTS_PATHS: s__('JobAssistant|Artifacts paths (optional)'),
+ ARTIFACTS_AND_CACHE: s__('JobAssistant|Artifacts and cache'),
+ ADD_PATH: s__('JobAssistant|Add path'),
+ RULES: s__('JobAssistant|Rules'),
+ WHEN: s__('JobAssistant|When'),
+ ALLOW_FAILURE: s__('JobAssistant|Allow failure'),
+ INVALID_START_IN: s__('JobAssistant|Error - Valid value is between 1 second and 1 week'),
+ ADD_SERVICE: s__('JobAssistant|Add service'),
+ SERVICE: s__('JobAssistant|Services'),
+ SERVICE_NAME: s__('JobAssistant|Service name (optional)'),
+ SERVICE_ENTRYPOINT: s__('JobAssistant|Service entrypoint (optional)'),
+ ENTRYPOINT_PLACEHOLDER_TEXT: s__('JobAssistant|Please enter the parameters.'),
};
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue
index 65c87df21cb..30746065732 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue
@@ -1,13 +1,29 @@
<script>
-import { GlDrawer, GlButton } from '@gitlab/ui';
+import { GlDrawer, GlAccordion, GlButton } from '@gitlab/ui';
+import { stringify, parse } from 'yaml';
+import { get, omit, toPath } from 'lodash';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
-import { DRAWER_CONTAINER_CLASS, i18n } from './constants';
+import eventHub, { SCROLL_EDITOR_TO_BOTTOM } from '~/ci/pipeline_editor/event_hub';
+import getRunnerTags from '../../graphql/queries/runner_tags.query.graphql';
+import { DRAWER_CONTAINER_CLASS, JOB_TEMPLATE, JOB_RULES_WHEN, i18n } from './constants';
+import { removeEmptyObj, trimFields, validateEmptyValue, validateStartIn } from './utils';
+import JobSetupItem from './accordion_items/job_setup_item.vue';
+import ImageItem from './accordion_items/image_item.vue';
+import ServicesItem from './accordion_items/services_item.vue';
+import ArtifactsAndCacheItem from './accordion_items/artifacts_and_cache_item.vue';
+import RulesItem from './accordion_items/rules_item.vue';
export default {
i18n,
components: {
GlDrawer,
+ GlAccordion,
GlButton,
+ JobSetupItem,
+ ImageItem,
+ ServicesItem,
+ ArtifactsAndCacheItem,
+ RulesItem,
},
props: {
isVisible: {
@@ -20,16 +36,136 @@ export default {
required: false,
default: 200,
},
+ ciConfigData: {
+ type: Object,
+ required: true,
+ },
+ ciFileContent: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isNameValid: true,
+ isScriptValid: true,
+ isStartValid: true,
+ job: JSON.parse(JSON.stringify(JOB_TEMPLATE)),
+ };
+ },
+ apollo: {
+ runners: {
+ query: getRunnerTags,
+ update(data) {
+ return data?.runners?.nodes || [];
+ },
+ },
},
computed: {
+ availableStages() {
+ if (this.ciConfigData?.mergedYaml) {
+ return parse(this.ciConfigData.mergedYaml).stages;
+ }
+ return [];
+ },
+ tagOptions() {
+ const options = [];
+ this.runners?.forEach((runner) => options.push(...runner.tagList));
+ return [...new Set(options)].map((tag) => {
+ return {
+ id: tag,
+ name: tag,
+ };
+ });
+ },
drawerHeightOffset() {
return getContentWrapperHeight(DRAWER_CONTAINER_CLASS);
},
+ isJobValid() {
+ return this.isNameValid && this.isScriptValid && this.isStartValid;
+ },
+ },
+
+ watch: {
+ 'job.name': function jobNameWatch(name) {
+ this.isNameValid = validateEmptyValue(name);
+ },
+ 'job.script': function jobScriptWatch(script) {
+ this.isScriptValid = validateEmptyValue(script);
+ },
+ 'job.rules.0.start_in': function JobRulesStartInWatch(startIn) {
+ this.isStartValid = validateStartIn(this.job.rules[0].when, startIn);
+ },
},
methods: {
closeDrawer() {
+ this.clearJob();
this.$emit('close-job-assistant-drawer');
},
+ addCiConfig() {
+ this.validateJob();
+
+ if (!this.isJobValid) {
+ return;
+ }
+
+ const newJobString = this.generateYmlString();
+ this.$emit('updateCiConfig', `${this.ciFileContent}\n${newJobString}`);
+ eventHub.$emit(SCROLL_EDITOR_TO_BOTTOM);
+
+ this.closeDrawer();
+ },
+ generateYmlString() {
+ let job = JSON.parse(JSON.stringify(this.job));
+ const jobName = job.name;
+ job = this.removeUnnecessaryKeys(job);
+ job.tags = job.tags.map((tag) => tag.name); // Tag item is originally an option object, we need a string here to match `.gitlab-ci.yml` rules
+ const cleanedJob = trimFields(removeEmptyObj(job));
+ return stringify({ [jobName]: cleanedJob });
+ },
+ removeUnnecessaryKeys(job) {
+ const keys = ['name'];
+
+ // rules[0].allow_failure value should not be passed down
+ // if it equals the default value
+ if (this.job.rules[0].allow_failure === false) {
+ keys.push('rules[0].allow_failure');
+ }
+ // rules[0].when value should not be passed down
+ // if it equals the default value
+ if (this.job.rules[0].when === JOB_RULES_WHEN.onSuccess.value) {
+ keys.push('rules[0].when');
+ }
+ // rules[0].start_in value should not be passed down
+ // if rules[0].start_in doesn't equal 'delayed'
+ if (this.job.rules[0].when !== JOB_RULES_WHEN.delayed.value) {
+ keys.push('rules[0].start_in');
+ }
+ return omit(job, keys);
+ },
+ clearJob() {
+ this.job = JSON.parse(JSON.stringify(JOB_TEMPLATE));
+ this.$nextTick(() => {
+ this.isNameValid = true;
+ this.isScriptValid = true;
+ this.isStartValid = true;
+ });
+ },
+ updateJob(key, value) {
+ const path = toPath(key);
+ const targetObj = path.length === 1 ? this.job : get(this.job, path.slice(0, -1));
+ const lastKey = path[path.length - 1];
+ if (value !== undefined) {
+ this.$set(targetObj, lastKey, value);
+ } else {
+ this.$delete(targetObj, lastKey);
+ }
+ },
+ validateJob() {
+ this.isNameValid = validateEmptyValue(this.job.name);
+ this.isScriptValid = validateEmptyValue(this.job.script);
+ this.isStartValid = validateStartIn(this.job.rules[0].when, this.job.rules[0].start_in);
+ },
},
};
</script>
@@ -44,6 +180,20 @@ export default {
<template #title>
<h2 class="gl-m-0 gl-font-lg">{{ $options.i18n.ADD_JOB }}</h2>
</template>
+ <gl-accordion :header-level="3">
+ <job-setup-item
+ :tag-options="tagOptions"
+ :job="job"
+ :is-name-valid="isNameValid"
+ :is-script-valid="isScriptValid"
+ :available-stages="availableStages"
+ @update-job="updateJob"
+ />
+ <image-item :job="job" @update-job="updateJob" />
+ <services-item :job="job" @update-job="updateJob" />
+ <artifacts-and-cache-item :job="job" @update-job="updateJob" />
+ <rules-item :job="job" :is-start-valid="isStartValid" @update-job="updateJob" />
+ </gl-accordion>
<template #footer>
<div class="gl-display-flex gl-justify-content-end">
<gl-button
@@ -51,11 +201,15 @@ export default {
class="gl-mr-3"
data-testid="cancel-button"
@click="closeDrawer"
- >{{ __('Cancel') }}</gl-button
- >
- <gl-button category="primary" variant="confirm" data-testid="confirm-button">{{
- __('Add')
- }}</gl-button>
+ >{{ __('Cancel') }}
+ </gl-button>
+ <gl-button
+ category="primary"
+ variant="confirm"
+ data-testid="confirm-button"
+ @click="addCiConfig"
+ >{{ __('Add') }}
+ </gl-button>
</div>
</template>
</gl-drawer>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/utils.js b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/utils.js
new file mode 100644
index 00000000000..a604d79259d
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/utils.js
@@ -0,0 +1,53 @@
+import { isEmpty, isObject, isArray, isString, reject, omitBy, mapValues, map, trim } from 'lodash';
+import {
+ JOB_RULES_WHEN,
+ SECONDS_MULTIPLE_MAP,
+} from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
+
+const isEmptyValue = (val) => (isObject(val) || isString(val)) && isEmpty(val);
+const trimText = (val) => (isString(val) ? trim(val) : val);
+
+export const removeEmptyObj = (obj) => {
+ if (isArray(obj)) {
+ return reject(map(obj, removeEmptyObj), isEmptyValue);
+ } else if (isObject(obj)) {
+ return omitBy(mapValues(obj, removeEmptyObj), isEmptyValue);
+ }
+ return obj;
+};
+
+export const trimFields = (data) => {
+ if (isArray(data)) {
+ return data.map(trimFields);
+ } else if (isObject(data)) {
+ return mapValues(data, trimFields);
+ }
+ return trimText(data);
+};
+
+export const validateEmptyValue = (value) => {
+ return trim(value) !== '';
+};
+
+export const validateStartIn = (when, startIn) => {
+ const hasNoValue = when !== JOB_RULES_WHEN.delayed.value;
+ if (hasNoValue) {
+ return true;
+ }
+
+ let [startInNumber, startInUnit] = startIn.split(' ');
+
+ startInNumber = Number(startInNumber);
+ if (!Number.isInteger(startInNumber)) {
+ return false;
+ }
+
+ const isPlural = startInUnit.slice(-1) === 's';
+ if (isPlural) {
+ startInUnit = startInUnit.slice(0, -1);
+ }
+
+ const multiple = SECONDS_MULTIPLE_MAP[startInUnit];
+
+ return startInNumber * multiple >= 1 && startInNumber * multiple <= SECONDS_MULTIPLE_MAP.week;
+};
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
index fd6547468d9..403793a255a 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -31,7 +31,7 @@ export default {
tabEdit: s__('Pipelines|Edit'),
tabGraph: s__('Pipelines|Visualize'),
tabLint: s__('Pipelines|Lint'),
- tabMergedYaml: s__('Pipelines|View merged YAML'),
+ tabMergedYaml: s__('Pipelines|Full configuration'),
tabValidate: s__('Pipelines|Validate'),
empty: {
visualization: s__(
@@ -41,12 +41,12 @@ export default {
'PipelineEditor|The CI/CD configuration is continuously validated. Errors and warnings are displayed when the CI/CD configuration file is not empty.',
),
merge: s__(
- 'PipelineEditor|The merged YAML view is displayed when the CI/CD configuration file has valid syntax.',
+ 'PipelineEditor|The full configuration view is displayed when the CI/CD configuration file has valid syntax.',
),
},
},
errorTexts: {
- loadMergedYaml: s__('Pipelines|Could not load merged YAML content'),
+ loadMergedYaml: s__('Pipelines|Could not load full configuration content'),
},
query: {
TAB_QUERY_PARAM,
@@ -99,6 +99,10 @@ export default {
type: Boolean,
required: true,
},
+ showAiAssistantDrawer: {
+ type: Boolean,
+ required: true,
+ },
},
apollo: {
appStatus: {
@@ -194,6 +198,7 @@ export default {
<ci-editor-header
:show-drawer="showDrawer"
:show-job-assistant-drawer="showJobAssistantDrawer"
+ :show-ai-assistant-drawer="showAiAssistantDrawer"
v-on="$listeners"
/>
<text-editor :commit-sha="commitSha" :value="ciFileContent" v-on="$listeners" />
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue b/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
index 83fcab4b343..ba33888e2fb 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
@@ -2,7 +2,7 @@
import {
GlAlert,
GlButton,
- GlDropdown,
+ GlDisclosureDropdown,
GlIcon,
GlLoadingIcon,
GlLink,
@@ -61,7 +61,7 @@ export default {
CiLintResults,
GlAlert,
GlButton,
- GlDropdown,
+ GlDisclosureDropdown,
GlIcon,
GlLoadingIcon,
GlLink,
@@ -195,11 +195,11 @@ export default {
<div class="gl-display-flex gl-justify-content-space-between gl-mt-3">
<div>
<label>{{ $options.i18n.pipelineSource }}</label>
- <gl-dropdown
+ <gl-disclosure-dropdown
v-gl-tooltip.hover
class="gl-ml-3"
:title="$options.i18n.pipelineSourceTooltip"
- :text="$options.i18n.pipelineSourceDefault"
+ :toggle-text="$options.i18n.pipelineSourceDefault"
disabled
data-testid="pipeline-source"
/>
diff --git a/app/assets/javascripts/ci/pipeline_editor/constants.js b/app/assets/javascripts/ci/pipeline_editor/constants.js
index dd25c4d433b..912e0fcbff9 100644
--- a/app/assets/javascripts/ci/pipeline_editor/constants.js
+++ b/app/assets/javascripts/ci/pipeline_editor/constants.js
@@ -67,6 +67,7 @@ export const pipelineEditorTrackingOptions = {
actions: {
browseTemplates: 'browse_templates',
closeHelpDrawer: 'close_help_drawer',
+ commitCiConfig: 'commit_ci_config',
helpDrawerLinks: {
[CI_EXAMPLES_LINK]: 'visit_help_drawer_link_ci_examples',
[CI_HELP_LINK]: 'visit_help_drawer_link_ci_help',
@@ -86,25 +87,8 @@ export const VALIDATE_TAB_FEEDBACK_URL = 'https://gitlab.com/gitlab-org/gitlab/-
export const COMMIT_SHA_POLL_INTERVAL = 1000;
-export const RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME = 'runners_availability_section';
-export const RUNNERS_SETTINGS_LINK_CLICKED_EVENT = 'runners_settings_link_clicked';
-export const RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT = 'runners_documentation_link_clicked';
-export const RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT = 'runners_settings_button_clicked';
export const I18N = {
title: s__('Pipelines|Get started with GitLab CI/CD'),
- runners: {
- title: s__('Pipelines|Runners are available to run your jobs now'),
- subtitle: s__(
- 'Pipelines|GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline. There are active runners available to run your jobs right now. If you prefer, you can %{settingsLinkStart}configure your runners%{settingsLinkEnd} or %{docsLinkStart}learn more%{docsLinkEnd} about runners.',
- ),
- },
- noRunners: {
- title: s__('Pipelines|No runners detected'),
- subtitle: s__(
- 'Pipelines|A GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline. Install GitLab Runner and register your own runners to get started with CI/CD.',
- ),
- cta: s__('Pipelines|Install GitLab Runner'),
- },
learnBasics: {
title: s__('Pipelines|Learn the basics of pipelines and .yml files'),
subtitle: s__(
diff --git a/app/assets/javascripts/ci/pipeline_editor/event_hub.js b/app/assets/javascripts/ci/pipeline_editor/event_hub.js
new file mode 100644
index 00000000000..c64eaf5ef5c
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_editor/event_hub.js
@@ -0,0 +1,5 @@
+import createEventHub from '~/helpers/event_hub_factory';
+
+export default createEventHub();
+
+export const SCROLL_EDITOR_TO_BOTTOM = Symbol('scrollEditorToBottom');
diff --git a/app/assets/javascripts/ci/pipeline_editor/graphql/queries/runner_tags.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/runner_tags.query.graphql
new file mode 100644
index 00000000000..aab30257d13
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/runner_tags.query.graphql
@@ -0,0 +1,8 @@
+query getRunnerTags {
+ runners {
+ nodes {
+ id
+ tagList
+ }
+ }
+}
diff --git a/app/assets/javascripts/ci/pipeline_editor/index.js b/app/assets/javascripts/ci/pipeline_editor/index.js
index 6d91c339833..b8d6c27435d 100644
--- a/app/assets/javascripts/ci/pipeline_editor/index.js
+++ b/app/assets/javascripts/ci/pipeline_editor/index.js
@@ -29,12 +29,12 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
ciExamplesHelpPagePath,
ciHelpPagePath,
ciLintPath,
+ ciTroubleshootingPath,
defaultBranch,
emptyStateIllustrationPath,
helpPaths,
includesHelpPagePath,
lintHelpPagePath,
- lintUnavailableHelpPagePath,
needsHelpPagePath,
newMergeRequestPath,
pipelinePagePath,
@@ -46,6 +46,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
usesExternalConfig,
validateTabIllustrationPath,
ymlHelpPagePath,
+ aiChatAvailable,
} = el.dataset;
const configurationPaths = Object.fromEntries(
@@ -115,10 +116,12 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
el,
apolloProvider,
provide: {
+ aiChatAvailable: parseBoolean(aiChatAvailable),
ciConfigPath,
ciExamplesHelpPagePath,
ciHelpPagePath,
ciLintPath,
+ ciTroubleshootingPath,
configurationPaths,
dataMethod: 'graphql',
defaultBranch,
@@ -126,7 +129,6 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
helpPaths,
includesHelpPagePath,
lintHelpPagePath,
- lintUnavailableHelpPagePath,
needsHelpPagePath,
newMergeRequestPath,
pipelinePagePath,
diff --git a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue
index ff848a973e3..de8e5a1a284 100644
--- a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import { fetchPolicies } from '~/lib/graphql';
-import { mergeUrlParams, queryToObject, redirectTo } from '~/lib/utils/url_utility';
+import { mergeUrlParams, queryToObject, redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { __, s__ } from '~/locale';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
@@ -325,7 +325,7 @@ export default {
},
this.newMergeRequestPath,
);
- redirectTo(url);
+ redirectTo(url); // eslint-disable-line import/no-deprecated
},
async refetchContent() {
this.$apollo.queries.initialCiFileContent.skip = false;
diff --git a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
index 59863edbe0b..647e33333ce 100644
--- a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
@@ -1,6 +1,7 @@
<script>
import { GlModal } from '@gitlab/ui';
import { __ } from '~/locale';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import CommitSection from './components/commit/commit_section.vue';
import PipelineEditorDrawer from './components/drawer/pipeline_editor_drawer.vue';
import JobAssistantDrawer from './components/job_assistant_drawer/job_assistant_drawer.vue';
@@ -10,6 +11,9 @@ import PipelineEditorHeader from './components/header/pipeline_editor_header.vue
import PipelineEditorTabs from './components/pipeline_editor_tabs.vue';
import { CREATE_TAB, FILE_TREE_DISPLAY_KEY } from './constants';
+const AiAssistantDrawer = () =>
+ import('ee_component/ci/pipeline_editor/components/ai_assistant_drawer.vue');
+
export default {
commitSectionRef: 'commitSectionRef',
modal: {
@@ -30,11 +34,13 @@ export default {
GlModal,
PipelineEditorDrawer,
JobAssistantDrawer,
+ AiAssistantDrawer,
PipelineEditorFileNav,
PipelineEditorFileTree,
PipelineEditorHeader,
PipelineEditorTabs,
},
+ mixins: [glFeatureFlagMixin()],
props: {
ciConfigData: {
type: Object,
@@ -66,8 +72,10 @@ export default {
shouldLoadNewBranch: false,
showDrawer: false,
showJobAssistantDrawer: false,
+ showAiAssistantDrawer: false,
drawerIndex: 200,
jobAssistantIndex: 200,
+ aiAssistantIndex: 200,
showFileTree: false,
showSwitchBranchModal: false,
};
@@ -93,6 +101,13 @@ export default {
closeJobAssistantDrawer() {
this.showJobAssistantDrawer = false;
},
+ closeAiAssistantDrawer() {
+ this.showAiAssistantDrawer = false;
+ },
+ openAiAssistantDrawer() {
+ this.showAiAssistantDrawer = true;
+ this.aiAssistantIndex = this.drawerIndex + 1;
+ },
handleConfirmSwitchBranch() {
this.showSwitchBranchModal = true;
},
@@ -167,11 +182,14 @@ export default {
:is-new-ci-config-file="isNewCiConfigFile"
:show-drawer="showDrawer"
:show-job-assistant-drawer="showJobAssistantDrawer"
+ :show-ai-assistant-drawer="showAiAssistantDrawer"
v-on="$listeners"
@open-drawer="openDrawer"
@close-drawer="closeDrawer"
@open-job-assistant-drawer="openJobAssistantDrawer"
@close-job-assistant-drawer="closeJobAssistantDrawer"
+ @open-ai-assistant-drawer="openAiAssistantDrawer"
+ @close-ai-assistant-drawer="closeAiAssistantDrawer"
@set-current-tab="setCurrentTab"
@walkthrough-popover-cta-clicked="setScrollToCommitForm"
/>
@@ -195,10 +213,18 @@ export default {
@close-drawer="closeDrawer"
/>
<job-assistant-drawer
+ :ci-config-data="ciConfigData"
+ :ci-file-content="ciFileContent"
:is-visible="showJobAssistantDrawer"
:z-index="jobAssistantIndex"
v-on="$listeners"
@close-job-assistant-drawer="closeJobAssistantDrawer"
/>
+ <ai-assistant-drawer
+ v-if="glFeatures.aiCiConfigGenerator"
+ :is-visible="showAiAssistantDrawer"
+ :z-index="aiAssistantIndex"
+ @close-ai-assistant-drawer="closeAiAssistantDrawer"
+ />
</div>
</template>