Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
c1462c63
Commit
c1462c63
authored
Mar 08, 2022
by
Janis Altherr
Committed by
Kushal Pandya
Mar 08, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add the pipeline wizard step component
Signed-off-by:
Janis Altherr
<
jaltherr@gitlab.com
>
parent
4bd9a354
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
424 additions
and
0 deletions
+424
-0
app/assets/javascripts/pipeline_wizard/components/step.vue
app/assets/javascripts/pipeline_wizard/components/step.vue
+149
-0
locale/gitlab.pot
locale/gitlab.pot
+3
-0
spec/frontend/pipeline_wizard/components/step_spec.js
spec/frontend/pipeline_wizard/components/step_spec.js
+227
-0
spec/frontend/pipeline_wizard/mock/yaml.js
spec/frontend/pipeline_wizard/mock/yaml.js
+45
-0
No files found.
app/assets/javascripts/pipeline_wizard/components/step.vue
0 → 100644
View file @
c1462c63
<
script
>
import
{
GlAlert
}
from
'
@gitlab/ui
'
;
import
{
isNode
,
isDocument
,
parseDocument
,
Document
}
from
'
yaml
'
;
import
{
merge
}
from
'
~/lib/utils/yaml
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
logError
}
from
'
~/lib/logger
'
;
import
InputWrapper
from
'
./input.vue
'
;
import
StepNav
from
'
./step_nav.vue
'
;
export
default
{
name
:
'
PipelineWizardStep
'
,
i18n
:
{
errors
:
{
cloneErrorUserMessage
:
s__
(
'
PipelineWizard|There was an unexpected error trying to set up the template. The error has been logged.
'
,
),
},
},
components
:
{
StepNav
,
InputWrapper
,
GlAlert
,
},
props
:
{
// As the inputs prop we expect to receive an array of instructions
// on how to display the input fields that will be used to obtain the
// user's input. Each input instruction needs a target prop, specifying
// the placeholder in the template that will be replaced by the user's
// input. The selected widget may require additional validation for the
// input object.
inputs
:
{
type
:
Array
,
required
:
true
,
validator
:
(
value
)
=>
value
.
every
((
i
)
=>
{
return
i
?.
target
&&
i
?.
widget
;
}),
},
template
:
{
type
:
null
,
required
:
true
,
validator
:
(
v
)
=>
isNode
(
v
),
},
hasPreviousStep
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
compiled
:
{
type
:
Object
,
required
:
true
,
validator
:
(
v
)
=>
isDocument
(
v
),
},
},
data
()
{
return
{
wasCompiled
:
false
,
validate
:
false
,
inputValidStates
:
Array
(
this
.
inputs
.
length
).
fill
(
null
),
error
:
null
,
};
},
computed
:
{
inputValidStatesThatAreNotNull
()
{
return
this
.
inputValidStates
?.
filter
((
s
)
=>
s
!==
null
);
},
areAllInputValidStatesNull
()
{
return
!
this
.
inputValidStatesThatAreNotNull
?.
length
;
},
isValid
()
{
return
this
.
areAllInputValidStatesNull
||
this
.
inputValidStatesThatAreNotNull
.
every
((
s
)
=>
s
);
},
},
methods
:
{
forceClone
(
yamlNode
)
{
try
{
// document.clone() will only clone the root document object,
// but the references to the child nodes inside will be retained.
// So in order to ensure a full clone, we need to stringify
// and parse until there's a better implementation in the
// yaml package.
return
parseDocument
(
new
Document
(
yamlNode
).
toString
());
}
catch
(
e
)
{
// eslint-disable-next-line @gitlab/require-i18n-strings
logError
(
'
An unexpected error occurred while trying to clone a template
'
,
e
);
this
.
error
=
this
.
$options
.
i18n
.
errors
.
cloneErrorUserMessage
;
return
null
;
}
},
compile
()
{
if
(
this
.
wasCompiled
)
return
;
// NOTE: This modifies this.compiled without triggering reactivity.
// this is done on purpose, see
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81412#note_862972703
// for more information
merge
(
this
.
compiled
,
this
.
forceClone
(
this
.
template
));
this
.
wasCompiled
=
true
;
},
onUpdate
(
c
)
{
this
.
$emit
(
'
update:compiled
'
,
c
);
},
onPrevClick
()
{
this
.
$emit
(
'
back
'
);
},
async
onNextClick
()
{
this
.
validate
=
true
;
await
this
.
$nextTick
();
if
(
this
.
isValid
)
{
this
.
$emit
(
'
next
'
);
}
},
onInputValidationStateChange
(
inputId
,
value
)
{
this
.
$set
(
this
.
inputValidStates
,
inputId
,
value
);
},
onHighlight
(
path
)
{
this
.
$emit
(
'
update:highlight
'
,
path
);
},
},
};
</
script
>
<
template
>
<div>
<gl-alert
v-if=
"error"
class=
"gl-mb-4"
variant=
"danger"
>
{{
error
}}
</gl-alert>
<input-wrapper
v-for=
"(input, i) in inputs"
:key=
"input.target"
:compiled=
"compiled"
:target=
"input.target"
:template=
"template"
:validate=
"validate"
:widget=
"input.widget"
class=
"gl-mb-2"
v-bind=
"input"
@
highlight=
"onHighlight"
@
update:valid=
"(validationState) => onInputValidationStateChange(i, validationState)"
@
update:compiled=
"onUpdate"
@
beforeUpdate:compiled.once=
"compile"
/>
<step-nav
:next-button-enabled=
"isValid"
:show-back-button=
"hasPreviousStep"
show-next-button
@
back=
"onPrevClick"
@
next=
"onNextClick"
/>
</div>
</
template
>
locale/gitlab.pot
View file @
c1462c63
...
...
@@ -26915,6 +26915,9 @@ msgstr ""
msgid "PipelineWizard|There was a problem while checking whether your file already exists in the specified branch."
msgstr ""
msgid "PipelineWizard|There was an unexpected error trying to set up the template. The error has been logged."
msgstr ""
msgid "Pipelines"
msgstr ""
...
...
spec/frontend/pipeline_wizard/components/step_spec.js
0 → 100644
View file @
c1462c63
import
{
parseDocument
,
Document
}
from
'
yaml
'
;
import
{
omit
}
from
'
lodash
'
;
import
{
nextTick
}
from
'
vue
'
;
import
{
shallowMountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
PipelineWizardStep
from
'
~/pipeline_wizard/components/step.vue
'
;
import
InputWrapper
from
'
~/pipeline_wizard/components/input.vue
'
;
import
StepNav
from
'
~/pipeline_wizard/components/step_nav.vue
'
;
import
{
stepInputs
,
stepTemplate
,
compiledYamlBeforeSetup
,
compiledYamlAfterInitialLoad
,
compiledYaml
,
}
from
'
../mock/yaml
'
;
describe
(
'
Pipeline Wizard - Step Page
'
,
()
=>
{
const
inputs
=
parseDocument
(
stepInputs
).
toJS
();
let
wrapper
;
let
input1
;
let
input2
;
const
getInputWrappers
=
()
=>
wrapper
.
findAllComponents
(
InputWrapper
);
const
forEachInputWrapper
=
(
cb
)
=>
{
getInputWrappers
().
wrappers
.
forEach
(
cb
);
};
const
getStepNav
=
()
=>
{
return
wrapper
.
findComponent
(
StepNav
);
};
const
mockNextClick
=
()
=>
{
getStepNav
().
vm
.
$emit
(
'
next
'
);
};
const
mockPrevClick
=
()
=>
{
getStepNav
().
vm
.
$emit
(
'
back
'
);
};
const
expectFalsyAttributeValue
=
(
testedWrapper
,
attributeName
)
=>
{
expect
([
false
,
null
,
undefined
]).
toContain
(
testedWrapper
.
attributes
(
attributeName
));
};
const
findInputWrappers
=
()
=>
{
const
inputWrappers
=
wrapper
.
findAllComponents
(
InputWrapper
);
input1
=
inputWrappers
.
at
(
0
);
input2
=
inputWrappers
.
at
(
1
);
};
const
createComponent
=
(
props
=
{})
=>
{
const
template
=
parseDocument
(
stepTemplate
).
get
(
'
template
'
);
const
defaultProps
=
{
inputs
,
template
,
};
wrapper
=
shallowMountExtended
(
PipelineWizardStep
,
{
propsData
:
{
...
defaultProps
,
compiled
:
parseDocument
(
compiledYamlBeforeSetup
),
...
props
,
},
});
};
afterEach
(
async
()
=>
{
await
wrapper
.
destroy
();
});
describe
(
'
input children
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
});
it
(
'
mounts an inputWrapper for each input type
'
,
()
=>
{
forEachInputWrapper
((
inputWrapper
,
i
)
=>
expect
(
inputWrapper
.
attributes
(
'
widget
'
)).
toBe
(
inputs
[
i
].
widget
),
);
});
it
(
'
passes all unused props to the inputWrapper
'
,
()
=>
{
const
pickChildProperties
=
(
from
)
=>
{
return
omit
(
from
,
[
'
target
'
,
'
widget
'
]);
};
forEachInputWrapper
((
inputWrapper
,
i
)
=>
{
const
expectedProps
=
pickChildProperties
(
inputs
[
i
]);
Object
.
entries
(
expectedProps
).
forEach
(([
key
,
value
])
=>
{
expect
(
inputWrapper
.
attributes
(
key
.
toLowerCase
())).
toEqual
(
value
.
toString
());
});
});
});
});
const
yamlDocument
=
new
Document
({
foo
:
{
bar
:
'
baz
'
}
});
const
yamlNode
=
yamlDocument
.
get
(
'
foo
'
);
describe
(
'
prop validation
'
,
()
=>
{
describe
.
each
`
componentProp | required | valid | invalid
${
'
inputs
'
}
|
${
true
}
|
${[
inputs
,
[]]}
|
${[[
'
invalid
'
],
[
null
],
[{},
{}]]}
${
'
template
'
}
|
${
true
}
|
${[
yamlNode
]}
|
${[
'
invalid
'
,
null
,
{
foo
:
1
},
yamlDocument
]}
${
'
compiled
'
}
|
${
true
}
|
${[
yamlDocument
]}
|
${[
'
invalid
'
,
null
,
{
foo
:
1
},
yamlNode
]}
`
(
'
testing `$componentProp` prop
'
,
({
componentProp
,
required
,
valid
,
invalid
})
=>
{
it
(
'
expects prop to be required
'
,
()
=>
{
expect
(
PipelineWizardStep
.
props
[
componentProp
].
required
).
toEqual
(
required
);
});
it
(
'
prop validators return false for invalid types
'
,
()
=>
{
const
validatorFunc
=
PipelineWizardStep
.
props
[
componentProp
].
validator
;
invalid
.
forEach
((
invalidType
)
=>
{
expect
(
validatorFunc
(
invalidType
)).
toBe
(
false
);
});
});
it
(
'
prop validators return true for valid types
'
,
()
=>
{
const
validatorFunc
=
PipelineWizardStep
.
props
[
componentProp
].
validator
;
valid
.
forEach
((
validType
)
=>
{
expect
(
validatorFunc
(
validType
)).
toBe
(
true
);
});
});
});
});
describe
(
'
navigation
'
,
()
=>
{
it
(
'
shows the next button
'
,
()
=>
{
createComponent
();
expect
(
getStepNav
().
attributes
(
'
nextbuttonenabled
'
)).
toEqual
(
'
true
'
);
});
it
(
'
does not show a back button if hasPreviousStep is false
'
,
()
=>
{
createComponent
({
hasPreviousStep
:
false
});
expectFalsyAttributeValue
(
getStepNav
(),
'
showbackbutton
'
);
});
it
(
'
shows a back button if hasPreviousStep is true
'
,
()
=>
{
createComponent
({
hasPreviousStep
:
true
});
expect
(
getStepNav
().
attributes
(
'
showbackbutton
'
)).
toBe
(
'
true
'
);
});
it
(
'
lets "back" event bubble upwards
'
,
async
()
=>
{
createComponent
();
await
mockPrevClick
();
await
nextTick
();
expect
(
wrapper
.
emitted
().
back
).
toBeTruthy
();
});
it
(
'
lets "next" event bubble upwards
'
,
async
()
=>
{
createComponent
();
await
mockNextClick
();
await
nextTick
();
expect
(
wrapper
.
emitted
().
next
).
toBeTruthy
();
});
});
describe
(
'
validation
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
hasNextPage
:
true
});
findInputWrappers
();
});
it
(
'
sets invalid once one input field has an invalid value
'
,
async
()
=>
{
input1
.
vm
.
$emit
(
'
update:valid
'
,
true
);
input2
.
vm
.
$emit
(
'
update:valid
'
,
false
);
await
mockNextClick
();
expectFalsyAttributeValue
(
getStepNav
(),
'
nextbuttonenabled
'
);
});
it
(
'
returns to valid state once the invalid input is valid again
'
,
async
()
=>
{
input1
.
vm
.
$emit
(
'
update:valid
'
,
true
);
input2
.
vm
.
$emit
(
'
update:valid
'
,
false
);
await
mockNextClick
();
expectFalsyAttributeValue
(
getStepNav
(),
'
nextbuttonenabled
'
);
input2
.
vm
.
$emit
(
'
update:valid
'
,
true
);
await
nextTick
();
expect
(
getStepNav
().
attributes
(
'
nextbuttonenabled
'
)).
toBe
(
'
true
'
);
});
it
(
'
passes validate state to all input wrapper children when next is clicked
'
,
async
()
=>
{
forEachInputWrapper
((
inputWrapper
)
=>
{
expectFalsyAttributeValue
(
inputWrapper
,
'
validate
'
);
});
await
mockNextClick
();
expect
(
input1
.
attributes
(
'
validate
'
)).
toBe
(
'
true
'
);
});
it
(
'
not emitting a valid state is considered valid
'
,
async
()
=>
{
// input1 does not emit a update:valid event
input2
.
vm
.
$emit
(
'
update:valid
'
,
true
);
await
mockNextClick
();
expect
(
getStepNav
().
attributes
(
'
nextbuttonenabled
'
)).
toBe
(
'
true
'
);
});
});
describe
(
'
template compilation
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
findInputWrappers
();
});
it
(
'
injects the template when an input wrapper emits a beforeUpdate:compiled event
'
,
async
()
=>
{
input1
.
vm
.
$emit
(
'
beforeUpdate:compiled
'
);
expect
(
wrapper
.
vm
.
compiled
.
toString
()).
toBe
(
compiledYamlAfterInitialLoad
);
});
it
(
'
lets the "update:compiled" event bubble upwards
'
,
async
()
=>
{
const
compiled
=
parseDocument
(
compiledYaml
);
await
input1
.
vm
.
$emit
(
'
update:compiled
'
,
compiled
);
const
updateEvents
=
wrapper
.
emitted
()[
'
update:compiled
'
];
const
latestUpdateEvent
=
updateEvents
[
updateEvents
.
length
-
1
];
expect
(
latestUpdateEvent
[
0
].
toString
()).
toBe
(
compiled
.
toString
());
});
});
});
spec/frontend/pipeline_wizard/mock/yaml.js
0 → 100644
View file @
c1462c63
export
const
stepInputs
=
`
- label: "Build Steps"
description: "Enter the steps necessary for your application."
widget: text
target: $BUILD_STEPS
- label: "Select a deployment branch"
description: "Select the branch we should use to generate your site from."
widget: text
target: $BRANCH
pattern: "^[a-z]+$"
invalidFeedback: "This field may only contain lowercase letters"
required: true
`
;
export
const
stepTemplate
=
`template:
pages:
script: $BUILD_STEPS
artifacts:
paths:
- public
only:
- $BRANCH
`
;
export
const
compiledYamlBeforeSetup
=
`abc: def`
;
export
const
compiledYamlAfterInitialLoad
=
`abc: def
pages:
script: $BUILD_STEPS
artifacts:
paths:
- public
only:
- $BRANCH
`
;
export
const
compiledYaml
=
`abc: def
pages:
script: foo
artifacts:
paths:
- public
only:
- bar
`
;
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment