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
6f5bf2cf
Commit
6f5bf2cf
authored
May 15, 2018
by
Filipa Lacerda
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Handles action icons requests in a contained way and shows a loading icon to the user
parent
db32e49e
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
171 additions
and
112 deletions
+171
-112
app/assets/javascripts/pipelines/components/graph/action_component.vue
...vascripts/pipelines/components/graph/action_component.vue
+43
-24
app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
...pts/pipelines/components/graph/dropdown_job_component.vue
+6
-7
app/assets/javascripts/pipelines/components/graph/graph_component.vue
...avascripts/pipelines/components/graph/graph_component.vue
+5
-6
app/assets/javascripts/pipelines/components/graph/job_component.vue
.../javascripts/pipelines/components/graph/job_component.vue
+6
-6
app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
...pts/pipelines/components/graph/stage_column_component.vue
+6
-6
app/assets/javascripts/pipelines/components/pipelines_table_row.vue
.../javascripts/pipelines/components/pipelines_table_row.vue
+1
-0
app/assets/javascripts/pipelines/components/stage.vue
app/assets/javascripts/pipelines/components/stage.vue
+17
-0
app/assets/javascripts/pipelines/pipeline_details_bundle.js
app/assets/javascripts/pipelines/pipeline_details_bundle.js
+8
-22
spec/javascripts/pipelines/graph/action_component_spec.js
spec/javascripts/pipelines/graph/action_component_spec.js
+30
-41
spec/javascripts/pipelines/stage_spec.js
spec/javascripts/pipelines/stage_spec.js
+49
-0
No files found.
app/assets/javascripts/pipelines/components/graph/action_component.vue
View file @
6f5bf2cf
<
script
>
import
$
from
'
jquery
'
;
import
tooltip
from
'
../../../vue_shared/directives/tooltip
'
;
import
Icon
from
'
../../../vue_shared/components/icon.vue
'
;
import
{
dasherize
}
from
'
../../../lib/utils/text_utility
'
;
import
eventHub
from
'
../../event_hub
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
dasherize
}
from
'
~/lib/utils/text_utility
'
;
import
{
__
}
from
'
~/locale
'
;
import
createFlash
from
'
~/flash
'
;
import
tooltip
from
'
~/vue_shared/directives/tooltip
'
;
import
LoadingIcon
from
'
~/vue_shared/components/loading_icon.vue
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
/**
* Renders either a cancel, retry or play icon pointing to the given path.
* Renders either a cancel, retry or play icon button and handles the post request
*
* Used in:
* - mr widget mini pipeline graph: `mr_widget_pipeline.vue`
* - pipelines table
* - pipelines table in merge request page
* - pipelines table in commit page
* - pipelines detail page in big graph
*/
export
default
{
components
:
{
Icon
,
LoadingIcon
,
},
directives
:
{
...
...
@@ -32,16 +44,10 @@ export default {
required
:
true
,
},
requestFinishedFor
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
data
()
{
return
{
isDisabled
:
false
,
linkRequested
:
''
,
isLoading
:
false
,
};
},
...
...
@@ -51,19 +57,28 @@ export default {
return
`
${
actionIconDash
}
js-icon-
${
actionIconDash
}
`
;
},
},
watch
:
{
requestFinishedFor
()
{
if
(
this
.
requestFinishedFor
===
this
.
linkRequested
)
{
this
.
isDisabled
=
false
;
}
},
},
methods
:
{
/**
* The request should not be handled here.
* However due to this component being used in several
* different apps it avoids repetition & complexity.
*
*/
onClickAction
()
{
$
(
this
.
$el
).
tooltip
(
'
hide
'
);
eventHub
.
$emit
(
'
postAction
'
,
this
.
link
);
this
.
linkRequested
=
this
.
link
;
this
.
isDisabled
=
true
;
this
.
isLoading
=
true
;
axios
.
post
(
`
${
this
.
link
}
.json`
)
.
then
(()
=>
{
this
.
isLoading
=
false
;
this
.
$emit
(
'
pipelineActionRequestComplete
'
);
})
.
catch
(()
=>
{
this
.
isLoading
=
false
;
createFlash
(
__
(
'
An error occurred while making the request.
'
));
});
},
},
};
...
...
@@ -78,8 +93,12 @@ export default {
btn-transparent ci-action-icon-container ci-action-icon-wrapper"
:class=
"cssClass"
data-container=
"body"
:disabled=
"is
Disabled
"
:disabled=
"is
Loading
"
>
<icon
:name=
"actionIcon"
/>
<icon
v-if=
"!isLoading"
:name=
"actionIcon"
/>
<loading-icon
v-else
/>
</button>
</
template
>
app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
View file @
6f5bf2cf
...
...
@@ -42,11 +42,6 @@ export default {
type
:
Object
,
required
:
true
,
},
requestFinishedFor
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
computed
:
{
...
...
@@ -76,11 +71,15 @@ export default {
e
.
stopPropagation
();
});
},
pipelineActionRequestComplete
()
{
this
.
$emit
(
'
pipelineActionRequestComplete
'
);
},
},
};
</
script
>
<
template
>
<div
class=
"ci-job-dropdown-container"
>
<div
class=
"ci-job-dropdown-container
dropdown
"
>
<button
v-tooltip
type=
"button"
...
...
@@ -110,7 +109,7 @@ export default {
<job-component
:job=
"item"
css-class-job-name=
"mini-pipeline-graph-dropdown-item"
:request-finished-for=
"requestFinishedFor
"
@
pipelineActionRequestComplete=
"pipelineActionRequestComplete
"
/>
</li>
</ul>
...
...
app/assets/javascripts/pipelines/components/graph/graph_component.vue
View file @
6f5bf2cf
...
...
@@ -18,11 +18,6 @@ export default {
type
:
Object
,
required
:
true
,
},
requestFinishedFor
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
computed
:
{
...
...
@@ -66,6 +61,10 @@ export default {
return
className
;
},
refreshPipelineGraph
()
{
this
.
$emit
(
'
refreshPipelineGraph
'
);
},
},
};
</
script
>
...
...
@@ -105,7 +104,7 @@ export default {
:key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
:request-finished-for="requestFinishedFor
"
@refreshPipelineGraph="refreshPipelineGraph
"
:has-triggered-by="hasTriggeredBy"
/>
</ul>
...
...
app/assets/javascripts/pipelines/components/graph/job_component.vue
View file @
6f5bf2cf
...
...
@@ -46,11 +46,6 @@ export default {
required
:
false
,
default
:
''
,
},
requestFinishedFor
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
computed
:
{
status
()
{
...
...
@@ -84,6 +79,11 @@ export default {
return
this
.
job
.
status
&&
this
.
job
.
status
.
action
&&
this
.
job
.
status
.
action
.
path
;
},
},
methods
:
{
pipelineActionRequestComplete
()
{
this
.
$emit
(
'
pipelineActionRequestComplete
'
);
},
},
};
</
script
>
<
template
>
...
...
@@ -126,7 +126,7 @@ export default {
:tooltip-text=
"status.action.title"
:link=
"status.action.path"
:action-icon=
"status.action.icon"
:request-finished-for=
"requestFinishedFor
"
@
pipelineActionRequestComplete=
"pipelineActionRequestComplete
"
/>
</div>
</
template
>
app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
View file @
6f5bf2cf
...
...
@@ -30,11 +30,6 @@ export default {
default
:
''
,
},
requestFinishedFor
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
hasTriggeredBy
:
{
type
:
Boolean
,
required
:
true
,
...
...
@@ -53,6 +48,10 @@ export default {
buildConnnectorClass
(
index
)
{
return
index
===
0
&&
!
this
.
isFirstColumn
?
'
left-connector
'
:
''
;
},
pipelineActionRequestComplete
()
{
this
.
$emit
(
'
refreshPipelineGraph
'
);
},
},
};
</
script
>
...
...
@@ -81,12 +80,13 @@ export default {
v-if=
"job.size === 1"
:job=
"job"
css-class-job-name=
"build-content"
@
pipelineActionRequestComplete=
"pipelineActionRequestComplete"
/>
<dropdown-job-component
v-if=
"job.size > 1"
:job=
"job"
:request-finished-for=
"requestFinishedFor
"
@
pipelineActionRequestComplete=
"pipelineActionRequestComplete
"
/>
</li>
...
...
app/assets/javascripts/pipelines/components/pipelines_table_row.vue
View file @
6f5bf2cf
...
...
@@ -297,6 +297,7 @@
v-for=
"(stage, index) in pipeline.details.stages"
:key=
"index"
>
<pipeline-stage
type=
"PIPELINES_TABLE"
:stage=
"stage"
:update-dropdown=
"updateGraphDropdown"
/>
...
...
app/assets/javascripts/pipelines/components/stage.vue
View file @
6f5bf2cf
...
...
@@ -44,6 +44,12 @@ export default {
required
:
false
,
default
:
false
,
},
type
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
data
()
{
...
...
@@ -133,6 +139,16 @@ export default {
isDropdownOpen
()
{
return
this
.
$el
.
classList
.
contains
(
'
open
'
);
},
pipelineActionRequestComplete
()
{
if
(
this
.
type
===
'
PIPELINES_TABLE
'
)
{
// warn the table to update
eventHub
.
$emit
(
'
clickedDropdown
'
);
}
else
{
// refresh the content
this
.
fetchJobs
();
}
},
},
};
</
script
>
...
...
@@ -188,6 +204,7 @@ export default {
<job-component
:job=
"job"
css-class-job-name=
"mini-pipeline-graph-dropdown-item"
@
pipelineActionRequestComplete=
"pipelineActionRequestComplete"
/>
</li>
</ul>
...
...
app/assets/javascripts/pipelines/pipeline_details_bundle.js
View file @
6f5bf2cf
...
...
@@ -29,30 +29,14 @@ export default () => {
data
()
{
return
{
mediator
,
requestFinishedFor
:
null
,
};
},
created
()
{
eventHub
.
$on
(
'
postAction
'
,
this
.
postAction
);
},
beforeDestroy
()
{
eventHub
.
$off
(
'
postAction
'
,
this
.
postAction
);
},
methods
:
{
postAction
(
action
)
{
// Click was made, reset this variable
this
.
requestFinishedFor
=
null
;
this
.
mediator
.
service
.
postAction
(
action
)
.
then
(()
=>
{
this
.
mediator
.
refreshPipeline
();
this
.
requestFinishedFor
=
action
;
})
.
catch
(()
=>
{
this
.
requestFinishedFor
=
action
;
Flash
(
__
(
'
An error occurred while making the request.
'
));
});
requestRefreshPipelineGraph
()
{
// When an action is clicked
// (wether in the dropdown or in the main nodes, we refresh the big graph)
this
.
mediator
.
refreshPipeline
()
.
catch
(()
=>
Flash
(
__
(
'
An error occurred while making the request.
'
)));
},
},
render
(
createElement
)
{
...
...
@@ -60,7 +44,9 @@ export default () => {
props
:
{
isLoading
:
this
.
mediator
.
state
.
isLoading
,
pipeline
:
this
.
mediator
.
store
.
state
.
pipeline
,
requestFinishedFor
:
this
.
requestFinishedFor
,
},
on
:
{
refreshPipelineGraph
:
this
.
requestRefreshPipelineGraph
,
},
});
},
...
...
spec/javascripts/pipelines/graph/action_component_spec.js
View file @
6f5bf2cf
import
Vue
from
'
vue
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
actionComponent
from
'
~/pipelines/components/graph/action_component.vue
'
;
import
eventHub
from
'
~/pipelines/event_hub
'
;
import
mountComponent
from
'
../../helpers/vue_mount_component_helper
'
;
describe
(
'
pipeline graph action component
'
,
()
=>
{
let
component
;
let
mock
;
beforeEach
(
done
=>
{
const
ActionComponent
=
Vue
.
extend
(
actionComponent
);
mock
=
new
MockAdapter
(
axios
);
mock
.
onPost
(
'
foo.json
'
).
reply
(
200
);
component
=
mountComponent
(
ActionComponent
,
{
tooltipText
:
'
bar
'
,
link
:
'
foo
'
,
...
...
@@ -18,15 +24,10 @@ describe('pipeline graph action component', () => {
});
afterEach
(()
=>
{
mock
.
restore
();
component
.
$destroy
();
});
it
(
'
should emit an event with the provided link
'
,
()
=>
{
eventHub
.
$on
(
'
postAction
'
,
link
=>
{
expect
(
link
).
toEqual
(
'
foo
'
);
});
});
it
(
'
should render the provided title as a bootstrap tooltip
'
,
()
=>
{
expect
(
component
.
$el
.
getAttribute
(
'
data-original-title
'
)).
toEqual
(
'
bar
'
);
});
...
...
@@ -34,10 +35,12 @@ describe('pipeline graph action component', () => {
it
(
'
should update bootstrap tooltip when title changes
'
,
done
=>
{
component
.
tooltipText
=
'
changed
'
;
setTimeout
(()
=>
{
component
.
$nextTick
()
.
then
(()
=>
{
expect
(
component
.
$el
.
getAttribute
(
'
data-original-title
'
)).
toBe
(
'
changed
'
);
done
();
});
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'
should render an svg
'
,
()
=>
{
...
...
@@ -45,44 +48,30 @@ describe('pipeline graph action component', () => {
expect
(
component
.
$el
.
querySelector
(
'
svg
'
)).
toBeDefined
();
});
it
(
'
disables the button when clicked
'
,
done
=>
{
component
.
$el
.
click
();
component
.
$nextTick
(()
=>
{
expect
(
component
.
$el
.
getAttribute
(
'
disabled
'
)).
toEqual
(
'
disabled
'
);
done
();
});
});
it
(
'
re-enabled the button when `requestFinishedFor` matches `linkRequested`
'
,
done
=>
{
component
.
$el
.
click
();
it
(
'
renders a loading icon while component is loading
'
,
done
=>
{
component
.
isLoading
=
true
;
component
.
$nextTick
()
component
.
$nextTick
()
.
then
(()
=>
{
expect
(
component
.
$el
.
getAttribute
(
'
disabled
'
)).
toEqual
(
'
disabled
'
);
component
.
requestFinishedFor
=
'
foo
'
;
})
.
then
(()
=>
{
expect
(
component
.
$el
.
getAttribute
(
'
disabled
'
)).
toBeNull
();
expect
(
component
.
$el
.
querySelector
(
'
.fa-spin
'
)).
not
.
toBeNull
();
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'
does not re-enable the button when `requestFinishedFor` does not matches `linkRequested`
'
,
done
=>
{
component
.
$el
.
click
();
describe
(
'
on click
'
,
()
=>
{
it
(
'
emits `pipelineActionRequestComplete` after a successfull request
'
,
done
=>
{
spyOn
(
component
,
'
$emit
'
);
component
.
$nextTick
()
.
then
(()
=>
{
expect
(
component
.
$el
.
getAttribute
(
'
disabled
'
)).
toEqual
(
'
disabled
'
);
component
.
requestFinishedFor
=
'
bar
'
;
})
.
then
(()
=>
{
expect
(
component
.
$el
.
getAttribute
(
'
disabled
'
)).
toEqual
(
'
disabled
'
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
component
.
$el
.
click
();
expect
(
component
.
isLoading
).
toEqual
(
true
);
component
.
$nextTick
()
.
then
(()
=>
{
expect
(
component
.
$emit
).
toHaveBeenCalledWith
(
'
pipelineActionRequestComplete
'
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
});
});
spec/javascripts/pipelines/stage_spec.js
View file @
6f5bf2cf
...
...
@@ -102,4 +102,53 @@ describe('Pipelines stage component', () => {
});
});
});
describe
(
'
pipelineActionRequestComplete
'
,
()
=>
{
beforeEach
(()
=>
{
mock
.
onGet
(
'
path.json
'
).
reply
(
200
,
stageReply
);
mock
.
onPost
(
`
${
stageReply
.
latest_statuses
[
0
].
status
.
action
.
path
}
.json`
).
reply
(
200
);
});
describe
(
'
within pipeline table
'
,
()
=>
{
it
(
'
emits `clickedDropdown` event when `pipelineActionRequestComplete` is triggered
'
,
done
=>
{
spyOn
(
eventHub
,
'
$emit
'
);
component
.
type
=
'
PIPELINES_TABLE
'
;
component
.
$el
.
querySelector
(
'
button
'
).
click
();
setTimeout
(()
=>
{
component
.
$el
.
querySelector
(
'
.js-ci-action
'
).
click
();
component
.
$nextTick
()
.
then
(()
=>
{
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'
clickedDropdown
'
);
expect
(
eventHub
.
$emit
).
toHaveBeenCalledTimes
(
2
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
},
0
);
});
});
describe
(
'
without a type
'
,
()
=>
{
it
(
'
fetches dropdown content again
'
,
done
=>
{
spyOn
(
component
,
'
fetchJobs
'
).
and
.
callThrough
();
component
.
$el
.
querySelector
(
'
button
'
).
click
();
expect
(
component
.
fetchJobs
).
toHaveBeenCalledTimes
(
1
);
setTimeout
(()
=>
{
component
.
$el
.
querySelector
(
'
.js-ci-action
'
).
click
();
component
.
$nextTick
()
.
then
(()
=>
{
expect
(
component
.
fetchJobs
).
toHaveBeenCalledTimes
(
2
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
},
0
);
});
});
});
});
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