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
4ff2e634
Commit
4ff2e634
authored
May 24, 2018
by
Kushal Pandya
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add license report information to MR widget
parent
682abe40
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
410 additions
and
5 deletions
+410
-5
ee/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
...avascripts/vue_merge_request_widget/mr_widget_options.vue
+57
-0
ee/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
...cripts/vue_merge_request_widget/stores/mr_widget_store.js
+44
-0
ee/app/assets/javascripts/vue_shared/security_reports/components/issues_list.vue
...ts/vue_shared/security_reports/components/issues_list.vue
+6
-1
ee/app/assets/javascripts/vue_shared/security_reports/components/report_issues.vue
.../vue_shared/security_reports/components/report_issues.vue
+18
-1
ee/app/assets/stylesheets/pages/security_reports.scss
ee/app/assets/stylesheets/pages/security_reports.scss
+13
-0
spec/javascripts/vue_mr_widget/ee_mr_widget_options_spec.js
spec/javascripts/vue_mr_widget/ee_mr_widget_options_spec.js
+108
-1
spec/javascripts/vue_mr_widget/mock_data.js
spec/javascripts/vue_mr_widget/mock_data.js
+142
-2
spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
.../javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
+22
-0
No files found.
ee/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
View file @
4ff2e634
...
...
@@ -20,8 +20,10 @@ export default {
return
{
isLoadingCodequality
:
false
,
isLoadingPerformance
:
false
,
isLoadingLicenseReport
:
false
,
loadingCodequalityFailed
:
false
,
loadingPerformanceFailed
:
false
,
loadingLicenseReportFailed
:
false
,
};
},
computed
:
{
...
...
@@ -32,6 +34,10 @@ export default {
const
{
codeclimate
}
=
this
.
mr
;
return
codeclimate
&&
codeclimate
.
head_path
&&
codeclimate
.
base_path
;
},
shouldRenderLicenseReport
()
{
const
{
licenseManagement
}
=
this
.
mr
;
return
licenseManagement
&&
licenseManagement
.
head_path
&&
licenseManagement
.
base_path
;
},
hasCodequalityIssues
()
{
return
(
this
.
mr
.
codeclimateMetrics
&&
...
...
@@ -49,6 +55,10 @@ export default {
(
this
.
mr
.
performanceMetrics
.
neutral
&&
this
.
mr
.
performanceMetrics
.
neutral
.
length
>
0
))
);
},
hasLicenseReportIssues
()
{
const
{
licenseReport
}
=
this
.
mr
;
return
licenseReport
&&
licenseReport
.
length
>
0
;
},
shouldRenderPerformance
()
{
const
{
performance
}
=
this
.
mr
;
return
performance
&&
performance
.
head_path
&&
performance
.
base_path
;
...
...
@@ -111,6 +121,18 @@ export default {
return
text
.
join
(
''
);
},
licenseReportText
()
{
const
{
licenseReport
}
=
this
.
mr
;
if
(
licenseReport
.
length
>
0
)
{
return
sprintf
(
s__
(
'
ciReport|License management detected %{licenseInfo}
'
),
{
licenseInfo
:
n__
(
'
%d new license
'
,
'
%d new licenses
'
,
licenseReport
.
length
),
});
}
return
s__
(
'
ciReport|License management detected no new licenses
'
);
},
codequalityStatus
()
{
return
this
.
checkReportStatus
(
this
.
isLoadingCodequality
,
this
.
loadingCodequalityFailed
);
},
...
...
@@ -118,6 +140,10 @@ export default {
performanceStatus
()
{
return
this
.
checkReportStatus
(
this
.
isLoadingPerformance
,
this
.
loadingPerformanceFailed
);
},
licenseReportStatus
()
{
return
this
.
checkReportStatus
(
this
.
isLoadingLicenseReport
,
this
.
loadingLicenseReportFailed
);
},
},
created
()
{
if
(
this
.
shouldRenderCodeQuality
)
{
...
...
@@ -127,6 +153,10 @@ export default {
if
(
this
.
shouldRenderPerformance
)
{
this
.
fetchPerformance
();
}
if
(
this
.
shouldRenderLicenseReport
)
{
this
.
fetchLicenseReport
();
}
},
methods
:
{
fetchCodeQuality
()
{
...
...
@@ -166,6 +196,22 @@ export default {
});
},
fetchLicenseReport
()
{
const
{
head_path
,
base_path
}
=
this
.
mr
.
licenseManagement
;
this
.
isLoadingLicenseReport
=
true
;
Promise
.
all
([
this
.
service
.
fetchReport
(
head_path
),
this
.
service
.
fetchReport
(
base_path
)])
.
then
(
values
=>
{
this
.
mr
.
parseLicenseReportMetrics
(
values
[
0
],
values
[
1
]);
this
.
isLoadingLicenseReport
=
false
;
})
.
catch
(()
=>
{
this
.
isLoadingLicenseReport
=
false
;
this
.
loadingLicenseReportFailed
=
true
;
});
},
translateText
(
type
)
{
return
{
error
:
sprintf
(
s__
(
'
ciReport|Failed to load %{reportName} report
'
),
{
...
...
@@ -243,6 +289,17 @@ export default {
:vulnerability-feedback-help-path=
"mr.vulnerabilityFeedbackHelpPath"
:pipeline-id=
"mr.securityReportsPipelineId"
/>
<report-section
class=
"js-license-report-widget mr-widget-border-top"
v-if=
"shouldRenderLicenseReport"
type=
"license"
:status=
"licenseReportStatus"
:loading-text=
"translateText('license management').loading"
:error-text=
"translateText('license management').error"
:success-text=
"licenseReportText"
:unresolved-issues=
"mr.licenseReport"
:has-issues=
"hasLicenseReportIssues"
/>
<div
class=
"mr-widget-section"
>
<component
:is=
"componentName"
...
...
ee/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
View file @
4ff2e634
...
...
@@ -22,6 +22,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this
.
initCodeclimate
(
data
);
this
.
initPerformanceReport
(
data
);
this
.
initLicenseReport
(
data
);
}
setData
(
data
)
{
...
...
@@ -67,6 +68,11 @@ export default class MergeRequestStore extends CEMergeRequestStore {
};
}
initLicenseReport
(
data
)
{
this
.
licenseManagement
=
data
.
license_management
;
this
.
licenseReport
=
[];
}
compareCodeclimateMetrics
(
headIssues
,
baseIssues
,
headBlobPath
,
baseBlobPath
)
{
const
parsedHeadIssues
=
MergeRequestStore
.
parseCodeclimateMetrics
(
headIssues
,
headBlobPath
);
const
parsedBaseIssues
=
MergeRequestStore
.
parseCodeclimateMetrics
(
baseIssues
,
baseBlobPath
);
...
...
@@ -127,6 +133,44 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this
.
performanceMetrics
=
{
improved
,
degraded
,
neutral
};
}
parseLicenseReportMetrics
(
headMetrics
,
baseMetrics
)
{
const
headLicenses
=
headMetrics
.
licenses
;
const
headDependencies
=
headMetrics
.
dependencies
;
const
baseLicenses
=
baseMetrics
.
licenses
;
if
(
headLicenses
.
length
>
0
&&
headDependencies
.
length
>
0
)
{
const
report
=
{};
const
knownLicenses
=
baseLicenses
.
map
(
license
=>
license
.
name
);
const
newLicenses
=
[];
headLicenses
.
forEach
(
license
=>
{
if
(
knownLicenses
.
indexOf
(
license
.
name
)
===
-
1
)
{
report
[
license
.
name
]
=
{
name
:
license
.
name
,
count
:
license
.
count
,
url
:
''
,
packages
:
[],
};
newLicenses
.
push
(
license
.
name
);
}
});
headDependencies
.
forEach
(
dependencyItem
=>
{
const
licenseName
=
dependencyItem
.
license
.
name
;
if
(
newLicenses
.
indexOf
(
licenseName
)
>
-
1
)
{
if
(
!
report
[
licenseName
].
url
)
{
report
[
licenseName
].
url
=
dependencyItem
.
license
.
url
;
}
report
[
licenseName
].
packages
.
push
(
dependencyItem
.
dependency
);
}
});
this
.
licenseReport
=
newLicenses
.
map
(
licenseName
=>
report
[
licenseName
]);
}
}
// normalize performance metrics by indexing on performance subject and metric name
static
normalizePerformanceMetrics
(
performanceData
)
{
const
indexedSubjects
=
{};
...
...
ee/app/assets/javascripts/vue_shared/security_reports/components/issues_list.vue
View file @
4ff2e634
...
...
@@ -44,6 +44,11 @@ export default {
isFullReportVisible
:
false
,
};
},
computed
:
{
unresolvedIssuesStatus
()
{
return
this
.
type
===
'
license
'
?
'
neutral
'
:
'
failed
'
;
},
},
methods
:
{
openFullReport
()
{
this
.
isFullReportVisible
=
true
;
...
...
@@ -59,7 +64,7 @@ export default {
class=
"js-mr-code-new-issues"
v-if=
"unresolvedIssues.length"
:type=
"type"
status=
"failed
"
:status=
"unresolvedIssuesStatus
"
:issues=
"unresolvedIssues"
/>
...
...
ee/app/assets/javascripts/vue_shared/security_reports/components/report_issues.vue
View file @
4ff2e634
...
...
@@ -2,6 +2,7 @@
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
PerformanceIssue
from
'
ee/vue_merge_request_widget/components/performance_issue_body.vue
'
;
import
CodequalityIssue
from
'
ee/vue_merge_request_widget/components/codequality_issue_body.vue
'
;
import
LicenseIssue
from
'
ee/vue_merge_request_widget/components/license_issue_body.vue
'
;
import
SastIssue
from
'
./sast_issue_body.vue
'
;
import
SastContainerIssue
from
'
./sast_container_issue_body.vue
'
;
import
DastIssue
from
'
./dast_issue_body.vue
'
;
...
...
@@ -17,13 +18,14 @@ export default {
DastIssue
,
PerformanceIssue
,
CodequalityIssue
,
LicenseIssue
,
},
props
:
{
issues
:
{
type
:
Array
,
required
:
true
,
},
// security || codequality || performance || docker || dast
// security || codequality || performance || docker || dast
|| license
type
:
{
type
:
String
,
required
:
true
,
...
...
@@ -59,6 +61,9 @@ export default {
isTypePerformance
()
{
return
this
.
type
===
'
performance
'
;
},
isTypeLicense
()
{
return
this
.
type
===
'
license
'
;
},
isTypeSast
()
{
return
this
.
type
===
SAST
;
},
...
...
@@ -89,6 +94,13 @@ export default {
}"
>
<icon
v-if=
"isTypeLicense"
name=
"status_created_borderless"
css-classes=
"prepend-left-4"
:size=
"24"
/>
<icon
v-else
:name=
"iconName"
:size=
"32"
/>
...
...
@@ -120,6 +132,11 @@ export default {
v-else-if=
"isTypePerformance"
:issue=
"issue"
/>
<license-issue
v-else-if=
"isTypeLicense"
:issue=
"issue"
/>
</li>
</ul>
</div>
...
...
ee/app/assets/stylesheets/pages/security_reports.scss
View file @
4ff2e634
...
...
@@ -56,6 +56,19 @@
list-style
:
none
;
padding
:
0
1px
;
margin
:
0
;
.license-item
{
line-height
:
$gl-padding-24
;
.license-dependencies
{
color
:
$gl-text-color-tertiary
;
}
.btn-show-all-packages
{
line-height
:
$gl-btn-line-height
;
margin-bottom
:
2px
;
}
}
}
.report-block-list-icon
{
...
...
spec/javascripts/vue_mr_widget/ee_mr_widget_options_spec.js
View file @
4ff2e634
...
...
@@ -6,7 +6,14 @@ import MRWidgetService from 'ee/vue_merge_request_widget/services/mr_widget_serv
import
MRWidgetStore
from
'
ee/vue_merge_request_widget/stores/mr_widget_store
'
;
import
mountComponent
from
'
spec/helpers/vue_mount_component_helper
'
;
import
state
from
'
ee/vue_shared/security_reports/store/state
'
;
import
mockData
,
{
baseIssues
,
headIssues
,
basePerformance
,
headPerformance
}
from
'
./mock_data
'
;
import
mockData
,
{
baseIssues
,
headIssues
,
basePerformance
,
headPerformance
,
licenseBaseIssues
,
licenseHeadIssues
,
}
from
'
./mock_data
'
;
import
{
sastIssues
,
...
...
@@ -654,6 +661,106 @@ describe('ee merge request widget options', () => {
});
});
describe
(
'
license management report
'
,
()
=>
{
beforeEach
(()
=>
{
gl
.
mrWidgetData
=
{
...
mockData
,
license_management
:
{
head_path
:
'
head.json
'
,
base_path
:
'
base.json
'
,
},
};
Component
.
mr
=
new
MRWidgetStore
(
gl
.
mrWidgetData
);
Component
.
service
=
new
MRWidgetService
({});
});
describe
(
'
when it is loading
'
,
()
=>
{
it
(
'
should render loading indicator
'
,
()
=>
{
mock
.
onGet
(
'
head.json
'
).
reply
(
200
,
licenseHeadIssues
);
mock
.
onGet
(
'
base.json
'
).
reply
(
200
,
licenseBaseIssues
);
vm
=
mountComponent
(
Component
);
expect
(
removeBreakLine
(
vm
.
$el
.
querySelector
(
'
.js-license-report-widget
'
).
textContent
),
).
toContain
(
'
Loading license management report
'
);
});
});
describe
(
'
with successful request
'
,
()
=>
{
beforeEach
(()
=>
{
mock
.
onGet
(
'
head.json
'
).
reply
(
200
,
licenseHeadIssues
);
mock
.
onGet
(
'
base.json
'
).
reply
(
200
,
licenseBaseIssues
);
vm
=
mountComponent
(
Component
);
});
it
(
'
should render report overview
'
,
done
=>
{
setTimeout
(()
=>
{
expect
(
removeBreakLine
(
vm
.
$el
.
querySelector
(
'
.js-license-report-widget .js-code-text
'
).
textContent
,
),
).
toEqual
(
'
License management detected 1 new license
'
);
done
();
},
0
);
});
it
(
'
should render report issues list in section body
'
,
done
=>
{
setTimeout
(()
=>
{
const
sectionBodyEl
=
vm
.
$el
.
querySelector
(
'
.js-license-report-widget .js-report-section-container
'
,
);
expect
(
sectionBodyEl
).
not
.
toBeNull
();
expect
(
sectionBodyEl
.
querySelectorAll
(
'
li.report-block-list-issue
'
).
length
).
toBe
(
licenseHeadIssues
.
licenses
.
length
-
1
,
);
done
();
},
0
);
});
});
describe
(
'
with empty successful request
'
,
()
=>
{
beforeEach
(()
=>
{
mock
.
onGet
(
'
head.json
'
).
reply
(
200
,
licenseBaseIssues
);
mock
.
onGet
(
'
base.json
'
).
reply
(
200
,
licenseBaseIssues
);
vm
=
mountComponent
(
Component
);
});
afterEach
(()
=>
{
mock
.
restore
();
});
it
(
'
should render report overview
'
,
done
=>
{
setTimeout
(()
=>
{
expect
(
removeBreakLine
(
vm
.
$el
.
querySelector
(
'
.js-license-report-widget .js-code-text
'
).
textContent
,
),
).
toEqual
(
'
License management detected no new licenses
'
);
done
();
},
0
);
});
});
describe
(
'
with failed request
'
,
()
=>
{
beforeEach
(()
=>
{
mock
.
onGet
(
'
head.json
'
).
reply
(
500
,
{});
mock
.
onGet
(
'
base.json
'
).
reply
(
500
,
{});
vm
=
mountComponent
(
Component
);
});
it
(
'
should render error indicator
'
,
done
=>
{
setTimeout
(()
=>
{
expect
(
removeBreakLine
(
vm
.
$el
.
querySelector
(
'
.js-license-report-widget .js-code-text
'
).
textContent
,
),
).
toContain
(
'
Failed to load license management report
'
);
done
();
},
0
);
});
});
});
describe
(
'
computed
'
,
()
=>
{
describe
(
'
shouldRenderApprovals
'
,
()
=>
{
it
(
'
should return false when no approvals
'
,
()
=>
{
...
...
spec/javascripts/vue_mr_widget/mock_data.js
View file @
4ff2e634
...
...
@@ -224,8 +224,10 @@ export default {
base_path
:
'
blob_path
'
,
head_path
:
'
blob_path
'
,
},
vulnerability_feedback_help_path
:
'
/help/user/project/merge_requests/index#interacting-with-security-reports-ultimate
'
,
merge_commit_path
:
'
http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775
'
,
vulnerability_feedback_help_path
:
'
/help/user/project/merge_requests/index#interacting-with-security-reports-ultimate
'
,
merge_commit_path
:
'
http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775
'
,
};
// Codeclimate
export
const
headIssues
=
[
...
...
@@ -396,3 +398,141 @@ export const codequalityParsedIssues = [
urlPath
:
'
foo/Gemfile.lock
'
,
},
];
export
const
licenseBaseIssues
=
{
licenses
:
[
{
count
:
1
,
name
:
'
MIT
'
,
},
],
dependencies
:
[
{
license
:
{
name
:
'
MIT
'
,
url
:
'
http://opensource.org/licenses/mit-license
'
,
},
dependency
:
{
name
:
'
bundler
'
,
url
:
'
http://bundler.io
'
,
description
:
'
The best way to manage your application
\'
s dependencies
'
,
pathes
:
[
'
.
'
,
],
},
},
],
};
export
const
licenseHeadIssues
=
{
licenses
:
[
{
count
:
3
,
name
:
'
New BSD
'
,
},
{
count
:
1
,
name
:
'
MIT
'
,
},
],
dependencies
:
[
{
license
:
{
name
:
'
New BSD
'
,
url
:
'
http://opensource.org/licenses/BSD-3-Clause
'
,
},
dependency
:
{
name
:
'
pg
'
,
url
:
'
https://bitbucket.org/ged/ruby-pg
'
,
description
:
'
Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]
'
,
pathes
:
[
'
.
'
],
},
},
{
license
:
{
name
:
'
New BSD
'
,
url
:
'
http://opensource.org/licenses/BSD-3-Clause
'
,
},
dependency
:
{
name
:
'
puma
'
,
url
:
'
http://puma.io
'
,
description
:
'
Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications
'
,
pathes
:
[
'
.
'
],
},
},
{
license
:
{
name
:
'
New BSD
'
,
url
:
'
http://opensource.org/licenses/BSD-3-Clause
'
,
},
dependency
:
{
name
:
'
foo
'
,
url
:
'
http://foo.io
'
,
description
:
'
Foo is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications
'
,
pathes
:
[
'
.
'
],
},
},
{
license
:
{
name
:
'
MIT
'
,
url
:
'
http://opensource.org/licenses/mit-license
'
,
},
dependency
:
{
name
:
'
execjs
'
,
url
:
'
https://github.com/rails/execjs
'
,
description
:
'
Run JavaScript code from Ruby
'
,
pathes
:
[
'
.
'
,
],
},
},
],
};
export
const
licenseReport
=
[
{
name
:
'
New BSD
'
,
count
:
5
,
url
:
'
http://opensource.org/licenses/BSD-3-Clause
'
,
packages
:
[
{
name
:
'
pg
'
,
url
:
'
https://bitbucket.org/ged/ruby-pg
'
,
description
:
'
Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]
'
,
pathes
:
[
'
.
'
],
},
{
name
:
'
puma
'
,
url
:
'
http://puma.io
'
,
description
:
'
Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications
'
,
pathes
:
[
'
.
'
],
},
{
name
:
'
foo
'
,
url
:
'
https://bitbucket.org/ged/ruby-pg
'
,
description
:
'
Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]
'
,
pathes
:
[
'
.
'
],
},
{
name
:
'
bar
'
,
url
:
'
http://puma.io
'
,
description
:
'
Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications
'
,
pathes
:
[
'
.
'
],
},
{
name
:
'
baz
'
,
url
:
'
https://bitbucket.org/ged/ruby-pg
'
,
description
:
'
Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]
'
,
pathes
:
[
'
.
'
],
},
],
},
];
spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
View file @
4ff2e634
...
...
@@ -5,6 +5,8 @@ import mockData, {
baseIssues
,
parsedBaseIssues
,
parsedHeadIssues
,
licenseBaseIssues
,
licenseHeadIssues
,
}
from
'
../mock_data
'
;
describe
(
'
MergeRequestStore
'
,
()
=>
{
...
...
@@ -95,6 +97,26 @@ describe('MergeRequestStore', () => {
});
});
describe
(
'
parseLicenseReportMetrics
'
,
()
=>
{
it
(
'
should parse the received issues
'
,
()
=>
{
store
.
parseLicenseReportMetrics
(
licenseHeadIssues
,
licenseBaseIssues
);
expect
(
store
.
licenseReport
[
0
].
name
).
toBe
(
licenseHeadIssues
.
licenses
[
0
].
name
);
expect
(
store
.
licenseReport
[
0
].
url
).
toBe
(
licenseHeadIssues
.
dependencies
[
0
].
license
.
url
);
});
it
(
'
should ommit issues from base report
'
,
()
=>
{
const
knownLicenseName
=
licenseBaseIssues
.
licenses
[
0
].
name
;
store
.
parseLicenseReportMetrics
(
licenseHeadIssues
,
licenseBaseIssues
);
expect
(
store
.
licenseReport
.
length
).
toBe
(
licenseHeadIssues
.
licenses
.
length
-
1
);
expect
(
store
.
licenseReport
[
0
].
packages
.
length
).
toBe
(
licenseHeadIssues
.
dependencies
.
length
-
1
,
);
store
.
licenseReport
.
forEach
(
license
=>
{
expect
(
license
.
name
).
not
.
toBe
(
knownLicenseName
);
});
});
});
describe
(
'
isNothingToMergeState
'
,
()
=>
{
it
(
'
returns true when nothingToMerge
'
,
()
=>
{
store
.
state
=
stateKey
.
nothingToMerge
;
...
...
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