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
0
Merge Requests
0
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
Boxiang Sun
gitlab-ce
Commits
e69732e2
Commit
e69732e2
authored
May 04, 2017
by
Filipa Lacerda
Committed by
Phil Hughes
May 04, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Pipeline table mini graph dropdown remains open when table is refreshed
parent
dcdced81
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
309 additions
and
99 deletions
+309
-99
app/assets/javascripts/commit/pipelines/pipelines_table.js
app/assets/javascripts/commit/pipelines/pipelines_table.js
+10
-1
app/assets/javascripts/pipelines/components/stage.vue
app/assets/javascripts/pipelines/components/stage.vue
+173
-0
app/assets/javascripts/pipelines/pipelines.js
app/assets/javascripts/pipelines/pipelines.js
+10
-1
app/assets/javascripts/vue_shared/components/pipelines_table.js
...sets/javascripts/vue_shared/components/pipelines_table.js
+9
-2
app/assets/javascripts/vue_shared/components/pipelines_table_row.js
.../javascripts/vue_shared/components/pipelines_table_row.js
+11
-2
app/assets/stylesheets/pages/pipelines.scss
app/assets/stylesheets/pages/pipelines.scss
+22
-28
app/views/shared/_mini_pipeline_graph.html.haml
app/views/shared/_mini_pipeline_graph.html.haml
+4
-4
changelogs/unreleased/31558-job-dropdown.yml
changelogs/unreleased/31558-job-dropdown.yml
+4
-0
spec/javascripts/fixtures/mini_dropdown_graph.html.haml
spec/javascripts/fixtures/mini_dropdown_graph.html.haml
+3
-3
spec/javascripts/pipelines/stage_spec.js
spec/javascripts/pipelines/stage_spec.js
+63
-58
No files found.
app/assets/javascripts/commit/pipelines/pipelines_table.js
View file @
e69732e2
...
...
@@ -46,6 +46,7 @@ export default Vue.component('pipelines-table', {
isLoading
:
false
,
hasError
:
false
,
isMakingRequest
:
false
,
updateGraphDropdown
:
false
,
};
},
...
...
@@ -130,15 +131,21 @@ export default Vue.component('pipelines-table', {
const
pipelines
=
response
.
pipelines
||
response
;
this
.
store
.
storePipelines
(
pipelines
);
this
.
isLoading
=
false
;
this
.
updateGraphDropdown
=
true
;
},
errorCallback
()
{
this
.
hasError
=
true
;
this
.
isLoading
=
false
;
this
.
updateGraphDropdown
=
false
;
},
setIsMakingRequest
(
isMakingRequest
)
{
this
.
isMakingRequest
=
isMakingRequest
;
if
(
isMakingRequest
)
{
this
.
updateGraphDropdown
=
false
;
}
},
},
...
...
@@ -163,7 +170,9 @@ export default Vue.component('pipelines-table', {
v-if="shouldRenderTable">
<pipelines-table-component
:pipelines="state.pipelines"
:service="service" />
:service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</div>
</div>
`
,
...
...
app/assets/javascripts/pipelines/components/stage.
js
→
app/assets/javascripts/pipelines/components/stage.
vue
View file @
e69732e2
<
script
>
/**
* Renders each stage of the pipeline mini graph.
*
* Given the provided endpoint will make a request to
* fetch the dropdown data when the stage is clicked.
*
* Request is made inside this component to make it reusable between:
* 1. Pipelines main table
* 2. Pipelines table in commit and Merge request views
* 3. Merge request widget
* 4. Commit widget
*/
/* global Flash */
import
StatusIconEntityMap
from
'
../../ci_status_icons
'
;
...
...
@@ -7,36 +22,55 @@ export default {
type
:
Object
,
required
:
true
,
},
updateDropdown
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
data
()
{
return
{
builds
:
''
,
spinner
:
'
<span class="fa fa-spinner fa-spin"></span>
'
,
isLoading
:
false
,
dropdownContent
:
''
,
endpoint
:
this
.
stage
.
dropdown_path
,
};
},
updated
()
{
if
(
this
.
builds
)
{
if
(
this
.
dropdownContent
.
length
>
0
)
{
this
.
stopDropdownClickPropagation
();
}
},
methods
:
{
fetchBuilds
(
e
)
{
const
ariaExpanded
=
e
.
currentTarget
.
attributes
[
'
aria-expanded
'
];
watch
:
{
updateDropdown
()
{
if
(
this
.
updateDropdown
&&
this
.
isDropdownOpen
()
&&
!
this
.
isLoading
)
{
this
.
fetchJobs
();
}
},
},
if
(
ariaExpanded
&&
(
ariaExpanded
.
textContent
===
'
true
'
))
return
null
;
methods
:
{
onClickStage
()
{
if
(
!
this
.
isDropdownOpen
())
{
this
.
isLoading
=
true
;
this
.
fetchJobs
();
}
},
return
this
.
$http
.
get
(
this
.
stage
.
dropdown_path
)
fetchJobs
()
{
this
.
$http
.
get
(
this
.
endpoint
)
.
then
((
response
)
=>
{
this
.
builds
=
JSON
.
parse
(
response
.
body
).
html
;
this
.
dropdownContent
=
response
.
json
().
html
;
this
.
isLoading
=
false
;
})
.
catch
(()
=>
{
// If dropdown is opened we'll close it.
if
(
this
.
$el
.
classList
.
contains
(
'
open
'
))
{
$
(
this
.
$refs
.
dropdown
).
dropdown
(
'
toggle
'
);
}
this
.
closeDropdown
();
this
.
isLoading
=
false
;
const
flash
=
new
Flash
(
'
Something went wrong on our end.
'
);
return
flash
;
...
...
@@ -57,59 +91,83 @@ export default {
e
.
stopPropagation
();
});
},
closeDropdown
()
{
if
(
this
.
isDropdownOpen
())
{
$
(
this
.
$refs
.
dropdown
).
dropdown
(
'
toggle
'
);
}
},
isDropdownOpen
()
{
return
this
.
$el
.
classList
.
contains
(
'
open
'
);
},
},
computed
:
{
buildsOrSpinner
()
{
return
this
.
builds
?
this
.
builds
:
this
.
spinner
;
},
dropdownClass
()
{
if
(
this
.
builds
)
return
'
js-builds-dropdown-container
'
;
return
'
js-builds-dropdown-loading builds-dropdown-loading
'
;
},
buildStatus
()
{
return
`Build:
${
this
.
stage
.
status
.
label
}
`
;
},
tooltip
()
{
return
`has-tooltip ci-status-icon ci-status-icon-
${
this
.
stage
.
status
.
group
}
`
;
return
this
.
dropdownContent
.
length
>
0
?
'
js-builds-dropdown-container
'
:
'
js-builds-dropdown-loading
'
;
},
triggerButtonClass
()
{
return
`
mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button
ci-status-icon-
${
this
.
stage
.
status
.
group
}
`
;
return
`ci-status-icon-
${
this
.
stage
.
status
.
group
}
`
;
},
svgHTML
()
{
svgIcon
()
{
return
StatusIconEntityMap
[
this
.
stage
.
status
.
icon
];
},
},
template
:
`
<div>
<button
@click="fetchBuilds($event)"
:class="triggerButtonClass"
:title="stage.title"
data-placement="top"
data-toggle="dropdown"
type="button"
:aria-label="stage.title"
ref="dropdown">
<span
v-html="svgHTML"
aria-hidden="true">
</span>
<i
class="fa fa-caret-down"
aria-hidden="true" />
</button>
<ul
ref="dropdown-content"
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div
class="arrow-up"
aria-hidden="true"></div>
};
</
script
>
<
template
>
<div
class=
"dropdown"
>
<button
:class=
"triggerButtonClass"
@
click=
"onClickStage"
class=
"mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button"
:title=
"stage.title"
data-placement=
"top"
data-toggle=
"dropdown"
type=
"button"
id=
"stageDropdown"
aria-haspopup=
"true"
aria-expanded=
"false"
>
<span
v-html=
"svgIcon"
aria-hidden=
"true"
:aria-label=
"stage.title"
>
</span>
<i
class=
"fa fa-caret-down"
aria-hidden=
"true"
>
</i>
</button>
<ul
class=
"dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"
aria-labelledby=
"stageDropdown"
>
<li
:class=
"dropdownClass"
class=
"js-builds-dropdown-list scrollable-menu"
>
<div
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu"
v-html="buildsOrSpinner">
class=
"text-center"
v-if=
"isLoading"
>
<i
class=
"fa fa-spin fa-spinner"
aria-hidden=
"true"
aria-label=
"Loading"
>
</i>
</div>
</ul>
</div>
`
,
};
<ul
v-else
v-html=
"dropdownContent"
>
</ul>
</li>
</ul>
</div>
</script>
app/assets/javascripts/pipelines/pipelines.js
View file @
e69732e2
...
...
@@ -49,6 +49,7 @@ export default {
isLoading
:
false
,
hasError
:
false
,
isMakingRequest
:
false
,
updateGraphDropdown
:
false
,
};
},
...
...
@@ -198,15 +199,21 @@ export default {
this
.
store
.
storePagination
(
response
.
headers
);
this
.
isLoading
=
false
;
this
.
updateGraphDropdown
=
true
;
},
errorCallback
()
{
this
.
hasError
=
true
;
this
.
isLoading
=
false
;
this
.
updateGraphDropdown
=
false
;
},
setIsMakingRequest
(
isMakingRequest
)
{
this
.
isMakingRequest
=
isMakingRequest
;
if
(
isMakingRequest
)
{
this
.
updateGraphDropdown
=
false
;
}
},
},
...
...
@@ -263,7 +270,9 @@ export default {
<pipelines-table-component
:pipelines="state.pipelines"
:service="service"/>
:service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</div>
<gl-pagination
...
...
app/assets/javascripts/vue_shared/components/pipelines_table.js
View file @
e69732e2
...
...
@@ -10,13 +10,18 @@ export default {
pipelines
:
{
type
:
Array
,
required
:
true
,
default
:
()
=>
([]),
},
service
:
{
type
:
Object
,
required
:
true
,
},
updateGraphDropdown
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
components
:
{
...
...
@@ -40,7 +45,9 @@ export default {
v-bind:model="model">
<tr is="pipelines-table-row-component"
:pipeline="model"
:service="service"></tr>
:service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</template>
</tbody>
</table>
...
...
app/assets/javascripts/vue_shared/components/pipelines_table_row.js
View file @
e69732e2
...
...
@@ -3,7 +3,7 @@ import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
import
PipelinesActionsComponent
from
'
../../pipelines/components/pipelines_actions
'
;
import
PipelinesArtifactsComponent
from
'
../../pipelines/components/pipelines_artifacts
'
;
import
PipelinesStatusComponent
from
'
../../pipelines/components/status
'
;
import
PipelinesStageComponent
from
'
../../pipelines/components/stage
'
;
import
PipelinesStageComponent
from
'
../../pipelines/components/stage
.vue
'
;
import
PipelinesUrlComponent
from
'
../../pipelines/components/pipeline_url
'
;
import
PipelinesTimeagoComponent
from
'
../../pipelines/components/time_ago
'
;
import
CommitComponent
from
'
./commit
'
;
...
...
@@ -24,6 +24,12 @@ export default {
type
:
Object
,
required
:
true
,
},
updateGraphDropdown
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
components
:
{
...
...
@@ -213,7 +219,10 @@ export default {
<div class="stage-container dropdown js-mini-pipeline-graph"
v-if="pipeline.details.stages.length > 0"
v-for="stage in pipeline.details.stages">
<dropdown-stage :stage="stage"/>
<dropdown-stage
:stage="stage"
:update-dropdown="updateGraphDropdown"/>
</div>
</td>
...
...
app/assets/stylesheets/pages/pipelines.scss
View file @
e69732e2
...
...
@@ -781,16 +781,11 @@
}
.scrollable-menu
{
padding
:
0
;
max-height
:
245px
;
overflow
:
auto
;
}
// Loading icon
.builds-dropdown-loading
{
margin
:
0
auto
;
width
:
20px
;
}
// Action icon on the right
a
.ci-action-icon-wrapper
{
color
:
$action-icon-color
;
...
...
@@ -893,30 +888,29 @@
* Top arrow in the dropdown in the mini pipeline graph
*/
.mini-pipeline-graph-dropdown-menu
{
.arrow-up
{
&
:
:
before
,
&::
after
{
content
:
''
;
display
:
inline-block
;
position
:
absolute
;
width
:
0
;
height
:
0
;
border-color
:
transparent
;
border-style
:
solid
;
top
:
-6px
;
left
:
2px
;
border-width
:
0
5px
6px
;
}
&
:
:
before
{
border-width
:
0
5px
5px
;
border-bottom-color
:
$border-color
;
}
&
:
:
before
,
&::
after
{
content
:
''
;
display
:
inline-block
;
position
:
absolute
;
width
:
0
;
height
:
0
;
border-color
:
transparent
;
border-style
:
solid
;
top
:
-6px
;
left
:
2px
;
border-width
:
0
5px
6px
;
}
&
:
:
after
{
margin-top
:
1px
;
border-bottom-color
:
$white-light
;
}
&
:
:
before
{
border-width
:
0
5px
5px
;
border-bottom-color
:
$border-color
;
}
&
:
:
after
{
margin-top
:
1px
;
border-bottom-color
:
$white-light
;
}
}
...
...
app/views/shared/_mini_pipeline_graph.html.haml
View file @
e69732e2
...
...
@@ -11,8 +11,8 @@
=
icon
(
'caret-down'
)
%ul
.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
.arrow-up
.js-builds-dropdown-list.scrollable-menu
%li
.js-builds-dropdown-list.scrollable-menu
.js-builds-dropdown-loading.builds-dropdown-loading.hidden
%span
.fa.fa-spinner.fa-spin
%li
.js-builds-dropdown-loading.hidden
.text-center
%i
.fa.fa-spinner.fa-spin
{
'aria-hidden'
:
'true'
,
'aria-label'
:
'Loading'
}
changelogs/unreleased/31558-job-dropdown.yml
0 → 100644
View file @
e69732e2
---
title
:
Job dropdown of pipeline mini graph updates in realtime when its opened
merge_request
:
author
:
spec/javascripts/fixtures/mini_dropdown_graph.html.haml
View file @
e69732e2
...
...
@@ -3,7 +3,7 @@
Dropdown
%ul
.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
.js-builds-dropdown-list.scrollable-menu
%li
.js-builds-dropdown-list.scrollable-menu
.js-builds-dropdown-loading.
builds-dropdown-loading.hidden
%span
.fa.fa-spinner
.fa-spin
%li
.js-
builds-dropdown-loading.hidden
%span
.fa.fa-spinner
spec/javascripts/pipelines/stage_spec.js
View file @
e69732e2
import
Vue
from
'
vue
'
;
import
{
SUCCESS_SVG
}
from
'
~/ci_status_icons
'
;
import
Stage
from
'
~/pipelines/components/stage
'
;
import
stage
from
'
~/pipelines/components/stage.vue
'
;
describe
(
'
Pipelines stage component
'
,
()
=>
{
let
StageComponent
;
let
component
;
beforeEach
(()
=>
{
StageComponent
=
Vue
.
extend
(
stage
);
component
=
new
StageComponent
({
propsData
:
{
stage
:
{
status
:
{
group
:
'
success
'
,
icon
:
'
icon_status_success
'
,
title
:
'
success
'
,
},
dropdown_path
:
'
foo
'
,
},
updateDropdown
:
false
,
},
}).
$mount
();
});
function
minify
(
string
)
{
return
string
.
replace
(
/
\s
/g
,
''
);
}
it
(
'
should render a dropdown with the status icon
'
,
()
=>
{
expect
(
component
.
$el
.
getAttribute
(
'
class
'
)).
toEqual
(
'
dropdown
'
);
expect
(
component
.
$el
.
querySelector
(
'
svg
'
)).
toBeDefined
();
expect
(
component
.
$el
.
querySelector
(
'
button
'
).
getAttribute
(
'
data-toggle
'
)).
toEqual
(
'
dropdown
'
);
});
describe
(
'
Pipelines Stage
'
,
()
=>
{
describe
(
'
data
'
,
()
=>
{
let
stageReturnValue
;
describe
(
'
with successfull request
'
,
()
=>
{
const
interceptor
=
(
request
,
next
)
=>
{
next
(
request
.
respondWith
(
JSON
.
stringify
({
html
:
'
foo
'
}),
{
status
:
200
,
}));
};
beforeEach
(()
=>
{
stageReturnValue
=
Stage
.
data
(
);
Vue
.
http
.
interceptors
.
push
(
interceptor
);
});
it
(
'
should return object with .builds and .spinner
'
,
()
=>
{
expect
(
stageReturnValue
).
toEqual
({
builds
:
''
,
spinner
:
'
<span class="fa fa-spinner fa-spin"></span>
'
,
});
afterEach
(()
=>
{
Vue
.
http
.
interceptors
=
_
.
without
(
Vue
.
http
.
interceptors
,
interceptor
,
);
});
});
describe
(
'
computed
'
,
()
=>
{
describe
(
'
svgHTML
'
,
function
()
{
let
stage
;
let
svgHTML
;
it
(
'
should render the received data
'
,
(
done
)
=>
{
component
.
$el
.
querySelector
(
'
button
'
).
click
();
beforeEach
(()
=>
{
stage
=
{
stage
:
{
status
:
{
icon
:
'
icon_status_success
'
}
}
};
svgHTML
=
Stage
.
computed
.
svgHTML
.
call
(
stage
);
});
it
(
"
should return the correct icon for the stage's status
"
,
()
=>
{
expect
(
svgHTML
).
toBe
(
SUCCESS_SVG
);
});
setTimeout
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-builds-dropdown-container ul
'
).
textContent
.
trim
(),
).
toEqual
(
'
foo
'
);
done
();
},
0
);
});
});
describe
(
'
when mounted
'
,
()
=>
{
let
StageComponent
;
let
renderedComponent
;
let
stage
;
describe
(
'
when request fails
'
,
()
=>
{
const
interceptor
=
(
request
,
next
)
=>
{
next
(
request
.
respondWith
(
JSON
.
stringify
({}),
{
status
:
500
,
}));
};
beforeEach
(()
=>
{
stage
=
{
status
:
{
icon
:
'
icon_status_success
'
}
};
StageComponent
=
Vue
.
extend
(
Stage
);
renderedComponent
=
new
StageComponent
({
propsData
:
{
stage
,
},
}).
$mount
();
Vue
.
http
.
interceptors
.
push
(
interceptor
);
});
it
(
'
should render the correct status svg
'
,
()
=>
{
const
minifiedComponent
=
minify
(
renderedComponent
.
$el
.
outerHTML
);
const
expectedSVG
=
minify
(
SUCCESS_SVG
);
expect
(
minifiedComponent
).
toContain
(
expectedSVG
);
afterEach
(()
=>
{
Vue
.
http
.
interceptors
=
_
.
without
(
Vue
.
http
.
interceptors
,
interceptor
,
);
});
});
describe
(
'
when request fails
'
,
()
=>
{
it
(
'
closes dropdown
'
,
()
=>
{
spyOn
(
$
,
'
ajax
'
).
and
.
callFake
(
options
=>
options
.
error
());
const
StageComponent
=
Vue
.
extend
(
Stage
);
const
component
=
new
StageComponent
({
propsData
:
{
stage
:
{
status
:
{
icon
:
'
foo
'
}
}
},
}).
$mount
();
it
(
'
should close the dropdown
'
,
()
=>
{
component
.
$el
.
click
();
expect
(
component
.
$el
.
classList
.
contains
(
'
open
'
),
).
toEqual
(
false
);
setTimeout
(()
=>
{
expect
(
component
.
$el
.
classList
.
contains
(
'
open
'
)).
toEqual
(
false
);
},
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