Skip to content
代码片段 群组 项目
提交 10661e7d 编辑于 作者: Payton Burdette's avatar Payton Burdette 提交者: Albert Salim
浏览文件

Add update schedule ability

Add the ability to update pipeline
schedule behind pipeline_schedules_vue
feature flag refactor to Vue.
上级 8d3d0e79
No related branches found
No related tags found
无相关合并请求
显示
536 个添加58 个删除
...@@ -8,17 +8,22 @@ import { ...@@ -8,17 +8,22 @@ import {
GlFormGroup, GlFormGroup,
GlFormInput, GlFormInput,
GlFormTextarea, GlFormTextarea,
GlLoadingIcon,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { createAlert } from '~/alert'; import { createAlert } from '~/alert';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl, queryToObject } from '~/lib/utils/url_utility';
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants'; import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
import RefSelector from '~/ref/components/ref_selector.vue'; import RefSelector from '~/ref/components/ref_selector.vue';
import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue'; import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue'; import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
import createPipelineScheduleMutation from '../graphql/mutations/create_pipeline_schedule.mutation.graphql'; import createPipelineScheduleMutation from '../graphql/mutations/create_pipeline_schedule.mutation.graphql';
import updatePipelineScheduleMutation from '../graphql/mutations/update_pipeline_schedule.mutation.graphql';
import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql';
import { VARIABLE_TYPE, FILE_TYPE } from '../constants'; import { VARIABLE_TYPE, FILE_TYPE } from '../constants';
const scheduleId = queryToObject(window.location.search).id;
export default { export default {
components: { components: {
GlButton, GlButton,
...@@ -29,20 +34,12 @@ export default { ...@@ -29,20 +34,12 @@ export default {
GlFormGroup, GlFormGroup,
GlFormInput, GlFormInput,
GlFormTextarea, GlFormTextarea,
GlLoadingIcon,
RefSelector, RefSelector,
TimezoneDropdown, TimezoneDropdown,
IntervalPatternInput, IntervalPatternInput,
}, },
inject: [ inject: ['fullPath', 'projectId', 'defaultBranch', 'dailyLimit', 'settingsLink', 'schedulesPath'],
'fullPath',
'projectId',
'defaultBranch',
'cron',
'cronTimezone',
'dailyLimit',
'settingsLink',
'schedulesPath',
],
props: { props: {
timezoneData: { timezoneData: {
type: Array, type: Array,
...@@ -58,24 +55,74 @@ export default { ...@@ -58,24 +55,74 @@ export default {
required: true, required: true,
}, },
}, },
apollo: {
schedule: {
query: getPipelineSchedulesQuery,
variables() {
return {
projectPath: this.fullPath,
ids: scheduleId,
};
},
update(data) {
return data.project?.pipelineSchedules?.nodes[0] || {};
},
result({ data }) {
if (data) {
const {
project: {
pipelineSchedules: { nodes },
},
} = data;
const schedule = nodes[0];
const variables = schedule.variables?.nodes || [];
this.description = schedule.description;
this.cron = schedule.cron;
this.cronTimezone = schedule.cronTimezone;
this.scheduleRef = schedule.ref;
this.variables = variables.map((variable) => {
return {
id: variable.id,
variableType: variable.variableType,
key: variable.key,
value: variable.value,
destroy: false,
};
});
this.addEmptyVariable();
this.activated = schedule.active;
}
},
skip() {
return !this.editing;
},
error() {
createAlert({ message: this.$options.i18n.scheduleFetchError });
},
},
},
data() { data() {
return { return {
cronValue: this.cron, cron: '',
description: '', description: '',
scheduleRef: this.defaultBranch, scheduleRef: this.defaultBranch,
activated: true, activated: true,
timezone: this.cronTimezone, cronTimezone: '',
variables: [], variables: [],
schedule: {},
}; };
}, },
i18n: { i18n: {
activated: __('Activated'), activated: __('Activated'),
cronTimezone: s__('PipelineSchedules|Cron timezone'), cronTimezoneText: s__('PipelineSchedules|Cron timezone'),
description: s__('PipelineSchedules|Description'), description: s__('PipelineSchedules|Description'),
shortDescriptionPipeline: s__( shortDescriptionPipeline: s__(
'PipelineSchedules|Provide a short description for this pipeline', 'PipelineSchedules|Provide a short description for this pipeline',
), ),
savePipelineSchedule: s__('PipelineSchedules|Save pipeline schedule'), editScheduleBtnText: s__('PipelineSchedules|Edit pipeline schedule'),
createScheduleBtnText: s__('PipelineSchedules|Create pipeline schedule'),
cancel: __('Cancel'), cancel: __('Cancel'),
targetBranchTag: __('Select target branch or tag'), targetBranchTag: __('Select target branch or tag'),
intervalPattern: s__('PipelineSchedules|Interval Pattern'), intervalPattern: s__('PipelineSchedules|Interval Pattern'),
...@@ -87,6 +134,12 @@ export default { ...@@ -87,6 +134,12 @@ export default {
scheduleCreateError: s__( scheduleCreateError: s__(
'PipelineSchedules|An error occurred while creating the pipeline schedule.', 'PipelineSchedules|An error occurred while creating the pipeline schedule.',
), ),
scheduleUpdateError: s__(
'PipelineSchedules|An error occurred while updating the pipeline schedule.',
),
scheduleFetchError: s__(
'PipelineSchedules|An error occurred while trying to fetch the pipeline schedule.',
),
}, },
typeOptions: { typeOptions: {
[VARIABLE_TYPE]: __('Variable'), [VARIABLE_TYPE]: __('Variable'),
...@@ -114,9 +167,26 @@ export default { ...@@ -114,9 +167,26 @@ export default {
getEnabledRefTypes() { getEnabledRefTypes() {
return [REF_TYPE_BRANCHES, REF_TYPE_TAGS]; return [REF_TYPE_BRANCHES, REF_TYPE_TAGS];
}, },
preparedVariables() { preparedVariablesUpdate() {
return this.variables.filter((variable) => variable.key !== ''); return this.variables.filter((variable) => variable.key !== '');
}, },
preparedVariablesCreate() {
return this.preparedVariablesUpdate.map((variable) => {
return {
key: variable.key,
value: variable.value,
variableType: variable.variableType,
};
});
},
loading() {
return this.$apollo.queries.schedule.loading;
},
buttonText() {
return this.editing
? this.$options.i18n.editScheduleBtnText
: this.$options.i18n.createScheduleBtnText;
},
}, },
created() { created() {
this.addEmptyVariable(); this.addEmptyVariable();
...@@ -133,6 +203,7 @@ export default { ...@@ -133,6 +203,7 @@ export default {
variableType: VARIABLE_TYPE, variableType: VARIABLE_TYPE,
key: '', key: '',
value: '', value: '',
destroy: false,
}); });
}, },
setVariableAttribute(key, attribute, value) { setVariableAttribute(key, attribute, value) {
...@@ -140,16 +211,11 @@ export default { ...@@ -140,16 +211,11 @@ export default {
variable[attribute] = value; variable[attribute] = value;
}, },
removeVariable(index) { removeVariable(index) {
this.variables.splice(index, 1); this.variables[index].destroy = true;
}, },
canRemove(index) { canRemove(index) {
return index < this.variables.length - 1; return index < this.variables.length - 1;
}, },
scheduleHandler() {
if (!this.editing) {
this.createPipelineSchedule();
}
},
async createPipelineSchedule() { async createPipelineSchedule() {
try { try {
const { const {
...@@ -161,10 +227,10 @@ export default { ...@@ -161,10 +227,10 @@ export default {
variables: { variables: {
input: { input: {
description: this.description, description: this.description,
cron: this.cronValue, cron: this.cron,
cronTimezone: this.timezone, cronTimezone: this.cronTimezone,
ref: this.scheduleRef, ref: this.scheduleRef,
variables: this.preparedVariables, variables: this.preparedVariablesCreate,
active: this.activated, active: this.activated,
projectPath: this.fullPath, projectPath: this.fullPath,
}, },
...@@ -180,11 +246,48 @@ export default { ...@@ -180,11 +246,48 @@ export default {
createAlert({ message: this.$options.i18n.scheduleCreateError }); createAlert({ message: this.$options.i18n.scheduleCreateError });
} }
}, },
async updatePipelineSchedule() {
try {
const {
data: {
pipelineScheduleUpdate: { errors },
},
} = await this.$apollo.mutate({
mutation: updatePipelineScheduleMutation,
variables: {
input: {
id: this.schedule.id,
description: this.description,
cron: this.cron,
cronTimezone: this.cronTimezone,
ref: this.scheduleRef,
variables: this.preparedVariablesUpdate,
active: this.activated,
},
},
});
if (errors.length > 0) {
createAlert({ message: errors[0] });
} else {
visitUrl(this.schedulesPath);
}
} catch {
createAlert({ message: this.$options.i18n.scheduleUpdateError });
}
},
scheduleHandler() {
if (this.editing) {
this.updatePipelineSchedule();
} else {
this.createPipelineSchedule();
}
},
setCronValue(cron) { setCronValue(cron) {
this.cronValue = cron; this.cron = cron;
}, },
setTimezone(timezone) { setTimezone(timezone) {
this.timezone = timezone.identifier || ''; this.cronTimezone = timezone.identifier || '';
}, },
}, },
}; };
...@@ -192,7 +295,8 @@ export default { ...@@ -192,7 +295,8 @@ export default {
<template> <template>
<div class="col-lg-8 gl-pl-0"> <div class="col-lg-8 gl-pl-0">
<gl-form> <gl-loading-icon v-if="loading && editing" size="lg" />
<gl-form v-else>
<!--Description--> <!--Description-->
<gl-form-group :label="$options.i18n.description" label-for="schedule-description"> <gl-form-group :label="$options.i18n.description" label-for="schedule-description">
<gl-form-input <gl-form-input
...@@ -215,10 +319,10 @@ export default { ...@@ -215,10 +319,10 @@ export default {
/> />
</gl-form-group> </gl-form-group>
<!--Timezone--> <!--Timezone-->
<gl-form-group :label="$options.i18n.cronTimezone" label-for="schedule-timezone"> <gl-form-group :label="$options.i18n.cronTimezoneText" label-for="schedule-timezone">
<timezone-dropdown <timezone-dropdown
id="schedule-timezone" id="schedule-timezone"
:value="timezone" :value="cronTimezone"
:timezone-data="timezoneData" :timezone-data="timezoneData"
name="schedule-timezone" name="schedule-timezone"
@input="setTimezone" @input="setTimezone"
...@@ -242,12 +346,12 @@ export default { ...@@ -242,12 +346,12 @@ export default {
<div <div
v-for="(variable, index) in variables" v-for="(variable, index) in variables"
:key="`var-${index}`" :key="`var-${index}`"
class="gl-mb-3 gl-pb-2"
data-testid="ci-variable-row"
data-qa-selector="ci_variable_row_container" data-qa-selector="ci_variable_row_container"
> >
<div <div
class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row" v-if="!variable.destroy"
class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row gl-mb-3 gl-pb-2"
data-testid="ci-variable-row"
> >
<gl-dropdown <gl-dropdown
:text="$options.typeOptions[variable.variableType]" :text="$options.typeOptions[variable.variableType]"
...@@ -308,7 +412,7 @@ export default { ...@@ -308,7 +412,7 @@ export default {
</gl-form-checkbox> </gl-form-checkbox>
<gl-button variant="confirm" data-testid="schedule-submit-button" @click="scheduleHandler"> <gl-button variant="confirm" data-testid="schedule-submit-button" @click="scheduleHandler">
{{ $options.i18n.savePipelineSchedule }} {{ buttonText }}
</gl-button> </gl-button>
<gl-button :href="schedulesPath" data-testid="schedule-cancel-button"> <gl-button :href="schedulesPath" data-testid="schedule-cancel-button">
{{ $options.i18n.cancel }} {{ $options.i18n.cancel }}
......
mutation updatePipelineSchedule($input: PipelineScheduleUpdateInput!) {
pipelineScheduleUpdate(input: $input) {
clientMutationId
errors
}
}
query getPipelineSchedulesQuery($projectPath: ID!, $status: PipelineScheduleStatus) { query getPipelineSchedulesQuery(
$projectPath: ID!
$status: PipelineScheduleStatus
$ids: [ID!] = null
) {
currentUser { currentUser {
id id
username username
} }
project(fullPath: $projectPath) { project(fullPath: $projectPath) {
id id
pipelineSchedules(status: $status) { pipelineSchedules(status: $status, ids: $ids) {
count count
nodes { nodes {
id id
description description
cron
cronTimezone
ref
forTag forTag
editPath editPath
refPath refPath
...@@ -35,6 +42,14 @@ query getPipelineSchedulesQuery($projectPath: ID!, $status: PipelineScheduleStat ...@@ -35,6 +42,14 @@ query getPipelineSchedulesQuery($projectPath: ID!, $status: PipelineScheduleStat
name name
webPath webPath
} }
variables {
nodes {
id
variableType
key
value
}
}
userPermissions { userPermissions {
playPipelineSchedule playPipelineSchedule
updatePipelineSchedule updatePipelineSchedule
......
...@@ -18,10 +18,8 @@ export default (selector, editing = false) => { ...@@ -18,10 +18,8 @@ export default (selector, editing = false) => {
const { const {
fullPath, fullPath,
cron,
dailyLimit, dailyLimit,
timezoneData, timezoneData,
cronTimezone,
projectId, projectId,
defaultBranch, defaultBranch,
settingsLink, settingsLink,
...@@ -37,8 +35,6 @@ export default (selector, editing = false) => { ...@@ -37,8 +35,6 @@ export default (selector, editing = false) => {
projectId, projectId,
defaultBranch, defaultBranch,
dailyLimit: dailyLimit ?? '', dailyLimit: dailyLimit ?? '',
cronTimezone: cronTimezone ?? '',
cron: cron ?? '',
settingsLink, settingsLink,
schedulesPath, schedulesPath,
}, },
......
...@@ -43,7 +43,7 @@ class Update < Base ...@@ -43,7 +43,7 @@ class Update < Base
def resolve(id:, variables: [], **pipeline_schedule_attrs) def resolve(id:, variables: [], **pipeline_schedule_attrs)
schedule = authorized_find!(id: id) schedule = authorized_find!(id: id)
params = pipeline_schedule_attrs.merge(variables_attributes: variables.map(&:to_h)) params = pipeline_schedule_attrs.merge(variables_attributes: variable_attributes_for(variables))
service_response = ::Ci::PipelineSchedules::UpdateService service_response = ::Ci::PipelineSchedules::UpdateService
.new(schedule, current_user, params) .new(schedule, current_user, params)
...@@ -54,6 +54,18 @@ def resolve(id:, variables: [], **pipeline_schedule_attrs) ...@@ -54,6 +54,18 @@ def resolve(id:, variables: [], **pipeline_schedule_attrs)
errors: service_response.errors errors: service_response.errors
} }
end end
private
def variable_attributes_for(variables)
variables.map do |variable|
variable.to_h.tap do |hash|
hash[:id] = GlobalID::Locator.locate(hash[:id]).id if hash[:id]
hash[:_destroy] = hash.delete(:destroy)
end
end
end
end end
end end
end end
......
...@@ -8,11 +8,18 @@ class VariableInputType < Types::BaseInputObject ...@@ -8,11 +8,18 @@ class VariableInputType < Types::BaseInputObject
description 'Attributes for the pipeline schedule variable.' description 'Attributes for the pipeline schedule variable.'
PipelineScheduleVariableID = ::Types::GlobalIDType[::Ci::PipelineScheduleVariable]
argument :id, PipelineScheduleVariableID, required: false, description: 'ID of the variable to mutate.'
argument :key, GraphQL::Types::String, required: true, description: 'Name of the variable.' argument :key, GraphQL::Types::String, required: true, description: 'Name of the variable.'
argument :value, GraphQL::Types::String, required: true, description: 'Value of the variable.' argument :value, GraphQL::Types::String, required: true, description: 'Value of the variable.'
argument :variable_type, Types::Ci::VariableTypeEnum, required: true, description: 'Type of the variable.' argument :variable_type, Types::Ci::VariableTypeEnum, required: true, description: 'Type of the variable.'
argument :destroy, GraphQL::Types::Boolean, required: false,
description: 'Boolean option to destroy the variable.'
end end
end end
end end
......
# frozen_string_literal: true
module Ci
module PipelineSchedulesHelper
def js_pipeline_schedules_form_data(project, schedule)
{
full_path: project.full_path,
daily_limit: schedule.daily_limit,
timezone_data: timezone_data.to_json,
project_id: project.id,
default_branch: project.default_branch,
settings_link: project_settings_ci_cd_path(project),
schedules_path: pipeline_schedules_path(project)
}
end
end
end
Ci::PipelineSchedulesHelper.prepend_mod_with('Ci::PipelineSchedulesHelper')
...@@ -5,9 +5,8 @@ ...@@ -5,9 +5,8 @@
%h1.page-title.gl-font-size-h-display %h1.page-title.gl-font-size-h-display
= _("Edit Pipeline Schedule") = _("Edit Pipeline Schedule")
%hr
- if Feature.enabled?(:pipeline_schedules_vue, @project) - if Feature.enabled?(:pipeline_schedules_vue, @project)
#pipeline-schedules-form-edit{ data: { full_path: @project.full_path } } #pipeline-schedules-form-edit{ data: js_pipeline_schedules_form_data(@project, @schedule) }
- else - else
= render "form" = render "form"
...@@ -9,6 +9,6 @@ ...@@ -9,6 +9,6 @@
= _("Schedule a new pipeline") = _("Schedule a new pipeline")
- if Feature.enabled?(:pipeline_schedules_vue, @project) - if Feature.enabled?(:pipeline_schedules_vue, @project)
#pipeline-schedules-form-new{ data: { full_path: @project.full_path, cron: @schedule.cron, daily_limit: @schedule.daily_limit, timezone_data: timezone_data.to_json, cron_timezone: @schedule.cron_timezone, project_id: @project.id, default_branch: @project.default_branch, settings_link: project_settings_ci_cd_path(@project), schedules_path: pipeline_schedules_path(@project) } } #pipeline-schedules-form-new{ data: js_pipeline_schedules_form_data(@project, @schedule) }
- else - else
= render "form" = render "form"
...@@ -27110,6 +27110,12 @@ A `CiPipelineScheduleID` is a global ID. It is encoded as a string. ...@@ -27110,6 +27110,12 @@ A `CiPipelineScheduleID` is a global ID. It is encoded as a string.
   
An example `CiPipelineScheduleID` is: `"gid://gitlab/Ci::PipelineSchedule/1"`. An example `CiPipelineScheduleID` is: `"gid://gitlab/Ci::PipelineSchedule/1"`.
   
### `CiPipelineScheduleVariableID`
A `CiPipelineScheduleVariableID` is a global ID. It is encoded as a string.
An example `CiPipelineScheduleVariableID` is: `"gid://gitlab/Ci::PipelineScheduleVariable/1"`.
### `CiRunnerID` ### `CiRunnerID`
   
A `CiRunnerID` is a global ID. It is encoded as a string. A `CiRunnerID` is a global ID. It is encoded as a string.
...@@ -28961,6 +28967,8 @@ Attributes for the pipeline schedule variable. ...@@ -28961,6 +28967,8 @@ Attributes for the pipeline schedule variable.
   
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="pipelineschedulevariableinputdestroy"></a>`destroy` | [`Boolean`](#boolean) | Boolean option to destroy the variable. |
| <a id="pipelineschedulevariableinputid"></a>`id` | [`CiPipelineScheduleVariableID`](#cipipelineschedulevariableid) | ID of the variable to mutate. |
| <a id="pipelineschedulevariableinputkey"></a>`key` | [`String!`](#string) | Name of the variable. | | <a id="pipelineschedulevariableinputkey"></a>`key` | [`String!`](#string) | Name of the variable. |
| <a id="pipelineschedulevariableinputvalue"></a>`value` | [`String!`](#string) | Value of the variable. | | <a id="pipelineschedulevariableinputvalue"></a>`value` | [`String!`](#string) | Value of the variable. |
| <a id="pipelineschedulevariableinputvariabletype"></a>`variableType` | [`CiVariableType!`](#civariabletype) | Type of the variable. | | <a id="pipelineschedulevariableinputvariabletype"></a>`variableType` | [`CiVariableType!`](#civariabletype) | Type of the variable. |
...@@ -33336,6 +33336,12 @@ msgstr "" ...@@ -33336,6 +33336,12 @@ msgstr ""
msgid "PipelineSchedules|An error occurred while creating the pipeline schedule." msgid "PipelineSchedules|An error occurred while creating the pipeline schedule."
msgstr "" msgstr ""
   
msgid "PipelineSchedules|An error occurred while trying to fetch the pipeline schedule."
msgstr ""
msgid "PipelineSchedules|An error occurred while updating the pipeline schedule."
msgstr ""
msgid "PipelineSchedules|Are you sure you want to delete this pipeline schedule?" msgid "PipelineSchedules|Are you sure you want to delete this pipeline schedule?"
msgstr "" msgstr ""
   
...@@ -33345,6 +33351,9 @@ msgstr "" ...@@ -33345,6 +33351,9 @@ msgstr ""
msgid "PipelineSchedules|Create a new pipeline schedule" msgid "PipelineSchedules|Create a new pipeline schedule"
msgstr "" msgstr ""
   
msgid "PipelineSchedules|Create pipeline schedule"
msgstr ""
msgid "PipelineSchedules|Cron timezone" msgid "PipelineSchedules|Cron timezone"
msgstr "" msgstr ""
   
...@@ -33402,9 +33411,6 @@ msgstr "" ...@@ -33402,9 +33411,6 @@ msgstr ""
msgid "PipelineSchedules|Runs with the same project permissions as the schedule owner." msgid "PipelineSchedules|Runs with the same project permissions as the schedule owner."
msgstr "" msgstr ""
   
msgid "PipelineSchedules|Save pipeline schedule"
msgstr ""
msgid "PipelineSchedules|Successfully scheduled a pipeline to run. Go to the %{linkStart}Pipelines page%{linkEnd} for details. " msgid "PipelineSchedules|Successfully scheduled a pipeline to run. Go to the %{linkStart}Pipelines page%{linkEnd} for details. "
msgstr "" msgstr ""
   
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { GlForm } from '@gitlab/ui'; import { GlForm, GlLoadingIcon } from '@gitlab/ui';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
...@@ -14,8 +14,14 @@ import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants'; ...@@ -14,8 +14,14 @@ import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue'; import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue'; import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
import createPipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/create_pipeline_schedule.mutation.graphql'; import createPipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/create_pipeline_schedule.mutation.graphql';
import updatePipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/update_pipeline_schedule.mutation.graphql';
import getPipelineSchedulesQuery from '~/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql';
import { timezoneDataFixture } from '../../../vue_shared/components/timezone_dropdown/helpers'; import { timezoneDataFixture } from '../../../vue_shared/components/timezone_dropdown/helpers';
import { createScheduleMutationResponse } from '../mock_data'; import {
createScheduleMutationResponse,
updateScheduleMutationResponse,
mockSinglePipelineScheduleNode,
} from '../mock_data';
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -23,8 +29,20 @@ jest.mock('~/alert'); ...@@ -23,8 +29,20 @@ jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(), visitUrl: jest.fn(),
joinPaths: jest.fn().mockReturnValue(''), joinPaths: jest.fn().mockReturnValue(''),
queryToObject: jest.fn().mockReturnValue({ id: '1' }),
})); }));
const {
data: {
project: {
pipelineSchedules: { nodes },
},
},
} = mockSinglePipelineScheduleNode;
const schedule = nodes[0];
const variables = schedule.variables.nodes;
describe('Pipeline schedules form', () => { describe('Pipeline schedules form', () => {
let wrapper; let wrapper;
const defaultBranch = 'main'; const defaultBranch = 'main';
...@@ -32,8 +50,13 @@ describe('Pipeline schedules form', () => { ...@@ -32,8 +50,13 @@ describe('Pipeline schedules form', () => {
const cron = ''; const cron = '';
const dailyLimit = ''; const dailyLimit = '';
const querySuccessHandler = jest.fn().mockResolvedValue(mockSinglePipelineScheduleNode);
const queryFailedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const createMutationHandlerSuccess = jest.fn().mockResolvedValue(createScheduleMutationResponse); const createMutationHandlerSuccess = jest.fn().mockResolvedValue(createScheduleMutationResponse);
const createMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error')); const createMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const updateMutationHandlerSuccess = jest.fn().mockResolvedValue(updateScheduleMutationResponse);
const updateMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const createMockApolloProvider = ( const createMockApolloProvider = (
requestHandlers = [[createPipelineScheduleMutation, createMutationHandlerSuccess]], requestHandlers = [[createPipelineScheduleMutation, createMutationHandlerSuccess]],
...@@ -52,8 +75,6 @@ describe('Pipeline schedules form', () => { ...@@ -52,8 +75,6 @@ describe('Pipeline schedules form', () => {
fullPath: 'gitlab-org/gitlab', fullPath: 'gitlab-org/gitlab',
projectId, projectId,
defaultBranch, defaultBranch,
cron,
cronTimezone: '',
dailyLimit, dailyLimit,
settingsLink: '', settingsLink: '',
schedulesPath: '/root/ci-project/-/pipeline_schedules', schedulesPath: '/root/ci-project/-/pipeline_schedules',
...@@ -69,6 +90,7 @@ describe('Pipeline schedules form', () => { ...@@ -69,6 +90,7 @@ describe('Pipeline schedules form', () => {
const findRefSelector = () => wrapper.findComponent(RefSelector); const findRefSelector = () => wrapper.findComponent(RefSelector);
const findSubmitButton = () => wrapper.findByTestId('schedule-submit-button'); const findSubmitButton = () => wrapper.findByTestId('schedule-submit-button');
const findCancelButton = () => wrapper.findByTestId('schedule-cancel-button'); const findCancelButton = () => wrapper.findByTestId('schedule-cancel-button');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
// Variables // Variables
const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row'); const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row');
const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key'); const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key');
...@@ -187,7 +209,38 @@ describe('Pipeline schedules form', () => { ...@@ -187,7 +209,38 @@ describe('Pipeline schedules form', () => {
}); });
}); });
describe('schedule creation', () => { describe('Button text', () => {
it.each`
editing | expectedText
${true} | ${'Edit pipeline schedule'}
${false} | ${'Create pipeline schedule'}
`(
'button text is $expectedText when editing is $editing',
async ({ editing, expectedText }) => {
createComponent(shallowMountExtended, editing, [
[getPipelineSchedulesQuery, querySuccessHandler],
]);
await waitForPromises();
expect(findSubmitButton().text()).toBe(expectedText);
},
);
});
describe('Schedule creation', () => {
it('when creating a schedule the query is not called', () => {
createComponent();
expect(querySuccessHandler).not.toHaveBeenCalled();
});
it('does not show loading state when creating new schedule', () => {
createComponent();
expect(findLoadingIcon().exists()).toBe(false);
});
describe('schedule creation success', () => { describe('schedule creation success', () => {
let mock; let mock;
...@@ -259,4 +312,125 @@ describe('Pipeline schedules form', () => { ...@@ -259,4 +312,125 @@ describe('Pipeline schedules form', () => {
}); });
}); });
}); });
describe('Schedule editing', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
it('shows loading state when editing', async () => {
createComponent(shallowMountExtended, true, [
[getPipelineSchedulesQuery, querySuccessHandler],
]);
expect(findLoadingIcon().exists()).toBe(true);
await waitForPromises();
expect(findLoadingIcon().exists()).toBe(false);
});
describe('schedule fetch success', () => {
it('fetches schedule and sets form data correctly', async () => {
createComponent(mountExtended, true, [[getPipelineSchedulesQuery, querySuccessHandler]]);
expect(querySuccessHandler).toHaveBeenCalled();
await waitForPromises();
expect(findDescription().element.value).toBe(schedule.description);
expect(findIntervalComponent().props('initialCronInterval')).toBe(schedule.cron);
expect(findTimezoneDropdown().props('value')).toBe(schedule.cronTimezone);
expect(findRefSelector().props('value')).toBe(schedule.ref);
expect(findVariableRows()).toHaveLength(3);
expect(findKeyInputs().at(0).element.value).toBe(variables[0].key);
expect(findKeyInputs().at(1).element.value).toBe(variables[1].key);
expect(findValueInputs().at(0).element.value).toBe(variables[0].value);
expect(findValueInputs().at(1).element.value).toBe(variables[1].value);
});
});
it('schedule fetch failure', async () => {
createComponent(shallowMountExtended, true, [
[getPipelineSchedulesQuery, queryFailedHandler],
]);
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred while trying to fetch the pipeline schedule.',
});
});
it('edit schedule success', async () => {
createComponent(mountExtended, true, [
[getPipelineSchedulesQuery, querySuccessHandler],
[updatePipelineScheduleMutation, updateMutationHandlerSuccess],
]);
await waitForPromises();
findDescription().element.value = 'Updated schedule';
findDescription().trigger('change');
findIntervalComponent().vm.$emit('cronValue', '0 22 16 * *');
// Ensures variable is sent with destroy property set true
findRemoveIcons().at(0).vm.$emit('click');
findSubmitButton().vm.$emit('click');
await waitForPromises();
expect(updateMutationHandlerSuccess).toHaveBeenCalledWith({
input: {
active: schedule.active,
cron: '0 22 16 * *',
cronTimezone: schedule.cronTimezone,
id: schedule.id,
ref: schedule.ref,
description: 'Updated schedule',
variables: [
{
destroy: true,
id: variables[0].id,
key: variables[0].key,
value: variables[0].value,
variableType: variables[0].variableType,
},
{
destroy: false,
id: variables[1].id,
key: variables[1].key,
value: variables[1].value,
variableType: variables[1].variableType,
},
],
},
});
});
it('edit schedule failure', async () => {
createComponent(shallowMountExtended, true, [
[getPipelineSchedulesQuery, querySuccessHandler],
[updatePipelineScheduleMutation, updateMutationHandlerFailed],
]);
await waitForPromises();
findSubmitButton().vm.$emit('click');
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred while updating the pipeline schedule.',
});
});
});
}); });
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import mockGetPipelineSchedulesGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.json'; import mockGetPipelineSchedulesGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.json';
import mockGetPipelineSchedulesAsGuestGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.as_guest.json'; import mockGetPipelineSchedulesAsGuestGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.as_guest.json';
import mockGetPipelineSchedulesTakeOwnershipGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.take_ownership.json'; import mockGetPipelineSchedulesTakeOwnershipGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.take_ownership.json';
import mockGetSinglePipelineScheduleGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.single.json';
const { const {
data: { data: {
...@@ -30,10 +31,10 @@ const { ...@@ -30,10 +31,10 @@ const {
export const mockPipelineScheduleNodes = nodes; export const mockPipelineScheduleNodes = nodes;
export const mockPipelineScheduleCurrentUser = currentUser; export const mockPipelineScheduleCurrentUser = currentUser;
export const mockPipelineScheduleAsGuestNodes = guestNodes; export const mockPipelineScheduleAsGuestNodes = guestNodes;
export const mockTakeOwnershipNodes = takeOwnershipNodes; export const mockTakeOwnershipNodes = takeOwnershipNodes;
export const mockSinglePipelineScheduleNode = mockGetSinglePipelineScheduleGraphQLResponse;
export const emptyPipelineSchedulesResponse = { export const emptyPipelineSchedulesResponse = {
data: { data: {
project: { project: {
...@@ -89,4 +90,14 @@ export const createScheduleMutationResponse = { ...@@ -89,4 +90,14 @@ export const createScheduleMutationResponse = {
}, },
}; };
export const updateScheduleMutationResponse = {
data: {
pipelineScheduleUpdate: {
clientMutationId: null,
errors: [],
__typename: 'PipelineScheduleUpdatePayload',
},
},
};
export { mockGetPipelineSchedulesGraphQLResponse }; export { mockGetPipelineSchedulesGraphQLResponse };
...@@ -63,6 +63,12 @@ ...@@ -63,6 +63,12 @@
expect_graphql_errors_to_be_empty expect_graphql_errors_to_be_empty
end end
it "#{fixtures_path}#{get_pipeline_schedules_query}.single.json" do
post_graphql(query, current_user: user, variables: { projectPath: project.full_path, ids: pipeline_schedule_populated.id })
expect_graphql_errors_to_be_empty
end
it "#{fixtures_path}#{get_pipeline_schedules_query}.as_guest.json" do it "#{fixtures_path}#{get_pipeline_schedules_query}.as_guest.json" do
guest = create(:user) guest = create(:user)
project.add_guest(user) project.add_guest(user)
......
...@@ -5,5 +5,5 @@ ...@@ -5,5 +5,5 @@
RSpec.describe Mutations::Ci::PipelineSchedule::VariableInputType, feature_category: :continuous_integration do RSpec.describe Mutations::Ci::PipelineSchedule::VariableInputType, feature_category: :continuous_integration do
specify { expect(described_class.graphql_name).to eq('PipelineScheduleVariableInput') } specify { expect(described_class.graphql_name).to eq('PipelineScheduleVariableInput') }
it { expect(described_class.arguments.keys).to match_array(%w[key value variableType]) } it { expect(described_class.arguments.keys).to match_array(%w[id key value variableType destroy]) }
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::PipelineSchedulesHelper, feature_category: :continuous_integration do
let_it_be(:project) { build_stubbed(:project) }
let_it_be(:user) { build_stubbed(:user) }
let_it_be(:pipeline_schedule) { build_stubbed(:ci_pipeline_schedule, project: project, owner: user) }
let_it_be(:timezones) { [{ identifier: "Pacific/Honolulu", name: "Hawaii" }] }
let_it_be(:pipeline_schedule_variable) do
build_stubbed(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule)
end
describe '#js_pipeline_schedules_form_data' do
before do
allow(helper).to receive(:timezone_data).and_return(timezones)
end
it 'returns pipeline schedule form data' do
expect(helper.js_pipeline_schedules_form_data(project, pipeline_schedule)).to include({
full_path: project.full_path,
daily_limit: nil,
project_id: project.id,
schedules_path: pipeline_schedules_path(project),
settings_link: project_settings_ci_cd_path(project),
timezone_data: timezones.to_json
})
end
end
end
...@@ -9,6 +9,14 @@ ...@@ -9,6 +9,14 @@
let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) } let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
let_it_be(:variable_one) do
create(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule)
end
let_it_be(:variable_two) do
create(:ci_pipeline_schedule_variable, key: 'bar', value: 'barvalue', pipeline_schedule: pipeline_schedule)
end
let(:mutation) do let(:mutation) do
variables = { variables = {
id: pipeline_schedule.to_global_id.to_s, id: pipeline_schedule.to_global_id.to_s,
...@@ -30,6 +38,7 @@ ...@@ -30,6 +38,7 @@
nodes { nodes {
key key
value value
variableType
} }
} }
} }
...@@ -88,8 +97,37 @@ ...@@ -88,8 +97,37 @@
expect(mutation_response['pipelineSchedule']['refForDisplay']).to eq(pipeline_schedule_parameters[:ref]) expect(mutation_response['pipelineSchedule']['refForDisplay']).to eq(pipeline_schedule_parameters[:ref])
expect(mutation_response['pipelineSchedule']['variables']['nodes'][0]['key']).to eq('AAA') expect(mutation_response['pipelineSchedule']['variables']['nodes'][2]['key']).to eq('AAA')
expect(mutation_response['pipelineSchedule']['variables']['nodes'][0]['value']).to eq('AAA123') expect(mutation_response['pipelineSchedule']['variables']['nodes'][2]['value']).to eq('AAA123')
end
end
context 'when updating and removing variables' do
let(:pipeline_schedule_parameters) do
{
variables: [
{ key: 'ABC', value: "ABC123", variableType: 'ENV_VAR', destroy: false },
{ id: variable_one.to_global_id.to_s,
key: 'foo', value: "foovalue",
variableType: 'ENV_VAR',
destroy: true },
{ id: variable_two.to_global_id.to_s, key: 'newbar', value: "newbarvalue", variableType: 'ENV_VAR' }
]
}
end
it 'processes variables correctly' do
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['pipelineSchedule']['variables']['nodes'])
.to match_array(
[
{ "key" => 'newbar', "value" => 'newbarvalue', "variableType" => 'ENV_VAR' },
{ "key" => 'ABC', "value" => "ABC123", "variableType" => 'ENV_VAR' }
]
)
end end
end end
......
...@@ -8,9 +8,16 @@ ...@@ -8,9 +8,16 @@
let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) } let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
let_it_be(:pipeline_schedule_variable) do
create(:ci_pipeline_schedule_variable,
key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule)
end
before_all do before_all do
project.add_maintainer(user) project.add_maintainer(user)
project.add_reporter(reporter) project.add_reporter(reporter)
pipeline_schedule.reload
end end
describe "execute" do describe "execute" do
...@@ -35,7 +42,10 @@ ...@@ -35,7 +42,10 @@
description: 'updated_desc', description: 'updated_desc',
ref: 'patch-x', ref: 'patch-x',
active: false, active: false,
cron: '*/1 * * * *' cron: '*/1 * * * *',
variables_attributes: [
{ id: pipeline_schedule_variable.id, key: 'bar', secret_value: 'barvalue' }
]
} }
end end
...@@ -47,6 +57,42 @@ ...@@ -47,6 +57,42 @@
.and change { pipeline_schedule.ref }.from('master').to('patch-x') .and change { pipeline_schedule.ref }.from('master').to('patch-x')
.and change { pipeline_schedule.active }.from(true).to(false) .and change { pipeline_schedule.active }.from(true).to(false)
.and change { pipeline_schedule.cron }.from('0 1 * * *').to('*/1 * * * *') .and change { pipeline_schedule.cron }.from('0 1 * * *').to('*/1 * * * *')
.and change { pipeline_schedule.variables.last.key }.from('foo').to('bar')
.and change { pipeline_schedule.variables.last.value }.from('foovalue').to('barvalue')
end
context 'when creating a variable' do
let(:params) do
{
variables_attributes: [
{ key: 'ABC', secret_value: 'ABC123' }
]
}
end
it 'creates the new variable' do
expect { service.execute }.to change { Ci::PipelineScheduleVariable.count }.by(1)
expect(pipeline_schedule.variables.last.key).to eq('ABC')
expect(pipeline_schedule.variables.last.value).to eq('ABC123')
end
end
context 'when deleting a variable' do
let(:params) do
{
variables_attributes: [
{
id: pipeline_schedule_variable.id,
_destroy: true
}
]
}
end
it 'deletes the existing variable' do
expect { service.execute }.to change { Ci::PipelineScheduleVariable.count }.by(-1)
end
end end
it 'returns ServiceResponse.success' do it 'returns ServiceResponse.success' do
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册