diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-17 19:05:49 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-17 19:05:49 +0300 |
commit | 43a25d93ebdabea52f99b05e15b06250cd8f07d7 (patch) | |
tree | dceebdc68925362117480a5d672bcff122fb625b /app/assets/javascripts/ci/pipeline_editor | |
parent | 20c84b99005abd1c82101dfeff264ac50d2df211 (diff) |
Add latest changes from gitlab-org/gitlab@16-0-stable-eev16.0.0-rc42
Diffstat (limited to 'app/assets/javascripts/ci/pipeline_editor')
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> |