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
ccb1ff62
Commit
ccb1ff62
authored
Mar 22, 2021
by
Daniel Tian
Committed by
Jose Ivan Vargas
Mar 22, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Show loading spinner for security dashboard charts
parent
c28c03e0
Changes
5
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
135 additions
and
81 deletions
+135
-81
ee/app/assets/javascripts/security_dashboard/components/first_class_vulnerability_chart.vue
..._dashboard/components/first_class_vulnerability_chart.vue
+44
-15
ee/app/assets/javascripts/security_dashboard/components/first_class_vulnerability_severities.vue
...board/components/first_class_vulnerability_severities.vue
+13
-5
ee/changelogs/unreleased/284471-show-loading-spinner-for-security-dashboard.yml
...ed/284471-show-loading-spinner-for-security-dashboard.yml
+5
-0
ee/spec/frontend/security_dashboard/components/first_class_vulnerability_chart_spec.js
...hboard/components/first_class_vulnerability_chart_spec.js
+45
-28
ee/spec/frontend/security_dashboard/components/first_class_vulnerability_severities_spec.js
...d/components/first_class_vulnerability_severities_spec.js
+28
-33
No files found.
ee/app/assets/javascripts/security_dashboard/components/first_class_vulnerability_chart.vue
View file @
ccb1ff62
<
script
>
import
{
GlTooltipDirective
,
GlTable
}
from
'
@gitlab/ui
'
;
import
{
GlTooltipDirective
,
GlTable
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
GlSparklineChart
}
from
'
@gitlab/ui/dist/charts
'
;
import
SeverityBadge
from
'
ee/vue_shared/security_reports/components/severity_badge.vue
'
;
import
{
firstAndLastY
}
from
'
~/lib/utils/chart_utils
'
;
...
...
@@ -14,12 +14,17 @@ import { SEVERITY_LEVELS, DAYS } from '../store/constants';
import
ChartButtons
from
'
./vulnerability_chart_buttons.vue
'
;
const
ISO_DATE
=
'
isoDate
'
;
const
TH_CLASS
=
'
gl-bg-white!
'
;
const
TD_CLASS
=
'
gl-border-none!
'
;
const
TH_CLASS_TEXT_RIGHT
=
`
${
TH_CLASS
}
gl-text-right`
;
const
TD_CLASS_TEXT_RIGHT
=
`
${
TD_CLASS
}
gl-text-right`
;
export
default
{
components
:
{
ChartButtons
,
GlSparklineChart
,
GlTable
,
GlLoadingIcon
,
SeverityBadge
,
},
directives
:
{
...
...
@@ -38,14 +43,29 @@ export default {
},
days
:
Object
.
values
(
DAYS
),
fields
:
[
{
key
:
'
severityLevel
'
,
label
:
s__
(
'
VulnerabilityChart|Severity
'
),
tdClass
:
'
border-0
'
},
{
key
:
'
chartData
'
,
label
:
''
,
tdClass
:
'
border-0 w-100
'
},
{
key
:
'
changeInPercent
'
,
label
:
'
%
'
,
thClass
:
'
text-right
'
,
tdClass
:
'
border-0 text-right
'
},
{
key
:
'
severityLevel
'
,
label
:
s__
(
'
VulnerabilityChart|Severity
'
),
thClass
:
TH_CLASS
,
tdClass
:
TD_CLASS
,
},
{
key
:
'
chartData
'
,
label
:
''
,
thClass
:
TH_CLASS
,
tdClass
:
`
${
TD_CLASS
}
gl-w-full`
,
},
{
key
:
'
changeInPercent
'
,
label
:
'
%
'
,
thClass
:
TH_CLASS_TEXT_RIGHT
,
tdClass
:
TD_CLASS_TEXT_RIGHT
,
},
{
key
:
'
currentVulnerabilitiesCount
'
,
label
:
'
#
'
,
thClass
:
'
text-right
'
,
tdClass
:
'
border-0 text-right
'
,
thClass
:
TH_CLASS_TEXT_RIGHT
,
tdClass
:
TD_CLASS_TEXT_RIGHT
,
},
],
severityLevels
:
[
...
...
@@ -114,6 +134,9 @@ export default {
[
formattedEndDate
,
0
],
];
},
isLoadingHistory
()
{
return
this
.
$apollo
.
queries
.
vulnerabilitiesHistory
.
loading
;
},
},
watch
:
{
startDate
()
{
...
...
@@ -162,29 +185,35 @@ export default {
</
script
>
<
template
>
<section
class=
"
border rounded p-
0"
>
<div
class=
"
p-3
"
>
<section
class=
"
gl-border-solid gl-rounded-base gl-border-1 gl-border-gray-10
0"
>
<div
class=
"
gl-p-5
"
>
<header
id=
"vulnerability-chart-header"
>
<h4
class=
"my-0"
>
<h4
class=
"
gl-
my-0"
>
{{
__
(
'
Vulnerabilities over time
'
)
}}
</h4>
<p
ref=
"timeInfo"
class=
"text-secondary mt-0 js-vulnerabilities-chart-time-info"
>
<p
v-if=
"!isLoadingHistory"
data-testid=
"timeInfo"
class=
"gl-text-gray-500 js-vulnerabilities-chart-time-info"
>
{{
dateInfo
}}
</p>
</header>
<chart-buttons
v-if=
"!isLoadingHistory"
:days=
"$options.days"
:active-day=
"vulnerabilitiesHistoryDayRange"
@
click=
"setVulnerabilitiesHistoryDayRange"
/>
</div>
<gl-loading-icon
v-if=
"isLoadingHistory"
size=
"lg"
class=
"gl-my-12"
/>
<gl-table
v-else
:fields=
"$options.fields"
:items=
"charts"
:borderless=
"true"
thead-class=
"thead-white"
class=
"js-vulnerabilities-chart-severity-level-breakdown mb-2"
borderless
class=
"js-vulnerabilities-chart-severity-level-breakdown gl-mb-3"
>
<template
#head(changeInPercent)=
"
{ label }">
<span
v-gl-tooltip
:title=
"__('Difference between start date and now')"
>
{{
label
}}
</span>
...
...
@@ -198,14 +227,14 @@ export default {
<severity-badge
:ref=
"`severityBadge$
{value}`" :severity="value" />
</
template
>
<
template
#cell(chartData)=
"{ item }"
>
<div
class=
"
position-relative h-32-px
"
>
<div
class=
"
gl-relative gl-p-5
"
>
<gl-sparkline-chart
:ref=
"`sparklineChart$
{item.severityLevel}`"
:height="32"
:data="item.chartData"
:tooltip-label="__('Vulnerabilities')"
:show-last-y-value="false"
class="
position-absolute w-100 position-top-0 position
-left-0"
class="
gl-absolute gl-w-full gl-top-0 gl
-left-0"
/>
</div>
</
template
>
...
...
ee/app/assets/javascripts/security_dashboard/components/first_class_vulnerability_severities.vue
View file @
ccb1ff62
<
script
>
import
{
GlLink
,
GlTooltipDirective
,
GlIcon
}
from
'
@gitlab/ui
'
;
import
{
GlLink
,
GlTooltipDirective
,
GlIcon
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
severityGroupTypes
,
severityLevels
,
...
...
@@ -28,7 +28,7 @@ export default {
},
},
accordionItemsContentMaxHeight
:
'
445px
'
,
components
:
{
Accordion
,
AccordionItem
,
GlLink
,
GlIcon
},
components
:
{
Accordion
,
AccordionItem
,
GlLink
,
GlIcon
,
GlLoadingIcon
},
directives
:
{
'
gl-tooltip
'
:
GlTooltipDirective
,
},
...
...
@@ -69,6 +69,9 @@ export default {
},
},
computed
:
{
isLoadingGrades
()
{
return
this
.
$apollo
.
queries
.
vulnerabilityGrades
.
loading
;
},
severityGroups
()
{
return
SEVERITY_GROUPS
.
map
((
group
)
=>
({
...
group
,
...
...
@@ -137,7 +140,7 @@ export default {
<section
class=
"gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base gl-display-flex gl-flex-direction-column"
>
<header
class=
"gl-
border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-
p-5"
>
<header
class=
"gl-p-5"
>
<h4
class=
"gl-my-0"
>
{{
__
(
'
Project security status
'
)
}}
<gl-link
...
...
@@ -148,11 +151,16 @@ export default {
><gl-icon
name=
"question"
/></gl-link>
</h4>
<p
class=
"gl-text-gray-500 gl-m-0"
>
<p
v-if=
"!isLoadingGrades"
class=
"gl-text-gray-500 gl-m-0"
>
{{
__
(
'
Projects are graded based on the highest severity vulnerability present
'
)
}}
</p>
</header>
<accordion
class=
"security-dashboard-accordion gl-px-5 gl-display-flex gl-flex-fill-1"
>
<gl-loading-icon
v-if=
"isLoadingGrades"
size=
"lg"
class=
"gl-my-12"
/>
<accordion
v-else
class=
"security-dashboard-accordion gl-px-5 gl-display-flex gl-flex-fill-1 gl-border-t-1 gl-border-t-solid gl-border-t-gray-100"
>
<template
#default
="
{ accordionId }">
<accordion-item
v-for=
"severityGroup in severityGroups"
...
...
ee/changelogs/unreleased/284471-show-loading-spinner-for-security-dashboard.yml
0 → 100644
View file @
ccb1ff62
---
title
:
Show loading spinner for security dashboard
merge_request
:
56493
author
:
type
:
fixed
ee/spec/frontend/security_dashboard/components/first_class_vulnerability_chart_spec.js
View file @
ccb1ff62
import
{
GlLoadingIcon
,
GlTable
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
VulnerabilityChart
from
'
ee/security_dashboard/components/first_class_vulnerability_chart
'
;
import
ChartButtons
from
'
ee/security_dashboard/components/vulnerability_chart_buttons.vue
'
;
import
stubChildren
from
'
helpers/stub_children
'
;
import
{
extendedWrapper
}
from
'
helpers/vue_test_utils_helper
'
;
describe
(
'
First class vulnerability chart component
'
,
()
=>
{
let
wrapper
;
...
...
@@ -16,29 +17,33 @@ describe('First class vulnerability chart component', () => {
},
};
const
findTimeInfo
=
()
=>
wrapper
.
find
({
ref
:
'
timeInfo
'
}
);
const
findChartButtons
=
()
=>
wrapper
.
find
(
ChartButtons
);
const
find
Active
ChartButton
=
()
=>
findChartButtons
().
find
(
'
.selected
'
);
const
findTimeInfo
=
()
=>
wrapper
.
find
ByTestId
(
'
timeInfo
'
);
const
findChartButtons
=
()
=>
wrapper
.
find
Component
(
ChartButtons
);
const
find
Selected
ChartButton
=
()
=>
findChartButtons
().
find
(
'
.selected
'
);
const
find90DaysChartButton
=
()
=>
findChartButtons
().
find
(
'
[data-days="90"]
'
);
const
mockApollo
=
(
options
)
=>
{
return
{
queries
:
{
vulnerabilitiesHistory
:
{
...
options
},
},
};
};
const
createComponent
=
({
$apollo
,
propsData
,
stubs
,
data
,
provide
}
=
{})
=>
{
const
instance
=
shallowMount
(
VulnerabilityChart
,
{
$apollo
,
return
extendedWrapper
(
shallowMount
(
VulnerabilityChart
,
{
propsData
:
{
query
:
{},
...
propsData
},
provide
:
{
groupFullPath
:
undefined
,
...
provide
},
stubs
:
{
...
stubChildren
(
VulnerabilityChart
),
...
stubs
,
},
mocks
:
{
$apollo
:
mockApollo
(
$apollo
)
},
stubs
,
data
,
});
instance
.
vm
.
$apollo
=
{
queries
:
{
vulnerabilitiesHistory
:
{
refetch
:
jest
.
fn
()
}
}
};
return
instance
;
}),
);
};
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
describe
(
'
header
'
,
()
=>
{
...
...
@@ -63,7 +68,10 @@ describe('First class vulnerability chart component', () => {
describe
(
'
date range selectors
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
=
createComponent
({
stubs
:
{
ChartButtons
}
});
wrapper
=
createComponent
({
stubs
:
{
ChartButtons
},
$apollo
:
{
refetch
:
jest
.
fn
()
},
});
});
it
(
'
should contain the chart buttons
'
,
()
=>
{
...
...
@@ -73,10 +81,10 @@ describe('First class vulnerability chart component', () => {
});
it
(
'
should change the actively selected chart button and refetch the new data
'
,
()
=>
{
expect
(
find
Active
ChartButton
().
text
()).
toContain
(
'
30 Days
'
);
expect
(
find
Selected
ChartButton
().
text
()).
toContain
(
'
30 Days
'
);
find90DaysChartButton
().
vm
.
$emit
(
'
click
'
);
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
find
Active
ChartButton
().
text
()).
toContain
(
'
90 Days
'
);
expect
(
find
Selected
ChartButton
().
text
()).
toContain
(
'
90 Days
'
);
expect
(
wrapper
.
vm
.
$apollo
.
queries
.
vulnerabilitiesHistory
.
refetch
).
toHaveBeenCalledTimes
(
1
);
});
});
...
...
@@ -86,9 +94,6 @@ describe('First class vulnerability chart component', () => {
beforeEach
(()
=>
{
wrapper
=
createComponent
({
provide
:
{
groupFullPath
:
'
gitlab-org
'
},
$apollo
:
{
queries
:
{
vulnerabilitiesHistory
:
{
group
:
responseData
}
},
},
});
});
...
...
@@ -102,11 +107,7 @@ describe('First class vulnerability chart component', () => {
describe
(
'
when loading the history chart for instance level dashboard
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
=
createComponent
({
$apollo
:
{
queries
:
{
vulnerabilitiesHistory
:
responseData
},
},
});
wrapper
=
createComponent
();
});
it
(
'
should process the data returned from GraphQL properly
'
,
()
=>
{
...
...
@@ -116,4 +117,20 @@ describe('First class vulnerability chart component', () => {
});
});
});
describe
(
'
when query is loading
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
=
createComponent
({
$apollo
:
{
loading
:
true
},
});
});
it
(
'
only shows the header and loading icon
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
h4
'
).
exists
()).
toBe
(
true
);
expect
(
wrapper
.
findComponent
(
GlLoadingIcon
).
exists
()).
toBe
(
true
);
expect
(
findTimeInfo
().
exists
()).
toBe
(
false
);
expect
(
findChartButtons
().
exists
()).
toBe
(
false
);
expect
(
wrapper
.
findComponent
(
GlTable
).
exists
()).
toBe
(
false
);
});
});
});
ee/spec/frontend/security_dashboard/components/first_class_vulnerability_severities_spec.js
View file @
ccb1ff62
import
{
GlLink
}
from
'
@gitlab/ui
'
;
import
{
GlLink
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
VulnerabilitySeverity
from
'
ee/security_dashboard/components/first_class_vulnerability_severities.vue
'
;
...
...
@@ -29,25 +29,19 @@ describe('Vulnerability Severity component', () => {
.
findAll
(
'
[data-testid="vulnerability-severity-groups"]
'
)
.
wrappers
.
map
((
item
)
=>
trimText
(
item
.
text
()));
const
createApolloProvider
=
(...
queries
)
=>
{
return
createMockApollo
([...
queries
]);
};
const
createComponent
=
({
propsData
,
data
,
apolloProvider
,
provide
})
=>
{
const
createComponent
=
({
provide
,
query
,
mockData
}
=
{})
=>
{
return
shallowMount
(
VulnerabilitySeverity
,
{
localVue
,
apolloProvider
,
apolloProvider
:
createMockApollo
([[
query
,
jest
.
fn
().
mockResolvedValue
(
mockData
)]])
,
propsData
:
{
query
:
{}
,
query
,
helpPagePath
,
...
propsData
,
},
provide
:
{
groupFullPath
:
undefined
,
...
provide
},
stubs
:
{
Accordion
,
AccordionItem
,
},
data
,
});
};
...
...
@@ -59,20 +53,14 @@ describe('Vulnerability Severity component', () => {
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
describe
(
'
when loading the project severity component for group level dashboard
'
,
()
=>
{
beforeEach
(()
=>
{
const
apolloProvider
=
createApolloProvider
([
groupVulnerabilityGradesQuery
,
jest
.
fn
().
mockResolvedValue
(
mockGroupVulnerabilityGrades
()),
]);
wrapper
=
createComponent
({
propsData
:
{
query
:
groupVulnerabilityGradesQuery
},
provide
:
{
groupFullPath
:
'
gitlab-org
'
},
apolloProvider
,
query
:
groupVulnerabilityGradesQuery
,
mockData
:
mockGroupVulnerabilityGrades
(),
});
return
wrapper
.
vm
.
$nextTick
();
...
...
@@ -91,14 +79,9 @@ describe('Vulnerability Severity component', () => {
describe
(
'
when loading the project severity component for instance level dashboard
'
,
()
=>
{
beforeEach
(()
=>
{
const
apolloProvider
=
createApolloProvider
([
instanceVulnerabilityGradesQuery
,
jest
.
fn
().
mockResolvedValue
(
mockInstanceVulnerabilityGrades
()),
]);
wrapper
=
createComponent
({
propsData
:
{
query
:
instanceVulnerabilityGradesQuery
}
,
apolloProvider
,
query
:
instanceVulnerabilityGradesQuery
,
mockData
:
mockInstanceVulnerabilityGrades
()
,
});
return
wrapper
.
vm
.
$nextTick
();
...
...
@@ -117,7 +100,10 @@ describe('Vulnerability Severity component', () => {
describe
(
'
for all cases
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
=
createComponent
({});
wrapper
=
createComponent
({
query
:
instanceVulnerabilityGradesQuery
,
mockData
:
mockInstanceVulnerabilityGrades
(),
});
});
it
(
'
has the link to the help page
'
,
()
=>
{
...
...
@@ -151,14 +137,9 @@ describe('Vulnerability Severity component', () => {
beforeEach
(
async
()
=>
{
// Here instance or group does not matter. We just need some data to test
// common functionality.
const
apolloProvider
=
createApolloProvider
([
instanceVulnerabilityGradesQuery
,
jest
.
fn
().
mockResolvedValue
(
mockInstanceVulnerabilityGrades
()),
]);
wrapper
=
createComponent
({
propsData
:
{
query
:
instanceVulnerabilityGradesQuery
}
,
apolloProvider
,
query
:
instanceVulnerabilityGradesQuery
,
mockData
:
mockInstanceVulnerabilityGrades
()
,
});
await
wrapper
.
vm
.
$nextTick
();
...
...
@@ -194,4 +175,18 @@ describe('Vulnerability Severity component', () => {
});
},
);
describe
(
'
when query is loading
'
,
()
=>
{
it
(
'
only shows the header and loading icon
'
,
()
=>
{
wrapper
=
createComponent
({
query
:
instanceVulnerabilityGradesQuery
,
mockData
:
mockInstanceVulnerabilityGrades
(),
});
expect
(
wrapper
.
findComponent
(
GlLoadingIcon
).
exists
()).
toBe
(
true
);
expect
(
findHeader
().
exists
()).
toBe
(
true
);
expect
(
findDescription
().
exists
()).
toBe
(
false
);
expect
(
wrapper
.
findComponent
(
Accordion
).
exists
()).
toBe
(
false
);
});
});
});
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